Repository: TES3MP/openmw-tes3mp Branch: 0.8.1 Commit: 49be5b6405d6 Files: 2839 Total size: 14.4 MB Directory structure: gitextract_s3cv9ifw/ ├── .editorconfig ├── .github/ │ └── FUNDING.yml ├── .gitignore ├── .gitlab-ci.yml ├── .gitmodules ├── .readthedocs.yaml ├── .travis.yml ├── AUTHORS.md ├── CHANGELOG.md ├── CHANGELOG_PR.md ├── CI/ │ ├── ActivateMSVC.ps1 │ ├── activate_msvc.sh │ ├── before_install.android.sh │ ├── before_install.linux.sh │ ├── before_install.osx.sh │ ├── before_script.android.sh │ ├── before_script.linux.sh │ ├── before_script.msvc.sh │ ├── before_script.osx.sh │ ├── build.msvc.sh │ ├── build_googletest.sh │ ├── check_package.osx.sh │ ├── check_tabs.sh │ ├── deploy.osx.sh │ └── install_debian_deps.sh ├── CMakeLists.txt ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── apps/ │ ├── benchmarks/ │ │ ├── CMakeLists.txt │ │ └── detournavigator/ │ │ └── navmeshtilescache.cpp │ ├── browser/ │ │ ├── CMakeLists.txt │ │ ├── MainWindow.cpp │ │ ├── MainWindow.hpp │ │ ├── MySortFilterProxyModel.cpp │ │ ├── MySortFilterProxyModel.hpp │ │ ├── PingHelper.cpp │ │ ├── PingHelper.hpp │ │ ├── PingUpdater.cpp │ │ ├── PingUpdater.hpp │ │ ├── QueryHelper.cpp │ │ ├── QueryHelper.hpp │ │ ├── ServerInfoDialog.cpp │ │ ├── ServerInfoDialog.hpp │ │ ├── ServerModel.cpp │ │ ├── ServerModel.hpp │ │ ├── Types.hpp │ │ ├── main.cpp │ │ └── netutils/ │ │ ├── HTTPNetwork.cpp │ │ ├── HTTPNetwork.hpp │ │ ├── QueryClient.cpp │ │ ├── QueryClient.hpp │ │ ├── Utils.cpp │ │ └── Utils.hpp │ ├── bsatool/ │ │ ├── CMakeLists.txt │ │ └── bsatool.cpp │ ├── doc.hpp │ ├── essimporter/ │ │ ├── CMakeLists.txt │ │ ├── convertacdt.cpp │ │ ├── convertacdt.hpp │ │ ├── convertcntc.cpp │ │ ├── convertcntc.hpp │ │ ├── convertcrec.cpp │ │ ├── convertcrec.hpp │ │ ├── converter.cpp │ │ ├── converter.hpp │ │ ├── convertinventory.cpp │ │ ├── convertinventory.hpp │ │ ├── convertnpcc.cpp │ │ ├── convertnpcc.hpp │ │ ├── convertplayer.cpp │ │ ├── convertplayer.hpp │ │ ├── convertscpt.cpp │ │ ├── convertscpt.hpp │ │ ├── convertscri.cpp │ │ ├── convertscri.hpp │ │ ├── importacdt.cpp │ │ ├── importacdt.hpp │ │ ├── importcellref.cpp │ │ ├── importcellref.hpp │ │ ├── importcntc.cpp │ │ ├── importcntc.hpp │ │ ├── importcrec.cpp │ │ ├── importcrec.hpp │ │ ├── importdial.cpp │ │ ├── importdial.hpp │ │ ├── importer.cpp │ │ ├── importer.hpp │ │ ├── importercontext.cpp │ │ ├── importercontext.hpp │ │ ├── importgame.cpp │ │ ├── importgame.hpp │ │ ├── importinfo.cpp │ │ ├── importinfo.hpp │ │ ├── importinventory.cpp │ │ ├── importinventory.hpp │ │ ├── importjour.cpp │ │ ├── importjour.hpp │ │ ├── importklst.cpp │ │ ├── importklst.hpp │ │ ├── importnpcc.cpp │ │ ├── importnpcc.hpp │ │ ├── importplayer.cpp │ │ ├── importplayer.hpp │ │ ├── importproj.cpp │ │ ├── importproj.h │ │ ├── importques.cpp │ │ ├── importques.hpp │ │ ├── importscpt.cpp │ │ ├── importscpt.hpp │ │ ├── importscri.cpp │ │ ├── importscri.hpp │ │ ├── importsplm.cpp │ │ ├── importsplm.h │ │ └── main.cpp │ ├── launcher/ │ │ ├── CMakeLists.txt │ │ ├── advancedpage.cpp │ │ ├── advancedpage.hpp │ │ ├── datafilespage.cpp │ │ ├── datafilespage.hpp │ │ ├── graphicspage.cpp │ │ ├── graphicspage.hpp │ │ ├── main.cpp │ │ ├── maindialog.cpp │ │ ├── maindialog.hpp │ │ ├── playpage.cpp │ │ ├── playpage.hpp │ │ ├── sdlinit.cpp │ │ ├── sdlinit.hpp │ │ ├── settingspage.cpp │ │ ├── settingspage.hpp │ │ ├── textslotmsgbox.cpp │ │ ├── textslotmsgbox.hpp │ │ └── utils/ │ │ ├── cellnameloader.cpp │ │ ├── cellnameloader.hpp │ │ ├── lineedit.cpp │ │ ├── lineedit.hpp │ │ ├── openalutil.cpp │ │ ├── openalutil.hpp │ │ ├── profilescombobox.cpp │ │ ├── profilescombobox.hpp │ │ ├── textinputdialog.cpp │ │ └── textinputdialog.hpp │ ├── master/ │ │ ├── CMakeLists.txt │ │ ├── MasterServer.cpp │ │ ├── MasterServer.hpp │ │ ├── RestServer.cpp │ │ ├── RestServer.hpp │ │ ├── ServerTest.cpp │ │ ├── SimpleWeb/ │ │ │ ├── base_server.hpp │ │ │ ├── http_server.hpp │ │ │ └── https_server.hpp │ │ └── main.cpp │ ├── mwiniimporter/ │ │ ├── CMakeLists.txt │ │ ├── importer.cpp │ │ ├── importer.hpp │ │ └── main.cpp │ ├── niftest/ │ │ ├── CMakeLists.txt │ │ └── niftest.cpp │ ├── opencs/ │ │ ├── CMakeLists.txt │ │ ├── Networking.cpp │ │ ├── editor.cpp │ │ ├── editor.hpp │ │ ├── main.cpp │ │ ├── model/ │ │ │ ├── doc/ │ │ │ │ ├── blacklist.cpp │ │ │ │ ├── blacklist.hpp │ │ │ │ ├── document.cpp │ │ │ │ ├── document.hpp │ │ │ │ ├── documentmanager.cpp │ │ │ │ ├── documentmanager.hpp │ │ │ │ ├── loader.cpp │ │ │ │ ├── loader.hpp │ │ │ │ ├── messages.cpp │ │ │ │ ├── messages.hpp │ │ │ │ ├── operation.cpp │ │ │ │ ├── operation.hpp │ │ │ │ ├── operationholder.cpp │ │ │ │ ├── operationholder.hpp │ │ │ │ ├── runner.cpp │ │ │ │ ├── runner.hpp │ │ │ │ ├── saving.cpp │ │ │ │ ├── saving.hpp │ │ │ │ ├── savingstages.cpp │ │ │ │ ├── savingstages.hpp │ │ │ │ ├── savingstate.cpp │ │ │ │ ├── savingstate.hpp │ │ │ │ ├── stage.cpp │ │ │ │ ├── stage.hpp │ │ │ │ └── state.hpp │ │ │ ├── filter/ │ │ │ │ ├── andnode.cpp │ │ │ │ ├── andnode.hpp │ │ │ │ ├── booleannode.cpp │ │ │ │ ├── booleannode.hpp │ │ │ │ ├── leafnode.cpp │ │ │ │ ├── leafnode.hpp │ │ │ │ ├── narynode.cpp │ │ │ │ ├── narynode.hpp │ │ │ │ ├── node.cpp │ │ │ │ ├── node.hpp │ │ │ │ ├── notnode.cpp │ │ │ │ ├── notnode.hpp │ │ │ │ ├── ornode.cpp │ │ │ │ ├── ornode.hpp │ │ │ │ ├── parser.cpp │ │ │ │ ├── parser.hpp │ │ │ │ ├── textnode.cpp │ │ │ │ ├── textnode.hpp │ │ │ │ ├── unarynode.cpp │ │ │ │ ├── unarynode.hpp │ │ │ │ ├── valuenode.cpp │ │ │ │ └── valuenode.hpp │ │ │ ├── prefs/ │ │ │ │ ├── boolsetting.cpp │ │ │ │ ├── boolsetting.hpp │ │ │ │ ├── category.cpp │ │ │ │ ├── category.hpp │ │ │ │ ├── coloursetting.cpp │ │ │ │ ├── coloursetting.hpp │ │ │ │ ├── doublesetting.cpp │ │ │ │ ├── doublesetting.hpp │ │ │ │ ├── enumsetting.cpp │ │ │ │ ├── enumsetting.hpp │ │ │ │ ├── intsetting.cpp │ │ │ │ ├── intsetting.hpp │ │ │ │ ├── modifiersetting.cpp │ │ │ │ ├── modifiersetting.hpp │ │ │ │ ├── setting.cpp │ │ │ │ ├── setting.hpp │ │ │ │ ├── shortcut.cpp │ │ │ │ ├── shortcut.hpp │ │ │ │ ├── shortcuteventhandler.cpp │ │ │ │ ├── shortcuteventhandler.hpp │ │ │ │ ├── shortcutmanager.cpp │ │ │ │ ├── shortcutmanager.hpp │ │ │ │ ├── shortcutsetting.cpp │ │ │ │ ├── shortcutsetting.hpp │ │ │ │ ├── state.cpp │ │ │ │ ├── state.hpp │ │ │ │ ├── stringsetting.cpp │ │ │ │ └── stringsetting.hpp │ │ │ ├── tools/ │ │ │ │ ├── birthsigncheck.cpp │ │ │ │ ├── birthsigncheck.hpp │ │ │ │ ├── bodypartcheck.cpp │ │ │ │ ├── bodypartcheck.hpp │ │ │ │ ├── classcheck.cpp │ │ │ │ ├── classcheck.hpp │ │ │ │ ├── enchantmentcheck.cpp │ │ │ │ ├── enchantmentcheck.hpp │ │ │ │ ├── factioncheck.cpp │ │ │ │ ├── factioncheck.hpp │ │ │ │ ├── gmstcheck.cpp │ │ │ │ ├── gmstcheck.hpp │ │ │ │ ├── journalcheck.cpp │ │ │ │ ├── journalcheck.hpp │ │ │ │ ├── magiceffectcheck.cpp │ │ │ │ ├── magiceffectcheck.hpp │ │ │ │ ├── mandatoryid.cpp │ │ │ │ ├── mandatoryid.hpp │ │ │ │ ├── mergeoperation.cpp │ │ │ │ ├── mergeoperation.hpp │ │ │ │ ├── mergestages.cpp │ │ │ │ ├── mergestages.hpp │ │ │ │ ├── mergestate.hpp │ │ │ │ ├── pathgridcheck.cpp │ │ │ │ ├── pathgridcheck.hpp │ │ │ │ ├── racecheck.cpp │ │ │ │ ├── racecheck.hpp │ │ │ │ ├── referenceablecheck.cpp │ │ │ │ ├── referenceablecheck.hpp │ │ │ │ ├── referencecheck.cpp │ │ │ │ ├── referencecheck.hpp │ │ │ │ ├── regioncheck.cpp │ │ │ │ ├── regioncheck.hpp │ │ │ │ ├── reportmodel.cpp │ │ │ │ ├── reportmodel.hpp │ │ │ │ ├── scriptcheck.cpp │ │ │ │ ├── scriptcheck.hpp │ │ │ │ ├── search.cpp │ │ │ │ ├── search.hpp │ │ │ │ ├── searchoperation.cpp │ │ │ │ ├── searchoperation.hpp │ │ │ │ ├── searchstage.cpp │ │ │ │ ├── searchstage.hpp │ │ │ │ ├── skillcheck.cpp │ │ │ │ ├── skillcheck.hpp │ │ │ │ ├── soundcheck.cpp │ │ │ │ ├── soundcheck.hpp │ │ │ │ ├── soundgencheck.cpp │ │ │ │ ├── soundgencheck.hpp │ │ │ │ ├── spellcheck.cpp │ │ │ │ ├── spellcheck.hpp │ │ │ │ ├── startscriptcheck.cpp │ │ │ │ ├── startscriptcheck.hpp │ │ │ │ ├── tools.cpp │ │ │ │ ├── tools.hpp │ │ │ │ ├── topicinfocheck.cpp │ │ │ │ └── topicinfocheck.hpp │ │ │ └── world/ │ │ │ ├── actoradapter.cpp │ │ │ ├── actoradapter.hpp │ │ │ ├── cell.cpp │ │ │ ├── cell.hpp │ │ │ ├── cellcoordinates.cpp │ │ │ ├── cellcoordinates.hpp │ │ │ ├── cellselection.cpp │ │ │ ├── cellselection.hpp │ │ │ ├── collection.hpp │ │ │ ├── collectionbase.cpp │ │ │ ├── collectionbase.hpp │ │ │ ├── columnbase.cpp │ │ │ ├── columnbase.hpp │ │ │ ├── columnimp.cpp │ │ │ ├── columnimp.hpp │ │ │ ├── columns.cpp │ │ │ ├── columns.hpp │ │ │ ├── commanddispatcher.cpp │ │ │ ├── commanddispatcher.hpp │ │ │ ├── commandmacro.cpp │ │ │ ├── commandmacro.hpp │ │ │ ├── commands.cpp │ │ │ ├── commands.hpp │ │ │ ├── data.cpp │ │ │ ├── data.hpp │ │ │ ├── defaultgmsts.cpp │ │ │ ├── defaultgmsts.hpp │ │ │ ├── idcollection.hpp │ │ │ ├── idcompletionmanager.cpp │ │ │ ├── idcompletionmanager.hpp │ │ │ ├── idtable.cpp │ │ │ ├── idtable.hpp │ │ │ ├── idtablebase.cpp │ │ │ ├── idtablebase.hpp │ │ │ ├── idtableproxymodel.cpp │ │ │ ├── idtableproxymodel.hpp │ │ │ ├── idtree.cpp │ │ │ ├── idtree.hpp │ │ │ ├── info.hpp │ │ │ ├── infocollection.cpp │ │ │ ├── infocollection.hpp │ │ │ ├── infoselectwrapper.cpp │ │ │ ├── infoselectwrapper.hpp │ │ │ ├── infotableproxymodel.cpp │ │ │ ├── infotableproxymodel.hpp │ │ │ ├── land.cpp │ │ │ ├── land.hpp │ │ │ ├── landtexture.cpp │ │ │ ├── landtexture.hpp │ │ │ ├── landtexturetableproxymodel.cpp │ │ │ ├── landtexturetableproxymodel.hpp │ │ │ ├── metadata.cpp │ │ │ ├── metadata.hpp │ │ │ ├── nestedcoladapterimp.cpp │ │ │ ├── nestedcoladapterimp.hpp │ │ │ ├── nestedcollection.cpp │ │ │ ├── nestedcollection.hpp │ │ │ ├── nestedcolumnadapter.hpp │ │ │ ├── nestedidcollection.hpp │ │ │ ├── nestedinfocollection.cpp │ │ │ ├── nestedinfocollection.hpp │ │ │ ├── nestedtableproxymodel.cpp │ │ │ ├── nestedtableproxymodel.hpp │ │ │ ├── nestedtablewrapper.cpp │ │ │ ├── nestedtablewrapper.hpp │ │ │ ├── pathgrid.cpp │ │ │ ├── pathgrid.hpp │ │ │ ├── record.cpp │ │ │ ├── record.hpp │ │ │ ├── ref.cpp │ │ │ ├── ref.hpp │ │ │ ├── refcollection.cpp │ │ │ ├── refcollection.hpp │ │ │ ├── refidadapter.cpp │ │ │ ├── refidadapter.hpp │ │ │ ├── refidadapterimp.cpp │ │ │ ├── refidadapterimp.hpp │ │ │ ├── refidcollection.cpp │ │ │ ├── refidcollection.hpp │ │ │ ├── refiddata.cpp │ │ │ ├── refiddata.hpp │ │ │ ├── regionmap.cpp │ │ │ ├── regionmap.hpp │ │ │ ├── resources.cpp │ │ │ ├── resources.hpp │ │ │ ├── resourcesmanager.cpp │ │ │ ├── resourcesmanager.hpp │ │ │ ├── resourcetable.cpp │ │ │ ├── resourcetable.hpp │ │ │ ├── scope.cpp │ │ │ ├── scope.hpp │ │ │ ├── scriptcontext.cpp │ │ │ ├── scriptcontext.hpp │ │ │ ├── subcellcollection.hpp │ │ │ ├── tablemimedata.cpp │ │ │ ├── tablemimedata.hpp │ │ │ ├── universalid.cpp │ │ │ └── universalid.hpp │ │ └── view/ │ │ ├── doc/ │ │ │ ├── adjusterwidget.cpp │ │ │ ├── adjusterwidget.hpp │ │ │ ├── filedialog.cpp │ │ │ ├── filedialog.hpp │ │ │ ├── filewidget.cpp │ │ │ ├── filewidget.hpp │ │ │ ├── globaldebugprofilemenu.cpp │ │ │ ├── globaldebugprofilemenu.hpp │ │ │ ├── loader.cpp │ │ │ ├── loader.hpp │ │ │ ├── newgame.cpp │ │ │ ├── newgame.hpp │ │ │ ├── operation.cpp │ │ │ ├── operation.hpp │ │ │ ├── operations.cpp │ │ │ ├── operations.hpp │ │ │ ├── runlogsubview.cpp │ │ │ ├── runlogsubview.hpp │ │ │ ├── sizehint.cpp │ │ │ ├── sizehint.hpp │ │ │ ├── startup.cpp │ │ │ ├── startup.hpp │ │ │ ├── subview.cpp │ │ │ ├── subview.hpp │ │ │ ├── subviewfactory.cpp │ │ │ ├── subviewfactory.hpp │ │ │ ├── subviewfactoryimp.hpp │ │ │ ├── view.cpp │ │ │ ├── view.hpp │ │ │ ├── viewmanager.cpp │ │ │ └── viewmanager.hpp │ │ ├── filter/ │ │ │ ├── editwidget.cpp │ │ │ ├── editwidget.hpp │ │ │ ├── filterbox.cpp │ │ │ ├── filterbox.hpp │ │ │ ├── recordfilterbox.cpp │ │ │ └── recordfilterbox.hpp │ │ ├── prefs/ │ │ │ ├── contextmenulist.cpp │ │ │ ├── contextmenulist.hpp │ │ │ ├── dialogue.cpp │ │ │ ├── dialogue.hpp │ │ │ ├── keybindingpage.cpp │ │ │ ├── keybindingpage.hpp │ │ │ ├── page.cpp │ │ │ ├── page.hpp │ │ │ ├── pagebase.cpp │ │ │ └── pagebase.hpp │ │ ├── render/ │ │ │ ├── actor.cpp │ │ │ ├── actor.hpp │ │ │ ├── brushdraw.cpp │ │ │ ├── brushdraw.hpp │ │ │ ├── cameracontroller.cpp │ │ │ ├── cameracontroller.hpp │ │ │ ├── cell.cpp │ │ │ ├── cell.hpp │ │ │ ├── cellarrow.cpp │ │ │ ├── cellarrow.hpp │ │ │ ├── cellborder.cpp │ │ │ ├── cellborder.hpp │ │ │ ├── cellmarker.cpp │ │ │ ├── cellmarker.hpp │ │ │ ├── cellwater.cpp │ │ │ ├── cellwater.hpp │ │ │ ├── commands.cpp │ │ │ ├── commands.hpp │ │ │ ├── editmode.cpp │ │ │ ├── editmode.hpp │ │ │ ├── instancedragmodes.hpp │ │ │ ├── instancemode.cpp │ │ │ ├── instancemode.hpp │ │ │ ├── instancemovemode.cpp │ │ │ ├── instancemovemode.hpp │ │ │ ├── instanceselectionmode.cpp │ │ │ ├── instanceselectionmode.hpp │ │ │ ├── lighting.cpp │ │ │ ├── lighting.hpp │ │ │ ├── lightingbright.cpp │ │ │ ├── lightingbright.hpp │ │ │ ├── lightingday.cpp │ │ │ ├── lightingday.hpp │ │ │ ├── lightingnight.cpp │ │ │ ├── lightingnight.hpp │ │ │ ├── mask.hpp │ │ │ ├── object.cpp │ │ │ ├── object.hpp │ │ │ ├── orbitcameramode.cpp │ │ │ ├── orbitcameramode.hpp │ │ │ ├── pagedworldspacewidget.cpp │ │ │ ├── pagedworldspacewidget.hpp │ │ │ ├── pathgrid.cpp │ │ │ ├── pathgrid.hpp │ │ │ ├── pathgridmode.cpp │ │ │ ├── pathgridmode.hpp │ │ │ ├── pathgridselectionmode.cpp │ │ │ ├── pathgridselectionmode.hpp │ │ │ ├── previewwidget.cpp │ │ │ ├── previewwidget.hpp │ │ │ ├── scenewidget.cpp │ │ │ ├── scenewidget.hpp │ │ │ ├── selectionmode.cpp │ │ │ ├── selectionmode.hpp │ │ │ ├── tagbase.cpp │ │ │ ├── tagbase.hpp │ │ │ ├── terrainselection.cpp │ │ │ ├── terrainselection.hpp │ │ │ ├── terrainshapemode.cpp │ │ │ ├── terrainshapemode.hpp │ │ │ ├── terrainstorage.cpp │ │ │ ├── terrainstorage.hpp │ │ │ ├── terraintexturemode.cpp │ │ │ ├── terraintexturemode.hpp │ │ │ ├── unpagedworldspacewidget.cpp │ │ │ ├── unpagedworldspacewidget.hpp │ │ │ ├── worldspacewidget.cpp │ │ │ └── worldspacewidget.hpp │ │ ├── tools/ │ │ │ ├── merge.cpp │ │ │ ├── merge.hpp │ │ │ ├── reportsubview.cpp │ │ │ ├── reportsubview.hpp │ │ │ ├── reporttable.cpp │ │ │ ├── reporttable.hpp │ │ │ ├── searchbox.cpp │ │ │ ├── searchbox.hpp │ │ │ ├── searchsubview.cpp │ │ │ ├── searchsubview.hpp │ │ │ ├── subviews.cpp │ │ │ └── subviews.hpp │ │ ├── widget/ │ │ │ ├── brushshapes.hpp │ │ │ ├── coloreditor.cpp │ │ │ ├── coloreditor.hpp │ │ │ ├── colorpickerpopup.cpp │ │ │ ├── colorpickerpopup.hpp │ │ │ ├── completerpopup.cpp │ │ │ ├── completerpopup.hpp │ │ │ ├── droplineedit.cpp │ │ │ ├── droplineedit.hpp │ │ │ ├── modebutton.cpp │ │ │ ├── modebutton.hpp │ │ │ ├── pushbutton.cpp │ │ │ ├── pushbutton.hpp │ │ │ ├── scenetool.cpp │ │ │ ├── scenetool.hpp │ │ │ ├── scenetoolbar.cpp │ │ │ ├── scenetoolbar.hpp │ │ │ ├── scenetoolmode.cpp │ │ │ ├── scenetoolmode.hpp │ │ │ ├── scenetoolrun.cpp │ │ │ ├── scenetoolrun.hpp │ │ │ ├── scenetoolshapebrush.cpp │ │ │ ├── scenetoolshapebrush.hpp │ │ │ ├── scenetooltexturebrush.cpp │ │ │ ├── scenetooltexturebrush.hpp │ │ │ ├── scenetooltoggle.cpp │ │ │ ├── scenetooltoggle.hpp │ │ │ ├── scenetooltoggle2.cpp │ │ │ └── scenetooltoggle2.hpp │ │ └── world/ │ │ ├── bodypartcreator.cpp │ │ ├── bodypartcreator.hpp │ │ ├── cellcreator.cpp │ │ ├── cellcreator.hpp │ │ ├── colordelegate.cpp │ │ ├── colordelegate.hpp │ │ ├── creator.cpp │ │ ├── creator.hpp │ │ ├── datadisplaydelegate.cpp │ │ ├── datadisplaydelegate.hpp │ │ ├── dialoguecreator.cpp │ │ ├── dialoguecreator.hpp │ │ ├── dialoguespinbox.cpp │ │ ├── dialoguespinbox.hpp │ │ ├── dialoguesubview.cpp │ │ ├── dialoguesubview.hpp │ │ ├── dragdroputils.cpp │ │ ├── dragdroputils.hpp │ │ ├── dragrecordtable.cpp │ │ ├── dragrecordtable.hpp │ │ ├── enumdelegate.cpp │ │ ├── enumdelegate.hpp │ │ ├── extendedcommandconfigurator.cpp │ │ ├── extendedcommandconfigurator.hpp │ │ ├── genericcreator.cpp │ │ ├── genericcreator.hpp │ │ ├── globalcreator.cpp │ │ ├── globalcreator.hpp │ │ ├── idcompletiondelegate.cpp │ │ ├── idcompletiondelegate.hpp │ │ ├── idtypedelegate.cpp │ │ ├── idtypedelegate.hpp │ │ ├── idvalidator.cpp │ │ ├── idvalidator.hpp │ │ ├── infocreator.cpp │ │ ├── infocreator.hpp │ │ ├── landcreator.cpp │ │ ├── landcreator.hpp │ │ ├── landtexturecreator.cpp │ │ ├── landtexturecreator.hpp │ │ ├── nestedtable.cpp │ │ ├── nestedtable.hpp │ │ ├── pathgridcreator.cpp │ │ ├── pathgridcreator.hpp │ │ ├── previewsubview.cpp │ │ ├── previewsubview.hpp │ │ ├── recordbuttonbar.cpp │ │ ├── recordbuttonbar.hpp │ │ ├── recordstatusdelegate.cpp │ │ ├── recordstatusdelegate.hpp │ │ ├── referenceablecreator.cpp │ │ ├── referenceablecreator.hpp │ │ ├── referencecreator.cpp │ │ ├── referencecreator.hpp │ │ ├── regionmap.cpp │ │ ├── regionmap.hpp │ │ ├── regionmapsubview.cpp │ │ ├── regionmapsubview.hpp │ │ ├── scenesubview.cpp │ │ ├── scenesubview.hpp │ │ ├── scriptedit.cpp │ │ ├── scriptedit.hpp │ │ ├── scripterrortable.cpp │ │ ├── scripterrortable.hpp │ │ ├── scripthighlighter.cpp │ │ ├── scripthighlighter.hpp │ │ ├── scriptsubview.cpp │ │ ├── scriptsubview.hpp │ │ ├── startscriptcreator.cpp │ │ ├── startscriptcreator.hpp │ │ ├── subviews.cpp │ │ ├── subviews.hpp │ │ ├── table.cpp │ │ ├── table.hpp │ │ ├── tablebottombox.cpp │ │ ├── tablebottombox.hpp │ │ ├── tableeditidaction.cpp │ │ ├── tableeditidaction.hpp │ │ ├── tablesubview.cpp │ │ ├── tablesubview.hpp │ │ ├── util.cpp │ │ ├── util.hpp │ │ ├── vartypedelegate.cpp │ │ └── vartypedelegate.hpp │ ├── openmw/ │ │ ├── CMakeLists.txt │ │ ├── android_main.cpp │ │ ├── doc.hpp │ │ ├── engine.cpp │ │ ├── engine.hpp │ │ ├── main.cpp │ │ ├── mwbase/ │ │ │ ├── dialoguemanager.hpp │ │ │ ├── environment.cpp │ │ │ ├── environment.hpp │ │ │ ├── inputmanager.hpp │ │ │ ├── journal.hpp │ │ │ ├── mechanicsmanager.hpp │ │ │ ├── rotationflags.hpp │ │ │ ├── scriptmanager.hpp │ │ │ ├── soundmanager.hpp │ │ │ ├── statemanager.hpp │ │ │ ├── windowmanager.hpp │ │ │ └── world.hpp │ │ ├── mwclass/ │ │ │ ├── activator.cpp │ │ │ ├── activator.hpp │ │ │ ├── actor.cpp │ │ │ ├── actor.hpp │ │ │ ├── apparatus.cpp │ │ │ ├── apparatus.hpp │ │ │ ├── armor.cpp │ │ │ ├── armor.hpp │ │ │ ├── bodypart.cpp │ │ │ ├── bodypart.hpp │ │ │ ├── book.cpp │ │ │ ├── book.hpp │ │ │ ├── classes.cpp │ │ │ ├── classes.hpp │ │ │ ├── clothing.cpp │ │ │ ├── clothing.hpp │ │ │ ├── container.cpp │ │ │ ├── container.hpp │ │ │ ├── creature.cpp │ │ │ ├── creature.hpp │ │ │ ├── creaturelevlist.cpp │ │ │ ├── creaturelevlist.hpp │ │ │ ├── door.cpp │ │ │ ├── door.hpp │ │ │ ├── ingredient.cpp │ │ │ ├── ingredient.hpp │ │ │ ├── itemlevlist.cpp │ │ │ ├── itemlevlist.hpp │ │ │ ├── light.cpp │ │ │ ├── light.hpp │ │ │ ├── lockpick.cpp │ │ │ ├── lockpick.hpp │ │ │ ├── misc.cpp │ │ │ ├── misc.hpp │ │ │ ├── npc.cpp │ │ │ ├── npc.hpp │ │ │ ├── potion.cpp │ │ │ ├── potion.hpp │ │ │ ├── probe.cpp │ │ │ ├── probe.hpp │ │ │ ├── repair.cpp │ │ │ ├── repair.hpp │ │ │ ├── static.cpp │ │ │ ├── static.hpp │ │ │ ├── weapon.cpp │ │ │ └── weapon.hpp │ │ ├── mwdialogue/ │ │ │ ├── dialoguemanagerimp.cpp │ │ │ ├── dialoguemanagerimp.hpp │ │ │ ├── filter.cpp │ │ │ ├── filter.hpp │ │ │ ├── hypertextparser.cpp │ │ │ ├── hypertextparser.hpp │ │ │ ├── journalentry.cpp │ │ │ ├── journalentry.hpp │ │ │ ├── journalimp.cpp │ │ │ ├── journalimp.hpp │ │ │ ├── keywordsearch.cpp │ │ │ ├── keywordsearch.hpp │ │ │ ├── quest.cpp │ │ │ ├── quest.hpp │ │ │ ├── scripttest.cpp │ │ │ ├── scripttest.hpp │ │ │ ├── selectwrapper.cpp │ │ │ ├── selectwrapper.hpp │ │ │ ├── topic.cpp │ │ │ └── topic.hpp │ │ ├── mwgui/ │ │ │ ├── alchemywindow.cpp │ │ │ ├── alchemywindow.hpp │ │ │ ├── backgroundimage.cpp │ │ │ ├── backgroundimage.hpp │ │ │ ├── birth.cpp │ │ │ ├── birth.hpp │ │ │ ├── bookpage.cpp │ │ │ ├── bookpage.hpp │ │ │ ├── bookwindow.cpp │ │ │ ├── bookwindow.hpp │ │ │ ├── charactercreation.cpp │ │ │ ├── charactercreation.hpp │ │ │ ├── class.cpp │ │ │ ├── class.hpp │ │ │ ├── companionitemmodel.cpp │ │ │ ├── companionitemmodel.hpp │ │ │ ├── companionwindow.cpp │ │ │ ├── companionwindow.hpp │ │ │ ├── confirmationdialog.cpp │ │ │ ├── confirmationdialog.hpp │ │ │ ├── console.cpp │ │ │ ├── console.hpp │ │ │ ├── container.cpp │ │ │ ├── container.hpp │ │ │ ├── containeritemmodel.cpp │ │ │ ├── containeritemmodel.hpp │ │ │ ├── controllers.cpp │ │ │ ├── controllers.hpp │ │ │ ├── countdialog.cpp │ │ │ ├── countdialog.hpp │ │ │ ├── cursor.cpp │ │ │ ├── cursor.hpp │ │ │ ├── debugwindow.cpp │ │ │ ├── debugwindow.hpp │ │ │ ├── dialogue.cpp │ │ │ ├── dialogue.hpp │ │ │ ├── draganddrop.cpp │ │ │ ├── draganddrop.hpp │ │ │ ├── enchantingdialog.cpp │ │ │ ├── enchantingdialog.hpp │ │ │ ├── exposedwindow.cpp │ │ │ ├── exposedwindow.hpp │ │ │ ├── formatting.cpp │ │ │ ├── formatting.hpp │ │ │ ├── hud.cpp │ │ │ ├── hud.hpp │ │ │ ├── inventoryitemmodel.cpp │ │ │ ├── inventoryitemmodel.hpp │ │ │ ├── inventorywindow.cpp │ │ │ ├── inventorywindow.hpp │ │ │ ├── itemchargeview.cpp │ │ │ ├── itemchargeview.hpp │ │ │ ├── itemmodel.cpp │ │ │ ├── itemmodel.hpp │ │ │ ├── itemselection.cpp │ │ │ ├── itemselection.hpp │ │ │ ├── itemview.cpp │ │ │ ├── itemview.hpp │ │ │ ├── itemwidget.cpp │ │ │ ├── itemwidget.hpp │ │ │ ├── jailscreen.cpp │ │ │ ├── jailscreen.hpp │ │ │ ├── journalbooks.cpp │ │ │ ├── journalbooks.hpp │ │ │ ├── journalviewmodel.cpp │ │ │ ├── journalviewmodel.hpp │ │ │ ├── journalwindow.cpp │ │ │ ├── journalwindow.hpp │ │ │ ├── keyboardnavigation.cpp │ │ │ ├── keyboardnavigation.hpp │ │ │ ├── layout.cpp │ │ │ ├── layout.hpp │ │ │ ├── levelupdialog.cpp │ │ │ ├── levelupdialog.hpp │ │ │ ├── loadingscreen.cpp │ │ │ ├── loadingscreen.hpp │ │ │ ├── mainmenu.cpp │ │ │ ├── mainmenu.hpp │ │ │ ├── mapwindow.cpp │ │ │ ├── mapwindow.hpp │ │ │ ├── merchantrepair.cpp │ │ │ ├── merchantrepair.hpp │ │ │ ├── messagebox.cpp │ │ │ ├── messagebox.hpp │ │ │ ├── mode.hpp │ │ │ ├── pickpocketitemmodel.cpp │ │ │ ├── pickpocketitemmodel.hpp │ │ │ ├── quickkeysmenu.cpp │ │ │ ├── quickkeysmenu.hpp │ │ │ ├── race.cpp │ │ │ ├── race.hpp │ │ │ ├── recharge.cpp │ │ │ ├── recharge.hpp │ │ │ ├── referenceinterface.cpp │ │ │ ├── referenceinterface.hpp │ │ │ ├── repair.cpp │ │ │ ├── repair.hpp │ │ │ ├── resourceskin.cpp │ │ │ ├── resourceskin.hpp │ │ │ ├── review.cpp │ │ │ ├── review.hpp │ │ │ ├── savegamedialog.cpp │ │ │ ├── savegamedialog.hpp │ │ │ ├── screenfader.cpp │ │ │ ├── screenfader.hpp │ │ │ ├── scrollwindow.cpp │ │ │ ├── scrollwindow.hpp │ │ │ ├── settingswindow.cpp │ │ │ ├── settingswindow.hpp │ │ │ ├── sortfilteritemmodel.cpp │ │ │ ├── sortfilteritemmodel.hpp │ │ │ ├── soulgemdialog.cpp │ │ │ ├── soulgemdialog.hpp │ │ │ ├── spellbuyingwindow.cpp │ │ │ ├── spellbuyingwindow.hpp │ │ │ ├── spellcreationdialog.cpp │ │ │ ├── spellcreationdialog.hpp │ │ │ ├── spellicons.cpp │ │ │ ├── spellicons.hpp │ │ │ ├── spellmodel.cpp │ │ │ ├── spellmodel.hpp │ │ │ ├── spellview.cpp │ │ │ ├── spellview.hpp │ │ │ ├── spellwindow.cpp │ │ │ ├── spellwindow.hpp │ │ │ ├── statswatcher.cpp │ │ │ ├── statswatcher.hpp │ │ │ ├── statswindow.cpp │ │ │ ├── statswindow.hpp │ │ │ ├── textcolours.cpp │ │ │ ├── textcolours.hpp │ │ │ ├── textinput.cpp │ │ │ ├── textinput.hpp │ │ │ ├── timeadvancer.cpp │ │ │ ├── timeadvancer.hpp │ │ │ ├── tooltips.cpp │ │ │ ├── tooltips.hpp │ │ │ ├── tradeitemmodel.cpp │ │ │ ├── tradeitemmodel.hpp │ │ │ ├── tradewindow.cpp │ │ │ ├── tradewindow.hpp │ │ │ ├── trainingwindow.cpp │ │ │ ├── trainingwindow.hpp │ │ │ ├── travelwindow.cpp │ │ │ ├── travelwindow.hpp │ │ │ ├── videowidget.cpp │ │ │ ├── videowidget.hpp │ │ │ ├── waitdialog.cpp │ │ │ ├── waitdialog.hpp │ │ │ ├── widgets.cpp │ │ │ ├── widgets.hpp │ │ │ ├── windowbase.cpp │ │ │ ├── windowbase.hpp │ │ │ ├── windowmanagerimp.cpp │ │ │ ├── windowmanagerimp.hpp │ │ │ ├── windowpinnablebase.cpp │ │ │ └── windowpinnablebase.hpp │ │ ├── mwinput/ │ │ │ ├── actionmanager.cpp │ │ │ ├── actionmanager.hpp │ │ │ ├── actions.hpp │ │ │ ├── bindingsmanager.cpp │ │ │ ├── bindingsmanager.hpp │ │ │ ├── controllermanager.cpp │ │ │ ├── controllermanager.hpp │ │ │ ├── controlswitch.cpp │ │ │ ├── controlswitch.hpp │ │ │ ├── inputmanagerimp.cpp │ │ │ ├── inputmanagerimp.hpp │ │ │ ├── keyboardmanager.cpp │ │ │ ├── keyboardmanager.hpp │ │ │ ├── mousemanager.cpp │ │ │ ├── mousemanager.hpp │ │ │ ├── sdlmappings.cpp │ │ │ ├── sdlmappings.hpp │ │ │ ├── sensormanager.cpp │ │ │ └── sensormanager.hpp │ │ ├── mwmechanics/ │ │ │ ├── activespells.cpp │ │ │ ├── activespells.hpp │ │ │ ├── actor.cpp │ │ │ ├── actor.hpp │ │ │ ├── actors.cpp │ │ │ ├── actors.hpp │ │ │ ├── actorutil.cpp │ │ │ ├── actorutil.hpp │ │ │ ├── aiactivate.cpp │ │ │ ├── aiactivate.hpp │ │ │ ├── aiavoiddoor.cpp │ │ │ ├── aiavoiddoor.hpp │ │ │ ├── aibreathe.cpp │ │ │ ├── aibreathe.hpp │ │ │ ├── aicast.cpp │ │ │ ├── aicast.hpp │ │ │ ├── aicombat.cpp │ │ │ ├── aicombat.hpp │ │ │ ├── aicombataction.cpp │ │ │ ├── aicombataction.hpp │ │ │ ├── aiescort.cpp │ │ │ ├── aiescort.hpp │ │ │ ├── aiface.cpp │ │ │ ├── aiface.hpp │ │ │ ├── aifollow.cpp │ │ │ ├── aifollow.hpp │ │ │ ├── aipackage.cpp │ │ │ ├── aipackage.hpp │ │ │ ├── aipackagetypeid.hpp │ │ │ ├── aipursue.cpp │ │ │ ├── aipursue.hpp │ │ │ ├── aisequence.cpp │ │ │ ├── aisequence.hpp │ │ │ ├── aistate.hpp │ │ │ ├── aitimer.hpp │ │ │ ├── aitravel.cpp │ │ │ ├── aitravel.hpp │ │ │ ├── aiwander.cpp │ │ │ ├── aiwander.hpp │ │ │ ├── alchemy.cpp │ │ │ ├── alchemy.hpp │ │ │ ├── autocalcspell.cpp │ │ │ ├── autocalcspell.hpp │ │ │ ├── character.cpp │ │ │ ├── character.hpp │ │ │ ├── combat.cpp │ │ │ ├── combat.hpp │ │ │ ├── creaturestats.cpp │ │ │ ├── creaturestats.hpp │ │ │ ├── difficultyscaling.cpp │ │ │ ├── difficultyscaling.hpp │ │ │ ├── disease.hpp │ │ │ ├── drawstate.hpp │ │ │ ├── enchanting.cpp │ │ │ ├── enchanting.hpp │ │ │ ├── levelledlist.hpp │ │ │ ├── linkedeffects.cpp │ │ │ ├── linkedeffects.hpp │ │ │ ├── magiceffects.cpp │ │ │ ├── magiceffects.hpp │ │ │ ├── mechanicsmanagerimp.cpp │ │ │ ├── mechanicsmanagerimp.hpp │ │ │ ├── movement.hpp │ │ │ ├── npcstats.cpp │ │ │ ├── npcstats.hpp │ │ │ ├── objects.cpp │ │ │ ├── objects.hpp │ │ │ ├── obstacle.cpp │ │ │ ├── obstacle.hpp │ │ │ ├── pathfinding.cpp │ │ │ ├── pathfinding.hpp │ │ │ ├── pathgrid.cpp │ │ │ ├── pathgrid.hpp │ │ │ ├── pickpocket.cpp │ │ │ ├── pickpocket.hpp │ │ │ ├── recharge.cpp │ │ │ ├── recharge.hpp │ │ │ ├── repair.cpp │ │ │ ├── repair.hpp │ │ │ ├── security.cpp │ │ │ ├── security.hpp │ │ │ ├── spellabsorption.cpp │ │ │ ├── spellabsorption.hpp │ │ │ ├── spellcasting.cpp │ │ │ ├── spellcasting.hpp │ │ │ ├── spelllist.cpp │ │ │ ├── spelllist.hpp │ │ │ ├── spellpriority.cpp │ │ │ ├── spellpriority.hpp │ │ │ ├── spellresistance.cpp │ │ │ ├── spellresistance.hpp │ │ │ ├── spells.cpp │ │ │ ├── spells.hpp │ │ │ ├── spellutil.cpp │ │ │ ├── spellutil.hpp │ │ │ ├── stat.cpp │ │ │ ├── stat.hpp │ │ │ ├── steering.cpp │ │ │ ├── steering.hpp │ │ │ ├── summoning.cpp │ │ │ ├── summoning.hpp │ │ │ ├── tickableeffects.cpp │ │ │ ├── tickableeffects.hpp │ │ │ ├── trading.cpp │ │ │ ├── trading.hpp │ │ │ ├── typedaipackage.hpp │ │ │ ├── weaponpriority.cpp │ │ │ ├── weaponpriority.hpp │ │ │ ├── weapontype.cpp │ │ │ └── weapontype.hpp │ │ ├── mwmp/ │ │ │ ├── ActorList.cpp │ │ │ ├── ActorList.hpp │ │ │ ├── Cell.cpp │ │ │ ├── Cell.hpp │ │ │ ├── CellController.cpp │ │ │ ├── CellController.hpp │ │ │ ├── DedicatedActor.cpp │ │ │ ├── DedicatedActor.hpp │ │ │ ├── DedicatedPlayer.cpp │ │ │ ├── DedicatedPlayer.hpp │ │ │ ├── GUI/ │ │ │ │ ├── GUIChat.cpp │ │ │ │ ├── GUIChat.hpp │ │ │ │ ├── GUIDialogList.cpp │ │ │ │ ├── GUIDialogList.hpp │ │ │ │ ├── GUILogin.cpp │ │ │ │ ├── GUILogin.hpp │ │ │ │ ├── PlayerMarkerCollection.cpp │ │ │ │ ├── PlayerMarkerCollection.hpp │ │ │ │ ├── TextInputDialog.cpp │ │ │ │ └── TextInputDialog.hpp │ │ │ ├── GUIController.cpp │ │ │ ├── GUIController.hpp │ │ │ ├── LocalActor.cpp │ │ │ ├── LocalActor.hpp │ │ │ ├── LocalPlayer.cpp │ │ │ ├── LocalPlayer.hpp │ │ │ ├── LocalSystem.cpp │ │ │ ├── LocalSystem.hpp │ │ │ ├── Main.cpp │ │ │ ├── Main.hpp │ │ │ ├── MechanicsHelper.cpp │ │ │ ├── MechanicsHelper.hpp │ │ │ ├── Networking.cpp │ │ │ ├── Networking.hpp │ │ │ ├── ObjectList.cpp │ │ │ ├── ObjectList.hpp │ │ │ ├── PlayerList.cpp │ │ │ ├── PlayerList.hpp │ │ │ ├── RecordHelper.cpp │ │ │ ├── RecordHelper.hpp │ │ │ ├── ScriptController.cpp │ │ │ ├── ScriptController.hpp │ │ │ ├── Worldstate.cpp │ │ │ ├── Worldstate.hpp │ │ │ └── processors/ │ │ │ ├── ActorProcessor.cpp │ │ │ ├── ActorProcessor.hpp │ │ │ ├── BaseClientPacketProcessor.cpp │ │ │ ├── BaseClientPacketProcessor.hpp │ │ │ ├── ObjectProcessor.cpp │ │ │ ├── ObjectProcessor.hpp │ │ │ ├── PlayerProcessor.cpp │ │ │ ├── PlayerProcessor.hpp │ │ │ ├── ProcessorInitializer.cpp │ │ │ ├── ProcessorInitializer.hpp │ │ │ ├── SystemProcessor.cpp │ │ │ ├── SystemProcessor.hpp │ │ │ ├── WorldstateProcessor.cpp │ │ │ ├── WorldstateProcessor.hpp │ │ │ ├── actor/ │ │ │ │ ├── ProcessorActorAI.hpp │ │ │ │ ├── ProcessorActorAnimFlags.hpp │ │ │ │ ├── ProcessorActorAnimPlay.hpp │ │ │ │ ├── ProcessorActorAttack.hpp │ │ │ │ ├── ProcessorActorAuthority.hpp │ │ │ │ ├── ProcessorActorCast.hpp │ │ │ │ ├── ProcessorActorCellChange.hpp │ │ │ │ ├── ProcessorActorDeath.hpp │ │ │ │ ├── ProcessorActorEquipment.hpp │ │ │ │ ├── ProcessorActorList.hpp │ │ │ │ ├── ProcessorActorPosition.hpp │ │ │ │ ├── ProcessorActorSpeech.hpp │ │ │ │ ├── ProcessorActorSpellsActive.hpp │ │ │ │ ├── ProcessorActorStatsDynamic.hpp │ │ │ │ └── ProcessorActorTest.hpp │ │ │ ├── object/ │ │ │ │ ├── BaseObjectProcessor.hpp │ │ │ │ ├── ProcessorClientScriptLocal.hpp │ │ │ │ ├── ProcessorConsoleCommand.hpp │ │ │ │ ├── ProcessorContainer.hpp │ │ │ │ ├── ProcessorDoorDestination.hpp │ │ │ │ ├── ProcessorDoorState.hpp │ │ │ │ ├── ProcessorMusicPlay.hpp │ │ │ │ ├── ProcessorObjectActivate.hpp │ │ │ │ ├── ProcessorObjectAnimPlay.hpp │ │ │ │ ├── ProcessorObjectAttach.hpp │ │ │ │ ├── ProcessorObjectDelete.hpp │ │ │ │ ├── ProcessorObjectDialogueChoice.hpp │ │ │ │ ├── ProcessorObjectHit.hpp │ │ │ │ ├── ProcessorObjectLock.hpp │ │ │ │ ├── ProcessorObjectMiscellaneous.hpp │ │ │ │ ├── ProcessorObjectMove.hpp │ │ │ │ ├── ProcessorObjectPlace.hpp │ │ │ │ ├── ProcessorObjectRestock.hpp │ │ │ │ ├── ProcessorObjectRotate.hpp │ │ │ │ ├── ProcessorObjectScale.hpp │ │ │ │ ├── ProcessorObjectSound.hpp │ │ │ │ ├── ProcessorObjectSpawn.hpp │ │ │ │ ├── ProcessorObjectState.hpp │ │ │ │ ├── ProcessorObjectTrap.hpp │ │ │ │ ├── ProcessorScriptMemberShort.hpp │ │ │ │ └── ProcessorVideoPlay.hpp │ │ │ ├── player/ │ │ │ │ ├── ProcessorChatMessage.hpp │ │ │ │ ├── ProcessorGUIMessageBox.hpp │ │ │ │ ├── ProcessorGameSettings.hpp │ │ │ │ ├── ProcessorPlayerAlly.hpp │ │ │ │ ├── ProcessorPlayerAnimFlags.hpp │ │ │ │ ├── ProcessorPlayerAnimPlay.hpp │ │ │ │ ├── ProcessorPlayerAttack.hpp │ │ │ │ ├── ProcessorPlayerAttribute.hpp │ │ │ │ ├── ProcessorPlayerBaseInfo.hpp │ │ │ │ ├── ProcessorPlayerBehavior.hpp │ │ │ │ ├── ProcessorPlayerBook.hpp │ │ │ │ ├── ProcessorPlayerBounty.hpp │ │ │ │ ├── ProcessorPlayerCast.hpp │ │ │ │ ├── ProcessorPlayerCellChange.hpp │ │ │ │ ├── ProcessorPlayerCellState.hpp │ │ │ │ ├── ProcessorPlayerCharClass.hpp │ │ │ │ ├── ProcessorPlayerCharGen.hpp │ │ │ │ ├── ProcessorPlayerCooldowns.hpp │ │ │ │ ├── ProcessorPlayerDeath.hpp │ │ │ │ ├── ProcessorPlayerDisposition.hpp │ │ │ │ ├── ProcessorPlayerEquipment.hpp │ │ │ │ ├── ProcessorPlayerFaction.hpp │ │ │ │ ├── ProcessorPlayerInput.hpp │ │ │ │ ├── ProcessorPlayerInventory.hpp │ │ │ │ ├── ProcessorPlayerItemUse.hpp │ │ │ │ ├── ProcessorPlayerJail.hpp │ │ │ │ ├── ProcessorPlayerJournal.hpp │ │ │ │ ├── ProcessorPlayerLevel.hpp │ │ │ │ ├── ProcessorPlayerMiscellaneous.hpp │ │ │ │ ├── ProcessorPlayerMomentum.hpp │ │ │ │ ├── ProcessorPlayerPosition.hpp │ │ │ │ ├── ProcessorPlayerQuickKeys.hpp │ │ │ │ ├── ProcessorPlayerReputation.hpp │ │ │ │ ├── ProcessorPlayerRest.hpp │ │ │ │ ├── ProcessorPlayerResurrect.hpp │ │ │ │ ├── ProcessorPlayerShapeshift.hpp │ │ │ │ ├── ProcessorPlayerSkill.hpp │ │ │ │ ├── ProcessorPlayerSpeech.hpp │ │ │ │ ├── ProcessorPlayerSpellbook.hpp │ │ │ │ ├── ProcessorPlayerSpellsActive.hpp │ │ │ │ ├── ProcessorPlayerStatsDynamic.hpp │ │ │ │ ├── ProcessorPlayerTopic.hpp │ │ │ │ └── ProcessorUserDisconnected.hpp │ │ │ ├── system/ │ │ │ │ └── ProcessorSystemHandshake.hpp │ │ │ └── worldstate/ │ │ │ ├── ProcessorCellReset.hpp │ │ │ ├── ProcessorClientScriptGlobal.hpp │ │ │ ├── ProcessorClientScriptSettings.hpp │ │ │ ├── ProcessorRecordDynamic.hpp │ │ │ ├── ProcessorWorldCollisionOverride.hpp │ │ │ ├── ProcessorWorldDestinationOverride.hpp │ │ │ ├── ProcessorWorldKillCount.hpp │ │ │ ├── ProcessorWorldMap.hpp │ │ │ ├── ProcessorWorldRegionAuthority.hpp │ │ │ ├── ProcessorWorldTime.hpp │ │ │ └── ProcessorWorldWeather.hpp │ │ ├── mwphysics/ │ │ │ ├── actor.cpp │ │ │ ├── actor.hpp │ │ │ ├── actorconvexcallback.cpp │ │ │ ├── actorconvexcallback.hpp │ │ │ ├── closestnotmerayresultcallback.cpp │ │ │ ├── closestnotmerayresultcallback.hpp │ │ │ ├── collisiontype.hpp │ │ │ ├── constants.hpp │ │ │ ├── contacttestresultcallback.cpp │ │ │ ├── contacttestresultcallback.hpp │ │ │ ├── contacttestwrapper.cpp │ │ │ ├── contacttestwrapper.h │ │ │ ├── deepestnotmecontacttestresultcallback.cpp │ │ │ ├── deepestnotmecontacttestresultcallback.hpp │ │ │ ├── hasspherecollisioncallback.hpp │ │ │ ├── heightfield.cpp │ │ │ ├── heightfield.hpp │ │ │ ├── movementsolver.cpp │ │ │ ├── movementsolver.hpp │ │ │ ├── mtphysics.cpp │ │ │ ├── mtphysics.hpp │ │ │ ├── object.cpp │ │ │ ├── object.hpp │ │ │ ├── physicssystem.cpp │ │ │ ├── physicssystem.hpp │ │ │ ├── projectile.cpp │ │ │ ├── projectile.hpp │ │ │ ├── projectileconvexcallback.cpp │ │ │ ├── projectileconvexcallback.hpp │ │ │ ├── ptrholder.hpp │ │ │ ├── raycasting.hpp │ │ │ ├── stepper.cpp │ │ │ ├── stepper.hpp │ │ │ ├── trace.cpp │ │ │ └── trace.h │ │ ├── mwrender/ │ │ │ ├── .gitignore │ │ │ ├── actoranimation.cpp │ │ │ ├── actoranimation.hpp │ │ │ ├── actorspaths.cpp │ │ │ ├── actorspaths.hpp │ │ │ ├── animation.cpp │ │ │ ├── animation.hpp │ │ │ ├── bulletdebugdraw.cpp │ │ │ ├── bulletdebugdraw.hpp │ │ │ ├── camera.cpp │ │ │ ├── camera.hpp │ │ │ ├── cell.hpp │ │ │ ├── characterpreview.cpp │ │ │ ├── characterpreview.hpp │ │ │ ├── creatureanimation.cpp │ │ │ ├── creatureanimation.hpp │ │ │ ├── effectmanager.cpp │ │ │ ├── effectmanager.hpp │ │ │ ├── fogmanager.cpp │ │ │ ├── fogmanager.hpp │ │ │ ├── globalmap.cpp │ │ │ ├── globalmap.hpp │ │ │ ├── groundcover.cpp │ │ │ ├── groundcover.hpp │ │ │ ├── landmanager.cpp │ │ │ ├── landmanager.hpp │ │ │ ├── localmap.cpp │ │ │ ├── localmap.hpp │ │ │ ├── navmesh.cpp │ │ │ ├── navmesh.hpp │ │ │ ├── npcanimation.cpp │ │ │ ├── npcanimation.hpp │ │ │ ├── objectpaging.cpp │ │ │ ├── objectpaging.hpp │ │ │ ├── objects.cpp │ │ │ ├── objects.hpp │ │ │ ├── pathgrid.cpp │ │ │ ├── pathgrid.hpp │ │ │ ├── recastmesh.cpp │ │ │ ├── recastmesh.hpp │ │ │ ├── renderbin.hpp │ │ │ ├── renderinginterface.hpp │ │ │ ├── renderingmanager.cpp │ │ │ ├── renderingmanager.hpp │ │ │ ├── rendermode.hpp │ │ │ ├── ripplesimulation.cpp │ │ │ ├── ripplesimulation.hpp │ │ │ ├── rotatecontroller.cpp │ │ │ ├── rotatecontroller.hpp │ │ │ ├── screenshotmanager.cpp │ │ │ ├── screenshotmanager.hpp │ │ │ ├── sky.cpp │ │ │ ├── sky.hpp │ │ │ ├── terrainstorage.cpp │ │ │ ├── terrainstorage.hpp │ │ │ ├── util.cpp │ │ │ ├── util.hpp │ │ │ ├── viewovershoulder.cpp │ │ │ ├── viewovershoulder.hpp │ │ │ ├── vismask.hpp │ │ │ ├── water.cpp │ │ │ ├── water.hpp │ │ │ ├── weaponanimation.cpp │ │ │ └── weaponanimation.hpp │ │ ├── mwscript/ │ │ │ ├── aiextensions.cpp │ │ │ ├── aiextensions.hpp │ │ │ ├── animationextensions.cpp │ │ │ ├── animationextensions.hpp │ │ │ ├── cellextensions.cpp │ │ │ ├── cellextensions.hpp │ │ │ ├── compilercontext.cpp │ │ │ ├── compilercontext.hpp │ │ │ ├── consoleextensions.cpp │ │ │ ├── consoleextensions.hpp │ │ │ ├── containerextensions.cpp │ │ │ ├── containerextensions.hpp │ │ │ ├── controlextensions.cpp │ │ │ ├── controlextensions.hpp │ │ │ ├── dialogueextensions.cpp │ │ │ ├── dialogueextensions.hpp │ │ │ ├── docs/ │ │ │ │ └── vmformat.txt │ │ │ ├── extensions.cpp │ │ │ ├── extensions.hpp │ │ │ ├── globalscripts.cpp │ │ │ ├── globalscripts.hpp │ │ │ ├── guiextensions.cpp │ │ │ ├── guiextensions.hpp │ │ │ ├── interpretercontext.cpp │ │ │ ├── interpretercontext.hpp │ │ │ ├── locals.cpp │ │ │ ├── locals.hpp │ │ │ ├── miscextensions.cpp │ │ │ ├── miscextensions.hpp │ │ │ ├── ref.cpp │ │ │ ├── ref.hpp │ │ │ ├── scriptmanagerimp.cpp │ │ │ ├── scriptmanagerimp.hpp │ │ │ ├── skyextensions.cpp │ │ │ ├── skyextensions.hpp │ │ │ ├── soundextensions.cpp │ │ │ ├── soundextensions.hpp │ │ │ ├── statsextensions.cpp │ │ │ ├── statsextensions.hpp │ │ │ ├── transformationextensions.cpp │ │ │ ├── transformationextensions.hpp │ │ │ ├── userextensions.cpp │ │ │ └── userextensions.hpp │ │ ├── mwsound/ │ │ │ ├── alext.h │ │ │ ├── efx-presets.h │ │ │ ├── efx.h │ │ │ ├── ffmpeg_decoder.cpp │ │ │ ├── ffmpeg_decoder.hpp │ │ │ ├── loudness.cpp │ │ │ ├── loudness.hpp │ │ │ ├── movieaudiofactory.cpp │ │ │ ├── movieaudiofactory.hpp │ │ │ ├── openal_output.cpp │ │ │ ├── openal_output.hpp │ │ │ ├── regionsoundselector.cpp │ │ │ ├── regionsoundselector.hpp │ │ │ ├── sound.hpp │ │ │ ├── sound_buffer.cpp │ │ │ ├── sound_buffer.hpp │ │ │ ├── sound_decoder.hpp │ │ │ ├── sound_output.hpp │ │ │ ├── soundmanagerimp.cpp │ │ │ ├── soundmanagerimp.hpp │ │ │ ├── type.hpp │ │ │ ├── volumesettings.cpp │ │ │ ├── volumesettings.hpp │ │ │ ├── watersoundupdater.cpp │ │ │ └── watersoundupdater.hpp │ │ ├── mwstate/ │ │ │ ├── character.cpp │ │ │ ├── character.hpp │ │ │ ├── charactermanager.cpp │ │ │ ├── charactermanager.hpp │ │ │ ├── quicksavemanager.cpp │ │ │ ├── quicksavemanager.hpp │ │ │ ├── statemanagerimp.cpp │ │ │ └── statemanagerimp.hpp │ │ └── mwworld/ │ │ ├── action.cpp │ │ ├── action.hpp │ │ ├── actionalchemy.cpp │ │ ├── actionalchemy.hpp │ │ ├── actionapply.cpp │ │ ├── actionapply.hpp │ │ ├── actiondoor.cpp │ │ ├── actiondoor.hpp │ │ ├── actioneat.cpp │ │ ├── actioneat.hpp │ │ ├── actionequip.cpp │ │ ├── actionequip.hpp │ │ ├── actionharvest.cpp │ │ ├── actionharvest.hpp │ │ ├── actionopen.cpp │ │ ├── actionopen.hpp │ │ ├── actionread.cpp │ │ ├── actionread.hpp │ │ ├── actionrepair.cpp │ │ ├── actionrepair.hpp │ │ ├── actionsoulgem.cpp │ │ ├── actionsoulgem.hpp │ │ ├── actiontake.cpp │ │ ├── actiontake.hpp │ │ ├── actiontalk.cpp │ │ ├── actiontalk.hpp │ │ ├── actionteleport.cpp │ │ ├── actionteleport.hpp │ │ ├── actiontrap.cpp │ │ ├── actiontrap.hpp │ │ ├── cellpreloader.cpp │ │ ├── cellpreloader.hpp │ │ ├── cellref.cpp │ │ ├── cellref.hpp │ │ ├── cellreflist.hpp │ │ ├── cells.cpp │ │ ├── cells.hpp │ │ ├── cellstore.cpp │ │ ├── cellstore.hpp │ │ ├── cellvisitors.hpp │ │ ├── class.cpp │ │ ├── class.hpp │ │ ├── containerstore.cpp │ │ ├── containerstore.hpp │ │ ├── contentloader.hpp │ │ ├── customdata.cpp │ │ ├── customdata.hpp │ │ ├── datetimemanager.cpp │ │ ├── datetimemanager.hpp │ │ ├── doorstate.hpp │ │ ├── esmloader.cpp │ │ ├── esmloader.hpp │ │ ├── esmstore.cpp │ │ ├── esmstore.hpp │ │ ├── failedaction.cpp │ │ ├── failedaction.hpp │ │ ├── globals.cpp │ │ ├── globals.hpp │ │ ├── inventorystore.cpp │ │ ├── inventorystore.hpp │ │ ├── livecellref.cpp │ │ ├── livecellref.hpp │ │ ├── localscripts.cpp │ │ ├── localscripts.hpp │ │ ├── manualref.cpp │ │ ├── manualref.hpp │ │ ├── nullaction.hpp │ │ ├── player.cpp │ │ ├── player.hpp │ │ ├── projectilemanager.cpp │ │ ├── projectilemanager.hpp │ │ ├── ptr.cpp │ │ ├── ptr.hpp │ │ ├── recordcmp.hpp │ │ ├── refdata.cpp │ │ ├── refdata.hpp │ │ ├── scene.cpp │ │ ├── scene.hpp │ │ ├── store.cpp │ │ ├── store.hpp │ │ ├── timestamp.cpp │ │ ├── timestamp.hpp │ │ ├── weather.cpp │ │ ├── weather.hpp │ │ ├── worldimp.cpp │ │ └── worldimp.hpp │ ├── openmw-mp/ │ │ ├── CMakeLists.txt │ │ ├── Cell.cpp │ │ ├── Cell.hpp │ │ ├── CellController.cpp │ │ ├── CellController.hpp │ │ ├── MasterClient.cpp │ │ ├── MasterClient.hpp │ │ ├── Networking.cpp │ │ ├── Networking.hpp │ │ ├── Player.cpp │ │ ├── Player.hpp │ │ ├── Script/ │ │ │ ├── API/ │ │ │ │ ├── PublicFnAPI.cpp │ │ │ │ ├── PublicFnAPI.hpp │ │ │ │ ├── TimerAPI.cpp │ │ │ │ └── TimerAPI.hpp │ │ │ ├── Functions/ │ │ │ │ ├── Actors.cpp │ │ │ │ ├── Actors.hpp │ │ │ │ ├── Books.cpp │ │ │ │ ├── Books.hpp │ │ │ │ ├── Cells.cpp │ │ │ │ ├── Cells.hpp │ │ │ │ ├── CharClass.cpp │ │ │ │ ├── CharClass.hpp │ │ │ │ ├── Chat.cpp │ │ │ │ ├── Chat.hpp │ │ │ │ ├── Dialogue.cpp │ │ │ │ ├── Dialogue.hpp │ │ │ │ ├── Factions.cpp │ │ │ │ ├── Factions.hpp │ │ │ │ ├── GUI.cpp │ │ │ │ ├── GUI.hpp │ │ │ │ ├── Items.cpp │ │ │ │ ├── Items.hpp │ │ │ │ ├── Mechanics.cpp │ │ │ │ ├── Mechanics.hpp │ │ │ │ ├── Miscellaneous.cpp │ │ │ │ ├── Miscellaneous.hpp │ │ │ │ ├── Objects.cpp │ │ │ │ ├── Objects.hpp │ │ │ │ ├── Positions.cpp │ │ │ │ ├── Positions.hpp │ │ │ │ ├── Quests.cpp │ │ │ │ ├── Quests.hpp │ │ │ │ ├── RecordsDynamic.cpp │ │ │ │ ├── RecordsDynamic.hpp │ │ │ │ ├── Server.cpp │ │ │ │ ├── Server.hpp │ │ │ │ ├── Settings.cpp │ │ │ │ ├── Settings.hpp │ │ │ │ ├── Shapeshift.cpp │ │ │ │ ├── Shapeshift.hpp │ │ │ │ ├── Spells.cpp │ │ │ │ ├── Spells.hpp │ │ │ │ ├── Stats.cpp │ │ │ │ ├── Stats.hpp │ │ │ │ ├── Timer.cpp │ │ │ │ ├── Worldstate.cpp │ │ │ │ └── Worldstate.hpp │ │ │ ├── LangLua/ │ │ │ │ ├── LangLua.cpp │ │ │ │ ├── LangLua.hpp │ │ │ │ └── LuaFunc.cpp │ │ │ ├── LangNative/ │ │ │ │ ├── LangNative.cpp │ │ │ │ └── LangNative.hpp │ │ │ ├── Language.hpp │ │ │ ├── Platform.hpp │ │ │ ├── Script.cpp │ │ │ ├── Script.hpp │ │ │ ├── ScriptFunction.cpp │ │ │ ├── ScriptFunction.hpp │ │ │ ├── ScriptFunctions.cpp │ │ │ ├── ScriptFunctions.hpp │ │ │ ├── SystemInterface.hpp │ │ │ └── Types.hpp │ │ ├── Utils.cpp │ │ ├── Utils.hpp │ │ ├── handleInput.cpp │ │ ├── main.cpp │ │ └── processors/ │ │ ├── ActorProcessor.cpp │ │ ├── ActorProcessor.hpp │ │ ├── ObjectProcessor.cpp │ │ ├── ObjectProcessor.hpp │ │ ├── PlayerProcessor.cpp │ │ ├── PlayerProcessor.hpp │ │ ├── ProcessorInitializer.cpp │ │ ├── ProcessorInitializer.hpp │ │ ├── WorldstateProcessor.cpp │ │ ├── WorldstateProcessor.hpp │ │ ├── actor/ │ │ │ ├── ProcessorActorAI.hpp │ │ │ ├── ProcessorActorAnimFlags.hpp │ │ │ ├── ProcessorActorAnimPlay.hpp │ │ │ ├── ProcessorActorAttack.hpp │ │ │ ├── ProcessorActorCast.hpp │ │ │ ├── ProcessorActorCellChange.hpp │ │ │ ├── ProcessorActorDeath.hpp │ │ │ ├── ProcessorActorEquipment.hpp │ │ │ ├── ProcessorActorList.hpp │ │ │ ├── ProcessorActorPosition.hpp │ │ │ ├── ProcessorActorSpeech.hpp │ │ │ ├── ProcessorActorSpellsActive.hpp │ │ │ ├── ProcessorActorStatsDynamic.hpp │ │ │ └── ProcessorActorTest.hpp │ │ ├── object/ │ │ │ ├── ProcessorClientScriptLocal.hpp │ │ │ ├── ProcessorConsoleCommand.hpp │ │ │ ├── ProcessorContainer.hpp │ │ │ ├── ProcessorDoorState.hpp │ │ │ ├── ProcessorMusicPlay.hpp │ │ │ ├── ProcessorObjectActivate.hpp │ │ │ ├── ProcessorObjectAnimPlay.hpp │ │ │ ├── ProcessorObjectDelete.hpp │ │ │ ├── ProcessorObjectDialogueChoice.hpp │ │ │ ├── ProcessorObjectHit.hpp │ │ │ ├── ProcessorObjectLock.hpp │ │ │ ├── ProcessorObjectMiscellaneous.hpp │ │ │ ├── ProcessorObjectMove.hpp │ │ │ ├── ProcessorObjectPlace.hpp │ │ │ ├── ProcessorObjectRestock.hpp │ │ │ ├── ProcessorObjectRotate.hpp │ │ │ ├── ProcessorObjectScale.hpp │ │ │ ├── ProcessorObjectSound.hpp │ │ │ ├── ProcessorObjectSpawn.hpp │ │ │ ├── ProcessorObjectState.hpp │ │ │ ├── ProcessorObjectTrap.hpp │ │ │ ├── ProcessorScriptMemberShort.hpp │ │ │ └── ProcessorVideoPlay.hpp │ │ ├── player/ │ │ │ ├── ProcessorChatMsg.hpp │ │ │ ├── ProcessorGUIMessageBox.hpp │ │ │ ├── ProcessorPlayerAnimFlags.hpp │ │ │ ├── ProcessorPlayerAnimPlay.hpp │ │ │ ├── ProcessorPlayerAttack.hpp │ │ │ ├── ProcessorPlayerAttribute.hpp │ │ │ ├── ProcessorPlayerBook.hpp │ │ │ ├── ProcessorPlayerBounty.hpp │ │ │ ├── ProcessorPlayerCast.hpp │ │ │ ├── ProcessorPlayerCellChange.hpp │ │ │ ├── ProcessorPlayerCellState.hpp │ │ │ ├── ProcessorPlayerCharClass.hpp │ │ │ ├── ProcessorPlayerCharGen.hpp │ │ │ ├── ProcessorPlayerCooldowns.hpp │ │ │ ├── ProcessorPlayerDeath.hpp │ │ │ ├── ProcessorPlayerDisposition.hpp │ │ │ ├── ProcessorPlayerEquipment.hpp │ │ │ ├── ProcessorPlayerFaction.hpp │ │ │ ├── ProcessorPlayerInput.hpp │ │ │ ├── ProcessorPlayerInventory.hpp │ │ │ ├── ProcessorPlayerItemUse.hpp │ │ │ ├── ProcessorPlayerJournal.hpp │ │ │ ├── ProcessorPlayerLevel.hpp │ │ │ ├── ProcessorPlayerMiscellaneous.hpp │ │ │ ├── ProcessorPlayerPlaceholder.hpp │ │ │ ├── ProcessorPlayerPosition.hpp │ │ │ ├── ProcessorPlayerQuickKeys.hpp │ │ │ ├── ProcessorPlayerReputation.hpp │ │ │ ├── ProcessorPlayerRest.hpp │ │ │ ├── ProcessorPlayerResurrect.hpp │ │ │ ├── ProcessorPlayerShapeshift.hpp │ │ │ ├── ProcessorPlayerSkill.hpp │ │ │ ├── ProcessorPlayerSpeech.hpp │ │ │ ├── ProcessorPlayerSpellbook.hpp │ │ │ ├── ProcessorPlayerSpellsActive.hpp │ │ │ ├── ProcessorPlayerStatsDynamic.hpp │ │ │ └── ProcessorPlayerTopic.hpp │ │ └── worldstate/ │ │ ├── ProcessorClientScriptGlobal.hpp │ │ ├── ProcessorRecordDynamic.hpp │ │ ├── ProcessorWorldKillCount.hpp │ │ ├── ProcessorWorldMap.hpp │ │ └── ProcessorWorldWeather.hpp │ ├── openmw_test_suite/ │ │ ├── CMakeLists.txt │ │ ├── detournavigator/ │ │ │ ├── gettilespositions.cpp │ │ │ ├── navigator.cpp │ │ │ ├── navmeshtilescache.cpp │ │ │ ├── operators.hpp │ │ │ ├── recastmeshbuilder.cpp │ │ │ ├── recastmeshobject.cpp │ │ │ ├── settingsutils.cpp │ │ │ └── tilecachedrecastmeshmanager.cpp │ │ ├── esm/ │ │ │ ├── test_fixed_string.cpp │ │ │ └── variant.cpp │ │ ├── misc/ │ │ │ ├── test_endianness.cpp │ │ │ └── test_stringops.cpp │ │ ├── mwdialogue/ │ │ │ └── test_keywordsearch.cpp │ │ ├── mwworld/ │ │ │ └── test_store.cpp │ │ ├── nifloader/ │ │ │ └── testbulletnifloader.cpp │ │ ├── openmw_test_suite.cpp │ │ ├── settings/ │ │ │ └── parser.cpp │ │ └── shader/ │ │ ├── parsedefines.cpp │ │ ├── parsefors.cpp │ │ └── shadermanager.cpp │ └── wizard/ │ ├── CMakeLists.txt │ ├── componentselectionpage.cpp │ ├── componentselectionpage.hpp │ ├── conclusionpage.cpp │ ├── conclusionpage.hpp │ ├── existinginstallationpage.cpp │ ├── existinginstallationpage.hpp │ ├── importpage.cpp │ ├── importpage.hpp │ ├── inisettings.cpp │ ├── inisettings.hpp │ ├── installationpage.cpp │ ├── installationpage.hpp │ ├── installationtargetpage.cpp │ ├── installationtargetpage.hpp │ ├── intropage.cpp │ ├── intropage.hpp │ ├── languageselectionpage.cpp │ ├── languageselectionpage.hpp │ ├── main.cpp │ ├── mainwizard.cpp │ ├── mainwizard.hpp │ ├── methodselectionpage.cpp │ ├── methodselectionpage.hpp │ ├── unshield/ │ │ ├── unshieldworker.cpp │ │ └── unshieldworker.hpp │ └── utils/ │ ├── componentlistwidget.cpp │ └── componentlistwidget.hpp ├── appveyor.yml ├── cmake/ │ ├── COPYING-CMAKE-SCRIPTS │ ├── CheckBulletPrecision.cmake │ ├── FindCallFF.cmake │ ├── FindFFmpeg.cmake │ ├── FindGMock.cmake │ ├── FindLIBUNSHIELD.cmake │ ├── FindLZ4.cmake │ ├── FindLuaJit.cmake │ ├── FindMyGUI.cmake │ ├── FindOSGPlugins.cmake │ ├── FindRakNet.cmake │ ├── FindRecastNavigation.cmake │ ├── FindSDL2.cmake │ ├── FindSphinx.cmake │ ├── FindTinyXML.cmake │ ├── GitVersion.cmake │ ├── LibFindMacros.cmake │ ├── OpenMWMacros.cmake │ ├── WholeArchive.cmake │ └── base64.cmake ├── components/ │ ├── CMakeLists.txt │ ├── bsa/ │ │ ├── bsa_file.cpp │ │ ├── bsa_file.hpp │ │ ├── compressedbsafile.cpp │ │ ├── compressedbsafile.hpp │ │ ├── memorystream.cpp │ │ └── memorystream.hpp │ ├── bullethelpers/ │ │ ├── aabb.hpp │ │ ├── operators.hpp │ │ ├── processtrianglecallback.hpp │ │ └── transformboundingbox.hpp │ ├── compiler/ │ │ ├── context.hpp │ │ ├── controlparser.cpp │ │ ├── controlparser.hpp │ │ ├── declarationparser.cpp │ │ ├── declarationparser.hpp │ │ ├── discardparser.cpp │ │ ├── discardparser.hpp │ │ ├── errorhandler.cpp │ │ ├── errorhandler.hpp │ │ ├── exception.hpp │ │ ├── exprparser.cpp │ │ ├── exprparser.hpp │ │ ├── extensions.cpp │ │ ├── extensions.hpp │ │ ├── extensions0.cpp │ │ ├── extensions0.hpp │ │ ├── fileparser.cpp │ │ ├── fileparser.hpp │ │ ├── generator.cpp │ │ ├── generator.hpp │ │ ├── junkparser.cpp │ │ ├── junkparser.hpp │ │ ├── lineparser.cpp │ │ ├── lineparser.hpp │ │ ├── literals.cpp │ │ ├── literals.hpp │ │ ├── locals.cpp │ │ ├── locals.hpp │ │ ├── nullerrorhandler.cpp │ │ ├── nullerrorhandler.hpp │ │ ├── opcodes.cpp │ │ ├── opcodes.hpp │ │ ├── output.cpp │ │ ├── output.hpp │ │ ├── parser.cpp │ │ ├── parser.hpp │ │ ├── quickfileparser.cpp │ │ ├── quickfileparser.hpp │ │ ├── scanner.cpp │ │ ├── scanner.hpp │ │ ├── scriptparser.cpp │ │ ├── scriptparser.hpp │ │ ├── skipparser.cpp │ │ ├── skipparser.hpp │ │ ├── streamerrorhandler.cpp │ │ ├── streamerrorhandler.hpp │ │ ├── stringparser.cpp │ │ ├── stringparser.hpp │ │ └── tokenloc.hpp │ ├── config/ │ │ ├── gamesettings.cpp │ │ ├── gamesettings.hpp │ │ ├── launchersettings.cpp │ │ ├── launchersettings.hpp │ │ └── settingsbase.hpp │ ├── contentselector/ │ │ ├── model/ │ │ │ ├── contentmodel.cpp │ │ │ ├── contentmodel.hpp │ │ │ ├── esmfile.cpp │ │ │ ├── esmfile.hpp │ │ │ ├── loadordererror.cpp │ │ │ ├── loadordererror.hpp │ │ │ ├── modelitem.cpp │ │ │ ├── modelitem.hpp │ │ │ ├── naturalsort.cpp │ │ │ └── naturalsort.hpp │ │ └── view/ │ │ ├── combobox.cpp │ │ ├── combobox.hpp │ │ ├── contentselector.cpp │ │ └── contentselector.hpp │ ├── crashcatcher/ │ │ ├── crashcatcher.cpp │ │ ├── crashcatcher.hpp │ │ ├── windows_crashcatcher.cpp │ │ ├── windows_crashcatcher.hpp │ │ ├── windows_crashmonitor.cpp │ │ ├── windows_crashmonitor.hpp │ │ └── windows_crashshm.hpp │ ├── debug/ │ │ ├── debugging.cpp │ │ ├── debugging.hpp │ │ ├── debuglog.cpp │ │ ├── debuglog.hpp │ │ ├── gldebug.cpp │ │ └── gldebug.hpp │ ├── detournavigator/ │ │ ├── areatype.hpp │ │ ├── asyncnavmeshupdater.cpp │ │ ├── asyncnavmeshupdater.hpp │ │ ├── bounds.hpp │ │ ├── cachedrecastmeshmanager.cpp │ │ ├── cachedrecastmeshmanager.hpp │ │ ├── debug.cpp │ │ ├── debug.hpp │ │ ├── dtstatus.hpp │ │ ├── exceptions.hpp │ │ ├── findrandompointaroundcircle.cpp │ │ ├── findrandompointaroundcircle.hpp │ │ ├── findsmoothpath.cpp │ │ ├── findsmoothpath.hpp │ │ ├── flags.hpp │ │ ├── gettilespositions.hpp │ │ ├── makenavmesh.cpp │ │ ├── makenavmesh.hpp │ │ ├── navigator.cpp │ │ ├── navigator.hpp │ │ ├── navigatorimpl.cpp │ │ ├── navigatorimpl.hpp │ │ ├── navigatorstub.hpp │ │ ├── navmeshcacheitem.hpp │ │ ├── navmeshdata.hpp │ │ ├── navmeshmanager.cpp │ │ ├── navmeshmanager.hpp │ │ ├── navmeshtilescache.cpp │ │ ├── navmeshtilescache.hpp │ │ ├── navmeshtileview.cpp │ │ ├── navmeshtileview.hpp │ │ ├── objectid.hpp │ │ ├── offmeshconnection.hpp │ │ ├── offmeshconnectionsmanager.cpp │ │ ├── offmeshconnectionsmanager.hpp │ │ ├── oscillatingrecastmeshobject.cpp │ │ ├── oscillatingrecastmeshobject.hpp │ │ ├── raycast.cpp │ │ ├── raycast.hpp │ │ ├── recastallocutils.hpp │ │ ├── recastglobalallocator.hpp │ │ ├── recastmesh.cpp │ │ ├── recastmesh.hpp │ │ ├── recastmeshbuilder.cpp │ │ ├── recastmeshbuilder.hpp │ │ ├── recastmeshmanager.cpp │ │ ├── recastmeshmanager.hpp │ │ ├── recastmeshobject.cpp │ │ ├── recastmeshobject.hpp │ │ ├── recastmeshtiles.hpp │ │ ├── recasttempallocator.hpp │ │ ├── settings.cpp │ │ ├── settings.hpp │ │ ├── settingsutils.hpp │ │ ├── sharednavmesh.hpp │ │ ├── status.hpp │ │ ├── tilebounds.hpp │ │ ├── tilecachedrecastmeshmanager.cpp │ │ ├── tilecachedrecastmeshmanager.hpp │ │ ├── tileposition.hpp │ │ ├── version.hpp │ │ └── waitconditiontype.hpp │ ├── doc.hpp │ ├── esm/ │ │ ├── activespells.cpp │ │ ├── activespells.hpp │ │ ├── aipackage.cpp │ │ ├── aipackage.hpp │ │ ├── aisequence.cpp │ │ ├── aisequence.hpp │ │ ├── animationstate.cpp │ │ ├── animationstate.hpp │ │ ├── attr.cpp │ │ ├── attr.hpp │ │ ├── cellid.cpp │ │ ├── cellid.hpp │ │ ├── cellref.cpp │ │ ├── cellref.hpp │ │ ├── cellstate.cpp │ │ ├── cellstate.hpp │ │ ├── containerstate.cpp │ │ ├── containerstate.hpp │ │ ├── controlsstate.cpp │ │ ├── controlsstate.hpp │ │ ├── creaturelevliststate.cpp │ │ ├── creaturelevliststate.hpp │ │ ├── creaturestate.cpp │ │ ├── creaturestate.hpp │ │ ├── creaturestats.cpp │ │ ├── creaturestats.hpp │ │ ├── custommarkerstate.cpp │ │ ├── custommarkerstate.hpp │ │ ├── debugprofile.cpp │ │ ├── debugprofile.hpp │ │ ├── defs.hpp │ │ ├── dialoguestate.cpp │ │ ├── dialoguestate.hpp │ │ ├── doorstate.cpp │ │ ├── doorstate.hpp │ │ ├── effectlist.cpp │ │ ├── effectlist.hpp │ │ ├── esmcommon.hpp │ │ ├── esmreader.cpp │ │ ├── esmreader.hpp │ │ ├── esmwriter.cpp │ │ ├── esmwriter.hpp │ │ ├── filter.cpp │ │ ├── filter.hpp │ │ ├── fogstate.cpp │ │ ├── fogstate.hpp │ │ ├── globalmap.cpp │ │ ├── globalmap.hpp │ │ ├── globalscript.cpp │ │ ├── globalscript.hpp │ │ ├── inventorystate.cpp │ │ ├── inventorystate.hpp │ │ ├── journalentry.cpp │ │ ├── journalentry.hpp │ │ ├── loadacti.cpp │ │ ├── loadacti.hpp │ │ ├── loadalch.cpp │ │ ├── loadalch.hpp │ │ ├── loadappa.cpp │ │ ├── loadappa.hpp │ │ ├── loadarmo.cpp │ │ ├── loadarmo.hpp │ │ ├── loadbody.cpp │ │ ├── loadbody.hpp │ │ ├── loadbook.cpp │ │ ├── loadbook.hpp │ │ ├── loadbsgn.cpp │ │ ├── loadbsgn.hpp │ │ ├── loadcell.cpp │ │ ├── loadcell.hpp │ │ ├── loadclas.cpp │ │ ├── loadclas.hpp │ │ ├── loadclot.cpp │ │ ├── loadclot.hpp │ │ ├── loadcont.cpp │ │ ├── loadcont.hpp │ │ ├── loadcrea.cpp │ │ ├── loadcrea.hpp │ │ ├── loaddial.cpp │ │ ├── loaddial.hpp │ │ ├── loaddoor.cpp │ │ ├── loaddoor.hpp │ │ ├── loadench.cpp │ │ ├── loadench.hpp │ │ ├── loadfact.cpp │ │ ├── loadfact.hpp │ │ ├── loadglob.cpp │ │ ├── loadglob.hpp │ │ ├── loadgmst.cpp │ │ ├── loadgmst.hpp │ │ ├── loadinfo.cpp │ │ ├── loadinfo.hpp │ │ ├── loadingr.cpp │ │ ├── loadingr.hpp │ │ ├── loadland.cpp │ │ ├── loadland.hpp │ │ ├── loadlevlist.cpp │ │ ├── loadlevlist.hpp │ │ ├── loadligh.cpp │ │ ├── loadligh.hpp │ │ ├── loadlock.cpp │ │ ├── loadlock.hpp │ │ ├── loadltex.cpp │ │ ├── loadltex.hpp │ │ ├── loadmgef.cpp │ │ ├── loadmgef.hpp │ │ ├── loadmisc.cpp │ │ ├── loadmisc.hpp │ │ ├── loadnpc.cpp │ │ ├── loadnpc.hpp │ │ ├── loadpgrd.cpp │ │ ├── loadpgrd.hpp │ │ ├── loadprob.cpp │ │ ├── loadprob.hpp │ │ ├── loadrace.cpp │ │ ├── loadrace.hpp │ │ ├── loadregn.cpp │ │ ├── loadregn.hpp │ │ ├── loadrepa.cpp │ │ ├── loadrepa.hpp │ │ ├── loadscpt.cpp │ │ ├── loadscpt.hpp │ │ ├── loadskil.cpp │ │ ├── loadskil.hpp │ │ ├── loadsndg.cpp │ │ ├── loadsndg.hpp │ │ ├── loadsoun.cpp │ │ ├── loadsoun.hpp │ │ ├── loadspel.cpp │ │ ├── loadspel.hpp │ │ ├── loadsscr.cpp │ │ ├── loadsscr.hpp │ │ ├── loadstat.cpp │ │ ├── loadstat.hpp │ │ ├── loadtes3.cpp │ │ ├── loadtes3.hpp │ │ ├── loadweap.cpp │ │ ├── loadweap.hpp │ │ ├── locals.cpp │ │ ├── locals.hpp │ │ ├── magiceffects.cpp │ │ ├── magiceffects.hpp │ │ ├── mappings.cpp │ │ ├── mappings.hpp │ │ ├── npcstate.cpp │ │ ├── npcstate.hpp │ │ ├── npcstats.cpp │ │ ├── npcstats.hpp │ │ ├── objectstate.cpp │ │ ├── objectstate.hpp │ │ ├── player.cpp │ │ ├── player.hpp │ │ ├── projectilestate.cpp │ │ ├── projectilestate.hpp │ │ ├── queststate.cpp │ │ ├── queststate.hpp │ │ ├── quickkeys.cpp │ │ ├── quickkeys.hpp │ │ ├── records.hpp │ │ ├── savedgame.cpp │ │ ├── savedgame.hpp │ │ ├── spelllist.cpp │ │ ├── spelllist.hpp │ │ ├── spellstate.cpp │ │ ├── spellstate.hpp │ │ ├── statstate.cpp │ │ ├── statstate.hpp │ │ ├── stolenitems.cpp │ │ ├── stolenitems.hpp │ │ ├── transport.cpp │ │ ├── transport.hpp │ │ ├── util.hpp │ │ ├── variant.cpp │ │ ├── variant.hpp │ │ ├── variantimp.cpp │ │ ├── variantimp.hpp │ │ ├── weatherstate.cpp │ │ └── weatherstate.hpp │ ├── esmterrain/ │ │ ├── storage.cpp │ │ └── storage.hpp │ ├── fallback/ │ │ ├── fallback.cpp │ │ ├── fallback.hpp │ │ ├── validate.cpp │ │ └── validate.hpp │ ├── files/ │ │ ├── androidpath.cpp │ │ ├── androidpath.hpp │ │ ├── collections.cpp │ │ ├── collections.hpp │ │ ├── configurationmanager.cpp │ │ ├── configurationmanager.hpp │ │ ├── constrainedfilestream.cpp │ │ ├── constrainedfilestream.hpp │ │ ├── escape.cpp │ │ ├── escape.hpp │ │ ├── fixedpath.hpp │ │ ├── linuxpath.cpp │ │ ├── linuxpath.hpp │ │ ├── lowlevelfile.cpp │ │ ├── lowlevelfile.hpp │ │ ├── macospath.cpp │ │ ├── macospath.hpp │ │ ├── memorystream.hpp │ │ ├── multidircollection.cpp │ │ ├── multidircollection.hpp │ │ ├── windowspath.cpp │ │ └── windowspath.hpp │ ├── fontloader/ │ │ ├── fontloader.cpp │ │ └── fontloader.hpp │ ├── interpreter/ │ │ ├── context.hpp │ │ ├── controlopcodes.hpp │ │ ├── defines.cpp │ │ ├── defines.hpp │ │ ├── docs/ │ │ │ └── vmformat.txt │ │ ├── genericopcodes.hpp │ │ ├── installopcodes.cpp │ │ ├── installopcodes.hpp │ │ ├── interpreter.cpp │ │ ├── interpreter.hpp │ │ ├── localopcodes.hpp │ │ ├── mathopcodes.hpp │ │ ├── miscopcodes.hpp │ │ ├── opcodes.hpp │ │ ├── runtime.cpp │ │ ├── runtime.hpp │ │ └── types.hpp │ ├── loadinglistener/ │ │ └── loadinglistener.hpp │ ├── misc/ │ │ ├── algorithm.hpp │ │ ├── barrier.hpp │ │ ├── budgetmeasurement.hpp │ │ ├── constants.hpp │ │ ├── convert.hpp │ │ ├── coordinateconverter.hpp │ │ ├── debugging.hpp │ │ ├── endianness.hpp │ │ ├── frameratelimiter.hpp │ │ ├── guarded.hpp │ │ ├── hash.hpp │ │ ├── helpviewer.cpp │ │ ├── helpviewer.hpp │ │ ├── mathutil.hpp │ │ ├── messageformatparser.cpp │ │ ├── messageformatparser.hpp │ │ ├── objectpool.hpp │ │ ├── resourcehelpers.cpp │ │ ├── resourcehelpers.hpp │ │ ├── rng.cpp │ │ ├── rng.hpp │ │ ├── stringops.hpp │ │ ├── thread.cpp │ │ ├── thread.hpp │ │ ├── timer.hpp │ │ ├── utf8stream.hpp │ │ └── weakcache.hpp │ ├── myguiplatform/ │ │ ├── additivelayer.cpp │ │ ├── additivelayer.hpp │ │ ├── myguicompat.h │ │ ├── myguidatamanager.cpp │ │ ├── myguidatamanager.hpp │ │ ├── myguiloglistener.cpp │ │ ├── myguiloglistener.hpp │ │ ├── myguiplatform.cpp │ │ ├── myguiplatform.hpp │ │ ├── myguirendermanager.cpp │ │ ├── myguirendermanager.hpp │ │ ├── myguitexture.cpp │ │ ├── myguitexture.hpp │ │ ├── scalinglayer.cpp │ │ └── scalinglayer.hpp │ ├── nif/ │ │ ├── base.hpp │ │ ├── controlled.cpp │ │ ├── controlled.hpp │ │ ├── controller.cpp │ │ ├── controller.hpp │ │ ├── data.cpp │ │ ├── data.hpp │ │ ├── effect.cpp │ │ ├── effect.hpp │ │ ├── extra.cpp │ │ ├── extra.hpp │ │ ├── niffile.cpp │ │ ├── niffile.hpp │ │ ├── nifkey.hpp │ │ ├── nifstream.cpp │ │ ├── nifstream.hpp │ │ ├── niftypes.hpp │ │ ├── node.cpp │ │ ├── node.hpp │ │ ├── property.cpp │ │ ├── property.hpp │ │ ├── record.hpp │ │ └── recordptr.hpp │ ├── nifbullet/ │ │ ├── bulletnifloader.cpp │ │ └── bulletnifloader.hpp │ ├── nifosg/ │ │ ├── controller.cpp │ │ ├── controller.hpp │ │ ├── matrixtransform.cpp │ │ ├── matrixtransform.hpp │ │ ├── nifloader.cpp │ │ ├── nifloader.hpp │ │ ├── particle.cpp │ │ └── particle.hpp │ ├── openmw-mp/ │ │ ├── Base/ │ │ │ ├── BaseActor.hpp │ │ │ ├── BaseObject.hpp │ │ │ ├── BasePacketProcessor.hpp │ │ │ ├── BasePlayer.hpp │ │ │ ├── BaseStructs.hpp │ │ │ ├── BaseSystem.hpp │ │ │ └── BaseWorldstate.hpp │ │ ├── Controllers/ │ │ │ ├── ActorPacketController.cpp │ │ │ ├── ActorPacketController.hpp │ │ │ ├── ObjectPacketController.cpp │ │ │ ├── ObjectPacketController.hpp │ │ │ ├── PlayerPacketController.cpp │ │ │ ├── PlayerPacketController.hpp │ │ │ ├── SystemPacketController.cpp │ │ │ ├── SystemPacketController.hpp │ │ │ ├── WorldstatePacketController.cpp │ │ │ └── WorldstatePacketController.hpp │ │ ├── ErrorMessages.hpp │ │ ├── Master/ │ │ │ ├── MasterData.hpp │ │ │ ├── PacketMasterAnnounce.cpp │ │ │ ├── PacketMasterAnnounce.hpp │ │ │ ├── PacketMasterQuery.cpp │ │ │ ├── PacketMasterQuery.hpp │ │ │ ├── PacketMasterUpdate.cpp │ │ │ ├── PacketMasterUpdate.hpp │ │ │ └── ProxyMasterPacket.hpp │ │ ├── NetworkMessages.hpp │ │ ├── Packets/ │ │ │ ├── Actor/ │ │ │ │ ├── ActorPacket.cpp │ │ │ │ ├── ActorPacket.hpp │ │ │ │ ├── PacketActorAI.cpp │ │ │ │ ├── PacketActorAI.hpp │ │ │ │ ├── PacketActorAnimFlags.cpp │ │ │ │ ├── PacketActorAnimFlags.hpp │ │ │ │ ├── PacketActorAnimPlay.cpp │ │ │ │ ├── PacketActorAnimPlay.hpp │ │ │ │ ├── PacketActorAttack.cpp │ │ │ │ ├── PacketActorAttack.hpp │ │ │ │ ├── PacketActorAuthority.cpp │ │ │ │ ├── PacketActorAuthority.hpp │ │ │ │ ├── PacketActorCast.cpp │ │ │ │ ├── PacketActorCast.hpp │ │ │ │ ├── PacketActorCellChange.cpp │ │ │ │ ├── PacketActorCellChange.hpp │ │ │ │ ├── PacketActorDeath.cpp │ │ │ │ ├── PacketActorDeath.hpp │ │ │ │ ├── PacketActorEquipment.cpp │ │ │ │ ├── PacketActorEquipment.hpp │ │ │ │ ├── PacketActorList.cpp │ │ │ │ ├── PacketActorList.hpp │ │ │ │ ├── PacketActorPosition.cpp │ │ │ │ ├── PacketActorPosition.hpp │ │ │ │ ├── PacketActorSpeech.cpp │ │ │ │ ├── PacketActorSpeech.hpp │ │ │ │ ├── PacketActorSpellsActive.cpp │ │ │ │ ├── PacketActorSpellsActive.hpp │ │ │ │ ├── PacketActorStatsDynamic.cpp │ │ │ │ ├── PacketActorStatsDynamic.hpp │ │ │ │ ├── PacketActorTest.cpp │ │ │ │ └── PacketActorTest.hpp │ │ │ ├── BasePacket.cpp │ │ │ ├── BasePacket.hpp │ │ │ ├── Object/ │ │ │ │ ├── ObjectPacket.cpp │ │ │ │ ├── ObjectPacket.hpp │ │ │ │ ├── PacketClientScriptLocal.cpp │ │ │ │ ├── PacketClientScriptLocal.hpp │ │ │ │ ├── PacketConsoleCommand.cpp │ │ │ │ ├── PacketConsoleCommand.hpp │ │ │ │ ├── PacketContainer.cpp │ │ │ │ ├── PacketContainer.hpp │ │ │ │ ├── PacketDoorDestination.cpp │ │ │ │ ├── PacketDoorDestination.hpp │ │ │ │ ├── PacketDoorState.cpp │ │ │ │ ├── PacketDoorState.hpp │ │ │ │ ├── PacketMusicPlay.cpp │ │ │ │ ├── PacketMusicPlay.hpp │ │ │ │ ├── PacketObjectActivate.cpp │ │ │ │ ├── PacketObjectActivate.hpp │ │ │ │ ├── PacketObjectAnimPlay.cpp │ │ │ │ ├── PacketObjectAnimPlay.hpp │ │ │ │ ├── PacketObjectAttach.cpp │ │ │ │ ├── PacketObjectAttach.hpp │ │ │ │ ├── PacketObjectDelete.cpp │ │ │ │ ├── PacketObjectDelete.hpp │ │ │ │ ├── PacketObjectDialogueChoice.cpp │ │ │ │ ├── PacketObjectDialogueChoice.hpp │ │ │ │ ├── PacketObjectHit.cpp │ │ │ │ ├── PacketObjectHit.hpp │ │ │ │ ├── PacketObjectLock.cpp │ │ │ │ ├── PacketObjectLock.hpp │ │ │ │ ├── PacketObjectMiscellaneous.cpp │ │ │ │ ├── PacketObjectMiscellaneous.hpp │ │ │ │ ├── PacketObjectMove.cpp │ │ │ │ ├── PacketObjectMove.hpp │ │ │ │ ├── PacketObjectPlace.cpp │ │ │ │ ├── PacketObjectPlace.hpp │ │ │ │ ├── PacketObjectRestock.cpp │ │ │ │ ├── PacketObjectRestock.hpp │ │ │ │ ├── PacketObjectRotate.cpp │ │ │ │ ├── PacketObjectRotate.hpp │ │ │ │ ├── PacketObjectScale.cpp │ │ │ │ ├── PacketObjectScale.hpp │ │ │ │ ├── PacketObjectSound.cpp │ │ │ │ ├── PacketObjectSound.hpp │ │ │ │ ├── PacketObjectSpawn.cpp │ │ │ │ ├── PacketObjectSpawn.hpp │ │ │ │ ├── PacketObjectState.cpp │ │ │ │ ├── PacketObjectState.hpp │ │ │ │ ├── PacketObjectTrap.cpp │ │ │ │ ├── PacketObjectTrap.hpp │ │ │ │ ├── PacketScriptMemberShort.cpp │ │ │ │ ├── PacketScriptMemberShort.hpp │ │ │ │ ├── PacketVideoPlay.cpp │ │ │ │ └── PacketVideoPlay.hpp │ │ │ ├── PacketPreInit.cpp │ │ │ ├── PacketPreInit.hpp │ │ │ ├── Player/ │ │ │ │ ├── PacketChatMessage.cpp │ │ │ │ ├── PacketChatMessage.hpp │ │ │ │ ├── PacketDisconnect.hpp │ │ │ │ ├── PacketGUIBoxes.cpp │ │ │ │ ├── PacketGUIBoxes.hpp │ │ │ │ ├── PacketGameSettings.cpp │ │ │ │ ├── PacketGameSettings.hpp │ │ │ │ ├── PacketLoaded.hpp │ │ │ │ ├── PacketPlayerAlly.cpp │ │ │ │ ├── PacketPlayerAlly.hpp │ │ │ │ ├── PacketPlayerAnimFlags.cpp │ │ │ │ ├── PacketPlayerAnimFlags.hpp │ │ │ │ ├── PacketPlayerAnimPlay.cpp │ │ │ │ ├── PacketPlayerAnimPlay.hpp │ │ │ │ ├── PacketPlayerAttack.cpp │ │ │ │ ├── PacketPlayerAttack.hpp │ │ │ │ ├── PacketPlayerAttribute.cpp │ │ │ │ ├── PacketPlayerAttribute.hpp │ │ │ │ ├── PacketPlayerBaseInfo.cpp │ │ │ │ ├── PacketPlayerBaseInfo.hpp │ │ │ │ ├── PacketPlayerBehavior.cpp │ │ │ │ ├── PacketPlayerBehavior.hpp │ │ │ │ ├── PacketPlayerBook.cpp │ │ │ │ ├── PacketPlayerBook.hpp │ │ │ │ ├── PacketPlayerBounty.cpp │ │ │ │ ├── PacketPlayerBounty.hpp │ │ │ │ ├── PacketPlayerCast.cpp │ │ │ │ ├── PacketPlayerCast.hpp │ │ │ │ ├── PacketPlayerCellChange.cpp │ │ │ │ ├── PacketPlayerCellChange.hpp │ │ │ │ ├── PacketPlayerCellState.cpp │ │ │ │ ├── PacketPlayerCellState.hpp │ │ │ │ ├── PacketPlayerCharGen.cpp │ │ │ │ ├── PacketPlayerCharGen.hpp │ │ │ │ ├── PacketPlayerClass.cpp │ │ │ │ ├── PacketPlayerClass.hpp │ │ │ │ ├── PacketPlayerCooldowns.cpp │ │ │ │ ├── PacketPlayerCooldowns.hpp │ │ │ │ ├── PacketPlayerDeath.cpp │ │ │ │ ├── PacketPlayerDeath.hpp │ │ │ │ ├── PacketPlayerEquipment.cpp │ │ │ │ ├── PacketPlayerEquipment.hpp │ │ │ │ ├── PacketPlayerFaction.cpp │ │ │ │ ├── PacketPlayerFaction.hpp │ │ │ │ ├── PacketPlayerInput.cpp │ │ │ │ ├── PacketPlayerInput.hpp │ │ │ │ ├── PacketPlayerInventory.cpp │ │ │ │ ├── PacketPlayerInventory.hpp │ │ │ │ ├── PacketPlayerItemUse.cpp │ │ │ │ ├── PacketPlayerItemUse.hpp │ │ │ │ ├── PacketPlayerJail.cpp │ │ │ │ ├── PacketPlayerJail.hpp │ │ │ │ ├── PacketPlayerJournal.cpp │ │ │ │ ├── PacketPlayerJournal.hpp │ │ │ │ ├── PacketPlayerLevel.cpp │ │ │ │ ├── PacketPlayerLevel.hpp │ │ │ │ ├── PacketPlayerMiscellaneous.cpp │ │ │ │ ├── PacketPlayerMiscellaneous.hpp │ │ │ │ ├── PacketPlayerMomentum.cpp │ │ │ │ ├── PacketPlayerMomentum.hpp │ │ │ │ ├── PacketPlayerPosition.cpp │ │ │ │ ├── PacketPlayerPosition.hpp │ │ │ │ ├── PacketPlayerQuickKeys.cpp │ │ │ │ ├── PacketPlayerQuickKeys.hpp │ │ │ │ ├── PacketPlayerRegionAuthority.cpp │ │ │ │ ├── PacketPlayerReputation.cpp │ │ │ │ ├── PacketPlayerReputation.hpp │ │ │ │ ├── PacketPlayerRest.cpp │ │ │ │ ├── PacketPlayerRest.hpp │ │ │ │ ├── PacketPlayerResurrect.cpp │ │ │ │ ├── PacketPlayerResurrect.hpp │ │ │ │ ├── PacketPlayerShapeshift.cpp │ │ │ │ ├── PacketPlayerShapeshift.hpp │ │ │ │ ├── PacketPlayerSkill.cpp │ │ │ │ ├── PacketPlayerSkill.hpp │ │ │ │ ├── PacketPlayerSpeech.cpp │ │ │ │ ├── PacketPlayerSpeech.hpp │ │ │ │ ├── PacketPlayerSpellbook.cpp │ │ │ │ ├── PacketPlayerSpellbook.hpp │ │ │ │ ├── PacketPlayerSpellsActive.cpp │ │ │ │ ├── PacketPlayerSpellsActive.hpp │ │ │ │ ├── PacketPlayerStatsDynamic.cpp │ │ │ │ ├── PacketPlayerStatsDynamic.hpp │ │ │ │ ├── PacketPlayerTopic.cpp │ │ │ │ ├── PacketPlayerTopic.hpp │ │ │ │ ├── PlayerPacket.cpp │ │ │ │ └── PlayerPacket.hpp │ │ │ ├── System/ │ │ │ │ ├── PacketSystemHandshake.cpp │ │ │ │ ├── PacketSystemHandshake.hpp │ │ │ │ ├── SystemPacket.cpp │ │ │ │ └── SystemPacket.hpp │ │ │ └── Worldstate/ │ │ │ ├── PacketCellReset.cpp │ │ │ ├── PacketCellReset.hpp │ │ │ ├── PacketClientScriptGlobal.cpp │ │ │ ├── PacketClientScriptGlobal.hpp │ │ │ ├── PacketClientScriptSettings.cpp │ │ │ ├── PacketClientScriptSettings.hpp │ │ │ ├── PacketRecordDynamic.cpp │ │ │ ├── PacketRecordDynamic.hpp │ │ │ ├── PacketWorldCollisionOverride.cpp │ │ │ ├── PacketWorldCollisionOverride.hpp │ │ │ ├── PacketWorldDestinationOverride.cpp │ │ │ ├── PacketWorldDestinationOverride.hpp │ │ │ ├── PacketWorldKillCount.cpp │ │ │ ├── PacketWorldKillCount.hpp │ │ │ ├── PacketWorldMap.cpp │ │ │ ├── PacketWorldMap.hpp │ │ │ ├── PacketWorldRegionAuthority.cpp │ │ │ ├── PacketWorldRegionAuthority.hpp │ │ │ ├── PacketWorldTime.cpp │ │ │ ├── PacketWorldTime.hpp │ │ │ ├── PacketWorldWeather.cpp │ │ │ ├── PacketWorldWeather.hpp │ │ │ ├── WorldstatePacket.cpp │ │ │ └── WorldstatePacket.hpp │ │ ├── TimedLog.cpp │ │ ├── TimedLog.hpp │ │ ├── Utils.cpp │ │ ├── Utils.hpp │ │ └── Version.hpp │ ├── process/ │ │ ├── processinvoker.cpp │ │ └── processinvoker.hpp │ ├── resource/ │ │ ├── animation.cpp │ │ ├── animation.hpp │ │ ├── bulletshape.cpp │ │ ├── bulletshape.hpp │ │ ├── bulletshapemanager.cpp │ │ ├── bulletshapemanager.hpp │ │ ├── imagemanager.cpp │ │ ├── imagemanager.hpp │ │ ├── keyframemanager.cpp │ │ ├── keyframemanager.hpp │ │ ├── multiobjectcache.cpp │ │ ├── multiobjectcache.hpp │ │ ├── niffilemanager.cpp │ │ ├── niffilemanager.hpp │ │ ├── objectcache.hpp │ │ ├── resourcemanager.hpp │ │ ├── resourcesystem.cpp │ │ ├── resourcesystem.hpp │ │ ├── scenemanager.cpp │ │ ├── scenemanager.hpp │ │ ├── stats.cpp │ │ └── stats.hpp │ ├── sceneutil/ │ │ ├── actorutil.cpp │ │ ├── actorutil.hpp │ │ ├── agentpath.cpp │ │ ├── agentpath.hpp │ │ ├── attach.cpp │ │ ├── attach.hpp │ │ ├── clone.cpp │ │ ├── clone.hpp │ │ ├── controller.cpp │ │ ├── controller.hpp │ │ ├── detourdebugdraw.cpp │ │ ├── detourdebugdraw.hpp │ │ ├── keyframe.hpp │ │ ├── lightcontroller.cpp │ │ ├── lightcontroller.hpp │ │ ├── lightmanager.cpp │ │ ├── lightmanager.hpp │ │ ├── lightutil.cpp │ │ ├── lightutil.hpp │ │ ├── morphgeometry.cpp │ │ ├── morphgeometry.hpp │ │ ├── mwshadowtechnique.cpp │ │ ├── mwshadowtechnique.hpp │ │ ├── navmesh.cpp │ │ ├── navmesh.hpp │ │ ├── optimizer.cpp │ │ ├── optimizer.hpp │ │ ├── osgacontroller.cpp │ │ ├── osgacontroller.hpp │ │ ├── pathgridutil.cpp │ │ ├── pathgridutil.hpp │ │ ├── positionattitudetransform.cpp │ │ ├── positionattitudetransform.hpp │ │ ├── recastmesh.cpp │ │ ├── recastmesh.hpp │ │ ├── riggeometry.cpp │ │ ├── riggeometry.hpp │ │ ├── serialize.cpp │ │ ├── serialize.hpp │ │ ├── shadow.cpp │ │ ├── shadow.hpp │ │ ├── shadowsbin.cpp │ │ ├── shadowsbin.hpp │ │ ├── skeleton.cpp │ │ ├── skeleton.hpp │ │ ├── statesetupdater.cpp │ │ ├── statesetupdater.hpp │ │ ├── textkeymap.hpp │ │ ├── unrefqueue.cpp │ │ ├── unrefqueue.hpp │ │ ├── util.cpp │ │ ├── util.hpp │ │ ├── visitor.cpp │ │ ├── visitor.hpp │ │ ├── waterutil.cpp │ │ ├── waterutil.hpp │ │ ├── workqueue.cpp │ │ ├── workqueue.hpp │ │ ├── writescene.cpp │ │ └── writescene.hpp │ ├── sdlutil/ │ │ ├── events.hpp │ │ ├── gl4es_init.cpp │ │ ├── gl4es_init.h │ │ ├── imagetosurface.cpp │ │ ├── imagetosurface.hpp │ │ ├── sdlcursormanager.cpp │ │ ├── sdlcursormanager.hpp │ │ ├── sdlgraphicswindow.cpp │ │ ├── sdlgraphicswindow.hpp │ │ ├── sdlinputwrapper.cpp │ │ ├── sdlinputwrapper.hpp │ │ ├── sdlvideowrapper.cpp │ │ └── sdlvideowrapper.hpp │ ├── settings/ │ │ ├── categories.hpp │ │ ├── parser.cpp │ │ ├── parser.hpp │ │ ├── settings.cpp │ │ └── settings.hpp │ ├── shader/ │ │ ├── removedalphafunc.cpp │ │ ├── removedalphafunc.hpp │ │ ├── shadermanager.cpp │ │ ├── shadermanager.hpp │ │ ├── shadervisitor.cpp │ │ └── shadervisitor.hpp │ ├── terrain/ │ │ ├── buffercache.cpp │ │ ├── buffercache.hpp │ │ ├── cellborder.cpp │ │ ├── cellborder.hpp │ │ ├── chunkmanager.cpp │ │ ├── chunkmanager.hpp │ │ ├── compositemaprenderer.cpp │ │ ├── compositemaprenderer.hpp │ │ ├── defs.hpp │ │ ├── material.cpp │ │ ├── material.hpp │ │ ├── quadtreenode.cpp │ │ ├── quadtreenode.hpp │ │ ├── quadtreeworld.cpp │ │ ├── quadtreeworld.hpp │ │ ├── storage.hpp │ │ ├── terraindrawable.cpp │ │ ├── terraindrawable.hpp │ │ ├── terraingrid.cpp │ │ ├── terraingrid.hpp │ │ ├── texturemanager.cpp │ │ ├── texturemanager.hpp │ │ ├── viewdata.cpp │ │ ├── viewdata.hpp │ │ ├── world.cpp │ │ └── world.hpp │ ├── to_utf8/ │ │ ├── .gitignore │ │ ├── gen_iconv.cpp │ │ ├── tables_gen.hpp │ │ ├── tests/ │ │ │ ├── .gitignore │ │ │ ├── output/ │ │ │ │ └── to_utf8_test.out │ │ │ ├── test.sh │ │ │ ├── test_data/ │ │ │ │ ├── french-utf8.txt │ │ │ │ ├── french-win1252.txt │ │ │ │ ├── russian-utf8.txt │ │ │ │ └── russian-win1251.txt │ │ │ └── to_utf8_test.cpp │ │ ├── to_utf8.cpp │ │ └── to_utf8.hpp │ ├── translation/ │ │ ├── translation.cpp │ │ └── translation.hpp │ ├── version/ │ │ ├── version.cpp │ │ └── version.hpp │ ├── vfs/ │ │ ├── archive.hpp │ │ ├── bsaarchive.cpp │ │ ├── bsaarchive.hpp │ │ ├── filesystemarchive.cpp │ │ ├── filesystemarchive.hpp │ │ ├── manager.cpp │ │ ├── manager.hpp │ │ ├── registerarchives.cpp │ │ └── registerarchives.hpp │ └── widgets/ │ ├── box.cpp │ ├── box.hpp │ ├── fontwrapper.hpp │ ├── imagebutton.cpp │ ├── imagebutton.hpp │ ├── list.cpp │ ├── list.hpp │ ├── numericeditbox.cpp │ ├── numericeditbox.hpp │ ├── sharedstatebutton.cpp │ ├── sharedstatebutton.hpp │ ├── tags.cpp │ ├── tags.hpp │ ├── widgets.cpp │ ├── widgets.hpp │ ├── windowcaption.cpp │ └── windowcaption.hpp ├── docs/ │ ├── Doxyfile.cmake │ ├── DoxyfilePages.cmake │ ├── license/ │ │ └── RussoOne Font License.txt │ ├── mainpage.hpp.cmake │ ├── openmw-stage1.md │ ├── requirements.txt │ └── source/ │ ├── _static/ │ │ ├── .keep │ │ └── figures.css │ ├── conf.py │ ├── index.rst │ ├── manuals/ │ │ ├── index.rst │ │ ├── installation/ │ │ │ ├── common-problems.rst │ │ │ ├── index.rst │ │ │ ├── install-game-files.rst │ │ │ └── install-openmw.rst │ │ └── openmw-cs/ │ │ ├── files-and-directories.rst │ │ ├── foreword.rst │ │ ├── index.rst │ │ ├── record-filters.rst │ │ ├── record-types.rst │ │ ├── starting-dialog.rst │ │ ├── tables.rst │ │ └── tour.rst │ ├── reference/ │ │ ├── documentationHowTo.rst │ │ ├── index.rst │ │ └── modding/ │ │ ├── custom-models/ │ │ │ ├── index.rst │ │ │ ├── pipeline-blender-collada.rst │ │ │ ├── pipeline-blender-nif.rst │ │ │ └── pipeline-blender-osgnative.rst │ │ ├── differences.rst │ │ ├── extended.rst │ │ ├── font.rst │ │ ├── foreword.rst │ │ ├── index.rst │ │ ├── mod-install.rst │ │ ├── openmw-game-template.rst │ │ ├── paths.rst │ │ ├── settings/ │ │ │ ├── GUI.rst │ │ │ ├── HUD.rst │ │ │ ├── camera.rst │ │ │ ├── cells.rst │ │ │ ├── fog.rst │ │ │ ├── game.rst │ │ │ ├── general.rst │ │ │ ├── groundcover.rst │ │ │ ├── index.rst │ │ │ ├── input.rst │ │ │ ├── map.rst │ │ │ ├── models.rst │ │ │ ├── navigator.rst │ │ │ ├── physics.rst │ │ │ ├── saves.rst │ │ │ ├── shaders.rst │ │ │ ├── shadows.rst │ │ │ ├── sound.rst │ │ │ ├── terrain.rst │ │ │ ├── video.rst │ │ │ ├── water.rst │ │ │ └── windows.rst │ │ ├── sky-system.rst │ │ └── texture-modding/ │ │ ├── convert-bump-mapped-mods.rst │ │ ├── index.rst │ │ └── texture-basics.rst │ └── tutorial-style-guide.txt ├── extern/ │ ├── Base64/ │ │ ├── Base64.h │ │ └── CMakeLists.txt │ ├── CMakeLists.txt │ ├── LuaBridge/ │ │ ├── List.h │ │ ├── LuaBridge.h │ │ ├── Map.h │ │ ├── RefCountedObject.h │ │ ├── RefCountedPtr.h │ │ ├── Vector.h │ │ └── detail/ │ │ ├── CFunctions.h │ │ ├── ClassInfo.h │ │ ├── Constructor.h │ │ ├── FuncTraits.h │ │ ├── Iterator.h │ │ ├── LuaException.h │ │ ├── LuaHelpers.h │ │ ├── LuaRef.h │ │ ├── Namespace.h │ │ ├── Security.h │ │ ├── Stack.h │ │ ├── TypeList.h │ │ ├── TypeTraits.h │ │ ├── Userdata.h │ │ └── dump.h │ ├── PicoSHA2/ │ │ └── picosha2.h │ ├── oics/ │ │ ├── CMakeLists.txt │ │ ├── ICSChannel.cpp │ │ ├── ICSChannel.h │ │ ├── ICSChannelListener.h │ │ ├── ICSControl.cpp │ │ ├── ICSControl.h │ │ ├── ICSControlListener.h │ │ ├── ICSInputControlSystem.cpp │ │ ├── ICSInputControlSystem.h │ │ ├── ICSInputControlSystem_joystick.cpp │ │ ├── ICSInputControlSystem_keyboard.cpp │ │ ├── ICSInputControlSystem_mouse.cpp │ │ ├── ICSPrerequisites.cpp │ │ ├── ICSPrerequisites.h │ │ ├── tinystr.cpp │ │ ├── tinystr.h │ │ ├── tinyxml.cpp │ │ ├── tinyxml.h │ │ ├── tinyxmlerror.cpp │ │ └── tinyxmlparser.cpp │ ├── osg-ffmpeg-videoplayer/ │ │ ├── CMakeLists.txt │ │ ├── License.txt │ │ ├── audiodecoder.cpp │ │ ├── audiodecoder.hpp │ │ ├── audiofactory.hpp │ │ ├── videodefs.hpp │ │ ├── videoplayer.cpp │ │ ├── videoplayer.hpp │ │ ├── videostate.cpp │ │ └── videostate.hpp │ └── osgQt/ │ ├── CMakeLists.txt │ ├── GraphicsWindowQt │ └── GraphicsWindowQt.cpp ├── files/ │ ├── CMakeLists.txt │ ├── gamecontrollerdb.txt │ ├── launcher/ │ │ ├── icons/ │ │ │ └── tango/ │ │ │ └── index.theme │ │ └── launcher.qrc │ ├── mac/ │ │ ├── openmw-Info.plist.in │ │ ├── openmw-cs-Info.plist.in │ │ ├── openmw-cs.icns │ │ ├── openmw.icns │ │ └── qt.conf │ ├── mygui/ │ │ ├── CMakeLists.txt │ │ ├── DejaVuFontLicense.txt │ │ ├── OpenMWResourcePlugin.xml │ │ ├── core.skin │ │ ├── core.xml │ │ ├── core_layouteditor.xml │ │ ├── openmw_alchemy_window.layout │ │ ├── openmw_book.layout │ │ ├── openmw_box.skin.xml │ │ ├── openmw_button.skin.xml │ │ ├── openmw_chargen_birth.layout │ │ ├── openmw_chargen_class.layout │ │ ├── openmw_chargen_class_description.layout │ │ ├── openmw_chargen_create_class.layout │ │ ├── openmw_chargen_generate_class_result.layout │ │ ├── openmw_chargen_race.layout │ │ ├── openmw_chargen_review.layout │ │ ├── openmw_chargen_select_attribute.layout │ │ ├── openmw_chargen_select_skill.layout │ │ ├── openmw_chargen_select_specialization.layout │ │ ├── openmw_companion_window.layout │ │ ├── openmw_confirmation_dialog.layout │ │ ├── openmw_console.layout │ │ ├── openmw_console.skin.xml │ │ ├── openmw_container_window.layout │ │ ├── openmw_count_window.layout │ │ ├── openmw_debug_window.layout │ │ ├── openmw_debug_window.skin.xml │ │ ├── openmw_dialogue_window.layout │ │ ├── openmw_dialogue_window.skin.xml │ │ ├── openmw_edit.skin.xml │ │ ├── openmw_edit_effect.layout │ │ ├── openmw_edit_note.layout │ │ ├── openmw_enchanting_dialog.layout │ │ ├── openmw_font.xml │ │ ├── openmw_hud.layout │ │ ├── openmw_hud_box.skin.xml │ │ ├── openmw_hud_energybar.skin.xml │ │ ├── openmw_infobox.layout │ │ ├── openmw_interactive_messagebox.layout │ │ ├── openmw_interactive_messagebox_notransp.layout │ │ ├── openmw_inventory_window.layout │ │ ├── openmw_itemselection_dialog.layout │ │ ├── openmw_jail_screen.layout │ │ ├── openmw_journal.layout │ │ ├── openmw_journal.skin.xml │ │ ├── openmw_layers.xml │ │ ├── openmw_levelup_dialog.layout │ │ ├── openmw_list.skin.xml │ │ ├── openmw_loading_screen.layout │ │ ├── openmw_magicselection_dialog.layout │ │ ├── openmw_mainmenu.layout │ │ ├── openmw_mainmenu.skin.xml │ │ ├── openmw_map_window.layout │ │ ├── openmw_map_window.skin.xml │ │ ├── openmw_merchantrepair.layout │ │ ├── openmw_messagebox.layout │ │ ├── openmw_persuasion_dialog.layout │ │ ├── openmw_pointer.xml │ │ ├── openmw_progress.skin.xml │ │ ├── openmw_quickkeys_menu.layout │ │ ├── openmw_quickkeys_menu_assign.layout │ │ ├── openmw_recharge_dialog.layout │ │ ├── openmw_repair.layout │ │ ├── openmw_resources.xml │ │ ├── openmw_savegame_dialog.layout │ │ ├── openmw_screen_fader.layout │ │ ├── openmw_screen_fader_hit.layout │ │ ├── openmw_scroll.layout │ │ ├── openmw_scroll.skin.xml │ │ ├── openmw_settings.xml │ │ ├── openmw_settings_window.layout │ │ ├── openmw_spell_buying_window.layout │ │ ├── openmw_spell_window.layout │ │ ├── openmw_spellcreation_dialog.layout │ │ ├── openmw_stats_window.layout │ │ ├── openmw_text.skin.xml │ │ ├── openmw_text_input.layout │ │ ├── openmw_tooltips.layout │ │ ├── openmw_trade_window.layout │ │ ├── openmw_trainingwindow.layout │ │ ├── openmw_travel_window.layout │ │ ├── openmw_wait_dialog.layout │ │ ├── openmw_wait_dialog_progressbar.layout │ │ ├── openmw_windows.skin.xml │ │ ├── skins.xml │ │ ├── tes3mp_chat.layout │ │ ├── tes3mp_chat.skin.xml │ │ ├── tes3mp_dialog_list.layout │ │ ├── tes3mp_login.layout │ │ ├── tes3mp_login.skin.xml │ │ └── tes3mp_text_input.layout │ ├── opencs/ │ │ ├── defaultfilters │ │ ├── raster/ │ │ │ ├── scene-exterior-parts/ │ │ │ │ └── composite_the_icons.sh │ │ │ ├── scene-play-rev9_masked.xcf │ │ │ └── scene-view-parts/ │ │ │ ├── README │ │ │ └── composite_the_32_icons.sh │ │ ├── resources.qrc │ │ └── scalable/ │ │ ├── referenceable-record/ │ │ │ └── book2.svgz │ │ └── startup/ │ │ ├── configure.svgz │ │ ├── create-addon.svgz │ │ ├── edit-content.svgz │ │ └── new-game.svgz │ ├── openmw-cs.cfg │ ├── openmw.appdata.xml │ ├── openmw.cfg │ ├── openmw.cfg.local │ ├── org.openmw.cs.desktop │ ├── org.openmw.launcher.desktop │ ├── settings-default.cfg │ ├── shaders/ │ │ ├── CMakeLists.txt │ │ ├── alpha.glsl │ │ ├── groundcover_fragment.glsl │ │ ├── groundcover_vertex.glsl │ │ ├── lighting.glsl │ │ ├── lighting_util.glsl │ │ ├── nv_default_fragment.glsl │ │ ├── nv_default_vertex.glsl │ │ ├── nv_nolighting_fragment.glsl │ │ ├── nv_nolighting_vertex.glsl │ │ ├── objects_fragment.glsl │ │ ├── objects_vertex.glsl │ │ ├── parallax.glsl │ │ ├── s360_fragment.glsl │ │ ├── s360_vertex.glsl │ │ ├── shadowcasting_fragment.glsl │ │ ├── shadowcasting_vertex.glsl │ │ ├── shadows_fragment.glsl │ │ ├── shadows_vertex.glsl │ │ ├── terrain_fragment.glsl │ │ ├── terrain_vertex.glsl │ │ ├── vertexcolors.glsl │ │ ├── water_fragment.glsl │ │ └── water_vertex.glsl │ ├── tes3mp/ │ │ ├── browser.rc │ │ ├── tes3mp-client-default.cfg │ │ ├── tes3mp-server-default.cfg │ │ ├── tes3mp.exe.manifest │ │ ├── tes3mp.rc │ │ └── ui/ │ │ ├── Main.ui │ │ └── ServerInfo.ui │ ├── tes3mp-browser.desktop │ ├── ui/ │ │ ├── advancedpage.ui │ │ ├── contentselector.ui │ │ ├── datafilespage.ui │ │ ├── filedialog.ui │ │ ├── graphicspage.ui │ │ ├── mainwindow.ui │ │ ├── playpage.ui │ │ ├── settingspage.ui │ │ └── wizard/ │ │ ├── componentselectionpage.ui │ │ ├── conclusionpage.ui │ │ ├── existinginstallationpage.ui │ │ ├── importpage.ui │ │ ├── installationpage.ui │ │ ├── installationtargetpage.ui │ │ ├── intropage.ui │ │ ├── languageselectionpage.ui │ │ └── methodselectionpage.ui │ ├── version.in │ ├── vfs/ │ │ ├── CMakeLists.txt │ │ └── textures/ │ │ ├── omw_menu_scroll_center_h.dds │ │ ├── omw_menu_scroll_center_v.dds │ │ ├── omw_menu_scroll_down.dds │ │ ├── omw_menu_scroll_left.dds │ │ ├── omw_menu_scroll_right.dds │ │ └── omw_menu_scroll_up.dds │ ├── windows/ │ │ ├── launcher.rc │ │ ├── opencs.rc │ │ ├── openmw-wizard.rc │ │ ├── openmw.exe.manifest │ │ └── openmw.rc │ └── wizard/ │ ├── icons/ │ │ └── tango/ │ │ └── index.theme │ └── wizard.qrc ├── manual/ │ └── opencs/ │ ├── .gitignore │ ├── creating_file.tex │ ├── files_and_directories.tex │ ├── filters.tex │ ├── main.tex │ ├── recordmodification.tex │ ├── recordtypes.tex │ ├── tables.tex │ └── windows.tex ├── tes3mp-changelog.md └── tes3mp-credits.md ================================================ FILE CONTENTS ================================================ ================================================ FILE: .editorconfig ================================================ root = true [*.cpp] indent_style = space indent_size = 4 insert_final_newline = true [*.hpp] indent_style = space indent_size = 4 insert_final_newline = true [*.glsl] indent_style = space indent_size = 4 insert_final_newline = false ================================================ FILE: .github/FUNDING.yml ================================================ patreon: davidcernat custom: https://www.paypal.me/DavidCernat ================================================ FILE: .gitignore ================================================ ## make CMakeFiles */CMakeFiles CMakeCache.txt cmake_install.cmake Makefile makefile build*/ prebuilt ##windows build process /deps /MSVC* ## doxygen Doxygen ## ides/editors *~ *.kdev4 *.swp *.swo *.kate-swp .cproject .project .settings .directory .idea cmake-build-* files/windows/*.aps cmake-build-* ## qt-creator CMakeLists.txt.user* .vs .vscode ## resources data resources /*.cfg /*.desktop /*.install ## binaries /esmtool /openmw /opencs /niftest /bsatool /openmw-cs /openmw-essimporter /openmw-iniimporter /openmw-launcher /openmw-wizard ## generated objects apps/openmw/config.hpp apps/launcher/ui_contentselector.h apps/launcher/ui_settingspage.h apps/opencs/ui_contentselector.h apps/opencs/ui_filedialog.h apps/wizard/qrc_wizard.cxx apps/wizard/ui_componentselectionpage.h apps/wizard/ui_conclusionpage.h apps/wizard/ui_existinginstallationpage.h apps/wizard/ui_importpage.h apps/wizard/ui_installationpage.h apps/wizard/ui_installationtargetpage.h apps/wizard/ui_intropage.h apps/wizard/ui_languageselectionpage.h apps/wizard/ui_methodselectionpage.h components/ui_contentselector.h docs/mainpage.hpp docs/Doxyfile docs/DoxyfilePages moc_*.cxx *.cxx_parameters *qrc_launcher.cxx *qrc_resources.cxx *__* *ui_datafilespage.h *ui_graphicspage.h *ui_mainwindow.h *ui_playpage.h *.[ao] *.so venv/ ================================================ FILE: .gitlab-ci.yml ================================================ # Note: We set `needs` on each job to control the job DAG. # See https://docs.gitlab.com/ee/ci/yaml/#needs stages: - build .Debian_Image: tags: - docker - linux image: debian:bullseye .Debian: extends: .Debian_Image cache: paths: - apt-cache/ - ccache/ stage: build script: - export CCACHE_BASEDIR="`pwd`" - export CCACHE_DIR="`pwd`/ccache" && mkdir -pv "$CCACHE_DIR" - ccache -z -M "${CCACHE_SIZE}" - CI/before_script.linux.sh - cd build - cmake --build . -- -j $(nproc) - cmake --install . - if [[ "${BUILD_TESTS_ONLY}" ]]; then ./openmw_test_suite; fi - if [[ "${BUILD_TESTS_ONLY}" ]]; then ./openmw_detournavigator_navmeshtilescache_benchmark; fi - ccache -s artifacts: paths: - build/install/ Coverity: extends: .Debian_Image stage: build rules: - if: '$CI_PIPELINE_SOURCE == "schedule"' before_script: - CI/install_debian_deps.sh gcc 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 # Add more than just `openmw` once we can build everything under 3h - cov-analysis-linux64-*/bin/cov-build --dir cov-int cmake --build build -- -j $(nproc) openmw 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" variables: CC: gcc CXX: g++ timeout: 8h Debian_GCC: extends: .Debian cache: key: Debian_GCC.v2 before_script: - CI/install_debian_deps.sh gcc openmw-deps openmw-deps-dynamic variables: CC: gcc CXX: g++ CCACHE_SIZE: 3G # 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 cache: key: Debian_GCC_tests.v2 variables: CCACHE_SIZE: 1G BUILD_TESTS_ONLY: 1 Debian_GCC_Static_Deps: extends: Debian_GCC cache: key: Debian_GCC_Static_Deps paths: - apt-cache/ - ccache/ - build/extern/fetched/ before_script: - CI/install_debian_deps.sh gcc openmw-deps openmw-deps-static variables: CI_OPENMW_USE_STATIC_DEPS: 1 Debian_GCC_Static_Deps_tests: extends: Debian_GCC_Static_Deps cache: key: Debian_GCC_Static_Deps_tests variables: CCACHE_SIZE: 1G BUILD_TESTS_ONLY: 1 Debian_Clang: extends: .Debian before_script: - CI/install_debian_deps.sh clang openmw-deps openmw-deps-dynamic cache: key: Debian_Clang.v2 variables: CC: clang CXX: clang++ CCACHE_SIZE: 2G # 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 cache: key: Debian_Clang_tests.v2 variables: CCACHE_SIZE: 1G BUILD_TESTS_ONLY: 1 .MacOS: image: macos-11-xcode-12 tags: - shared-macos-amd64 stage: build only: variables: - $CI_PROJECT_ID == "7107382" cache: paths: - ccache/ script: - rm -fr build # remove the build directory - CI/before_install.osx.sh - export CCACHE_BASEDIR="$(pwd)" - export CCACHE_DIR="$(pwd)/ccache" - mkdir -pv "${CCACHE_DIR}" - 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 - ccache -s artifacts: paths: - build/OpenMW-*.dmg - "build/**/*.log" macOS11_Xcode12: extends: .MacOS image: macos-11-xcode-12 allow_failure: true cache: key: macOS11_Xcode12.v1 variables: CCACHE_SIZE: 3G macOS10.15_Xcode11: extends: .MacOS image: macos-10.15-xcode-11 cache: key: macOS10.15_Xcode11.v1 variables: CCACHE_SIZE: 3G variables: &engine-targets targets: "openmw,openmw-essimporter,openmw-iniimporter,openmw-launcher,openmw-wizard" package: "Engine" variables: &cs-targets targets: "openmw-cs,bsatool,esmtool,niftest" package: "CS" variables: &tests-targets targets: "openmw_test_suite,openmw_detournavigator_navmeshtilescache_benchmark" package: "Tests" .Windows_Ninja_Base: tags: - windows before_script: - Import-Module "$env:ChocolateyInstall\helpers\chocolateyProfile.psm1" - choco source add -n=openmw-proxy -s="https://repo.openmw.org/repository/Chocolatey/" --priority=1 - choco install git --force --params "/GitAndUnixToolsOnPath" -y - choco install 7zip -y - choco install cmake.install --installargs 'ADD_CMAKE_TO_PATH=System' -y - choco install vswhere -y - choco install ninja -y - choco install python -y - refreshenv stage: build script: - $time = (Get-Date -Format "HH:mm:ss") - echo ${time} - echo "started by ${GITLAB_USER_NAME}" - sh CI/before_script.msvc.sh -c $config -p Win64 -v 2019 -k -V -N -b -t - cd MSVC2019_64_Ninja - .\ActivateMSVC.ps1 - cmake --build . --config $config --target ($targets.Split(',')) - cd $config - echo "CI_COMMIT_REF_NAME ${CI_COMMIT_REF_NAME}`nCI_JOB_ID ${CI_JOB_ID}`nCI_COMMIT_SHA ${CI_COMMIT_SHA}" | Out-File -Encoding UTF8 CI-ID.txt - | if (Get-ChildItem -Recurse *.pdb) { 7z a -tzip ..\..\OpenMW_MSVC2019_64_${package}_${config}_${CI_COMMIT_REF_NAME}_${CI_JOB_ID}_symbols.zip '*.pdb' CI-ID.txt Get-ChildItem -Recurse *.pdb | Remove-Item } - 7z a -tzip ..\..\OpenMW_MSVC2019_64_${package}_${config}_${CI_COMMIT_REF_NAME}.zip '*' - if ($executables) { foreach ($exe in $executables.Split(',')) { & .\$exe } } after_script: - Copy-Item C:\ProgramData\chocolatey\logs\chocolatey.log cache: key: ninja-v2 paths: - deps - MSVC2019_64_Ninja/deps/Qt artifacts: when: always paths: - "*.zip" - "*.log" - MSVC2019_64_Ninja/*.log - MSVC2019_64_Ninja/*/*.log - MSVC2019_64_Ninja/*/*/*.log - MSVC2019_64_Ninja/*/*/*/*.log - MSVC2019_64_Ninja/*/*/*/*/*.log - MSVC2019_64_Ninja/*/*/*/*/*/*.log - MSVC2019_64_Ninja/*/*/*/*/*/*/*.log - MSVC2019_64_Ninja/*/*/*/*/*/*/*/*.log Windows_Ninja_Engine_Release: extends: - .Windows_Ninja_Base variables: <<: *engine-targets config: "Release" Windows_Ninja_Engine_Debug: extends: - .Windows_Ninja_Base variables: <<: *engine-targets config: "Debug" Windows_Ninja_Engine_RelWithDebInfo: extends: - .Windows_Ninja_Base variables: <<: *engine-targets config: "RelWithDebInfo" Windows_Ninja_CS_Release: extends: - .Windows_Ninja_Base variables: <<: *cs-targets config: "Release" Windows_Ninja_CS_Debug: extends: - .Windows_Ninja_Base variables: <<: *cs-targets config: "Debug" Windows_Ninja_CS_RelWithDebInfo: extends: - .Windows_Ninja_Base variables: <<: *cs-targets config: "RelWithDebInfo" Windows_Ninja_Tests_RelWithDebInfo: extends: .Windows_Ninja_Base stage: build variables: <<: *tests-targets config: "RelWithDebInfo" # Gitlab can't successfully execute following binaries due to unknown reason # executables: "openmw_test_suite.exe,openmw_detournavigator_navmeshtilescache_benchmark.exe" .Windows_MSBuild_Base: tags: - windows before_script: - Import-Module "$env:ChocolateyInstall\helpers\chocolateyProfile.psm1" - choco source add -n=openmw-proxy -s="https://repo.openmw.org/repository/Chocolatey/" --priority=1 - choco install git --force --params "/GitAndUnixToolsOnPath" -y - choco install 7zip -y - choco install cmake.install --installargs 'ADD_CMAKE_TO_PATH=System' -y - choco install vswhere -y - choco install python -y - refreshenv stage: build script: - $time = (Get-Date -Format "HH:mm:ss") - echo ${time} - echo "started by ${GITLAB_USER_NAME}" - sh CI/before_script.msvc.sh -c $config -p Win64 -v 2019 -k -V -b -t - cd MSVC2019_64 - cmake --build . --config $config --target ($targets.Split(',')) - cd $config - echo "CI_COMMIT_REF_NAME ${CI_COMMIT_REF_NAME}`nCI_JOB_ID ${CI_JOB_ID}`nCI_COMMIT_SHA ${CI_COMMIT_SHA}" | Out-File -Encoding UTF8 CI-ID.txt - | if (Get-ChildItem -Recurse *.pdb) { 7z a -tzip ..\..\OpenMW_MSVC2019_64_${package}_${config}_${CI_COMMIT_REF_NAME}_${CI_JOB_ID}_symbols.zip '*.pdb' CI-ID.txt Get-ChildItem -Recurse *.pdb | Remove-Item } - 7z a -tzip ..\..\OpenMW_MSVC2019_64_${package}_${config}_${CI_COMMIT_REF_NAME}.zip '*' - if ($executables) { foreach ($exe in $executables.Split(',')) { & .\$exe } } after_script: - Copy-Item C:\ProgramData\chocolatey\logs\chocolatey.log cache: key: msbuild-v2 paths: - deps - MSVC2019_64/deps/Qt artifacts: when: always paths: - "*.zip" - "*.log" - MSVC2019_64/*.log - MSVC2019_64/*/*.log - MSVC2019_64/*/*/*.log - MSVC2019_64/*/*/*/*.log - MSVC2019_64/*/*/*/*/*.log - MSVC2019_64/*/*/*/*/*/*.log - MSVC2019_64/*/*/*/*/*/*/*.log - MSVC2019_64/*/*/*/*/*/*/*/*.log Windows_MSBuild_Engine_Release: extends: - .Windows_MSBuild_Base variables: <<: *engine-targets config: "Release" Windows_MSBuild_Engine_Debug: extends: - .Windows_MSBuild_Base variables: <<: *engine-targets config: "Debug" Windows_MSBuild_Engine_RelWithDebInfo: extends: - .Windows_MSBuild_Base variables: <<: *engine-targets config: "RelWithDebInfo" Windows_MSBuild_CS_Release: extends: - .Windows_MSBuild_Base variables: <<: *cs-targets config: "Release" Windows_MSBuild_CS_Debug: extends: - .Windows_MSBuild_Base variables: <<: *cs-targets config: "Debug" Windows_MSBuild_CS_RelWithDebInfo: extends: - .Windows_MSBuild_Base variables: <<: *cs-targets config: "RelWithDebInfo" Windows_MSBuild_Tests_RelWithDebInfo: extends: .Windows_MSBuild_Base stage: build variables: <<: *tests-targets config: "RelWithDebInfo" # Gitlab can't successfully execute following binaries due to unknown reason # executables: "openmw_test_suite.exe,openmw_detournavigator_navmeshtilescache_benchmark.exe" Debian_AndroidNDK_arm64-v8a: tags: - linux image: debian:bullseye variables: CCACHE_SIZE: 3G cache: key: Debian_AndroidNDK_arm64-v8a.v3 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 stage: build script: - export CCACHE_BASEDIR="`pwd`" - export CCACHE_DIR="`pwd`/ccache" && mkdir -pv "$CCACHE_DIR" - ccache -z -M "${CCACHE_SIZE}" - CI/before_install.android.sh - CI/before_script.android.sh - cd build - cmake --build . -- -j $(nproc) - cmake --install . - ccache -s artifacts: paths: - build/install/ # 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 ================================================ FILE: .gitmodules ================================================ [submodule "extern/breakpad"] path = extern/breakpad url = https://chromium.googlesource.com/breakpad/breakpad ================================================ FILE: .readthedocs.yaml ================================================ version: 2 sphinx: configuration: docs/source/conf.py python: version: 3.8 install: - requirements: docs/requirements.txt ================================================ FILE: .travis.yml ================================================ language: cpp branches: only: - master - coverity_scan - /openmw-.*$/ - /^[0-9]+\.[0-9]+\.[0-9]+.*$/ env: global: # The next declaration is the encrypted COVERITY_SCAN_TOKEN, created # via the "travis encrypt" command using the project repo's public key - secure: "1QK0yVyoOB+gf2I7XzvhXu9w/5lq4stBXIwJbVCTjz4Q4XVHCosURaW1MAgKzMrPnbFEwjyn5uQ8BwsvvfkuN1AZD0YXITgc7gyI+J1wQ/p/ljxRxglakU6WEgsTs2J5z9UmGac4YTXg+quK7YP3rv+zuGim2I2rhzImejyzp0Ym3kRCnNcy+SGBsiRaevRJMe00Ch8zGAbEhduQGeSoS6W0rcu02DNlQKiq5NktWsXR+TWWWVfIeIlQR/lbPsCd0pdxMaMv2QCY0rVbwrYxWJwr/Qe45dAdWp+8/C3PbXpeMSGxlLa33nJNX4Lf/djxbjm8KWk6edaXPajrjR/0iwcpwq0jg2Jt6XfEdnJt35F1gpXlc04sxStjG45uloOKCFYT0wdhIO1Lq+hDP54wypQl+JInd5qC001O7pwhVxO36EgKWqo8HD+BqGDBwsNj2engy9Qcp3wO6G0rLBPB3CrZsk9wrHVv5cSiQSLMhId3Xviu3ZI2qEDA+kgTvxrKrsnMj4bILVCyG5Ka2Mj22wIDW9e8oIab9oTdujax3DTN1GkD6QuOAGzwDsNwGASsgfoeZ+FUhgM75RlBWGMilgkmnF7EJ0oAXLEpjtABnEr2d4qHv+y08kOuTDBLB9ExzCIj024dYYYNLZrqPKx0ncHuCMG2QNj2aJAJEZtj1rQ=" cache: ccache addons: apt: sources: - sourceline: 'ppa:openmw/openmw' - ubuntu-toolchain-r-test packages: [ # Dev cmake, clang-tools-7, gcc-8, g++-8, 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-3.4-dev, libmygui-dev, # tes3mp stuff libboost-dev, libqt5opengl5-dev, libluajit-5.1-dev ] coverity_scan: project: name: "TES3MP/openmw-tes3mp" description: "" branch_pattern: coverity_scan notification_email: koncord@tes3mp.com build_command_prepend: "cov-configure --comptype gcc --compiler gcc-8 --template; cmake . -DBUILD_UNITTESTS=FALSE -DBUILD_OPENCS=FALSE -DBUILD_BSATOOL=FALSE -DBUILD_ESMTOOL=FALSE -DBUILD_MWINIIMPORTER=FALSE -DBUILD_LAUNCHER=FALSE" build_command: "make VERBOSE=1 -j3" matrix: include: - os: linux env: - ANALYZE="scan-build-7 --use-cc clang-7 --use-c++ clang++-7 " - MATRIX_CC="CC=clang-7 && CXX=clang++-7" compiler: clang sudo: required dist: bionic - os: linux env: - MATRIX_CC="CC=gcc-8 && CXX=g++-8" sudo: required dist: bionic - os: linux env: - MATRIX_CC="CC=clang-7 && CXX=clang++-7" sudo: required dist: bionic allow_failures: - env: - MATRIX_CC="CC=clang-7 && CXX=clang++-7" - env: - ANALYZE="scan-build-7 --use-cc clang-7 --use-c++ clang++-7 " - MATRIX_CC="CC=clang-7 && CXX=clang++-7" before_install: - ./CI/before_install.${TRAVIS_OS_NAME}.sh before_script: - ccache -z - ./CI/before_script.${TRAVIS_OS_NAME}.sh script: - cd ./build - ${ANALYZE} make -j3; fi - 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: TES3MP/openmw-tes3mp #notifications: # email: # recipients: # - corrmage+travis-ci@gmail.com # on_success: change # on_failure: always # irc: # channels: # - "chat.freenode.net#openmw" # on_success: change # on_failure: always # use_notice: true ================================================ FILE: AUTHORS.md ================================================ Contributors ============ The OpenMW project was started in 2008 by Nicolay Korslund. In the course of years many people have contributed to the project. If you feel your name is missing from this list, please add it to `AUTHORS.md`. Programmers ----------- Bret Curtis (psi29a) - Project leader 2019-present Marc Zinnschlag (Zini) - Project leader 2010-2018 Nicolay Korslund - Project leader 2008-2010 scrawl - Top contributor Adam Hogan (aurix) Aesylwinn aegis AHSauge Aleksandar Jovanov Alex Haddad (rainChu) Alex McKibben alexanderkjall Alexander Nadeau (wareya) Alexander Olofsson (Ananace) Alex Rice Alex S (docwest) Allofich Andrei Kortunov (akortunov) AnyOldName3 Ardekantur Armin Preiml Artem Kotsynyak (greye) Artem Nykolenko (anikm21) artemutin Arthur Moore (EmperorArthur) Assumeru athile Aussiemon Austin Salgat (Salgat) Ben Shealy (bentsherman) Berulacks Britt Mathis (galdor557) Capostrophic Carl Maxwell cc9cii Cédric Mocquillon Chris Boyce (slothlife) Chris Robinson (KittyCat) Cody Glassman (Wazabear) Coleman Smith (olcoal) Cory F. Cohen (cfcohen) Cris Mihalache (Mirceam) crussell187 DanielVukelich darkf David Cernat (davidcernat) Declan Millar (declan-millar) devnexen Dieho Dmitry Shkurskiy (endorph) Douglas Diniz (Dgdiniz) Douglas Mencken (dougmencken) dreamer-dead David Teviotdale (dteviot) Diggory Hardy Dmitry Marakasov (AMDmi3) Edmondo Tommasina (edmondo) Eduard Cot (trombonecot) Eli2 Emanuel Guével (potatoesmaster) eroen escondida Evgeniy Mineev (sandstranger) Federico Guerra (FedeWar) Fil Krynicki (filkry) Finbar Crago (finbar-crago) Florian Weber (Florianjw) Frédéric Chardon (fr3dz10) Gaëtan Dezeiraud (Brouilles) Gašper Sedej Gijsbert ter Horst (Ghostbird) Gohan1989 gugus/gus guidoj Haoda Wang (h313) hristoast Internecine Jackerty Jacob Essex (Yacoby) Jacob Turnbull (Tankinfrank) Jake Westrip (16bitint) James Carty (MrTopCat) James Moore (moore.work) James Stephens (james-h-stephens) Jan-Peter Nilsson (peppe) Jan Borsodi (am0s) Jason Hooks (jhooks) jeaye jefetienne Jeffrey Haines (Jyby) Jengerer Jiří Kuneš (kunesj) Joe Wilkerson (neuralroberts) Joel Graff (graffy) John Blomberg (fstp) Jordan Ayers Jordan Milne Josua Grawitter Jules Blok (Armada651) julianko Julien Voisin (jvoisin/ap0) Karl-Felix Glatzer (k1ll) Kevin Poitra (PuppyKevin) Koncord Kurnevsky Evgeny (kurnevsky) Lars Söderberg (Lazaroth) lazydev Leon Krieg (lkrieg) Leon Saunders (emoose) logzero lohikaarme Lordrea Łukasz Gołębiewski (lukago) Lukasz Gromanowski (lgro) Marc Bouvier (CramitDeFrog) Marcin Hulist (Gohan) Mark Siewert (mark76) Marco Melletti (mellotanica) Marco Schulze Martin Otto (MAtahualpa) Mateusz Kołaczek (PL_kolek) Mateusz Malisz (malice) Max Henzerling (SaintMercury) megaton Michael Hogan (Xethik) Michael Mc Donnell Michael Papageorgiou (werdanith) Michael Stopa (Stomy) Michał Ściubidło (mike-sc) Michał Bień (Glorf) Michał Moroz (dragonee) Miloslav Číž (drummyfish) Miroslav Puda (pakanek) MiroslavR Mitchell Schwitzer (schwitzerm) naclander Narmo Nat Meo (Utopium) Nathan Jeffords (blunted2night) NeveHanter Nialsy Nick Crawford (nighthawk469) Nikolay Kasyanov (corristo) nobrakal Nolan Poe (nopoe) Oleg Chkan (mrcheko) Paul Cercueil (pcercuei) Paul McElroy (Greendogo) pchan3 Perry Hugh Petr Mikheev (ptmikheev) Phillip Andrews (PhillipAnd) Pi03k Pieter van der Kloet (pvdk) pkubik PLkolek PlutonicOverkill Radu-Marius Popovici (rpopovici) Rafael Moura (dhustkoder) rdimesio rexelion riothamus Rob Cutmore (rcutmore) Robert MacGregor (Ragora) Rohit Nirmal Roman Melnik (Kromgart) Roman Proskuryakov (kpp) Roman Siromakha (elsid) Sandy Carter (bwrsandman) Scott Howard (maqifrnswa) Sebastian Wick (swick) Sergey Fukanchik Sergey Shambir (sergey-shambir) sergoz ShadowRadiance Siimacore Simon Meulenbeek (simonmb) sir_herrbatka smbas Sophie Kirschner (pineapplemachine) spycrab Stefan Galowicz (bogglez) Stanislav Bobrov (Jiub) Stanislaw Halik (sthalik) Star-Demon stil-t svaante Sylvain Thesnieres (Garvek) t6 terrorfisch Tess (tescoShoppah) thegriglat Thomas Luppi (Digmaster) tlmullis tri4ng1e Thoronador Tom Mason (wheybags) Torben Leif Carrington (TorbenC) unelsson uramer viadanna Vincent Heuken Vladimir Panteleev (CyberShadow) Wang Ryu (bzzt) Will Herrmann (Thunderforge) vocollapse xyzz Yohaulticetl Yuri Krupenin zelurker Noah Gooder Andrew Appuhamy (andrew-app) Documentation ------------- Adam Bowen (adamnbowen) Alejandro Sanchez (HiPhish) Bodillium Bret Curtis (psi29a) Cramal David Walley (Loriel) Diego Crespo Joakim Berg (lysol90) Ryan Tucker (Ravenwing) sir_herrbatka Packagers --------- Alexander Olofsson (Ananace) - Windows and Flatpak Alexey Sokolov (DarthGandalf) - Gentoo Linux Bret Curtis (psi29a) - Debian and Ubuntu Linux Edmondo Tommasina (edmondo) - Gentoo Linux Julian Ospald (hasufell) - Gentoo Linux Karl-Felix Glatzer (k1ll) - Linux Binaries Kenny Armstrong (artorius) - Fedora Linux Nikolay Kasyanov (corristo) - Mac OS X Sandy Carter (bwrsandman) - Arch Linux Public Relations and Translations --------------------------------- Artem Kotsynyak (greye) - Russian News Writer Dawid Lakomy (Vedyimyn) - Polish News Writer ElderTroll - Release Manager Jim Clauwaert (Zedd) - Public Outreach juanmnzsk8 - Spanish News Writer Julien Voisin (jvoisin/ap0) - French News Writer Kingpix - Italian News Writer Lukasz Gromanowski (lgro) - English News Writer Martin Otto (Atahualpa) - Podcaster, Public Outreach, German Translator Mickey Lyle (raevol) - Release Manager Nekochan - English News Writer penguinroad - Indonesian News Writer Pithorn - Chinese News Writer sir_herrbatka - Polish News Writer spyboot - German Translator Tom Koenderink (Okulo) - English News Writer Website ------- Lukasz Gromanowski (Lgro) - Website Administrator Ryan Sardonic (Wry) - Wiki Editor sir_herrbatka - Forum Administrator Formula Research ---------------- Hrnchamd Epsilon fragonard Greendogo HiPhish modred11 Myckel natirips Sadler Artwork ------- Necrod - OpenMW Logo Mickey Lyle (raevol) - Wordpress Theme Tom Koenderink (Okulo), SirHerrbatka, crysthala, Shnatsel, Lamoot - OpenMW Editor Icons Additional Credits ------------------ In this section we would like to thank people not part of OpenMW for their work. Thanks to Maxim Nikolaev, for allowing us to use his excellent Morrowind fan-art on our website and in other places. Thanks to DokterDume, for kindly providing us with the Moon and Star logo, used as the application icon and project logo. Thanks to Kevin Ryan, for creating the icon used for the Data Files tab of the OpenMW Launcher. Thanks to DejaVu team, for their DejaVuLGCSansMono fontface, see DejaVuFontLicense.txt for their license terms. ================================================ FILE: CHANGELOG.md ================================================ 0.47.0 ------ Bug #1662: Qt4 and Windows binaries crash if there's a non-ASCII character in a file path/config path Bug #1901: Actors colliding behaviour is different from vanilla Bug #1952: Incorrect particle lighting Bug #2069: Fireflies in Fireflies invade Morrowind look wrong Bug #2311: Targeted scripts are not properly supported on non-unique RefIDs Bug #2473: Unable to overstock merchants Bug #2976: [reopened]: Issues combining settings from the command line and both config files Bug #3137: Walking into a wall prevents jumping Bug #3372: Projectiles and magic bolts go through moving targets Bug #3676: NiParticleColorModifier isn't applied properly Bug #3714: Savegame fails to load due to conflict between SpellState and MagicEffects Bug #3789: Crash in visitEffectSources while in battle Bug #3862: Random container contents behave differently than vanilla Bug #3929: Leveled list merchant containers respawn on barter Bug #4021: Attributes and skills are not stored as floats Bug #4039: Multiple followers should have the same following distance Bug #4055: Local scripts don't inherit variables from their base record Bug #4083: Door animation freezes when colliding with actors Bug #4247: Cannot walk up stairs in Ebonheart docks Bug #4357: OpenMW-CS: TopicInfos index sorting and rearranging isn't fully functional Bug #4363: OpenMW-CS: Defect in Clone Function for Dialogue Info records Bug #4447: Actor collision capsule shape allows looking through some walls Bug #4465: Collision shape overlapping causes twitching Bug #4476: Abot Gondoliers: player hangs in air during scenic travel Bug #4568: Too many actors in one spot can push other actors out of bounds Bug #4623: Corprus implementation is incorrect Bug #4631: Setting MSAA level too high doesn't fall back to highest supported level Bug #4764: Data race in osg ParticleSystem Bug #4765: Data race in ChunkManager -> Array::setBinding Bug #4774: Guards are ignorant of an invisible player that tries to attack them Bug #5026: Data races with rain intensity uniform set by sky and used by water Bug #5101: Hostile followers travel with the player Bug #5108: Savegame bloating due to inefficient fog textures format Bug #5165: Active spells should use real time intead of timestamps Bug #5300: NPCs don't switch from torch to shield when starting combat Bug #5358: ForceGreeting always resets the dialogue window completely Bug #5363: Enchantment autocalc not always 0/1 Bug #5364: Script fails/stops if trying to startscript an unknown script Bug #5367: Selecting a spell on an enchanted item per hotkey always plays the equip sound Bug #5369: Spawnpoint in the Grazelands doesn't produce oversized creatures Bug #5370: Opening an unlocked but trapped door uses the key Bug #5384: OpenMW-CS: Deleting an instance requires reload of scene window to show in editor Bug #5387: Move/MoveWorld don't update the object's cell properly Bug #5391: Races Redone 1.2 bodies don't show on the inventory Bug #5397: NPC greeting does not reset if you leave + reenter area Bug #5400: OpenMW-CS: Verifier checks race of non-skin bodyparts Bug #5403: Enchantment effect doesn't show on an enemy during death animation Bug #5415: Environment maps in ebony cuirass and HiRez Armors Indoril cuirass don't work Bug #5416: Junk non-node records before the root node are not handled gracefully Bug #5422: The player loses all spells when resurrected Bug #5423: Guar follows actors too closely Bug #5424: Creatures do not headtrack player Bug #5425: Poison effect only appears for one frame Bug #5427: GetDistance unknown ID error is misleading Bug #5431: Physics performance degradation after a specific number of actors on a scene Bug #5435: Enemies can't hurt the player when collision is off Bug #5441: Enemies can't push a player character when in critical strike stance Bug #5451: Magic projectiles don't disappear with the caster Bug #5452: Autowalk is being included in savegames Bug #5469: Local map is reset when re-entering certain cells Bug #5472: Mistify mod causes CTD in 0.46 on Mac Bug #5473: OpenMW-CS: Cell border lines don't update properly on terrain change Bug #5479: NPCs who should be walking around town are standing around without walking Bug #5484: Zero value items shouldn't be able to be bought or sold for 1 gold Bug #5485: Intimidate doesn't increase disposition on marginal wins Bug #5490: Hits to carried left slot aren't redistributed if there's no shield equipped Bug #5499: Faction advance is available when requirements not met Bug #5502: Dead zone for analogue stick movement is too small Bug #5507: Sound volume is not clamped on ingame settings update Bug #5525: Case-insensitive search in the inventory window does not work with non-ASCII characters Bug #5531: Actors flee using current rotation by axis x Bug #5539: Window resize breaks when going from a lower resolution to full screen resolution Bug #5548: Certain exhausted topics can be highlighted again even though there's no new dialogue Bug #5557: Diagonal movement is noticeably slower with analogue stick Bug #5588: Randomly clicking on the journal's right-side page when it's empty shows random topics Bug #5603: Setting constant effect cast style doesn't correct effects view Bug #5604: Only one valid NIF root node is loaded from a single file Bug #5611: Usable items with "0 Uses" should be used only once Bug #5619: Input events are queued during save loading Bug #5622: Can't properly interact with the console when in pause menu Bug #5627: Bookart not shown if it isn't followed by
statement Bug #5633: Damage Spells in effect before god mode is enabled continue to hurt the player character and can kill them Bug #5639: Tooltips cover Messageboxes Bug #5644: Summon effects running on the player during game initialization cause crashes Bug #5656: Sneaking characters block hits while standing Bug #5661: Region sounds don't play at the right interval Bug #5675: OpenMW-CS: FRMR subrecords are saved with the wrong MastIdx Bug #5680: Bull Netches incorrectly aim over the player character's head and always miss Bug #5681: Player character can clip or pass through bridges instead of colliding against them Bug #5687: Bound items covering the same inventory slot expiring at the same time freezes the game Bug #5688: Water shader broken indoors with enable indoor shadows = false Bug #5695: ExplodeSpell for actors doesn't target the ground Bug #5703: OpenMW-CS menu system crashing on XFCE Bug #5706: AI sequences stop looping after the saved game is reloaded Bug #5713: OpenMW-CS: Collada models are corrupted in Qt-based scene view Bug #5731: OpenMW-CS: skirts are invisible on characters Bug #5739: Saving and loading the save a second or two before hitting the ground doesn't count fall damage Bug #5758: Paralyzed actors behavior is inconsistent with vanilla Bug #5762: Movement solver is insufficiently robust Bug #5800: Equipping a CE enchanted ring deselects an already equipped and selected enchanted ring from the spell menu Bug #5807: Video decoding crash on ARM Bug #5821: NPCs from mods getting removed if mod order was changed Bug #5835: OpenMW doesn't accept negative values for NPC's hello, alarm, fight, and flee Bug #5836: OpenMW dialogue/greeting/voice filter doesn't accept negative Ai values for NPC's hello, alarm, fight, and flee Bug #5838: Local map and other menus become blank in some locations while playing Wizards' Islands mod. Bug #5840: GetSoundPlaying "Health Damage" doesn't play when NPC hits target with shield effect ( vanilla engine behavior ) Bug #5841: Can't Cast Zero Cost Spells When Magicka is < 0 Bug #5869: Guards can initiate arrest dialogue behind locked doors Bug #5871: The console appears if you type the Russian letter "Ё" in the name of the enchantment Bug #5877: Effects appearing with empty icon Bug #5899: Visible modal windows and dropdowns crashing game on exit Bug #5902: NiZBufferProperty is unable to disable the depth test Bug #5906: Sunglare doesn't work with Mesa drivers and AMD GPUs Bug #5912: ImprovedBound mod doesn't work Bug #5914: BM: The Swimmer can't reach destination Bug #5923: Clicking on empty spaces between journal entries might show random topics Bug #5934: AddItem command doesn't accept negative values Bug #5975: NIF controllers from sheath meshes are used Bug #5991: Activate should always be allowed for inventory items Bug #5995: NiUVController doesn't calculate the UV offset properly Bug #6007: Crash when ending cutscene is playing Bug #6016: Greeting interrupts Fargoth's sneak-walk Bug #6022: OpenMW-CS: Terrain selection is not updated when undoing/redoing terrain changes Bug #6023: OpenMW-CS: Clicking on a reference in "Terrain land editing" mode discards corresponding select/edit action Bug #6028: Particle system controller values are incorrectly used Bug #6035: OpenMW-CS: Circle brush in "Terrain land editing" mode sometimes includes vertices outside its radius Bug #6036: OpenMW-CS: Terrain selection at the border of cells omits certain corner vertices 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 #6294: Game crashes with empty pathgrid Feature #390: 3rd person look "over the shoulder" Feature #832: OpenMW-CS: Handle deleted references Feature #1536: Show more information about level on menu Feature #2159: "Graying out" exhausted dialogue topics Feature #2386: Distant Statics in the form of Object Paging Feature #2404: Levelled List can not be placed into a container Feature #2686: Timestamps in openmw.log Feature #2798: Mutable ESM records Feature #3171: OpenMW-CS: Instance drag selection Feature #3983: Wizard: Add link to buy Morrowind Feature #4201: Projectile-projectile collision Feature #4486: Handle crashes on Windows Feature #4894: Consider actors as obstacles for pathfinding Feature #4899: Alpha-To-Coverage Anti-Aliasing for alpha testing Feature #4917: Do not trigger NavMesh update when RecastMesh update should not change NavMesh Feature #4977: Use the "default icon.tga" when an item's icon is not found Feature #5043: Head Bobbing Feature #5199: OpenMW-CS: Improve scene view colors Feature #5297: Add a search function to the "Datafiles" tab of the OpenMW launcher Feature #5362: Show the soul gems' trapped soul in count dialog Feature #5445: Handle NiLines Feature #5456: Basic collada animation support Feature #5457: Realistic diagonal movement Feature #5486: Fixes trainers to choose their training skills based on their base skill points Feature #5500: Prepare enough navmesh tiles before scene loading ends Feature #5511: Add in game option to toggle HRTF support in OpenMW Feature #5519: Code Patch tab in launcher Feature #5524: Resume failed script execution after reload Feature #5545: Option to allow stealing from an unconscious NPC during combat Feature #5551: Do not reboot PC after OpenMW installation on Windows Feature #5563: Run physics update in background thread Feature #5579: MCP SetAngle enhancement Feature #5580: Service refusal filtering Feature #5610: Actors movement should be smoother Feature #5642: Ability to attach arrows to actor skeleton instead of bow mesh Feature #5649: Skyrim SE compressed BSA format support Feature #5672: Make stretch menu background configuration more accessible Feature #5692: Improve spell/magic item search to factor in magic effect names Feature #5730: Add graphic herbalism option to the launcher and documents Feature #5771: ori command should report where a mesh is loaded from and whether the x version is used. Feature #5813: Instanced groundcover support Feature #5814: Bsatool should be able to create BSA archives, not only to extract it Feature #5828: Support more than 8 lights Feature #5910: Fall back to delta time when physics can't keep up Feature #5980: Support Bullet with double precision instead of one with single precision Feature #6024: OpenMW-CS: Selecting terrain in "Terrain land editing" should support "Add to selection" and "Remove from selection" modes Feature #6033: Include pathgrid to navigation mesh Feature #6034: Find path based on area cost depending on NPC stats Task #5480: Drop Qt4 support Task #5520: Improve cell name autocompleter implementation 0.46.0 ------ Bug #1515: Opening console masks dialogue, inventory menu Bug #1933: Actors can have few stocks of the same item Bug #2395: Duplicated plugins in the launcher when multiple data directories provide the same plugin Bug #2679: Unable to map mouse wheel under control settings Bug #2969: Scripted items can stack Bug #2976: [reopened in 0.47] Data lines in global openmw.cfg take priority over user openmw.cfg Bug #2987: Editor: some chance and AI data fields can overflow Bug #3006: 'else if' operator breaks script compilation Bug #3109: SetPos/Position handles actors differently Bug #3282: Unintended behaviour when assigning F3 and Windows keys Bug #3550: Companion from mod attacks the air after combat has ended Bug #3609: Items from evidence chest are not considered to be stolen if player is allowed to use the chest Bug #3623: Display scaling breaks mouse recognition Bug #3725: Using script function in a non-conditional expression breaks script compilation Bug #3733: Normal maps are inverted on mirrored UVs Bug #3765: DisableTeleporting makes Mark/Recall/Intervention effects undetectable Bug #3778: [Mod] Improved Thrown Weapon Projectiles - weapons have wrong transformation during throw animation Bug #3812: Wrong multiline tooltips width when word-wrapping is enabled Bug #3894: Hostile spell effects not detected/present on first frame of OnPCHitMe Bug #3977: Non-ASCII characters in object ID's are not supported Bug #4009: Launcher does not show data files on the first run after installing Bug #4077: Enchanted items are not recharged if they are not in the player's inventory Bug #4141: PCSkipEquip isn't set to 1 when reading books/scrolls Bug #4240: Ash storm origin coordinates and hand shielding animation behavior are incorrect Bug #4262: Rain settings are hardcoded Bug #4270: Closing doors while they are obstructed desyncs closing sfx Bug #4276: Resizing character window differs from vanilla Bug #4284: ForceSneak behaviour is inconsistent if the target has AiWander package Bug #4329: Removed birthsign abilities are restored after reloading the save Bug #4341: Error message about missing GDB is too vague Bug #4383: Bow model obscures crosshair when arrow is drawn Bug #4384: Resist Normal Weapons only checks ammunition for ranged weapons Bug #4411: Reloading a saved game while falling prevents damage in some cases Bug #4449: Value returned by GetWindSpeed is incorrect Bug #4456: AiActivate should not be cancelled after target activation Bug #4493: If the setup doesn't find what it is expecting, it fails silently and displays the requester again instead of letting the user know what wasn't found. Bug #4523: "player->ModCurrentFatigue -0.001" in global script does not cause the running player to fall Bug #4540: Rain delay when exiting water Bug #4594: Actors without AI packages don't use Hello dialogue Bug #4598: Script parser does not support non-ASCII characters Bug #4600: Crash when no sound output is available or --no-sound is used. Bug #4601: Filtering referenceables by gender is broken Bug #4639: Black screen after completing first mages guild mission + training Bug #4650: Focus is lost after pressing ESC in confirmation dialog inside savegame dialog Bug #4680: Heap corruption on faulty esp Bug #4701: PrisonMarker record is not hardcoded like other markers Bug #4703: Editor: it's possible to preview levelled list records Bug #4705: Editor: unable to open exterior cell views from Instances table Bug #4714: Crash upon game load in the repair menu while the "Your repair failed!" message is active Bug #4715: "Cannot get class of an empty object" exception after pressing ESC in the dialogue mode Bug #4720: Inventory avatar has shield with two-handed weapon during [un]equipping animation Bug #4723: ResetActors command works incorrectly Bug #4736: LandTexture records overrides do not work Bug #4745: Editor: Interior cell lighting field values are not displayed as colors Bug #4746: Non-solid player can't run or sneak Bug #4747: Bones are not read from X.NIF file for NPC animation Bug #4748: Editor: Cloned, moved, added instances re-use RefNum indices Bug #4750: Sneaking doesn't work in first person view if the player is in attack ready state Bug #4756: Animation issues with VAOs Bug #4757: Content selector: files can be cleared when there aren't any files to clear Bug #4768: Fallback numerical value recovery chokes on invalid arguments Bug #4775: Slowfall effect resets player jumping flag Bug #4778: Interiors of Illusion puzzle in Sotha Sil Expanded mod is broken Bug #4783: Blizzard behavior is incorrect Bug #4787: Sneaking makes 1st person walking/bobbing animation super-slow Bug #4797: Player sneaking and running stances are not accounted for when in air Bug #4800: Standing collisions are not updated immediately when an object is teleported without a cell change Bug #4802: You can rest before taking falling damage from landing from a jump Bug #4803: Stray special characters before begin statement break script compilation Bug #4804: Particle system with the "Has Sizes = false" causes an exception Bug #4805: NPC movement speed calculations do not take race Weight into account Bug #4810: Raki creature broken in OpenMW Bug #4813: Creatures with known file but no "Sound Gen Creature" assigned use default sounds Bug #4815: "Finished" journal entry with lower index doesn't close journal, SetJournalIndex closes journal Bug #4820: Spell absorption is broken Bug #4823: Jail progress bar works incorrectly Bug #4826: Uninitialized memory in unit test Bug #4827: NiUVController is handled incorrectly Bug #4828: Potion looping effects VFX are not shown for NPCs Bug #4837: CTD when a mesh with NiLODNode root node with particles is loaded Bug #4841: Russian localization ignores implicit keywords Bug #4844: Data race in savegame loading / GlobalMap render Bug #4847: Idle animation reset oddities Bug #4851: No shadows since switch to OSG Bug #4860: Actors outside of processing range visible for one frame after spawning Bug #4867: Arbitrary text after local variable declarations breaks script compilation Bug #4876: AI ratings handling inconsistencies Bug #4877: Startup script executes only on a new game start Bug #4879: SayDone returns 0 on the frame Say is called Bug #4888: Global variable stray explicit reference calls break script compilation Bug #4896: Title screen music doesn't loop Bug #4902: Using scrollbars in settings causes resolution to change Bug #4904: Editor: Texture painting with duplicate of a base-version texture Bug #4911: Editor: QOpenGLContext::swapBuffers() warning with Qt5 Bug #4916: Specular power (shininess) material parameter is ignored when shaders are used. Bug #4918: Abilities don't play looping VFX when they're initially applied Bug #4920: Combat AI uses incorrect invisibility check Bug #4922: Werewolves can not attack if the transformation happens during attack Bug #4927: Spell effect having both a skill and an attribute assigned is a fatal error Bug #4932: Invalid records matching when loading save with edited plugin Bug #4933: Field of View not equal with Morrowind Bug #4938: Strings from subrecords with actually empty headers can't be empty Bug #4942: Hand-to-Hand attack type is chosen randomly when "always use best attack" is turned off Bug #4945: Poor random magic magnitude distribution Bug #4947: Player character doesn't use lip animation Bug #4948: Footstep sounds while levitating on ground level Bug #4952: Torches held by NPCs flicker too quickly Bug #4961: Flying creature combat engagement takes z-axis into account Bug #4963: Enchant skill progress is incorrect Bug #4964: Multiple effect spell projectile sounds play louder than vanilla Bug #4965: Global light attenuation settings setup is lacking Bug #4969: "Miss" sound plays for any actor Bug #4972: Player is able to use quickkeys while disableplayerfighting is active Bug #4979: AiTravel maximum range depends on "actors processing range" setting Bug #4980: Drowning mechanics is applied for actors indifferently from distance to player Bug #4984: "Friendly hits" feature should be used only for player's followers Bug #4989: Object dimension-dependent VFX scaling behavior is inconsistent Bug #4990: Dead bodies prevent you from hitting Bug #4991: Jumping occasionally takes too much fatigue Bug #4999: Drop instruction behaves differently from vanilla Bug #5001: Possible data race in the Animation::setAlpha() Bug #5004: Werewolves shield their eyes during storm Bug #5012: "Take all" on owned container generates a messagebox per item Bug #5018: Spell tooltips don't support purely negative magnitudes Bug #5025: Data race in the ICO::setMaximumNumOfObjectsToCompilePerFrame() Bug #5028: Offered price caps are not trading-specific Bug #5038: Enchanting success chance calculations are blatantly wrong Bug #5047: # in cell names sets color Bug #5050: Invalid spell effects are not handled gracefully Bug #5055: Mark, Recall, Intervention magic effect abilities have no effect when added and removed in the same frame Bug #5056: Calling Cast function on player doesn't equip the spell but casts it Bug #5059: Modded animation with combined attack keys always does max damage and can double damage Bug #5060: Magic effect visuals stop when death animation begins instead of when it ends Bug #5063: Shape named "Tri Shadow" in creature mesh is visible if it isn't hidden Bug #5067: Ranged attacks on unaware opponents ("critical hits") differ from the vanilla engine Bug #5069: Blocking creatures' attacks doesn't degrade shields Bug #5073: NPCs open doors in front of them even if they don't have to Bug #5074: Paralyzed actors greet the player Bug #5075: Enchanting cast style can be changed if there's no object Bug #5078: DisablePlayerLooking is broken Bug #5081: OpenMW-CS: Apparatus type "Alembic" is erroneously named "Albemic" Bug #5082: Scrolling with controller in GUI mode is broken Bug #5087: Some valid script names can't be used as string arguments Bug #5089: Swimming/Underwater creatures only swim around ground level Bug #5092: NPCs with enchanted weapons play sound when out of charges Bug #5093: Hand to hand sound plays on knocked out enemies Bug #5097: String arguments can't be parsed as number literals in scripts Bug #5099: Non-swimming enemies will enter water if player is water walking Bug #5103: Sneaking state behavior is still inconsistent Bug #5104: Black Dart's enchantment doesn't trigger at low Enchant levels Bug #5106: Still can jump even when encumbered Bug #5110: ModRegion with a redundant numerical argument breaks script execution Bug #5112: Insufficient magicka for current spell not reflected on HUD icon Bug #5113: Unknown alchemy question mark not centered Bug #5123: Script won't run on respawn Bug #5124: Arrow remains attached to actor if pulling animation was cancelled Bug #5126: Swimming creatures without RunForward animations are motionless during combat Bug #5134: Doors rotation by "Lock" console command is inconsistent Bug #5136: LegionUniform script: can not access local variables Bug #5137: Textures with Clamp Mode set to Clamp instead of Wrap are too dark outside the boundaries Bug #5138: Actors stuck in half closed door Bug #5149: Failing lock pick attempts isn't always a crime Bug #5155: Lock/unlock behavior differs from vanilla Bug #5158: Objects without a name don't fallback to their ID Bug #5159: NiMaterialColorController can only control the diffuse color Bug #5161: Creature companions can't be activated when they are knocked down Bug #5163: UserData is not copied during node cloning Bug #5164: Faction owned items handling is incorrect Bug #5166: Scripts still should be executed after player's death Bug #5167: Player can select and cast spells before magic menu is enabled Bug #5168: Force1stPerson and Force3rdPerson commands are not really force view change Bug #5169: Nested levelled items/creatures have significantly higher chance not to spawn Bug #5175: Random script function returns an integer value Bug #5177: Editor: Unexplored map tiles get corrupted after a file with terrain is saved Bug #5182: OnPCEquip doesn't trigger on skipped beast race attempts to equip something not equippable by beasts Bug #5186: Equipped item enchantments don't affect creatures Bug #5190: On-strike enchantments can be applied to and used with non-projectile ranged weapons Bug #5196: Dwarven ghosts do not use idle animations Bug #5206: A "class does not have NPC stats" error when player's follower kills an enemy with damage spell Bug #5209: Spellcasting ignores race height Bug #5210: AiActivate allows actors to open dialogue and inventory windows Bug #5211: Screen fades in if the first loaded save is in interior cell Bug #5212: AiTravel does not work for actors outside of AI processing range Bug #5213: SameFaction script function is broken Bug #5218: Crash when disabling ToggleBorders Bug #5220: GetLOS crashes when actor isn't loaded Bug #5222: Empty cell name subrecords are not saved Bug #5223: Bow replacement during attack animation removes attached arrow Bug #5226: Reputation should be capped Bug #5229: Crash if mesh controller node has no data node Bug #5239: OpenMW-CS does not support non-ASCII characters in path names Bug #5241: On-self absorb spells cannot be detected Bug #5242: ExplodeSpell behavior differs from Cast behavior Bug #5246: Water ripples persist after cell change Bug #5249: Wandering NPCs start walking too soon after they hello Bug #5250: Creatures display shield ground mesh instead of shield body part Bug #5255: "GetTarget, player" doesn't return 1 during NPC hello Bug #5261: Creatures can sometimes become stuck playing idles and never wander again Bug #5264: "Damage Fatigue" Magic Effect Can Bring Fatigue below 0 Bug #5269: Editor: Cell lighting in resaved cleaned content files is corrupted Bug #5278: Console command Show doesn't fall back to global variable after local var not found Bug #5308: World map copying makes save loading much slower Bug #5313: Node properties of identical type are not applied in the correct order Bug #5326: Formatting issues in the settings.cfg Bug #5328: Skills aren't properly reset for dead actors Bug #5345: Dopey Necromancy does not work due to a missing quote Bug #5350: An attempt to launch magic bolt causes "AL error invalid value" error Bug #5352: Light source items' duration is decremented while they aren't visible Feature #1724: Handle AvoidNode Feature #2229: Improve pathfinding AI Feature #3025: Analogue gamepad movement controls Feature #3442: Default values for fallbacks from ini file Feature #3517: Multiple projectiles enchantment Feature #3610: Option to invert X axis Feature #3871: Editor: Terrain Selection Feature #3893: Implicit target for "set" function in console Feature #3980: In-game option to disable controller Feature #3999: Shift + Double Click should maximize/restore menu size Feature #4001: Toggle sneak controller shortcut Feature #4068: OpenMW-CS: Add a button to reset key bindings to defaults Feature #4129: Beta Comment to File Feature #4202: Open .omwaddon files without needing to open openmw-cs first Feature #4209: Editor: Faction rank sub-table Feature #4255: Handle broken RepairedOnMe script function Feature #4316: Implement RaiseRank/LowerRank functions properly Feature #4360: Improve default controller bindings Feature #4544: Actors movement deceleration Feature #4673: Weapon sheathing Feature #4675: Support for NiRollController Feature #4708: Radial fog support Feature #4730: Native animated containers support Feature #4784: Launcher: Duplicate Content Lists Feature #4812: Support NiSwitchNode Feature #4831: Item search in the player's inventory Feature #4836: Daytime node switch Feature #4840: Editor: Transient terrain change support Feature #4859: Make water reflections more configurable Feature #4882: Support for NiPalette node Feature #4887: Add openmw command option to set initial random seed Feature #4890: Make Distant Terrain configurable Feature #4944: Pause audio when OpenMW is minimized Feature #4958: Support eight blood types Feature #4962: Add casting animations for magic items Feature #4968: Scalable UI widget skins Feature #4971: OpenMW-CS: Make rotations display as degrees instead of radians Feature #4994: Persistent pinnable windows hiding Feature #5000: Compressed BSA format support Feature #5005: Editor: Instance window via Scene window Feature #5010: Native graphics herbalism support Feature #5031: Make GetWeaponType function return different values for tools Feature #5033: Magic armor mitigation for creatures Feature #5034: Make enchanting window stay open after a failed attempt Feature #5036: Allow scripted faction leaving Feature #5046: Gamepad thumbstick cursor speed Feature #5051: Provide a separate textures for scrollbars Feature #5091: Human-readable light source duration Feature #5094: Unix like console hotkeys Feature #5098: Allow user controller bindings Feature #5114: Refresh launcher mod list Feature #5121: Handle NiTriStrips and NiTriStripsData Feature #5122: Use magic glow for enchanted arrows Feature #5131: Custom skeleton bones Feature #5132: Unique animations for different weapon types Feature #5146: Safe Dispose corpse Feature #5147: Show spell magicka cost in spell buying window Feature #5170: Editor: Land shape editing, land selection Feature #5172: Editor: Delete instances/references with keypress in scene window Feature #5193: Shields sheathing Feature #5201: Editor: Show tool outline in scene view, when using editmodes Feature #5219: Impelement TestCells console command Feature #5224: Handle NiKeyframeController for NiTriShape Feature #5274: Editor: Keyboard shortcut to drop objects to ground/obstacle in scene view Feature #5304: Morrowind-style bump-mapping Feature #5311: Support for gyroscopic input (e.g. Android) Feature #5314: Ingredient filter in the alchemy window Task #4686: Upgrade media decoder to a more current FFmpeg API Task #4695: Optimize Distant Terrain memory consumption Task #4789: Optimize cell transitions Task #4721: Add NMake support to the Windows prebuild script 0.45.0 ------ Bug #1875: Actors in inactive cells don't heal from resting Bug #1990: Sunrise/sunset not set correct Bug #2131: Lustidrike's spell misses the player every time Bug #2222: Fatigue's effect on selling price is backwards Bug #2256: Landing sound not playing when jumping immediately after landing Bug #2274: Thin platform clips through player character instead of lifting Bug #2326: After a bound item expires the last equipped item of that type is not automatically re-equipped Bug #2446: Restore Attribute/Skill should allow restoring drained attributes Bug #2455: Creatures attacks degrade armor Bug #2562: Forcing AI to activate a teleport door sometimes causes a crash Bug #2626: Resurrecting the player does not resume the game Bug #2772: Non-existing class or faction freezes the game Bug #2835: Player able to slowly move when overencumbered Bug #2852: No murder bounty when a player follower commits murder Bug #2862: [macOS] Can't quit launcher using Command-Q or OpenMW->Quit Bug #2872: Tab completion in console doesn't work with explicit reference Bug #2971: Compiler did not reject lines with naked expressions beginning with x.y Bug #3049: Drain and Fortify effects are not properly applied on health, magicka and fatigue Bug #3059: Unable to hit with marksman weapons when too close to an enemy Bug #3072: Fatal error on AddItem that has a script containing Equip Bug #3219: NPC and creature initial position tracing down limit is too small Bug #3249: Fixed revert function not updating views properly Bug #3288: TrueType fonts are handled incorrectly Bug #3374: Touch spells not hitting kwama foragers Bug #3486: [Mod] NPC Commands does not work Bug #3533: GetSpellEffects should detect effects with zero duration Bug #3591: Angled hit distance too low Bug #3629: DB assassin attack never triggers creature spawning Bug #3681: OpenMW-CS: Clicking Scripts in Preferences spawns many color pickers Bug #3762: AddSoulGem and RemoveSpell redundant count arguments break script execution Bug #3788: GetPCInJail and GetPCTraveling do not work as in vanilla Bug #3836: Script fails to compile when command argument contains "\n" Bug #3876: Landscape texture painting is misaligned Bug #3890: Magic light source attenuation is inaccurate Bug #3897: Have Goodbye give all choices the effects of Goodbye Bug #3911: [macOS] Typing in the "Content List name" dialog box produces double characters Bug #3920: RemoveSpellEffects doesn't remove constant effects Bug #3948: AiCombat moving target aiming uses incorrect speed for magic projectiles Bug #3950: FLATTEN_STATIC_TRANSFORMS optimization breaks animated collision shapes Bug #3993: Terrain texture blending map is not upscaled Bug #3997: Almalexia doesn't pace Bug #4036: Weird behaviour of AI packages if package target has non-unique ID Bug #4047: OpenMW not reporting its version number in MacOS; OpenMW-CS not doing it fully Bug #4110: Fixed undo / redo menu text losing the assigned shortcuts Bug #4125: OpenMW logo cropped on bugtracker Bug #4215: OpenMW shows book text after last EOL tag Bug #4217: Fixme implementation differs from Morrowind's Bug #4221: Characters get stuck in V-shaped terrain Bug #4230: AiTravel package issues break some Tribunal quests Bug #4231: Infected rats from the "Crimson Plague" quest rendered unconscious by change in Drain Fatigue functionality Bug #4251: Stationary NPCs do not return to their position after combat Bug #4260: Keyboard navigation makes persuasion exploitable Bug #4271: Scamp flickers when attacking Bug #4274: Pre-0.43 death animations are not forward-compatible with 0.43+ Bug #4286: Scripted animations can be interrupted Bug #4291: Non-persistent actors that started the game as dead do not play death animations Bug #4292: CenterOnCell implementation differs from vanilla Bug #4293: Faction members are not aware of faction ownerships in barter Bug #4304: "Follow" not working as a second AI package Bug #4307: World cleanup should remove dead bodies only if death animation is finished Bug #4311: OpenMW does not handle RootCollisionNode correctly Bug #4327: Missing animations during spell/weapon stance switching Bug #4333: Keyboard navigation in containers is not intuitive Bug #4358: Running animation is interrupted when magic mode is toggled Bug #4368: Settings window ok button doesn't have key focus by default Bug #4378: On-self absorb spells restore stats Bug #4393: NPCs walk back to where they were after using ResetActors Bug #4416: Non-music files crash the game when they are tried to be played Bug #4419: MRK NiStringExtraData is handled incorrectly Bug #4426: RotateWorld behavior is incorrect Bug #4429: [Windows] Error on build INSTALL.vcxproj project (debug) with cmake 3.7.2 Bug #4431: "Lock 0" console command is a no-op Bug #4432: Guards behaviour is incorrect if they do not have AI packages Bug #4433: Guard behaviour is incorrect with Alarm = 0 Bug #4451: Script fails to compile when using "Begin, [ScriptName]" syntax Bug #4452: Default terrain texture bleeds through texture transitions Bug #4453: Quick keys behaviour is invalid for equipment Bug #4454: AI opens doors too slow Bug #4457: Item without CanCarry flag prevents shield autoequipping in dark areas Bug #4458: AiWander console command handles idle chances incorrectly Bug #4459: NotCell dialogue condition doesn't support partial matches Bug #4460: Script function "Equip" doesn't bypass beast restrictions Bug #4461: "Open" spell from non-player caster isn't a crime Bug #4463: %g format doesn't return more digits Bug #4464: OpenMW keeps AiState cached storages even after we cancel AI packages Bug #4467: Content selector: cyrillic characters are decoded incorrectly in plugin descriptions Bug #4469: Abot Silt Striders – Model turn 90 degrees on horizontal Bug #4470: Non-bipedal creatures with Weapon & Shield flag have inconsistent behaviour Bug #4474: No fallback when getVampireHead fails Bug #4475: Scripted animations should not cause movement Bug #4479: "Game" category on Advanced page is getting too long Bug #4480: Segfault in QuickKeysMenu when item no longer in inventory Bug #4489: Goodbye doesn't block dialogue hyperlinks Bug #4490: PositionCell on player gives "Error: tried to add local script twice" Bug #4494: Training cap based off Base Skill instead of Modified Skill Bug #4495: Crossbow animations blending is buggy Bug #4496: SpellTurnLeft and SpellTurnRight animation groups are unused Bug #4497: File names starting with x or X are not classified as animation Bug #4503: Cast and ExplodeSpell commands increase alteration skill Bug #4510: Division by zero in MWMechanics::CreatureStats::setAttribute Bug #4519: Knockdown does not discard movement in the 1st-person mode Bug #4527: Sun renders on water shader in some situations where it shouldn't Bug #4531: Movement does not reset idle animations Bug #4532: Underwater sfx isn't tied to 3rd person camera Bug #4539: Paper Doll is affected by GUI scaling Bug #4543: Picking cursed items through inventory (menumode) makes it disappear Bug #4545: Creatures flee from werewolves Bug #4551: Replace 0 sound range with default range separately Bug #4553: Forcegreeting on non-actor opens a dialogue window which cannot be closed Bug #4557: Topics with reserved names are handled differently from vanilla Bug #4558: Mesh optimizer: check for reserved node name is case-sensitive Bug #4560: OpenMW does not update pinned windows properly Bug #4563: Fast travel price logic checks destination cell instead of service actor cell Bug #4565: Underwater view distance should be limited Bug #4573: Player uses headtracking in the 1st-person mode Bug #4574: Player turning animations are twitchy Bug #4575: Weird result of attack animation blending with movement animations Bug #4576: Reset of idle animations when attack can not be started Bug #4591: Attack strength should be 0 if player did not hold the attack button Bug #4593: Editor: Instance dragging is broken Bug #4597: <> operator causes a compile error Bug #4604: Picking up gold from the ground only makes 1 grabbed Bug #4607: Scaling for animated collision shapes is applied twice Bug #4608: Falling damage is applied twice Bug #4611: Instant magic effects have 0 duration in custom spell cost calculations unlike vanilla Bug #4614: Crash due to division by zero when FlipController has no textures Bug #4615: Flicker effects for light sources are handled incorrectly Bug #4617: First person sneaking offset is not applied while the character is in air Bug #4618: Sneaking is possible while the character is flying Bug #4622: Recharging enchanted items with Soul Gems does not award experience if it fails Bug #4628: NPC record reputation, disposition and faction rank should have unsigned char type Bug #4633: Sneaking stance affects speed even if the actor is not able to crouch Bug #4641: GetPCJumping is handled incorrectly Bug #4644: %Name should be available for all actors, not just for NPCs Bug #4646: Weapon force-equipment messes up ongoing attack animations Bug #4648: Hud thinks that throwing weapons have condition Bug #4649: Levelup fully restores health Bug #4653: Length of non-ASCII strings is handled incorrectly in ESM reader Bug #4654: Editor: UpdateVisitor does not initialize skeletons for animated objects Bug #4656: Combat AI: back up behaviour is incorrect Bug #4668: Editor: Light source color is displayed as an integer Bug #4669: ToggleCollision should trace the player down after collision being enabled Bug #4671: knownEffect functions should use modified Alchemy skill Bug #4672: Pitch factor is handled incorrectly for crossbow animations Bug #4674: Journal can be opened when settings window is open Bug #4677: Crash in ESM reader when NPC record has DNAM record without DODT one Bug #4678: Crash in ESP parser when SCVR has no variable names Bug #4684: Spell Absorption is additive Bug #4685: Missing sound causes an exception inside Say command Bug #4689: Default creature soundgen entries are not used Bug #4691: Loading bar for cell should be moved up when text is still active at bottom of screen Feature #912: Editor: Add missing icons to UniversalId tables Feature #1221: Editor: Creature/NPC rendering Feature #1617: Editor: Enchantment effect record verifier Feature #1645: Casting effects from objects Feature #2606: Editor: Implemented (optional) case sensitive global search Feature #2787: Use the autogenerated collision box, if the creature mesh has no predefined one Feature #2845: Editor: add record view and preview default keybindings Feature #2847: Content selector: allow to copy the path to a file by using the context menu Feature #3083: Play animation when NPC is casting spell via script Feature #3103: Provide option for disposition to get increased by successful trade Feature #3276: Editor: Search - Show number of (remaining) search results and indicate a search without any results Feature #3641: Editor: Limit FPS in 3d preview window Feature #3703: Ranged sneak attack criticals Feature #4012: Editor: Write a log file if OpenCS crashes Feature #4222: 360° screenshots Feature #4256: Implement ToggleBorders (TB) console command Feature #4285: Support soundgen calls for activators Feature #4324: Add CFBundleIdentifier in Info.plist to allow for macOS function key shortcuts Feature #4345: Add equivalents for the command line commands to Launcher Feature #4404: Editor: All EnumDelegate fields should have their items sorted alphabetically Feature #4444: Per-group KF-animation files support Feature #4466: Editor: Add option to ignore "Base" records when running verifier Feature #4488: Make water shader rougher during rain Feature #4509: Show count of enchanted items in stack in the spells list Feature #4512: Editor: Use markers for lights and creatures levelled lists Feature #4548: Weapon priority: use the actual chance to hit the target instead of weapon skill Feature #4549: Weapon priority: use the actual damage in weapon rating calculations Feature #4550: Weapon priority: make ranged weapon bonus more sensible Feature #4579: Add option for applying Strength into hand to hand damage Feature #4581: Use proper logging system Feature #4624: Spell priority: don't cast hit chance-affecting spells if the enemy is not in respective stance at the moment Feature #4625: Weapon priority: use weighted mean for melee damage rating Feature #4626: Weapon priority: account for weapon speed Feature #4632: AI priority: utilize vanilla AI GMSTs for priority rating Feature #4636: Use sTo GMST in spellmaking menu Feature #4642: Batching potion creation Feature #4647: Cull actors outside of AI processing range Feature #4682: Use the collision box from basic creature mesh if the X one have no collisions Feature #4697: Use the real thrown weapon damage in tooltips and AI Task #2490: Don't open command prompt window on Release-mode builds automatically Task #4545: Enable is_pod string test Task #4605: Optimize skinning Task #4606: Support Rapture3D's OpenAL driver Task #4613: Incomplete type errors when compiling with g++ on OSX 10.9 Task #4621: Optimize combat AI Task #4643: Revise editor record verifying functionality Task #4645: Use constants instead of widely used magic numbers Task #4652: Move call to enemiesNearby() from InputManager::rest() to World::canRest() 0.44.0 ------ Bug #1428: Daedra summoning scripts aren't executed when the item is taken through the inventory Bug #1987: Some glyphs are not supported Bug #2254: Magic related visual effects are not rendered when loading a saved game Bug #2485: Journal alphabetical index doesn't match "Morrowind content language" setting Bug #2703: OnPCHitMe is not handled correctly Bug #2829: Incorrect order for content list consisting of a game file and an esp without dependencies Bug #2841: "Total eclipse" happens if weather settings are not defined. Bug #2897: Editor: Rename "Original creature" field Bug #3278: Editor: Unchecking "Auto Calc" flag changes certain values Bug #3343: Editor: ID sorting is case-sensitive in certain tables Bug #3557: Resource priority confusion when using the local data path as installation root Bug #3587: Pathgrid and Flying Creatures wrong behaviour – abotWhereAreAllBirdsGoing Bug #3603: SetPos should not skip weather transitions Bug #3618: Myar Aranath total conversion can't be started due to capital-case extension of the master file Bug #3638: Fast forwarding can move NPC inside objects Bug #3664: Combat music does not start in dialogue Bug #3696: Newlines are accompanied by empty rectangle glyph in dialogs Bug #3708: Controllers broken on macOS Bug #3726: Items with suppressed activation can be picked up via the inventory menu Bug #3783: [Mod] Abot's Silt Striders 1.16 - silt strider "falls" to ground and glides on floor during travel Bug #3863: Can be forced to not resist arrest if you cast Calm Humanoid on aggroed death warrant guards Bug #3884: Incorrect enemy behavior when exhausted Bug #3926: Installation Wizard places Morrowind.esm after Tribunal/Bloodmoon if it has a later file creation date Bug #4061: Scripts error on special token included in name Bug #4111: Crash when mouse over soulgem with a now-missing soul Bug #4122: Swim animation should not be interrupted during underwater attack Bug #4134: Battle music behaves different than vanilla Bug #4135: Reflecting an absorb spell different from vanilla Bug #4136: Enchanted weapons without "ignore normal weapons" flag don't bypass creature "ignore normal weapons" effect Bug #4143: Antialiasing produces graphical artifacts when used with shader lighting Bug #4159: NPCs' base skeleton files should not be optimized Bug #4177: Jumping/landing animation interference/flickering Bug #4179: NPCs do not face target Bug #4180: Weapon switch sound playing even though no weapon is switched Bug #4184: Guards can initiate dialogue even though you are far above them Bug #4190: Enchanted clothes changes visibility with Chameleon on equip/unequip Bug #4191: "screenshot saved" message also appears in the screenshot image Bug #4192: Archers in OpenMW have shorter attack range than archers in Morrowind Bug #4210: Some dialogue topics are not highlighted on first encounter Bug #4211: FPS drops after minimizing the game during rainy weather Bug #4216: Thrown weapon projectile doesn't rotate Bug #4223: Displayed spell casting chance must be 0 if player doesn't have enough magicka to cast it Bug #4225: Double "Activate" key presses with Mouse and Gamepad. Bug #4226: The current player's class should be default value in the class select menu Bug #4229: Tribunal/Bloodmoon summoned creatures fight other summons Bug #4233: W and A keys override S and D Keys Bug #4235: Wireframe mode affects local map Bug #4239: Quick load from container screen causes crash Bug #4242: Crime greetings display in Journal Bug #4245: Merchant NPCs sell ingredients growing on potted plants they own Bug #4246: Take armor condition into account when calcuting armor rating Bug #4250: Jumping is not as fluid as it was pre-0.43.0 Bug #4252: "Error in frame: FFmpeg exception: Failed to allocate input stream" message spam if OpenMW encounter non-music file in the Music folder Bug #4261: Magic effects from eaten ingredients always have 1 sec duration Bug #4263: Arrow position is incorrect in 3rd person view during attack for beast races Bug #4264: Player in god mode can be affected by some negative spell effects Bug #4269: Crash when hovering the faction section and the 'sAnd' GMST is missing (as in MW 1.0) Bug #4272: Root note transformations are discarded again Bug #4279: Sometimes cells are not marked as explored on the map Bug #4298: Problem with MessageBox and chargen menu interaction order Bug #4301: Optimizer breaks LOD nodes Bug #4308: PlaceAtMe doesn't inherit scale of calling object Bug #4309: Only harmful effects with resistance effect set are resistable Bug #4313: Non-humanoid creatures are capable of opening doors Bug #4314: Rainy weather slows down the game when changing from indoors/outdoors Bug #4319: Collisions for certain meshes are incorrectly ignored Bug #4320: Using mouse 1 to move forward causes selection dialogues to jump selections forward. Bug #4322: NPC disposition: negative faction reaction modifier doesn't take PC rank into account Bug #4328: Ownership by dead actors is not cleared from picked items Bug #4334: Torch and shield usage inconsistent with original game Bug #4336: Wizard: Incorrect Morrowind assets path autodetection Bug #4343: Error message for coc and starting cell shouldn't imply that it only works for interior cells Bug #4346: Count formatting does not work well with very high numbers Bug #4351: Using AddSoulgem fills all soul gems of the specified type Bug #4391: No visual indication is provided when an unavailable spell fails to be chosen via a quick key Bug #4392: Inventory filter breaks after loading a game Bug #4405: No default terrain in empty cells when distant terrain is enabled Bug #4410: [Mod] Arktwend: OpenMW does not use default marker definitions Bug #4412: openmw-iniimporter ignores data paths from config Bug #4413: Moving with 0 strength uses all of your fatigue Bug #4420: Camera flickering when I open up and close menus while sneaking Bug #4424: [macOS] Cursor is either empty or garbage when compiled against macOS 10.13 SDK Bug #4435: Item health is considered a signed integer Bug #4441: Adding items to currently disabled weapon-wielding creatures crashes the game Feature #1786: Round up encumbrance value in the encumbrance bar Feature #2694: Editor: rename "model" column to make its purpose clear Feature #3870: Editor: Terrain Texture Brush Button Feature #3872: Editor: Edit functions in terrain texture editing mode Feature #4054: Launcher: Create menu for settings.cfg options Feature #4064: Option for fast travel services to charge for the first companion Feature #4142: Implement fWereWolfHealth GMST Feature #4174: Multiple quicksaves Feature #4407: Support NiLookAtController Feature #4423: Rebalance soul gem values Task #4015: Use AppVeyor build artifact features to make continuous builds available Editor: New (and more complete) icon set 0.43.0 ------ Bug #815: Different settings cause inconsistent underwater visibility Bug #1452: autosave is not executed when waiting Bug #1555: Closing containers with spacebar doesn't work after touching an item Bug #1692: Can't close container when item is "held" Bug #2405: Maximum distance for guards attacking hostile creatures is incorrect Bug #2445: Spellcasting can be interrupted Bug #2489: Keeping map open not persisted between saves Bug #2594: 1st person view uses wrong body texture with Better bodies Bug #2628: enablestatreviewmenu command doen't read race, class and sign values from current game Bug #2639: Attacking flag isn't reset upon reloading Bug #2698: Snow and rain VFX move with the player Bug #2704: Some creature swim animations not being used Bug #2789: Potential risk of misunderstanding using the colored "owned" crosshair feature Bug #3045: Settings containing '#' cannot be loaded Bug #3097: Drop() doesn't work when an item is held (with the mouse) Bug #3110: GetDetected doesn't work without a reference Bug #3126: Framerate nosedives when adjusting dialogue window size Bug #3243: Ampersand in configuration files isn't escaped automatically Bug #3365: Wrong water reflection along banks Bug #3441: Golden saint always dispelling soul trap / spell priority issue Bug #3528: Disposing of corpses breaks quests Bug #3531: No FPS limit when playing bink videos even though "framerate limit" is set in settings.cfg Bug #3647: Multi-effect spells play audio louder than in Vanilla Bug #3656: NPCs forget where their place in the world is Bug #3665: Music transitions are too abrupt Bug #3679: Spell cast effect should disappear after using rest command Bug #3684: Merchants do not restock empty soul gems if they acquire filled ones. Bug #3694: Wrong magicka bonus applied on character creation Bug #3706: Guards don't try to arrest the player if attacked Bug #3709: Editor: Camera is not positioned correctly on mode switches related to orbital mode Bug #3720: Death counter not cleaned of non-existing IDs when loading a game Bug #3744: "Greater/lesser or equal" operators are not parsed when their signs are swapped Bug #3749: Yagrum Bagarn moves to different position on encountering Bug #3766: DisableLevitation does not remove visuals of preexisting effect Bug #3787: Script commands in result box for voiced dialogue are ignored Bug #3793: OpenMW tries to animate animated references even when they are disabled Bug #3794: Default sound buffer size is too small for mods Bug #3796: Mod 'Undress for me' doesn't work: NPCs re-equip everything Bug #3798: tgm command behaviour differs from vanilla Bug #3804: [Mod] Animated Morrowind: some animations do not loop correctly Bug #3805: Slight enchant miscalculation Bug #3826: Rendering problems with an image in a letter Bug #3833: [Mod] Windows Glow: windows textures are much darker than in original game Bug #3835: Bodyparts with multiple NiTriShapes are not handled correctly Bug #3839: InventoryStore::purgeEffect() removes only first effect with argument ID Bug #3843: Wrong jumping fatigue loss calculations Bug #3850: Boethiah's voice is distorted underwater Bug #3851: NPCs and player say things while underwater Bug #3864: Crash when exiting to Khartag point from Ilunibi Bug #3878: Swapping soul gems while enchanting allows constant effect enchantments using any soul gem Bug #3879: Dialogue option: Go to jail, persists beyond quickload Bug #3891: Journal displays empty entries Bug #3892: Empty space before dialogue entry display Bug #3898: (mod) PositionCell in dialogue results closes dialogue window Bug #3906: "Could not find Data Files location" dialog can appear multiple times Bug #3908: [Wizard] User gets stuck if they cancel out of installing from a CD Bug #3909: Morrowind Content Language dropdown is the only element on the right half of the Settings window Bug #3910: Launcher window can be resized so that it cuts off the scroll Bug #3915: NC text key on nifs doesn't work Bug #3919: Closing inventory while cursor hovers over spell (or other magic menu item) produces left click sound Bug #3922: Combat AI should avoid enemy hits when casts Self-ranged spells Bug #3934: [macOS] Copy/Paste from system clipboard uses Control key instead of Command key Bug #3935: Incorrect attack strength for AI actors Bug #3937: Combat AI: enchanted weapons have too high rating Bug #3942: UI sounds are distorted underwater Bug #3943: CPU/GPU usage should stop when the game is minimised Bug #3944: Attempting to sell stolen items back to their owner does not remove them from your inventory Bug #3955: Player's avatar rendering issues Bug #3956: EditEffectDialog: Cancel button does not update a Range button and an Area slider properly Bug #3957: Weird bodypart rendering if a node has reserved name Bug #3960: Clothes with high cost (> 32768) are not handled properly Bug #3963: When on edge of being burdened the condition doesn't lower as you run. Bug #3971: Editor: Incorrect colour field in cell table Bug #3974: Journal page turning doesn't produce sounds Bug #3978: Instant opening and closing happens when using a Controller with Menus/Containers Bug #3981: Lagging when spells are cast, especially noticeable on new landmasses such as Tamriel Rebuilt Bug #3982: Down sounds instead of Up ones are played when trading Bug #3987: NPCs attack after some taunting with no "Goodbye" Bug #3991: Journal can still be opened at main menu Bug #3995: Dispel cancels every temporary magic effect Bug #4002: Build broken on OpenBSD with clang Bug #4003: Reduce Render Area of Inventory Doll to Fit Within Border Bug #4004: Manis Virmaulese attacks without saying anything Bug #4010: AiWander: "return to the spawn position" feature does not work properly Bug #4016: Closing menus with spacebar will still send certain assigned actions through afterwards Bug #4017: GetPCRunning and GetPCSneaking should check that the PC is actually moving Bug #4024: Poor music track distribution Bug #4025: Custom spell with copy-pasted name always sorts to top of spell list Bug #4027: Editor: OpenMW-CS misreports its own name as "OpenCS", under Mac OS Bug #4033: Archers don't attack if the arrows have run out and there is no other weapon Bug #4037: Editor: New greetings do not work in-game. Bug #4049: Reloading a saved game while falling prevents damage Bug #4056: Draw animation should not be played when player equips a new weapon Bug #4074: Editor: Merging of LAND/LTEX records Bug #4076: Disposition bar is not updated when "goodbye" selected in dialogue Bug #4079: Alchemy skill increases do not take effect until next batch Bug #4093: GetResistFire, getResistFrost and getResistShock doesn't work as in vanilla Bug #4094: Level-up messages for levels past 20 are hardcoded not to be used Bug #4095: Error in framelistener when take all items from a dead corpse Bug #4096: Messagebox with the "%0.f" format should use 0 digit precision Bug #4104: Cycling through weapons does not skip broken ones Bug #4105: birthsign generation menu does not show full details Bug #4107: Editor: Left pane in Preferences window is too narrow Bug #4112: Inventory sort order is inconsistent Bug #4113: 'Resolution not supported in fullscreen' message is inconvenient Bug #4131: Pickpocketing behaviour is different from vanilla Bug #4155: NPCs don't equip a second ring in some cases Bug #4156: Snow doesn't create water ripples Bug #4165: NPCs autoequip new clothing with the same price Feature #452: Rain-induced water ripples Feature #824: Fading for doors and teleport commands Feature #933: Editor: LTEX record table Feature #936: Editor: LAND record table Feature #1374: AI: Resurface to breathe Feature #2320: ess-Importer: convert projectiles Feature #2509: Editor: highlighting occurrences of a word in a script Feature #2748: Editor: Should use one resource manager per document Feature #2834: Have openMW's UI remember what menu items were 'pinned' across boots. Feature #2923: Option to show the damage of the arrows through tooltip. Feature #3099: Disabling inventory while dragging an item forces you to drop it Feature #3274: Editor: Script Editor - Shortcuts and context menu options for commenting code out and uncommenting code respectively Feature #3275: Editor: User Settings- Add an option to reset settings to their default status (per category / all) Feature #3400: Add keyboard shortcuts for menus Feature #3492: Show success rate while enchanting Feature #3530: Editor: Reload data files Feature #3682: Editor: Default key binding reset Feature #3921: Combat AI: aggro priorities Feature #3941: Allow starting at an unnamed exterior cell with --start Feature #3952: Add Visual Studio 2017 support Feature #3953: Combat AI: use "WhenUsed" enchantments Feature #4082: Leave the stack of ingredients or potions grabbed after using an ingredient/potion Task #2258: Windows installer: launch OpenMW tickbox Task #4152: The Windows CI script is moving files around that CMake should be dealing with 0.42.0 ------ Bug #1956: Duplicate objects after loading the game, when a mod was edited Bug #2100: Falling leaves in Vurt's Leafy West Gash II not rendered correctly Bug #2116: Cant fit through some doorways pressed against staircases Bug #2289: Some modal dialogs are not centered on the screen when the window resizes Bug #2409: Softlock when pressing weapon/magic switch keys during chargen, afterwards switches weapons even though a text field is selected Bug #2483: Previous/Next Weapon hotkeys triggered while typing the name of game save Bug #2629: centeroncell, coc causes death / fall damage time to time when teleporting from high Bug #2645: Cycling weapons is possible while console/pause menu is open Bug #2678: Combat with water creatures do not end upon exiting water Bug #2759: Light Problems in Therana's Chamber in Tel Branora Bug #2771: unhandled sdl event of type 0x302 Bug #2777: (constant/on cast) disintegrate armor/weapon on self is seemingly not working Bug #2838: Editor: '.' in a record name should be allowed Bug #2909: NPCs appear floating when standing on a slope Bug #3093: Controller movement cannot be used while mouse is moving Bug #3134: Crash possible when using console with open container Bug #3254: AI enemies hit between them. Bug #3344: Editor: Verification results sorting by Type is not alphabetical. Bug #3345: Editor: Cloned and added pathgrids are lost after reopen of saved omwgame file Bug #3355: [MGSO] Physics maxing out in south cornerclub Balmora Bug #3484: Editor: camera position is not set when changing cell via drag&drop Bug #3508: Slowfall kills Jump momentum Bug #3580: Crash: Error ElementBufferObject::remove BufferData<0> out of range Bug #3581: NPCs wander too much Bug #3601: Menu Titles not centered vertically Bug #3607: [Mac OS] Beginning of NPC speech cut off (same issue as closed bug #3453) Bug #3613: Can not map "next weapon" or "next spell" to controller Bug #3617: Enchanted arrows don't explode when hitting the ground Bug #3645: Unable to use steps in Vivec, Palace of Vivec Bug #3650: Tamriel Rebuilt 16.09.1 – Hist Cuirass GND nif is rendered inside a Pink Box Bug #3652: Item icon shadows get stuck in the alchemy GUI Bug #3653: Incorrect swish sounds Bug #3666: NPC collision should not be disabled until death animation has finished Bug #3669: Editor: Text field was missing from book object editing dialogue Bug #3670: Unhandled SDL event of type 0x304 Bug #3671: Incorrect local variable value after picking up bittercup Bug #3686: Travelling followers doesn't increase travel fee Bug #3689: Problematic greetings from Antares Big Mod that override the appropriate ones. Bug #3690: Certain summoned creatures do not engage in combat with underwater creatures Bug #3691: Enemies do not initiate combat with player followers on sight Bug #3695: [Regression] Dispel does not always dispel spell effects in 0.41 Bug #3699: Crash on MWWorld::ProjectileManager::moveMagicBolts Bug #3700: Climbing on rocks and mountains Bug #3704: Creatures don't auto-equip their shields on creation Bug #3705: AI combat engagement logic differs from vanilla Bug #3707: Animation playing does some very odd things if pc comes in contact with the animated mesh Bug #3712: [Mod] Freeze upon entering Adanumuran with mod Adanumuran Reclaimed Bug #3713: [Regression] Cancelling dialogue or using travel with creatures throws a (possibly game-breaking) exception Bug #3719: Dropped identification papers can't be picked up again Bug #3722: Command spell doesn't bring enemies out of combat Bug #3727: Using "Activate" mid-script-execution invalidates interpreter context Bug #3746: Editor: Book records show attribute IDs instead of skill IDs for teached skills entry. Bug #3755: Followers stop following after loading from savegame Bug #3772: ModStat lowers attribute to 100 if it was greater Bug #3781: Guns in Clean Hunter Rifles mod use crossbow sounds Bug #3797: NPC and creature names don't show up in combat when RMB windows are displayed Bug #3800: Wrong tooltip maximum width Bug #3801: Drowning widget is bugged Bug #3802: BarterOffer shouldn't limit pcMercantile Bug #3813: Some fatal error Bug #3816: Expression parser thinks the -> token is unexpected when a given explicit refID clashes with a journal ID Bug #3822: Custom added creatures are not animated Feature #451: Water sounds Feature #2691: Light particles sometimes not shown in inventory character preview Feature #3523: Light source on magic projectiles Feature #3644: Nif NiSphericalCollider Unknown Record Type Feature #3675: ess-Importer: convert mark location Feature #3693: ess-Importer: convert last known exterior cell Feature #3748: Editor: Replace "Scroll" check box in Book records with "Book Type" combo box. Feature #3751: Editor: Replace "Xyz Blood" check boxes in NPC and Creature records with "Blood Type" combo box Feature #3752: Editor: Replace emitter check boxes in Light records with "Emitter Type" combo box Feature #3756: Editor: Replace "Female" check box in NPC records with "Gender" combo box Feature #3757: Editor: Replace "Female" check box in BodyPart records with "Gender" combo box Task #3092: const version of ContainerStoreIterator Task #3795: /deps folder not in .gitignore 0.41.0 ------ Bug #1138: Casting water walking doesn't move the player out of the water Bug #1931: Rocks from blocked passage in Bamz-Amschend, Radacs Forge can reset and cant be removed again. Bug #2048: Almvisi and Divine Intervention display wrong spell effect Bug #2054: Show effect-indicator for "instant effect" spells and potions Bug #2150: Clockwork City door animation problem Bug #2288: Playback of weapon idle animation not correct Bug #2410: Stat-review window doesn't display starting spells, powers, or abilities Bug #2493: Repairing occasionally very slow Bug #2716: [OSG] Water surface is too transparent from some angles Bug #2859: [MAC OS X] Cannot exit fullscreen once enabled Bug #3091: Editor: will not save addon if global variable value type is null Bug #3277: Editor: Non-functional nested tables in subviews need to be hidden instead of being disabled Bug #3348: Disabled map markers show on minimap Bug #3350: Extending selection to instances with same object results in duplicates. Bug #3353: [Mod] Romance version 3.7 script failed Bug #3376: [Mod] Vampire Embrace script fails to execute Bug #3385: Banners don't animate in stormy weather as they do in the original game Bug #3393: Akulakhan re-enabled after main quest Bug #3427: Editor: OpenMW-CS instances won´t get deleted Bug #3451: Feril Salmyn corpse isn't where it is supposed to be Bug #3497: Zero-weight armor is displayed as "heavy" in inventory tooltip Bug #3499: Idle animations don't always loop Bug #3500: Spark showers at Sotha Sil do not appear until you look at the ceiling Bug #3515: Editor: Moved objects in interior cells are teleported to exterior cells. Bug #3520: Editor: OpenMW-CS cannot find project file when launching the game Bug #3521: Armed NPCs don't use correct melee attacks Bug #3535: Changing cell immediately after dying causes character to freeze. Bug #3542: Unable to rest if unalerted slaughterfish are in the cell with you Bug #3549: Blood effects occur even when a hit is resisted Bug #3551: NPC Todwendy in german version can't interact Bug #3552: Opening the journal when fonts are missing results in a crash Bug #3555: SetInvisible command should not apply graphic effect Bug #3561: Editor: changes from omwaddon are not loaded in [New Addon] mode Bug #3562: Non-hostile NPCs can be disarmed by stealing their weapons via sneaking Bug #3564: Editor: openmw-cs verification results Bug #3568: Items that should be invisible are shown in the inventory Bug #3574: Alchemy: Alembics and retorts are used in reverse Bug #3575: Diaglog choices don't work in mw 0.40 Bug #3576: Minor differences in AI reaction to hostile spell effects Bug #3577: not local nolore dialog test Bug #3578: Animation Replacer hangs after one cicle/step Bug #3579: Bound Armor skillups and sounds Bug #3583: Targetted GetCurrentAiPackage returns 0 Bug #3584: Persuasion bug Bug #3590: Vendor, Ilen Faveran, auto equips items from stock Bug #3594: Weather doesn't seem to update correctly in Mournhold Bug #3598: Saving doesn't save status of objects Bug #3600: Screen goes black when trying to travel to Sadrith Mora Bug #3608: Water ripples aren't created when walking on water Bug #3626: Argonian NPCs swim like khajiits Bug #3627: Cannot delete "Blessed touch" spell from spellbook Bug #3634: An enchanted throwing weapon consumes charges from the stack in your inventory. (0.40.0) Bug #3635: Levelled items in merchants are "re-rolled" (not bug 2952, see inside) Feature #1118: AI combat: flee Feature #1596: Editor: Render water Feature #2042: Adding a non-portable Light to the inventory should cause the player to glow Feature #3166: Editor: Instance editing mode - rotate sub mode Feature #3167: Editor: Instance editing mode - scale sub mode Feature #3420: ess-Importer: player control flags Feature #3489: You shouldn't be be able to re-cast a bound equipment spell Feature #3496: Zero-weight boots should play light boot footsteps Feature #3516: Water Walking should give a "can't cast" message and fail when you are too deep Feature #3519: Play audio and visual effects for all effects in a spell Feature #3527: Double spell explosion scaling Feature #3534: Play particle textures for spell effects Feature #3539: Make NPCs use opponent's weapon range to decide whether to dodge Feature #3540: Allow dodging for creatures with "biped" flag Feature #3545: Drop shadow for items in menu Feature #3558: Implement same spell range for "on touch" spells as original engine Feature #3560: Allow using telekinesis with touch spells on objects Task #3585: Some objects added by Morrowind Rebirth do not display properly their texture 0.40.0 ------ Bug #1320: AiWander - Creatures in cells without pathgrids do not wander Bug #1873: Death events are triggered at the beginning of the death animation Bug #1996: Resting interrupts magic effects Bug #2399: Vampires can rest in broad daylight and survive the experience Bug #2604: Incorrect magicka recalculation Bug #2721: Telekinesis extends interaction range where it shouldn't Bug #2981: When waiting, NPCs can go where they wouldn't go normally. Bug #3045: Esp files containing the letter '#' in the file name cannot be loaded on startup Bug #3071: Slowfall does not stop momentum when jumping Bug #3085: Plugins can not replace parent cell references with a cell reference of different type Bug #3145: Bug with AI Cliff Racer. He will not attack you, unless you put in front of him. Bug #3149: Editor: Weather tables were missing from regions Bug #3201: Netch shoots over your head Bug #3269: If you deselect a mod and try to load a save made inside a cell added by it, you end bellow the terrain in the grid 0/0 Bug #3286: Editor: Script editor tab width Bug #3329: Teleportation spells cause crash to desktop after build update from 0.37 to 0.38.0 Bug #3331: Editor: Start Scripts table: Adding a script doesn't refresh the list of Start Scripts and allows to add a single script multiple times Bug #3332: Editor: Scene view: Tool tips only occur when holding the left mouse button Bug #3340: ESS-Importer does not separate item stacks Bug #3342: Editor: Creation of pathgrids did not check if the pathgrid already existed Bug #3346: "Talked to PC" is always 0 for "Hello" dialogue Bug #3349: AITravel doesn't repeat Bug #3370: NPCs wandering to invalid locations after training Bug #3378: "StopCombat" command does not function in vanilla quest Bug #3384: Battle at Nchurdamz - Larienna Macrina does not stop combat after killing Hrelvesuu Bug #3388: Monster Respawn tied to Quicksave Bug #3390: Strange visual effect in Dagoth Ur's chamber Bug #3391: Inappropriate Blight weather behavior at end of main quest Bug #3394: Replaced dialogue inherits some of its old data Bug #3397: Actors that start the game dead always have the same death pose Bug #3401: Sirollus Saccus sells not glass arrows Bug #3402: Editor: Weapon data not being properly set Bug #3405: Mulvisic Othril will not use her chitin throwing stars Bug #3407: Tanisie Verethi will immediately detect the player Bug #3408: Improper behavior of ashmire particles Bug #3412: Ai Wander start time resets when saving/loading the game Bug #3416: 1st person and 3rd person camera isn't converted from .ess correctly Bug #3421: Idling long enough while paralyzed sometimes causes character to get stuck Bug #3423: Sleep interruption inside dungeons too agressive Bug #3424: Pickpocketing sometimes won't work Bug #3432: AiFollow / AiEscort durations handled incorrectly Bug #3434: Dead NPC's and Creatures still contribute to sneak skill increases Bug #3437: Weather-conditioned dialogue should not play in interiors Bug #3439: Effects cast by summon stick around after their death Bug #3440: Parallax maps looks weird Bug #3443: Class graphic for custom class should be Acrobat Bug #3446: OpenMW segfaults when using Atrayonis's "Anthology Solstheim: Tomb of the Snow Prince" mod Bug #3448: After dispelled, invisibility icon is still displayed Bug #3453: First couple of seconds of NPC speech is muted Bug #3455: Portable house mods lock player and npc movement up exiting house. Bug #3456: Equipping an item will undo dispel of constant effect invisibility Bug #3458: Constant effect restore health doesn't work during Wait Bug #3466: It is possible to stack multiple scroll effects of the same type Bug #3471: When two mods delete the same references, many references are not disabled by the engine. Bug #3473: 3rd person camera can be glitched Feature #1424: NPC "Face" function Feature #2974: Editor: Multiple Deletion of Subrecords Feature #3044: Editor: Render path grid v2 Feature #3362: Editor: Configurable key bindings Feature #3375: Make sun / moon reflections weather dependent Feature #3386: Editor: Edit pathgrid 0.39.0 ------ Bug #1384: Dark Brotherhood Assassin (and other scripted NPCs?) spawns beneath/inside solid objects Bug #1544: "Drop" drops equipped item in a separate stack Bug #1587: Collision detection glitches Bug #1629: Container UI locks up in Vivec at Jeanne's Bug #1771: Dark Brotherhood Assassin oddity in Eight Plates Bug #1827: Unhandled NiTextureEffect in ex_dwrv_ruin30.nif Bug #2089: When saving while swimming in water in an interior cell, you will be spawned under water on loading Bug #2295: Internal texture not showing, nipixeldata Bug #2363: Corpses don't disappear Bug #2369: Respawns should be timed individually Bug #2393: Сharacter is stuck in the tree Bug #2444: [Mod] NPCs from Animated Morrowind appears not using proper animations Bug #2467: Creatures do not respawn Bug #2515: Ghosts in Ibar-Dad spawn stuck in walls Bug #2610: FixMe script still needs to be implemented Bug #2689: Riekling raider pig constantly screams while running Bug #2719: Vivec don't put their hands on the knees with this replacer (Psymoniser Vivec God Replacement NPC Edition v1.0 Bug #2737: Camera shaking when side stepping around object Bug #2760: AI Combat Priority Problem - Use of restoration spell instead of attacking Bug #2806: Stack overflow in LocalScripts::getNext Bug #2807: Collision detection allows player to become stuck inside objects Bug #2814: Stairs to Marandus have improper collision Bug #2925: Ranes Ienith will not appear, breaking the Morag Tong and Thieves Guid questlines Bug #3024: Editor: Creator bar in startscript subview does not accept script ID drops Bug #3046: Sleep creature: Velk is spawned half-underground in the Thirr River Valley Bug #3080: Calling aifollow without operant in local script every frame causes mechanics to overheat + log Bug #3101: Regression: White guar does not move Bug #3108: Game Freeze after Killing Diseased Rat in Foreign Quarter Tomb Bug #3124: Bloodmoon Quest - Rite of the Wolf Giver (BM_WolfGiver) – Innocent victim won't turn werewolf Bug #3125: Improper dialogue window behavior when talking to creatures Bug #3130: Some wandering NPCs disappearing, cannot finish quests Bug #3132: Editor: GMST ID named sMake Enchantment is instead named sMake when making new game from scratch Bug #3133: OpenMW and the OpenCS are writting warnings about scripts that use the function GetDisabled. Bug #3135: Journal entry for The Pigrim's Path missing name Bug #3136: Dropped bow is displaced Bug #3140: Editor: OpenMW-CS fails to open newly converted and saved omwaddon file. Bug #3142: Duplicate Resist Magic message Bug #3143: Azura missing her head Bug #3146: Potion effect showing when ingredient effects are not known Bug #3155: When executing chop attack with a spear, hands turn partly invisible Bug #3161: Fast travel from Silt Strider or Boat Ride will break save files made afterwards Bug #3163: Editor: Objects dropped to scene do not always save Bug #3173: Game Crashes After Casting Recall Spell Bug #3174: Constant effect enchantments play spell animation on dead bodies Bug #3175: Spell effects do not wear down when caster dies Bug #3176: NPCs appearing randomly far away from towns Bug #3177: Submerged corpse floats ontop of water when it shouldn't (Widow Vabdas' Deed quest) Bug #3184: Bacola Closcius in Balmora, South Wall Cornerclub spams magic effects if attacked Bug #3207: Editor: New objects do not render Bug #3212: Arrow of Ranged Silence Bug #3213: Looking at Floor After Magical Transport Bug #3220: The number of remaining ingredients in the alchemy window doesn't go down when failing to brew a potion Bug #3222: Falling through the water in Vivec Bug #3223: Crash at the beginning with MOD (The Symphony) Bug #3228: Purple screen when leveling up. Bug #3233: Infinite disposition via MWDialogue::Filter::testDisposition() glitch Bug #3234: Armor mesh stuck on body in inventory menu Bug #3235: Unlike vanilla, OpenMW don't allow statics and activators cast effects on the player. Bug #3238: Not loading cells when using Poorly Placed Object Fix.esm Bug #3248: Editor: Using the "Next Script" and "Previous Script" buttons changes the record status to "Modified" Bug #3258: Woman biped skeleton Bug #3259: No alternating punches Bug #3262: Crash in class selection menu Bug #3279: Load menu: Deleting a savegame makes scroll bar jump to the top Bug #3326: Starting a new game, getting to class selection, then starting another new game temporarily assigns Acrobat class Bug #3327: Stuck in table after loading when character was sneaking when quicksave Feature #652: Editor: GMST verifier Feature #929: Editor: Info record verifier Feature #1279: Editor: Render cell border markers Feature #2482: Background cell loading and caching of loaded cells Feature #2484: Editor: point lighting Feature #2801: Support NIF bump map textures in osg Feature #2926: Editor: Optional line wrap in script editor wrap lines Feature #3000: Editor: Reimplement 3D scene camera system Feature #3035: Editor: Make scenes a drop target for referenceables Feature #3043: Editor: Render cell markers v2 Feature #3164: Editor: Instance Selection Menu Feature #3165: Editor: Instance editing mode - move sub mode Feature #3244: Allow changing water Level of Interiors behaving like exteriors Feature #3250: Editor: Use "Enter" key instead of clicking "Create" button to confirm ID input in Creator Bar Support #3179: Fatal error on startup 0.38.0 ------ Bug #1699: Guard will continuously run into mudcrab Bug #1934: Saw in Dome of Kasia doesnt harm the player Bug #1962: Rat floats when killed near the door Bug #1963: Kwama eggsacks pulse too fast Bug #2198: NPC voice sound source should be placed at their head Bug #2210: OpenMW installation wizard crashes... Bug #2211: Editor: handle DELE subrecord at the end of a record Bug #2413: ESM error Unknown subrecord in Grandmaster of Hlaalu Bug #2537: Bloodmoon quest Ristaag: Sattir not consistently dying, plot fails to advance; same with Grerid Bug #2697: "The Swimmer" moves away after leading you to underwater cave Bug #2724: Loading previous save duplicates containers and harvestables Bug #2769: Inventory doll - Cursor not respecting order of clothes Bug #2865: Scripts silently fail when moving NPCs between cells. Bug #2873: Starting a new game leads to CTD / Fatal Error Bug #2918: Editor: it's not possible to create an omwaddon containing a dot in the file name Bug #2933: Dialog box can't disable a npc if it is in another cell. (Rescue Madura Seran). Bug #2942: atronach sign behavior (spell absorption) changes when trying to receive a blessing at "shrine of tribunal" Bug #2952: Enchantment Merchant Items reshuffled EVERY time 'barter' is clicked Bug #2961: ESM Error: Unknown subrecord if Deus Ex Machina mod is loaded Bug #2972: Resurrecting the player via console does not work when health was 0 Bug #2986: Projectile weapons work underwater Bug #2988: "Expected subrecord" bugs showing up. Bug #2991: Can't use keywords in strings for MessageBox Bug #2993: Tribunal:The Shrine of the Dead – Urvel Dulni can't stop to follow the player. Bug #3008: NIFFile Error while loading meshes with a NiLODNode Bug #3010: Engine: items should sink to the ground when dropped under water Bug #3011: NIFFile Error while loading meshes with a NiPointLight Bug #3016: Engine: something wrong with scripting - crash / fatal error Bug #3020: Editor: verify does not check if given "item ID" (as content) for a "container" exists Bug #3026: [MOD: Julan Ashlander Companion] Dialogue not triggering correctly Bug #3028: Tooltips for Health, Magicka and Fatigue show in Options menu even when bars aren't visible Bug #3034: Item count check dialogue option doesn't work (Guards accept gold even if you don't have enough) Bug #3036: Owned tooltip color affects spell tooltips incorrrectly Bug #3037: Fatal error loading old ES_Landscape.esp in Store::search Bug #3038: Player sounds come from underneath Bug #3040: Execution of script failed: There is a message box already Bug #3047: [MOD: Julan Ashlander Companion] Scripts KS_Bedscript or KS_JulanNight not working as intended Bug #3048: Fatal Error Bug #3051: High field of view results in first person rendering glitches Bug #3053: Crash on new game at character class selection Bug #3058: Physiched sleeves aren't rendered correctly. Bug #3060: NPCs use wrong landing sound Bug #3062: Mod support regression: Andromeda's fast travel. Bug #3063: Missing Journal Textures without Tribunal and Bloodmoon installed Bug #3077: repeated aifollow causes the distance to stack Bug #3078: Creature Dialogues not showing when certain Function/Conditions are required. Bug #3082: Crash when entering Holamayan Monastery with mesh replacer installed Bug #3086: Party at Boro's House – Creature with Class don't talk under OpenMW Bug #3089: Dreamers spawn too soon Bug #3100: Certain controls erroneously work as a werewolf Bug #3102: Multiple unique soultrap spell sources clone souls. Bug #3105: Summoned creatures and objects disappear at midnight Bug #3112: gamecontrollerdb file creation with wrong extension Bug #3116: Dialogue Function "Same Race" is avoided Bug #3117: Dialogue Bug: Choice conditions are tested when not in a choice Bug #3118: Body Parts are not rendered when used in a pose. Bug #3122: NPC direction is reversed during sneak awareness check Feature #776: Sound effects from one direction don't necessarily affect both speakers in stereo Feature #858: Different fov settings for hands and the game world Feature #1176: Handle movement of objects between cells Feature #2507: Editor: choosing colors for syntax highlighting Feature #2867: Editor: hide script error list when there are no errors Feature #2885: Accept a file format other than nif Feature #2982: player->SetDelete 1 results in: PC can't move, menu can be opened Feature #2996: Editor: make it possible to preset the height of the script check area in a script view Feature #3014: Editor: Tooltips in 3D scene Feature #3064: Werewolf field of view Feature #3074: Quicksave indicator Task #287: const version of Ptr Task #2542: Editor: redo user settings system 0.37.0 ------ Bug #385: Light emitting objects have a too short distance of activation Bug #455: Animation doesn't resize creature's bounding box Bug #602: Only collision model is updated when modifying objects trough console Bug #639: Sky horizon at nighttime Bug #672: incorrect trajectory of the moons Bug #814: incorrect NPC width Bug #827: Inaccurate raycasting for dead actors Bug #996: Can see underwater clearly when at right height/angle Bug #1317: Erene Llenim in Seyda Neen does not walk around Bug #1330: Cliff racers fail to hit the player Bug #1366: Combat AI can't aim down (in order to hit small creatures) Bug #1511: View distance while under water is much too short Bug #1563: Terrain positioned incorrectly and appears to vibrate in far-out cells Bug #1612: First person models clip through walls Bug #1647: Crash switching from full screen to windows mode - D3D9 Bug #1650: No textures with directx on windows Bug #1730: Scripts names starting with digit(s) fail to compile Bug #1738: Socucius Ergalla's greetings are doubled during the tutorial Bug #1784: First person weapons always in the same position Bug #1813: Underwater flora lighting up entire area. Bug #1871: Handle controller extrapolation flags Bug #1921: Footstep frequency and velocity do not immediately update when speed attribute changes Bug #2001: OpenMW crashes on start with OpenGL 1.4 drivers Bug #2014: Antialiasing setting does nothing on Linux Bug #2037: Some enemies attack the air when spotting the player Bug #2052: NIF rotation matrices including scales are not supported Bug #2062: Crank in Old Mournhold: Forgotten Sewer turns about the wrong axis Bug #2111: Raindrops in front of fire look wrong Bug #2140: [OpenGL] Water effects, flames and parts of creatures solid black when observed through brazier flame Bug #2147: Trueflame and Hopesfire flame effects not properly aligned with blade Bug #2148: Verminous fabricants have little coloured box beneath their feet Bug #2149: Sparks in Clockwork City should bounce off the floor Bug #2151: Clockwork City dicer trap doesn't activate when you're too close Bug #2186: Mini map contains scrambled pixels that cause the mini map to flicker Bug #2187: NIF file with more than 255 NiBillboardNodes does not load Bug #2191: Editor: Crash when trying to view cell in render view in OpenCS Bug #2270: Objects flicker transparently Bug #2280: Latest 32bit windows build of openmw runns out of vram Bug #2281: NPCs don't scream when they die Bug #2286: Jumping animation restarts when equipping mid-air Bug #2287: Weapon idle animation stops when turning Bug #2355: Light spell doesn't work in 1st person view Bug #2362: Lantern glas opaque to flame effect from certain viewing angles Bug #2364: Light spells are not as bright as in Morrowind Bug #2383: Remove the alpha testing override list Bug #2436: Crash on entering cell "Tower of Tel Fyr, Hall of Fyr" Bug #2457: Player followers should not report crimes Bug #2458: crash in some fighting situations Bug #2464: Hiding an emitter node should make that emitter stop firing particles Bug #2466: Can't load a save created with OpenMW-0.35.0-win64 Bug #2468: music from title screen continues after loading savegame Bug #2494: Map not consistent between saves Bug #2504: Dialog scroll should always start at the top Bug #2506: Editor: Undo/Redo shortcuts do not work in script editor Bug #2513: Mannequins in mods appear as dead bodies Bug #2524: Editor: TopicInfo "custom" condition section is missing Bug #2540: Editor: search and verification result table can not be sorted by clicking on the column names Bug #2543: Editor: there is a problem with spell effects Bug #2544: Editor fails to save NPC information correctly. Bug #2545: Editor: delete record in Objects (referenceables) table messes up data Bug #2546: Editor: race base attributes and skill boni are not displayed, thus not editable Bug #2547: Editor: some NPC data is not displayed, thus not editable Bug #2551: Editor: missing data in cell definition Bug #2553: Editor: value filter does not work for float values Bug #2555: Editor: undo leaves the record status as Modified Bug #2559: Make Detect Enchantment marks appear on top of the player arrow Bug #2563: position consoling npc doesn't work without cell reload Bug #2564: Editor: Closing a subview from code does not clean up properly and will lead to crash on opening the next subview Bug #2568: Editor: Setting default window size is ignored Bug #2569: Editor: saving from an esp to omwaddon file results in data loss for TopicInfo Bug #2575: Editor: Deleted record (with Added (ModifiedOnly) status) remains in the Dialog SubView Bug #2576: Editor: Editor doesn't scroll to a newly opened subview, when ScrollBar Only mode is active Bug #2578: Editor: changing Level or Reputation of an NPC crashes the editor Bug #2579: Editor: filters not updated when adding or cloning records Bug #2580: Editor: omwaddon makes OpenMW crash Bug #2581: Editor: focus problems in edit subviews single- and multiline input fields Bug #2582: Editor: object verifier should check for non-existing scripts being referenced Bug #2583: Editor: applying filter to TopicInfo on mods that have added dialouge makes the Editor crash Bug #2586: Editor: some dialogue only editable items do not refresh after undo Bug #2588: Editor: Cancel button exits program Bug #2589: Editor: Regions table - mapcolor does not change correctly Bug #2591: Placeatme - spurious 5th parameter raises error Bug #2593: COC command prints multiple times when GUI is hidden Bug #2598: Editor: scene view of instances has to be zoomed out to displaying something - center camera instance please Bug #2607: water behind an invisible NPC becomes invisible as well Bug #2611: Editor: Sort problem in Objects table when few nested rows are added Bug #2621: crash when a creature has no model Bug #2624: Editor: missing columns in tables Bug #2627: Character sheet doesn't properly update when backing out of CharGen Bug #2642: Editor: endif without if - is not reported as error when "verify" was executed Bug #2644: Editor: rebuild the list of available content files when opening the open/new dialogues Bug #2656: OpenMW & OpenMW-CS: setting "Flies" flag for ghosts has no effect Bug #2659: OpenMW & OpenMW-CS: savegame load fail due to script attached to NPCs Bug #2668: Editor: reputation value in the input field is not stored Bug #2696: Horkers use land idle animations under water Bug #2705: Editor: Sort by Record Type (Objects table) is incorrect Bug #2711: Map notes on an exterior cell that shows up with a map marker on the world map do not show up in the tooltip for that cell's marker on the world map Bug #2714: Editor: Can't reorder rows with the same topic in different letter case Bug #2720: Head tracking for creatures not implemented Bug #2722: Alchemy should only include effects shared by at least 2 ingredients Bug #2723: "ori" console command is not working Bug #2726: Ashlanders in front of Ghostgate start wandering around Bug #2727: ESM writer does not handle encoding when saving the TES3 header Bug #2728: Editor: Incorrect position of an added row in Info tables Bug #2731: Editor: Deleting a record triggers a Qt warning Bug #2733: Editor: Undo doesn't restore the Modified status of a record when a nested data is changed Bug #2734: Editor: The Search doesn't work Bug #2738: Additive moon blending Bug #2746: NIF node names should be case insensitive Bug #2752: Fog depth/density not handled correctly Bug #2753: Editor: line edit in dialogue subview tables shows after a single click Bug #2755: Combat AI changes target too frequently Bug #2761: Can't attack during block animations Bug #2764: Player doesn't raise arm in 3rd person for weathertype 9 Bug #2768: Current screen resolution not selected in options when starting OpenMW Bug #2773: Editor: Deleted scripts are editable Bug #2776: ordinators still think I'm wearing their helm even though Khajiit and argonians can't Bug #2779: Slider bars continue to move if you don't release mouse button Bug #2781: sleep interruption is a little off (is this an added feature?) Bug #2782: erroneously able to ready weapon/magic (+sheathe weapon/magic) while paralyzed Bug #2785: Editor: Incorrect GMSTs for newly created omwgame files Bug #2786: Kwama Queen head is inverted under OpenMW Bug #2788: additem and removeitem incorrect gold behavior Bug #2790: --start doesn't trace down Bug #2791: Editor: Listed attributes and skill should not be based on number of NPC objects. Bug #2792: glitched merchantile/infinite free items Bug #2794: Need to ignore quotes in names of script function Bug #2797: Editor: Crash when removing the first row in a nested table Bug #2800: Show an error message when S3TC support is missing Bug #2811: Targetted Open spell effect persists. Bug #2819: Editor: bodypart's race filter not displayed correctly Bug #2820: Editor: table sorting is inverted Bug #2821: Editor: undo/redo command labels are incorrect Bug #2826: locking beds that have been locked via magic psuedo-freezes the game Bug #2830: Script compiler does not accept IDs as instruction/functions arguments if the ID is also a keyword Bug #2832: Cell names are not localized on the world map Bug #2833: [cosmetic] Players swimming at water's surface are slightly too low. Bug #2840: Save/load menu is not entirely localized Bug #2853: [exploit/bug] disintegrate weapon incorrectly applying to lockpicks, probes. creates unbreakable lockpicks Bug #2855: Mouse wheel in journal is not disabled by "Options" panel. Bug #2856: Heart of Lorkhan doesn't visually respond to attacks Bug #2863: Inventory highlights wrong category after load Bug #2864: Illuminated Order 1.0c Bug – The teleport amulet is not placed in the PC inventory. Bug #2866: Editor: use checkbox instead of combobox for boolean values Bug #2875: special cases of fSleepRandMod not behaving properly. Bug #2878: Editor: Verify reports "creature has non-positive level" but there is no level setting Bug #2879: Editor: entered value of field "Buys *" is not saved for a creature Bug #2880: OpenMW & OpenMW-CS: having a scale value of 0.000 makes the game laggy Bug #2882: Freeze when entering cell "Guild of Fighters (Ald'ruhn)" after dropping some items inside Bug #2883: game not playable if mod providing a spell is removed but the list of known spells still contains it Bug #2884: NPC chats about wrong player race Bug #2886: Adding custom races breaks existing numbering of PcRace Bug #2888: Editor: value entered in "AI Wander Idle" is not kept Bug #2889: Editor: creatures made with the CS (not cloned) are always dead Bug #2890: Editor: can't make NPC say a specific "Hello" voice-dialouge Bug #2893: Editor: making a creature use textual dialogue doesn't work. Bug #2901: Editor: gold for trading can not be set for creatures Bug #2907: looking from uderwater part of the PC that is below the surface looks like it would be above the water Bug #2914: Magicka not recalculated on character generation Bug #2915: When paralyzed, you can still enter and exit sneak Bug #2917: chameleon does not work for creatures Bug #2927: Editor: in the automatic script checker local variable caches are not invalidated/updated on modifications of other scripts Bug #2930: Editor: AIWander Idle can not be set for a creature Bug #2932: Editor: you can add rows to "Creature Attack" but you can not enter values Bug #2938: Editor: Can't add a start script. Bug #2944: Spell chance for power to show as 0 on hud when used Bug #2953: Editor: rightclick in an empty place in the menu bar shows an unnamed checkbox Bug #2956: Editor: freezes while editing Filter Bug #2959: space character in field enchantment (of an amulet) prevents rendering of surroundings Bug #2962: OpenMW: Assertion `it != invStore.end()' failed Bug #2964: Recursive script execution can corrupt script runtime data Bug #2973: Editor: placing a chest in the game world and activating it heavily blurrs the character portrait Bug #2978: Editor: Cannot edit alchemy ingredient properties Bug #2980: Editor: Attribute and Skill can be selected for spells that do not require these parameters, leading to non-functional spells Bug #2990: Compiling a script with warning mode 2 and enabled error downgrading leads to infinite recursion Bug #2992: [Mod: Great House Dagoth] Killing Dagoth Gares freezes the game Bug #3007: PlaceItem takes radians instead of degrees + angle reliability Feature #706: Editor: Script Editor enhancements Feature #872: Editor: Colour values in tables Feature #880: Editor: ID auto-complete Feature #928: Editor: Partial sorting in info tables Feature #942: Editor: Dialogue for editing/viewing content file meta information Feature #1057: NiStencilProperty Feature #1278: Editor: Mouse picking in worldspace widget Feature #1280: Editor: Cell border arrows Feature #1401: Editor: Cloning enhancements Feature #1463: Editor: Fine grained configuration of extended revert/delete commands Feature #1591: Editor: Make fields in creation bar drop targets where applicable Feature #1998: Editor: Magic effect record verifier Feature #1999: Editor Sound Gen record verifier Feature #2000: Editor: Pathgrid record verifier Feature #2528: Game Time Tracker Feature #2534: Editor: global search does not auomatically focus the search input field Feature #2535: OpenMW: allow comments in openmw.cfg Feature #2541: Editor: provide a go to the very bottom button for TopicInfo and JournalInfo Feature #2549: Editor: add a horizontal slider to scroll between opened tables Feature #2558: Editor: provide a shortcut for closing the subview that has the focus Feature #2565: Editor: add context menu for dialogue sub view fields with an item matching "Edit 'x'" from the table subview context menu Feature #2585: Editor: Ignore mouse wheel input for numeric values unless the respective widget has the focus Feature #2620: Editor: make the verify-view refreshable Feature #2622: Editor: Make double click behaviour in result tables configurable (see ID tables) Feature #2717: Editor: Add severity column to report tables Feature #2729: Editor: Various dialogue button bar improvements Feature #2739: Profiling overlay Feature #2740: Resource manager optimizations Feature #2741: Make NIF files into proper resources Feature #2742: Use the skinning data in NIF files as-is Feature #2743: Small feature culling Feature #2744: Configurable near clip distance Feature #2745: GUI scaling option Feature #2747: Support anonymous textures Feature #2749: Loading screen optimizations Feature #2751: Character preview optimization Feature #2804: Editor: Merge Tool Feature #2818: Editor: allow copying a record ID to the clipboard Feature #2946: Editor: add script line number in results of search Feature #2963: Editor: Mouse button bindings in 3D scene Feature #2983: Sun Glare fader Feature #2999: Scaling of journal and books Task #2665: Support building with Qt5 Task #2725: Editor: Remove Display_YesNo Task #2730: Replace hardcoded column numbers in SimpleDialogueSubView/DialogueSubView Task #2750: Bullet shape instancing optimization Task #2793: Replace grid size setting with half grid size setting Task #3003: Support FFMPEG 2.9 (Debian request) 0.36.1 ------ Bug #2590: Start scripts not added correctly 0.36.0 ------ Bug #923: Editor: Operations-Multithreading is broken Bug #1317: Erene Llenim in Seyda Neen does not walk around Bug #1405: Water rendering glitch near Seyda Neen lighthouse Bug #1621: "Error Detecting Morrowind Installation" in the default directory Bug #2216: Creating a clone of the player stops you moving. Bug #2387: Casting bound weapon spell doesn't switch to "ready weapon" mode Bug #2407: Default to (0, 0) when "unknown cell" is encountered. Bug #2411: enchanted item charges don't update/refresh if spell list window is pinned open Bug #2428: Editor: cloning / creating new container class results in invalid omwaddon file - openmw-0.35 Bug #2429: Editor - cloning omits some values or sets different values than the original has Bug #2430: NPC with negative fatigue don't fall (LGNPC Vivec, Foreign Quarter v2.21) Bug #2432: Error on startup with Uvirith's Legacy enabled Bug #2435: Editor: changed entries in the objects window are not shown as such Bug #2437: Editor: changing an entry of a container/NPC/clothing/ingredient/globals will not be saved in the omwaddon file Bug #2447: Editor doesn't save terrain information Bug #2451: Editor not listing files with accented characters Bug #2453: Chargen: sex, race and hair sliders not initialized properly Bug #2459: Minor terrain clipping through statics due to difference in triangle alignment Bug #2461: Invisible sound mark has collision in Sandus Ancestral Tomb Bug #2465: tainted gold stack Bug #2475: cumulative stacks of 100 point fortify skill speechcraft boosts do not apply correctly Bug #2498: Editor: crash when issuing undo command after the table subview is closed Bug #2500: Editor: object table - can't undo delete record Bug #2518: OpenMW detect spell returns false positives Bug #2521: NPCs don't react to stealing when inventory menu is open. Bug #2525: Can't click on red dialogue choice [rise of house telvanni][60fffec] Bug #2530: GetSpellEffects not working as in vanilla Bug #2557: Crash on first launch after choosing "Run installation wizard" Feature #139: Editor: Global Search & Replace Feature #1219: Editor: Add dialogue mode only columns Feature #2024: Hotkey for hand to hand (i.e. unequip any weapon) Feature #2119: "Always Sneak" key bind Feature #2262: Editor: Handle moved instances Feature #2425: Editor: Add start script table Feature #2426: Editor: start script record verifier Feature #2480: Launcher: Multiselect entries in the Data Files list Feature #2505: Editor: optionally show a line number column in the script editor Feature #2512: Editor: Offer use of monospace fonts in the script editor as an option Feature #2514: Editor: focus on ID input field on clone/add Feature #2519: it is not possible to change icons that appear on the map after casting the Detect spells Task #2460: OS X: Use Application Support directory as user data path Task #2516: Editor: Change References / Referenceables terminology 0.35.1 ------ Bug #781: incorrect trajectory of the sun Bug #1079: Wrong starting position in "Character Stuff Wonderland" Bug #1443: Repetitive taking of a stolen object is repetitively considered as a crime Bug #1533: Divine Intervention goes to the wrong place. Bug #1714: No visual indicator for time passed during training Bug #1916: Telekinesis does not allow safe opening of traps Bug #2227: Editor: addon file name inconsistency Bug #2271: Player can melee enemies from water with impunity Bug #2275: Objects with bigger scale move further using Move script Bug #2285: Aryon's Dominator enchantment does not work properly Bug #2290: No punishment for stealing gold from owned containers Bug #2328: Launcher does not respond to Ctrl+C Bug #2334: Drag-and-drop on a content file in the launcher creates duplicate items Bug #2338: Arrows reclaimed from corpses do not stack sometimes Bug #2344: Launcher - Settings importer running correctly? Bug #2346: Launcher - Importing plugins into content list screws up the load order Bug #2348: Mod: H.E.L.L.U.V.A. Handy Holdables does not appear in the content list Bug #2353: Detect Animal detects dead creatures Bug #2354: Cmake does not respect LIB_SUFFIX Bug #2356: Active magic set inactive when switching magic items Bug #2361: ERROR: ESM Error: Previous record contains unread bytes Bug #2382: Switching spells with "next spell" or "previous spell" while holding shift promps delete spell dialog Bug #2388: Regression: Can't toggle map on/off Bug #2392: MOD Shrines - Restore Health and Cancel Options adds 100 health points Bug #2394: List of Data Files tab in openmw-laucher needs to show all content files. Bug #2402: Editor: skills saved incorrectly Bug #2408: Equipping a constant effect Restore Health/Magicka/Fatigue item will permanently boost the stat it's restoring Bug #2415: It is now possible to fall off the prison ship into the water when starting a new game Bug #2419: MOD MCA crash to desktop Bug #2420: Game crashes when character enters a certain area Bug #2421: infinite loop when using cycle weapon without having a weapon Feature #2221: Cannot dress dead NPCs Feature #2349: Check CMake sets correct MSVC compiler settings for release build. Feature #2397: Set default values for global mandatory records. Feature #2412: Basic joystick support 0.35.0 ------ Bug #244: Clipping/static in relation to the ghostgate/fence sound. Bug #531: Missing transparent menu items Bug #811: Content Lists in openmw.cfg are overwritten Bug #925: OpenCS doesn't launch because it thinks its already started Bug #969: Water shader strange behaviour on AMD card Bug #1049: Partially highlighted word in dialogue may cause incorrect line break Bug #1069: omwlauncher.exe crashes due to file lock Bug #1192: It is possible to jump on top of hostile creatures in combat Bug #1342: Loud ambient sounds Bug #1431: Creatures can climb the player Bug #1605: Guard in CharGen doesn't turn around to face you when reaching stairs Bug #1624: Moon edges don't transition properly Bug #1634: Items dropped by PC have collision Bug #1637: Weird NPC behaviour in Vivec, Hlaalu Ancestral Vaults? Bug #1638: Cannot climb staircases Bug #1648: Enchanted equipment badly handled at game reload Bug #1663: Crash when casting spell at enemy near you Bug #1683: Scale doesn't apply to animated collision nodes Bug #1702: Active enchanted item forgotten Bug #1730: Scripts names starting with digit(s) fail to compile Bug #1743: Moons are transparent Bug #1745: Shadows crash: Assertion `mEffects.empty()' failed. Bug #1785: Can't equip two-handed weapon and shield Bug #1809: Player falls too easily Bug #1825: Sword of Perithia can´t run in OpenMW Bug #1899: The launcher resets any alterations you´ve made in the mod list order, Bug #1964: Idle voices/dialogs not triggered correctly Bug #1980: Please, change default click behavior in OpenMW Launchers Data Files list Bug #1984: Vampire corpses standing up when looting the first item Bug #1985: Calm spell does nothing Bug #1986: Spell name lights up on mouseover but spell cost does not Bug #1989: Tooltip still shown when menu toggled off Bug #2010: Raindrops Displayed While Underwater Bug #2023: Walking into plants causes massive framedrop Bug #2031: [MOD: Shrines - Restore Health and Cancel Options]: Restore health option doesn't work Bug #2039: Lake Fjalding pillar of fire not rendered Bug #2040: AI_follow should stop further from the target Bug #2076: Slaughterfish AI Bug #2077: Direction of long jump can be changed much more than it is possible in vanilla Bug #2078: error during rendering: Object '' not found (const) Bug #2105: Lockpicking causes screen sync glitch Bug #2113: [MOD: Julan Ashlander Companion] Julan does not act correctly within the Ghostfence. Bug #2123: Window glow mod: Collision issues Bug #2133: Missing collision for bridges in Balmora when using Morrowind Rebirth 2.81 Bug #2135: Casting a summon spell while the summon is active does not reset the summon. Bug #2144: Changing equipment will unequip drawn arrows/bolts Bug #2169: Yellow on faces when using opengl renderer and mods from overhaul on windows Bug #2175: Pathgrid mods do not overwrite the existing pathgrid Bug #2176: Morrowind -Russian localization end add-on ChaosHeart. Error in framelistener;object ;frenzying toush; not found Bug #2181: Mod Morrowind crafting merchants die. Bug #2182: mods changing skill progression double the bonus for class specialization Bug #2183: Editor: Skills "use value" only allows integer between 0 and 99 Bug #2184: Animated Morrowind Expanded produces an error on Open MW Launch Bug #2185: Conditional Operator formats Bug #2193: Quest: Gateway Ghost Bug #2194: Cannot summon multiples of the same creature Bug #2195: Pathgrid in the (0,0) exterior cell not loaded Bug #2200: Outdoor NPCs can stray away and keep walking into a wall Bug #2201: Creatures do not receive fall damage Bug #2202: The enchantment the item can hold is calculated incorrectly Bug #2203: Having the mod Living Cities of Vvardenfall running causes the game world to fail to load after leaving the prison ship Bug #2204: Abot's Water Life - Book rendered incorrectly Bug #2205: sound_waterfall script no longer compiles Bug #2206: Dialogue script fails to compile (extra .) Bug #2207: Script using – instead of - character does not compile Bug #2208: Failing dialogue scripts in french Morrowind.esm Bug #2214: LGNPC Vivec Redoran 1.62 and The King Rat (Size and inventory Issues) Bug #2215: Beast races can use enchanted boots Bug #2218: Incorrect names body parts in 3D models for open helmet with skinning Bug #2219: Orcs in Ghorak Manor in Caldera don't attack if you pick their pockets. Bug #2220: Chargen race preview head incorrect orientation Bug #2223: Reseting rock falling animation Bug #2224: Fortify Attribute effects do not stack when Spellmaking. Bug #2226: OpenCS pseudo-crash Bug #2230: segfaulting when entering Ald'ruhn with a specific mod: "fermeture la nuit" (closed by night) Bug #2233: Area effect spells on touch do not have the area effect Bug #2234: Dwarven Crossbow clips through the ground when dropped Bug #2235: class SettingsBase<> reverses the order of entries with multiple keys. Bug #2236: Weird two handed longsword + torch interaction Bug #2237: Shooting arrows while sneaking do not agro Bug #2238: Bipedal creatures not using weapons are not handled properly Bug #2245: Incorrect topic highlighting in HT_SpyBaladas quest Bug #2252: Tab completion incomplete for places using COC from the console. Bug #2255: Camera reverts to first person on load Bug #2259: enhancement: the save/load progress bar is not very progressive Bug #2263: TogglePOV can not be bound to Alt key Bug #2267: dialogue disabling via mod Bug #2268: Highlighting Files with load order problems in Data Files tab of Launcher Bug #2276: [Mod]ShotN issues with Karthwasten Bug #2283: Count argument for PlaceAt functions not working Bug #2284: Local map notes should be visible on door marker leading to the cell with the note Bug #2293: There is a graphical glitch at the end of the spell's animation in 3rd Person (looking over the shoulder) view Bug #2294: When using Skyrim UI Overhaul, the tops of pinnable menus are invisible Bug #2302: Random leveled items repeat way too often in a single dungeon Bug #2306: Enchanted arrows should not be retrievable from corpses Bug #2308: No sound effect when drawing the next throwing knife Bug #2309: Guards chase see the player character even if they're invisible Bug #2319: Inverted controls and other issues after becoming a vampire Bug #2324: Spells cast when crossing cell border are imprinted on the local map Bug #2330: Actors with Drain Health effect retain health after dying Bug #2331: tgm (god mode) won't allow the player to cast spells if the player doesn't have enough mana Bug #2332: Error in framelistener: Need a skeleton to attach the arrow to Feature #114: ess-Importer Feature #504: Editor: Delete selected rows from result windows Feature #1024: Addition of remaining equipping hotkeys Feature #1067: Handle NIF interpolation type 4 (XYZ_ROTATION_KEY) Feature #1125: AI fast-forward Feature #1228: Drowning while knocked out Feature #1325: Editor: Opening window and User Settings window cleanup Feature #1537: Ability to change the grid size from 3x3 to 5x5 (or more with good pc) Feature #1546: Leveled list script functions Feature #1659: Test dialogue scripts in --script-all Feature #1720: NPC lookAt controller Feature #2178: Load initial particle system state from NIF files Feature #2197: Editor: When clicking on a script error in the report window set cursor in script editor to the respective line/column Feature #2261: Warn when loading save games with mod mismatch Feature #2313: ess-Importer: convert global map exploration overlay Feature #2318: Add commandline option to load a save game Task #810: Rename "profile" to "content list" Task #2196: Label local/global openmw.cfg files via comments 0.34.0 ------ Bug #904: omwlauncher doesn't allow installing Tribunal and Bloodmoon if only MW is installed Bug #986: Launcher: renaming profile names is broken Bug #1061: "Browse to CD..." launcher crash Bug #1135: Launcher crashes if user does not have write permission Bug #1231: Current installer in launcher does not correctly import russian Morrowind.ini settings from setup.inx Bug #1288: Fix the Alignment of the Resolution Combobox Bug #1343: BIK videos occasionally out of sync with audio Bug #1684: Morrowind Grass Mod graphical glitches Bug #1734: NPC in fight with invisible/sneaking player Bug #1982: Long class names are cut off in the UI Bug #2012: Editor: OpenCS script compiler sometimes fails to find IDs Bug #2015: Running while levitating does not affect speed but still drains fatigue Bug #2018: OpenMW don´t reset modified cells to vanilla when a plugin is deselected and don´t apply changes to cells already visited. Bug #2045: ToggleMenus command should close dialogue windows Bug #2046: Crash: light_de_streetlight_01_223 Bug #2047: Buglamp tooltip minor correction Bug #2050: Roobrush floating texture bits Bug #2053: Slaves react negatively to PC picking up slave's bracers Bug #2055: Dremora corpses use the wrong model Bug #2056: Mansilamat Vabdas's corpse is floating in the water Bug #2057: "Quest: Larius Varro Tells A Little Story": Bounty not completely removed after finishing quest Bug #2059: Silenced enemies try to cast spells anyway Bug #2060: Editor: Special case implementation for top level window with single sub-window should be optional Bug #2061: Editor: SubView closing that is not directly triggered by the user isn't handled properly Bug #2063: Tribunal: Quest 'The Warlords' doesn't work Bug #2064: Sneak attack on hostiles causes bounty Bug #2065: Editor: Qt signal-slot error when closing a dialogue subview Bug #2070: Loading ESP in OpenMW works but fails in OpenCS Bug #2071: CTD in 0.33 Bug #2073: Storm atronach animation stops now and then Bug #2075: Molag Amur Region, Map shows water on solid ground Bug #2080: game won't work with fair magicka regen Bug #2082: NPCs appear frozen or switched off after leaving and quickly reentering a cell Bug #2088: OpenMW is unable to play OGG files. Bug #2093: Darth Gares talks to you in Ilunibi even when he's not there, screwing up the Main Quests Bug #2095: Coordinate and rotation editing in the Reference table does not work. Bug #2096: Some overflow fun and bartering exploit Bug #2098: [D3D] Game crash on maximize Bug #2099: Activate, player seems not to work Bug #2104: Only labels are sensitive in buttons Bug #2107: "Slowfall" effect is too weak Bug #2114: OpenCS doesn't load an ESP file full of errors even though Vanilla MW Construction Set can Bug #2117: Crash when encountering bandits on opposite side of river from the egg mine south of Balmora Bug #2124: [Mod: Baldurians Transparent Glass Amor] Armor above head Bug #2125: Unnamed NiNodes in weapons problem in First Person Bug #2126: Dirty dialog script in tribunal.esm causing bug in Tribunal MQ Bug #2128: Crash when picking character's face Bug #2129: Disable the third-person zoom feature by default Bug #2130: Ash storm particles shown too long during transition to clear sky Bug #2137: Editor: exception caused by following the Creature column of a SoundGen record Bug #2139: Mouse movement should be ignored during intro video Bug #2143: Editor: Saving is broken Bug #2145: OpenMW - crash while exiting x64 debug build Bug #2152: You can attack Almalexia during her final monologue Bug #2154: Visual effects behave weirdly after loading/taking a screenshot Bug #2155: Vivec has too little magicka Bug #2156: Azura's spirit fades away too fast Bug #2158: [Mod]Julan Ashlander Companion 2.0: Negative magicka Bug #2161: Editor: combat/magic/stealth values of creature not displayed correctly Bug #2163: OpenMW can't detect death if the NPC die by the post damage effect of a magic weapon. Bug #2168: Westly's Master Head Pack X – Some hairs aren't rendered correctly. Bug #2170: Mods using conversations to update PC inconsistant Bug #2180: Editor: Verifier doesn't handle Windows-specific path issues when dealing with resources Bug #2212: Crash or unexpected behavior while closing OpenCS cell render window on OS X Feature #238: Add UI to run INI-importer from the launcher Feature #854: Editor: Add user setting to show status bar Feature #987: Launcher: first launch instructions for CD need to be more explicit Feature #1232: There is no way to set the "encoding" option using launcher UI. Feature #1281: Editor: Render cell markers Feature #1918: Editor: Functionality for Double-Clicking in Tables Feature #1966: Editor: User Settings dialogue grouping/labelling/tooltips Feature #2097: Editor: Edit position of references in 3D scene Feature #2121: Editor: Add edit mode button to scene toolbar Task #1965: Editor: Improve layout of user settings dialogue 0.33.1 ------ Bug #2108: OpenCS fails to build 0.33.0 ------ Bug #371: If console assigned to ` (probably to any symbolic key), "`" symbol will be added to console every time it closed Bug #1148: Some books'/scrolls' contents are displayed incorrectly Bug #1290: Editor: status bar is not updated when record filter is changed Bug #1292: Editor: Documents are not removed on closing the last view Bug #1301: Editor: File->Exit only checks the document it was issued from. Bug #1353: Bluetooth on with no speaker connected results in significantly longer initial load times Bug #1436: NPCs react from too far distance Bug #1472: PC is placed on top of following NPC when changing cell Bug #1487: Tall PC can get stuck in staircases Bug #1565: Editor: Subviews are deleted on shutdown instead when they are closed Bug #1623: Door marker on Ghorak Manor's balcony makes PC stuck Bug #1633: Loaddoor to Sadrith Mora, Telvanni Council House spawns PC in the air Bug #1655: Use Appropriate Application Icons on Windows Bug #1679: Tribunal expansion, Meryn Othralas the backstage manager in the theatre group in Mournhold in the great bazaar district is floating a good feet above the ground. Bug #1705: Rain is broken in third person Bug #1706: Thunder and lighting still occurs while the game is paused during the rain Bug #1708: No long jumping Bug #1710: Editor: ReferenceableID drag to references record filter field creates incorrect filter Bug #1712: Rest on Water Bug #1715: "Cancel" button is not always on the same side of menu Bug #1725: Editor: content file can be opened multiple times from the same dialogue Bug #1730: [MOD: Less Generic Nerevarine] Compile failure attempting to enter the Corprusarium. Bug #1733: Unhandled ffmpeg sample formats Bug #1735: Editor: "Edit Record" context menu button not opening subview for journal infos Bug #1750: Editor: record edits result in duplicate entries Bug #1789: Editor: Some characters cannot be used in addon name Bug #1803: Resizing the map does not keep the pre-resize center at the post-resize center Bug #1821: Recovering Cloudcleaver quest: attacking Sosia is considered a crime when you side with Hlormar Bug #1838: Editor: Preferences window appears off screen Bug #1839: Editor: Record filter title should be moved two pixels to the right Bug #1849: Subrecord error in MAO_Containers Bug #1854: Knocked-out actors don't fully act knocked out Bug #1855: "Soul trapped" sound doesn't play Bug #1857: Missing sound effect for enchanted items with empty charge Bug #1859: Missing console command: ResetActors (RA) Bug #1861: Vendor category "MagicItems" is unhandled Bug #1862: Launcher doesn't start if a file listed in launcher.cfg has correct name but wrong capitalization Bug #1864: Editor: Region field for cell record in dialogue subview not working Bug #1869: Editor: Change label "Musics" to "Music" Bug #1870: Goblins killed while knocked down remain in knockdown-pose Bug #1874: CellChanged events should not trigger when crossing exterior cell border Bug #1877: Spriggans killed instantly if hit while regening Bug #1878: Magic Menu text not un-highlighting correctly when going from spell to item as active magic Bug #1881: Stuck in ceiling when entering castle karstaags tower Bug #1884: Unlit torches still produce a burning sound Bug #1885: Can type text in price field in barter window Bug #1887: Equipped items do not emit sounds Bug #1889: draugr lord aesliip will attack you and remain non-hostile Bug #1892: Guard asks player to pay bounty of 0 gold Bug #1895: getdistance should only return max float if ref and target are in different worldspaces Bug #1896: Crash Report Bug #1897: Conjured Equipment cant be re-equipped if removed Bug #1898: Only Gidar Verothan follows you during establish the mine quest Bug #1900: Black screen when you open the door and breath underwater Bug #1904: Crash on casting recall spell Bug #1906: Bound item checks should use the GMSTs Bug #1907: Bugged door. Mournhold, The Winged Guar Bug #1908: Crime reported for attacking Drathas Nerus's henchmen while they attack Dilborn Bug #1909: Weird Quest Flow Infidelities quest Bug #1910: Follower fighting with gone npc Bug #1911: Npcs will drown themselves Bug #1912: World map arrow stays static when inside a building Bug #1920: Ulyne Henim disappears when game is loaded inside Vas Bug #1922: alchemy-> potion of paralyze Bug #1923: "levitation magic cannot be used here" shows outside of tribunal Bug #1927: AI prefer melee over magic. Bug #1929: Tamriel Rebuilt: Named cells that lie within the overlap with Morrowind.esm are not shown Bug #1932: BTB - Spells 14.1 magic effects don´t overwrite the Vanilla ones but are added Bug #1935: Stacks of items are worth more when sold individually Bug #1940: Launcher does not list addon files if base game file is renamed to a different case Bug #1946: Mod "Tel Nechim - moved" breaks savegames Bug #1947: Buying/Selling price doesn't properly affect the growth of mercantile skill Bug #1950: followers from east empire company quest will fight each other if combat happens with anything Bug #1958: Journal can be scrolled indefinitely with a mouse wheel Bug #1959: Follower not leaving party on quest end Bug #1960: Key bindings not always saved correctly Bug #1961: Spell merchants selling racial bonus spells Bug #1967: segmentation fault on load saves Bug #1968: Jump sounds are not controlled by footsteps slider, sound weird compared to footsteps Bug #1970: PC suffers silently when taking damage from lava Bug #1971: Dwarven Sceptre collision area is not removed after killing one Bug #1974: Dalin/Daris Norvayne follows player indefinitely Bug #1975: East Empire Company faction rank breaks during Raven Rock questline Bug #1979: 0 strength = permanently over encumbered Bug #1993: Shrine blessing in Maar Gan doesn't work Bug #2008: Enchanted items do not recharge Bug #2011: Editor: OpenCS script compiler doesn't handle member variable access properly Bug #2016: Dagoth Ur already dead in Facility Cavern Bug #2017: Fighters Guild Quest: The Code Book - dialogue loop when UMP is loaded. Bug #2019: Animation of 'Correct UV Mudcrabs' broken Bug #2022: Alchemy window - Removing ingredient doesn't remove the number of ingredients Bug #2025: Missing mouse-over text for non affordable items Bug #2028: [MOD: Tamriel Rebuilt] Crashing when trying to enter interior cell "Ruinous Keep, Great Hall" Bug #2029: Ienith Brothers Thiev's Guild quest journal entry not adding Feature #471: Editor: Special case implementation for top-level window with single sub-window Feature #472: Editor: Sub-Window re-use settings Feature #704: Font colors import from fallback settings Feature #879: Editor: Open sub-views in a new top-level window Feature #932: Editor: magic effect table Feature #937: Editor: Path Grid table Feature #938: Editor: Sound Gen table Feature #1117: Death and LevelUp music Feature #1226: Editor: Request UniversalId editing from table columns Feature #1545: Targeting console on player Feature #1597: Editor: Render terrain Feature #1695: Editor: add column for CellRef's global variable Feature #1696: Editor: use ESM::Cell's RefNum counter Feature #1697: Redden player's vision when hit Feature #1856: Spellcasting for non-biped creatures Feature #1879: Editor: Run OpenMW with the currently edited content list Task #1851: Move AI temporary state out of AI packages Task #1865: Replace char type in records 0.32.0 ------ Bug #1132: Unable to jump when facing a wall Bug #1341: Summoned Creatures do not immediately disappear when killed. Bug #1430: CharGen Revamped script does not compile Bug #1451: NPCs shouldn't equip weapons prior to fighting Bug #1461: Stopped start scripts do not restart on load Bug #1473: Dead NPC standing and in 2 pieces Bug #1482: Abilities are depleted when interrupted during casting Bug #1503: Behaviour of NPCs facing the player Bug #1506: Missing character, French edition: three-points Bug #1528: Inventory very slow after 2 hours Bug #1540: Extra arguments should be ignored for script functions Bug #1541: Helseth's Champion: Tribunal Bug #1570: Journal cannot be opened while in inventory screen Bug #1573: PC joins factions at random Bug #1576: NPCs aren't switching their weapons when out of ammo Bug #1579: Guards detect creatures in far distance, instead on sight Bug #1588: The Siege of the Skaal Village: bloodmoon Bug #1593: The script compiler isn't recognising some names that contain a - Bug #1606: Books: Question marks instead of quotation marks Bug #1608: Dead bodies prevent door from opening/closing. Bug #1609: Imperial guards in Sadrith Mora are not using their spears Bug #1610: The bounty number is not displayed properly with high numbers Bug #1620: Implement correct formula for auto-calculated NPC spells Bug #1630: Boats standing vertically in Vivec Bug #1635: Arrest dialogue is executed second time after I select "Go to jail" Bug #1637: Weird NPC behaviour in Vivec, Hlaalu Ancestral Vaults? Bug #1641: Persuasion dialog remains after loading, possibly resulting in crash Bug #1644: "Goodbye" and similar options on dialogues prevents escape working properly. Bug #1646: PC skill stats are not updated immediately when changing equipment Bug #1652: Non-aggressive creature Bug #1653: Quickloading while the container window is open crashes the game Bug #1654: Priority of checks in organic containers Bug #1656: Inventory items merge issue when repairing Bug #1657: Attacked state of NPCs is not saved properly Bug #1660: Rank dialogue condition ignored Bug #1668: Game starts on day 2 instead of day 1 Bug #1669: Critical Strikes while fighting a target who is currently fighting me Bug #1672: OpenCS doesn't save the projects Bug #1673: Fatigue decreasing by only one point when running Bug #1675: Minimap and localmap graphic glitches Bug #1676: Pressing the OK button on the travel menu cancels the travel and exits the menu Bug #1677: Sleeping in a rented bed is considered a crime Bug #1685: NPCs turn towards player even if invisible/sneaking Bug #1686: UI bug: cursor is clicking "world/local" map button while inventory window is closed? Bug #1690: Double clicking on a inventory window header doesn't close it. Bug #1693: Spell Absorption does not absorb shrine blessings Bug #1694: journal displays learned topics as quests Bug #1700: Sideways scroll of text boxes Bug #1701: Player enchanting requires player hold money, always 100% sucessful. Bug #1704: self-made Fortify Intelligence/Drain willpower potions are broken Bug #1707: Pausing the game through the esc menu will silence rain, pausing it by opening the inventory will not. Bug #1709: Remesa Othril is hostile to Hlaalu members Bug #1713: Crash on load after death Bug #1719: Blind effect has slight border at the edge of the screen where it is ineffective. Bug #1722: Crash after creating enchanted item, reloading saved game Bug #1723: Content refs that are stacked share the same index after unstacking Bug #1726: Can't finish Aengoth the Jeweler's quest : Retrieve the Scrap Metal Bug #1727: Targets almost always resist soultrap scrolls Bug #1728: Casting a soultrap spell on invalid target yields no message Bug #1729: Chop attack doesn't work if walking diagonally Bug #1732: Error handling for missing script function arguments produces weird message Bug #1736: Alt-tabbing removes detail from overworld map. Bug #1737: Going through doors with (high magnitude?) leviation will put the player high up, possibly even out of bounds. Bug #1739: Setting a variable on an NPC from another NPC's dialogue result sets the wrong variable Bug #1741: The wait dialogue doesn't black the screen out properly during waiting. Bug #1742: ERROR: Object 'sDifficulty' not found (const) Bug #1744: Night sky in Skies V.IV (& possibly v3) by SWG rendered incorrectly Bug #1746: Bow/marksman weapon condition does not degrade with use Bug #1749: Constant Battle Music Bug #1752: Alt-Tabbing in the character menus makes the paper doll disappear temporarily Bug #1753: Cost of training is not added to merchant's inventory Bug #1755: Disposition changes do not persist if the conversation menu is closed by purchasing training. Bug #1756: Caught Blight after being cured of Corprus Bug #1758: Crash Upon Loading New Cell Bug #1760: Player's Magicka is not recalculated upon drained or boosted intelligence Bug #1761: Equiped torches lost on reload Bug #1762: Your spell did not get a target. Soul trap. Gorenea Andrano Bug #1763: Custom Spell Magicka Cost Bug #1765: Azuras Star breaks on recharging item Bug #1767: GetPCRank did not handle ignored explicit references Bug #1772: Dark Brotherhood Assassins never use their Carved Ebony Dart, sticking to their melee weapon. Bug #1774: String table overflow also occurs when loading TheGloryRoad.esm Bug #1776: dagoth uthol runs in slow motion Bug #1778: Incorrect values in spellmaking window Bug #1779: Icon of Master Propylon Index is not visible Bug #1783: Invisible NPC after looting corpse Bug #1787: Health Calculation Bug #1788: Skeletons, ghosts etc block doors when we try to open Bug #1791: [MOD: LGNPC Foreign Quarter] NPC in completely the wrong place. Bug #1792: Potions should show more effects Bug #1793: Encumbrance while bartering Bug #1794: Fortify attribute not affecting fatigue Bug #1795: Too much magicka Bug #1796: "Off by default" torch burning Bug #1797: Fish too slow Bug #1798: Rest until healed shouldn't show with full health and magicka Bug #1802: Mark location moved Bug #1804: stutter with recent builds Bug #1810: attack gothens dremora doesnt agro the others. Bug #1811: Regression: Crash Upon Loading New Cell Bug #1812: Mod: "QuickChar" weird button placement Bug #1815: Keys show value and weight, Vanilla Morrowind's keys dont. Bug #1817: Persuasion results do not show using unpatched MW ESM Bug #1818: Quest B3_ZainabBride moves to stage 47 upon loading save while Falura Llervu is following Bug #1823: AI response to theft incorrect - only guards react, in vanilla everyone does. Bug #1829: On-Target Spells Rendered Behind Water Surface Effects Bug #1830: Galsa Gindu's house is on fire Bug #1832: Fatal Error: OGRE Exception(2:InvalidParametersException) Bug #1836: Attacked Guards open "fine/jail/resist"-dialogue after killing you Bug #1840: Infinite recursion in ActionTeleport Bug #1843: Escorted people change into player's cell after completion of escort stage Bug #1845: Typing 'j' into 'Name' fields opens the journal Bug #1846: Text pasted into the console still appears twice (Windows) Bug #1847: "setfatigue 0" doesn't render NPC unconscious Bug #1848: I can talk to unconscious actors Bug #1866: Crash when player gets killed by a creature summoned by him Bug #1868: Memory leaking when openmw window is minimized Feature #47: Magic Effects Feature #642: Control NPC mouth movement using current Say sound Feature #939: Editor: Resources tables Feature #961: AI Combat for magic (spells, potions and enchanted items) Feature #1111: Collision script instructions (used e.g. by Lava) Feature #1120: Command creature/humanoid magic effects Feature #1121: Elemental shield magic effects Feature #1122: Light magic effect Feature #1139: AI: Friendly hits Feature #1141: AI: combat party Feature #1326: Editor: Add tooltips to all graphical buttons Feature #1489: Magic effect Get/Mod/Set functions Feature #1505: Difficulty slider Feature #1538: Targeted scripts Feature #1571: Allow creating custom markers on the local map Feature #1615: Determine local variables from compiled scripts instead of the values in the script record Feature #1616: Editor: Body part record verifier Feature #1651: Editor: Improved keyboard navigation for scene toolbar Feature #1666: Script blacklisting Feature #1711: Including the Git revision number from the command line "--version" switch. Feature #1721: NPC eye blinking Feature #1740: Scene toolbar buttons for selecting which type of elements are rendered Feature #1790: Mouse wheel scrolling for the journal Feature #1850: NiBSPArrayController Task #768: On windows, settings folder should be "OpenMW", not "openmw" Task #908: Share keyframe data Task #1716: Remove defunct option for building without FFmpeg 0.31.0 ------ Bug #245: Cloud direction and weather systems differ from Morrowind Bug #275: Local Map does not always show objects that span multiple cells Bug #538: Update CenterOnCell (COC) function behavior Bug #618: Local and World Map Textures are sometimes Black Bug #640: Water behaviour at night Bug #668: OpenMW doesn't support non-latin paths on Windows Bug #746: OpenMW doesn't check if the background music was already played Bug #747: Door is stuck if cell is left before animation finishes Bug #772: Disabled statics are visible on map Bug #829: OpenMW uses up all available vram, when playing for extended time Bug #869: Dead bodies don't collide with anything Bug #894: Various character creation issues Bug #897/#1369: opencs Segmentation Fault after "new" or "load" Bug #899: Various jumping issues Bug #952: Reflection effects are one frame delayed Bug #993: Able to interact with world during Wait/Rest dialog Bug #995: Dropped items can be placed inside the wall Bug #1008: Corpses always face up upon reentering the cell Bug #1035: Random colour patterns appearing in automap Bug #1037: Footstep volume issues Bug #1047: Creation of wrong links in dialogue window Bug #1129: Summoned creature time life duration seems infinite Bug #1134: Crimes can be committed against hostile NPCs Bug #1136: Creature run speed formula is incorrect Bug #1150: Weakness to Fire doesn't apply to Fire Damage in the same spell Bug #1155: NPCs killing each other Bug #1166: Bittercup script still does not work Bug #1178: .bsa file names are case sensitive. Bug #1179: Crash after trying to load game after being killed Bug #1180: Changing footstep sound location Bug #1196: Jumping not disabled when showing messageboxes Bug #1202: "strange" keys are not shown in binding menu, and are not saved either, but works Bug #1216: Broken dialog topics in russian Morrowind Bug #1217: Container content changes based on the current position of the mouse Bug #1234: Loading/saving issues with dynamic records Bug #1277: Text pasted into the console appears twice Bug #1284: Crash on New Game Bug #1303: It's possible to skip the chargen Bug #1304: Slaughterfish should not detect the player unless the player is in the water Bug #1311: Editor: deleting Record Filter line does not reset the filter Bug #1324: ERROR: ESM Error: String table overflow when loading Animated Morrowind.esp Bug #1328: Editor: Bogus Filter created when dragging multiple records to filter bar of non-applicable table Bug #1331: Walking/running sound persist after killing NPC`s that are walking/running. Bug #1334: Previously equipped items not shown as unequipped after attempting to sell them. Bug #1335: Actors ignore vertical axis when deciding to attack Bug #1338: Unknown toggle option for shadows Bug #1339: "Ashlands Region" is visible when beginning new game during "Loading Area" process Bug #1340: Guards prompt Player with punishment options after resisting arrest with another guard. Bug #1348: Regression: Bug #1098 has returned with a vengeance Bug #1349: [TR] TR_Data mesh tr_ex_imp_gatejamb01 cannot be activated Bug #1352: Disabling an ESX file does not disable dependent ESX files Bug #1355: CppCat Checks OpenMW Bug #1356: Incorrect voice type filtering for sleep interrupts Bug #1357: Restarting the game clears saves Bug #1360: Seyda Neen silk rider dialog problem Bug #1361: Some lights don't work Bug #1364: It is difficult to bind "Mouse 1" to an action in the options menu Bug #1370: Animation compilation mod does not work properly Bug #1371: SL_Pick01.nif from third party fails to load in openmw, but works in Vanilla Bug #1373: When stealing in front of Sellus Gravius cannot exit the dialog Bug #1378: Installs to /usr/local are not working Bug #1380: Loading a save file fail if one of the content files is disabled Bug #1382: "getHExact() size mismatch" crash on loading official plugin "Siege at Firemoth.esp" Bug #1386: Arkngthand door will not open Bug #1388: Segfault when modifying View Distance in Menu options Bug #1389: Crash when loading a save after dying Bug #1390: Apostrophe characters not displayed [French version] Bug #1391: Custom made icon background texture for magical weapons and stuff isn't scaled properly on GUI. Bug #1393: Coin icon during the level up dialogue are off of the background Bug #1394: Alt+F4 doesn't work on Win version Bug #1395: Changing rings switches only the last one put on Bug #1396: Pauldron parts aren't showing when the robe is equipped Bug #1402: Dialogue of some shrines have wrong button orientation Bug #1403: Items are floating in the air when they're dropped onto dead bodies. Bug #1404: Forearms are not rendered on Argonian females Bug #1407: Alchemy allows making potions from two of the same item Bug #1408: "Max sale" button gives you all the items AND all the trader's gold Bug #1409: Rest "Until Healed" broken for characters with stunted magicka. Bug #1412: Empty travel window opens while playing through start game Bug #1413: Save game ignores missing writing permission Bug #1414: The Underground 2 ESM Error Bug #1416: Not all splash screens in the Splash directory are used Bug #1417: Loading saved game does not terminate Bug #1419: Skyrim: Home of the Nords error Bug #1422: ClearInfoActor Bug #1423: ForceGreeting closes existing dialogue windows Bug #1425: Cannot load save game Bug #1426: Read skill books aren't stored in savegame Bug #1427: Useless items can be set under hotkeys Bug #1429: Text variables in journal Bug #1432: When attacking friendly NPC, the crime is reported and bounty is raised after each swing Bug #1435: Stealing priceless items is without punishment Bug #1437: Door marker at Jobasha's Rare Books is spawning PC in the air Bug #1440: Topic selection menu should be wider Bug #1441: Dropping items on the rug makes them inaccessible Bug #1442: When dropping and taking some looted items, bystanders consider that as a crime Bug #1444: Arrows and bolts are not dropped where the cursor points Bug #1445: Security trainers offering acrobatics instead Bug #1447: Character dash not displayed, French edition Bug #1448: When the player is killed by the guard while having a bounty on his head, the guard dialogue opens over and over instead of loading dialogue Bug #1454: Script error in SkipTutorial Bug #1456: Bad lighting when using certain Morrowind.ini generated by MGE Bug #1457: Heart of Lorkan comes after you when attacking it Bug #1458: Modified Keybindings are not remembered Bug #1459: Dura Gra-Bol doesn't respond to PC attack Bug #1462: Interior cells not loaded with Morrowind Patch active Bug #1469: Item tooltip should show the base value, not real value Bug #1477: Death count is not stored in savegame Bug #1478: AiActivate does not trigger activate scripts Bug #1481: Weapon not rendered when partially submerged in water Bug #1483: Enemies are attacking even while dying Bug #1486: ESM Error: Don't know what to do with INFO Bug #1490: Arrows shot at PC can end up in inventory Bug #1492: Monsters respawn on top of one another Bug #1493: Dialogue box opens with follower NPC even if NPC is dead Bug #1494: Paralysed cliffracers remain airbourne Bug #1495: Dialogue box opens with follower NPC even the game is paused Bug #1496: GUI messages are not cleared when loading another saved game Bug #1499: Underwater sound sometimes plays when transitioning from interior. Bug #1500: Targetted spells and water. Bug #1502: Console error message on info refusal Bug #1507: Bloodmoon MQ The Ritual of Beasts: Can't remove the arrow Bug #1508: Bloodmoon: Fort Frostmoth, cant talk with Carnius Magius Bug #1516: PositionCell doesn't move actors to current cell Bug #1518: ForceGreeting broken for explicit references Bug #1522: Crash after attempting to play non-music file Bug #1523: World map empty after loading interior save Bug #1524: Arrows in waiting/resting dialog act like minimum and maximum buttons Bug #1525: Werewolf: Killed NPC's don't fill werewolfs hunger for blood Bug #1527: Werewolf: Detect life detects wrong type of actor Bug #1529: OpenMW crash during "the shrine of the dead" mission (tribunal) Bug #1530: Selected text in the console has the same color as the background Bug #1539: Barilzar's Mazed Band: Tribunal Bug #1542: Looping taunts from NPC`s after death: Tribunal Bug #1543: OpenCS crash when using drag&drop in script editor Bug #1547: Bamz-Amschend: Centurion Archers combat problem Bug #1548: The Missing Hand: Tribunal Bug #1549: The Mad God: Tribunal, Dome of Serlyn Bug #1557: A bounty is calculated from actual item cost Bug #1562: Invisible terrain on top of Red Mountain Bug #1564: Cave of the hidden music: Bloodmoon Bug #1567: Editor: Deleting of referenceables does not work Bug #1568: Picking up a stack of items and holding the enter key and moving your mouse around paints a bunch of garbage on screen. Bug #1574: Solstheim: Drauger cant inflict damage on player Bug #1578: Solstheim: Bonewolf running animation not working Bug #1585: Particle effects on PC are stopped when paralyzed Bug #1589: Tribunal: Crimson Plague quest does not update when Gedna Relvel is killed Bug #1590: Failed to save game: compile error Bug #1598: Segfault when making Drain/Fortify Skill spells Bug #1599: Unable to switch to fullscreen Bug #1613: Morrowind Rebirth duplicate objects / vanilla objects not removed Bug #1618: Death notice fails to show up Bug #1628: Alt+Tab Segfault Feature #32: Periodic Cleanup/Refill Feature #41: Precipitation and weather particles Feature #568: Editor: Configuration setup Feature #649: Editor: Threaded loading Feature #930: Editor: Cell record saving Feature #934: Editor: Body part table Feature #935: Editor: Enchantment effect table Feature #1162: Dialogue merging Feature #1174: Saved Game: add missing creature state Feature #1177: Saved Game: fog of war state Feature #1312: Editor: Combat/Magic/Stealth values for creatures are not displayed Feature #1314: Make NPCs and creatures fight each other Feature #1315: Crime: Murder Feature #1321: Sneak skill enhancements Feature #1323: Handle restocking items Feature #1332: Saved Game: levelled creatures Feature #1347: modFactionReaction script instruction Feature #1362: Animated main menu support Feature #1433: Store walk/run toggle Feature #1449: Use names instead of numbers for saved game files and folders Feature #1453: Adding Delete button to the load menu Feature #1460: Enable Journal screen while in dialogue Feature #1480: Play Battle music when in combat Feature #1501: Followers unable to fast travel with you Feature #1520: Disposition and distance-based aggression/ShouldAttack Feature #1595: Editor: Object rendering in cells Task #940: Move license to locations where applicable Task #1333: Remove cmake git tag reading Task #1566: Editor: Object rendering refactoring 0.30.0 ------ Bug #416: Extreme shaking can occur during cell transitions while moving Bug #1003: Province Cyrodiil: Ogre Exception in Stirk Bug #1071: Crash when given a non-existent content file Bug #1080: OpenMW allows resting/using a bed while in combat Bug #1097: Wrong punishment for stealing in Census and Excise Office at the start of a new game Bug #1098: Unlocked evidence chests should get locked after new evidence is put into them Bug #1099: NPCs that you attacked still fight you after you went to jail/paid your fine Bug #1100: Taking items from a corpse is considered stealing Bug #1126: Some creatures can't get close enough to attack Bug #1144: Killed creatures seem to die again each time player transitions indoors/outdoors Bug #1181: loading a saved game does not reset the player control status Bug #1185: Collision issues in Addamasartus Bug #1187: Athyn Sarethi mission, rescuing varvur sarethi from the doesnt end the mission Bug #1189: Crash when entering interior cell "Gnisis, Arvs-Drelen" Bug #1191: Picking up papers without inventory in new game Bug #1195: NPCs do not equip torches in certain interiors Bug #1197: mouse wheel makes things scroll too fast Bug #1200: door blocked by monsters Bug #1201: item's magical charges are only refreshed when they are used Bug #1203: Scribs do not defend themselves Bug #1204: creatures life is not empty when they are dead Bug #1205: armor experience does not progress when hits are taken Bug #1206: blood particules always red. Undeads and mechanicals should have a different one. Bug #1209: Tarhiel never falls Bug #1210: journal adding script is ran again after having saved/loaded Bug #1224: Names of custom classes are not properly handled in save games Bug #1227: Editor: Fixed case handling for broken localised versions of Morrowind.esm Bug #1235: Indoors walk stutter Bug #1236: Aborting intro movie brings up the menu Bug #1239: NPCs get stuck when walking past each other Bug #1240: BTB - Settings 14.1 and Health Bar. Bug #1241: BTB - Character and Khajiit Prejudice Bug #1248: GUI Weapon icon is changed to hand-to-hand after save load Bug #1254: Guild ranks do not show in dialogue Bug #1255: When opening a container and selecting "Take All", the screen flashes blue Bug #1260: Level Up menu doesn't show image when using a custom class Bug #1265: Quit Menu Has Misaligned Buttons Bug #1270: Active weapon icon is not updated when weapon is repaired Bug #1271: NPC Stuck in hovering "Jumping" animation Bug #1272: Crash when attempting to load Big City esm file. Bug #1276: Editor: Dropping a region into the filter of a cell subview fails Bug #1286: Dialogue topic list clips with window frame Bug #1291: Saved game: store faction membership Bug #1293: Pluginless Khajiit Head Pack by ashiraniir makes OpenMW close. Bug #1294: Pasting in console adds text to end, not at cursor Bug #1295: Conversation loop when asking about "specific place" in Vivec Bug #1296: Caius doesn't leave at start of quest "Mehra Milo and the Lost Prophecies" Bug #1297: Saved game: map markers Bug #1302: ring_keley script causes vector::_M_range_check exception Bug #1309: Bug on "You violated the law" dialog Bug #1319: Creatures sometimes rendered incorrectly Feature #50: Ranged Combat Feature #58: Sneaking Skill Feature #73: Crime and Punishment Feature #135: Editor: OGRE integration Feature #541: Editor: Dialogue Sub-Views Feature #853: Editor: Rework User Settings Feature #944: Editor: lighting modes Feature #945: Editor: Camera navigation mode Feature #953: Trader gold Feature #1140: AI: summoned creatures Feature #1142: AI follow: Run stance Feature #1154: Not all NPCs get aggressive when one is attacked Feature #1169: Terrain threading Feature #1172: Loading screen and progress bars during saved/loading game Feature #1173: Saved Game: include weather state Feature #1207: Class creation form does not remember Feature #1220: Editor: Preview Subview Feature #1223: Saved Game: Local Variables Feature #1229: Quicksave, quickload, autosave Feature #1230: Deleting saves Feature #1233: Bribe gold is placed into NPCs inventory Feature #1252: Saved Game: quick key bindings Feature #1273: Editor: Region Map context menu Feature #1274: Editor: Region Map drag & drop Feature #1275: Editor: Scene subview drop Feature #1282: Non-faction member crime recognition. Feature #1289: NPCs return to default position Task #941: Remove unused cmake files 0.29.0 ------ Bug #556: Video soundtrack not played when music volume is set to zero Bug #829: OpenMW uses up all available vram, when playing for extended time Bug #848: Wrong amount of footsteps playing in 1st person Bug #888: Ascended Sleepers have movement issues Bug #892: Explicit references are allowed on all script functions Bug #999: Graphic Herbalism (mod): sometimes doesn't activate properly Bug #1009: Lake Fjalding AI related slowdown. Bug #1041: Music playback issues on OS X >= 10.9 Bug #1043: No message box when advancing skill "Speechcraft" while in dialog window Bug #1060: Some message boxes are cut off at the bottom Bug #1062: Bittercup script does not work ('end' variable) Bug #1074: Inventory paperdoll obscures armour rating Bug #1077: Message after killing an essential NPC disappears too fast Bug #1078: "Clutterbane" shows empty charge bar Bug #1083: UndoWerewolf fails Bug #1088: Better Clothes Bloodmoon Plus 1.5 by Spirited Treasure pants are not rendered Bug #1090: Start scripts fail when going to a non-predefined cell Bug #1091: Crash: Assertion `!q.isNaN() && "Invalid orientation supplied as parameter"' failed. Bug #1093: Weapons of aggressive NPCs are invisible after you exit and re-enter interior Bug #1105: Magicka is depleted when using uncastable spells Bug #1106: Creatures should be able to run Bug #1107: TR cliffs have way too huge collision boxes in OpenMW Bug #1109: Cleaning True Light and Darkness with Tes3cmd makes Addamasartus , Zenarbael and Yasamsi flooded. Bug #1114: Bad output for desktop-file-validate on openmw.desktop (and opencs.desktop) Bug #1115: Memory leak when spying on Fargoth Bug #1137: Script execution fails (drenSlaveOwners script) Bug #1143: Mehra Milo quest (vivec informants) is broken Bug #1145: Issues with moving gold between inventory and containers Bug #1146: Issues with picking up stacks of gold Bug #1147: Dwemer Crossbows are held incorrectly Bug #1158: Armor rating should always stay below inventory mannequin Bug #1159: Quick keys can be set during character generation Bug #1160: Crash on equip lockpick when Bug #1167: Editor: Referenceables are not correctly loaded when dealing with more than one content file Bug #1184: Game Save: overwriting an existing save does not actually overwrites the file Feature #30: Loading/Saving (still missing a few parts) Feature #101: AI Package: Activate Feature #103: AI Package: Follow, FollowCell Feature #138: Editor: Drag & Drop Feature #428: Player death Feature #505: Editor: Record Cloning Feature #701: Levelled creatures Feature #708: Improved Local Variable handling Feature #709: Editor: Script verifier Feature #764: Missing journal backend features Feature #777: Creature weapons/shields Feature #789: Editor: Referenceable record verifier Feature #924: Load/Save GUI (still missing loading screen and progress bars) Feature #946: Knockdown Feature #947: Decrease fatigue when running, swimming and attacking Feature #956: Melee Combat: Blocking Feature #957: Area magic Feature #960: Combat/AI combat for creatures Feature #962: Combat-Related AI instructions Feature #1075: Damage/Restore skill/attribute magic effects Feature #1076: Soultrap magic effect Feature #1081: Disease contraction Feature #1086: Blood particles Feature #1092: Interrupt resting Feature #1101: Inventory equip scripts Feature #1116: Version/Build number in Launcher window Feature #1119: Resistance/weakness to normal weapons magic effect Feature #1123: Slow Fall magic effect Feature #1130: Auto-calculate spells Feature #1164: Editor: Case-insensitive sorting in tables 0.28.0 ------ Bug #399: Inventory changes are not visible immediately Bug #417: Apply weather instantly when teleporting Bug #566: Global Map position marker not updated for interior cells Bug #712: Looting corpse delay Bug #716: Problem with the "Vurt's Ascadian Isles Mod" mod Bug #805: Two TR meshes appear black (v0.24RC) Bug #841: Third-person activation distance taken from camera rather than head Bug #845: NPCs hold torches during the day Bug #855: Vvardenfell Visages Volume I some hairs don´t appear since 0,24 Bug #856: Maormer race by Mac Kom - The heads are way up Bug #864: Walk locks during loading in 3rd person Bug #871: active weapon/magic item icon is not immediately made blank if item is removed during dialog Bug #882: Hircine's Ring doesn't always work Bug #909: [Tamriel Rebuilt] crashes in Akamora Bug #922: Launcher writing merged openmw.cfg files Bug #943: Random magnitude should be calculated per effect Bug #948: Negative fatigue level should be allowed Bug #949: Particles in world space Bug #950: Hard crash on x64 Linux running --new-game (on startup) Bug #951: setMagicka and setFatigue have no effect Bug #954: Problem with equipping inventory items when using a keyboard shortcut Bug #955: Issues with equipping torches Bug #966: Shield is visible when casting spell Bug #967: Game crashes when equipping silver candlestick Bug #970: Segmentation fault when starting at Bal Isra Bug #977: Pressing down key in console doesn't go forward in history Bug #979: Tooltip disappears when changing inventory Bug #980: Barter: item category is remembered, but not shown Bug #981: Mod: replacing model has wrong position/orientation Bug #982: Launcher: Addon unchecking is not saved Bug #983: Fix controllers to affect objects attached to the base node Bug #985: Player can talk to NPCs who are in combat Bug #989: OpenMW crashes when trying to include mod with capital .ESP Bug #991: Merchants equip items with harmful constant effect enchantments Bug #994: Don't cap skills/attributes when set via console Bug #998: Setting the max health should also set the current health Bug #1005: Torches are visible when casting spells and during hand to hand combat. Bug #1006: Many NPCs have 0 skill Bug #1007: Console fills up with text Bug #1013: Player randomly loses health or dies Bug #1014: Persuasion window is not centered in maximized window Bug #1015: Player status window scroll state resets on status change Bug #1016: Notification window not big enough for all skill level ups Bug #1020: Saved window positions are not rescaled appropriately on resolution change Bug #1022: Messages stuck permanently on screen when they pile up Bug #1023: Journals doesn't open Bug #1026: Game loses track of torch usage. Bug #1028: Crash on pickup of jug in Unexplored Shipwreck, Upper level Bug #1029: Quick keys menu: Select compatible replacement when tool used up Bug #1042: TES3 header data wrong encoding Bug #1045: OS X: deployed OpenCS won't launch Bug #1046: All damaged weaponry is worth 1 gold Bug #1048: Links in "locked" dialogue are still clickable Bug #1052: Using color codes when naming your character actually changes the name's color Bug #1054: Spell effects not visible in front of water Bug #1055: Power-Spell animation starts even though you already casted it that day Bug #1059: Cure disease potion removes all effects from player, even your race bonus and race ability Bug #1063: Crash upon checking out game start ship area in Seyda Neen Bug #1064: openmw binaries link to unnecessary libraries Bug #1065: Landing from a high place in water still causes fall damage Bug #1072: Drawing weapon increases torch brightness Bug #1073: Merchants sell stacks of gold Feature #43: Visuals for Magic Effects Feature #51: Ranged Magic Feature #52: Touch Range Magic Feature #53: Self Range Magic Feature #54: Spell Casting Feature #70: Vampirism Feature #100: Combat AI Feature #171: Implement NIF record NiFlipController Feature #410: Window to restore enchanted item charge Feature #647: Enchanted item glow Feature #723: Invisibility/Chameleon magic effects Feature #737: Resist Magicka magic effect Feature #758: GetLOS Feature #926: Editor: Info-Record tables Feature #958: Material controllers Feature #959: Terrain bump, specular, & parallax mapping Feature #990: Request: unlock mouse when in any menu Feature #1018: Do not allow view mode switching while performing an action Feature #1027: Vertex morph animation (NiGeomMorpherController) Feature #1031: Handle NiBillboardNode Feature #1051: Implement NIF texture slot DarkTexture Task #873: Unify OGRE initialisation 0.27.0 ------ Bug #597: Assertion `dialogue->mId == id' failed in esmstore.cpp Bug #794: incorrect display of decimal numbers Bug #840: First-person sneaking camera height Bug #887: Ambient sounds playing while paused Bug #902: Problems with Polish character encoding Bug #907: Entering third person using the mousewheel is possible even if it's impossible using the key Bug #910: Some CDs not working correctly with Unshield installer Bug #917: Quick character creation plugin does not work Bug #918: Fatigue does not refill Bug #919: The PC falls dead in Beshara - OpenMW nightly Win64 (708CDE2) Feature #57: Acrobatics Skill Feature #462: Editor: Start Dialogue Feature #546: Modify ESX selector to handle new content file scheme Feature #588: Editor: Adjust name/path of edited content files Feature #644: Editor: Save Feature #710: Editor: Configure script compiler context Feature #790: God Mode Feature #881: Editor: Allow only one instance of OpenCS Feature #889: Editor: Record filtering Feature #895: Extinguish torches Feature #898: Breath meter enhancements Feature #901: Editor: Default record filter Feature #913: Merge --master and --plugin switches 0.26.0 ------ Bug #274: Inconsistencies in the terrain Bug #557: Already-dead NPCs do not equip clothing/items. Bug #592: Window resizing Bug #612: [Tamriel Rebuilt] Missing terrain (South of Tel Oren) Bug #664: Heart of lorkhan acts like a dead body (container) Bug #767: Wonky ramp physics & water Bug #780: Swimming out of water Bug #792: Wrong ground alignment on actors when no clipping Bug #796: Opening and closing door sound issue Bug #797: No clipping hinders opening and closing of doors Bug #799: sliders in enchanting window Bug #838: Pressing key during startup procedure freezes the game Bug #839: Combat/magic stances during character creation Bug #843: [Tribunal] Dark Brotherhood assassin appears without equipment Bug #844: Resting "until healed" option given even with full stats Bug #846: Equipped torches are invisible. Bug #847: Incorrect formula for autocalculated NPC initial health Bug #850: Shealt weapon sound plays when leaving magic-ready stance Bug #852: Some boots do not produce footstep sounds Bug #860: FPS bar misalignment Bug #861: Unable to print screen Bug #863: No sneaking and jumping at the same time Bug #866: Empty variables in [Movies] section of Morrowind.ini gets imported into OpenMW.cfg as blank fallback option and crashes game on start. Bug #867: Dancing girls in "Suran, Desele's House of Earthly Delights" don't dance. Bug #868: Idle animations are repeated Bug #874: Underwater swimming close to the ground is jerky Bug #875: Animation problem while swimming on the surface and looking up Bug #876: Always a starting upper case letter in the inventory Bug #878: Active spell effects don't update the layout properly when ended Bug #891: Cell 24,-12 (Tamriel Rebuilt) crashes on load Bug #896: New game sound issue Feature #49: Melee Combat Feature #71: Lycanthropy Feature #393: Initialise MWMechanics::AiSequence from ESM::AIPackageList Feature #622: Multiple positions for inventory window Feature #627: Drowning Feature #786: Allow the 'Activate' key to close the countdialog window Feature #798: Morrowind installation via Launcher (Linux/Max OS only) Feature #851: First/Third person transitions with mouse wheel Task #689: change PhysicActor::enableCollisions Task #707: Reorganise Compiler 0.25.0 ------ Bug #411: Launcher crash on OS X < 10.8 Bug #604: Terrible performance drop in the Census and Excise Office. Bug #676: Start Scripts fail to load Bug #677: OpenMW does not accept script names with - Bug #766: Extra space in front of topic links Bug #793: AIWander Isn't Being Passed The Repeat Parameter Bug #795: Sound playing with drawn weapon and crossing cell-border Bug #800: can't select weapon for enchantment Bug #801: Player can move while over-encumbered Bug #802: Dead Keys not working Bug #808: mouse capture Bug #809: ini Importer does not work without an existing cfg file Bug #812: Launcher will run OpenMW with no ESM or ESP selected Bug #813: OpenMW defaults to Morrowind.ESM with no ESM or ESP selected Bug #817: Dead NPCs and Creatures still have collision boxes Bug #820: Incorrect sorting of answers (Dialogue) Bug #826: mwinimport dumps core when given an unknown parameter Bug #833: getting stuck in door Bug #835: Journals/books not showing up properly. Feature #38: SoundGen Feature #105: AI Package: Wander Feature #230: 64-bit compatibility for OS X Feature #263: Hardware mouse cursors Feature #449: Allow mouse outside of window while paused Feature #736: First person animations Feature #750: Using mouse wheel in third person mode Feature #822: Autorepeat for slider buttons 0.24.0 ------ Bug #284: Book's text misalignment Bug #445: Camera able to get slightly below floor / terrain Bug #582: Seam issue in Red Mountain Bug #632: Journal Next Button shows white square Bug #653: IndexedStore ignores index Bug #694: Parser does not recognize float values starting with . Bug #699: Resource handling broken with Ogre 1.9 trunk Bug #718: components/esm/loadcell is using the mwworld subsystem Bug #729: Levelled item list tries to add nonexistent item Bug #730: Arrow buttons in the settings menu do not work. Bug #732: Erroneous behavior when binding keys Bug #733: Unclickable dialogue topic Bug #734: Book empty line problem Bug #738: OnDeath only works with implicit references Bug #740: Script compiler fails on scripts with special names Bug #742: Wait while no clipping Bug #743: Problem with changeweather console command Bug #744: No wait dialogue after starting a new game Bug #748: Player is not able to unselect objects with the console Bug #751: AddItem should only spawn a message box when called from dialogue Bug #752: The enter button has several functions in trade and looting that is not impelemted. Bug #753: Fargoth's Ring Quest Strange Behavior Bug #755: Launcher writes duplicate lines into settings.cfg Bug #759: Second quest in mages guild does not work Bug #763: Enchantment cast cost is wrong Bug #770: The "Take" and "Close" buttons in the scroll GUI are stretched incorrectly Bug #773: AIWander Isn't Being Passed The Correct idle Values Bug #778: The journal can be opened at the start of a new game Bug #779: Divayth Fyr starts as dead Bug #787: "Batch count" on detailed FPS counter gets cut-off Bug #788: chargen scroll layout does not match vanilla Feature #60: Atlethics Skill Feature #65: Security Skill Feature #74: Interaction with non-load-doors Feature #98: Render Weapon and Shield Feature #102: AI Package: Escort, EscortCell Feature #182: Advanced Journal GUI Feature #288: Trading enhancements Feature #405: Integrate "new game" into the menu Feature #537: Highlight dialogue topic links Feature #658: Rotate, RotateWorld script instructions and local rotations Feature #690: Animation Layering Feature #722: Night Eye/Blind magic effects Feature #735: Move, MoveWorld script instructions. Feature #760: Non-removable corpses 0.23.0 ------ Bug #522: Player collides with placeable items Bug #553: Open/Close sounds played when accessing main menu w/ Journal Open Bug #561: Tooltip word wrapping delay Bug #578: Bribing works incorrectly Bug #601: PositionCell fails on negative coordinates Bug #606: Some NPCs hairs not rendered with Better Heads addon Bug #609: Bad rendering of bone boots Bug #613: Messagebox causing assert to fail Bug #631: Segfault on shutdown Bug #634: Exception when talking to Calvus Horatius in Mournhold, royal palace courtyard Bug #635: Scale NPCs depending on race Bug #643: Dialogue Race select function is inverted Bug #646: Twohanded weapons don't work properly Bug #654: Crash when dropping objects without a collision shape Bug #655/656: Objects that were disabled or deleted (but not both) were added to the scene when re-entering a cell Bug #660: "g" in "change" cut off in Race Menu Bug #661: Arrille sells me the key to his upstairs room Bug #662: Day counter starts at 2 instead of 1 Bug #663: Cannot select "come unprepared" topic in dialog with Dagoth Ur Bug #665: Pickpocket -> "Grab all" grabs all NPC inventory, even not listed in container window. Bug #666: Looking up/down problem Bug #667: Active effects border visible during loading Bug #669: incorrect player position at new game start Bug #670: race selection menu: sex, face and hair left button not totally clickable Bug #671: new game: player is naked Bug #674: buying or selling items doesn't change amount of gold Bug #675: fatigue is not set to its maximum when starting a new game Bug #678: Wrong rotation order causes RefData's rotation to be stored incorrectly Bug #680: different gold coins in Tel Mara Bug #682: Race menu ignores playable flag for some hairs and faces Bug #685: Script compiler does not accept ":" after a function name Bug #688: dispose corpse makes cross-hair to disappear Bug #691: Auto equipping ignores equipment conditions Bug #692: OpenMW doesnt load "loose file" texture packs that places resources directly in data folder Bug #696: Draugr incorrect head offset Bug #697: Sail transparency issue Bug #700: "On the rocks" mod does not load its UV coordinates correctly. Bug #702: Some race mods don't work Bug #711: Crash during character creation Bug #715: Growing Tauryon Bug #725: Auto calculate stats Bug #728: Failure to open container and talk dialogue Bug #731: Crash with Mush-Mere's "background" topic Feature #55/657: Item Repairing Feature #62/87: Enchanting Feature #99: Pathfinding Feature #104: AI Package: Travel Feature #129: Levelled items Feature #204: Texture animations Feature #239: Fallback-Settings Feature #535: Console object selection improvements Feature #629: Add levelup description in levelup layout dialog Feature #630: Optional format subrecord in (tes3) header Feature #641: Armor rating Feature #645: OnDeath script function Feature #683: Companion item UI Feature #698: Basic Particles Task #648: Split up components/esm/loadlocks Task #695: mwgui cleanup 0.22.0 ------ Bug #311: Potential infinite recursion in script compiler Bug #355: Keyboard repeat rate (in Xorg) are left disabled after game exit. Bug #382: Weird effect in 3rd person on water Bug #387: Always use detailed shape for physics raycasts Bug #420: Potion/ingredient effects do not stack Bug #429: Parts of dwemer door not picked up correctly for activation/tooltips Bug #434/Bug #605: Object movement between cells not properly implemented Bug #502: Duplicate player collision model at origin Bug #509: Dialogue topic list shifts inappropriately Bug #513: Sliding stairs Bug #515: Launcher does not support non-latin strings Bug #525: Race selection preview camera wrong position Bug #526: Attributes / skills should not go below zero Bug #529: Class and Birthsign menus options should be preselected Bug #530: Lock window button graphic missing Bug #532: Missing map menu graphics Bug #545: ESX selector does not list ESM files properly Bug #547: Global variables of type short are read incorrectly Bug #550: Invisible meshes collision and tooltip Bug #551: Performance drop when loading multiple ESM files Bug #552: Don't list CG in options if it is not available Bug #555: Character creation windows "OK" button broken Bug #558: Segmentation fault when Alt-tabbing with console opened Bug #559: Dialog window should not be available before character creation is finished Bug #560: Tooltip borders should be stretched Bug #562: Sound should not be played when an object cannot be picked up Bug #565: Water animation speed + timescale Bug #572: Better Bodies' textures don't work Bug #573: OpenMW doesn't load if TR_Mainland.esm is enabled (Tamriel Rebuilt mod) Bug #574: Moving left/right should not cancel auto-run Bug #575: Crash entering the Chamber of Song Bug #576: Missing includes Bug #577: Left Gloves Addon causes ESMReader exception Bug #579: Unable to open container "Kvama Egg Sack" Bug #581: Mimicking vanilla Morrowind water Bug #583: Gender not recognized Bug #586: Wrong char gen behaviour Bug #587: "End" script statements with spaces don't work Bug #589: Closing message boxes by pressing the activation key Bug #590: Ugly Dagoth Ur rendering Bug #591: Race selection issues Bug #593: Persuasion response should be random Bug #595: Footless guard Bug #599: Waterfalls are invisible from a certain distance Bug #600: Waterfalls rendered incorrectly, cut off by water Bug #607: New beast bodies mod crashes Bug #608: Crash in cell "Mournhold, Royal Palace" Bug #611: OpenMW doesn't find some of textures used in Tamriel Rebuilt Bug #613: Messagebox causing assert to fail Bug #615: Meshes invisible from above water Bug #617: Potion effects should be hidden until discovered Bug #619: certain moss hanging from tree has rendering bug Bug #621: Batching bloodmoon's trees Bug #623: NiMaterialProperty alpha unhandled Bug #628: Launcher in latest master crashes the game Bug #633: Crash on startup: Better Heads Bug #636: Incorrect Char Gen Menu Behavior Feature #29: Allow ESPs and multiple ESMs Feature #94: Finish class selection-dialogue Feature #149: Texture Alphas Feature #237: Run Morrowind-ini importer from launcher Feature #286: Update Active Spell Icons Feature #334: Swimming animation Feature #335: Walking animation Feature #360: Proper collision shapes for NPCs and creatures Feature #367: Lights that behave more like original morrowind implementation Feature #477: Special local scripting variables Feature #528: Message boxes should close when enter is pressed under certain conditions. Feature #543: Add bsa files to the settings imported by the ini importer Feature #594: coordinate space and utility functions Feature #625: Zoom in vanity mode Task #464: Refactor launcher ESX selector into a re-usable component Task #624: Unified implementation of type-variable sub-records 0.21.0 ------ Bug #253: Dialogs don't work for Russian version of Morrowind Bug #267: Activating creatures without dialogue can still activate the dialogue GUI Bug #354: True flickering lights Bug #386: The main menu's first entry is wrong (in french) Bug #479: Adding the spell "Ash Woe Blight" to the player causes strange attribute oscillations Bug #495: Activation Range Bug #497: Failed Disposition check doesn't stop a dialogue entry from being returned Bug #498: Failing a disposition check shouldn't eliminate topics from the the list of those available Bug #500: Disposition for most NPCs is 0/100 Bug #501: Getdisposition command wrongly returns base disposition Bug #506: Journal UI doesn't update anymore Bug #507: EnableRestMenu is not a valid command - change it to EnableRest Bug #508: Crash in Ald Daedroth Shrine Bug #517: Wrong price calculation when untrading an item Bug #521: MWGui::InventoryWindow creates a duplicate player actor at the origin Bug #524: Beast races are able to wear shoes Bug #527: Background music fails to play Bug #533: The arch at Gnisis entrance is not displayed Bug #534: Terrain gets its correct shape only some time after the cell is loaded Bug #536: The same entry can be added multiple times to the journal Bug #539: Race selection is broken Bug #544: Terrain normal map corrupt when the map is rendered Feature #39: Video Playback Feature #151: ^-escape sequences in text output Feature #392: Add AI related script functions Feature #456: Determine required ini fallback values and adjust the ini importer accordingly Feature #460: Experimental DirArchives improvements Feature #540: Execute scripts of objects in containers/inventories in active cells Task #401: Review GMST fixing Task #453: Unify case smashing/folding Task #512: Rewrite utf8 component 0.20.0 ------ Bug #366: Changing the player's race during character creation does not change the look of the player character Bug #430: Teleporting and using loading doors linking within the same cell reloads the cell Bug #437: Stop animations when paused Bug #438: Time displays as "0 a.m." when it should be "12 a.m." Bug #439: Text in "name" field of potion/spell creation window is persistent Bug #440: Starting date at a new game is off by one day Bug #442: Console window doesn't close properly sometimes Bug #448: Do not break container window formatting when item names are very long Bug #458: Topics sometimes not automatically added to known topic list Bug #476: Auto-Moving allows player movement after using DisablePlayerControls Bug #478: After sleeping in a bed the rest dialogue window opens automtically again Bug #492: On creating potions the ingredients are removed twice Feature #63: Mercantile skill Feature #82: Persuasion Dialogue Feature #219: Missing dialogue filters/functions Feature #369: Add a FailedAction Feature #377: Select head/hair on character creation Feature #391: Dummy AI package classes Feature #435: Global Map, 2nd Layer Feature #450: Persuasion Feature #457: Add more script instructions Feature #474: update the global variable pcrace when the player's race is changed Task #158: Move dynamically generated classes from Player class to World Class Task #159: ESMStore rework and cleanup Task #163: More Component Namespace Cleanup Task #402: Move player data from MWWorld::Player to the player's NPC record Task #446: Fix no namespace in BulletShapeLoader 0.19.0 ------ Bug #374: Character shakes in 3rd person mode near the origin Bug #404: Gamma correct rendering Bug #407: Shoes of St. Rilm do not work Bug #408: Rugs has collision even if they are not supposed to Bug #412: Birthsign menu sorted incorrectly Bug #413: Resolutions presented multiple times in launcher Bug #414: launcher.cfg file stored in wrong directory Bug #415: Wrong esm order in openmw.cfg Bug #418: Sound listener position updates incorrectly Bug #423: wrong usage of "Version" entry in openmw.desktop Bug #426: Do not use hardcoded splash images Bug #431: Don't use markers for raycast Bug #432: Crash after picking up items from an NPC Feature #21/#95: Sleeping/resting Feature #61: Alchemy Skill Feature #68: Death Feature #69/#86: Spell Creation Feature #72/#84: Travel Feature #76: Global Map, 1st Layer Feature #120: Trainer Window Feature #152: Skill Increase from Skill Books Feature #160: Record Saving Task #400: Review GMST access 0.18.0 ------ Bug #310: Button of the "preferences menu" are too small Bug #361: Hand-to-hand skill is always 100 Bug #365: NPC and creature animation is jerky; Characters float around when they are not supposed to Bug #372: playSound3D uses original coordinates instead of current coordinates. Bug #373: Static OGRE build faulty Bug #375: Alt-tab toggle view Bug #376: Screenshots are disable Bug #378: Exception when drinking self-made potions Bug #380: Cloth visibility problem Bug #384: Weird character on doors tooltip. Bug #398: Some objects do not collide in MW, but do so in OpenMW Feature #22: Implement level-up Feature #36: Hide Marker Feature #88: Hotkey Window Feature #91: Level-Up Dialogue Feature #118: Keyboard and Mouse-Button bindings Feature #119: Spell Buying Window Feature #133: Handle resources across multiple data directories Feature #134: Generate a suitable default-value for --data-local Feature #292: Object Movement/Creation Script Instructions Feature #340: AIPackage data structures Feature #356: Ingredients use Feature #358: Input system rewrite Feature #370: Target handling in actions Feature #379: Door markers on the local map Feature #389: AI framework Feature #395: Using keys to open doors / containers Feature #396: Loading screens Feature #397: Inventory avatar image and race selection head preview Task #339: Move sounds into Action 0.17.0 ------ Bug #225: Valgrind reports about 40MB of leaked memory Bug #241: Some physics meshes still don't match Bug #248: Some textures are too dark Bug #300: Dependency on proprietary CG toolkit Bug #302: Some objects don't collide although they should Bug #308: Freeze in Balmora, Meldor: Armorer Bug #313: openmw without a ~/.config/openmw folder segfault. Bug #317: adding non-existing spell via console locks game Bug #318: Wrong character normals Bug #341: Building with Ogre Debug libraries does not use debug version of plugins Bug #347: Crash when running openmw with --start="XYZ" Bug #353: FindMyGUI.cmake breaks path on Windows Bug #359: WindowManager throws exception at destruction Bug #364: Laggy input on OS X due to bug in Ogre's event pump implementation Feature #33: Allow objects to cross cell-borders Feature #59: Dropping Items (replaced stopgap implementation with a proper one) Feature #93: Main Menu Feature #96/329/330/331/332/333: Player Control Feature #180: Object rotation and scaling. Feature #272: Incorrect NIF material sharing Feature #314: Potion usage Feature #324: Skill Gain Feature #342: Drain/fortify dynamic stats/attributes magic effects Feature #350: Allow console only script instructions Feature #352: Run scripts in console on startup Task #107: Refactor mw*-subsystems Task #325: Make CreatureStats into a class Task #345: Use Ogre's animation system Task #351: Rewrite Action class to support automatic sound playing 0.16.0 ------ Bug #250: OpenMW launcher erratic behaviour Bug #270: Crash because of underwater effect on OS X Bug #277: Auto-equipping in some cells not working Bug #294: Container GUI ignores disabled inventory menu Bug #297: Stats review dialog shows all skills and attribute values as 0 Bug #298: MechanicsManager::buildPlayer does not remove previous bonuses Bug #299: Crash in World::disable Bug #306: Non-existent ~/.config/openmw "crash" the launcher. Bug #307: False "Data Files" location make the launcher "crash" Feature #81: Spell Window Feature #85: Alchemy Window Feature #181: Support for x.y script syntax Feature #242: Weapon and Spell icons Feature #254: Ingame settings window Feature #293: Allow "stacking" game modes Feature #295: Class creation dialog tooltips Feature #296: Clicking on the HUD elements should show/hide the respective window Feature #301: Direction after using a Teleport Door Feature #303: Allow object selection in the console Feature #305: Allow the use of = as a synonym for == Feature #312: Compensation for slow object access in poorly written Morrowind.esm scripts Task #176: Restructure enabling/disabling of MW-references Task #283: Integrate ogre.cfg file in settings file Task #290: Auto-Close MW-reference related GUI windows 0.15.0 ------ Bug #5: Physics reimplementation (fixes various issues) Bug #258: Resizing arrow's background is not transparent Bug #268: Widening the stats window in X direction causes layout problems Bug #269: Topic pane in dialgoue window is too small for some longer topics Bug #271: Dialog choices are sorted incorrectly Bug #281: The single quote character is not rendered on dialog windows Bug #285: Terrain not handled properly in cells that are not predefined Bug #289: Dialogue filter isn't doing case smashing/folding for item IDs Feature #15: Collision with Terrain Feature #17: Inventory-, Container- and Trade-Windows Feature #44: Floating Labels above Focussed Objects Feature #80: Tooltips Feature #83: Barter Dialogue Feature #90: Book and Scroll Windows Feature #156: Item Stacking in Containers Feature #213: Pulsating lights Feature #218: Feather & Burden Feature #256: Implement magic effect bookkeeping Feature #259: Add missing information to Stats window Feature #260: Correct case for dialogue topics Feature #280: GUI texture atlasing Feature #291: Ability to use GMST strings from GUI layout files Task #255: Make MWWorld::Environment into a singleton 0.14.0 ------ Bug #1: Meshes rendered with wrong orientation Bug #6/Task #220: Picking up small objects doesn't always work Bug #127: tcg doesn't work Bug #178: Compablity problems with Ogre 1.8.0 RC 1 Bug #211: Wireframe mode (toggleWireframe command) should not apply to Console & other UI Bug #227: Terrain crashes when moving away from predefined cells Bug #229: On OS X Launcher cannot launch game if path to binary contains spaces Bug #235: TGA texture loading problem Bug #246: wireframe mode does not work in water Feature #8/#232: Water Rendering Feature #13: Terrain Rendering Feature #37: Render Path Grid Feature #66: Factions Feature #77: Local Map Feature #78: Compass/Mini-Map Feature #97: Render Clothing/Armour Feature #121: Window Pinning Feature #205: Auto equip Feature #217: Contiainer should track changes to its content Feature #221: NPC Dialogue Window Enhancements Feature #233: Game settings manager Feature #240: Spell List and selected spell (no GUI yet) Feature #243: Draw State Task #113: Morrowind.ini Importer Task #215: Refactor the sound code Task #216: Update MyGUI 0.13.0 ------ Bug #145: Fixed sound problems after cell change Bug #179: Pressing space in console triggers activation Bug #186: CMake doesn't use the debug versions of Ogre libraries on Linux Bug #189: ASCII 16 character added to console on it's activation on Mac OS X Bug #190: Case Folding fails with music files Bug #192: Keypresses write Text into Console no matter which gui element is active Bug #196: Collision shapes out of place Bug #202: ESMTool doesn't not work with localised ESM files anymore Bug #203: Torch lights only visible on short distance Bug #207: Ogre.log not written Bug #209: Sounds do not play Bug #210: Ogre crash at Dren plantation Bug #214: Unsupported file format version Bug #222: Launcher is writing openmw.cfg file to wrong location Feature #9: NPC Dialogue Window Feature #16/42: New sky/weather implementation Feature #40: Fading Feature #48: NPC Dialogue System Feature #117: Equipping Items (backend only, no GUI yet, no rendering of equipped items yet) Feature #161: Load REC_PGRD records Feature #195: Wireframe-mode Feature #198/199: Various sound effects Feature #206: Allow picking data path from launcher if non is set Task #108: Refactor window manager class Task #172: Sound Manager Cleanup Task #173: Create OpenEngine systems in the appropriate manager classes Task #184: Adjust MSVC and gcc warning levels Task #185: RefData rewrite Task #201: Workaround for transparency issues Task #208: silenced esm_reader.hpp warning 0.12.0 ------ Bug #154: FPS Drop Bug #169: Local scripts continue running if associated object is deleted Bug #174: OpenMW fails to start if the config directory doesn't exist Bug #187: Missing lighting Bug #188: Lights without a mesh are not rendered Bug #191: Taking screenshot causes crash when running installed Feature #28: Sort out the cell load problem Feature #31: Allow the player to move away from pre-defined cells Feature #35: Use alternate storage location for modified object position Feature #45: NPC animations Feature #46: Creature Animation Feature #89: Basic Journal Window Feature #110: Automatically pick up the path of existing MW-installations Feature #183: More FPS display settings Task #19: Refactor engine class Task #109/Feature #162: Automate Packaging Task #112: Catch exceptions thrown in input handling functions Task #128/#168: Cleanup Configuration File Handling Task #131: NPC Activation doesn't work properly Task #144: MWRender cleanup Task #155: cmake cleanup 0.11.1 ------ Bug #2: Resources loading doesn't work outside of bsa files Bug #3: GUI does not render non-English characters Bug #7: openmw.cfg location doesn't match Bug #124: The TCL alias for ToggleCollision is missing. Bug #125: Some command line options can't be used from a .cfg file Bug #126: Toggle-type script instructions are less verbose compared with original MW Bug #130: NPC-Record Loading fails for some NPCs Bug #167: Launcher sets invalid parameters in ogre config Feature #10: Journal Feature #12: Rendering Optimisations Feature #23: Change Launcher GUI to a tabbed interface Feature #24: Integrate the OGRE settings window into the launcher Feature #25: Determine openmw.cfg location (Launcher) Feature #26: Launcher Profiles Feature #79: MessageBox Feature #116: Tab-Completion in Console Feature #132: --data-local and multiple --data Feature #143: Non-Rendering Performance-Optimisations Feature #150: Accessing objects in cells via ID does only work for objects with all lower case IDs Feature #157: Version Handling Task #14: Replace tabs with 4 spaces Task #18: Move components from global namespace into their own namespace Task #123: refactor header files in components/esm 0.10.0 ------ * NPC dialogue window (not functional yet) * Collisions with objects * Refactor the PlayerPos class * Adjust file locations * CMake files and test linking for Bullet * Replace Ogre raycasting test for activation with something more precise * Adjust player movement according to collision results * FPS display * Various Portability Improvements * Mac OS X support is back! 0.9.0 ----- * Exterior cells loading, unloading and management * Character Creation GUI * Character creation * Make cell names case insensitive when doing internal lookups * Music player * NPCs rendering 0.8.0 ----- * GUI * Complete and working script engine * In game console * Sky rendering * Sound and music * Tons of smaller stuff 0.7.0 ----- * This release is a complete rewrite in C++. * All D code has been culled, and all modules have been rewritten. * The game is now back up to the level of rendering interior cells and moving around, but physics, sound, GUI, and scripting still remain to be ported from the old codebase. 0.6.0 ----- * Coded a GUI system using MyGUI * Skinned MyGUI to look like Morrowind (work in progress) * Integrated the Monster script engine * Rewrote some functions into script code * Very early MyGUI < > Monster binding * Fixed Windows sound problems (replaced old openal32.dll) 0.5.0 ----- * Collision detection with Bullet * Experimental walk & fall character physics * New key bindings: * t toggle physics mode (walking, flying, ghost), * n night eye, brightens the scene * Fixed incompatability with DMD 1.032 and newer compilers * * (thanks to tomqyp) * Various minor changes and updates 0.4.0 ----- * Switched from Audiere to OpenAL * * (BIG thanks to Chris Robinson) * Added complete Makefile (again) as a alternative build tool * More realistic lighting (thanks again to Chris Robinson) * Various localization fixes tested with Russian and French versions * Temporary workaround for the Unicode issue: invalid UTF displayed as '?' * Added ns option to disable sound, for debugging * Various bug fixes * Cosmetic changes to placate gdc Wall 0.3.0 ----- * Built and tested on Windows XP * Partial support for FreeBSD (exceptions do not work) * You no longer have to download Monster separately * Made an alternative for building without DSSS (but DSSS still works) * Renamed main program from 'morro' to 'openmw' * Made the config system more robust * Added oc switch for showing Ogre config window on startup * Removed some config files, these are auto generated when missing. * Separated plugins.cfg into linux and windows versions. * Updated Makefile and sources for increased portability * confirmed to work against OIS 1.0.0 (Ubuntu repository package) 0.2.0 ----- * Compiles with gdc * Switched to DSSS for building D code * Includes the program esmtool 0.1.0 ----- first release ================================================ FILE: CHANGELOG_PR.md ================================================ *** PLEASE PUT YOUR ISSUE DESCRIPTION FOR DUMMIES HERE FOR REVIEW *** - I'm just a placeholder description (#1337) - I'm also just a placeholder description, but I'm a more recent one (#42) *** 0.47.0 ------ The OpenMW team is proud to announce the release of version 0.47.0! Grab it from our Downloads Page for all operating systems. ***short summary: XXX *** Check out the release video (***add link***) and the OpenMW-CS release video (***add link***) by the ***add flattering adjective*** Atahualpa, and see below for the full list of changes. Known Issues: - To use generic Linux binaries, Qt4 and libpng12 must be installed on your system - On macOS, launching OpenMW from OpenMW-CS requires OpenMW.app and OpenMW-CS.app to be siblings New Features: - Dialogue to split item stacks now displays the name of the trapped soul for stacks of soul gems (#5362) - Basics of Collada animations are now supported via osgAnimation plugin (#5456) New Editor Features: - Instance selection modes are now implemented (centred cube, corner-dragged cube, sphere) with four user-configurable actions (select only, add to selection, remove from selection, invert selection) (#3171) Bug Fixes: - NiParticleColorModifier in NIF files is now properly handled which solves issues regarding particle effects, e.g., smoke and fire (#1952, #3676) - Targetting non-unique actors in scripts is now supported (#2311) - Guards no longer ignore attacks of invisible players but rather initiate dialogue and flee if the player resists being arrested (#4774) - Changing the dialogue window without closing it no longer clears the dialogue history in order to allow, e.g., emulation of three-way dialogue via ForceGreeting (#5358) - Scripts which try to start a non-existent global script now skip that step and continue execution instead of breaking (#5364) - Selecting already equipped spells or magic items via hotkey no longer triggers the equip sound to play (#5367) - 'Scale' argument in levelled creature lists is now taken into account when spawning creatures from such lists (#5369) - Morrowind legacy madness: Using a key on a trapped door/container now only disarms the trap if the door/container is locked (#5370) Editor Bug Fixes: - Deleted and moved objects within a cell are now saved properly (#832) - Disabled record sorting in Topic and Journal Info tables, implemented drag-move for records (#4357) - Topic and Journal Info records can now be cloned with a different parent Topic/Journal Id (#4363) - Verifier no longer checks for alleged 'race' entries in clothing body parts (#5400) - Cell borders are now properly redrawn when undoing/redoing terrain changes (#5473) - Loading mods now keeps the master index (#5675) - Flicker and crashing on XFCE4 fixed (#5703) - Collada models render properly in the Editor (#5713) - Terrain-selection grid is now properly updated when undoing/redoing terrain changes (#6022) - Tool outline and select/edit actions in "Terrain land editing" mode now ignore references (#6023) - Primary-select and secondary-select actions in "Terrain land editing" mode now behave like in "Instance editing" mode (#6024) - Using the circle brush to select terrain in the "Terrain land editing" mode no longer selects vertices outside the circle (#6035) - Vertices at the NW and SE corners of a cell can now also be selected in "Terrain land editing" mode if the adjacent cells aren't loaded yet (#6036) Miscellaneous: - Prevent save-game bloating by using an appropriate fog texture format (#5108) - Ensure that 'Enchantment autocalc" flag is treated as flag in OpenMW-CS and in our esm tools (#5363) ================================================ FILE: CI/ActivateMSVC.ps1 ================================================ & "${env:COMSPEC}" /c ActivateMSVC.bat "&&" set | ForEach-Object { if ($_.Contains("=")) { $name, $value = $_ -split '=', 2 Set-Content env:\"$name" $value } } $MissingTools = $false $tools = "cl", "link", "rc", "mt" $descriptions = "MSVC Compiler", "MSVC Linker", "MS Windows Resource Compiler", "MS Windows Manifest Tool" for ($i = 0; $i -lt $tools.Length; $i++) { $present = $true try { Get-Command $tools[$i] *>&1 | Out-Null $present = $present -and $? } catch { $present = $false } if (!$present) { Write-Warning "$($tools[$i]) ($($descriptions[$i])) missing." $MissingTools = $true } } if ($MissingTools) { Write-Error "Some build tools were unavailable after activating MSVC in the shell. It's likely that your Visual Studio $MSVC_DISPLAY_YEAR installation needs repairing." exit 1 } ================================================ FILE: CI/activate_msvc.sh ================================================ #!/bin/bash oldSettings=$- set -eu function restoreOldSettings { if [[ $oldSettings != *e* ]]; then set +e fi if [[ $oldSettings != *u* ]]; then set +u fi } if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then echo "Error: Script not sourced." echo "You must source this script for it to work, i.e. " echo "source ./activate_msvc.sh" echo "or" echo ". ./activate_msvc.sh" restoreOldSettings exit 1 fi command -v unixPathAsWindows >/dev/null 2>&1 || function unixPathAsWindows { if command -v cygpath >/dev/null 2>&1; then cygpath -w $1 else echo "$1" | sed "s,^/\([^/]\)/,\\1:/," | sed "s,/,\\\\,g" fi } # capture CMD environment in a shell with MSVC activated cmd //c "$(unixPathAsWindows "$(dirname "${BASH_SOURCE[0]}")")\ActivateMSVC.bat" "&&" "bash" "-c" "declare -px > declared_env.sh" source ./declared_env.sh rm declared_env.sh MISSINGTOOLS=0 command -v cl >/dev/null 2>&1 || { echo "Error: cl (MSVC Compiler) missing."; MISSINGTOOLS=1; } command -v link >/dev/null 2>&1 || { echo "Error: link (MSVC Linker) missing."; MISSINGTOOLS=1; } command -v rc >/dev/null 2>&1 || { echo "Error: rc (MS Windows Resource Compiler) missing."; MISSINGTOOLS=1; } command -v mt >/dev/null 2>&1 || { echo "Error: mt (MS Windows Manifest Tool) missing."; MISSINGTOOLS=1; } if [ $MISSINGTOOLS -ne 0 ]; then echo "Some build tools were unavailable after activating MSVC in the shell. It's likely that your Visual Studio $MSVC_DISPLAY_YEAR installation needs repairing." restoreOldSettings return 1 fi restoreOldSettings ================================================ FILE: CI/before_install.android.sh ================================================ #!/bin/sh -ex curl -fSL -R -J https://gitlab.com/OpenMW/openmw-deps/-/raw/main/android/openmw-android-deps-20201129.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 ================================================ FILE: CI/before_install.linux.sh ================================================ #!/bin/sh -ex echo -n | openssl s_client -connect scan.coverity.com:443 | sed -ne '/-BEGIN CERTIFICATE-/,/-END CERTIFICATE-/p' | sudo tee -a /etc/ssl/certs/ca- # Set up compilers if [ ! -z "${MATRIX_CC}" ]; then eval "${MATRIX_CC}" fi cd ~/ git clone https://github.com/TES3MP/CrabNet cd CrabNet cmake . -DCRABNET_ENABLE_DLL=OFF -DCRABNET_ENABLE_SAMPLES=OFF -DCMAKE_BUILD_TYPE=Release make -j3 ================================================ FILE: CI/before_install.osx.sh ================================================ #!/bin/sh -ex # workaround python issue on travis [ -z "${TRAVIS}" ] && HOMEBREW_NO_AUTO_UPDATE=1 brew uninstall --ignore-dependencies python@3.8 || true [ -z "${TRAVIS}" ] && HOMEBREW_NO_AUTO_UPDATE=1 brew uninstall --ignore-dependencies python@3.9 || true [ -z "${TRAVIS}" ] && HOMEBREW_NO_AUTO_UPDATE=1 brew uninstall --ignore-dependencies qt@6 || true # Some of these tools can come from places other than brew, so check before installing command -v ccache >/dev/null 2>&1 || brew install ccache command -v cmake >/dev/null 2>&1 || brew install cmake command -v qmake >/dev/null 2>&1 || brew install qt@5 export PATH="/usr/local/opt/qt@5/bin:$PATH" # needed to use qmake in none default path as qt now points to qt6 ccache --version cmake --version qmake --version curl -fSL -R -J https://gitlab.com/OpenMW/openmw-deps/-/raw/main/macos/openmw-deps-20210617.zip -o ~/openmw-deps.zip unzip -o ~/openmw-deps.zip -d /private/tmp/openmw-deps > /dev/null # additional libraries [ -z "${TRAVIS}" ] && HOMEBREW_NO_AUTO_UPDATE=1 brew install fontconfig ================================================ FILE: CI/before_script.android.sh ================================================ #!/bin/sh -ex # hack to work around: FFmpeg version is too old, 3.2 is required sed -i s/"NOT FFVER_OK"/"FALSE"/ CMakeLists.txt mkdir -p build cd build cmake \ -DCMAKE_TOOLCHAIN_FILE=/usr/lib/android-sdk/ndk-bundle/build/cmake/android.toolchain.cmake \ -DANDROID_ABI=arm64-v8a \ -DANDROID_PLATFORM=android-21 \ -DCMAKE_C_COMPILER_LAUNCHER=ccache \ -DCMAKE_CXX_COMPILER_LAUNCHER=ccache \ -DCMAKE_INSTALL_PREFIX=install \ -DBUILD_BSATOOL=0 \ -DBUILD_NIFTEST=0 \ -DBUILD_ESMTOOL=0 \ -DBUILD_LAUNCHER=0 \ -DBUILD_MWINIIMPORTER=0 \ -DBUILD_ESSIMPORTER=0 \ -DBUILD_OPENCS=0 \ -DBUILD_WIZARD=0 \ -DOPENMW_USE_SYSTEM_MYGUI=OFF \ -DOPENMW_USE_SYSTEM_OSG=OFF \ -DOPENMW_USE_SYSTEM_BULLET=OFF \ .. ================================================ FILE: CI/before_script.linux.sh ================================================ #!/bin/bash set -xeo pipefail free -m BUILD_UNITTESTS=OFF BUILD_BENCHMARKS=OFF if [[ "${BUILD_TESTS_ONLY}" ]]; then export GOOGLETEST_DIR="${PWD}/googletest/build/install" env GENERATOR='Unix Makefiles' CONFIGURATION=Release CI/build_googletest.sh BUILD_UNITTESTS=ON BUILD_BENCHMARKS=ON fi declare -a CMAKE_CONF_OPTS=( -DCMAKE_C_COMPILER="${CC:-/usr/bin/cc}" -DCMAKE_CXX_COMPILER="${CXX:-/usr/bin/c++}" -DCMAKE_C_COMPILER_LAUNCHER=ccache -DCMAKE_CXX_COMPILER_LAUNCHER=ccache -DCMAKE_INSTALL_PREFIX=install -DCMAKE_BUILD_TYPE=RelWithDebInfo -DBUILD_SHARED_LIBS=OFF -DUSE_SYSTEM_TINYXML=ON -DCMAKE_INSTALL_PREFIX=install -DRakNet_LIBRARY_RELEASE=~/CrabNet/lib/libRakNetLibStatic.a -DRakNet_LIBRARY_DEBUG=~/CrabNet/lib/libRakNetLibStatic.a ) if [[ $CI_OPENMW_USE_STATIC_DEPS ]]; then CMAKE_CONF_OPTS+=( -DOPENMW_USE_SYSTEM_MYGUI=OFF -DOPENMW_USE_SYSTEM_OSG=OFF -DOPENMW_USE_SYSTEM_BULLET=OFF ) fi mkdir -p build cd build # Set up compilers if [ ! -z "${MATRIX_CC}" ]; then eval "${MATRIX_CC}" fi export RAKNET_ROOT=~/CrabNet if [[ "${BUILD_TESTS_ONLY}" ]]; then ${ANALYZE} cmake \ "${CMAKE_CONF_OPTS[@]}" \ -DBUILD_OPENMW=OFF \ -DBUILD_OPENMW_MP=OFF \ -DBUILD_BSATOOL=OFF \ -DBUILD_ESMTOOL=OFF \ -DBUILD_LAUNCHER=OFF \ -DBUILD_BROWSER=OFF \ -DBUILD_MWINIIMPORTER=OFF \ -DBUILD_ESSIMPORTER=OFF \ -DBUILD_OPENCS=OFF \ -DBUILD_WIZARD=OFF \ -DBUILD_UNITTESTS=${BUILD_UNITTESTS} \ -DBUILD_BENCHMARKS=${BUILD_BENCHMARKS} \ -DGTEST_ROOT="${GOOGLETEST_DIR}" \ -DGMOCK_ROOT="${GOOGLETEST_DIR}" \ .. else ${ANALYZE} cmake \ "${CMAKE_CONF_OPTS[@]}" \ .. fi ================================================ FILE: CI/before_script.msvc.sh ================================================ #!/bin/bash # set -x # turn-on for debugging function wrappedExit { if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then exit $1 else return $1 fi } MISSINGTOOLS=0 command -v 7z >/dev/null 2>&1 || { echo "Error: 7z (7zip) is not on the path."; MISSINGTOOLS=1; } command -v cmake >/dev/null 2>&1 || { echo "Error: cmake (CMake) is not on the path."; MISSINGTOOLS=1; } MISSINGPYTHON=0 if ! command -v python >/dev/null 2>&1; then echo "Warning: Python is not on the path, automatic Qt installation impossible." MISSINGPYTHON=1 elif ! python --version >/dev/null 2>&1; then echo "Warning: Python is (probably) fake stub Python that comes bundled with newer versions of Windows, automatic Qt installation impossible." echo "If you think you have Python installed, try changing the order of your PATH environment variable in Advanced System Settings." MISSINGPYTHON=1 fi if [ $MISSINGTOOLS -ne 0 ]; then wrappedExit 1 fi WORKINGDIR="$(pwd)" case "$WORKINGDIR" in *[[:space:]]*) echo "Error: Working directory contains spaces." wrappedExit 1 ;; esac set -euo pipefail function windowsPathAsUnix { if command -v cygpath >/dev/null 2>&1; then cygpath -u $1 else echo "$1" | sed "s,\\\\,/,g" | sed "s,\(.\):,/\\1," fi } function unixPathAsWindows { if command -v cygpath >/dev/null 2>&1; then cygpath -w $1 else echo "$1" | sed "s,^/\([^/]\)/,\\1:/," | sed "s,/,\\\\,g" fi } APPVEYOR=${APPVEYOR:-} CI=${CI:-} STEP=${STEP:-} VERBOSE="" STRIP="" SKIP_DOWNLOAD="" SKIP_EXTRACT="" KEEP="" UNITY_BUILD="" VS_VERSION="" NMAKE="" NINJA="" PDBS="" PLATFORM="" CONFIGURATIONS=() TEST_FRAMEWORK="" GOOGLE_INSTALL_ROOT="" INSTALL_PREFIX="." BUILD_BENCHMARKS="" ACTIVATE_MSVC="" SINGLE_CONFIG="" while [ $# -gt 0 ]; do ARGSTR=$1 shift if [ ${ARGSTR:0:1} != "-" ]; then echo "Unknown argument $ARGSTR" echo "Try '$0 -h'" wrappedExit 1 fi for (( i=1; i<${#ARGSTR}; i++ )); do ARG=${ARGSTR:$i:1} case $ARG in V ) VERBOSE=true ;; d ) SKIP_DOWNLOAD=true ;; e ) SKIP_EXTRACT=true ;; k ) KEEP=true ;; u ) UNITY_BUILD=true ;; v ) VS_VERSION=$1 shift ;; n ) NMAKE=true ;; N ) NINJA=true ;; p ) PLATFORM=$1 shift ;; P ) PDBS=true ;; c ) CONFIGURATIONS+=( $1 ) shift ;; t ) TEST_FRAMEWORK=true ;; i ) INSTALL_PREFIX=$(echo "$1" | sed 's;\\;/;g' | sed -E 's;/+;/;g') shift ;; b ) BUILD_BENCHMARKS=true ;; h ) cat < Set the configuration, can also be set with environment variable CONFIGURATION. For mutli-config generators, this is ignored, and all configurations are set up. For single-config generators, several configurations can be set up at once by specifying -c multiple times. -d Skip checking the downloads. -e Skip extracting dependencies. -h Show this message. -k Keep the old build directory, default is to delete it. -p Set the build platform, can also be set with environment variable PLATFORM. -t Build unit tests / Google test -u Configure for unity builds. -v <2017/2019> Choose the Visual Studio version to use. -n Produce NMake makefiles instead of a Visual Studio solution. Cannot be used with -N. -N Produce Ninja (multi-config if CMake is new enough to support it) files instead of a Visual Studio solution. Cannot be used with -n.. -P Download debug symbols where available -V Run verbosely -i CMake install prefix -b Build benchmarks EOF wrappedExit 0 ;; * ) echo "Unknown argument $ARG." echo "Try '$0 -h'" wrappedExit 1 ;; esac done done if [ -n "$NMAKE" ] || [ -n "$NINJA" ]; then if [ -n "$NMAKE" ] && [ -n "$NINJA" ]; then echo "Cannot run in NMake and Ninja mode at the same time." wrappedExit 1 fi ACTIVATE_MSVC=true fi if [ -z $VERBOSE ]; then STRIP="> /dev/null 2>&1" fi if [ -z $APPVEYOR ]; then echo "Running prebuild outside of Appveyor." DIR=$(windowsPathAsUnix "${BASH_SOURCE[0]}") cd $(dirname "$DIR")/.. else echo "Running prebuild in Appveyor." cd "$APPVEYOR_BUILD_FOLDER" fi run_cmd() { CMD="$1" shift if [ -z $VERBOSE ]; then RET=0 eval $CMD $@ > output.log 2>&1 || RET=$? if [ $RET -ne 0 ]; then if [ -z $APPVEYOR ]; then echo "Command $CMD failed, output can be found in $(real_pwd)/output.log" else echo echo "Command $CMD failed;" cat output.log fi else rm output.log fi return $RET else RET=0 eval $CMD $@ || RET=$? return $RET fi } download() { if [ $# -lt 3 ]; then echo "Invalid parameters to download." return 1 fi NAME=$1 shift echo "$NAME..." while [ $# -gt 1 ]; do URL=$1 FILE=$2 shift shift if ! [ -f $FILE ]; then printf " Downloading $FILE... " if [ -z $VERBOSE ]; then RET=0 curl --silent --retry 10 -Ly 5 -o $FILE $URL || RET=$? else RET=0 curl --retry 10 -Ly 5 -o $FILE $URL || RET=$? fi if [ $RET -ne 0 ]; then echo "Failed!" wrappedExit $RET else echo "Done." fi else echo " $FILE exists, skipping." fi done if [ $# -ne 0 ]; then echo "Missing parameter." fi } real_pwd() { if type cygpath >/dev/null 2>&1; then cygpath -am "$PWD" else pwd # not git bash, Cygwin or the like fi } CMAKE_OPTS="" add_cmake_opts() { CMAKE_OPTS="$CMAKE_OPTS $@" } declare -A RUNTIME_DLLS RUNTIME_DLLS["Release"]="" RUNTIME_DLLS["Debug"]="" RUNTIME_DLLS["RelWithDebInfo"]="" add_runtime_dlls() { local CONFIG=$1 shift RUNTIME_DLLS[$CONFIG]="${RUNTIME_DLLS[$CONFIG]} $@" } declare -A OSG_PLUGINS OSG_PLUGINS["Release"]="" OSG_PLUGINS["Debug"]="" OSG_PLUGINS["RelWithDebInfo"]="" add_osg_dlls() { local CONFIG=$1 shift OSG_PLUGINS[$CONFIG]="${OSG_PLUGINS[$CONFIG]} $@" } declare -A QT_PLATFORMS QT_PLATFORMS["Release"]="" QT_PLATFORMS["Debug"]="" QT_PLATFORMS["RelWithDebInfo"]="" add_qt_platform_dlls() { local CONFIG=$1 shift QT_PLATFORMS[$CONFIG]="${QT_PLATFORMS[$CONFIG]} $@" } declare -A QT_STYLES QT_STYLES["Release"]="" QT_STYLES["Debug"]="" QT_STYLES["RelWithDebInfo"]="" add_qt_style_dlls() { local CONFIG=$1 shift QT_STYLES[$CONFIG]="${QT_STYLES[$CONFIG]} $@" } if [ -z $PLATFORM ]; then PLATFORM="$(uname -m)" fi if [ -z $VS_VERSION ]; then VS_VERSION="2017" fi case $VS_VERSION in 16|16.0|2019 ) GENERATOR="Visual Studio 16 2019" TOOLSET="vc142" MSVC_REAL_VER="16" MSVC_VER="14.2" MSVC_YEAR="2015" MSVC_REAL_YEAR="2019" MSVC_DISPLAY_YEAR="2019" BOOST_VER="1.71.0" BOOST_VER_URL="1_71_0" BOOST_VER_SDK="107100" ;; 15|15.0|2017 ) GENERATOR="Visual Studio 15 2017" TOOLSET="vc141" MSVC_REAL_VER="15" MSVC_VER="14.1" MSVC_YEAR="2015" MSVC_REAL_YEAR="2017" MSVC_DISPLAY_YEAR="2017" BOOST_VER="1.67.0" BOOST_VER_URL="1_67_0" BOOST_VER_SDK="106700" ;; 14|14.0|2015 ) echo "Visual Studio 2015 is no longer supported" wrappedExit 1 ;; 12|12.0|2013 ) echo "Visual Studio 2013 is no longer supported" wrappedExit 1 ;; esac case $PLATFORM in x64|x86_64|x86-64|win64|Win64 ) ARCHNAME="x86-64" ARCHSUFFIX="64" BITS="64" ;; x32|x86|i686|i386|win32|Win32 ) ARCHNAME="x86" ARCHSUFFIX="86" BITS="32" ;; * ) echo "Unknown platform $PLATFORM." wrappedExit 1 ;; esac if [ $BITS -eq 64 ] && [ $MSVC_REAL_VER -lt 16 ]; then GENERATOR="${GENERATOR} Win64" fi if [ -n "$NMAKE" ]; then GENERATOR="NMake Makefiles" SINGLE_CONFIG=true fi if [ -n "$NINJA" ]; then GENERATOR="Ninja Multi-Config" if ! cmake -E capabilities | grep -F "$GENERATOR" > /dev/null; then SINGLE_CONFIG=true GENERATOR="Ninja" fi fi if [ -n "$SINGLE_CONFIG" ]; then if [ ${#CONFIGURATIONS[@]} -eq 0 ]; then if [ -n "${CONFIGURATION:-}" ]; then CONFIGURATIONS=("$CONFIGURATION") else CONFIGURATIONS=("Debug") fi elif [ ${#CONFIGURATIONS[@]} -ne 1 ]; then # It's simplest just to recursively call the script a few times. RECURSIVE_OPTIONS=() if [ -n "$VERBOSE" ]; then RECURSIVE_OPTIONS+=("-V") fi if [ -n "$SKIP_DOWNLOAD" ]; then RECURSIVE_OPTIONS+=("-d") fi if [ -n "$SKIP_EXTRACT" ]; then RECURSIVE_OPTIONS+=("-e") fi if [ -n "$KEEP" ]; then RECURSIVE_OPTIONS+=("-k") fi if [ -n "$UNITY_BUILD" ]; then RECURSIVE_OPTIONS+=("-u") fi if [ -n "$NMAKE" ]; then RECURSIVE_OPTIONS+=("-n") fi if [ -n "$NINJA" ]; then RECURSIVE_OPTIONS+=("-N") fi if [ -n "$PDBS" ]; then RECURSIVE_OPTIONS+=("-P") fi if [ -n "$TEST_FRAMEWORK" ]; then RECURSIVE_OPTIONS+=("-t") fi RECURSIVE_OPTIONS+=("-v $VS_VERSION") RECURSIVE_OPTIONS+=("-p $PLATFORM") RECURSIVE_OPTIONS+=("-i '$INSTALL_PREFIX'") for config in ${CONFIGURATIONS[@]}; do $0 ${RECURSIVE_OPTIONS[@]} -c $config done wrappedExit 1 fi else if [ ${#CONFIGURATIONS[@]} -ne 0 ]; then echo "Ignoring configurations argument - generator is multi-config" fi CONFIGURATIONS=("Release" "Debug" "RelWithDebInfo") fi for i in ${!CONFIGURATIONS[@]}; do case ${CONFIGURATIONS[$i]} in debug|Debug|DEBUG ) CONFIGURATIONS[$i]=Debug ;; release|Release|RELEASE ) CONFIGURATIONS[$i]=Release ;; relwithdebinfo|RelWithDebInfo|RELWITHDEBINFO ) CONFIGURATIONS[$i]=RelWithDebInfo ;; esac done if [ $MSVC_REAL_VER -ge 16 ] && [ -z "$NMAKE" ] && [ -z "$NINJA" ]; then if [ $BITS -eq 64 ]; then add_cmake_opts "-G\"$GENERATOR\" -A x64" else add_cmake_opts "-G\"$GENERATOR\" -A Win32" fi else add_cmake_opts "-G\"$GENERATOR\"" fi if [ -n "$SINGLE_CONFIG" ]; then add_cmake_opts "-DCMAKE_BUILD_TYPE=${CONFIGURATIONS[0]}" fi if ! [ -z $UNITY_BUILD ]; then add_cmake_opts "-DOPENMW_UNITY_BUILD=True" fi echo echo "===================================" echo "Starting prebuild on MSVC${MSVC_DISPLAY_YEAR} WIN${BITS}" echo "===================================" echo # cd OpenMW/AppVeyor-test mkdir -p deps cd deps DEPS="$(pwd)" if [ -z $SKIP_DOWNLOAD ]; then echo "Downloading dependency packages." echo # Boost if [ -z $APPVEYOR ]; then download "Boost ${BOOST_VER}" \ "https://gitlab.com/OpenMW/openmw-deps/-/raw/main/windows/boost_${BOOST_VER_URL}-msvc-${MSVC_VER}-${BITS}.exe" \ "boost-${BOOST_VER}-msvc${MSVC_VER}-win${BITS}.exe" fi # Bullet download "Bullet 2.89" \ "https://gitlab.com/OpenMW/openmw-deps/-/raw/main/windows/Bullet-2.89-msvc${MSVC_YEAR}-win${BITS}-double.7z" \ "Bullet-2.89-msvc${MSVC_YEAR}-win${BITS}-double.7z" # FFmpeg download "FFmpeg 4.2.2" \ "https://gitlab.com/OpenMW/openmw-deps/-/raw/main/windows/ffmpeg-4.2.2-win${BITS}.zip" \ "ffmpeg-4.2.2-win${BITS}.zip" \ "https://gitlab.com/OpenMW/openmw-deps/-/raw/main/windows/ffmpeg-4.2.2-dev-win${BITS}.zip" \ "ffmpeg-4.2.2-dev-win${BITS}.zip" # MyGUI download "MyGUI 3.4.0" \ "https://gitlab.com/OpenMW/openmw-deps/-/raw/main/windows/MyGUI-3.4.0-msvc${MSVC_REAL_YEAR}-win${BITS}.7z" \ "MyGUI-3.4.0-msvc${MSVC_REAL_YEAR}-win${BITS}.7z" if [ -n "$PDBS" ]; then download "MyGUI symbols" \ "https://gitlab.com/OpenMW/openmw-deps/-/raw/main/windows/MyGUI-3.4.0-msvc${MSVC_REAL_YEAR}-win${BITS}-sym.7z" \ "MyGUI-3.4.0-msvc${MSVC_REAL_YEAR}-win${BITS}-sym.7z" fi # OpenAL download "OpenAL-Soft 1.20.1" \ "https://gitlab.com/OpenMW/openmw-deps/-/raw/main/windows/OpenAL-Soft-1.20.1.zip" \ "OpenAL-Soft-1.20.1.zip" # OSG download "OpenSceneGraph 3.6.5" \ "https://gitlab.com/OpenMW/openmw-deps/-/raw/main/windows/OSG-3.6.5-msvc${MSVC_REAL_YEAR}-win${BITS}.7z" \ "OSG-3.6.5-msvc${MSVC_REAL_YEAR}-win${BITS}.7z" if [ -n "$PDBS" ]; then download "OpenSceneGraph symbols" \ "https://gitlab.com/OpenMW/openmw-deps/-/raw/main/windows/OSG-3.6.5-msvc${MSVC_REAL_YEAR}-win${BITS}-sym.7z" \ "OSG-3.6.5-msvc${MSVC_REAL_YEAR}-win${BITS}-sym.7z" 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" # LZ4 download "LZ4 1.9.2" \ "https://gitlab.com/OpenMW/openmw-deps/-/raw/main/windows/lz4_win${BITS}_v1_9_2.7z" \ "lz4_win${BITS}_v1_9_2.7z" # Google test and mock if [ ! -z $TEST_FRAMEWORK ]; then echo "Google test 1.10.0..." if [ -d googletest ]; then printf " Google test exists, skipping." else git clone -b release-1.10.0 https://github.com/google/googletest.git fi fi fi cd .. #/.. # Set up dependencies BUILD_DIR="MSVC${MSVC_DISPLAY_YEAR}_${BITS}" if [ -n "$NMAKE" ]; then BUILD_DIR="${BUILD_DIR}_NMake" elif [ -n "$NINJA" ]; then BUILD_DIR="${BUILD_DIR}_Ninja" fi if [ -n "$SINGLE_CONFIG" ]; then BUILD_DIR="${BUILD_DIR}_${CONFIGURATIONS[0]}" fi if [ -z $KEEP ]; then echo echo "(Re)Creating build directory." rm -rf "$BUILD_DIR" fi mkdir -p "${BUILD_DIR}/deps" cd "${BUILD_DIR}/deps" DEPS_INSTALL="$(pwd)" cd $DEPS echo echo "Extracting dependencies, this might take a while..." echo "---------------------------------------------------" echo # Boost if [ -z $APPVEYOR ]; then printf "Boost ${BOOST_VER}... " else printf "Boost ${BOOST_VER} AppVeyor... " fi { if [ -z $APPVEYOR ]; then cd $DEPS_INSTALL BOOST_SDK="$(real_pwd)/Boost" # Boost's installer is still based on ms-dos API that doesn't support larger than 260 char path names # We work around this by installing to root of the current working drive and then move it to our deps # get the current working drive's root, we'll install to that temporarily CWD_DRIVE_ROOT="$(powershell -command '(get-location).Drive.Root')Boost_temp" CWD_DRIVE_ROOT_BASH=$(windowsPathAsUnix "$CWD_DRIVE_ROOT") if [ -d CWD_DRIVE_ROOT_BASH ]; then printf "Cannot continue, ${CWD_DRIVE_ROOT_BASH} aka ${CWD_DRIVE_ROOT} already exists. Please remove before re-running. "; wrappedExit 1; fi if [ -d ${BOOST_SDK} ] && grep "BOOST_VERSION ${BOOST_VER_SDK}" Boost/boost/version.hpp > /dev/null; then printf "Exists. " elif [ -z $SKIP_EXTRACT ]; then rm -rf Boost CI_EXTRA_INNO_OPTIONS="" [ -n "$CI" ] && CI_EXTRA_INNO_OPTIONS="//SUPPRESSMSGBOXES //LOG='boost_install.log'" "${DEPS}/boost-${BOOST_VER}-msvc${MSVC_VER}-win${BITS}.exe" //DIR="${CWD_DRIVE_ROOT}" //VERYSILENT //NORESTART ${CI_EXTRA_INNO_OPTIONS} mv "${CWD_DRIVE_ROOT_BASH}" "${BOOST_SDK}" fi add_cmake_opts -DBOOST_ROOT="$BOOST_SDK" \ -DBOOST_LIBRARYDIR="${BOOST_SDK}/lib${BITS}-msvc-${MSVC_VER}" add_cmake_opts -DBoost_COMPILER="-${TOOLSET}" echo Done. else # Appveyor has all the boost we need already BOOST_SDK="c:/Libraries/boost_${BOOST_VER_URL}" add_cmake_opts -DBOOST_ROOT="$BOOST_SDK" \ -DBOOST_LIBRARYDIR="${BOOST_SDK}/lib${BITS}-msvc-${MSVC_VER}.1" add_cmake_opts -DBoost_COMPILER="-${TOOLSET}" echo Done. fi } cd $DEPS echo # Bullet printf "Bullet 2.89... " { cd $DEPS_INSTALL if [ -d Bullet ]; then printf -- "Exists. (No version checking) " elif [ -z $SKIP_EXTRACT ]; then rm -rf Bullet eval 7z x -y "${DEPS}/Bullet-2.89-msvc${MSVC_YEAR}-win${BITS}-double.7z" $STRIP mv "Bullet-2.89-msvc${MSVC_YEAR}-win${BITS}-double" Bullet fi add_cmake_opts -DBULLET_ROOT="$(real_pwd)/Bullet" echo Done. } cd $DEPS echo # FFmpeg printf "FFmpeg 4.2.2... " { cd $DEPS_INSTALL if [ -d FFmpeg ] && grep "4.2.2" FFmpeg/README.txt > /dev/null; then printf "Exists. " elif [ -z $SKIP_EXTRACT ]; then rm -rf FFmpeg eval 7z x -y "${DEPS}/ffmpeg-4.2.2-win${BITS}.zip" $STRIP eval 7z x -y "${DEPS}/ffmpeg-4.2.2-dev-win${BITS}.zip" $STRIP mv "ffmpeg-4.2.2-win${BITS}-shared" FFmpeg cp -r "ffmpeg-4.2.2-win${BITS}-dev/"* FFmpeg/ rm -rf "ffmpeg-4.2.2-win${BITS}-dev" fi export FFMPEG_HOME="$(real_pwd)/FFmpeg" for config in ${CONFIGURATIONS[@]}; do add_runtime_dlls $config "$(pwd)/FFmpeg/bin/"{avcodec-58,avformat-58,avutil-56,swresample-3,swscale-5}.dll done if [ $BITS -eq 32 ]; then add_cmake_opts "-DCMAKE_EXE_LINKER_FLAGS=\"/machine:X86 /safeseh:no\"" fi echo Done. } cd $DEPS echo # MyGUI printf "MyGUI 3.4.0... " { cd $DEPS_INSTALL if [ -d MyGUI ] && \ grep "MYGUI_VERSION_MAJOR 3" MyGUI/include/MYGUI/MyGUI_Prerequest.h > /dev/null && \ grep "MYGUI_VERSION_MINOR 4" MyGUI/include/MYGUI/MyGUI_Prerequest.h > /dev/null && \ grep "MYGUI_VERSION_PATCH 0" MyGUI/include/MYGUI/MyGUI_Prerequest.h > /dev/null then printf "Exists. " elif [ -z $SKIP_EXTRACT ]; then rm -rf MyGUI eval 7z x -y "${DEPS}/MyGUI-3.4.0-msvc${MSVC_REAL_YEAR}-win${BITS}.7z" $STRIP [ -n "$PDBS" ] && eval 7z x -y "${DEPS}/MyGUI-3.4.0-msvc${MSVC_REAL_YEAR}-win${BITS}-sym.7z" $STRIP mv "MyGUI-3.4.0-msvc${MSVC_REAL_YEAR}-win${BITS}" MyGUI fi export MYGUI_HOME="$(real_pwd)/MyGUI" for CONFIGURATION in ${CONFIGURATIONS[@]}; do if [ $CONFIGURATION == "Debug" ]; then SUFFIX="_d" MYGUI_CONFIGURATION="Debug" else SUFFIX="" MYGUI_CONFIGURATION="RelWithDebInfo" fi add_runtime_dlls $CONFIGURATION "$(pwd)/MyGUI/bin/${MYGUI_CONFIGURATION}/MyGUIEngine${SUFFIX}.dll" done echo Done. } cd $DEPS echo # OpenAL printf "OpenAL-Soft 1.20.1... " { if [ -d openal-soft-1.20.1-bin ]; then printf "Exists. " elif [ -z $SKIP_EXTRACT ]; then rm -rf openal-soft-1.20.1-bin eval 7z x -y OpenAL-Soft-1.20.1.zip $STRIP fi OPENAL_SDK="$(real_pwd)/openal-soft-1.20.1-bin" add_cmake_opts -DOPENAL_INCLUDE_DIR="${OPENAL_SDK}/include/AL" \ -DOPENAL_LIBRARY="${OPENAL_SDK}/libs/Win${BITS}/OpenAL32.lib" for config in ${CONFIGURATIONS[@]}; do add_runtime_dlls $config "$(pwd)/openal-soft-1.20.1-bin/bin/WIN${BITS}/soft_oal.dll:OpenAL32.dll" done echo Done. } cd $DEPS echo # OSG printf "OSG 3.6.5... " { cd $DEPS_INSTALL if [ -d OSG ] && \ grep "OPENSCENEGRAPH_MAJOR_VERSION 3" OSG/include/osg/Version > /dev/null && \ grep "OPENSCENEGRAPH_MINOR_VERSION 6" OSG/include/osg/Version > /dev/null && \ grep "OPENSCENEGRAPH_PATCH_VERSION 5" OSG/include/osg/Version > /dev/null then printf "Exists. " elif [ -z $SKIP_EXTRACT ]; then rm -rf OSG eval 7z x -y "${DEPS}/OSG-3.6.5-msvc${MSVC_REAL_YEAR}-win${BITS}.7z" $STRIP [ -n "$PDBS" ] && eval 7z x -y "${DEPS}/OSG-3.6.5-msvc${MSVC_REAL_YEAR}-win${BITS}-sym.7z" $STRIP mv "OSG-3.6.5-msvc${MSVC_REAL_YEAR}-win${BITS}" OSG fi OSG_SDK="$(real_pwd)/OSG" add_cmake_opts -DOSG_DIR="$OSG_SDK" for CONFIGURATION in ${CONFIGURATIONS[@]}; do if [ $CONFIGURATION == "Debug" ]; then SUFFIX="d" else SUFFIX="" fi add_runtime_dlls $CONFIGURATION "$(pwd)/OSG/bin/"{OpenThreads,zlib,libpng}${SUFFIX}.dll \ "$(pwd)/OSG/bin/osg"{,Animation,DB,FX,GA,Particle,Text,Util,Viewer,Shadow}${SUFFIX}.dll add_osg_dlls $CONFIGURATION "$(pwd)/OSG/bin/osgPlugins-3.6.5/osgdb_"{bmp,dds,freetype,jpeg,osg,png,tga}${SUFFIX}.dll add_osg_dlls $CONFIGURATION "$(pwd)/OSG/bin/osgPlugins-3.6.5/osgdb_serializers_osg"{,animation,fx,ga,particle,text,util,viewer,shadow}${SUFFIX}.dll done echo Done. } cd $DEPS echo # Qt if [ -z $APPVEYOR ]; then printf "Qt 5.15.0... " else printf "Qt 5.13 AppVeyor... " fi { if [ $BITS -eq 64 ]; then SUFFIX="_64" else SUFFIX="" fi if [ -z $APPVEYOR ]; then cd $DEPS_INSTALL qt_version="5.15.0" if [ "win${BITS}_msvc${MSVC_REAL_YEAR}${SUFFIX}" == "win64_msvc2017_64" ]; then echo "This combination of options is known not to work. Falling back to Qt 5.14.2." qt_version="5.14.2" fi QT_SDK="$(real_pwd)/Qt/${qt_version}/msvc${MSVC_REAL_YEAR}${SUFFIX}" if [ -d "Qt/${qt_version}" ]; then printf "Exists. " elif [ -z $SKIP_EXTRACT ]; then if [ $MISSINGPYTHON -ne 0 ]; then echo "Can't be automatically installed without Python." wrappedExit 1 fi pushd "$DEPS" > /dev/null if ! [ -d 'aqt-venv' ]; then echo " Creating Virtualenv for aqt..." run_cmd python -m venv aqt-venv fi if [ -d 'aqt-venv/bin' ]; then VENV_BIN_DIR='bin' elif [ -d 'aqt-venv/Scripts' ]; then VENV_BIN_DIR='Scripts' else echo "Error: Failed to create virtualenv in expected location." wrappedExit 1 fi # check version aqt-venv/${VENV_BIN_DIR}/pip list | grep 'aqtinstall\s*1.1.3' || [ $? -ne 0 ] if [ $? -eq 0 ]; then echo " Installing aqt wheel into virtualenv..." run_cmd "aqt-venv/${VENV_BIN_DIR}/pip" install aqtinstall==1.1.3 fi popd > /dev/null rm -rf Qt mkdir Qt cd Qt run_cmd "${DEPS}/aqt-venv/${VENV_BIN_DIR}/aqt" install $qt_version windows desktop "win${BITS}_msvc${MSVC_REAL_YEAR}${SUFFIX}" printf " Cleaning up extraneous data... " rm -rf Qt/{aqtinstall.log,Tools} echo Done. fi cd $QT_SDK add_cmake_opts -DQT_QMAKE_EXECUTABLE="${QT_SDK}/bin/qmake.exe" \ -DCMAKE_PREFIX_PATH="$QT_SDK" for CONFIGURATION in ${CONFIGURATIONS[@]}; do if [ $CONFIGURATION == "Debug" ]; then DLLSUFFIX="d" else DLLSUFFIX="" fi add_runtime_dlls $CONFIGURATION "$(pwd)/bin/Qt5"{Core,Gui,Network,OpenGL,Widgets}${DLLSUFFIX}.dll add_qt_platform_dlls $CONFIGURATION "$(pwd)/plugins/platforms/qwindows${DLLSUFFIX}.dll" add_qt_style_dlls $CONFIGURATION "$(pwd)/plugins/styles/qwindowsvistastyle${DLLSUFFIX}.dll" done echo Done. else QT_SDK="C:/Qt/5.13/msvc2017${SUFFIX}" add_cmake_opts -DQT_QMAKE_EXECUTABLE="${QT_SDK}/bin/qmake.exe" \ -DCMAKE_PREFIX_PATH="$QT_SDK" for CONFIGURATION in ${CONFIGURATIONS[@]}; do if [ $CONFIGURATION == "Debug" ]; then DLLSUFFIX="d" else DLLSUFFIX="" fi DIR=$(windowsPathAsUnix "${QT_SDK}") add_runtime_dlls $CONFIGURATION "${DIR}/bin/Qt5"{Core,Gui,Network,OpenGL,Widgets}${DLLSUFFIX}.dll add_qt_platform_dlls $CONFIGURATION "${DIR}/plugins/platforms/qwindows${DLLSUFFIX}.dll" add_qt_style_dlls $CONFIGURATION "${DIR}/plugins/styles/qwindowsvistastyle${DLLSUFFIX}.dll" done echo Done. fi } cd $DEPS echo # SDL2 printf "SDL 2.0.12... " { if [ -d SDL2-2.0.12 ]; then printf "Exists. " elif [ -z $SKIP_EXTRACT ]; then rm -rf SDL2-2.0.12 eval 7z x -y SDL2-2.0.12.zip $STRIP fi export SDL2DIR="$(real_pwd)/SDL2-2.0.12" for config in ${CONFIGURATIONS[@]}; do add_runtime_dlls $config "$(pwd)/SDL2-2.0.12/lib/x${ARCHSUFFIX}/SDL2.dll" done echo Done. } cd $DEPS echo # LZ4 printf "LZ4 1.9.2... " { if [ -d LZ4_1.9.2 ]; then printf "Exists. " elif [ -z $SKIP_EXTRACT ]; then rm -rf LZ4_1.9.2 eval 7z x -y lz4_win${BITS}_v1_9_2.7z -o$(real_pwd)/LZ4_1.9.2 $STRIP fi export LZ4DIR="$(real_pwd)/LZ4_1.9.2" add_cmake_opts -DLZ4_INCLUDE_DIR="${LZ4DIR}/include" \ -DLZ4_LIBRARY="${LZ4DIR}/lib/liblz4.lib" for CONFIGURATION in ${CONFIGURATIONS[@]}; do if [ $CONFIGURATION == "Debug" ]; then LZ4_CONFIGURATION="Debug" else SUFFIX="" LZ4_CONFIGURATION="Release" fi add_runtime_dlls $CONFIGURATION "$(pwd)/LZ4_1.9.2/bin/${LZ4_CONFIGURATION}/liblz4.dll" done echo Done. } cd $DEPS echo # Google Test and Google Mock if [ ! -z $TEST_FRAMEWORK ]; then printf "Google test 1.10.0 ..." cd googletest mkdir -p build${MSVC_REAL_YEAR} cd build${MSVC_REAL_YEAR} GOOGLE_INSTALL_ROOT="${DEPS_INSTALL}/GoogleTest" for CONFIGURATION in ${CONFIGURATIONS[@]}; do # FindGMock.cmake mentions Release explicitly, but not RelWithDebInfo. Only one optimised library config can be used, so go for the safer one. GTEST_CONFIG=$([ $CONFIGURATION == "RelWithDebInfo" ] && echo "Release" || echo "$CONFIGURATION" ) if [ $GTEST_CONFIG == "Debug" ]; then DEBUG_SUFFIX="d" else DEBUG_SUFFIX="" fi if [ ! -f "$GOOGLE_INSTALL_ROOT/lib/gtest${DEBUG_SUFFIX}.lib" ]; then # Always use MSBuild solution files as they don't need the environment activating cmake .. -DCMAKE_USE_WIN32_THREADS_INIT=1 -G "Visual Studio $MSVC_REAL_VER $MSVC_REAL_YEAR$([ $BITS -eq 64 ] && [ $MSVC_REAL_VER -lt 16 ] && echo " Win64")" $([ $MSVC_REAL_VER -ge 16 ] && echo "-A $([ $BITS -eq 64 ] && echo "x64" || echo "Win32")") -DBUILD_SHARED_LIBS=1 cmake --build . --config "${GTEST_CONFIG}" cmake --install . --config "${GTEST_CONFIG}" --prefix "${GOOGLE_INSTALL_ROOT}" fi add_runtime_dlls $CONFIGURATION "${GOOGLE_INSTALL_ROOT}\bin\gtest_main${DEBUG_SUFFIX}.dll" add_runtime_dlls $CONFIGURATION "${GOOGLE_INSTALL_ROOT}\bin\gtest${DEBUG_SUFFIX}.dll" add_runtime_dlls $CONFIGURATION "${GOOGLE_INSTALL_ROOT}\bin\gmock_main${DEBUG_SUFFIX}.dll" add_runtime_dlls $CONFIGURATION "${GOOGLE_INSTALL_ROOT}\bin\gmock${DEBUG_SUFFIX}.dll" done add_cmake_opts -DBUILD_UNITTESTS=yes # FindGTest and FindGMock do not work perfectly on Windows # but we can help them by telling them everything we know about installation add_cmake_opts -DGMOCK_ROOT="$GOOGLE_INSTALL_ROOT" add_cmake_opts -DGTEST_ROOT="$GOOGLE_INSTALL_ROOT" add_cmake_opts -DGTEST_LIBRARY="$GOOGLE_INSTALL_ROOT/lib/gtest.lib" add_cmake_opts -DGTEST_MAIN_LIBRARY="$GOOGLE_INSTALL_ROOT/lib/gtest_main.lib" add_cmake_opts -DGMOCK_LIBRARY="$GOOGLE_INSTALL_ROOT/lib/gmock.lib" add_cmake_opts -DGMOCK_MAIN_LIBRARY="$GOOGLE_INSTALL_ROOT/lib/gmock_main.lib" add_cmake_opts -DGTEST_LIBRARY_DEBUG="$GOOGLE_INSTALL_ROOT/lib/gtestd.lib" add_cmake_opts -DGTEST_MAIN_LIBRARY_DEBUG="$GOOGLE_INSTALL_ROOT/lib/gtest_maind.lib" add_cmake_opts -DGMOCK_LIBRARY_DEBUG="$GOOGLE_INSTALL_ROOT/lib/gmockd.lib" add_cmake_opts -DGMOCK_MAIN_LIBRARY_DEBUG="$GOOGLE_INSTALL_ROOT/lib/gmock_maind.lib" add_cmake_opts -DGTEST_LINKED_AS_SHARED_LIBRARY=True add_cmake_opts -DGTEST_LIBRARY_TYPE=SHARED add_cmake_opts -DGTEST_MAIN_LIBRARY_TYPE=SHARED echo Done. fi echo cd $DEPS_INSTALL/.. echo echo "Setting up OpenMW build..." add_cmake_opts -DOPENMW_MP_BUILD=on add_cmake_opts -DCMAKE_INSTALL_PREFIX="${INSTALL_PREFIX}" if [ ! -z $CI ]; then case $STEP in components ) echo " Building subproject: Components." add_cmake_opts -DBUILD_ESSIMPORTER=no \ -DBUILD_LAUNCHER=no \ -DBUILD_MWINIIMPORTER=no \ -DBUILD_OPENCS=no \ -DBUILD_OPENMW=no \ -DBUILD_WIZARD=no ;; openmw ) echo " Building subproject: OpenMW." add_cmake_opts -DBUILD_ESSIMPORTER=no \ -DBUILD_LAUNCHER=no \ -DBUILD_MWINIIMPORTER=no \ -DBUILD_OPENCS=no \ -DBUILD_WIZARD=no ;; opencs ) echo " Building subproject: OpenCS." add_cmake_opts -DBUILD_ESSIMPORTER=no \ -DBUILD_LAUNCHER=no \ -DBUILD_MWINIIMPORTER=no \ -DBUILD_OPENMW=no \ -DBUILD_WIZARD=no ;; misc ) echo " Building subprojects: Misc." add_cmake_opts -DBUILD_OPENCS=no \ -DBUILD_OPENMW=no ;; esac fi # NOTE: Disable this when/if we want to run test cases #if [ -z $CI ]; then for CONFIGURATION in ${CONFIGURATIONS[@]}; do echo "- Copying Runtime DLLs for $CONFIGURATION..." DLL_PREFIX="" if [ -z $SINGLE_CONFIG ]; then mkdir -p $CONFIGURATION DLL_PREFIX="$CONFIGURATION/" fi for DLL in ${RUNTIME_DLLS[$CONFIGURATION]}; do TARGET="$(basename "$DLL")" if [[ "$DLL" == *":"* ]]; then originalIFS="$IFS" IFS=':'; SPLIT=( ${DLL} ); IFS=$originalIFS DLL=${SPLIT[0]} TARGET=${SPLIT[1]} fi echo " ${TARGET}." cp "$DLL" "${DLL_PREFIX}$TARGET" done echo echo "- OSG Plugin DLLs..." mkdir -p ${DLL_PREFIX}osgPlugins-3.6.5 for DLL in ${OSG_PLUGINS[$CONFIGURATION]}; do echo " $(basename $DLL)." cp "$DLL" ${DLL_PREFIX}osgPlugins-3.6.5 done echo echo "- Qt Platform DLLs..." mkdir -p ${DLL_PREFIX}platforms for DLL in ${QT_PLATFORMS[$CONFIGURATION]}; do echo " $(basename $DLL)" cp "$DLL" "${DLL_PREFIX}platforms" done echo echo "- Qt Style DLLs..." mkdir -p ${DLL_PREFIX}styles for DLL in ${QT_STYLES[$CONFIGURATION]}; do echo " $(basename $DLL)" cp "$DLL" "${DLL_PREFIX}styles" done echo done #fi if [ "${BUILD_BENCHMARKS}" ]; then add_cmake_opts -DBUILD_BENCHMARKS=ON fi if [ -n "$ACTIVATE_MSVC" ]; then echo -n "- Activating MSVC in the current shell... " command -v vswhere >/dev/null 2>&1 || { echo "Error: vswhere is not on the path."; wrappedExit 1; } # There are so many arguments now that I'm going to document them: # * products: allow Visual Studio or standalone build tools # * version: obvious. Awk helps make a version range by adding one. # * property installationPath: only give the installation path. # * latest: return only one result if several candidates exist. Prefer the last installed/updated # * requires: make sure it's got the MSVC compiler instead of, for example, just the .NET compiler. The .x86.x64 suffix means it's for either, not that it's the x64 on x86 cross compiler as you always get both MSVC_INSTALLATION_PATH=$(vswhere -products '*' -version "[$MSVC_REAL_VER,$(awk "BEGIN { print $MSVC_REAL_VER + 1; exit }"))" -property installationPath -latest -requires Microsoft.VisualStudio.Component.VC.Tools.x86.x64) if [ -z "$MSVC_INSTALLATION_PATH" ]; then echo "vswhere was unable to find MSVC $MSVC_DISPLAY_YEAR" wrappedExit 1 fi echo "@\"${MSVC_INSTALLATION_PATH}\Common7\Tools\VsDevCmd.bat\" -no_logo -arch=$([ $BITS -eq 64 ] && echo "amd64" || echo "x86") -host_arch=$([ $(uname -m) == 'x86_64' ] && echo "amd64" || echo "x86")" > ActivateMSVC.bat cp "../CI/activate_msvc.sh" . sed -i "s/\$MSVC_DISPLAY_YEAR/$MSVC_DISPLAY_YEAR/g" activate_msvc.sh source ./activate_msvc.sh cp "../CI/ActivateMSVC.ps1" . sed -i "s/\$MSVC_DISPLAY_YEAR/$MSVC_DISPLAY_YEAR/g" ActivateMSVC.ps1 echo "done." echo fi if [ -z $VERBOSE ]; then printf -- "- Configuring... " else echo "- cmake .. $CMAKE_OPTS" fi RET=0 run_cmd cmake .. $CMAKE_OPTS || RET=$? if [ -z $VERBOSE ]; then if [ $RET -eq 0 ]; then echo Done. else echo Failed. fi fi if [ $RET -ne 0 ]; then wrappedExit $RET fi echo "Script completed successfully." echo "You now have an OpenMW build system at $(unixPathAsWindows "$(pwd)")" if [ -n "$ACTIVATE_MSVC" ]; then echo echo "Note: you must manually activate MSVC for the shell in which you want to do the build." echo echo "Some scripts have been created in the build directory to do so in an existing shell." echo "Bash: source activate_msvc.sh" echo "CMD: ActivateMSVC.bat" echo "PowerShell: ActivateMSVC.ps1" echo echo "You may find options to launch a Development/Native Tools/Cross Tools shell in your start menu or Visual Studio." echo if [ $(uname -m) == 'x86_64' ]; then if [ $BITS -eq 64 ]; then inheritEnvironments=msvc_x64_x64 else inheritEnvironments=msvc_x64 fi else if [ $BITS -eq 64 ]; then inheritEnvironments=msvc_x86_x64 else inheritEnvironments=msvc_x86 fi fi echo "In Visual Studio 15.3 (2017 Update 3) or later, try setting '\"inheritEnvironments\": [ \"$inheritEnvironments\" ]' in CMakeSettings.json to build in the IDE." fi wrappedExit $RET ================================================ FILE: CI/before_script.osx.sh ================================================ #!/bin/sh -e export CXX=clang++ export CC=clang DEPENDENCIES_ROOT="/private/tmp/openmw-deps/openmw-deps" QT_PATH=$(brew --prefix qt@5) CCACHE_EXECUTABLE=$(brew --prefix ccache)/bin/ccache mkdir build cd build cmake \ -D CMAKE_PREFIX_PATH="$DEPENDENCIES_ROOT;$QT_PATH" \ -D CMAKE_C_COMPILER_LAUNCHER="$CCACHE_EXECUTABLE" \ -D CMAKE_CXX_COMPILER_LAUNCHER="$CCACHE_EXECUTABLE" \ -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_BUILD_TYPE=RELEASE \ -D OPENMW_OSX_DEPLOYMENT=TRUE \ -D BUILD_OPENMW=TRUE \ -D BUILD_OPENCS=TRUE \ -D BUILD_ESMTOOL=TRUE \ -D BUILD_BSATOOL=TRUE \ -D BUILD_ESSIMPORTER=TRUE \ -D BUILD_NIFTEST=TRUE \ -G"Unix Makefiles" \ .. ================================================ FILE: CI/build.msvc.sh ================================================ #!/bin/bash APPVEYOR="" CI="" PACKAGE="" PLATFORM="" CONFIGURATION="" VS_VERSION="" if [ -z $PLATFORM ]; then PLATFORM=`uname -m` fi if [ -z $CONFIGURATION ]; then CONFIGURATION="Debug" fi case $VS_VERSION in 14|14.0|2015 ) GENERATOR="Visual Studio 14 2015" MSVC_YEAR="2015" MSVC_VER="14.0" ;; # 12|2013| * ) GENERATOR="Visual Studio 12 2013" MSVC_YEAR="2013" MVSC_VER="12.0" ;; esac case $PLATFORM in x64|x86_64|x86-64|win64|Win64 ) BITS=64 ;; x32|x86|i686|i386|win32|Win32 ) BITS=32 ;; esac case $CONFIGURATION in debug|Debug|DEBUG ) CONFIGURATION=Debug ;; release|Release|RELEASE ) CONFIGURATION=Release ;; relwithdebinfo|RelWithDebInfo|RELWITHDEBINFO ) CONFIGURATION=RelWithDebInfo ;; esac if [ -z $APPVEYOR ]; then echo "Running ${BITS}-bit MSVC${MSVC_YEAR} ${CONFIGURATION} build outside of Appveyor." DIR=$(echo "$0" | sed "s,\\\\,/,g" | sed "s,\(.\):,/\\1,") cd $(dirname "$DIR")/.. else echo "Running ${BITS}-bit MSVC${MSVC_YEAR} ${CONFIGURATION} build in Appveyor." cd $APPVEYOR_BUILD_FOLDER fi BUILD_DIR="MSVC${MSVC_YEAR}_${BITS}" cd ${BUILD_DIR} which msbuild > /dev/null if [ $? -ne 0 ]; then msbuild() { /c/Program\ Files\ \(x86\)/MSBuild/${MSVC_VER}/Bin/MSBuild.exe "$@" } fi if [ -z $APPVEYOR ]; then msbuild OpenMW.sln //t:Build //p:Configuration=${CONFIGURATION} //m:8 else msbuild OpenMW.sln //t:Build //p:Configuration=${CONFIGURATION} //m:8 //logger:"C:\Program Files\AppVeyor\BuildAgent\Appveyor.MSBuildLogger.dll" fi RET=$? if [ $RET -eq 0 ] && [ ! -z $PACKAGE ]; then msbuild PACKAGE.vcxproj //t:Build //m:8 RET=$? fi exit $RET ================================================ FILE: CI/build_googletest.sh ================================================ #!/bin/sh -ex git clone -b release-1.10.0 https://github.com/google/googletest.git cd googletest mkdir build cd build cmake \ -D CMAKE_C_COMPILER="${CC}" \ -D CMAKE_CXX_COMPILER="${CXX}" \ -D CMAKE_C_COMPILER_LAUNCHER=ccache \ -D CMAKE_CXX_COMPILER_LAUNCHER=ccache \ -D CMAKE_BUILD_TYPE="${CONFIGURATION}" \ -D CMAKE_INSTALL_PREFIX="${GOOGLETEST_DIR}" \ -G "${GENERATOR}" \ .. cmake --build . --config "${CONFIGURATION}" -- -j $(nproc) cmake --install . --config "${CONFIGURATION}" ================================================ FILE: CI/check_package.osx.sh ================================================ #!/usr/bin/env bash hdiutil attach ./*.dmg -mountpoint "${TRAVIS_BUILD_DIR}/openmw-package" > /dev/null || echo "hdutil has failed" EXPECTED_PACKAGE_FILES=('Applications' 'OpenMW-CS.app' 'OpenMW.app') PACKAGE_FILES=$(ls "${TRAVIS_BUILD_DIR}/openmw-package" | LC_ALL=C sort) DIFF=$(diff <(printf "%s\n" "${EXPECTED_PACKAGE_FILES[@]}") <(printf "%s\n" "${PACKAGE_FILES[@]}")) DIFF_STATUS=$? if [[ $DIFF_STATUS -ne 0 ]]; then echo "The package should only contain an Applications symlink and two applications, see the following diff for details." >&2 echo "$DIFF" >&2 exit 1 fi ================================================ FILE: CI/check_tabs.sh ================================================ #!/bin/bash OUTPUT=$(grep -nRP '\t' --include=\*.{cpp,hpp,c,h} --exclude=ui_\* apps components) if [[ $OUTPUT ]] ; then echo "Error: Tab characters found!" echo $OUTPUT exit 1 fi ================================================ FILE: CI/deploy.osx.sh ================================================ #!/bin/sh # This script expect the following environment variables to be set: # - OSX_DEPLOY_KEY: private SSH key, must be encoded like this before adding it to Travis secrets: https://github.com/travis-ci/travis-ci/issues/7715#issuecomment-433301692 # - OSX_DEPLOY_HOST: string specifying SSH of the following format: ssh-user@ssh-host # - OSX_DEPLOY_PORT: SSH port, it can't be a part of the host string because scp doesn't accept hosts with ports # - OSX_DEPLOY_HOST_FINGERPRINT: fingerprint of the host, can be obtained by using ssh-keygen -F [host]:port & putting it in double quotes when adding to Travis secrets SSH_KEY_PATH="$HOME/.ssh/openmw_deploy" REMOTE_PATH="\$HOME/nightly" echo "$OSX_DEPLOY_KEY" > "$SSH_KEY_PATH" chmod 600 "$SSH_KEY_PATH" echo "$OSX_DEPLOY_HOST_FINGERPRINT" >> "$HOME/.ssh/known_hosts" cd build || exit 1 DATE=$(date +'%d%m%Y') SHORT_COMMIT=$(git rev-parse --short "${TRAVIS_COMMIT}") TARGET_FILENAME="OpenMW-${DATE}-${SHORT_COMMIT}.dmg" if ! ssh -p "$OSX_DEPLOY_PORT" -i "$SSH_KEY_PATH" "$OSX_DEPLOY_HOST" "ls \"$REMOTE_PATH\"" | grep "$SHORT_COMMIT" > /dev/null; then scp -P "$OSX_DEPLOY_PORT" -i "$SSH_KEY_PATH" ./*.dmg "$OSX_DEPLOY_HOST:$REMOTE_PATH/$TARGET_FILENAME" else echo "An existing nightly build for commit ${SHORT_COMMIT} has been found, skipping upload." fi ================================================ FILE: CI/install_debian_deps.sh ================================================ #!/bin/bash set -euo pipefail print_help() { echo "usage: $0 [group]..." echo echo " available groups: "${!GROUPED_DEPS[@]}"" } declare -rA GROUPED_DEPS=( [gcc]="binutils gcc g++ libc-dev" [clang]="binutils clang" # Common dependencies for building OpenMW. [openmw-deps]=" make cmake ccache git pkg-config libboost-filesystem-dev libboost-program-options-dev libboost-system-dev libboost-iostreams-dev 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 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" [coverity]="curl" # Pre-requisites for building MyGUI and OSG for static linking. # # * MyGUI and OSG: libsdl2-dev liblz4-dev libfreetype6-dev # * OSG: libgl-dev # # Plugins: # * DAE: libcollada-dom-dev libboost-system-dev libboost-filesystem-dev # * JPEG: libjpeg-dev # * PNG: libpng-dev [openmw-deps-static]=" make cmake ccache curl unzip libcollada-dom-dev libfreetype6-dev libjpeg-dev libpng-dev libsdl2-dev libboost-system-dev libboost-filesystem-dev libgl-dev " ) if [[ $# -eq 0 ]]; then >&2 print_help exit 1 fi deps=() for group in "$@"; do if [[ ! -v GROUPED_DEPS[$group] ]]; then >&2 echo "error: unknown group ${group}" exit 1 fi deps+=(${GROUPED_DEPS[$group]}) 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[@]}" ================================================ FILE: CMakeLists.txt ================================================ project(OpenMW) cmake_minimum_required(VERSION 3.1.0) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) # for link time optimization, remove if cmake version is >= 3.9 if(POLICY CMP0069) # LTO cmake_policy(SET CMP0069 NEW) endif() # for position-independent executable, remove if cmake version is >= 3.14 if(POLICY CMP0083) cmake_policy(SET CMP0083 NEW) endif() option(OPENMW_GL4ES_MANUAL_INIT "Manually initialize gl4es. This is more reliable on platforms without a windowing system. Requires gl4es to be configured with -DNOEGL=ON -DNO_LOADER=ON -DNO_INIT_CONSTRUCTOR=ON." OFF) if(OPENMW_GL4ES_MANUAL_INIT) add_definitions(-DOPENMW_GL4ES_MANUAL_INIT) endif() # Apps and tools option(BUILD_OPENMW "Build OpenMW" ON) option(BUILD_LAUNCHER "Build Launcher" ON) option(BUILD_WIZARD "Build Installation Wizard" ON) option(BUILD_MWINIIMPORTER "Build MWiniImporter" ON) option(BUILD_OPENCS "Build OpenMW Construction Set" ON) option(BUILD_ESSIMPORTER "Build ESS (Morrowind save game) importer" ON) option(BUILD_BSATOOL "Build BSA extractor" ON) option(BUILD_ESMTOOL "Build ESM inspector" ON) option(BUILD_NIFTEST "Build nif file tester" ON) 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_OPENMW_MP "Build OpenMW-MP" ON) option(BUILD_BROWSER "Build tes3mp Server Browser" ON) option(BUILD_MASTER "Build tes3mp Master Server" OFF) set(OpenGL_GL_PREFERENCE LEGACY) # Use LEGACY as we use GL2; GLNVD is for GL3 and up. if (NOT BUILD_LAUNCHER AND NOT BUILD_BROWSER AND NOT BUILD_OPENCS AND NOT BUILD_WIZARD) set(USE_QT FALSE) else() set(USE_QT TRUE) endif() # If the user doesn't supply a CMAKE_BUILD_TYPE via command line, choose one for them. IF(NOT CMAKE_BUILD_TYPE) SET(CMAKE_BUILD_TYPE RelWithDebInfo CACHE STRING "Choose the type of build, options are: None(CMAKE_CXX_FLAGS or CMAKE_C_FLAGS used) Debug Release RelWithDebInfo MinSizeRel." FORCE) set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS None Debug Release RelWithDebInfo MinSizeRel) ENDIF() if (APPLE) set(APP_BUNDLE_NAME "${CMAKE_PROJECT_NAME}.app") set(APP_BUNDLE_DIR "${OpenMW_BINARY_DIR}/${APP_BUNDLE_NAME}") endif (APPLE) set(CMAKE_MODULE_PATH ${CMAKE_SOURCE_DIR}/cmake/) if (ANDROID) set(CMAKE_FIND_ROOT_PATH ${OPENMW_DEPENDENCIES_DIR} "${CMAKE_FIND_ROOT_PATH}") endif() # Version message(STATUS "Configuring OpenMW...") set(OPENMW_VERSION_MAJOR 0) set(OPENMW_VERSION_MINOR 47) set(OPENMW_VERSION_RELEASE 0) set(OPENMW_VERSION_COMMITHASH "") set(OPENMW_VERSION_TAGHASH "") set(OPENMW_VERSION_COMMITDATE "") set(OPENMW_VERSION "${OPENMW_VERSION_MAJOR}.${OPENMW_VERSION_MINOR}.${OPENMW_VERSION_RELEASE}") set(OPENMW_DOC_BASEURL "https://openmw.readthedocs.io/en/stable/") set(GIT_CHECKOUT FALSE) if(EXISTS ${PROJECT_SOURCE_DIR}/.git) find_package(Git) if(GIT_FOUND) set(GIT_CHECKOUT TRUE) else(GIT_FOUND) message(WARNING "Git executable not found") endif(GIT_FOUND) if(GIT_FOUND) execute_process ( COMMAND ${GIT_EXECUTABLE} log -1 --format='%aI' WORKING_DIRECTORY ${PROJECT_SOURCE_DIR} RESULT_VARIABLE EXITCODE3 OUTPUT_VARIABLE OPENMW_VERSION_COMMITDATE OUTPUT_STRIP_TRAILING_WHITESPACE) if(NOT EXITCODE3) string(SUBSTRING ${OPENMW_VERSION_COMMITDATE} 1 10 OPENMW_VERSION_COMMITDATE) endif(NOT EXITCODE3) endif(GIT_FOUND) endif(EXISTS ${PROJECT_SOURCE_DIR}/.git) # Macros include(OpenMWMacros) include(WholeArchive) # doxygen main page configure_file ("${OpenMW_SOURCE_DIR}/docs/mainpage.hpp.cmake" "${OpenMW_BINARY_DIR}/docs/mainpage.hpp") option(BOOST_STATIC "Link static build of Boost into the binaries" FALSE) option(SDL2_STATIC "Link static build of SDL into the binaries" FALSE) option(QT_STATIC "Link static build of QT into the binaries" FALSE) option(OPENMW_USE_SYSTEM_BULLET "Use system provided bullet physics library" ON) if(OPENMW_USE_SYSTEM_BULLET) set(_bullet_static_default OFF) else() set(_bullet_static_default ON) endif() option(BULLET_STATIC "Link static build of Bullet into the binaries" ${_bullet_static_default}) option(OPENMW_USE_SYSTEM_OSG "Use system provided OpenSceneGraph libraries" ON) if(OPENMW_USE_SYSTEM_OSG) set(_osg_static_default OFF) else() set(_osg_static_default ON) endif() option(OSG_STATIC "Link static build of OpenSceneGraph into the binaries" ${_osg_static_default}) option(OPENMW_USE_SYSTEM_MYGUI "Use system provided mygui library" ON) if(OPENMW_USE_SYSTEM_MYGUI) set(_mygui_static_default OFF) else() set(_mygui_static_default ON) endif() option(MYGUI_STATIC "Link static build of Mygui into the binaries" ${_mygui_static_default}) option(OPENMW_USE_SYSTEM_RECASTNAVIGATION "Use system provided recastnavigation library" OFF) if(OPENMW_USE_SYSTEM_RECASTNAVIGATION) set(_recastnavigation_static_default OFF) find_package(RecastNavigation REQUIRED) else() set(_recastnavigation_static_default ON) endif() option(RECASTNAVIGATION_STATIC "Build recastnavigation static libraries" ${_recastnavigation_static_default}) option(OPENMW_UNITY_BUILD "Use fewer compilation units to speed up compile time" FALSE) option(OPENMW_LTO_BUILD "Build OpenMW with Link-Time Optimization (Needs ~2GB of RAM)" OFF) # what is necessary to build documentation IF( BUILD_DOCS ) # Builds the documentation. FIND_PACKAGE( Sphinx REQUIRED ) FIND_PACKAGE( Doxygen REQUIRED ) ENDIF() # OS X deployment option(OPENMW_OSX_DEPLOYMENT OFF) if (MSVC) option(OPENMW_MP_BUILD "Build OpenMW with /MP flag" OFF) endif() # Set up common paths if (APPLE) set(MORROWIND_DATA_FILES "./data" CACHE PATH "location of Morrowind data files") set(OPENMW_RESOURCE_FILES "../Resources/resources" CACHE PATH "location of OpenMW resources files") elseif(UNIX) # Paths SET(BINDIR "${CMAKE_INSTALL_PREFIX}/bin" CACHE PATH "Where to install binaries") SET(LIBDIR "${CMAKE_INSTALL_PREFIX}/lib${LIB_SUFFIX}" CACHE PATH "Where to install libraries") SET(DATAROOTDIR "${CMAKE_INSTALL_PREFIX}/share" CACHE PATH "Sets the root of data directories to a non-default location") SET(GLOBAL_DATA_PATH "${DATAROOTDIR}/games/" CACHE PATH "Set data path prefix") SET(DATADIR "${GLOBAL_DATA_PATH}/openmw" CACHE PATH "Sets the openmw data directories to a non-default location") SET(ICONDIR "${DATAROOTDIR}/pixmaps" CACHE PATH "Set icon dir") SET(LICDIR "${DATAROOTDIR}/licenses/openmw" CACHE PATH "Sets the openmw license directory to a non-default location.") IF("${CMAKE_INSTALL_PREFIX}" STREQUAL "/usr") SET(GLOBAL_CONFIG_PATH "/etc/" CACHE PATH "Set config dir prefix") ELSE() SET(GLOBAL_CONFIG_PATH "${CMAKE_INSTALL_PREFIX}/etc/" CACHE PATH "Set config dir prefix") ENDIF() SET(SYSCONFDIR "${GLOBAL_CONFIG_PATH}/openmw" CACHE PATH "Set config dir") set(MORROWIND_DATA_FILES "${DATADIR}/data" CACHE PATH "location of Morrowind data files") set(OPENMW_RESOURCE_FILES "${DATADIR}/resources" CACHE PATH "location of OpenMW resources files") else() set(MORROWIND_DATA_FILES "data" CACHE PATH "location of Morrowind data files") set(OPENMW_RESOURCE_FILES "resources" CACHE PATH "location of OpenMW resources files") endif(APPLE) if (WIN32) option(USE_DEBUG_CONSOLE "whether a debug console should be enabled for debug builds, if false debug output is redirected to Visual Studio output" ON) endif() find_package(RakNet REQUIRED) include_directories(${RakNet_INCLUDES}) # Dependencies find_package(OpenGL REQUIRED) find_package(LZ4 REQUIRED) if (USE_QT) find_package(Qt5Core 5.9 REQUIRED) # Temporary adjustment for TES3MP CI find_package(Qt5Widgets REQUIRED) find_package(Qt5Network REQUIRED) find_package(Qt5OpenGL REQUIRED) # Instruct CMake to run moc automatically when needed. #set(CMAKE_AUTOMOC ON) endif() # Start of tes3mp addition # # Don't require certain dependencies for the server IF(BUILD_OPENMW OR BUILD_OPENCS) # End of tes3mp addition set(USED_OSG_COMPONENTS osgDB osgViewer osgText osgGA osgParticle osgUtil osgFX osgShadow osgAnimation) set(USED_OSG_PLUGINS osgdb_bmp osgdb_dds osgdb_freetype osgdb_jpeg osgdb_osg osgdb_png osgdb_serializers_osg osgdb_tga) # Start of tes3mp addition # # Don't require certain dependencies for the server ENDIF(BUILD_OPENMW OR BUILD_OPENCS) # End of tes3mp addition add_subdirectory(extern) # Sound setup # Start of tes3mp addition # # Don't require certain dependencies for the server IF(BUILD_OPENMW OR BUILD_OPENCS) # End of tes3mp addition # Require at least ffmpeg 3.2 for now SET(FFVER_OK FALSE) find_package(FFmpeg REQUIRED COMPONENTS AVCODEC AVFORMAT AVUTIL SWSCALE SWRESAMPLE) if(FFmpeg_FOUND) SET(FFVER_OK TRUE) # Can not detect FFmpeg version on Windows for now if (NOT WIN32) if(FFmpeg_AVFORMAT_VERSION VERSION_LESS "57.56.100") message(STATUS "libavformat is too old! (${FFmpeg_AVFORMAT_VERSION}, wanted 57.56.100)") set(FFVER_OK FALSE) endif() if(FFmpeg_AVCODEC_VERSION VERSION_LESS "57.64.100") message(STATUS "libavcodec is too old! (${FFmpeg_AVCODEC_VERSION}, wanted 57.64.100)") set(FFVER_OK FALSE) endif() if(FFmpeg_AVUTIL_VERSION VERSION_LESS "55.34.100") message(STATUS "libavutil is too old! (${FFmpeg_AVUTIL_VERSION}, wanted 55.34.100)") set(FFVER_OK FALSE) endif() if(FFmpeg_SWSCALE_VERSION VERSION_LESS "4.2.100") message(STATUS "libswscale is too old! (${FFmpeg_SWSCALE_VERSION}, wanted 4.2.100)") set(FFVER_OK FALSE) endif() if(FFmpeg_SWRESAMPLE_VERSION VERSION_LESS "2.3.100") message(STATUS "libswresample is too old! (${FFmpeg_SWRESAMPLE_VERSION}, wanted 2.3.100)") set(FFVER_OK FALSE) endif() endif() if(NOT FFVER_OK AND NOT APPLE) # unable to detect on version on MacOS < 11.0 message(FATAL_ERROR "FFmpeg version is too old, 3.2 is required" ) endif() endif() if(NOT FFmpeg_FOUND) message(FATAL_ERROR "FFmpeg was not found" ) endif() if(WIN32) message("Can not detect FFmpeg version, at least the 3.2 is required" ) endif() # Required for building the FFmpeg headers add_definitions(-D__STDC_CONSTANT_MACROS) # Reqiuired for unity build add_definitions(-DMYGUI_DONT_REPLACE_NULLPTR) # TinyXML option(USE_SYSTEM_TINYXML "Use system TinyXML library instead of internal." OFF) if (USE_SYSTEM_TINYXML) find_package(TinyXML REQUIRED) add_definitions (-DTIXML_USE_STL) include_directories(SYSTEM ${TinyXML_INCLUDE_DIRS}) endif() # Start of tes3mp addition # # Don't require certain dependencies for the server ENDIF(BUILD_OPENMW OR BUILD_OPENCS) # End of tes3mp addition # Platform specific if (WIN32) if(NOT MINGW) set(Boost_USE_STATIC_LIBS ON) add_definitions(-DBOOST_ALL_NO_LIB) endif(NOT MINGW) # Suppress WinMain(), provided by SDL add_definitions(-DSDL_MAIN_HANDLED) # Get rid of useless crud from windows.h add_definitions(-DNOMINMAX -DWIN32_LEAN_AND_MEAN) endif() # Start of tes3mp addition # # Don't require certain dependencies for the server IF(BUILD_OPENMW OR BUILD_OPENCS) # End of tes3mp addition if(OPENMW_USE_SYSTEM_BULLET) set(REQUIRED_BULLET_VERSION 286) # Bullet 286 required due to runtime bugfixes for btCapsuleShape if (DEFINED ENV{TRAVIS_BRANCH} OR DEFINED ENV{APPVEYOR}) set(REQUIRED_BULLET_VERSION 283) # but for build testing, 283 is fine endif() # First, try BulletConfig-float64.cmake which comes with Debian derivatives. # This file does not define the Bullet version in a CMake-friendly way. find_package(Bullet CONFIGS BulletConfig-float64.cmake QUIET COMPONENTS BulletCollision LinearMath) if (BULLET_FOUND) string(REPLACE "." "" _bullet_version_num ${BULLET_VERSION_STRING}) if (_bullet_version_num VERSION_LESS REQUIRED_BULLET_VERSION) message(FATAL_ERROR "System bullet version too old, OpenMW requires at least ${REQUIRED_BULLET_VERSION}, got ${_bullet_version_num}") endif() # Fix the relative include: set(BULLET_INCLUDE_DIRS "${BULLET_ROOT_DIR}/${BULLET_INCLUDE_DIRS}") include(FindPackageMessage) find_package_message(Bullet "Found Bullet: ${BULLET_LIBRARIES} ${BULLET_VERSION_STRING}" "${BULLET_VERSION_STRING}-float64") else() find_package(Bullet ${REQUIRED_BULLET_VERSION} REQUIRED COMPONENTS BulletCollision LinearMath) endif() # Only link the Bullet libraries that we need: string(REGEX MATCHALL "((optimized|debug);)?[^;]*(BulletCollision|LinearMath)[^;]*" BULLET_LIBRARIES "${BULLET_LIBRARIES}") include(cmake/CheckBulletPrecision.cmake) if (HAS_DOUBLE_PRECISION_BULLET) message(STATUS "Bullet uses double precision") else() message(FATAL_ERROR "Bullet does not uses double precision") endif() endif() # Start of tes3mp addition # # Don't require certain dependencies for the server ENDIF(BUILD_OPENMW OR BUILD_OPENCS) # End of tes3mp addition if (NOT WIN32 AND BUILD_WIZARD) # windows users can just run the morrowind installer find_package(LIBUNSHIELD REQUIRED) # required only for non win32 when building openmw-wizard set(OPENMW_USE_UNSHIELD TRUE) endif() # Fix for not visible pthreads functions for linker with glibc 2.15 if (UNIX AND NOT APPLE) find_package (Threads) endif() # Look for stdint.h include(CheckIncludeFile) check_include_file(stdint.h HAVE_STDINT_H) if(NOT HAVE_STDINT_H) unset(HAVE_STDINT_H CACHE) message(FATAL_ERROR "stdint.h was not found" ) endif() # Start of tes3mp addition # # Don't require certain dependencies for the server # but keep OSG's headers (HACK) IF(BUILD_OPENMW OR BUILD_OPENCS) # End of tes3mp addition if(OPENMW_USE_SYSTEM_OSG) find_package(OpenSceneGraph 3.4.0 REQUIRED ${USED_OSG_COMPONENTS}) if (${OPENSCENEGRAPH_VERSION} VERSION_GREATER 3.6.2 AND ${OPENSCENEGRAPH_VERSION} VERSION_LESS 3.6.5) message(FATAL_ERROR "OpenSceneGraph version ${OPENSCENEGRAPH_VERSION} has critical regressions which cause crashes. Please upgrade to 3.6.5 or later. We strongly recommend using the tip of the official 'OpenSceneGraph-3.6' branch or the tip of '3.6' OpenMW/osg (OSGoS).") endif() if(OSG_STATIC) find_package(OSGPlugins REQUIRED COMPONENTS ${USED_OSG_PLUGINS}) endif() endif() include_directories(BEFORE SYSTEM ${OPENSCENEGRAPH_INCLUDE_DIRS}) if(OSG_STATIC) add_definitions(-DOSG_LIBRARY_STATIC) endif() # Start of tes3mp addition # # Don't require certain dependencies for the server # but keep OSG's headers (HACK) ELSE(BUILD_OPENMW OR BUILD_OPENCS) include_directories(${OPENSCENEGRAPH_INCLUDE_DIRS}) ENDIF(BUILD_OPENMW OR BUILD_OPENCS) # End of tes3mp addition set(BOOST_COMPONENTS system filesystem program_options iostreams) if(WIN32) set(BOOST_COMPONENTS ${BOOST_COMPONENTS} locale) if(MSVC) # boost-zlib is not present (nor needed) in vcpkg version of boost. # there, it is part of boost-iostreams instead. set(BOOST_OPTIONAL_COMPONENTS zlib) endif(MSVC) endif(WIN32) IF(BOOST_STATIC) set(Boost_USE_STATIC_LIBS ON) endif() set(Boost_NO_BOOST_CMAKE ON) find_package(Boost 1.6.2 REQUIRED COMPONENTS ${BOOST_COMPONENTS} OPTIONAL_COMPONENTS ${BOOST_OPTIONAL_COMPONENTS}) # Start of tes3mp addition # # Don't require certain dependencies for the server IF(BUILD_OPENMW OR BUILD_OPENCS) # End of tes3mp addition if(OPENMW_USE_SYSTEM_MYGUI) find_package(MyGUI 3.2.2 REQUIRED) endif() # End of tes3mp addition # # Don't require certain dependencies for the server ENDIF(BUILD_OPENMW OR BUILD_OPENCS) # End of tes3mp addition find_package(SDL2 2.0.9 REQUIRED) # Start of tes3mp addition # # Don't require certain dependencies for the server IF(BUILD_OPENMW OR BUILD_OPENCS) # End of tes3mp addition find_package(OpenAL REQUIRED) # End of tes3mp addition # # Don't require certain dependencies for the server ENDIF(BUILD_OPENMW OR BUILD_OPENCS) # End of tes3mp addition include_directories( BEFORE SYSTEM "." ${SDL2_INCLUDE_DIR} ${Boost_INCLUDE_DIR} ${MyGUI_INCLUDE_DIRS} ${OPENAL_INCLUDE_DIR} ${OPENGL_INCLUDE_DIR} ${BULLET_INCLUDE_DIRS} ) link_directories(${SDL2_LIBRARY_DIRS} ${Boost_LIBRARY_DIRS}) if(MYGUI_STATIC) add_definitions(-DMYGUI_STATIC) endif (MYGUI_STATIC) if (APPLE) configure_file(${OpenMW_SOURCE_DIR}/files/mac/openmw-Info.plist.in "${APP_BUNDLE_DIR}/Contents/Info.plist") configure_file(${OpenMW_SOURCE_DIR}/files/mac/openmw.icns "${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}) endif () add_subdirectory(files/) # Specify build paths if (APPLE) set(CMAKE_RUNTIME_OUTPUT_DIRECTORY "${APP_BUNDLE_DIR}/Contents/MacOS") set(CMAKE_LIBRARY_OUTPUT_DIRECTORY "${APP_BUNDLE_DIR}/Contents/MacOS") if (OPENMW_OSX_DEPLOYMENT) SET(CMAKE_INSTALL_RPATH_USE_LINK_PATH TRUE) endif() else (APPLE) set(CMAKE_RUNTIME_OUTPUT_DIRECTORY "${OpenMW_BINARY_DIR}") set(CMAKE_LIBRARY_OUTPUT_DIRECTORY "${OpenMW_BINARY_DIR}") endif (APPLE) # Other files configure_resource_file(${OpenMW_SOURCE_DIR}/files/tes3mp/tes3mp-client-default.cfg "${OpenMW_BINARY_DIR}" "tes3mp-client-default.cfg") configure_resource_file(${OpenMW_SOURCE_DIR}/files/tes3mp/tes3mp-server-default.cfg "${OpenMW_BINARY_DIR}" "tes3mp-server-default.cfg") pack_resource_file(${OpenMW_SOURCE_DIR}/files/settings-default.cfg "${OpenMW_BINARY_DIR}" "defaults.bin") configure_resource_file(${OpenMW_SOURCE_DIR}/files/openmw.appdata.xml "${OpenMW_BINARY_DIR}" "openmw.appdata.xml") if (NOT APPLE) configure_resource_file(${OpenMW_SOURCE_DIR}/files/openmw.cfg.local "${OpenMW_BINARY_DIR}" "openmw.cfg") configure_resource_file(${OpenMW_SOURCE_DIR}/files/openmw.cfg "${OpenMW_BINARY_DIR}" "openmw.cfg.install") else () configure_file(${OpenMW_SOURCE_DIR}/files/openmw.cfg "${OpenMW_BINARY_DIR}/openmw.cfg") endif () pack_resource_file(${OpenMW_SOURCE_DIR}/files/openmw-cs.cfg "${OpenMW_BINARY_DIR}" "defaults-cs.bin") # Needs the copy version because the configure version assumes the end of the file has been reached when a null character is reached and there are no CMake expressions to evaluate. copy_resource_file(${OpenMW_SOURCE_DIR}/files/opencs/defaultfilters "${OpenMW_BINARY_DIR}" "resources/defaultfilters") configure_resource_file(${OpenMW_SOURCE_DIR}/files/gamecontrollerdb.txt "${OpenMW_BINARY_DIR}" "gamecontrollerdb.txt") if (NOT WIN32 AND NOT APPLE) configure_file(${OpenMW_SOURCE_DIR}/files/org.openmw.launcher.desktop "${OpenMW_BINARY_DIR}/org.openmw.launcher.desktop") configure_file(${OpenMW_SOURCE_DIR}/files/openmw.appdata.xml "${OpenMW_BINARY_DIR}/openmw.appdata.xml") configure_file(${OpenMW_SOURCE_DIR}/files/tes3mp-browser.desktop "${OpenMW_BINARY_DIR}/tes3mp-browser.desktop") configure_file(${OpenMW_SOURCE_DIR}/files/org.openmw.cs.desktop "${OpenMW_BINARY_DIR}/org.openmw.cs.desktop") endif() if(OPENMW_LTO_BUILD) if(NOT CMAKE_VERSION VERSION_LESS 3.9) include(CheckIPOSupported) check_ipo_supported(RESULT HAVE_IPO OUTPUT HAVE_IPO_OUTPUT) if(HAVE_IPO) message(STATUS "LTO enabled for Release configuration.") set(CMAKE_INTERPROCEDURAL_OPTIMIZATION_RELEASE TRUE) else() message(WARNING "Requested option OPENMW_LTO_BUILD not supported by this compiler: ${HAVE_IPO_OUTPUT}") if(MSVC) message(STATUS "Note: Flags used to be set manually for this setting with MSVC. We now rely on CMake for this. Upgrade CMake to at least 3.13 to re-enable this setting.") endif() endif() else() message(WARNING "Requested option OPENMW_LTO_BUILD not supported by this cmake version: ${CMAKE_VERSION}. Upgrade CMake to at least 3.9 to enable support for certain compilers. Newer CMake versions support more compilers.") endif() endif() if (CMAKE_CXX_COMPILER_ID STREQUAL GNU OR CMAKE_CXX_COMPILER_ID STREQUAL Clang) set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -Wextra -Wundef -Wno-unused-parameter -pedantic -Wno-long-long") add_definitions( -DBOOST_NO_CXX11_SCOPED_ENUMS=ON ) if (APPLE) set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -stdlib=libc++") set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -stdlib=libc++") endif() if (CMAKE_CXX_COMPILER_ID STREQUAL Clang AND NOT APPLE) if (CMAKE_CXX_COMPILER_VERSION VERSION_GREATER 3.6 OR CMAKE_CXX_COMPILER_VERSION VERSION_EQUAL 3.6) set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wno-potentially-evaluated-expression") endif () endif() if (CMAKE_CXX_COMPILER_ID STREQUAL GNU AND CMAKE_CXX_COMPILER_VERSION VERSION_GREATER 4.6 OR CMAKE_CXX_COMPILER_VERSION VERSION_EQUAL 4.6) set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wno-unused-but-set-parameter") endif() endif (CMAKE_CXX_COMPILER_ID STREQUAL GNU OR CMAKE_CXX_COMPILER_ID STREQUAL Clang) # Extern # Start of tes3mp addition # # Don't require certain dependencies for the server IF(BUILD_OPENMW OR BUILD_OPENCS) # End of tes3mp addition add_subdirectory (extern/osg-ffmpeg-videoplayer) add_subdirectory (extern/oics) # Start of tes3mp addition # # Don't require certain dependencies for the server ENDIF(BUILD_OPENMW OR BUILD_OPENCS) # End of tes3mp addition add_subdirectory (extern/Base64) # Start of tes3mp addition # # Don't require certain dependencies for the server IF(BUILD_OPENMW OR BUILD_OPENCS) # End of tes3mp addition if (BUILD_OPENCS) add_subdirectory (extern/osgQt) endif() # Start of tes3mp addition # # Don't require certain dependencies for the server ENDIF(BUILD_OPENMW OR BUILD_OPENCS) # End of tes3mp addition # Components add_subdirectory (components) target_compile_definitions(components PRIVATE OPENMW_DOC_BASEURL="${OPENMW_DOC_BASEURL}") # Apps and tools if (BUILD_OPENMW_MP) add_subdirectory( apps/openmw-mp ) endif() if (BUILD_MASTER) add_subdirectory( apps/master ) endif() if (BUILD_OPENMW) add_subdirectory( apps/openmw ) endif() if (BUILD_BSATOOL) add_subdirectory( apps/bsatool ) endif() if (BUILD_ESMTOOL) add_subdirectory( apps/esmtool ) endif() if (BUILD_LAUNCHER) add_subdirectory( apps/launcher ) endif() if (BUILD_BROWSER) add_subdirectory( apps/browser ) endif() if (BUILD_MWINIIMPORTER) add_subdirectory( apps/mwiniimporter ) endif() if (BUILD_ESSIMPORTER) add_subdirectory (apps/essimporter ) endif() if (BUILD_OPENCS) add_subdirectory (apps/opencs) endif() if (BUILD_WIZARD) add_subdirectory(apps/wizard) endif() if (BUILD_NIFTEST) add_subdirectory(apps/niftest) endif(BUILD_NIFTEST) # UnitTests if (BUILD_UNITTESTS) add_subdirectory( apps/openmw_test_suite ) endif() if (BUILD_BENCHMARKS) add_subdirectory(apps/benchmarks) endif() if (WIN32) if (MSVC) if (OPENMW_MP_BUILD) set( MT_BUILD "/MP") endif() foreach( OUTPUTCONFIG ${CMAKE_CONFIGURATION_TYPES} ) string( TOUPPER ${OUTPUTCONFIG} OUTPUTCONFIG ) set( CMAKE_RUNTIME_OUTPUT_DIRECTORY_${OUTPUTCONFIG} "$(SolutionDir)$(Configuration)" ) set( CMAKE_LIBRARY_OUTPUT_DIRECTORY_${OUTPUTCONFIG} "$(ProjectDir)$(Configuration)" ) endforeach( OUTPUTCONFIG ) if (USE_DEBUG_CONSOLE AND BUILD_OPENMW) set_target_properties(tes3mp PROPERTIES LINK_FLAGS_DEBUG "/SUBSYSTEM:CONSOLE") set_target_properties(tes3mp PROPERTIES LINK_FLAGS_RELWITHDEBINFO "/SUBSYSTEM:CONSOLE") set_target_properties(tes3mp PROPERTIES COMPILE_DEFINITIONS $<$:_CONSOLE>) elseif (BUILD_OPENMW) # Turn off debug console, debug output will be written to visual studio output instead set_target_properties(tes3mp PROPERTIES LINK_FLAGS_DEBUG "/SUBSYSTEM:WINDOWS") set_target_properties(tes3mp PROPERTIES LINK_FLAGS_RELWITHDEBINFO "/SUBSYSTEM:WINDOWS") endif() if (BUILD_OPENMW) # Release builds don't use the debug console set_target_properties(tes3mp PROPERTIES LINK_FLAGS_RELEASE "/SUBSYSTEM:WINDOWS") set_target_properties(tes3mp PROPERTIES LINK_FLAGS_MINSIZEREL "/SUBSYSTEM:WINDOWS") endif() # Play a bit with the warning levels set(WARNINGS "/W4") set(WARNINGS_DISABLE 4100 # Unreferenced formal parameter (-Wunused-parameter) 4127 # Conditional expression is constant 4996 # Function was declared deprecated ) if( "${MyGUI_VERSION}" VERSION_LESS_EQUAL "3.4.0" ) set(WARNINGS_DISABLE ${WARNINGS_DISABLE} 4866 # compiler may not enforce left-to-right evaluation order for call ) endif() if( "${MyGUI_VERSION}" VERSION_LESS_EQUAL "3.4.1" ) set(WARNINGS_DISABLE ${WARNINGS_DISABLE} 4275 # non dll-interface class 'MyGUI::delegates::IDelegateUnlink' used as base for dll-interface class 'MyGUI::Widget' ) endif() foreach(d ${WARNINGS_DISABLE}) set(WARNINGS "${WARNINGS} /wd${d}") endforeach(d) set_target_properties(components PROPERTIES COMPILE_FLAGS "${WARNINGS} ${MT_BUILD}") if (BUILD_OPENMW) set_target_properties(osg-ffmpeg-videoplayer PROPERTIES COMPILE_FLAGS "${WARNINGS} ${MT_BUILD}") endif() if (MSVC_VERSION GREATER_EQUAL 1915 AND MSVC_VERSION LESS 1920) target_compile_definitions(components INTERFACE _ENABLE_EXTENDED_ALIGNED_STORAGE) endif() if (BUILD_BSATOOL) set_target_properties(bsatool PROPERTIES COMPILE_FLAGS "${WARNINGS} ${MT_BUILD}") endif() if (BUILD_ESMTOOL) set_target_properties(esmtool PROPERTIES COMPILE_FLAGS "${WARNINGS} ${MT_BUILD}") endif() if (BUILD_ESSIMPORTER) set_target_properties(openmw-essimporter PROPERTIES COMPILE_FLAGS "${WARNINGS} ${MT_BUILD}") endif() if (BUILD_LAUNCHER) set_target_properties(openmw-launcher PROPERTIES COMPILE_FLAGS "${WARNINGS} ${MT_BUILD}") endif() if (BUILD_BROWSER) set_target_properties(tes3mp-browser PROPERTIES COMPILE_FLAGS "${WARNINGS} ${MT_BUILD}") endif() if (BUILD_MWINIIMPORTER) set_target_properties(openmw-iniimporter PROPERTIES COMPILE_FLAGS "${WARNINGS} ${MT_BUILD}") endif() if (BUILD_OPENCS) set_target_properties(openmw-cs PROPERTIES COMPILE_FLAGS "${WARNINGS} ${MT_BUILD}") endif() if (BUILD_OPENMW) if (OPENMW_UNITY_BUILD) set_target_properties(tes3mp PROPERTIES COMPILE_FLAGS "${WARNINGS} ${MT_BUILD} /bigobj") else() set_target_properties(tes3mp PROPERTIES COMPILE_FLAGS "${WARNINGS} ${MT_BUILD}") endif() endif() if (BUILD_WIZARD) set_target_properties(openmw-wizard PROPERTIES COMPILE_FLAGS "${WARNINGS} ${MT_BUILD}") endif() if (BUILD_BENCHMARKS) set_target_properties(openmw_detournavigator_navmeshtilescache_benchmark 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 #set_target_properties(tes3mp PROPERTIES LINK_FLAGS_RELEASE "/SUBSYSTEM:WINDOWS") #set_target_properties(tes3mp PROPERTIES LINK_FLAGS_MINSIZEREL "/SUBSYSTEM:WINDOWS") endif() # Apple bundling if (OPENMW_OSX_DEPLOYMENT AND APPLE) if (CMAKE_VERSION VERSION_GREATER_EQUAL 3.13 AND CMAKE_VERSION VERSION_LESS 3.13.4) message(FATAL_ERROR "macOS packaging is broken in early CMake 3.13 releases, see https://gitlab.com/OpenMW/openmw/issues/4767. Please use at least 3.13.4 or an older version like 3.12.4") endif () get_property(QT_COCOA_PLUGIN_PATH TARGET Qt5::QCocoaIntegrationPlugin PROPERTY LOCATION_RELEASE) get_filename_component(QT_COCOA_PLUGIN_DIR "${QT_COCOA_PLUGIN_PATH}" DIRECTORY) get_filename_component(QT_COCOA_PLUGIN_GROUP "${QT_COCOA_PLUGIN_DIR}" NAME) get_filename_component(QT_COCOA_PLUGIN_NAME "${QT_COCOA_PLUGIN_PATH}" NAME) configure_file("${QT_COCOA_PLUGIN_PATH}" "${APP_BUNDLE_DIR}/Contents/PlugIns/${QT_COCOA_PLUGIN_GROUP}/${QT_COCOA_PLUGIN_NAME}" COPYONLY) configure_file("${OpenMW_SOURCE_DIR}/files/mac/qt.conf" "${APP_BUNDLE_DIR}/Contents/Resources/qt.conf" COPYONLY) if (BUILD_OPENCS) get_property(OPENCS_BUNDLE_NAME_TMP TARGET openmw-cs PROPERTY OUTPUT_NAME) set(OPENCS_BUNDLE_NAME "${OPENCS_BUNDLE_NAME_TMP}.app") configure_file("${QT_COCOA_PLUGIN_PATH}" "${OPENCS_BUNDLE_NAME}/Contents/PlugIns/${QT_COCOA_PLUGIN_GROUP}/${QT_COCOA_PLUGIN_NAME}" COPYONLY) configure_file("${OpenMW_SOURCE_DIR}/files/mac/qt.conf" "${OPENCS_BUNDLE_NAME}/Contents/Resources/qt.conf" COPYONLY) endif () install(DIRECTORY "${APP_BUNDLE_DIR}" USE_SOURCE_PERMISSIONS DESTINATION "." COMPONENT Runtime) set(CPACK_GENERATOR "DragNDrop") set(CPACK_PACKAGE_VERSION ${OPENMW_VERSION}) set(CPACK_PACKAGE_VERSION_MAJOR ${OPENMW_VERSION_MAJOR}) set(CPACK_PACKAGE_VERSION_MINOR ${OPENMW_VERSION_MINOR}) set(CPACK_PACKAGE_VERSION_PATCH ${OPENMW_VERSION_RELEASE}) set(INSTALLED_OPENMW_APP "\${CMAKE_INSTALL_PREFIX}/${APP_BUNDLE_NAME}") set(INSTALLED_OPENCS_APP "\${CMAKE_INSTALL_PREFIX}/${OPENCS_BUNDLE_NAME}") install(CODE " set(BU_CHMOD_BUNDLE_ITEMS ON) set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH}) include(BundleUtilities) cmake_minimum_required(VERSION 3.1) " COMPONENT Runtime) set(ABSOLUTE_PLUGINS "") set(OSGPlugins_DONT_FIND_DEPENDENCIES 1) find_package(OSGPlugins REQUIRED COMPONENTS ${USED_OSG_PLUGINS}) foreach (PLUGIN_NAME ${USED_OSG_PLUGINS}) string(TOUPPER ${PLUGIN_NAME} PLUGIN_NAME_UC) if(${PLUGIN_NAME_UC}_LIBRARY_RELEASE) set(PLUGIN_ABS ${${PLUGIN_NAME_UC}_LIBRARY_RELEASE}) elseif(${PLUGIN_NAME_UC}_LIBRARY) set(PLUGIN_ABS ${${PLUGIN_NAME_UC}_LIBRARY}) else() message(FATAL_ERROR "Can't find library file for ${PLUGIN_NAME}") # We used to construct the path manually from OSGPlugins_LIB_DIR and the plugin name. # Maybe that could be restored as a fallback? endif() set(ABSOLUTE_PLUGINS ${PLUGIN_ABS} ${ABSOLUTE_PLUGINS}) endforeach () set(OSG_PLUGIN_PREFIX_DIR "osgPlugins-${OPENSCENEGRAPH_VERSION}") # installs used plugins in bundle at given path (bundle_path must be relative to ${CMAKE_INSTALL_PREFIX}) # and returns list of install paths for all installed plugins function (install_plugins_for_bundle bundle_path plugins_var) set(RELATIVE_PLUGIN_INSTALL_BASE "${bundle_path}/Contents/PlugIns/${OSG_PLUGIN_PREFIX_DIR}") set(PLUGINS "") set(PLUGIN_INSTALL_BASE "\${CMAKE_INSTALL_PREFIX}/${RELATIVE_PLUGIN_INSTALL_BASE}") foreach (PLUGIN ${ABSOLUTE_PLUGINS}) get_filename_component(PLUGIN_RELATIVE ${PLUGIN} NAME) get_filename_component(PLUGIN_RELATIVE_WE ${PLUGIN} NAME_WE) set(PLUGIN_DYLIB_IN_BUNDLE "${PLUGIN_INSTALL_BASE}/${PLUGIN_RELATIVE}") set(PLUGINS ${PLUGINS} "${PLUGIN_DYLIB_IN_BUNDLE}") install(CODE " copy_resolved_item_into_bundle(\"${PLUGIN}\" \"${PLUGIN_DYLIB_IN_BUNDLE}\") " COMPONENT Runtime) endforeach () set(${plugins_var} ${PLUGINS} PARENT_SCOPE) endfunction (install_plugins_for_bundle) install_plugins_for_bundle("${APP_BUNDLE_NAME}" PLUGINS) install_plugins_for_bundle("${OPENCS_BUNDLE_NAME}" OPENCS_PLUGINS) set(PLUGINS ${PLUGINS} "${INSTALLED_OPENMW_APP}/Contents/PlugIns/${QT_COCOA_PLUGIN_GROUP}/${QT_COCOA_PLUGIN_NAME}") set(OPENCS_PLUGINS ${OPENCS_PLUGINS} "${INSTALLED_OPENCS_APP}/Contents/PlugIns/${QT_COCOA_PLUGIN_GROUP}/${QT_COCOA_PLUGIN_NAME}") install(CODE " function(gp_item_default_embedded_path_override item default_embedded_path_var) if (\${item} MATCHES ${OSG_PLUGIN_PREFIX_DIR}) set(path \"@executable_path/../PlugIns/${OSG_PLUGIN_PREFIX_DIR}\") set(\${default_embedded_path_var} \"\${path}\" PARENT_SCOPE) endif() endfunction() fixup_bundle(\"${INSTALLED_OPENMW_APP}\" \"${PLUGINS}\" \"\") fixup_bundle(\"${INSTALLED_OPENCS_APP}\" \"${OPENCS_PLUGINS}\" \"\") " COMPONENT Runtime) include(CPack) elseif(NOT APPLE) get_generator_is_multi_config(multi_config) if (multi_config) SET(INSTALL_SOURCE "${OpenMW_BINARY_DIR}/$") else () SET(INSTALL_SOURCE "${OpenMW_BINARY_DIR}") endif () if(WIN32) INSTALL(DIRECTORY "${INSTALL_SOURCE}/" DESTINATION "." FILES_MATCHING PATTERN "*.dll" PATTERN "deps" EXCLUDE PATTERN "apps" EXCLUDE PATTERN "CMakeFiles" EXCLUDE PATTERN "components" EXCLUDE PATTERN "docs" EXCLUDE PATTERN "extern" EXCLUDE PATTERN "files" EXCLUDE PATTERN "Testing" EXCLUDE) INSTALL(DIRECTORY "${INSTALL_SOURCE}/" DESTINATION "." CONFIGURATIONS Debug;RelWithDebInfo FILES_MATCHING PATTERN "*.pdb" PATTERN "deps" EXCLUDE PATTERN "apps" EXCLUDE PATTERN "CMakeFiles" EXCLUDE PATTERN "components" EXCLUDE PATTERN "docs" EXCLUDE PATTERN "extern" EXCLUDE PATTERN "files" EXCLUDE PATTERN "Testing" EXCLUDE) INSTALL(FILES "${INSTALL_SOURCE}/openmw.cfg.install" DESTINATION "." RENAME "openmw.cfg") INSTALL(FILES "${OpenMW_SOURCE_DIR}/CHANGELOG.md" DESTINATION "." RENAME "CHANGELOG.txt") INSTALL(FILES "${OpenMW_SOURCE_DIR}/README.md" DESTINATION "." RENAME "README.txt") INSTALL(FILES "${OpenMW_SOURCE_DIR}/LICENSE" DESTINATION "." RENAME "LICENSE.txt") INSTALL(FILES "${OpenMW_SOURCE_DIR}/files/mygui/DejaVuFontLicense.txt" DESTINATION ".") INSTALL(FILES "${INSTALL_SOURCE}/defaults.bin" DESTINATION ".") # Start of tes3mp addition INSTALL(FILES "${INSTALL_SOURCE}/tes3mp-client-default.cfg" DESTINATION ".") INSTALL(FILES "${INSTALL_SOURCE}/tes3mp-server-default.cfg" DESTINATION ".") # End of tes3mp addition INSTALL(FILES "${INSTALL_SOURCE}/gamecontrollerdb.txt" DESTINATION ".") INSTALL(DIRECTORY "${INSTALL_SOURCE}/resources" DESTINATION ".") SET(CPACK_GENERATOR "NSIS") SET(CPACK_PACKAGE_NAME "OpenMW") SET(CPACK_PACKAGE_VENDOR "OpenMW.org") SET(CPACK_PACKAGE_VERSION ${OPENMW_VERSION}) SET(CPACK_PACKAGE_VERSION_MAJOR ${OPENMW_VERSION_MAJOR}) SET(CPACK_PACKAGE_VERSION_MINOR ${OPENMW_VERSION_MINOR}) SET(CPACK_PACKAGE_VERSION_PATCH ${OPENMW_VERSION_RELEASE}) SET(CPACK_PACKAGE_EXECUTABLES "openmw;OpenMW") IF(BUILD_LAUNCHER) SET(CPACK_PACKAGE_EXECUTABLES "${CPACK_PACKAGE_EXECUTABLES};openmw-launcher;OpenMW Launcher") ENDIF(BUILD_LAUNCHER) # Start of tes3mp addition IF(BUILD_BROWSER) SET(CPACK_PACKAGE_EXECUTABLES "${CPACK_PACKAGE_EXECUTABLES};tes3mp-browser;tes3mp Launcher") ENDIF(BUILD_BROWSER) # End of tes3mp addition IF(BUILD_OPENCS) SET(CPACK_PACKAGE_EXECUTABLES "${CPACK_PACKAGE_EXECUTABLES};openmw-cs;OpenMW Construction Set") ENDIF(BUILD_OPENCS) IF(BUILD_WIZARD) SET(CPACK_PACKAGE_EXECUTABLES "${CPACK_PACKAGE_EXECUTABLES};openmw-wizard;OpenMW Wizard") ENDIF(BUILD_WIZARD) SET(CPACK_NSIS_CREATE_ICONS_EXTRA "CreateShortCut '\$SMPROGRAMS\\\\$STARTMENU_FOLDER\\\\Readme.lnk' '\$INSTDIR\\\\README.txt'") SET(CPACK_NSIS_DELETE_ICONS_EXTRA " !insertmacro MUI_STARTMENU_GETFOLDER Application $MUI_TEMP Delete \\\"$SMPROGRAMS\\\\$MUI_TEMP\\\\Readme.lnk\\\" ") SET(CPACK_RESOURCE_FILE_README "${OpenMW_SOURCE_DIR}/README.md") SET(CPACK_PACKAGE_DESCRIPTION_FILE "${OpenMW_SOURCE_DIR}/README.md") SET(CPACK_NSIS_EXECUTABLES_DIRECTORY ".") SET(CPACK_NSIS_DISPLAY_NAME "OpenMW ${OPENMW_VERSION}") SET(CPACK_NSIS_HELP_LINK "https:\\\\\\\\www.openmw.org") SET(CPACK_NSIS_URL_INFO_ABOUT "https:\\\\\\\\www.openmw.org") SET(CPACK_NSIS_INSTALLED_ICON_NAME "openmw-launcher.exe") SET(CPACK_NSIS_MUI_FINISHPAGE_RUN "openmw-launcher.exe") # Start of tes3mp change (major) SET(CPACK_NSIS_MUI_ICON "${OpenMW_SOURCE_DIR}/files/tes3mp/tes3mp.ico") SET(CPACK_NSIS_MUI_UNIICON "${OpenMW_SOURCE_DIR}/files/tes3mp/tes3mp.ico") # End of tes3mp change (major) SET(CPACK_PACKAGE_ICON "${OpenMW_SOURCE_DIR}\\\\files\\\\openmw.bmp") SET(VCREDIST32 "${OpenMW_BINARY_DIR}/vcredist_x86.exe") if(EXISTS ${VCREDIST32}) INSTALL(FILES ${VCREDIST32} DESTINATION "redist") SET(CPACK_NSIS_EXTRA_INSTALL_COMMANDS "ExecWait '\\\"$INSTDIR\\\\redist\\\\vcredist_x86.exe\\\" /q /norestart'" ) endif(EXISTS ${VCREDIST32}) SET(VCREDIST64 "${OpenMW_BINARY_DIR}/vcredist_x64.exe") if(EXISTS ${VCREDIST64}) INSTALL(FILES ${VCREDIST64} DESTINATION "redist") SET(CPACK_NSIS_EXTRA_INSTALL_COMMANDS "ExecWait '\\\"$INSTDIR\\\\redist\\\\vcredist_x64.exe\\\" /q /norestart'" ) endif(EXISTS ${VCREDIST64}) SET(OALREDIST "${OpenMW_BINARY_DIR}/oalinst.exe") if(EXISTS ${OALREDIST}) INSTALL(FILES ${OALREDIST} DESTINATION "redist") SET(CPACK_NSIS_EXTRA_INSTALL_COMMANDS "${CPACK_NSIS_EXTRA_INSTALL_COMMANDS} ExecWait '\\\"$INSTDIR\\\\redist\\\\oalinst.exe\\\" /s'" ) endif(EXISTS ${OALREDIST}) if(CMAKE_CL_64) SET(CPACK_NSIS_INSTALL_ROOT "$PROGRAMFILES64") endif() include(CPack) else(WIN32) # Linux installation # Install binaries IF(BUILD_OPENMW) # Start of tes3mp change (major) # # Use "tes3mp" as the target filename INSTALL(PROGRAMS "$" DESTINATION "${BINDIR}" ) # End of tes3mp change (major) ENDIF(BUILD_OPENMW) # Start of tes3mp addition # # Add a setting related to building the server IF(BUILD_OPENMW_MP) INSTALL(PROGRAMS "${INSTALL_SOURCE}/tes3mp-server" DESTINATION "${BINDIR}") ENDIF(BUILD_OPENMW_MP) # End of tes3mp addition IF(BUILD_LAUNCHER) INSTALL(PROGRAMS "${INSTALL_SOURCE}/openmw-launcher" DESTINATION "${BINDIR}" ) ENDIF(BUILD_LAUNCHER) # Start of tes3mp addition # # Add a setting related to building the server browser IF(BUILD_BROWSER) INSTALL(PROGRAMS "${INSTALL_SOURCE}/tes3mp-browser" DESTINATION "${BINDIR}" ) ENDIF(BUILD_BROWSER) # End of tes3mp addition IF(BUILD_BSATOOL) INSTALL(PROGRAMS "${INSTALL_SOURCE}/bsatool" DESTINATION "${BINDIR}" ) ENDIF(BUILD_BSATOOL) IF(BUILD_ESMTOOL) INSTALL(PROGRAMS "${INSTALL_SOURCE}/esmtool" DESTINATION "${BINDIR}" ) ENDIF(BUILD_ESMTOOL) IF(BUILD_NIFTEST) INSTALL(PROGRAMS "${INSTALL_SOURCE}/niftest" DESTINATION "${BINDIR}" ) ENDIF(BUILD_NIFTEST) IF(BUILD_MWINIIMPORTER) INSTALL(PROGRAMS "${INSTALL_SOURCE}/openmw-iniimporter" DESTINATION "${BINDIR}" ) ENDIF(BUILD_MWINIIMPORTER) IF(BUILD_ESSIMPORTER) INSTALL(PROGRAMS "${INSTALL_SOURCE}/openmw-essimporter" DESTINATION "${BINDIR}" ) ENDIF(BUILD_ESSIMPORTER) IF(BUILD_OPENCS) INSTALL(PROGRAMS "${INSTALL_SOURCE}/openmw-cs" DESTINATION "${BINDIR}" ) ENDIF(BUILD_OPENCS) IF(BUILD_WIZARD) INSTALL(PROGRAMS "${INSTALL_SOURCE}/openmw-wizard" DESTINATION "${BINDIR}" ) ENDIF(BUILD_WIZARD) # Install licenses INSTALL(FILES "files/mygui/DejaVuFontLicense.txt" DESTINATION "${LICDIR}" ) # Install icon and desktop file INSTALL(FILES "${OpenMW_BINARY_DIR}/org.openmw.launcher.desktop" DESTINATION "${DATAROOTDIR}/applications" COMPONENT "openmw") INSTALL(FILES "${OpenMW_SOURCE_DIR}/files/launcher/images/openmw.png" DESTINATION "${ICONDIR}" COMPONENT "openmw") INSTALL(FILES "${OpenMW_BINARY_DIR}/openmw.appdata.xml" DESTINATION "${DATAROOTDIR}/metainfo" COMPONENT "openmw") # Start of tes3mp addition IF(BUILD_BROWSER) INSTALL(FILES "${OpenMW_BINARY_DIR}/tes3mp-browser.desktop" DESTINATION "${DATAROOTDIR}/applications" COMPONENT "browser") ENDIF(BUILD_BROWSER) # End of tes3mp addition IF(BUILD_OPENCS) INSTALL(FILES "${OpenMW_BINARY_DIR}/org.openmw.cs.desktop" DESTINATION "${DATAROOTDIR}/applications" COMPONENT "opencs") INSTALL(FILES "${OpenMW_SOURCE_DIR}/files/opencs/openmw-cs.png" DESTINATION "${ICONDIR}" COMPONENT "opencs") ENDIF(BUILD_OPENCS) # Install global configuration files INSTALL(FILES "${INSTALL_SOURCE}/defaults.bin" DESTINATION "${SYSCONFDIR}" COMPONENT "openmw") INSTALL(FILES "${INSTALL_SOURCE}/openmw.cfg.install" DESTINATION "${SYSCONFDIR}" RENAME "openmw.cfg" COMPONENT "openmw") INSTALL(FILES "${INSTALL_SOURCE}/resources/version" DESTINATION "${SYSCONFDIR}" COMPONENT "openmw") INSTALL(FILES "${INSTALL_SOURCE}/gamecontrollerdb.txt" DESTINATION "${SYSCONFDIR}" COMPONENT "openmw") # Start of tes3mp addition INSTALL(FILES "${INSTALL_SOURCE}/tes3mp-client-default.cfg" DESTINATION "${SYSCONFDIR}" COMPONENT "openmw") INSTALL(FILES "${INSTALL_SOURCE}/tes3mp-server-default.cfg" DESTINATION "${SYSCONFDIR}" COMPONENT "openmw-mp") # End of tes3mp addition IF(BUILD_OPENCS) INSTALL(FILES "${INSTALL_SOURCE}/defaults-cs.bin" DESTINATION "${SYSCONFDIR}" COMPONENT "opencs") ENDIF(BUILD_OPENCS) # Install resources INSTALL(DIRECTORY "${INSTALL_SOURCE}/resources" DESTINATION "${DATADIR}" COMPONENT "Resources") INSTALL(DIRECTORY DESTINATION "${DATADIR}/data" COMPONENT "Resources") endif(WIN32) endif(OPENMW_OSX_DEPLOYMENT AND APPLE) # Doxygen Target -- simply run 'make doc' or 'make doc_pages' # output directory for 'make doc' is "${OpenMW_BINARY_DIR}/docs/Doxygen" # output directory for 'make doc_pages' is "${DOXYGEN_PAGES_OUTPUT_DIR}" if defined # or "${OpenMW_BINARY_DIR}/docs/Pages" otherwise find_package(Doxygen) if (DOXYGEN_FOUND) # determine output directory for doc_pages if (NOT DEFINED DOXYGEN_PAGES_OUTPUT_DIR) set(DOXYGEN_PAGES_OUTPUT_DIR "${OpenMW_BINARY_DIR}/docs/Pages") endif () configure_file(${OpenMW_SOURCE_DIR}/docs/Doxyfile.cmake ${OpenMW_BINARY_DIR}/docs/Doxyfile @ONLY) configure_file(${OpenMW_SOURCE_DIR}/docs/DoxyfilePages.cmake ${OpenMW_BINARY_DIR}/docs/DoxyfilePages @ONLY) add_custom_target(doc ${DOXYGEN_EXECUTABLE} ${OpenMW_BINARY_DIR}/docs/Doxyfile WORKING_DIRECTORY ${OpenMW_BINARY_DIR} COMMENT "Generating Doxygen documentation at ${OpenMW_BINARY_DIR}/docs/Doxygen" VERBATIM) add_custom_target(doc_pages ${DOXYGEN_EXECUTABLE} ${OpenMW_BINARY_DIR}/docs/DoxyfilePages WORKING_DIRECTORY ${OpenMW_BINARY_DIR} COMMENT "Generating documentation for the github-pages at ${DOXYGEN_PAGES_OUTPUT_DIR}" VERBATIM) endif () ================================================ FILE: CONTRIBUTING.md ================================================ How to contribute to OpenMW ======================= Not sure what to do with all your free time? Pick out a task from here: https://gitlab.com/OpenMW/openmw/issues Currently, we are focused on completing the MW game experience and general polishing. Features out of this scope may be approved in some cases, but you should probably start a discussion first. Note: * Tasks set to 'openmw-future' are usually out of the current scope of the project and can't be started yet. * Bugs that are not 'Confirmed' should be confirmed first. * Often, it's best to start a discussion about possible solutions before you jump into coding, especially for larger features. Aside from coding, you can also help by triaging the issues list. Check for bugs that are 'Unconfirmed' and try to confirm them on your end, working out any details that may be necessary. Check for bugs that do not conform to [Bug reporting guidelines](https://wiki.openmw.org/index.php?title=Bug_Reporting_Guidelines) and improve them to do so! There are various [Tools](https://wiki.openmw.org/index.php?title=Tools) to facilitate testing/development. Pull Request Guidelines ======================= To facilitate the review process, your pull request description should include the following, if applicable: * A link back to the bug report or forum discussion that prompted the change. Note: when linking bugs, use the syntax ```[Bug #xyz](https://gitlab.com/OpenMW/openmw/issues/#xyz)``` to create a clickable link. Writing only 'Bug #xyz' will unfortunately create a link to the Github pull request with that number instead. * Summary of the changes made * Reasoning / motivation behind the change * What testing you have carried out to verify the change Furthermore, we advise to: * Avoid stuffing unrelated commits into one pull request. As a rule of thumb, each feature and each bugfix should go into a separate PR, unless they are closely related or dependent upon each other. Small pull requests are easier to review, and are less likely to require further changes before we can merge them. A "mega" pull request with lots of unrelated commits in it is likely to get held up in review for a long time. * Feel free to submit incomplete pull requests. Even if the work can not be merged yet, pull requests are a great place to collect early feedback. Just make sure to mark it as *[Incomplete]* or *[Do not merge yet]* in the title. * If you plan on contributing often, please read the [Developer Reference](https://wiki.openmw.org/index.php?title=Developer_Reference) on our wiki, especially the [Policies and Standards](https://wiki.openmw.org/index.php?title=Policies_and_Standards). * Make sure each of your changes has a clear objective. Unnecessary changes may lead to merge conflicts, clutter the commit history and slow down review. Code formatting 'fixes' should be avoided, unless you were already changing that particular line anyway. * Reference the bug / feature ticket(s) in your commit message (e.g. 'Bug #123') to make it easier to keep track of what we changed for what reason. Our bugtracker will show those commits next to the ticket. If your commit message includes 'Fixes #123', that bug/feature will automatically be set to 'Closed' when your commit is merged. * When pulling changes from master, prefer rebase over merge. Consider using a merge if there are conflicts or for long-running PRs. Guidelines for original engine "fixes" ================================= From time to time you may be tempted to "fix" what you think was a "bug" in the original game engine. Unfortunately, the definition of what is a "bug" is not so clear. Consider that your "bug" is actually a feature unless proven otherwise: * We have no way of knowing what the original developers really intended (short of asking them, good luck with that). * What may seem like an illogical mechanic can actually be part of an attempt to balance the game. * Many people will actually like these "bugs" because that is what they remember the game for. * Exploits may be part of the fun of an open-world game - they reward knowledge with power. There are too many of them to plug them all, anyway. OpenMW, in its default configuration, is meant to be a faithful reimplementation of Morrowind, minus things like crash bugs, stability issues and severe design errors. However, we try to avoid touching anything that affects the core gameplay, the balancing of the game or introduces incompatibilities with existing mod content. That said, we may sometimes evaluate such issues on an individual basis. Common exceptions to the above would be: * Issues so glaring that they would severely limit the capabilities of the engine in the future (for example, the scripting engine not being allowed to access objects in remote cells) * Bugs where the intent is very obvious, and that have little to no balancing impact (e.g. the bug were being tired made it easier to repair items, instead of harder) * Bugs that were fixed in an official patch for Morrowind Feature additions policy ===================== We get it, you have waited so long for feature XYZ to be available in Morrowind and now that OpenMW is here you can not wait to implement your ingenious idea and share it with the world. Unfortunately, since maintaining features comes at a cost and our resources are limited, we have to be a little selective in what features we allow into the main repository. Generally: * Features should be as generic and non-redundant as possible. * Any feature that is also possible with modding should be done as a mod instead. * In the future, OpenMW plans to expand the scope of what is possible with modding, e.g. by moving certain game logic into editable scripts. * Currently, modders can edit OpenMW's GUI skins and layout XML files, although there are still a few missing hooks (e.g. scripting support) in order to make this into a powerful way of modding. * If a feature introduces new game UI strings, that reduces its chance of being accepted because we do not currently have any way of localizing these to the user's Morrowind installation language. If you are in doubt of your feature being within our scope, it is probably best to start a forum discussion first. See the [settings documentation](https://openmw.readthedocs.io/en/stable/reference/modding/settings/index.html) and [Features list](https://wiki.openmw.org/index.php?title=Features) for some examples of features that were deemed acceptable. Reviewing pull requests ======================= We welcome any help in reviewing open PRs. You don't need to be a developer to comment on new features. We also encourage ["junior" developers to review senior's work](https://pagefault.blog/2018/04/08/why-junior-devs-should-review-seniors-commits/). This review process is divided into two sections because complaining about code or style issues hardly makes sense until the functionality of the PR is deemed OK. Anyone can help with the Functionality Review while most parts of the Code Review require you to have programming experience. In addition to the checklist below, make sure to check that the Pull Request Guidelines (first half of this document) were followed. First review ============ 1. Ask for missing information or clarifications. Compare against the project's design goals and roadmap. 2. Check if the automated tests are passing. If they are not, make the PR author aware of the issue and potentially link to the error line on Travis CI or Appveyor. If the error appears unrelated to the PR and/or the master branch is failing with the same error, our CI has broken and needs to be fixed independently of any open PRs. Raise this issue on the forums, bug tracker or with the relevant maintainer. The PR can be merged in this case as long as you've built it yourself to make sure it does build. 3. Make sure that the new code has been tested thoroughly, either by asking the author or, preferably, testing yourself. In a complex project like OpenMW, it is easy to make mistakes, typos, etc. Therefore, prefer testing all code changes, no matter how trivial they look. When you have tested a PR that no one has tested so far, post a comment letting us know. 4. On long running PRs, request the author to update its description with the current state or a checklist of things left to do. Code Review =========== 1. Carefully review each line for issues the author may not have thought of, paying special attention to 'special' cases. Often, people build their code with a particular mindset and forget about other configurations or unexpected interactions. 2. If any changes are workarounds for an issue in an upstream library, make sure the issue was reported upstream so we can eventually drop the workaround when the issue is fixed and the new version of that library is a build dependency. 3. Make sure PRs do not turn into arguments about hardly related issues. If the PR author disagrees with an established part of the project (e.g. supported build environments), they should open a forum discussion or bug report and in the meantime adjust the PR to adhere to the established way, rather than leaving the PR hanging on a dispute. 4. Check if the code matches our style guidelines. 5. Check to make sure the commit history is clean. Squashing should be considered if the review process has made the commit history particularly long. Commits that don't build should be avoided because they are a nuisance for ```git bisect```. Merging ======= To be able to merge PRs, commit priviledges are required. If you do not have the priviledges, just ping someone that does have them with a short comment like "Looks good to me @user". The person to merge the PR may either use github's Merge button or if using the command line, use the ```--no-ff``` flag (so a merge commit is created, just like with Github's merge button) and include the pull request number in the commit description. Dealing with regressions ======================== The master branch should always be in a working state that is not worse than the previous release in any way. If a regression is found, the first and foremost priority should be to get the regression fixed quickly, either by reverting the change that caused it or finding a better solution. Please avoid leaving the project in the 'broken' state for an extensive period of time while proper solutions are found. If the solution takes more than a day or so then it is usually better to revert the offending change first and reapply it later when fixed. Other resources =============== [GitHub blog - how to write the perfect pull request](https://blog.github.com/2015-01-21-how-to-write-the-perfect-pull-request/) ================================================ FILE: LICENSE ================================================ GNU GENERAL PUBLIC LICENSE Version 3, 29 June 2007 Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The GNU General Public License is a free, copyleft license for software and other kinds of works. The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free software for all its users. We, the Free Software Foundation, use the GNU General Public License for most of our software; it applies also to any other work released this way by its authors. You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things. To protect your rights, we need to prevent others from denying you these rights or asking you to surrender the rights. Therefore, you have certain responsibilities if you distribute copies of the software, or if you modify it: responsibilities to respect the freedom of others. For example, if you distribute copies of such a program, whether gratis or for a fee, you must pass on to the recipients the same freedoms that you received. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. Developers that use the GNU GPL protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License giving you legal permission to copy, distribute and/or modify it. For the developers' and authors' protection, the GPL clearly explains that there is no warranty for this free software. For both users' and authors' sake, the GPL requires that modified versions be marked as changed, so that their problems will not be attributed erroneously to authors of previous versions. Some devices are designed to deny users access to install or run modified versions of the software inside them, although the manufacturer can do so. This is fundamentally incompatible with the aim of protecting users' freedom to change the software. The systematic pattern of such abuse occurs in the area of products for individuals to use, which is precisely where it is most unacceptable. Therefore, we have designed this version of the GPL to prohibit the practice for those products. If such problems arise substantially in other domains, we stand ready to extend this provision to those domains in future versions of the GPL, as needed to protect the freedom of users. Finally, every program is threatened constantly by software patents. States should not allow patents to restrict development and use of software on general-purpose computers, but in those that do, we wish to avoid the special danger that patents applied to a free program could make it effectively proprietary. To prevent this, the GPL assures that patents cannot be used to render the program non-free. The precise terms and conditions for copying, distribution and modification follow. TERMS AND CONDITIONS 0. Definitions. "This License" refers to version 3 of the GNU General Public License. "Copyright" also means copyright-like laws that apply to other kinds of works, such as semiconductor masks. "The Program" refers to any copyrightable work licensed under this License. Each licensee is addressed as "you". "Licensees" and "recipients" may be individuals or organizations. To "modify" a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission, other than the making of an exact copy. The resulting work is called a "modified version" of the earlier work or a work "based on" the earlier work. A "covered work" means either the unmodified Program or a work based on the Program. To "propagate" a work means to do anything with it that, without permission, would make you directly or secondarily liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy. Propagation includes copying, distribution (with or without modification), making available to the public, and in some countries other activities as well. To "convey" a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction with a user through a computer network, with no transfer of a copy, is not conveying. An interactive user interface displays "Appropriate Legal Notices" to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright notice, and (2) tells the user that there is no warranty for the work (except to the extent that warranties are provided), that licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a list of user commands or options, such as a menu, a prominent item in the list meets this criterion. 1. Source Code. The "source code" for a work means the preferred form of the work for making modifications to it. "Object code" means any non-source form of a work. A "Standard Interface" means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language. The "System Libraries" of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A "Major Component", in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it. The "Corresponding Source" for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those subprograms and other parts of the work. The Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source. The Corresponding Source for a work in source code form is that same work. 2. Basic Permissions. All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program. The output from running a covered work is covered by this License only if the output, given its content, constitutes a covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law. You may make, run and propagate covered works that you do not convey, without conditions so long as your license otherwise remains in force. You may convey covered works to others for the sole purpose of having them make modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with the terms of this License in conveying all material for which you do not control copyright. Those thus making or running the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that prohibit them from making any copies of your copyrighted material outside their relationship with you. Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary. 3. Protecting Users' Legal Rights From Anti-Circumvention Law. No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention of such measures. When you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users, your or third parties' legal rights to forbid circumvention of technological measures. 4. Conveying Verbatim Copies. You may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and give all recipients a copy of this License along with the Program. You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee. 5. Conveying Modified Source Versions. You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions: a) The work must carry prominent notices stating that you modified it, and giving a relevant date. b) The work must carry prominent notices stating that it is released under this License and any conditions added under section 7. This requirement modifies the requirement in section 4 to "keep intact all notices". c) You must license the entire work, as a whole, under this License to anyone who comes into possession of a copy. This License will therefore apply, along with any applicable section 7 additional terms, to the whole of the work, and all its parts, regardless of how they are packaged. This License gives no permission to license the work in any other way, but it does not invalidate such permission if you have separately received it. d) If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the Program has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so. A compilation of a covered work with other separate and independent works, which are not by their nature extensions of the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or distribution medium, is called an "aggregate" if the compilation and its resulting copyright are not used to limit the access or legal rights of the compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause this License to apply to the other parts of the aggregate. 6. Conveying Non-Source Forms. You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways: a) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange. b) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by a written offer, valid for at least three years and valid for as long as you offer spare parts or customer support for that product model, to give anyone who possesses the object code either (1) a copy of the Corresponding Source for all the software in the product that is covered by this License, on a durable physical medium customarily used for software interchange, for a price no more than your reasonable cost of physically performing this conveying of source, or (2) access to copy the Corresponding Source from a network server at no charge. c) Convey individual copies of the object code with a copy of the written offer to provide the Corresponding Source. This alternative is allowed only occasionally and noncommercially, and only if you received the object code with such an offer, in accord with subsection 6b. d) Convey the object code by offering access from a designated place (gratis or for a charge), and offer equivalent access to the Corresponding Source in the same way through the same place at no further charge. You need not require recipients to copy the Corresponding Source along with the object code. If the place to copy the object code is a network server, the Corresponding Source may be on a different server (operated by you or a third party) that supports equivalent copying facilities, provided you maintain clear directions next to the object code saying where to find the Corresponding Source. Regardless of what server hosts the Corresponding Source, you remain obligated to ensure that it is available for as long as needed to satisfy these requirements. e) Convey the object code using peer-to-peer transmission, provided you inform other peers where the object code and Corresponding Source of the work are being offered to the general public at no charge under subsection 6d. A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be included in conveying the object code work. A "User Product" is either (1) a "consumer product", which means any tangible personal property which is normally used for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a particular product received by a particular user, "normally used" refers to a typical or common use of that class of product, regardless of the status of the particular user or of the way in which the particular user actually uses, or expects or is expected to use, the product. A product is a consumer product regardless of whether the product has substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of the product. "Installation Information" for a User Product means any methods, procedures, authorization keys, or other information required to install and execute modified versions of a covered work in that User Product from a modified version of its Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code is in no case prevented or interfered with solely because modification has been made. If you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not apply if neither you nor any third party retains the ability to install modified object code on the User Product (for example, the work has been installed in ROM). The requirement to provide Installation Information does not include a requirement to continue to provide support service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product in which it has been modified or installed. Access to a network may be denied when the modification itself materially and adversely affects the operation of the network or violates the rules and protocols for communication across the network. Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format that is publicly documented (and with an implementation available to the public in source code form), and must require no special password or key for unpacking, reading or copying. 7. Additional Terms. "Additional permissions" are terms that supplement the terms of this License by making exceptions from one or more of its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were included in this License, to the extent that they are valid under applicable law. If additional permissions apply only to part of the Program, that part may be used separately under those permissions, but the entire Program remains governed by this License without regard to the additional permissions. When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or from any part of it. (Additional permissions may be written to require their own removal in certain cases when you modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have or can give appropriate copyright permission. Notwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by the copyright holders of that material) supplement the terms of this License with terms: a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or b) Requiring preservation of specified reasonable legal notices or author attributions in that material or in the Appropriate Legal Notices displayed by works containing it; or c) Prohibiting misrepresentation of the origin of that material, or requiring that modified versions of such material be marked in reasonable ways as different from the original version; or d) Limiting the use for publicity purposes of names of licensors or authors of the material; or e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or f) Requiring indemnification of licensors and authors of that material by anyone who conveys the material (or modified versions of it) with contractual assumptions of liability to the recipient, for any liability that these contractual assumptions directly impose on those licensors and authors. All other non-permissive additional terms are considered "further restrictions" within the meaning of section 10. If the Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with a term that is a further restriction, you may remove that term. If a license document contains a further restriction but permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of that license document, provided that the further restriction does not survive such relicensing or conveying. If you add terms to a covered work in accord with this section, you must place, in the relevant source files, a statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms. Additional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as exceptions; the above requirements apply either way. 8. Termination. You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to propagate or modify it is void, and will automatically terminate your rights under this License (including any patent licenses granted under the third paragraph of section 11). However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation. Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice. Termination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not qualify to receive new licenses for the same material under section 10. 9. Acceptance Not Required for Having Copies. You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a covered work, you indicate your acceptance of this License to do so. 10. Automatic Licensing of Downstream Recipients. Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third parties with this License. An "entity transaction" is a transaction transferring control of an organization, or substantially all assets of one, or subdividing an organization, or merging organizations. If propagation of a covered work results from an entity transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with reasonable efforts. You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, and you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it. 11. Patents. A "contributor" is a copyright holder who authorizes use under this License of the Program or a work on which the Program is based. The work thus licensed is called the contributor's "contributor version". A contributor's "essential patent claims" are all patent claims owned or controlled by the contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or selling its contributor version, but do not include claims that would be infringed only as a consequence of further modification of the contributor version. For purposes of this definition, "control" includes the right to grant patent sublicenses in a manner consistent with the requirements of this License. Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its contributor version. In the following three paragraphs, a "patent license" is any express agreement or commitment, however denominated, not to enforce a patent (such as an express permission to practice a patent or covenant not to sue for patent infringement). To "grant" such a patent license to a party means to make such an agreement or commitment not to enforce a patent against the party. If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not available for anyone to copy, free of charge and under the terms of this License, through a publicly available network server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a manner consistent with the requirements of this License, to extend the patent license to downstream recipients. "Knowingly relying" means you have actual knowledge that, but for the patent license, your conveying the covered work in a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in that country that you have reason to believe are valid. If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is automatically extended to all recipients of the covered work and works based on it. A patent license is "discriminatory" if it does not include within the scope of its coverage, prohibits the exercise of, or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You may not convey a covered work if you are a party to an arrangement with a third party that is in the business of distributing software, under which you make payment to the third party based on the extent of your activity of conveying the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007. Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to infringement that may otherwise be available to you under applicable patent law. 12. No Surrender of Others' Freedom. If conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot convey a covered work so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program. 13. Use with the GNU Affero General Public License. Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed under version 3 of the GNU Affero General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, but the special requirements of the GNU Affero General Public License, section 13, concerning interaction through a network will apply to the combination as such. 14. Revised Versions of this License. The Free Software Foundation may publish revised and/or new versions of the GNU General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies that a certain numbered version of the GNU General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the GNU General Public License, you may choose any version ever published by the Free Software Foundation. If the Program specifies that a proxy can decide which future versions of the GNU General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program. Later license versions may give you additional or different permissions. However, no additional obligations are imposed on any author or copyright holder as a result of your choosing to follow a later version. 15. Disclaimer of Warranty. THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 16. Limitation of Liability. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 17. Interpretation of Sections 15 and 16. If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee. END OF TERMS AND CONDITIONS ADDITIONAL TERMS APPLICABLE TO THE TES3MP GPL Source Code. The following additional terms ("Additional Terms") supplement and modify the GNU General Public License, Version 3 ("GPL") applicable to the TES3MP GPL Source Code ("TES3MP Source Code"). In addition to the terms and conditions of the GPL, the TES3MP Source Code is subject to the further restrictions below. 1. Replacement of Section 15. Section 15 of the GPL shall be deleted in its entirety and replaced with the following: "15. Disclaimer of Warranty. THE PROGRAM IS PROVIDED WITHOUT ANY WARRANTIES, WHETHER EXPRESSED OR IMPLIED, INCLUDING, WITHOUT LIMITATION, IMPLIED WARRANTIES OF FITNESS FOR A PARTICULAR PURPOSE, NON-INFRINGEMENT, TITLE AND MERCHANTABILITY. THE PROGRAM IS BEING DELIVERED OR MADE AVAILABLE "AS IS", "WITH ALL FAULTS" AND WITHOUT WARRANTY OR REPRESENTATION. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION." 2. Replacement of Section 16. Section 16 of the GPL shall be deleted in its entirety and replaced with the following: "16. LIMITATION OF LIABILITY. UNDER NO CIRCUMSTANCES SHALL ANY COPYRIGHT HOLDER OR ITS AFFILIATES, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, FOR ANY DAMAGES OR OTHER LIABILITY, INCLUDING ANY GENERAL, DIRECT, INDIRECT, SPECIAL, INCIDENTAL, CONSEQUENTIAL OR PUNITIVE DAMAGES ARISING FROM, OUT OF OR IN CONNECTION WITH THE USE OR INABILITY TO USE THE PROGRAM OR OTHER DEALINGS WITH THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), WHETHER OR NOT ANY COPYRIGHT HOLDER OR SUCH OTHER PARTY RECEIVES NOTICE OF ANY SUCH DAMAGES AND WHETHER OR NOT SUCH DAMAGES COULD HAVE BEEN FORESEEN." 3. LEGAL NOTICES; NO TRADEMARK LICENSE; ORIGIN. You must reproduce faithfully all trademark, copyright and other proprietary and legal notices on any copies of the Program or any other required author attributions. This license does not grant you rights to use any copyright holder or any other party's name, logo, or trademarks. Neither the name of the copyright holder or its affiliates, or any other party who modifies and/or conveys the Program may be used to endorse or promote products derived from this software without specific prior written permission. The origin of the Program must not be misrepresented; you must not claim that you wrote the original Program. Altered source versions must be plainly marked as such, and must not be misrepresented as being the original Program. 4. INDEMNIFICATION. IF YOU CONVEY A COVERED WORK AND AGREE WITH ANY RECIPIENT OF THAT COVERED WORK THAT YOU WILL ASSUME ANY LIABILITY FOR THAT COVERED WORK, YOU HEREBY AGREE TO INDEMNIFY, DEFEND AND HOLD HARMLESS THE OTHER LICENSORS AND AUTHORS OF THAT COVERED WORK FOR ANY DAMAEGS, DEMANDS, CLAIMS, LOSSES, CAUSES OF ACTION, LAWSUITS, JUDGMENTS EXPENSES (INCLUDING WITHOUT LIMITATION REASONABLE ATTORNEYS' FEES AND EXPENSES) OR ANY OTHER LIABLITY ARISING FROM, RELATED TO OR IN CONNECTION WITH YOUR ASSUMPTIONS OF LIABILITY. ================================================ FILE: README.md ================================================ TES3MP ====== Copyright (c) 2008-2015, OpenMW Team Copyright (c) 2016-2022, David Cernat & Stanislav Zhukov TES3MP is a project adding multiplayer functionality to [OpenMW](https://github.com/OpenMW/openmw), an open-source game engine that supports playing "The Elder Scrolls III: Morrowind" by Bethesda Softworks. * TES3MP version: 0.8.1 * OpenMW version: 0.47.0 * License: GPLv3 with additional allowed terms (see [LICENSE](https://github.com/TES3MP/TES3MP/blob/master/LICENSE) for more information) Font Licenses: * DejaVuLGCSansMono.ttf: custom (see [files/mygui/DejaVuFontLicense.txt](https://github.com/TES3MP/TES3MP/blob/master/files/mygui/DejaVuFontLicense.txt) for more information) Project status -------------- [Version changelog](https://github.com/TES3MP/TES3MP/blob/master/tes3mp-changelog.md) As of version 0.8.1, TES3MP is fully playable, providing very extensive player, NPC, world and quest synchronization, as well as state saving and loading, all of which are highly customizable via [serverside Lua scripts](https://github.com/TES3MP/CoreScripts). Remaining gameplay problems mostly relate to AI and the fact that clientside script variables need to be placed on a synchronization whitelist to avoid packet spam. TES3MP now also has a [VR branch](https://github.com/TES3MP/TES3MP/tree/0.8.1-vr) that combines its code with that of Mads Buvik Sandvei's [OpenMW VR](https://gitlab.com/madsbuvi/openmw). Donations --------------- You can benefit the project by donating on Patreon to our two developers, [David Cernat](https://www.patreon.com/davidcernat) and [Koncord](https://www.patreon.com/Koncord), as well as by supporting [OpenMW](https://openmw.org). Contributing --------------- Helping us with documentation, bug hunting and video showcases is always greatly appreciated. For code contributions, it's best to start out with modestly sized fixes and features and work your way up. There are so many different possible implementations of more major features – many of which would cause undesirable code or vision conflicts with OpenMW – that those should be talked over in advance with the existing developers before effort is spent on them. Feel free to contact the [team members](https://github.com/TES3MP/TES3MP/blob/master/tes3mp-credits.md) for any questions you might have. Getting started --------------- * [Quickstart guide](https://github.com/TES3MP/TES3MP/wiki/Quickstart-guide) * [Steam group](https://steamcommunity.com/groups/mwmulti) and its [detailed FAQ](https://steamcommunity.com/groups/mwmulti/discussions/1/353916184342480541/) * [TES3MP section on OpenMW forums](https://forum.openmw.org/viewforum.php?f=45) * [Discord server](https://discord.gg/ECJk293) * [Subreddit](https://www.reddit.com/r/tes3mp) * [Known issues and bug reports](https://github.com/TES3MP/TES3MP/issues) ================================================ FILE: apps/benchmarks/CMakeLists.txt ================================================ cmake_minimum_required(VERSION 3.11) set(BENCHMARK_ENABLE_TESTING OFF) set(BENCHMARK_ENABLE_INSTALL OFF) set(BENCHMARK_ENABLE_GTEST_TESTS OFF) set(SAVED_CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS}") string(REPLACE "-Wsuggest-override" "" CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS}") string(REPLACE "-Wundef" "" CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS}") include(FetchContent) FetchContent_Declare(benchmark URL https://github.com/google/benchmark/archive/refs/tags/v1.5.2.zip URL_HASH MD5=49395b757a7c4656d70f1328d93efd00 SOURCE_DIR fetched/benchmark ) FetchContent_MakeAvailableExcludeFromAll(benchmark) set(CMAKE_CXX_FLAGS "${SAVED_CMAKE_CXX_FLAGS}") openmw_add_executable(openmw_detournavigator_navmeshtilescache_benchmark detournavigator/navmeshtilescache.cpp) target_compile_features(openmw_detournavigator_navmeshtilescache_benchmark PRIVATE cxx_std_17) target_link_libraries(openmw_detournavigator_navmeshtilescache_benchmark benchmark::benchmark components) if (UNIX AND NOT APPLE) target_link_libraries(openmw_detournavigator_navmeshtilescache_benchmark ${CMAKE_THREAD_LIBS_INIT}) endif() ================================================ FILE: apps/benchmarks/detournavigator/navmeshtilescache.cpp ================================================ #include #include #include #include #include namespace { using namespace DetourNavigator; struct Key { osg::Vec3f mAgentHalfExtents; TilePosition mTilePosition; RecastMesh mRecastMesh; std::vector mOffMeshConnections; }; struct Item { Key mKey; NavMeshData mValue; }; template TilePosition generateTilePosition(int max, Random& random) { std::uniform_int_distribution distribution(0, max); return TilePosition(distribution(random), distribution(random)); } template osg::Vec3f generateAgentHalfExtents(float min, float max, Random& random) { std::uniform_int_distribution distribution(min, max); return osg::Vec3f(distribution(random), distribution(random), distribution(random)); } template void generateVertices(OutputIterator out, std::size_t number, Random& random) { std::uniform_real_distribution distribution(0.0, 1.0); std::generate_n(out, 3 * (number - number % 3), [&] { return distribution(random); }); } template void generateIndices(OutputIterator out, int max, std::size_t number, Random& random) { std::uniform_int_distribution distribution(0, max); std::generate_n(out, number - number % 3, [&] { return distribution(random); }); } AreaType toAreaType(int index) { switch (index) { case 0: return AreaType_null; case 1: return AreaType_water; case 2: return AreaType_door; case 3: return AreaType_pathgrid; case 4: return AreaType_ground; } return AreaType_null; } template AreaType generateAreaType(Random& random) { std::uniform_int_distribution distribution(0, 4); return toAreaType(distribution(random));; } template void generateAreaTypes(OutputIterator out, std::size_t triangles, Random& random) { std::generate_n(out, triangles, [&] { return generateAreaType(random); }); } template void generateWater(OutputIterator out, std::size_t count, Random& random) { std::uniform_real_distribution distribution(0.0, 1.0); std::generate_n(out, count, [&] { const btVector3 shift(distribution(random), distribution(random), distribution(random)); return RecastMesh::Water {1, btTransform(btMatrix3x3::getIdentity(), shift)}; }); } template void generateOffMeshConnection(OutputIterator out, std::size_t count, Random& random) { std::uniform_real_distribution distribution(0.0, 1.0); std::generate_n(out, count, [&] { const osg::Vec3f start(distribution(random), distribution(random), distribution(random)); const osg::Vec3f end(distribution(random), distribution(random), distribution(random)); return OffMeshConnection {start, end, generateAreaType(random)}; }); } template 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 std::size_t generation = std::uniform_int_distribution(0, 100)(random); const std::size_t revision = std::uniform_int_distribution(0, 10000)(random); std::vector vertices; generateVertices(std::back_inserter(vertices), triangles * 1.98, random); std::vector indices; generateIndices(std::back_inserter(indices), static_cast(vertices.size() / 3) - 1, vertices.size() * 1.53, random); std::vector areaTypes; generateAreaTypes(std::back_inserter(areaTypes), indices.size() / 3, random); std::vector water; generateWater(std::back_inserter(water), 2, random); RecastMesh recastMesh(generation, revision, std::move(indices), std::move(vertices), std::move(areaTypes), std::move(water)); std::vector offMeshConnections; generateOffMeshConnection(std::back_inserter(offMeshConnections), 300, random); return Key {agentHalfExtents, tilePosition, std::move(recastMesh), std::move(offMeshConnections)}; } constexpr std::size_t trianglesPerTile = 310; template void generateKeys(OutputIterator out, std::size_t count, Random& random) { std::generate_n(out, count, [&] { return generateKey(trianglesPerTile, random); }); } template void fillCache(OutputIterator out, Random& random, NavMeshTilesCache& cache) { std::size_t size = cache.getStats().mNavMeshCacheSize; while (true) { Key key = generateKey(trianglesPerTile, random); cache.set(key.mAgentHalfExtents, key.mTilePosition, key.mRecastMesh, key.mOffMeshConnections, NavMeshData()); *out++ = std::move(key); const std::size_t newSize = cache.getStats().mNavMeshCacheSize; if (size >= newSize) break; size = newSize; } } template void getFromFilledCache(benchmark::State& state) { NavMeshTilesCache cache(maxCacheSize); std::minstd_rand random; std::vector keys; fillCache(std::back_inserter(keys), random, cache); generateKeys(std::back_inserter(keys), keys.size() * (100 - hitPercentage) / 100, random); std::size_t n = 0; while (state.KeepRunning()) { const auto& key = keys[n++ % keys.size()]; const auto result = cache.get(key.mAgentHalfExtents, key.mTilePosition, key.mRecastMesh, key.mOffMeshConnections); benchmark::DoNotOptimize(result); } } constexpr auto getFromFilledCache_1m_100hit = getFromFilledCache<1 * 1024 * 1024, 100>; constexpr auto getFromFilledCache_4m_100hit = getFromFilledCache<4 * 1024 * 1024, 100>; constexpr auto getFromFilledCache_16m_100hit = getFromFilledCache<16 * 1024 * 1024, 100>; constexpr auto getFromFilledCache_64m_100hit = getFromFilledCache<64 * 1024 * 1024, 100>; constexpr auto getFromFilledCache_1m_70hit = getFromFilledCache<1 * 1024 * 1024, 70>; constexpr auto getFromFilledCache_4m_70hit = getFromFilledCache<4 * 1024 * 1024, 70>; constexpr auto getFromFilledCache_16m_70hit = getFromFilledCache<16 * 1024 * 1024, 70>; constexpr auto getFromFilledCache_64m_70hit = getFromFilledCache<64 * 1024 * 1024, 70>; template void setToBoundedNonEmptyCache(benchmark::State& state) { NavMeshTilesCache cache(maxCacheSize); std::minstd_rand random; std::vector keys; fillCache(std::back_inserter(keys), random, cache); generateKeys(std::back_inserter(keys), keys.size() * 2, random); std::reverse(keys.begin(), keys.end()); std::size_t n = 0; while (state.KeepRunning()) { const auto& key = keys[n++ % keys.size()]; const auto result = cache.set(key.mAgentHalfExtents, key.mTilePosition, key.mRecastMesh, key.mOffMeshConnections, NavMeshData()); benchmark::DoNotOptimize(result); } } constexpr auto setToBoundedNonEmptyCache_1m = setToBoundedNonEmptyCache<1 * 1024 * 1024>; constexpr auto setToBoundedNonEmptyCache_4m = setToBoundedNonEmptyCache<4 * 1024 * 1024>; constexpr auto setToBoundedNonEmptyCache_16m = setToBoundedNonEmptyCache<16 * 1024 * 1024>; constexpr auto setToBoundedNonEmptyCache_64m = setToBoundedNonEmptyCache<64 * 1024 * 1024>; } // namespace BENCHMARK(getFromFilledCache_1m_100hit); BENCHMARK(getFromFilledCache_4m_100hit); BENCHMARK(getFromFilledCache_16m_100hit); BENCHMARK(getFromFilledCache_64m_100hit); BENCHMARK(getFromFilledCache_1m_70hit); BENCHMARK(getFromFilledCache_4m_70hit); BENCHMARK(getFromFilledCache_16m_70hit); BENCHMARK(getFromFilledCache_64m_70hit); BENCHMARK(setToBoundedNonEmptyCache_1m); BENCHMARK(setToBoundedNonEmptyCache_4m); BENCHMARK(setToBoundedNonEmptyCache_16m); BENCHMARK(setToBoundedNonEmptyCache_64m); BENCHMARK_MAIN(); ================================================ FILE: apps/browser/CMakeLists.txt ================================================ set (CMAKE_CXX_STANDARD 14) set(BROWSER_UI ${CMAKE_SOURCE_DIR}/files/tes3mp/ui/Main.ui ${CMAKE_SOURCE_DIR}/files/tes3mp/ui/ServerInfo.ui ) set(BROWSER main.cpp MainWindow.cpp ServerModel.cpp ServerInfoDialog.cpp MySortFilterProxyModel.cpp netutils/HTTPNetwork.cpp netutils/Utils.cpp netutils/QueryClient.cpp PingUpdater.cpp PingHelper.cpp QueryHelper.cpp ${CMAKE_SOURCE_DIR}/files/tes3mp/browser.rc ) set(BROWSER_HEADER_MOC MainWindow.hpp ServerModel.hpp ServerInfoDialog.hpp MySortFilterProxyModel.hpp PingUpdater.hpp PingHelper.hpp QueryHelper.hpp ) set(BROWSER_HEADER ${BROWSER_HEADER_MOC} netutils/HTTPNetwork.hpp netutils/Utils.hpp netutils/QueryClient.hpp Types.hpp ) source_group(browser FILES ${BROWSER} ${BROWSER_HEADER}) set(QT_USE_QTGUI 1) # Set some platform specific settings if(WIN32) set(GUI_TYPE WIN32) set(QT_USE_QTMAIN TRUE) endif(WIN32) if (DESIRED_QT_VERSION MATCHES 4) message(SEND_ERROR "QT4 is not supported.") #include(${QT_USE_FILE}) #QT4_ADD_RESOURCES(RCC_SRCS ${CMAKE_SOURCE_DIR}/files/launcher/launcher.qrc) #QT4_WRAP_CPP(MOC_SRCS ${BROWSER_HEADER_MOC}) #QT4_WRAP_UI(UI_HDRS ${BROWSER_UI}) else() QT5_ADD_RESOURCES(RCC_SRCS ${CMAKE_SOURCE_DIR}/files/launcher/launcher.qrc) QT5_WRAP_CPP(MOC_SRCS ${BROWSER_HEADER_MOC}) QT5_WRAP_UI(UI_HDRS ${BROWSER_UI}) endif() include_directories(${CMAKE_CURRENT_BINARY_DIR}) if(NOT WIN32) include_directories(${LIBUNSHIELD_INCLUDE_DIR}) endif(NOT WIN32) # Main executable add_executable(tes3mp-browser ${GUI_TYPE} ${BROWSER} ${BROWSER_HEADER} ${RCC_SRCS} ${MOC_SRCS} ${UI_HDRS} ) if (WIN32) INSTALL(TARGETS tes3mp-browser RUNTIME DESTINATION ".") endif (WIN32) target_link_libraries(tes3mp-browser ${SDL2_LIBRARY_ONLY} ${RakNet_LIBRARY} components ) if (DESIRED_QT_VERSION MATCHES 4) # target_link_libraries(tes3mp-browser ${QT_QTGUI_LIBRARY} ${QT_QTCORE_LIBRARY}) # if(WIN32) # target_link_libraries(tes3mp-browser ${QT_QTMAIN_LIBRARY}) # endif(WIN32) else() qt5_use_modules(tes3mp-browser Widgets Core) endif() if (BUILD_WITH_CODE_COVERAGE) add_definitions (--coverage) target_link_libraries(tes3mp-browser gcov) endif() ================================================ FILE: apps/browser/MainWindow.cpp ================================================ #include "MainWindow.hpp" #include "QueryHelper.hpp" #include "PingHelper.hpp" #include "ServerInfoDialog.hpp" #include "components/files/configurationmanager.hpp" #include #include #include #include #include #include using namespace Process; using namespace std; MainWindow::MainWindow(QWidget *parent) { setupUi(this); mGameInvoker = new ProcessInvoker(); browser = new ServerModel; favorites = new ServerModel; proxyModel = new MySortFilterProxyModel(this); proxyModel->setSourceModel(browser); tblServerBrowser->setModel(proxyModel); tblFavorites->setModel(proxyModel); // Remove Favorites tab while it remains broken tabWidget->removeTab(1); tblServerBrowser->hideColumn(ServerData::ADDR); tblFavorites->hideColumn(ServerData::ADDR); PingHelper::Get().SetModel((ServerModel*)proxyModel->sourceModel()); queryHelper = new QueryHelper(proxyModel->sourceModel()); connect(queryHelper, &QueryHelper::started, [this](){actionRefresh->setEnabled(false);}); connect(queryHelper, &QueryHelper::finished, [this](){actionRefresh->setEnabled(true);}); connect(tabWidget, SIGNAL(currentChanged(int)), this, SLOT(tabSwitched(int))); connect(actionAdd, SIGNAL(triggered(bool)), this, SLOT(addServer())); connect(actionAdd_by_IP, SIGNAL(triggered(bool)), this, SLOT(addServerByIP())); connect(actionDelete, SIGNAL(triggered(bool)), this, SLOT(deleteServer())); connect(actionRefresh, SIGNAL(triggered(bool)), queryHelper, SLOT(refresh())); connect(actionPlay, SIGNAL(triggered(bool)), this, SLOT(play())); connect(tblServerBrowser, SIGNAL(clicked(QModelIndex)), this, SLOT(serverSelected())); connect(tblFavorites, SIGNAL(clicked(QModelIndex)), this, SLOT(serverSelected())); connect(tblFavorites, SIGNAL(doubleClicked(QModelIndex)), this, SLOT(play())); connect(tblServerBrowser, SIGNAL(doubleClicked(QModelIndex)), this, SLOT(play())); connect(cBoxNotFull, SIGNAL(toggled(bool)), this, SLOT(notFullSwitch(bool))); connect(cBoxWithPlayers, SIGNAL(toggled(bool)), this, SLOT(havePlayersSwitch(bool))); connect(cBBoxWOPass, SIGNAL(toggled(bool)), this, SLOT(noPasswordSwitch(bool))); connect(comboLatency, SIGNAL(currentIndexChanged(int)), this, SLOT(maxLatencyChanged(int))); connect(leGamemode, SIGNAL(textChanged(const QString &)), this, SLOT(gamemodeChanged(const QString &))); loadFavorites(); queryHelper->refresh(); } MainWindow::~MainWindow() { delete mGameInvoker; } void MainWindow::addServerAndUpdate(const QString &addr) { favorites->insertRow(0); QModelIndex mi = favorites->index(0, ServerData::ADDR); favorites->setData(mi, addr, Qt::EditRole); //NetController::get()->updateInfo(favorites, mi); //QueryClient::Update(RakNet::SystemAddress()) /*auto data = QueryClient::Get().Query(); if (data.empty()) return; transform(data.begin(), data.end(), back_inserter());*/ } void MainWindow::addServer() { int id = tblServerBrowser->selectionModel()->currentIndex().row(); if (id >= 0) { int sourceId = proxyModel->mapToSource(proxyModel->index(id, ServerData::ADDR)).row(); favorites->myData.push_back(browser->myData[sourceId]); } } void MainWindow::addServerByIP() { bool ok; QString text = QInputDialog::getText(this, tr("Add Server by address"), tr("Address:"), QLineEdit::Normal, "", &ok); if (ok && !text.isEmpty()) addServerAndUpdate(text); } void MainWindow::deleteServer() { if (tabWidget->currentIndex() != 1) return; int id = tblFavorites->selectionModel()->currentIndex().row(); if (id >= 0) { int sourceId = proxyModel->mapToSource(proxyModel->index(id, ServerData::ADDR)).row(); favorites->removeRow(sourceId); if (favorites->myData.isEmpty()) { actionPlay->setEnabled(false); actionDelete->setEnabled(false); } } } void MainWindow::play() { QTableView *curTable = tabWidget->currentIndex() ? tblFavorites : tblServerBrowser; int id = curTable->selectionModel()->currentIndex().row(); if (id < 0) return; ServerModel *sm = ((ServerModel*)proxyModel->sourceModel()); int sourceId = proxyModel->mapToSource(proxyModel->index(id, ServerData::ADDR)).row(); ServerInfoDialog infoDialog(sm->myData[sourceId].addr, this); if (!infoDialog.exec()) return; if (!infoDialog.isUpdated()) return; QStringList arguments; arguments.append(QLatin1String("--connect=") + sm->myData[sourceId].addr.toLatin1()); if (sm->myData[sourceId].GetPassword() == 1) { bool ok; QString passw = QInputDialog::getText(this, tr("Connecting to: ") + sm->myData[sourceId].addr, tr("Password: "), QLineEdit::Password, "", &ok); if (!ok) return; arguments.append(QLatin1String("--password=") + passw.toLatin1()); } if (mGameInvoker->startProcess(QLatin1String("tes3mp"), arguments, true)) return qApp->quit(); } void MainWindow::tabSwitched(int index) { if (index == 0) { proxyModel->setSourceModel(browser); actionDelete->setEnabled(false); } else { proxyModel->setSourceModel(favorites); } actionPlay->setEnabled(false); actionAdd->setEnabled(false); } void MainWindow::serverSelected() { actionPlay->setEnabled(true); if (tabWidget->currentIndex() == 0) actionAdd->setEnabled(true); if (tabWidget->currentIndex() == 1) actionDelete->setEnabled(true); } void MainWindow::closeEvent(QCloseEvent *event) { Files::ConfigurationManager cfgMgr; QString cfgPath = QString::fromStdString((cfgMgr.getUserConfigPath() / "favorites.dat").string()); QJsonArray saveData; for (auto server : favorites->myData) saveData.push_back(server.addr); QFile file(cfgPath); if (!file.open(QIODevice::WriteOnly)) { qDebug() << "Cannot save " << cfgPath; return; } file.write(QJsonDocument(saveData).toJson()); file.close(); } void MainWindow::loadFavorites() { Files::ConfigurationManager cfgMgr; QString cfgPath = QString::fromStdString((cfgMgr.getUserConfigPath() / "favorites.dat").string()); QFile file(cfgPath); if (!file.open(QIODevice::ReadOnly)) { qDebug() << "Cannot open " << cfgPath; return; } QJsonDocument jsonDoc(QJsonDocument::fromJson(file.readAll())); for (auto server : jsonDoc.array()) addServerAndUpdate(server.toString()); file.close(); } void MainWindow::notFullSwitch(bool state) { proxyModel->filterFullServer(state); } void MainWindow::havePlayersSwitch(bool state) { proxyModel->filterEmptyServers(state); } void MainWindow::noPasswordSwitch(bool state) { proxyModel->filterPassworded(state); } void MainWindow::maxLatencyChanged(int index) { int maxLatency = index * 50; proxyModel->pingLessThan(maxLatency); } void MainWindow::gamemodeChanged(const QString &text) { proxyModel->setFilterFixedString(text); proxyModel->setFilterKeyColumn(ServerData::MODNAME); } ================================================ FILE: apps/browser/MainWindow.hpp ================================================ #ifndef NEWLAUNCHER_MAIN_HPP #define NEWLAUNCHER_MAIN_HPP #include "ui_Main.h" #include "ServerModel.hpp" #include "MySortFilterProxyModel.hpp" #include class QueryHelper; class MainWindow : public QMainWindow, private Ui::MainWindow { Q_OBJECT public: explicit MainWindow(QWidget *parent = nullptr); ~MainWindow() override; protected: void closeEvent(QCloseEvent * event) Q_DECL_OVERRIDE; void addServerAndUpdate(const QString &addr); protected slots: void tabSwitched(int index); void addServer(); void addServerByIP(); void deleteServer(); void play(); void serverSelected(); void notFullSwitch(bool state); void havePlayersSwitch(bool state); void noPasswordSwitch(bool state); void maxLatencyChanged(int index); void gamemodeChanged(const QString &text); private: QueryHelper *queryHelper; Process::ProcessInvoker *mGameInvoker; ServerModel *browser, *favorites; MySortFilterProxyModel *proxyModel; void loadFavorites(); }; #endif //NEWLAUNCHER_MAIN_HPP ================================================ FILE: apps/browser/MySortFilterProxyModel.cpp ================================================ #include "MySortFilterProxyModel.hpp" #include "ServerModel.hpp" #include #include bool MySortFilterProxyModel::filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const { QModelIndex pingIndex = sourceModel()->index(sourceRow, ServerData::PING, sourceParent); QModelIndex plIndex = sourceModel()->index(sourceRow, ServerData::PLAYERS, sourceParent); QModelIndex maxPlIndex = sourceModel()->index(sourceRow, ServerData::MAX_PLAYERS, sourceParent); QModelIndex passwordIndex = sourceModel()->index(sourceRow, ServerData::PASSW, sourceParent); bool pingOk; int ping = sourceModel()->data(pingIndex).toInt(&pingOk); int players = sourceModel()->data(plIndex).toInt(); int maxPlayers = sourceModel()->data(maxPlIndex).toInt(); if (maxPing > 0 && (ping == -1 || ping > maxPing || !pingOk)) return false; if (filterEmpty && players == 0) return false; if (filterFull && players >= maxPlayers) return false; if(filterPasswEnabled && sourceModel()->data(passwordIndex).toString() == "Yes") return false; return QSortFilterProxyModel::filterAcceptsRow(sourceRow, sourceParent); } bool MySortFilterProxyModel::lessThan(const QModelIndex &source_left, const QModelIndex &source_right) const { if(sortColumn() == ServerData::PING) { bool valid; int pingright = sourceModel()->data(source_right).toInt(&valid); pingright = valid ? pingright : PING_UNREACHABLE; int pingleft = sourceModel()->data(source_left).toInt(&valid); pingleft = valid ? pingleft : PING_UNREACHABLE; return pingleft < pingright; } else return QSortFilterProxyModel::lessThan(source_left, source_right); } MySortFilterProxyModel::MySortFilterProxyModel(QObject *parent) : QSortFilterProxyModel(parent) { filterEmpty = false; filterFull = false; filterPasswEnabled = false; maxPing = 0; setSortCaseSensitivity(Qt::CaseSensitivity::CaseInsensitive); } void MySortFilterProxyModel::filterEmptyServers(bool state) { filterEmpty = state; invalidateFilter(); } void MySortFilterProxyModel::filterFullServer(bool state) { filterFull = state; invalidateFilter(); } void MySortFilterProxyModel::pingLessThan(int maxPing) { this->maxPing = maxPing; invalidateFilter(); } void MySortFilterProxyModel::filterPassworded(bool state) { filterPasswEnabled = state; invalidateFilter(); } ================================================ FILE: apps/browser/MySortFilterProxyModel.hpp ================================================ #ifndef OPENMW_MYSORTFILTERPROXYMODEL_HPP #define OPENMW_MYSORTFILTERPROXYMODEL_HPP #include class MySortFilterProxyModel : public QSortFilterProxyModel { Q_OBJECT protected: bool filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const Q_DECL_FINAL; bool lessThan(const QModelIndex &source_left, const QModelIndex &source_right) const Q_DECL_FINAL; public: explicit MySortFilterProxyModel(QObject *parent); void filterFullServer(bool state); void filterEmptyServers(bool state); void filterPassworded(bool state); void pingLessThan(int maxPing); private: bool filterEmpty, filterFull, filterPasswEnabled; int maxPing; }; #endif //OPENMW_MYSORTFILTERPROXYMODEL_HPP ================================================ FILE: apps/browser/PingHelper.cpp ================================================ #include "PingHelper.hpp" #include "ServerModel.hpp" #include #include "PingUpdater.hpp" void PingHelper::Add(int row, const AddrPair &addrPair) { pingUpdater->addServer(row, addrPair); if (!pingThread->isRunning()) pingThread->start(); } void PingHelper::Reset() { //if (pingThread->isRunning()) Stop(); } void PingHelper::Stop() { emit pingUpdater->stop(); } void PingHelper::SetModel(QAbstractTableModel *model) { this->model = model; } void PingHelper::update(int row, unsigned ping) { model->setData(model->index(row, ServerData::PING), ping); } PingHelper &PingHelper::Get() { static PingHelper helper; return helper; } PingHelper::PingHelper() : QObject() { pingThread = new QThread; pingUpdater = new PingUpdater; pingUpdater->moveToThread(pingThread); connect(pingThread, SIGNAL(started()), pingUpdater, SLOT(process())); connect(pingUpdater, SIGNAL(start()), pingThread, SLOT(start())); connect(pingUpdater, SIGNAL(finished()), pingThread, SLOT(quit())); connect(this, SIGNAL(stop()), pingUpdater, SLOT(stop())); //connect(pingUpdater, SIGNAL(finished()), pingUpdater, SLOT(deleteLater())); connect(pingUpdater, SIGNAL(updateModel(int, unsigned)), this, SLOT(update(int, unsigned))); } ================================================ FILE: apps/browser/PingHelper.hpp ================================================ #ifndef OPENMW_PINGHELPER_HPP #define OPENMW_PINGHELPER_HPP #include #include #include #include "Types.hpp" class PingUpdater; class PingHelper : public QObject { Q_OBJECT public: void Reset(); void Add(int row, const AddrPair &addrPair); void Stop(); void SetModel(QAbstractTableModel *model); //void UpdateImmedialy(PingUpdater::AddrPair addrPair); static PingHelper &Get(); PingHelper(const PingHelper&) = delete; PingHelper& operator=(const PingHelper&) = delete; private: PingHelper(); signals: void stop(); public slots: void update(int row, unsigned ping); private: QThread *pingThread; PingUpdater *pingUpdater; QAbstractTableModel *model; }; #endif //OPENMW_PINGHELPER_HPP ================================================ FILE: apps/browser/PingUpdater.cpp ================================================ #include "PingUpdater.hpp" #include "netutils/Utils.hpp" #include #include #include void PingUpdater::stop() { servers.clear(); run = false; } void PingUpdater::addServer(int row, const AddrPair &addr) { servers.push_back({row, addr}); run = true; emit start(); } void PingUpdater::process() { while (run) { if (servers.count() == 0) { QThread::msleep(1000); if (servers.count() == 0) { qDebug() << "PingUpdater stopped due to inactivity"; run = false; continue; } } ServerRow server = servers.back(); servers.pop_back(); unsigned ping = PingRakNetServer(server.second.first.toLatin1(), server.second.second); qDebug() << "Pong from" << server.second.first + "|" + QString::number(server.second.second) << ":" << ping << "ms" << "Sizeof servers: " << servers.size(); emit updateModel(server.first, ping); } emit finished(); } ================================================ FILE: apps/browser/PingUpdater.hpp ================================================ #ifndef OPENMW_PINGUPDATER_HPP #define OPENMW_PINGUPDATER_HPP #include #include #include "Types.hpp" class PingUpdater : public QObject { Q_OBJECT public: void addServer(int row, const AddrPair &addrPair); public slots: void stop(); void process(); signals: void start(); void updateModel(int row, unsigned ping); void finished(); private: QVector servers; bool run; }; #endif //OPENMW_PINGUPDATER_HPP ================================================ FILE: apps/browser/QueryHelper.cpp ================================================ #include "netutils/QueryClient.hpp" #include "netutils/Utils.hpp" #include "QueryHelper.hpp" #include "PingHelper.hpp" QueryUpdate *queryUpdate; QueryHelper::QueryHelper(QAbstractItemModel *model) { qRegisterMetaType("QueryData"); queryThread = new QThread; queryUpdate = new QueryUpdate; _model = model; connect(queryThread, SIGNAL(started()), queryUpdate, SLOT(process())); connect(queryUpdate, SIGNAL(finished()), queryThread, SLOT(quit())); connect(queryUpdate, &QueryUpdate::finished, [this](){emit finished();}); connect(queryUpdate, SIGNAL(updateModel(const QString&, unsigned short, const QueryData&)), this, SLOT(update(const QString&, unsigned short, const QueryData&))); queryUpdate->moveToThread(queryThread); } void QueryHelper::refresh() { if (!queryThread->isRunning()) { _model->removeRows(0, _model->rowCount()); PingHelper::Get().Stop(); queryThread->start(); emit started(); } } void QueryHelper::terminate() { queryThread->terminate(); } void QueryHelper::update(const QString &addr, unsigned short port, const QueryData& data) { ServerModel *model = ((ServerModel*)_model); model->insertRow(model->rowCount()); int row = model->rowCount() - 1; QModelIndex mi = model->index(row, ServerData::ADDR); model->setData(mi, addr + ":" + QString::number(port)); mi = model->index(row, ServerData::PLAYERS); model->setData(mi, (int)data.players.size()); mi = model->index(row, ServerData::MAX_PLAYERS); model->setData(mi, data.GetMaxPlayers()); mi = model->index(row, ServerData::HOSTNAME); model->setData(mi, data.GetName()); mi = model->index(row, ServerData::MODNAME); model->setData(mi, data.GetGameMode()); mi = model->index(row, ServerData::VERSION); model->setData(mi, data.GetVersion()); mi = model->index(row, ServerData::PASSW); model->setData(mi, data.GetPassword() == 1); mi = model->index(row, ServerData::PING); model->setData(mi, PING_UNREACHABLE); PingHelper::Get().Add(row, {addr, port}); } void QueryUpdate::process() { auto data = QueryClient::Get().Query(); if (QueryClient::Get().Status() != ID_MASTER_QUERY) { emit finished(); return; } for (const auto &server : data) emit updateModel(server.first.ToString(false), server.first.GetPort(), server.second); emit finished(); } ================================================ FILE: apps/browser/QueryHelper.hpp ================================================ #ifndef OPENMW_QUERYHELPER_HPP #define OPENMW_QUERYHELPER_HPP #include #include #include #include Q_DECLARE_METATYPE(QueryData) class QueryHelper : public QObject { Q_OBJECT public: explicit QueryHelper(QAbstractItemModel *model); public slots: void refresh(); void terminate(); private slots: void update(const QString &addr, unsigned short port, const QueryData& data); signals: void finished(); void started(); private: QThread *queryThread; QAbstractItemModel *_model; }; class QueryUpdate : public QObject { friend class QueryHelper; Q_OBJECT signals: void finished(); void updateModel(const QString &addr, unsigned short port, const QueryData& data); public slots: void process(); }; #endif //OPENMW_QUERYHELPER_HPP ================================================ FILE: apps/browser/ServerInfoDialog.cpp ================================================ #include #include "qdebug.h" #include "ServerInfoDialog.hpp" #include #include #include using namespace std; using namespace RakNet; ThrWorker::ThrWorker(ServerInfoDialog *dialog, QString addr, unsigned short port): addr(std::move(addr)), port(port), stopped(false) { this->dialog = dialog; } void ThrWorker::process() { stopped = false; auto newSD = QueryClient::Get().Update(SystemAddress(addr.toUtf8(), port)); if (dialog != nullptr) dialog->setData(newSD); stopped = true; emit finished(); } ServerInfoDialog::ServerInfoDialog(const QString &addr, QWidget *parent): QDialog(parent) { setupUi(this); refreshThread = new QThread; QStringList list = addr.split(':'); worker = new ThrWorker(this, list[0].toLatin1(), list[1].toUShort()); worker->moveToThread(refreshThread); connect(refreshThread, SIGNAL(started()), worker, SLOT(process())); connect(worker, SIGNAL(finished()), refreshThread, SLOT(quit())); connect(refreshThread, SIGNAL(finished()), this, SLOT(refresh())); connect(btnRefresh, &QPushButton::clicked, [this]{ if (!refreshThread->isRunning()) refreshThread->start(); }); } ServerInfoDialog::~ServerInfoDialog() { worker->dialog = nullptr; if (!refreshThread->isRunning()) refreshThread->terminate(); } bool ServerInfoDialog::isUpdated() { return sd.first != UNASSIGNED_SYSTEM_ADDRESS; } void ServerInfoDialog::setData(std::pair &newSD) { sd = newSD; } void ServerInfoDialog::refresh() { if (sd.first != UNASSIGNED_SYSTEM_ADDRESS) { leAddr->setText(sd.first.ToString(true, ':')); lblName->setText(sd.second.GetName()); int ping = PingRakNetServer(sd.first.ToString(false), sd.first.GetPort()); lblPing->setNum(ping); btnConnect->setDisabled(ping == PING_UNREACHABLE); listPlayers->clear(); for (const auto &player : sd.second.players) listPlayers->addItem(QString::fromStdString(player)); listPlugins->clear(); for (const auto &plugin : sd.second.plugins) listPlugins->addItem(QString::fromStdString(plugin.name)); listRules->clear(); const static vector defaultRules {"gamemode", "maxPlayers", "name", "passw", "players", "version"}; for (auto &rule : sd.second.rules) { if (::find(defaultRules.begin(), defaultRules.end(), rule.first) != defaultRules.end()) continue; QString ruleStr = QString::fromStdString(rule.first) + " : "; if (rule.second.type == 's') ruleStr += QString::fromStdString(rule.second.str); else ruleStr += QString::number(rule.second.val); listRules->addItem(ruleStr); } lblPlayers->setText(QString::number(sd.second.players.size()) + " / " + QString::number(sd.second.GetMaxPlayers())); } } int ServerInfoDialog::exec() { if (!refreshThread->isRunning()) refreshThread->start(); return QDialog::exec(); } ================================================ FILE: apps/browser/ServerInfoDialog.hpp ================================================ #ifndef NEWLAUNCHER_SERVERINFODIALOG_HPP #define NEWLAUNCHER_SERVERINFODIALOG_HPP #include "ui_ServerInfo.h" #include #include #include class ThrWorker; class ServerInfoDialog : public QDialog, public Ui::Dialog { Q_OBJECT public: explicit ServerInfoDialog(const QString &addr, QWidget *parent = nullptr); ~ServerInfoDialog() override; bool isUpdated(); void setData(std::pair &newSD); public slots: void refresh(); int exec() Q_DECL_OVERRIDE; private: QThread *refreshThread; ThrWorker* worker; std::pair sd; }; class ThrWorker: public QObject { friend class ServerInfoDialog; Q_OBJECT public: ThrWorker(ServerInfoDialog *dialog, QString addr, unsigned short port); public slots: void process(); signals: void finished(); private: QString addr; unsigned short port; bool stopped; ServerInfoDialog *dialog; }; #endif //NEWLAUNCHER_SERVERINFODIALOG_HPP ================================================ FILE: apps/browser/ServerModel.cpp ================================================ #include #include "ServerModel.hpp" #include #include ServerModel::ServerModel(QObject *parent) : QAbstractTableModel(parent) { } /*QHash ServerModel::roleNames() const { return roles; }*/ QVariant ServerModel::data(const QModelIndex &index, int role) const { if (!index.isValid()) return QVariant(); if (index.row() < 0 || index.row() > myData.size()) return QVariant(); const ServerData &sd = myData.at(index.row()); if (role == Qt::DisplayRole) { QVariant var; switch (index.column()) { case ServerData::ADDR: var = sd.addr; break; case ServerData::PASSW: var = (int)(sd.rules.at("passw").val) == 1 ? "Yes" : "No"; break; case ServerData::VERSION: var = QString(sd.rules.at("version").str.c_str()); break; case ServerData::PLAYERS: var = (int) sd.rules.at("players").val; break; case ServerData::MAX_PLAYERS: var = (int) sd.rules.at("maxPlayers").val; break; case ServerData::HOSTNAME: var = QString(sd.rules.at("name").str.c_str()); break; case ServerData::PING: var = sd.ping == PING_UNREACHABLE ? QVariant("Unreachable") : sd.ping; break; case ServerData::MODNAME: if (sd.rules.at("gamemode").str == "") var = "default"; else var = QString(sd.rules.at("gamemode").str.c_str()); break; } return var; } return QVariant(); } QVariant ServerModel::headerData(int section, Qt::Orientation orientation, int role) const { QVariant var; if (orientation == Qt::Horizontal) { if (role == Qt::SizeHintRole) { /*if (section == ServerData::HOSTNAME) var = QSize(200, 25);*/ } else if (role == Qt::DisplayRole) { switch (section) { case ServerData::ADDR: var = "Address"; break; case ServerData::PASSW: var = "Password"; break; case ServerData::VERSION: var = "Version"; break; case ServerData::HOSTNAME: var = "Host name"; break; case ServerData::PLAYERS: var = "Players"; break; case ServerData::MAX_PLAYERS: var = "Max players"; break; case ServerData::PING: var = "Ping"; break; case ServerData::MODNAME: var = "Game mode"; } } } return var; } int ServerModel::rowCount(const QModelIndex &parent) const { return myData.size(); } int ServerModel::columnCount(const QModelIndex &parent) const { return ServerData::LAST; } bool ServerModel::setData(const QModelIndex &index, const QVariant &value, int role) { if (index.isValid() && role == Qt::EditRole) { int row = index.row(); int col = index.column(); ServerData &sd = myData[row]; bool ok = true; switch(col) { case ServerData::ADDR: sd.addr = value.toString(); ok = !sd.addr.isEmpty(); break; case ServerData::PASSW: sd.SetPassword(value.toBool()); break; case ServerData::VERSION: sd.SetVersion(value.toString().toUtf8()); ok = !sd.addr.isEmpty(); break; case ServerData::PLAYERS: sd.SetPlayers(value.toInt(&ok)); break; case ServerData::MAX_PLAYERS: sd.SetMaxPlayers(value.toInt(&ok)); break; case ServerData::HOSTNAME: sd.SetName(value.toString().toUtf8()); ok = !sd.addr.isEmpty(); break; case ServerData::PING: sd.ping = value.toInt(&ok); break; case ServerData::MODNAME: sd.SetGameMode(value.toString().toUtf8()); break; default: return false; } if (ok) emit(dataChanged(index, index)); return true; } return false; } bool ServerModel::insertRows(int position, int count, const QModelIndex &index) { Q_UNUSED(index); beginInsertRows(QModelIndex(), position, position + count - 1); myData.insert(position, count, {}); endInsertRows(); return true; } bool ServerModel::removeRows(int position, int count, const QModelIndex &parent) { if (count == 0) return false; beginRemoveRows(parent, position, position + count - 1); myData.erase(myData.begin()+position, myData.begin() + position + count); endRemoveRows(); return true; } QModelIndex ServerModel::index(int row, int column, const QModelIndex &parent) const { QModelIndex index = QAbstractTableModel::index(row, column, parent); return index; } ================================================ FILE: apps/browser/ServerModel.hpp ================================================ #ifndef SERVERMODEL_FONTMODEL_HPP #define SERVERMODEL_FONTMODEL_HPP #include #include #include #include #include struct ServerData : public QueryData { QString addr; int ping; enum IDS { ADDR, HOSTNAME, PLAYERS, MAX_PLAYERS, PASSW, MODNAME, PING, VERSION, LAST }; }; class ServerModel: public QAbstractTableModel { Q_OBJECT public: explicit ServerModel(QObject *parent = nullptr); QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const Q_DECL_FINAL; int rowCount(const QModelIndex &parent = QModelIndex()) const Q_DECL_FINAL; int columnCount(const QModelIndex &parent) const Q_DECL_FINAL; bool setData(const QModelIndex &index, const QVariant &value, int role = Qt::EditRole) Q_DECL_FINAL; bool insertRows(int row, int count, const QModelIndex &index = QModelIndex()) Q_DECL_FINAL; bool removeRows(int row, int count, const QModelIndex &parent = QModelIndex()) Q_DECL_FINAL; QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const Q_DECL_FINAL; QModelIndex index(int row, int column, const QModelIndex &parent = QModelIndex()) const Q_DECL_FINAL; public: //QHash roles; QVector myData; }; #endif //SERVERMODEL_FONTMODEL_HPP ================================================ FILE: apps/browser/Types.hpp ================================================ #ifndef OPENMW_TYPES_HPP #define OPENMW_TYPES_HPP #include #include typedef QPair AddrPair; typedef QPair ServerRow; #endif //OPENMW_TYPES_HPP ================================================ FILE: apps/browser/main.cpp ================================================ #include #include #include #include #include "MainWindow.hpp" std::string loadSettings (Settings::Manager & settings) { Files::ConfigurationManager mCfgMgr; // Create the settings manager and load default settings file const std::string localdefault = (mCfgMgr.getLocalPath() / "tes3mp-client-default.cfg").string(); const std::string globaldefault = (mCfgMgr.getGlobalPath() / "tes3mp-client-default.cfg").string(); // prefer local if (boost::filesystem::exists(localdefault)) settings.loadDefault(localdefault, false); else if (boost::filesystem::exists(globaldefault)) settings.loadDefault(globaldefault, false); else throw std::runtime_error ("No default settings file found! Make sure the file \"tes3mp-client-default.cfg\" was properly installed."); // load user settings if they exist const std::string settingspath = (mCfgMgr.getUserConfigPath() / "tes3mp-client.cfg").string(); if (boost::filesystem::exists(settingspath)) settings.loadUser(settingspath); return settingspath; } int main(int argc, char *argv[]) { Settings::Manager mgr; loadSettings(mgr); std::string addr = mgr.getString("address", "Master"); int port = mgr.getInt("port", "Master"); // Is this an attempt to connect to the official master server at the old port? If so, // redirect it to the correct port for the currently used fork of RakNet if (Misc::StringUtils::ciEqual(addr, "master.tes3mp.com") && port == 25560) port = 25561; // initialize resources, if needed // Q_INIT_RESOURCE(resfile); QueryClient::Get().SetServer(addr, port); QApplication app(argc, argv); MainWindow d; d.show(); return app.exec(); } ================================================ FILE: apps/browser/netutils/HTTPNetwork.cpp ================================================ #include #include #include #include #include #include "HTTPNetwork.hpp" using namespace RakNet; HTTPNetwork::HTTPNetwork(std::string addr, unsigned short port) : address(addr), port(port) { httpConnection = HTTPConnection2::GetInstance(); tcpInterface = new TCPInterface; tcpInterface->Start(0, 64); tcpInterface->AttachPlugin(httpConnection); } HTTPNetwork::~HTTPNetwork() { delete tcpInterface; } std::string HTTPNetwork::answer() { RakNet::SystemAddress sa; RakNet::Packet *packet; RakNet::SystemAddress hostReceived; RakNet::RakString response; RakNet::RakString transmitted, hostTransmitted; int contentOffset = 0; while (true) { // This is kind of crappy, but for TCP plugins, always do HasCompletedConnectionAttempt, // then Receive(), then HasFailedConnectionAttempt(),HasLostConnection() sa = tcpInterface->HasCompletedConnectionAttempt(); if (sa != RakNet::UNASSIGNED_SYSTEM_ADDRESS) printf("Connected to master server: %s\n", sa.ToString()); sa = tcpInterface->HasFailedConnectionAttempt(); if (sa != RakNet::UNASSIGNED_SYSTEM_ADDRESS) { printf("Failed to connect to master server: %s\n", sa.ToString()); return "FAIL_CONNECT"; } sa = tcpInterface->HasLostConnection(); if (sa != RakNet::UNASSIGNED_SYSTEM_ADDRESS) { printf("Lost connection to master server: %s\n", sa.ToString()); return "LOST_CONNECTION"; } for (packet = tcpInterface->Receive(); packet; tcpInterface->DeallocatePacket( packet), packet = tcpInterface->Receive()); if (httpConnection->GetResponse(transmitted, hostTransmitted, response, hostReceived, contentOffset)) { if (contentOffset < 0) return "NO_CONTENT"; // no content tcpInterface->CloseConnection(sa); return (response.C_String() + contentOffset); } RakSleep(30); } } std::string HTTPNetwork::getData(const char *uri) { RakNet::RakString createRequest = RakNet::RakString::FormatForGET(uri); if (!httpConnection->TransmitRequest(createRequest, address.c_str(), port)) return "UNKNOWN_ADDRESS"; return answer(); } std::string HTTPNetwork::getDataPOST(const char *uri, const char* body, const char* contentType) { RakNet::RakString createRequest = RakNet::RakString::FormatForPOST(uri, contentType, body); if (!httpConnection->TransmitRequest(createRequest, address.c_str(), port)) return "UNKNOWN_ADDRESS"; return answer(); } std::string HTTPNetwork::getDataPUT(const char *uri, const char* body, const char* contentType) { RakNet::RakString createRequest = RakNet::RakString::FormatForPUT(uri, contentType, body); if (!httpConnection->TransmitRequest(createRequest, address.c_str(), port)) return "UNKNOWN_ADDRESS"; return answer(); } ================================================ FILE: apps/browser/netutils/HTTPNetwork.hpp ================================================ #ifndef NEWLAUNCHER_HTTPNETWORK_HPP #define NEWLAUNCHER_HTTPNETWORK_HPP #include namespace RakNet { class TCPInterface; class HTTPConnection2; } class HTTPNetwork { public: HTTPNetwork(std::string addr, unsigned short port); ~HTTPNetwork(); std::string getData(const char *uri); std::string getDataPOST(const char *uri, const char* body, const char* contentType = "application/json"); std::string getDataPUT(const char *uri, const char* body, const char* contentType = "application/json"); protected: RakNet::TCPInterface *tcpInterface; RakNet::HTTPConnection2 *httpConnection; std::string address; unsigned short port; std::string answer(); }; #endif //NEWLAUNCHER_HTTPNETWORK_HPP ================================================ FILE: apps/browser/netutils/QueryClient.cpp ================================================ #include "QueryClient.hpp" #include #include #include #include #include using namespace RakNet; using namespace std; using namespace mwmp; QueryClient::QueryClient() { peer = RakPeerInterface::GetInstance(); pmq = new PacketMasterQuery(peer); pmu = new PacketMasterUpdate(peer); RakNet::SocketDescriptor sd; peer->Startup(8, &sd, 1); status = -1; } QueryClient::~QueryClient() { delete pmq; delete pmu; RakPeerInterface::DestroyInstance(peer); } void QueryClient::SetServer(const string &addr, unsigned short port) { masterAddr = SystemAddress(addr.c_str(), port); } QueryClient &QueryClient::Get() { static QueryClient myInstance; return myInstance; } map QueryClient::Query() { map query; BitStream bs; bs.Write((unsigned char) (ID_MASTER_QUERY)); qDebug() << "Locking mutex in QueryClient::Query()"; mxServers.lock(); status = -1; int attempts = 3; do { if (Connect() == IS_NOT_CONNECTED) { qDebug() << "Unlocking mutex in QueryClient::Query()"; mxServers.unlock(); return query; } int code = peer->Send(&bs, HIGH_PRIORITY, RELIABLE_ORDERED, CHANNEL_MASTER, masterAddr, false); if (code == 0) { qDebug() << "Unlocking mutex in QueryClient::Query()"; mxServers.unlock(); return query; } pmq->SetServers(&query); status = GetAnswer(ID_MASTER_QUERY); RakSleep(100); } while(status != ID_MASTER_QUERY && attempts-- > 0); if(status != ID_MASTER_QUERY) qDebug() << "Getting query was failed"; qDebug() << "Unlocking mutex in QueryClient::Query()"; peer->CloseConnection(masterAddr, true); mxServers.unlock(); qDebug() <<"Answer" << (status == ID_MASTER_QUERY ? "ok." : "wrong."); return query; } pair QueryClient::Update(const RakNet::SystemAddress &addr) { qDebug() << "Locking mutex in QueryClient::Update(RakNet::SystemAddress addr)"; pair server; BitStream bs; bs.Write((unsigned char) (ID_MASTER_UPDATE)); bs.Write(addr); mxServers.lock(); status = -1; int attempts = 3; pmu->SetServer(&server); do { if (Connect() == IS_NOT_CONNECTED) { qDebug() << IS_NOT_CONNECTED; qDebug() << "Unlocking mutex in QueryClient::Update(RakNet::SystemAddress addr)"; mxServers.unlock(); return server; } peer->Send(&bs, HIGH_PRIORITY, RELIABLE_ORDERED, CHANNEL_MASTER, masterAddr, false); status = GetAnswer(ID_MASTER_UPDATE); RakSleep(100); } while(status != ID_MASTER_UPDATE && attempts-- > 0); if(status != ID_MASTER_UPDATE) qDebug() << "Getting update was failed"; peer->CloseConnection(masterAddr, true); qDebug() << "Unlocking mutex in QueryClient::Update(RakNet::SystemAddress addr)"; mxServers.unlock(); return server; } MASTER_PACKETS QueryClient::GetAnswer(MASTER_PACKETS waitingPacket) { RakNet::Packet *packet; bool update = true; unsigned char pid = 0; int id = -1; while (update) { for (packet = peer->Receive(); packet; peer->DeallocatePacket(packet), packet = peer->Receive()) { BitStream data(packet->data, packet->length, false); pmq->SetReadStream(&data); pmu->SetReadStream(&data); data.Read(pid); switch(pid) { case ID_CONNECTION_LOST: qDebug() << "ID_CONNECTION_LOST"; case ID_DISCONNECTION_NOTIFICATION: qDebug() << "Disconnected"; update = false; break; case ID_MASTER_QUERY: qDebug() << "ID_MASTER_QUERY"; if (waitingPacket == ID_MASTER_QUERY) pmq->Read(); else qDebug() << "Got wrong packet"; update = false; id = pid; break; case ID_MASTER_UPDATE: qDebug() << "ID_MASTER_UPDATE"; if (waitingPacket == ID_MASTER_UPDATE) pmu->Read(); else qDebug() << "Got wrong packet"; update = false; id = pid; break; case ID_MASTER_ANNOUNCE: qDebug() << "ID_MASTER_ANNOUNCE"; update = false; id = pid; break; case ID_CONNECTION_REQUEST_ACCEPTED: qDebug() << "ID_CONNECTION_REQUEST_ACCEPTED"; break; default: break; } } RakSleep(500); } return (MASTER_PACKETS)(id); } ConnectionState QueryClient::Connect() { ConnectionAttemptResult car = peer->Connect(masterAddr.ToString(false), masterAddr.GetPort(), TES3MP_MASTERSERVER_PASSW, strlen(TES3MP_MASTERSERVER_PASSW), nullptr, 0, 5, 500); while (true) { ConnectionState state = peer->GetConnectionState(masterAddr); switch (state) { case IS_CONNECTED: qDebug() << "Connected"; return IS_CONNECTED; case IS_NOT_CONNECTED: case IS_DISCONNECTED: case IS_SILENTLY_DISCONNECTING: case IS_DISCONNECTING: { qDebug() << "Cannot connect to the master server. Code:"<< state; return IS_NOT_CONNECTED; } case IS_PENDING: case IS_CONNECTING: qDebug() << "Pending"; break; } RakSleep(500); } } int QueryClient::Status() { return status; } ================================================ FILE: apps/browser/netutils/QueryClient.hpp ================================================ #ifndef OPENMW_QUERYCLIENT_HPP #define OPENMW_QUERYCLIENT_HPP #include #include #include #include #include #include class QueryClient { public: QueryClient(QueryClient const &) = delete; QueryClient(QueryClient &&) = delete; QueryClient &operator=(QueryClient const &) = delete; QueryClient &operator=(QueryClient &&) = delete; static QueryClient &Get(); void SetServer(const std::string &addr, unsigned short port); std::map Query(); std::pair Update(const RakNet::SystemAddress &addr); int Status(); private: RakNet::ConnectionState Connect(); MASTER_PACKETS GetAnswer(MASTER_PACKETS packet); protected: QueryClient(); ~QueryClient(); private: int status; RakNet::RakPeerInterface *peer; RakNet::SystemAddress masterAddr; mwmp::PacketMasterQuery *pmq; mwmp::PacketMasterUpdate *pmu; std::pair server; std::mutex mxServers; }; #endif //OPENMW_QUERYCLIENT_HPP ================================================ FILE: apps/browser/netutils/Utils.cpp ================================================ #include #include #include #include #include #include #include "Utils.hpp" using namespace std; unsigned int PingRakNetServer(const char *addr, unsigned short port) { RakNet::Packet *packet; bool done = false; RakNet::TimeMS time = PING_UNREACHABLE; RakNet::SocketDescriptor socketDescriptor{0, ""}; RakNet::RakPeerInterface *peer = RakNet::RakPeerInterface::GetInstance(); peer->Startup(1, &socketDescriptor, 1); if (!peer->Ping(addr, port, false)) return time; RakNet::TimeMS start = RakNet::GetTimeMS(); while (!done) { RakNet::TimeMS now = RakNet::GetTimeMS(); if (now - start >= PING_UNREACHABLE) break; packet = peer->Receive(); if (!packet) continue; switch (packet->data[0]) { case ID_DISCONNECTION_NOTIFICATION: case ID_CONNECTION_LOST: done = true; break; case ID_CONNECTED_PING: case ID_UNCONNECTED_PONG: { RakNet::BitStream bsIn(&packet->data[1], packet->length, false); bsIn.Read(time); time = now - time; done = true; break; } default: break; } peer->DeallocatePacket(packet); } peer->Shutdown(0); RakNet::RakPeerInterface::DestroyInstance(peer); return time > PING_UNREACHABLE ? PING_UNREACHABLE : time; } ServerExtendedData getExtendedData(const char *addr, unsigned short port) { ServerExtendedData data; RakNet::SocketDescriptor socketDescriptor = {0, ""}; RakNet::RakPeerInterface *peer = RakNet::RakPeerInterface::GetInstance(); peer->Startup(1, &socketDescriptor, 1); stringstream sstr; sstr << TES3MP_VERSION; sstr << TES3MP_PROTO_VERSION; std::string msg; if (peer->Connect(addr, port, sstr.str().c_str(), (int)(sstr.str().size()), nullptr, 0, 3, 500, 0) != RakNet::CONNECTION_ATTEMPT_STARTED) msg = "Connection attempt failed.\n"; int queue = 0; while (queue == 0) { for (RakNet::Packet *packet = peer->Receive(); packet; peer->DeallocatePacket( packet), packet = peer->Receive()) { switch (packet->data[0]) { case ID_CONNECTION_ATTEMPT_FAILED: { msg = "Connection failed.\n" "Either the IP address is wrong or a firewall on either system is blocking\n" "UDP packets on the port you have chosen."; queue = -1; break; } case ID_INVALID_PASSWORD: { msg = "Connection failed.\n" "The client or server is outdated.\n"; queue = -1; break; } case ID_CONNECTION_REQUEST_ACCEPTED: { msg = "Connection accepted.\n"; queue = 1; break; } case ID_DISCONNECTION_NOTIFICATION: throw runtime_error("ID_DISCONNECTION_NOTIFICATION.\n"); case ID_CONNECTION_BANNED: throw runtime_error("ID_CONNECTION_BANNED.\n"); case ID_CONNECTION_LOST: throw runtime_error("ID_CONNECTION_LOST.\n"); default: printf("Connection message with identifier %i has arrived in initialization.\n", packet->data[0]); } } } puts(msg.c_str()); if (queue == -1) // connection is failed return data; { RakNet::BitStream bs; bs.Write((unsigned char) (ID_USER_PACKET_ENUM + 1)); peer->Send(&bs, HIGH_PRIORITY, RELIABLE_ORDERED, 0, RakNet::UNASSIGNED_SYSTEM_ADDRESS, true); } RakNet::Packet *packet; bool done = false; while (!done) { for (packet = peer->Receive(); packet; peer->DeallocatePacket(packet), packet = peer->Receive()) { if (packet->data[0] == (ID_USER_PACKET_ENUM + 1)) { RakNet::BitStream bs(packet->data, packet->length, false); bs.IgnoreBytes(1); size_t length = 0; bs.Read(length); for (size_t i = 0; i < length; i++) { RakNet::RakString str; bs.Read(str); data.players.emplace_back(str.C_String()); } bs.Read(length); for (size_t i = 0; i < length; i++) { RakNet::RakString str; bs.Read(str); data.plugins.emplace_back(str.C_String()); } done = true; } } } peer->Shutdown(0); RakSleep(10); RakNet::RakPeerInterface::DestroyInstance(peer); return data; } ================================================ FILE: apps/browser/netutils/Utils.hpp ================================================ #ifndef NEWLAUNCHER_PING_HPP #define NEWLAUNCHER_PING_HPP #include #include #define PING_UNREACHABLE 999 unsigned int PingRakNetServer(const char *addr, unsigned short port); struct ServerExtendedData { std::vector players; std::vector plugins; }; ServerExtendedData getExtendedData(const char *addr, unsigned short port); #endif //NEWLAUNCHER_PING_HPP ================================================ FILE: apps/bsatool/CMakeLists.txt ================================================ set(BSATOOL bsatool.cpp ) source_group(apps\\bsatool FILES ${BSATOOL}) # Main executable openmw_add_executable(bsatool ${BSATOOL} ) target_link_libraries(bsatool ${Boost_PROGRAM_OPTIONS_LIBRARY} ${Boost_FILESYSTEM_LIBRARY} components ) if (BUILD_WITH_CODE_COVERAGE) add_definitions (--coverage) target_link_libraries(bsatool gcov) endif() ================================================ FILE: apps/bsatool/bsatool.cpp ================================================ #include #include #include #include #include #include #include #include #define BSATOOL_VERSION 1.1 // Create local aliases for brevity namespace bpo = boost::program_options; namespace bfs = boost::filesystem; struct Arguments { std::string mode; std::string filename; std::string extractfile; std::string addfile; std::string outdir; bool longformat; bool fullpath; }; bool parseOptions (int argc, char** argv, Arguments &info) { bpo::options_description desc("Inspect and extract files from Bethesda BSA archives\n\n" "Usages:\n" " bsatool list [-l] archivefile\n" " List the files presents in the input archive.\n\n" " bsatool extract [-f] archivefile [file_to_extract] [output_directory]\n" " Extract a file from the input archive.\n\n" " bsatool extractall archivefile [output_directory]\n" " Extract all files from the input archive.\n\n" " bsatool add [-a] archivefile file_to_add\n" " Add a file to the input archive.\n\n" " bsatool create [-c] archivefile\n" " Create an archive.\n\n" "Allowed options"); desc.add_options() ("help,h", "print help message.") ("version,v", "print version information and quit.") ("long,l", "Include extra information in archive listing.") ("full-path,f", "Create directory hierarchy on file extraction " "(always true for extractall).") ; // input-file is hidden and used as a positional argument bpo::options_description hidden("Hidden Options"); hidden.add_options() ( "mode,m", bpo::value(), "bsatool mode") ( "input-file,i", bpo::value< std::vector >(), "input file") ; bpo::positional_options_description p; p.add("mode", 1).add("input-file", 3); // there might be a better way to do this bpo::options_description all; all.add(desc).add(hidden); bpo::variables_map variables; try { bpo::parsed_options valid_opts = bpo::command_line_parser(argc, argv) .options(all).positional(p).run(); bpo::store(valid_opts, variables); } catch(std::exception &e) { std::cout << "ERROR parsing arguments: " << e.what() << "\n\n" << desc << std::endl; return false; } bpo::notify(variables); if (variables.count ("help")) { std::cout << desc << std::endl; return false; } if (variables.count ("version")) { std::cout << "BSATool version " << BSATOOL_VERSION << std::endl; return false; } if (!variables.count("mode")) { std::cout << "ERROR: no mode specified!\n\n" << desc << std::endl; return false; } info.mode = variables["mode"].as(); if (!(info.mode == "list" || info.mode == "extract" || info.mode == "extractall" || info.mode == "add" || info.mode == "create")) { std::cout << std::endl << "ERROR: invalid mode \"" << info.mode << "\"\n\n" << desc << std::endl; return false; } if (!variables.count("input-file")) { std::cout << "\nERROR: missing BSA archive\n\n" << desc << std::endl; return false; } info.filename = variables["input-file"].as< std::vector >()[0]; // Default output to the working directory info.outdir = "."; if (info.mode == "extract") { if (variables["input-file"].as< std::vector >().size() < 2) { std::cout << "\nERROR: file to extract unspecified\n\n" << desc << std::endl; return false; } if (variables["input-file"].as< std::vector >().size() > 1) info.extractfile = variables["input-file"].as< std::vector >()[1]; if (variables["input-file"].as< std::vector >().size() > 2) info.outdir = variables["input-file"].as< std::vector >()[2]; } else if (info.mode == "add") { if (variables["input-file"].as< std::vector >().size() < 1) { std::cout << "\nERROR: file to add unspecified\n\n" << desc << std::endl; return false; } if (variables["input-file"].as< std::vector >().size() > 1) info.addfile = variables["input-file"].as< std::vector >()[1]; } else if (variables["input-file"].as< std::vector >().size() > 1) info.outdir = variables["input-file"].as< std::vector >()[1]; info.longformat = variables.count("long") != 0; info.fullpath = variables.count("full-path") != 0; return true; } int list(std::unique_ptr& bsa, Arguments& info); int extract(std::unique_ptr& bsa, Arguments& info); int extractAll(std::unique_ptr& bsa, Arguments& info); int add(std::unique_ptr& bsa, Arguments& info); int main(int argc, char** argv) { try { Arguments info; if(!parseOptions (argc, argv, info)) return 1; // Open file std::unique_ptr bsa; Bsa::BsaVersion bsaVersion = Bsa::CompressedBSAFile::detectVersion(info.filename); if (bsaVersion == Bsa::BSAVER_COMPRESSED) bsa = std::make_unique(Bsa::CompressedBSAFile()); else bsa = std::make_unique(Bsa::BSAFile()); if (info.mode == "create") { bsa->open(info.filename); return 0; } bsa->open(info.filename); if (info.mode == "list") return list(bsa, info); else if (info.mode == "extract") return extract(bsa, info); else if (info.mode == "extractall") return extractAll(bsa, info); else if (info.mode == "add") return add(bsa, info); else { std::cout << "Unsupported mode. That is not supposed to happen." << std::endl; return 1; } } catch (std::exception& e) { std::cerr << "ERROR reading BSA archive\nDetails:\n" << e.what() << std::endl; return 2; } } int list(std::unique_ptr& bsa, Arguments& info) { // List all files const Bsa::BSAFile::FileList &files = bsa->getList(); for (const auto& file : files) { if(info.longformat) { // Long format std::ios::fmtflags f(std::cout.flags()); std::cout << std::setw(50) << std::left << file.name(); std::cout << std::setw(8) << std::left << std::dec << file.fileSize; std::cout << "@ 0x" << std::hex << file.offset << std::endl; std::cout.flags(f); } else std::cout << file.name() << std::endl; } return 0; } int extract(std::unique_ptr& bsa, Arguments& info) { std::string archivePath = info.extractfile; Misc::StringUtils::replaceAll(archivePath, "/", "\\"); std::string extractPath = info.extractfile; Misc::StringUtils::replaceAll(extractPath, "\\", "/"); if (!bsa->exists(archivePath.c_str())) { std::cout << "ERROR: file '" << archivePath << "' not found\n"; std::cout << "In archive: " << info.filename << std::endl; return 3; } // Get the target path (the path the file will be extracted to) bfs::path relPath (extractPath); bfs::path outdir (info.outdir); bfs::path target; if (info.fullpath) target = outdir / relPath; else target = outdir / relPath.filename(); // Create the directory hierarchy bfs::create_directories(target.parent_path()); bfs::file_status s = bfs::status(target.parent_path()); if (!bfs::is_directory(s)) { std::cout << "ERROR: " << target.parent_path() << " is not a directory." << std::endl; return 3; } // Get a stream for the file to extract Files::IStreamPtr stream = bsa->getFile(archivePath.c_str()); bfs::ofstream out(target, std::ios::binary); // Write the file to disk std::cout << "Extracting " << info.extractfile << " to " << target << std::endl; out << stream->rdbuf(); out.close(); return 0; } int extractAll(std::unique_ptr& bsa, Arguments& info) { for (const auto &file : bsa->getList()) { std::string extractPath(file.name()); Misc::StringUtils::replaceAll(extractPath, "\\", "/"); // Get the target path (the path the file will be extracted to) bfs::path target (info.outdir); target /= extractPath; // Create the directory hierarchy bfs::create_directories(target.parent_path()); bfs::file_status s = bfs::status(target.parent_path()); if (!bfs::is_directory(s)) { std::cout << "ERROR: " << target.parent_path() << " is not a directory." << std::endl; return 3; } // Get a stream for the file to extract // (inefficient because getFile iter on the list again) Files::IStreamPtr data = bsa->getFile(file.name()); bfs::ofstream out(target, std::ios::binary); // Write the file to disk std::cout << "Extracting " << target << std::endl; out << data->rdbuf(); out.close(); } return 0; } int add(std::unique_ptr& bsa, Arguments& info) { boost::filesystem::fstream stream(info.addfile, std::ios_base::binary | std::ios_base::out | std::ios_base::in); bsa->addFile(info.addfile, stream); return 0; } ================================================ FILE: apps/doc.hpp ================================================ // Note: This is not a regular source file. /// \defgroup apps Applications ================================================ FILE: apps/essimporter/CMakeLists.txt ================================================ set(ESSIMPORTER_FILES main.cpp importer.cpp importplayer.cpp importnpcc.cpp importcrec.cpp importcellref.cpp importacdt.cpp importinventory.cpp importklst.cpp importcntc.cpp importgame.cpp importinfo.cpp importdial.cpp importques.cpp importjour.cpp importscri.cpp importscpt.cpp importproj.cpp importsplm.cpp importercontext.cpp converter.cpp convertacdt.cpp convertnpcc.cpp convertinventory.cpp convertcrec.cpp convertcntc.cpp convertscri.cpp convertscpt.cpp convertplayer.cpp ) openmw_add_executable(openmw-essimporter ${ESSIMPORTER_FILES} ) target_link_libraries(openmw-essimporter ${Boost_PROGRAM_OPTIONS_LIBRARY} ${Boost_FILESYSTEM_LIBRARY} components ) if (BUILD_WITH_CODE_COVERAGE) add_definitions (--coverage) target_link_libraries(openmw-essimporter gcov) endif() if (WIN32) INSTALL(TARGETS openmw-essimporter RUNTIME DESTINATION ".") endif(WIN32) ================================================ FILE: apps/essimporter/convertacdt.cpp ================================================ #include #include #include #include #include "convertacdt.hpp" namespace ESSImport { int translateDynamicIndex(int mwIndex) { if (mwIndex == 1) return 2; else if (mwIndex == 2) return 1; return mwIndex; } void convertACDT (const ACDT& acdt, ESM::CreatureStats& cStats) { for (int i=0; i<3; ++i) { int writeIndex = translateDynamicIndex(i); cStats.mDynamic[writeIndex].mBase = acdt.mDynamic[i][1]; cStats.mDynamic[writeIndex].mMod = acdt.mDynamic[i][1]; cStats.mDynamic[writeIndex].mCurrent = acdt.mDynamic[i][0]; } for (int i=0; i<8; ++i) { cStats.mAttributes[i].mBase = static_cast(acdt.mAttributes[i][1]); cStats.mAttributes[i].mMod = static_cast(acdt.mAttributes[i][0]); cStats.mAttributes[i].mCurrent = static_cast(acdt.mAttributes[i][0]); } cStats.mGoldPool = acdt.mGoldPool; cStats.mTalkedTo = (acdt.mFlags & TalkedToPlayer) != 0; cStats.mAttacked = (acdt.mFlags & Attacked) != 0; } void convertACSC (const ACSC& acsc, ESM::CreatureStats& cStats) { cStats.mDead = (acsc.mFlags & Dead) != 0; } void convertNpcData (const ActorData& actorData, ESM::NpcStats& npcStats) { for (int i=0; i::max(); state.mScriptedAnims.push_back(scriptedAnim); } else // TODO: Handle 0xFF index, which seems to be used for finished animations. std::cerr << "unknown animation group index: " << static_cast(anis.mGroupIndex) << std::endl; } } ================================================ FILE: apps/essimporter/convertacdt.hpp ================================================ #ifndef OPENMW_ESSIMPORT_CONVERTACDT_H #define OPENMW_ESSIMPORT_CONVERTACDT_H #include #include #include #include #include "importacdt.hpp" namespace ESSImport { // OpenMW uses Health,Magicka,Fatigue, MW uses Health,Fatigue,Magicka int translateDynamicIndex(int mwIndex); void convertACDT (const ACDT& acdt, ESM::CreatureStats& cStats); void convertACSC (const ACSC& acsc, ESM::CreatureStats& cStats); void convertNpcData (const ActorData& actorData, ESM::NpcStats& npcStats); void convertANIS (const ANIS& anis, ESM::AnimationState& state); } #endif ================================================ FILE: apps/essimporter/convertcntc.cpp ================================================ #include "convertcntc.hpp" #include "convertinventory.hpp" namespace ESSImport { void convertCNTC(const CNTC &cntc, ESM::ContainerState &state) { convertInventory(cntc.mInventory, state.mInventory); } } ================================================ FILE: apps/essimporter/convertcntc.hpp ================================================ #ifndef OPENMW_ESSIMPORT_CONVERTCNTC_H #define OPENMW_ESSIMPORT_CONVERTCNTC_H #include "importcntc.hpp" #include namespace ESSImport { void convertCNTC(const CNTC& cntc, ESM::ContainerState& state); } #endif ================================================ FILE: apps/essimporter/convertcrec.cpp ================================================ #include "convertcrec.hpp" #include "convertinventory.hpp" namespace ESSImport { void convertCREC(const CREC &crec, ESM::CreatureState &state) { convertInventory(crec.mInventory, state.mInventory); } } ================================================ FILE: apps/essimporter/convertcrec.hpp ================================================ #ifndef OPENMW_ESSIMPORT_CONVERTCREC_H #define OPENMW_ESSIMPORT_CONVERTCREC_H #include "importcrec.hpp" #include namespace ESSImport { void convertCREC(const CREC& crec, ESM::CreatureState& state); } #endif ================================================ FILE: apps/essimporter/converter.cpp ================================================ #include "converter.hpp" #include #include #include #include #include #include #include "convertcrec.hpp" #include "convertcntc.hpp" #include "convertscri.hpp" namespace { void convertImage(char* data, int size, int width, int height, GLenum pf, const std::string& out) { osg::ref_ptr image (new osg::Image); image->allocateImage(width, height, 1, pf, GL_UNSIGNED_BYTE); memcpy(image->data(), data, size); image->flipVertical(); osgDB::writeImageFile(*image, out); } void convertCellRef(const ESSImport::CellRef& cellref, ESM::ObjectState& objstate) { objstate.mEnabled = cellref.mEnabled; objstate.mPosition = cellref.mPos; objstate.mRef.mRefNum = cellref.mRefNum; if (cellref.mDeleted) objstate.mCount = 0; convertSCRI(cellref.mSCRI, objstate.mLocals); objstate.mHasLocals = !objstate.mLocals.mVariables.empty(); if (cellref.mHasANIS) convertANIS(cellref.mANIS, objstate.mAnimationState); } bool isIndexedRefId(const std::string& indexedRefId) { if (indexedRefId.size() <= 8) return false; if (indexedRefId.find_first_not_of("0123456789") == std::string::npos) return false; // entirely numeric refid, this is a reference to // a dynamically created record e.g. player-enchanted weapon std::string index = indexedRefId.substr(indexedRefId.size()-8); return index.find_first_not_of("0123456789ABCDEF") == std::string::npos; } void splitIndexedRefId(const std::string& indexedRefId, int& refIndex, std::string& refId) { std::stringstream stream; stream << std::hex << indexedRefId.substr(indexedRefId.size()-8,8); stream >> refIndex; refId = indexedRefId.substr(0,indexedRefId.size()-8); } int convertActorId(const std::string& indexedRefId, ESSImport::Context& context) { if (isIndexedRefId(indexedRefId)) { int refIndex; std::string refId; splitIndexedRefId(indexedRefId, refIndex, refId); auto it = context.mActorIdMap.find(std::make_pair(refIndex, refId)); if (it == context.mActorIdMap.end()) return -1; return it->second; } else if (indexedRefId == "PlayerSaveGame") { return context.mPlayer.mObject.mCreatureStats.mActorId; } return -1; } } namespace ESSImport { struct MAPH { unsigned int size; unsigned int value; }; void ConvertFMAP::read(ESM::ESMReader &esm) { MAPH maph; esm.getHNT(maph, "MAPH"); std::vector data; esm.getSubNameIs("MAPD"); esm.getSubHeader(); data.resize(esm.getSubSize()); esm.getExact(&data[0], data.size()); mGlobalMapImage = new osg::Image; mGlobalMapImage->allocateImage(maph.size, maph.size, 1, GL_RGB, GL_UNSIGNED_BYTE); memcpy(mGlobalMapImage->data(), &data[0], data.size()); // to match openmw size // FIXME: filtering? mGlobalMapImage->scaleImage(maph.size*2, maph.size*2, 1, GL_UNSIGNED_BYTE); } void ConvertFMAP::write(ESM::ESMWriter &esm) { int numcells = mGlobalMapImage->s() / 18; // NB truncating, doesn't divide perfectly // with the 512x512 map the game has by default int cellSize = mGlobalMapImage->s()/numcells; // Note the upper left corner of the (0,0) cell should be at (width/2, height/2) mContext->mGlobalMapState.mBounds.mMinX = -numcells/2; mContext->mGlobalMapState.mBounds.mMaxX = (numcells-1)/2; mContext->mGlobalMapState.mBounds.mMinY = -(numcells-1)/2; mContext->mGlobalMapState.mBounds.mMaxY = numcells/2; osg::ref_ptr image2 (new osg::Image); int width = cellSize*numcells; int height = cellSize*numcells; std::vector data; data.resize(width*height*4, 0); image2->allocateImage(width, height, 1, GL_RGBA, GL_UNSIGNED_BYTE); memcpy(image2->data(), &data[0], data.size()); for (const auto & exploredCell : mContext->mExploredCells) { if (exploredCell.first > mContext->mGlobalMapState.mBounds.mMaxX || exploredCell.first < mContext->mGlobalMapState.mBounds.mMinX || exploredCell.second > mContext->mGlobalMapState.mBounds.mMaxY || exploredCell.second < mContext->mGlobalMapState.mBounds.mMinY) { // out of bounds, I think this could happen, since the original engine had a fixed-size map continue; } int imageLeftSrc = mGlobalMapImage->s()/2; int imageTopSrc = mGlobalMapImage->t()/2; imageLeftSrc += exploredCell.first * cellSize; imageTopSrc -= exploredCell.second * cellSize; int imageLeftDst = width/2; int imageTopDst = height/2; imageLeftDst += exploredCell.first * cellSize; imageTopDst -= exploredCell.second * cellSize; for (int x=0; xdata(imageLeftSrc+x, imageTopSrc+y, 0); *(unsigned int*)image2->data(imageLeftDst+x, imageTopDst+y, 0) = col; } } std::stringstream ostream; osgDB::ReaderWriter* readerwriter = osgDB::Registry::instance()->getReaderWriterForExtension("png"); if (!readerwriter) { std::cerr << "Error: can't write global map image, no png readerwriter found" << std::endl; return; } image2->flipVertical(); osgDB::ReaderWriter::WriteResult result = readerwriter->writeImage(*image2, ostream); if (!result.success()) { std::cerr << "Error: can't write global map image: " << result.message() << " code " << result.status() << std::endl; return; } std::string outData = ostream.str(); mContext->mGlobalMapState.mImageData = std::vector(outData.begin(), outData.end()); esm.startRecord(ESM::REC_GMAP); mContext->mGlobalMapState.save(esm); esm.endRecord(ESM::REC_GMAP); } void ConvertCell::read(ESM::ESMReader &esm) { ESM::Cell cell; bool isDeleted = false; cell.load(esm, isDeleted, false); // I wonder what 0x40 does? if (cell.isExterior() && cell.mData.mFlags & 0x20) { mContext->mGlobalMapState.mMarkers.insert(std::make_pair(cell.mData.mX, cell.mData.mY)); } // note if the player is in a nameless exterior cell, we will assign the cellId later based on player position if (cell.mName == mContext->mPlayerCellName) { mContext->mPlayer.mCellId = cell.getCellId(); } Cell newcell; newcell.mCell = cell; // fog of war // seems to be a 1-bit pixel format, 16*16 pixels // TODO: add bleeding of FOW into neighbouring cells (openmw handles this by writing to the textures, // MW handles it when rendering only) unsigned char nam8[32]; // exterior has 1 NAM8, interior can have multiple ones, and have an extra 4 byte flag at the start // (probably offset of that specific fog texture?) while (esm.isNextSub("NAM8")) { if (cell.isExterior()) // TODO: NAM8 occasionally exists for cells that haven't been explored. // are there any flags marking explored cells? mContext->mExploredCells.insert(std::make_pair(cell.mData.mX, cell.mData.mY)); esm.getSubHeader(); if (esm.getSubSize() == 36) { // flag on interiors esm.skip(4); } esm.getExact(nam8, 32); newcell.mFogOfWar.reserve(16*16); for (int x=0; x<16; ++x) { for (int y=0; y<16; ++y) { size_t pos = x*16+y; size_t bytepos = pos/8; assert(bytepos<32); int bit = pos%8; newcell.mFogOfWar.push_back(((nam8[bytepos] >> bit) & (0x1)) ? 0xffffffff : 0x000000ff); } } if (cell.isExterior()) { std::ostringstream filename; filename << "fog_" << cell.mData.mX << "_" << cell.mData.mY << ".tga"; convertImage((char*)&newcell.mFogOfWar[0], newcell.mFogOfWar.size()*4, 16, 16, GL_RGBA, filename.str()); } } // moved reference, not handled yet // NOTE: MVRF can also occur in within normal references (importcellref.cpp)? // this does not match the ESM file implementation, // verify if that can happen with ESM files too while (esm.isNextSub("MVRF")) { esm.skipHSub(); // skip MVRF esm.getSubName(); esm.skipHSub(); // skip CNDT } std::vector cellrefs; while (esm.hasMoreSubs() && esm.isNextSub("FRMR")) { CellRef ref; ref.load (esm); cellrefs.push_back(ref); } while (esm.isNextSub("MPCD")) { float notepos[3]; esm.getHT(notepos, 3*sizeof(float)); // Markers seem to be arranged in a 32*32 grid, notepos has grid-indices. // This seems to be the reason markers can't be placed everywhere in interior cells, // i.e. when the grid is exceeded. // Converting the interior markers correctly could be rather tricky, but is probably similar logic // as used for the FoW texture placement, which we need to figure out anyway notepos[1] += 31.f; notepos[0] += 0.5; notepos[1] += 0.5; notepos[0] = Constants::CellSizeInUnits * notepos[0] / 32.f; notepos[1] = Constants::CellSizeInUnits * notepos[1] / 32.f; if (cell.isExterior()) { notepos[0] += Constants::CellSizeInUnits * cell.mData.mX; notepos[1] += Constants::CellSizeInUnits * cell.mData.mY; } // TODO: what encoding is this in? std::string note = esm.getHNString("MPNT"); ESM::CustomMarker marker; marker.mWorldX = notepos[0]; marker.mWorldY = notepos[1]; marker.mNote = note; marker.mCell = cell.getCellId(); mMarkers.push_back(marker); } newcell.mRefs = cellrefs; if (cell.isExterior()) mExtCells[std::make_pair(cell.mData.mX, cell.mData.mY)] = newcell; else mIntCells[cell.mName] = newcell; } void ConvertCell::writeCell(const Cell &cell, ESM::ESMWriter& esm) { ESM::Cell esmcell = cell.mCell; esm.startRecord(ESM::REC_CSTA); ESM::CellState csta; csta.mHasFogOfWar = 0; csta.mId = esmcell.getCellId(); csta.mId.save(esm); // TODO csta.mLastRespawn; // shouldn't be needed if we respawn on global schedule like in original MW csta.mWaterLevel = esmcell.mWater; csta.save(esm); for (const auto & cellref : cell.mRefs) { ESM::CellRef out (cellref); // TODO: use mContext->mCreatures/mNpcs if (!isIndexedRefId(cellref.mIndexedRefId)) { // non-indexed RefNum, i.e. no CREC/NPCC/CNTC record associated with it // this could be any type of object really (even creatures/npcs too) out.mRefID = cellref.mIndexedRefId; std::string idLower = Misc::StringUtils::lowerCase(out.mRefID); ESM::ObjectState objstate; objstate.blank(); objstate.mRef = out; objstate.mRef.mRefID = idLower; objstate.mHasCustomState = false; convertCellRef(cellref, objstate); esm.writeHNT ("OBJE", 0); objstate.save(esm); continue; } else { int refIndex; splitIndexedRefId(cellref.mIndexedRefId, refIndex, out.mRefID); std::string idLower = Misc::StringUtils::lowerCase(out.mRefID); std::map, NPCC>::const_iterator npccIt = mContext->mNpcChanges.find( std::make_pair(refIndex, out.mRefID)); if (npccIt != mContext->mNpcChanges.end()) { ESM::NpcState objstate; objstate.blank(); objstate.mRef = out; objstate.mRef.mRefID = idLower; // TODO: need more micromanagement here so we don't overwrite values // from the ESM with default values if (cellref.mHasACDT) convertACDT(cellref.mACDT, objstate.mCreatureStats); if (cellref.mHasACSC) convertACSC(cellref.mACSC, objstate.mCreatureStats); convertNpcData(cellref, objstate.mNpcStats); convertNPCC(npccIt->second, objstate); convertCellRef(cellref, objstate); objstate.mCreatureStats.mActorId = mContext->generateActorId(); mContext->mActorIdMap.insert(std::make_pair(std::make_pair(refIndex, out.mRefID), objstate.mCreatureStats.mActorId)); esm.writeHNT ("OBJE", ESM::REC_NPC_); objstate.save(esm); continue; } std::map, CNTC>::const_iterator cntcIt = mContext->mContainerChanges.find( std::make_pair(refIndex, out.mRefID)); if (cntcIt != mContext->mContainerChanges.end()) { ESM::ContainerState objstate; objstate.blank(); objstate.mRef = out; objstate.mRef.mRefID = idLower; convertCNTC(cntcIt->second, objstate); convertCellRef(cellref, objstate); esm.writeHNT ("OBJE", ESM::REC_CONT); objstate.save(esm); continue; } std::map, CREC>::const_iterator crecIt = mContext->mCreatureChanges.find( std::make_pair(refIndex, out.mRefID)); if (crecIt != mContext->mCreatureChanges.end()) { ESM::CreatureState objstate; objstate.blank(); objstate.mRef = out; objstate.mRef.mRefID = idLower; // TODO: need more micromanagement here so we don't overwrite values // from the ESM with default values if (cellref.mHasACDT) convertACDT(cellref.mACDT, objstate.mCreatureStats); if (cellref.mHasACSC) convertACSC(cellref.mACSC, objstate.mCreatureStats); convertCREC(crecIt->second, objstate); convertCellRef(cellref, objstate); objstate.mCreatureStats.mActorId = mContext->generateActorId(); mContext->mActorIdMap.insert(std::make_pair(std::make_pair(refIndex, out.mRefID), objstate.mCreatureStats.mActorId)); esm.writeHNT ("OBJE", ESM::REC_CREA); objstate.save(esm); continue; } std::stringstream error; error << "Can't find type for " << cellref.mIndexedRefId << std::endl; throw std::runtime_error(error.str()); } } esm.endRecord(ESM::REC_CSTA); } void ConvertCell::write(ESM::ESMWriter &esm) { for (const auto & cell : mIntCells) writeCell(cell.second, esm); for (const auto & cell : mExtCells) writeCell(cell.second, esm); for (const auto & marker : mMarkers) { esm.startRecord(ESM::REC_MARK); marker.save(esm); esm.endRecord(ESM::REC_MARK); } } void ConvertPROJ::read(ESM::ESMReader& esm) { mProj.load(esm); } void ConvertPROJ::write(ESM::ESMWriter& esm) { for (const PROJ::PNAM& pnam : mProj.mProjectiles) { if (!pnam.isMagic()) { ESM::ProjectileState out; convertBaseState(out, pnam); out.mBowId = pnam.mBowId.toString(); out.mVelocity = pnam.mVelocity; out.mAttackStrength = pnam.mAttackStrength; esm.startRecord(ESM::REC_PROJ); out.save(esm); esm.endRecord(ESM::REC_PROJ); } else { ESM::MagicBoltState out; convertBaseState(out, pnam); auto it = std::find_if(mContext->mActiveSpells.begin(), mContext->mActiveSpells.end(), [&pnam](const SPLM::ActiveSpell& spell) -> bool { return spell.mIndex == pnam.mSplmIndex; }); if (it == mContext->mActiveSpells.end()) { std::cerr << "Warning: Skipped conversion for magic projectile \"" << pnam.mArrowId.toString() << "\" (invalid spell link)" << std::endl; continue; } out.mSpellId = it->mSPDT.mId.toString(); out.mSpeed = pnam.mSpeed * 0.001f; // not sure where this factor comes from esm.startRecord(ESM::REC_MPRJ); out.save(esm); esm.endRecord(ESM::REC_MPRJ); } } } void ConvertPROJ::convertBaseState(ESM::BaseProjectileState& base, const PROJ::PNAM& pnam) { base.mId = pnam.mArrowId.toString(); base.mPosition = pnam.mPosition; osg::Quat orient; orient.makeRotate(osg::Vec3f(0,1,0), pnam.mVelocity); base.mOrientation = orient; base.mActorId = convertActorId(pnam.mActorId.toString(), *mContext); } void ConvertSPLM::read(ESM::ESMReader& esm) { mSPLM.load(esm); mContext->mActiveSpells = mSPLM.mActiveSpells; } void ConvertSPLM::write(ESM::ESMWriter& esm) { std::cerr << "Warning: Skipped active spell conversion (not implemented)" << std::endl; } } ================================================ FILE: apps/essimporter/converter.hpp ================================================ #ifndef OPENMW_ESSIMPORT_CONVERTER_H #define OPENMW_ESSIMPORT_CONVERTER_H #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "importcrec.hpp" #include "importcntc.hpp" #include "importercontext.hpp" #include "importcellref.hpp" #include "importklst.hpp" #include "importgame.hpp" #include "importinfo.hpp" #include "importdial.hpp" #include "importques.hpp" #include "importjour.hpp" #include "importscpt.hpp" #include "importproj.h" #include "importsplm.h" #include "convertacdt.hpp" #include "convertnpcc.hpp" #include "convertscpt.hpp" #include "convertplayer.hpp" namespace ESSImport { class Converter { public: /// @return the order for writing this converter's records to the output file, in relation to other converters virtual int getStage() { return 1; } virtual ~Converter() {} void setContext(Context& context) { mContext = &context; } /// @note The load method of ESM records accept the deleted flag as a parameter. /// I don't know can the DELE sub-record appear in saved games, so the deleted flag will be ignored. virtual void read(ESM::ESMReader& esm) { } /// Called after the input file has been read in completely, which may be necessary /// if the conversion process relies on information in other records virtual void write(ESM::ESMWriter& esm) { } protected: Context* mContext; }; /// Default converter: simply reads the record and writes it unmodified to the output template class DefaultConverter : public Converter { public: int getStage() override { return 0; } void read(ESM::ESMReader& esm) override { T record; bool isDeleted = false; record.load(esm, isDeleted); mRecords[record.mId] = record; } void write(ESM::ESMWriter& esm) override { for (typename std::map::const_iterator it = mRecords.begin(); it != mRecords.end(); ++it) { esm.startRecord(T::sRecordId); it->second.save(esm); esm.endRecord(T::sRecordId); } } protected: std::map mRecords; }; class ConvertNPC : public Converter { public: void read(ESM::ESMReader &esm) override { ESM::NPC npc; bool isDeleted = false; npc.load(esm, isDeleted); if (npc.mId != "player") { // Handles changes to the NPC struct, but since there is no index here // it will apply to ALL instances of the class. seems to be the reason for the // "feature" in MW where changing AI settings of one guard will change it for all guards of that refID. mContext->mNpcs[Misc::StringUtils::lowerCase(npc.mId)] = npc; } else { mContext->mPlayer.mObject.mCreatureStats.mLevel = npc.mNpdt.mLevel; mContext->mPlayerBase = npc; ESM::SpellState::SpellParams empty; // FIXME: player start spells and birthsign spells aren't listed here, // need to fix openmw to account for this for (const auto & spell : npc.mSpells.mList) mContext->mPlayer.mObject.mCreatureStats.mSpells.mSpells[spell] = empty; // Clear the list now that we've written it, this prevents issues cropping up with // ensureCustomData() in OpenMW tripping over no longer existing spells, where an error would be fatal. mContext->mPlayerBase.mSpells.mList.clear(); // Same with inventory. Actually it's strange this would contain something, since there's already an // inventory list in NPCC. There seems to be a fair amount of redundancy in this format. mContext->mPlayerBase.mInventory.mList.clear(); } } }; class ConvertCREA : public Converter { public: void read(ESM::ESMReader &esm) override { // See comment in ConvertNPC ESM::Creature creature; bool isDeleted = false; creature.load(esm, isDeleted); mContext->mCreatures[Misc::StringUtils::lowerCase(creature.mId)] = creature; } }; // Do we need ConvertCONT? // I've seen a CONT record in a certain save file, but the container contents in it // were identical to a corresponding CNTC record. See previous comment about redundancy... class ConvertGlobal : public DefaultConverter { public: void read(ESM::ESMReader &esm) override { ESM::Global global; bool isDeleted = false; global.load(esm, isDeleted); if (Misc::StringUtils::ciEqual(global.mId, "gamehour")) mContext->mHour = global.mValue.getFloat(); if (Misc::StringUtils::ciEqual(global.mId, "day")) mContext->mDay = global.mValue.getInteger(); if (Misc::StringUtils::ciEqual(global.mId, "month")) mContext->mMonth = global.mValue.getInteger(); if (Misc::StringUtils::ciEqual(global.mId, "year")) mContext->mYear = global.mValue.getInteger(); mRecords[global.mId] = global; } }; class ConvertClass : public DefaultConverter { public: void read(ESM::ESMReader &esm) override { ESM::Class class_; bool isDeleted = false; class_.load(esm, isDeleted); if (class_.mId == "NEWCLASSID_CHARGEN") mContext->mCustomPlayerClassName = class_.mName; mRecords[class_.mId] = class_; } }; class ConvertBook : public DefaultConverter { public: void read(ESM::ESMReader &esm) override { ESM::Book book; bool isDeleted = false; book.load(esm, isDeleted); if (book.mData.mSkillId == -1) mContext->mPlayer.mObject.mNpcStats.mUsedIds.push_back(Misc::StringUtils::lowerCase(book.mId)); mRecords[book.mId] = book; } }; class ConvertNPCC : public Converter { public: void read(ESM::ESMReader &esm) override { std::string id = esm.getHNString("NAME"); NPCC npcc; npcc.load(esm); if (id == "PlayerSaveGame") { convertNPCC(npcc, mContext->mPlayer.mObject); } else { int index = npcc.mNPDT.mIndex; mContext->mNpcChanges.insert(std::make_pair(std::make_pair(index,id), npcc)); } } }; class ConvertREFR : public Converter { public: void read(ESM::ESMReader &esm) override { REFR refr; refr.load(esm); assert(refr.mRefID == "PlayerSaveGame"); mContext->mPlayer.mObject.mPosition = refr.mPos; ESM::CreatureStats& cStats = mContext->mPlayer.mObject.mCreatureStats; convertACDT(refr.mActorData.mACDT, cStats); ESM::NpcStats& npcStats = mContext->mPlayer.mObject.mNpcStats; convertNpcData(refr.mActorData, npcStats); mSelectedSpell = refr.mActorData.mSelectedSpell; if (!refr.mActorData.mSelectedEnchantItem.empty()) { ESM::InventoryState& invState = mContext->mPlayer.mObject.mInventory; for (unsigned int i=0; imPlayer, mContext->mDialogueState.mKnownTopics, mFirstPersonCam, mTeleportingEnabled, mLevitationEnabled, mContext->mControlsState); } void write(ESM::ESMWriter &esm) override { esm.startRecord(ESM::REC_ENAB); esm.writeHNT("TELE", mTeleportingEnabled); esm.writeHNT("LEVT", mLevitationEnabled); esm.endRecord(ESM::REC_ENAB); esm.startRecord(ESM::REC_CAM_); esm.writeHNT("FIRS", mFirstPersonCam); esm.endRecord(ESM::REC_CAM_); } private: bool mFirstPersonCam; bool mTeleportingEnabled; bool mLevitationEnabled; }; class ConvertCNTC : public Converter { void read(ESM::ESMReader &esm) override { std::string id = esm.getHNString("NAME"); CNTC cntc; cntc.load(esm); mContext->mContainerChanges.insert(std::make_pair(std::make_pair(cntc.mIndex,id), cntc)); } }; class ConvertCREC : public Converter { public: void read(ESM::ESMReader &esm) override { std::string id = esm.getHNString("NAME"); CREC crec; crec.load(esm); mContext->mCreatureChanges.insert(std::make_pair(std::make_pair(crec.mIndex,id), crec)); } }; class ConvertFMAP : public Converter { public: void read(ESM::ESMReader &esm) override; void write(ESM::ESMWriter &esm) override; private: osg::ref_ptr mGlobalMapImage; }; class ConvertCell : public Converter { public: void read(ESM::ESMReader& esm) override; void write(ESM::ESMWriter& esm) override; private: struct Cell { ESM::Cell mCell; std::vector mRefs; std::vector mFogOfWar; }; std::map mIntCells; std::map, Cell> mExtCells; std::vector mMarkers; void writeCell(const Cell& cell, ESM::ESMWriter &esm); }; class ConvertKLST : public Converter { public: void read(ESM::ESMReader& esm) override { KLST klst; klst.load(esm); mKillCounter = klst.mKillCounter; mContext->mPlayer.mObject.mNpcStats.mWerewolfKills = klst.mWerewolfKills; } void write(ESM::ESMWriter &esm) override { esm.startRecord(ESM::REC_DCOU); for (std::map::const_iterator it = mKillCounter.begin(); it != mKillCounter.end(); ++it) { esm.writeHNString("ID__", it->first); esm.writeHNT ("COUN", it->second); } esm.endRecord(ESM::REC_DCOU); } private: std::map mKillCounter; }; class ConvertFACT : public Converter { public: void read(ESM::ESMReader& esm) override { ESM::Faction faction; bool isDeleted = false; faction.load(esm, isDeleted); std::string id = Misc::StringUtils::lowerCase(faction.mId); for (std::map::const_iterator it = faction.mReactions.begin(); it != faction.mReactions.end(); ++it) { std::string faction2 = Misc::StringUtils::lowerCase(it->first); mContext->mDialogueState.mChangedFactionReaction[id].insert(std::make_pair(faction2, it->second)); } } }; /// Stolen items class ConvertSTLN : public Converter { public: void read(ESM::ESMReader &esm) override { std::string itemid = esm.getHNString("NAME"); Misc::StringUtils::lowerCaseInPlace(itemid); while (esm.isNextSub("FNAM") || esm.isNextSub("ONAM")) { if (esm.retSubName().toString() == "FNAM") { std::string factionid = esm.getHString(); mStolenItems[itemid].insert(std::make_pair(Misc::StringUtils::lowerCase(factionid), true)); } else { std::string ownerid = esm.getHString(); mStolenItems[itemid].insert(std::make_pair(Misc::StringUtils::lowerCase(ownerid), false)); } } } void write(ESM::ESMWriter &esm) override { ESM::StolenItems items; for (std::map >::const_iterator it = mStolenItems.begin(); it != mStolenItems.end(); ++it) { std::map, int> owners; for (const auto & ownerIt : it->second) { owners.insert(std::make_pair(std::make_pair(ownerIt.first, ownerIt.second) // Since OpenMW doesn't suffer from the owner contamination bug, // it needs a count argument. But for legacy savegames, we don't know // this count, so must assume all items of that ID are stolen, // like vanilla MW did. ,std::numeric_limits::max())); } items.mStolenItems.insert(std::make_pair(it->first, owners)); } esm.startRecord(ESM::REC_STLN); items.write(esm); esm.endRecord(ESM::REC_STLN); } private: typedef std::pair Owner; // std::map > mStolenItems; }; /// Seen responses for a dialogue topic? /// Each DIAL record is followed by a number of INFO records, I believe, just like in ESMs /// Dialogue conversion problems: /// - Journal is stored in one continuous HTML markup rather than each entry separately with associated info ID. /// - Seen dialogue responses only store the INFO id, rather than the fulltext. /// - Quest stages only store the INFO id, rather than the journal entry fulltext. class ConvertINFO : public Converter { public: void read(ESM::ESMReader& esm) override { INFO info; info.load(esm); } }; class ConvertDIAL : public Converter { public: void read(ESM::ESMReader& esm) override { std::string id = esm.getHNString("NAME"); DIAL dial; dial.load(esm); if (dial.mIndex > 0) mDials[id] = dial; } void write(ESM::ESMWriter &esm) override { for (std::map::const_iterator it = mDials.begin(); it != mDials.end(); ++it) { esm.startRecord(ESM::REC_QUES); ESM::QuestState state; state.mFinished = 0; state.mState = it->second.mIndex; state.mTopic = Misc::StringUtils::lowerCase(it->first); state.save(esm); esm.endRecord(ESM::REC_QUES); } } private: std::map mDials; }; class ConvertQUES : public Converter { public: void read(ESM::ESMReader& esm) override { std::string id = esm.getHNString("NAME"); QUES quest; quest.load(esm); } }; class ConvertJOUR : public Converter { public: void read(ESM::ESMReader& esm) override { JOUR journal; journal.load(esm); } }; class ConvertGAME : public Converter { public: ConvertGAME() : mHasGame(false) { } void read(ESM::ESMReader &esm) override { mGame.load(esm); mHasGame = true; } int validateWeatherID(int weatherID) { if(weatherID >= -1 && weatherID < 10) { return weatherID; } else { std::stringstream error; error << "Invalid weather ID:" << weatherID << std::endl; throw std::runtime_error(error.str()); } } void write(ESM::ESMWriter &esm) override { if (!mHasGame) return; esm.startRecord(ESM::REC_WTHR); ESM::WeatherState weather; weather.mTimePassed = 0.0f; weather.mFastForward = false; weather.mWeatherUpdateTime = mGame.mGMDT.mTimeOfNextTransition - mContext->mHour; weather.mTransitionFactor = 1 - (mGame.mGMDT.mWeatherTransition / 100.0f); weather.mCurrentWeather = validateWeatherID(mGame.mGMDT.mCurrentWeather); weather.mNextWeather = validateWeatherID(mGame.mGMDT.mNextWeather); weather.mQueuedWeather = -1; // TODO: Determine how ModRegion modifiers are saved in Morrowind. weather.save(esm); esm.endRecord(ESM::REC_WTHR); } private: bool mHasGame; GAME mGame; }; /// Running global script class ConvertSCPT : public Converter { public: void read(ESM::ESMReader &esm) override { SCPT script; script.load(esm); ESM::GlobalScript out; convertSCPT(script, out); mScripts.push_back(out); } void write(ESM::ESMWriter &esm) override { for (const auto & script : mScripts) { esm.startRecord(ESM::REC_GSCR); script.save(esm); esm.endRecord(ESM::REC_GSCR); } } private: std::vector mScripts; }; /// Projectile converter class ConvertPROJ : public Converter { public: int getStage() override { return 2; } void read(ESM::ESMReader& esm) override; void write(ESM::ESMWriter& esm) override; private: void convertBaseState(ESM::BaseProjectileState& base, const PROJ::PNAM& pnam); PROJ mProj; }; class ConvertSPLM : public Converter { public: void read(ESM::ESMReader& esm) override; void write(ESM::ESMWriter& esm) override; private: SPLM mSPLM; }; } #endif ================================================ FILE: apps/essimporter/convertinventory.cpp ================================================ #include "convertinventory.hpp" #include #include namespace ESSImport { void convertInventory(const Inventory &inventory, ESM::InventoryState &state) { int index = 0; for (const auto & item : inventory.mItems) { ESM::ObjectState objstate; objstate.blank(); objstate.mRef = item; objstate.mRef.mRefID = Misc::StringUtils::lowerCase(item.mId); objstate.mCount = std::abs(item.mCount); // restocking items have negative count in the savefile // openmw handles them differently, so no need to set any flags state.mItems.push_back(objstate); if (item.mRelativeEquipmentSlot != -1) // Note we should really write the absolute slot here, which we do not know about // Not a big deal, OpenMW will auto-correct to a valid slot, the only problem is when // an item could be equipped in two different slots (e.g. equipped two rings) state.mEquipmentSlots[index] = item.mRelativeEquipmentSlot; ++index; } } } ================================================ FILE: apps/essimporter/convertinventory.hpp ================================================ #ifndef OPENMW_ESSIMPORT_CONVERTINVENTORY_H #define OPENMW_ESSIMPORT_CONVERTINVENTORY_H #include "importinventory.hpp" #include namespace ESSImport { void convertInventory (const Inventory& inventory, ESM::InventoryState& state); } #endif ================================================ FILE: apps/essimporter/convertnpcc.cpp ================================================ #include "convertnpcc.hpp" #include "convertinventory.hpp" namespace ESSImport { void convertNPCC(const NPCC &npcc, ESM::NpcState &npcState) { npcState.mNpcStats.mDisposition = npcc.mNPDT.mDisposition; npcState.mNpcStats.mReputation = npcc.mNPDT.mReputation; convertInventory(npcc.mInventory, npcState.mInventory); } } ================================================ FILE: apps/essimporter/convertnpcc.hpp ================================================ #ifndef OPENMW_ESSIMPORT_CONVERTNPCC_H #define OPENMW_ESSIMPORT_CONVERTNPCC_H #include "importnpcc.hpp" #include namespace ESSImport { void convertNPCC (const NPCC& npcc, ESM::NpcState& npcState); } #endif ================================================ FILE: apps/essimporter/convertplayer.cpp ================================================ #include "convertplayer.hpp" #include #include namespace ESSImport { void convertPCDT(const PCDT& pcdt, ESM::Player& out, std::vector& outDialogueTopics, bool& firstPersonCam, bool& teleportingEnabled, bool& levitationEnabled, ESM::ControlsState& controls) { out.mBirthsign = pcdt.mBirthsign; out.mObject.mNpcStats.mBounty = pcdt.mBounty; for (const auto & essFaction : pcdt.mFactions) { ESM::NpcStats::Faction faction; faction.mExpelled = (essFaction.mFlags & 0x2) != 0; faction.mRank = essFaction.mRank; faction.mReputation = essFaction.mReputation; out.mObject.mNpcStats.mFactions[Misc::StringUtils::lowerCase(essFaction.mFactionName.toString())] = faction; } for (int i=0; i<3; ++i) out.mObject.mNpcStats.mSpecIncreases[i] = pcdt.mPNAM.mSpecIncreases[i]; for (int i=0; i<8; ++i) out.mObject.mNpcStats.mSkillIncrease[i] = pcdt.mPNAM.mSkillIncreases[i]; for (int i=0; i<27; ++i) out.mObject.mNpcStats.mSkills[i].mProgress = pcdt.mPNAM.mSkillProgress[i]; out.mObject.mNpcStats.mLevelProgress = pcdt.mPNAM.mLevelProgress; if (pcdt.mPNAM.mPlayerFlags & PCDT::PlayerFlags_WeaponDrawn) out.mObject.mCreatureStats.mDrawState = 1; if (pcdt.mPNAM.mPlayerFlags & PCDT::PlayerFlags_SpellDrawn) out.mObject.mCreatureStats.mDrawState = 2; firstPersonCam = !(pcdt.mPNAM.mPlayerFlags & PCDT::PlayerFlags_ThirdPerson); teleportingEnabled = !(pcdt.mPNAM.mPlayerFlags & PCDT::PlayerFlags_TeleportingDisabled); levitationEnabled = !(pcdt.mPNAM.mPlayerFlags & PCDT::PlayerFlags_LevitationDisabled); for (const auto & knownDialogueTopic : pcdt.mKnownDialogueTopics) { outDialogueTopics.push_back(Misc::StringUtils::lowerCase(knownDialogueTopic)); } controls.mViewSwitchDisabled = pcdt.mPNAM.mPlayerFlags & PCDT::PlayerFlags_ViewSwitchDisabled; controls.mControlsDisabled = pcdt.mPNAM.mPlayerFlags & PCDT::PlayerFlags_ControlsDisabled; controls.mJumpingDisabled = pcdt.mPNAM.mPlayerFlags & PCDT::PlayerFlags_JumpingDisabled; controls.mLookingDisabled = pcdt.mPNAM.mPlayerFlags & PCDT::PlayerFlags_LookingDisabled; controls.mVanityModeDisabled = pcdt.mPNAM.mPlayerFlags & PCDT::PlayerFlags_VanityModeDisabled; controls.mWeaponDrawingDisabled = pcdt.mPNAM.mPlayerFlags & PCDT::PlayerFlags_WeaponDrawingDisabled; controls.mSpellDrawingDisabled = pcdt.mPNAM.mPlayerFlags & PCDT::PlayerFlags_SpellDrawingDisabled; if (pcdt.mHasMark) { out.mHasMark = 1; const PCDT::PNAM::MarkLocation& mark = pcdt.mPNAM.mMarkLocation; ESM::CellId cell; cell.mWorldspace = ESM::CellId::sDefaultWorldspace; cell.mPaged = true; cell.mIndex.mX = mark.mCellX; cell.mIndex.mY = mark.mCellY; // TODO: Figure out a better way to detect interiors. (0, 0) is a valid exterior cell. if (mark.mCellX == 0 && mark.mCellY == 0) { cell.mWorldspace = pcdt.mMNAM; cell.mPaged = false; } out.mMarkedCell = cell; out.mMarkedPosition.pos[0] = mark.mX; out.mMarkedPosition.pos[1] = mark.mY; out.mMarkedPosition.pos[2] = mark.mZ; out.mMarkedPosition.rot[0] = out.mMarkedPosition.rot[1] = 0.0f; out.mMarkedPosition.rot[2] = mark.mRotZ; } if (pcdt.mHasENAM) { out.mLastKnownExteriorPosition[0] = (pcdt.mENAM.mCellX + 0.5f) * Constants::CellSizeInUnits; out.mLastKnownExteriorPosition[1] = (pcdt.mENAM.mCellY + 0.5f) * Constants::CellSizeInUnits; out.mLastKnownExteriorPosition[2] = 0.0f; } } } ================================================ FILE: apps/essimporter/convertplayer.hpp ================================================ #ifndef OPENMW_ESSIMPORT_CONVERTPLAYER_H #define OPENMW_ESSIMPORT_CONVERTPLAYER_H #include "importplayer.hpp" #include #include namespace ESSImport { void convertPCDT(const PCDT& pcdt, ESM::Player& out, std::vector& outDialogueTopics, bool& firstPersonCam, bool& teleportingEnabled, bool& levitationEnabled, ESM::ControlsState& controls); } #endif ================================================ FILE: apps/essimporter/convertscpt.cpp ================================================ #include "convertscpt.hpp" #include #include "convertscri.hpp" namespace ESSImport { void convertSCPT(const SCPT &scpt, ESM::GlobalScript &out) { out.mId = Misc::StringUtils::lowerCase(scpt.mSCHD.mName.toString()); out.mRunning = scpt.mRunning; out.mTargetRef.unset(); // TODO: convert target reference of global script convertSCRI(scpt.mSCRI, out.mLocals); } } ================================================ FILE: apps/essimporter/convertscpt.hpp ================================================ #ifndef OPENMW_ESSIMPORT_CONVERTSCPT_H #define OPENMW_ESSIMPORT_CONVERTSCPT_H #include #include "importscpt.hpp" namespace ESSImport { void convertSCPT(const SCPT& scpt, ESM::GlobalScript& out); } #endif ================================================ FILE: apps/essimporter/convertscri.cpp ================================================ #include "convertscri.hpp" namespace { template void storeVariables(const std::vector& variables, ESM::Locals& locals, const std::string& scriptname) { for (const auto& variable : variables) { ESM::Variant val(variable); val.setType(VariantType); locals.mVariables.emplace_back(std::string(), val); } } } namespace ESSImport { void convertSCRI(const SCRI &scri, ESM::Locals &locals) { // order *is* important, as we do not have variable names available in this format storeVariables (scri.mShorts, locals, scri.mScript); storeVariables (scri.mLongs, locals, scri.mScript); storeVariables (scri.mFloats, locals, scri.mScript); } } ================================================ FILE: apps/essimporter/convertscri.hpp ================================================ #ifndef OPENMW_ESSIMPORT_CONVERTSCRI_H #define OPENMW_ESSIMPORT_CONVERTSCRI_H #include "importscri.hpp" #include namespace ESSImport { /// Convert script variable assignments void convertSCRI (const SCRI& scri, ESM::Locals& locals); } #endif ================================================ FILE: apps/essimporter/importacdt.cpp ================================================ #include "importacdt.hpp" #include #include namespace ESSImport { void ActorData::load(ESM::ESMReader &esm) { if (esm.isNextSub("ACTN")) { /* Activation flags: ActivationFlag_UseEnabled = 1 ActivationFlag_OnActivate = 2 ActivationFlag_OnDeath = 10h ActivationFlag_OnKnockout = 20h ActivationFlag_OnMurder = 40h ActivationFlag_DoorOpening = 100h ActivationFlag_DoorClosing = 200h ActivationFlag_DoorJammedOpening = 400h ActivationFlag_DoorJammedClosing = 800h */ esm.skipHSub(); } if (esm.isNextSub("STPR")) esm.skipHSub(); if (esm.isNextSub("MNAM")) esm.skipHSub(); bool isDeleted = false; ESM::CellRef::loadData(esm, isDeleted); mHasACDT = false; if (esm.isNextSub("ACDT")) { mHasACDT = true; esm.getHT(mACDT); } mHasACSC = false; if (esm.isNextSub("ACSC")) { mHasACSC = true; esm.getHT(mACSC); } if (esm.isNextSub("ACSL")) esm.skipHSubSize(112); if (esm.isNextSub("CSTN")) esm.skipHSub(); // "PlayerSaveGame", link to some object? if (esm.isNextSub("LSTN")) esm.skipHSub(); // "PlayerSaveGame", link to some object? // unsure at which point between LSTN and TGTN if (esm.isNextSub("CSHN")) esm.skipHSub(); // "PlayerSaveGame", link to some object? // unsure if before or after CSTN/LSTN if (esm.isNextSub("LSHN")) esm.skipHSub(); // "PlayerSaveGame", link to some object? while (esm.isNextSub("TGTN")) esm.skipHSub(); // "PlayerSaveGame", link to some object? while (esm.isNextSub("FGTN")) esm.getHString(); // fight target? // unsure at which point between TGTN and CRED if (esm.isNextSub("AADT")) { // occurred when a creature was in the middle of its attack, 44 bytes esm.skipHSub(); } // unsure at which point between FGTN and CHRD if (esm.isNextSub("PWPC")) esm.skipHSub(); if (esm.isNextSub("PWPS")) esm.skipHSub(); if (esm.isNextSub("WNAM")) { std::string id = esm.getHString(); if (esm.isNextSub("XNAM")) mSelectedEnchantItem = esm.getHString(); else mSelectedSpell = id; if (esm.isNextSub("YNAM")) esm.skipHSub(); // 4 byte, 0 } while (esm.isNextSub("APUD")) { // used power esm.getSubHeader(); std::string id = esm.getString(32); (void)id; // timestamp can't be used: this is the total hours passed, calculated by // timestamp = 24 * (365 * year + cumulativeDays[month] + day) // unfortunately cumulativeDays[month] is not clearly defined, // in the (non-MCP) vanilla version the first month was missing, but MCP added it. double timestamp; esm.getT(timestamp); } // FIXME: not all actors have this, add flag if (esm.isNextSub("CHRD")) // npc only esm.getHExact(mSkills, 27*2*sizeof(int)); if (esm.isNextSub("CRED")) // creature only esm.getHExact(mCombatStats, 3*2*sizeof(int)); mSCRI.load(esm); if (esm.isNextSub("ND3D")) esm.skipHSub(); mHasANIS = false; if (esm.isNextSub("ANIS")) { mHasANIS = true; esm.getHT(mANIS); } } } ================================================ FILE: apps/essimporter/importacdt.hpp ================================================ #ifndef OPENMW_ESSIMPORT_ACDT_H #define OPENMW_ESSIMPORT_ACDT_H #include #include #include "importscri.hpp" namespace ESM { class ESMReader; } namespace ESSImport { enum ACDTFlags { TalkedToPlayer = 0x4, Attacked = 0x100, Unknown = 0x200 }; enum ACSCFlags { Dead = 0x2 }; /// Actor data, shared by (at least) REFR and CellRef #pragma pack(push) #pragma pack(1) struct ACDT { // Note, not stored at *all*: // - Level changes are lost on reload, except for the player (there it's in the NPC record). unsigned char mUnknown[12]; unsigned int mFlags; float mBreathMeter; // Seconds left before drowning unsigned char mUnknown2[20]; float mDynamic[3][2]; unsigned char mUnknown3[16]; float mAttributes[8][2]; float mMagicEffects[27]; // Effect attributes: https://wiki.openmw.org/index.php?title=Research:Magic#Effect_attributes unsigned char mUnknown4[4]; unsigned int mGoldPool; unsigned char mCountDown; // seen the same value as in ACSC.mCorpseClearCountdown, maybe // this one is for respawning? unsigned char mUnknown5[3]; }; struct ACSC { unsigned char mUnknown1[17]; unsigned char mFlags; // ACSCFlags unsigned char mUnknown2[22]; unsigned char mCorpseClearCountdown; // hours? unsigned char mUnknown3[71]; }; struct ANIS { unsigned char mGroupIndex; unsigned char mUnknown[3]; float mTime; }; #pragma pack(pop) struct ActorData : public ESM::CellRef { bool mHasACDT; ACDT mACDT; bool mHasACSC; ACSC mACSC; int mSkills[27][2]; // skills, base and modified // creature combat stats, base and modified // I think these can be ignored in the conversion, because it is not possible // to change them ingame int mCombatStats[3][2]; std::string mSelectedSpell; std::string mSelectedEnchantItem; SCRI mSCRI; bool mHasANIS; ANIS mANIS; // scripted animation state virtual void load(ESM::ESMReader& esm); virtual ~ActorData() = default; }; } #endif ================================================ FILE: apps/essimporter/importcellref.cpp ================================================ #include "importcellref.hpp" #include namespace ESSImport { void CellRef::load(ESM::ESMReader &esm) { blank(); // (FRMR subrecord name is already read by the loop in ConvertCell) esm.getHT(mRefNum.mIndex); // FRMR // this is required since openmw supports more than 255 content files int pluginIndex = (mRefNum.mIndex & 0xff000000) >> 24; mRefNum.mContentFile = pluginIndex-1; mRefNum.mIndex &= 0x00ffffff; mIndexedRefId = esm.getHNString("NAME"); ActorData::load(esm); if (esm.isNextSub("LVCR")) { // occurs on levelled creature spawner references // probably some identifier for the creature that has been spawned? unsigned char lvcr; esm.getHT(lvcr); //std::cout << "LVCR: " << (int)lvcr << std::endl; } mEnabled = true; esm.getHNOT(mEnabled, "ZNAM"); // DATA should occur for all references, except levelled creature spawners // I've seen DATA *twice* on a creature record, and with the exact same content too! weird // alarmvoi0000.ess esm.getHNOT(mPos, "DATA", 24); esm.getHNOT(mPos, "DATA", 24); mDeleted = 0; if (esm.isNextSub("DELE")) { unsigned int deleted; esm.getHT(deleted); mDeleted = ((deleted >> 24) & 0x2) != 0; // the other 3 bytes seem to be uninitialized garbage } if (esm.isNextSub("MVRF")) { esm.skipHSub(); esm.getSubName(); esm.skipHSub(); } } } ================================================ FILE: apps/essimporter/importcellref.hpp ================================================ #ifndef OPENMW_ESSIMPORT_CELLREF_H #define OPENMW_ESSIMPORT_CELLREF_H #include #include #include "importacdt.hpp" namespace ESM { class ESMReader; } namespace ESSImport { struct CellRef : public ActorData { std::string mIndexedRefId; std::string mScript; bool mEnabled; bool mDeleted; void load(ESM::ESMReader& esm) override; virtual ~CellRef() = default; }; } #endif ================================================ FILE: apps/essimporter/importcntc.cpp ================================================ #include "importcntc.hpp" #include namespace ESSImport { void CNTC::load(ESM::ESMReader &esm) { mIndex = 0; esm.getHNT(mIndex, "INDX"); mInventory.load(esm); } } ================================================ FILE: apps/essimporter/importcntc.hpp ================================================ #ifndef OPENMW_ESSIMPORT_IMPORTCNTC_H #define OPENMW_ESSIMPORT_IMPORTCNTC_H #include "importinventory.hpp" namespace ESM { class ESMReader; } namespace ESSImport { /// Changed container contents struct CNTC { int mIndex; Inventory mInventory; void load(ESM::ESMReader& esm); }; } #endif ================================================ FILE: apps/essimporter/importcrec.cpp ================================================ #include "importcrec.hpp" #include namespace ESSImport { void CREC::load(ESM::ESMReader &esm) { esm.getHNT(mIndex, "INDX"); // equivalent of ESM::Creature XSCL? probably don't have to convert this, // since the value can't be changed float scale; esm.getHNOT(scale, "XSCL"); while (esm.isNextSub("AI_W") || esm.isNextSub("AI_E") || esm.isNextSub("AI_T") || esm.isNextSub("AI_F") || esm.isNextSub("AI_A")) mAiPackages.add(esm); mInventory.load(esm); } } ================================================ FILE: apps/essimporter/importcrec.hpp ================================================ #ifndef OPENMW_ESSIMPORT_CREC_H #define OPENMW_ESSIMPORT_CREC_H #include "importinventory.hpp" #include namespace ESM { class ESMReader; } namespace ESSImport { /// Creature changes struct CREC { int mIndex; Inventory mInventory; ESM::AIPackageList mAiPackages; void load(ESM::ESMReader& esm); }; } #endif ================================================ FILE: apps/essimporter/importdial.cpp ================================================ #include "importdial.hpp" #include namespace ESSImport { void DIAL::load(ESM::ESMReader &esm) { // See ESM::Dialogue::Type enum, not sure why we would need this here though int type = 0; esm.getHNOT(type, "DATA"); // Deleted dialogue in a savefile. No clue what this means... int deleted = 0; esm.getHNOT(deleted, "DELE"); mIndex = 0; // *should* always occur except when the dialogue is deleted, but leaving it optional just in case... esm.getHNOT(mIndex, "XIDX"); } } ================================================ FILE: apps/essimporter/importdial.hpp ================================================ #ifndef OPENMW_ESSIMPORT_IMPORTDIAL_H #define OPENMW_ESSIMPORT_IMPORTDIAL_H namespace ESM { class ESMReader; } namespace ESSImport { struct DIAL { int mIndex; // Journal index void load(ESM::ESMReader& esm); }; } #endif ================================================ FILE: apps/essimporter/importer.cpp ================================================ #include "importer.hpp" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "importercontext.hpp" #include "converter.hpp" namespace { void writeScreenshot(const ESM::Header& fileHeader, ESM::SavedGame& out) { if (fileHeader.mSCRS.size() != 128*128*4) { std::cerr << "Error: unexpected screenshot size " << std::endl; return; } osg::ref_ptr image (new osg::Image); image->allocateImage(128, 128, 1, GL_RGB, GL_UNSIGNED_BYTE); // need to convert pixel format from BGRA to RGB as the jpg readerwriter doesn't support it otherwise auto it = fileHeader.mSCRS.begin(); for (int y=0; y<128; ++y) { for (int x=0; x<128; ++x) { assert(image->data(x,y)); *(image->data(x,y)+2) = *it++; *(image->data(x,y)+1) = *it++; *image->data(x,y) = *it++; ++it; // skip alpha } } image->flipVertical(); std::stringstream ostream; osgDB::ReaderWriter* readerwriter = osgDB::Registry::instance()->getReaderWriterForExtension("jpg"); if (!readerwriter) { std::cerr << "Error: can't write screenshot: no jpg readerwriter found" << std::endl; return; } osgDB::ReaderWriter::WriteResult result = readerwriter->writeImage(*image, ostream); if (!result.success()) { std::cerr << "Error: can't write screenshot: " << result.message() << " code " << result.status() << std::endl; return; } std::string data = ostream.str(); out.mScreenshot = std::vector(data.begin(), data.end()); } } namespace ESSImport { Importer::Importer(const std::string &essfile, const std::string &outfile, const std::string &encoding) : mEssFile(essfile) , mOutFile(outfile) , mEncoding(encoding) { } struct File { struct Subrecord { std::string mName; size_t mFileOffset; std::vector mData; }; struct Record { std::string mName; size_t mFileOffset; std::vector mSubrecords; }; std::vector mRecords; }; void read(const std::string& filename, File& file) { ESM::ESMReader esm; esm.open(filename); while (esm.hasMoreRecs()) { ESM::NAME n = esm.getRecName(); esm.getRecHeader(); File::Record rec; rec.mName = n.toString(); rec.mFileOffset = esm.getFileOffset(); while (esm.hasMoreSubs()) { File::Subrecord sub; esm.getSubName(); esm.getSubHeader(); sub.mFileOffset = esm.getFileOffset(); sub.mName = esm.retSubName().toString(); sub.mData.resize(esm.getSubSize()); esm.getExact(&sub.mData[0], sub.mData.size()); rec.mSubrecords.push_back(sub); } file.mRecords.push_back(rec); } } void Importer::compare() { // data that always changes (and/or is already fully decoded) should be blacklisted std::set > blacklist; blacklist.insert(std::make_pair("GLOB", "FLTV")); // gamehour blacklist.insert(std::make_pair("REFR", "DATA")); // player position blacklist.insert(std::make_pair("CELL", "NAM8")); // fog of war blacklist.insert(std::make_pair("GAME", "GMDT")); // weather data, current time always changes blacklist.insert(std::make_pair("CELL", "DELE")); // first 3 bytes are uninitialized // this changes way too often, name suggests some renderer internal data? blacklist.insert(std::make_pair("CELL", "ND3D")); blacklist.insert(std::make_pair("REFR", "ND3D")); File file1; read(mEssFile, file1); File file2; read(mOutFile, file2); // todo rename variable // FIXME: use max(size1, size2) for (unsigned int i=0; i= file2.mRecords.size()) { std::ios::fmtflags f(std::cout.flags()); std::cout << "Record in file1 not present in file2: (1) 0x" << std::hex << rec.mFileOffset << std::endl; std::cout.flags(f); return; } File::Record rec2 = file2.mRecords[i]; if (rec.mName != rec2.mName) { std::ios::fmtflags f(std::cout.flags()); std::cout << "Different record name at (2) 0x" << std::hex << rec2.mFileOffset << std::endl; std::cout.flags(f); return; // TODO: try to recover } // FIXME: use max(size1, size2) for (unsigned int j=0; j= rec2.mSubrecords.size()) { std::ios::fmtflags f(std::cout.flags()); std::cout << "Subrecord in file1 not present in file2: (1) 0x" << std::hex << sub.mFileOffset << std::endl; std::cout.flags(f); return; } File::Subrecord sub2 = rec2.mSubrecords[j]; if (sub.mName != sub2.mName) { std::ios::fmtflags f(std::cout.flags()); std::cout << "Different subrecord name (" << rec.mName << "." << sub.mName << " vs. " << sub2.mName << ") at (1) 0x" << std::hex << sub.mFileOffset << " (2) 0x" << sub2.mFileOffset << std::endl; std::cout.flags(f); break; // TODO: try to recover } if (sub.mData != sub2.mData) { if (blacklist.find(std::make_pair(rec.mName, sub.mName)) != blacklist.end()) continue; std::ios::fmtflags f(std::cout.flags()); std::cout << "Different subrecord data for " << rec.mName << "." << sub.mName << " at (1) 0x" << std::hex << sub.mFileOffset << " (2) 0x" << sub2.mFileOffset << std::endl; std::cout << "Data 1:" << std::endl; for (unsigned int k=0; k= sub2.mData.size() || sub2.mData[k] != sub.mData[k]) different = true; if (different) std::cout << "\033[033m"; std::cout << std::hex << std::setw(2) << std::setfill('0') << (int)sub.mData[k] << " "; if (different) std::cout << "\033[0m"; } std::cout << std::endl; std::cout << "Data 2:" << std::endl; for (unsigned int k=0; k= sub.mData.size() || sub.mData[k] != sub2.mData[k]) different = true; if (different) std::cout << "\033[033m"; std::cout << std::hex << std::setw(2) << std::setfill('0') << (int)sub2.mData[k] << " "; if (different) std::cout << "\033[0m"; } std::cout << std::endl; std::cout.flags(f); } } } } void Importer::run() { ToUTF8::Utf8Encoder encoder(ToUTF8::calculateEncoding(mEncoding)); ESM::ESMReader esm; esm.open(mEssFile); esm.setEncoder(&encoder); Context context; const ESM::Header& header = esm.getHeader(); context.mPlayerCellName = header.mGameData.mCurrentCell.toString(); const unsigned int recREFR = ESM::FourCC<'R','E','F','R'>::value; const unsigned int recPCDT = ESM::FourCC<'P','C','D','T'>::value; const unsigned int recFMAP = ESM::FourCC<'F','M','A','P'>::value; const unsigned int recKLST = ESM::FourCC<'K','L','S','T'>::value; const unsigned int recSTLN = ESM::FourCC<'S','T','L','N'>::value; const unsigned int recGAME = ESM::FourCC<'G','A','M','E'>::value; const unsigned int recJOUR = ESM::FourCC<'J','O','U','R'>::value; const unsigned int recSPLM = ESM::FourCC<'S','P','L','M'>::value; std::map > converters; converters[ESM::REC_GLOB] = std::shared_ptr(new ConvertGlobal()); converters[ESM::REC_BOOK] = std::shared_ptr(new ConvertBook()); converters[ESM::REC_NPC_] = std::shared_ptr(new ConvertNPC()); converters[ESM::REC_CREA] = std::shared_ptr(new ConvertCREA()); converters[ESM::REC_NPCC] = std::shared_ptr(new ConvertNPCC()); converters[ESM::REC_CREC] = std::shared_ptr(new ConvertCREC()); converters[recREFR ] = std::shared_ptr(new ConvertREFR()); converters[recPCDT ] = std::shared_ptr(new ConvertPCDT()); converters[recFMAP ] = std::shared_ptr(new ConvertFMAP()); converters[recKLST ] = std::shared_ptr(new ConvertKLST()); converters[recSTLN ] = std::shared_ptr(new ConvertSTLN()); converters[recGAME ] = std::shared_ptr(new ConvertGAME()); converters[ESM::REC_CELL] = std::shared_ptr(new ConvertCell()); converters[ESM::REC_ALCH] = std::shared_ptr(new DefaultConverter()); converters[ESM::REC_CLAS] = std::shared_ptr(new ConvertClass()); converters[ESM::REC_SPEL] = std::shared_ptr(new DefaultConverter()); converters[ESM::REC_ARMO] = std::shared_ptr(new DefaultConverter()); converters[ESM::REC_WEAP] = std::shared_ptr(new DefaultConverter()); converters[ESM::REC_CLOT] = std::shared_ptr(new DefaultConverter()); converters[ESM::REC_ENCH] = std::shared_ptr(new DefaultConverter()); converters[ESM::REC_WEAP] = std::shared_ptr(new DefaultConverter()); converters[ESM::REC_LEVC] = std::shared_ptr(new DefaultConverter()); converters[ESM::REC_LEVI] = std::shared_ptr(new DefaultConverter()); converters[ESM::REC_CNTC] = std::shared_ptr(new ConvertCNTC()); converters[ESM::REC_FACT] = std::shared_ptr(new ConvertFACT()); converters[ESM::REC_INFO] = std::shared_ptr(new ConvertINFO()); converters[ESM::REC_DIAL] = std::shared_ptr(new ConvertDIAL()); converters[ESM::REC_QUES] = std::shared_ptr(new ConvertQUES()); converters[recJOUR ] = std::shared_ptr(new ConvertJOUR()); converters[ESM::REC_SCPT] = std::shared_ptr(new ConvertSCPT()); converters[ESM::REC_PROJ] = std::shared_ptr(new ConvertPROJ()); converters[recSPLM] = std::shared_ptr(new ConvertSPLM()); // TODO: // - REGN (weather in certain regions?) // - VFXM // - SPLM (active spell effects) std::set unknownRecords; for (const auto & converter : converters) { converter.second->setContext(context); } while (esm.hasMoreRecs()) { ESM::NAME n = esm.getRecName(); esm.getRecHeader(); auto it = converters.find(n.intval); if (it != converters.end()) { it->second->read(esm); } else { if (unknownRecords.insert(n.intval).second) { std::ios::fmtflags f(std::cerr.flags()); std::cerr << "Error: unknown record " << n.toString() << " (0x" << std::hex << esm.getFileOffset() << ")" << std::endl; std::cerr.flags(f); } esm.skipRecord(); } } ESM::ESMWriter writer; writer.setFormat (ESM::SavedGame::sCurrentFormat); boost::filesystem::ofstream stream(boost::filesystem::path(mOutFile), std::ios::out | std::ios::binary); // all unused writer.setVersion(0); writer.setType(0); writer.setAuthor(""); writer.setDescription(""); writer.setRecordCount (0); for (const auto & master : header.mMaster) writer.addMaster(master.name, 0); // not using the size information anyway -> use value of 0 writer.save (stream); ESM::SavedGame profile; for (const auto & master : header.mMaster) { profile.mContentFiles.push_back(master.name); } profile.mDescription = esm.getDesc(); profile.mInGameTime.mDay = context.mDay; profile.mInGameTime.mGameHour = context.mHour; profile.mInGameTime.mMonth = context.mMonth; profile.mInGameTime.mYear = context.mYear; profile.mPlayerCell = header.mGameData.mCurrentCell.toString(); if (context.mPlayerBase.mClass == "NEWCLASSID_CHARGEN") profile.mPlayerClassName = context.mCustomPlayerClassName; else profile.mPlayerClassId = context.mPlayerBase.mClass; profile.mPlayerLevel = context.mPlayerBase.mNpdt.mLevel; profile.mPlayerName = header.mGameData.mPlayerName.toString(); writeScreenshot(header, profile); writer.startRecord (ESM::REC_SAVE); profile.save (writer); writer.endRecord (ESM::REC_SAVE); // Writing order should be Dynamic Store -> Cells -> Player, // so that references to dynamic records can be recognized when loading for (std::map >::const_iterator it = converters.begin(); it != converters.end(); ++it) { if (it->second->getStage() != 0) continue; it->second->write(writer); } writer.startRecord(ESM::REC_NPC_); context.mPlayerBase.mId = "player"; context.mPlayerBase.save(writer); writer.endRecord(ESM::REC_NPC_); for (std::map >::const_iterator it = converters.begin(); it != converters.end(); ++it) { if (it->second->getStage() != 1) continue; it->second->write(writer); } writer.startRecord(ESM::REC_PLAY); if (context.mPlayer.mCellId.mPaged) { // exterior cell -> determine cell coordinates based on position int cellX = static_cast(std::floor(context.mPlayer.mObject.mPosition.pos[0] / Constants::CellSizeInUnits)); int cellY = static_cast(std::floor(context.mPlayer.mObject.mPosition.pos[1] / Constants::CellSizeInUnits)); context.mPlayer.mCellId.mIndex.mX = cellX; context.mPlayer.mCellId.mIndex.mY = cellY; } context.mPlayer.save(writer); writer.endRecord(ESM::REC_PLAY); writer.startRecord(ESM::REC_ACTC); writer.writeHNT("COUN", context.mNextActorId); writer.endRecord(ESM::REC_ACTC); // Stage 2 requires cell references to be written / actors IDs assigned for (std::map >::const_iterator it = converters.begin(); it != converters.end(); ++it) { if (it->second->getStage() != 2) continue; it->second->write(writer); } writer.startRecord (ESM::REC_DIAS); context.mDialogueState.save(writer); writer.endRecord(ESM::REC_DIAS); writer.startRecord(ESM::REC_INPU); context.mControlsState.save(writer); writer.endRecord(ESM::REC_INPU); } } ================================================ FILE: apps/essimporter/importer.hpp ================================================ #ifndef OPENMW_ESSIMPORTER_IMPORTER_H #define OPENMW_ESSIMPORTER_IMPORTER_H #include namespace ESSImport { class Importer { public: Importer(const std::string& essfile, const std::string& outfile, const std::string& encoding); void run(); void compare(); private: std::string mEssFile; std::string mOutFile; std::string mEncoding; }; } #endif ================================================ FILE: apps/essimporter/importercontext.cpp ================================================ ================================================ FILE: apps/essimporter/importercontext.hpp ================================================ #ifndef OPENMW_ESSIMPORT_CONTEXT_H #define OPENMW_ESSIMPORT_CONTEXT_H #include #include #include #include #include #include #include #include #include "importnpcc.hpp" #include "importcrec.hpp" #include "importcntc.hpp" #include "importplayer.hpp" #include "importsplm.h" namespace ESSImport { struct Context { // set from the TES3 header std::string mPlayerCellName; ESM::Player mPlayer; ESM::NPC mPlayerBase; std::string mCustomPlayerClassName; ESM::DialogueState mDialogueState; ESM::ControlsState mControlsState; // cells which should show an explored overlay on the global map std::set > mExploredCells; ESM::GlobalMap mGlobalMapState; int mDay, mMonth, mYear; float mHour; // key std::map, CREC> mCreatureChanges; std::map, NPCC> mNpcChanges; std::map, CNTC> mContainerChanges; std::map, int> mActorIdMap; int mNextActorId; std::map mCreatures; std::map mNpcs; std::vector mActiveSpells; Context() : mDay(0) , mMonth(0) , mYear(0) , mHour(0.f) , mNextActorId(0) { ESM::CellId playerCellId; playerCellId.mPaged = true; playerCellId.mIndex.mX = playerCellId.mIndex.mY = 0; mPlayer.mCellId = playerCellId; mPlayer.mLastKnownExteriorPosition[0] = mPlayer.mLastKnownExteriorPosition[1] = mPlayer.mLastKnownExteriorPosition[2] = 0.0f; mPlayer.mHasMark = 0; mPlayer.mCurrentCrimeId = -1; // TODO mPlayer.mPaidCrimeId = -1; mPlayer.mObject.blank(); mPlayer.mObject.mEnabled = true; mPlayer.mObject.mRef.mRefID = "player"; // REFR.mRefID would be PlayerSaveGame mPlayer.mObject.mCreatureStats.mActorId = generateActorId(); mGlobalMapState.mBounds.mMinX = 0; mGlobalMapState.mBounds.mMaxX = 0; mGlobalMapState.mBounds.mMinY = 0; mGlobalMapState.mBounds.mMaxY = 0; mPlayerBase.blank(); } int generateActorId() { return mNextActorId++; } }; } #endif ================================================ FILE: apps/essimporter/importgame.cpp ================================================ #include "importgame.hpp" #include namespace ESSImport { void GAME::load(ESM::ESMReader &esm) { esm.getSubNameIs("GMDT"); esm.getSubHeader(); if (esm.getSubSize() == 92) { esm.getExact(&mGMDT, 92); mGMDT.mSecundaPhase = 0; } else if (esm.getSubSize() == 96) { esm.getT(mGMDT); } else esm.fail("unexpected subrecord size for GAME.GMDT"); mGMDT.mWeatherTransition &= (0x000000ff); mGMDT.mSecundaPhase &= (0x000000ff); mGMDT.mMasserPhase &= (0x000000ff); } } ================================================ FILE: apps/essimporter/importgame.hpp ================================================ #ifndef OPENMW_ESSIMPORT_GAME_H #define OPENMW_ESSIMPORT_GAME_H namespace ESM { class ESMReader; } namespace ESSImport { /// Weather data struct GAME { struct GMDT { char mCellName[64] {}; int mFogColour {0}; float mFogDensity {0.f}; int mCurrentWeather {0}, mNextWeather {0}; int mWeatherTransition {0}; // 0-100 transition between weathers, top 3 bytes may be garbage float mTimeOfNextTransition {0.f}; // weather changes when gamehour == timeOfNextTransition int mMasserPhase {0}, mSecundaPhase {0}; // top 3 bytes may be garbage }; GMDT mGMDT; void load(ESM::ESMReader& esm); }; } #endif ================================================ FILE: apps/essimporter/importinfo.cpp ================================================ #include "importinfo.hpp" #include namespace ESSImport { void INFO::load(ESM::ESMReader &esm) { mInfo = esm.getHNString("INAM"); mActorRefId = esm.getHNString("ACDT"); } } ================================================ FILE: apps/essimporter/importinfo.hpp ================================================ #ifndef OPENMW_ESSIMPORT_IMPORTINFO_H #define OPENMW_ESSIMPORT_IMPORTINFO_H #include namespace ESM { class ESMReader; } namespace ESSImport { struct INFO { std::string mInfo; std::string mActorRefId; void load(ESM::ESMReader& esm); }; } #endif ================================================ FILE: apps/essimporter/importinventory.cpp ================================================ #include "importinventory.hpp" #include #include namespace ESSImport { void Inventory::load(ESM::ESMReader &esm) { while (esm.isNextSub("NPCO")) { ContItem contItem; esm.getHT(contItem); InventoryItem item; item.mId = contItem.mItem.toString(); item.mCount = contItem.mCount; item.mRelativeEquipmentSlot = -1; item.mLockLevel = 0; item.mRefNum.unset(); unsigned int itemCount = std::abs(item.mCount); bool separateStacks = false; for (unsigned int i=0;i= int(mItems.size())) esm.fail("equipment item index out of range"); // appears to be a relative index for only the *possible* slots this item can be equipped in, // i.e. 0 most of the time int slotIndex; esm.getT(slotIndex); mItems[itemIndex].mRelativeEquipmentSlot = slotIndex; } } } ================================================ FILE: apps/essimporter/importinventory.hpp ================================================ #ifndef OPENMW_ESSIMPORT_IMPORTINVENTORY_H #define OPENMW_ESSIMPORT_IMPORTINVENTORY_H #include #include #include #include #include "importscri.hpp" namespace ESM { class ESMReader; } namespace ESSImport { struct ContItem { int mCount; ESM::NAME32 mItem; }; struct Inventory { struct InventoryItem : public ESM::CellRef { std::string mId; int mCount; int mRelativeEquipmentSlot; SCRI mSCRI; }; std::vector mItems; void load(ESM::ESMReader& esm); }; } #endif ================================================ FILE: apps/essimporter/importjour.cpp ================================================ #include "importjour.hpp" #include namespace ESSImport { void JOUR::load(ESM::ESMReader &esm) { mText = esm.getHNString("NAME"); } } ================================================ FILE: apps/essimporter/importjour.hpp ================================================ #ifndef OPENMW_ESSIMPORT_IMPORTJOUR_H #define OPENMW_ESSIMPORT_IMPORTJOUR_H #include namespace ESM { class ESMReader; } namespace ESSImport { /// Journal struct JOUR { // The entire journal, in HTML std::string mText; void load(ESM::ESMReader& esm); }; } #endif ================================================ FILE: apps/essimporter/importklst.cpp ================================================ #include "importklst.hpp" #include namespace ESSImport { void KLST::load(ESM::ESMReader &esm) { while (esm.isNextSub("KNAM")) { std::string refId = esm.getHString(); int count; esm.getHNT(count, "CNAM"); mKillCounter[refId] = count; } mWerewolfKills = 0; esm.getHNOT(mWerewolfKills, "INTV"); } } ================================================ FILE: apps/essimporter/importklst.hpp ================================================ #ifndef OPENMW_ESSIMPORT_KLST_H #define OPENMW_ESSIMPORT_KLST_H #include #include namespace ESM { class ESMReader; } namespace ESSImport { /// Kill Stats struct KLST { void load(ESM::ESMReader& esm); /// RefId, kill count std::map mKillCounter; int mWerewolfKills; }; } #endif ================================================ FILE: apps/essimporter/importnpcc.cpp ================================================ #include "importnpcc.hpp" #include namespace ESSImport { void NPCC::load(ESM::ESMReader &esm) { esm.getHNT(mNPDT, "NPDT"); while (esm.isNextSub("AI_W") || esm.isNextSub("AI_E") || esm.isNextSub("AI_T") || esm.isNextSub("AI_F") || esm.isNextSub("AI_A")) mAiPackages.add(esm); mInventory.load(esm); } } ================================================ FILE: apps/essimporter/importnpcc.hpp ================================================ #ifndef OPENMW_ESSIMPORT_NPCC_H #define OPENMW_ESSIMPORT_NPCC_H #include #include #include "importinventory.hpp" namespace ESM { class ESMReader; } namespace ESSImport { struct NPCC { struct NPDT { unsigned char mDisposition; unsigned char unknown; unsigned char mReputation; unsigned char unknown2; int mIndex; } mNPDT; Inventory mInventory; ESM::AIPackageList mAiPackages; void load(ESM::ESMReader &esm); }; } #endif ================================================ FILE: apps/essimporter/importplayer.cpp ================================================ #include "importplayer.hpp" #include namespace ESSImport { void REFR::load(ESM::ESMReader &esm) { esm.getHNT(mRefNum.mIndex, "FRMR"); mRefID = esm.getHNString("NAME"); mActorData.load(esm); esm.getHNOT(mPos, "DATA", 24); } void PCDT::load(ESM::ESMReader &esm) { while (esm.isNextSub("DNAM")) { mKnownDialogueTopics.push_back(esm.getHString()); } mHasMark = false; if (esm.isNextSub("MNAM")) { mHasMark = true; mMNAM = esm.getHString(); } esm.getHNT(mPNAM, "PNAM"); if (esm.isNextSub("SNAM")) esm.skipHSub(); if (esm.isNextSub("NAM9")) esm.skipHSub(); // Rest state. You shouldn't even be able to save during rest, but skip just in case. if (esm.isNextSub("RNAM")) /* int hoursLeft; float x, y, z; // resting position */ esm.skipHSub(); // 16 bytes mBounty = 0; esm.getHNOT(mBounty, "CNAM"); mBirthsign = esm.getHNOString("BNAM"); // Holds the names of the last used Alchemy apparatus. Don't need to import this ATM, // because our GUI auto-selects the best apparatus. if (esm.isNextSub("NAM0")) esm.skipHSub(); if (esm.isNextSub("NAM1")) esm.skipHSub(); if (esm.isNextSub("NAM2")) esm.skipHSub(); if (esm.isNextSub("NAM3")) esm.skipHSub(); mHasENAM = false; if (esm.isNextSub("ENAM")) { mHasENAM = true; esm.getHT(mENAM); } if (esm.isNextSub("LNAM")) esm.skipHSub(); while (esm.isNextSub("FNAM")) { FNAM fnam; esm.getHT(fnam); mFactions.push_back(fnam); } mHasAADT = false; if (esm.isNextSub("AADT")) // Attack animation data? { mHasAADT = true; esm.getHT(mAADT); } if (esm.isNextSub("KNAM")) esm.skipHSub(); // assigned Quick Keys, I think if (esm.isNextSub("ANIS")) esm.skipHSub(); // 16 bytes if (esm.isNextSub("WERE")) { // some werewolf data, 152 bytes // maybe current skills and attributes for werewolf form esm.getSubHeader(); esm.skip(152); } } } ================================================ FILE: apps/essimporter/importplayer.hpp ================================================ #ifndef OPENMW_ESSIMPORT_PLAYER_H #define OPENMW_ESSIMPORT_PLAYER_H #include #include #include #include #include #include "importacdt.hpp" namespace ESM { class ESMReader; } namespace ESSImport { /// Player-agnostic player data struct REFR { ActorData mActorData; std::string mRefID; ESM::Position mPos; ESM::RefNum mRefNum; void load(ESM::ESMReader& esm); }; /// Other player data struct PCDT { int mBounty; std::string mBirthsign; std::vector mKnownDialogueTopics; enum PlayerFlags { PlayerFlags_ViewSwitchDisabled = 0x1, PlayerFlags_ControlsDisabled = 0x4, PlayerFlags_Sleeping = 0x10, PlayerFlags_Waiting = 0x40, PlayerFlags_WeaponDrawn = 0x80, PlayerFlags_SpellDrawn = 0x100, PlayerFlags_InJail = 0x200, PlayerFlags_JumpingDisabled = 0x1000, PlayerFlags_LookingDisabled = 0x2000, PlayerFlags_VanityModeDisabled = 0x4000, PlayerFlags_WeaponDrawingDisabled = 0x8000, PlayerFlags_SpellDrawingDisabled = 0x10000, PlayerFlags_ThirdPerson = 0x20000, PlayerFlags_TeleportingDisabled = 0x40000, PlayerFlags_LevitationDisabled = 0x80000 }; #pragma pack(push) #pragma pack(1) struct FNAM { unsigned char mRank; unsigned char mUnknown1[3]; int mReputation; unsigned char mFlags; // 0x1: unknown, 0x2: expelled unsigned char mUnknown2[3]; ESM::NAME32 mFactionName; }; struct PNAM { struct MarkLocation { float mX, mY, mZ; // worldspace position float mRotZ; // Z angle in radians int mCellX, mCellY; // grid coordinates; for interior cells this is always (0, 0) }; int mPlayerFlags; // controls, camera and draw state unsigned int mLevelProgress; float mSkillProgress[27]; // skill progress, non-uniform scaled unsigned char mSkillIncreases[8]; // number of skill increases for each attribute int mTelekinesisRangeBonus; // in units; seems redundant float mVisionBonus; // range: <0.0, 1.0>; affected by light spells and Get/Mod/SetPCVisionBonus int mDetectKeyMagnitude; // seems redundant int mDetectEnchantmentMagnitude; // seems redundant int mDetectAnimalMagnitude; // seems redundant MarkLocation mMarkLocation; unsigned char mUnknown3[40]; unsigned char mSpecIncreases[3]; // number of skill increases for each specialization unsigned char mUnknown4; }; struct ENAM { int mCellX; int mCellY; }; struct AADT // 44 bytes { int animGroupIndex; // See convertANIS() for the mapping. unsigned char mUnknown5[40]; }; #pragma pack(pop) std::vector mFactions; PNAM mPNAM; bool mHasMark; std::string mMNAM; // mark cell name; can also be sDefaultCellname or region name bool mHasENAM; ENAM mENAM; // last exterior cell bool mHasAADT; AADT mAADT; void load(ESM::ESMReader& esm); }; } #endif ================================================ FILE: apps/essimporter/importproj.cpp ================================================ #include "importproj.h" #include namespace ESSImport { void ESSImport::PROJ::load(ESM::ESMReader& esm) { while (esm.isNextSub("PNAM")) { PNAM pnam; esm.getHT(pnam); mProjectiles.push_back(pnam); } } } ================================================ FILE: apps/essimporter/importproj.h ================================================ #ifndef OPENMW_ESSIMPORT_IMPORTPROJ_H #define OPENMW_ESSIMPORT_IMPORTPROJ_H #include #include #include namespace ESM { class ESMReader; } namespace ESSImport { struct PROJ { #pragma pack(push) #pragma pack(1) struct PNAM // 184 bytes { float mAttackStrength; float mSpeed; unsigned char mUnknown[4*2]; float mFlightTime; int mSplmIndex; // reference to a SPLM record (0 for ballistic projectiles) unsigned char mUnknown2[4]; ESM::Vector3 mVelocity; ESM::Vector3 mPosition; unsigned char mUnknown3[4*9]; ESM::NAME32 mActorId; // indexed refID (with the exception of "PlayerSaveGame") ESM::NAME32 mArrowId; ESM::NAME32 mBowId; bool isMagic() const { return mSplmIndex != 0; } }; #pragma pack(pop) std::vector mProjectiles; void load(ESM::ESMReader& esm); }; } #endif ================================================ FILE: apps/essimporter/importques.cpp ================================================ #include "importques.hpp" #include namespace ESSImport { void QUES::load(ESM::ESMReader &esm) { while (esm.isNextSub("DATA")) mInfo.push_back(esm.getHString()); } } ================================================ FILE: apps/essimporter/importques.hpp ================================================ #ifndef OPENMW_ESSIMPORT_IMPORTQUES_H #define OPENMW_ESSIMPORT_IMPORTQUES_H #include #include namespace ESM { class ESMReader; } namespace ESSImport { /// State for a quest /// Presumably this record only exists when Tribunal is installed, /// since pre-Tribunal there weren't any quest names in the data files. struct QUES { std::string mName; // NAME, should be assigned from outside as usual std::vector mInfo; // list of journal entries for the quest void load(ESM::ESMReader& esm); }; } #endif ================================================ FILE: apps/essimporter/importscpt.cpp ================================================ #include "importscpt.hpp" #include namespace ESSImport { void SCPT::load(ESM::ESMReader &esm) { esm.getHNT(mSCHD, "SCHD"); mSCRI.load(esm); mRefNum = -1; if (esm.isNextSub("RNAM")) { mRunning = true; esm.getHT(mRefNum); } else mRunning = false; } } ================================================ FILE: apps/essimporter/importscpt.hpp ================================================ #ifndef OPENMW_ESSIMPORT_IMPORTSCPT_H #define OPENMW_ESSIMPORT_IMPORTSCPT_H #include "importscri.hpp" #include namespace ESM { class ESMReader; } namespace ESSImport { struct SCHD { ESM::NAME32 mName; ESM::Script::SCHDstruct mData; }; // A running global script struct SCPT { SCHD mSCHD; // values of local variables SCRI mSCRI; bool mRunning; int mRefNum; // Targeted reference, -1: no reference void load(ESM::ESMReader& esm); }; } #endif ================================================ FILE: apps/essimporter/importscri.cpp ================================================ #include "importscri.hpp" #include namespace ESSImport { void SCRI::load(ESM::ESMReader &esm) { mScript = esm.getHNOString("SCRI"); int numShorts = 0, numLongs = 0, numFloats = 0; if (esm.isNextSub("SLCS")) { esm.getSubHeader(); esm.getT(numShorts); esm.getT(numLongs); esm.getT(numFloats); } if (esm.isNextSub("SLSD")) { esm.getSubHeader(); for (int i=0; i #include namespace ESM { class ESMReader; } namespace ESSImport { /// Local variable assignments for a running script struct SCRI { std::string mScript; std::vector mShorts; std::vector mLongs; std::vector mFloats; void load(ESM::ESMReader& esm); }; } #endif ================================================ FILE: apps/essimporter/importsplm.cpp ================================================ #include "importsplm.h" #include namespace ESSImport { void SPLM::load(ESM::ESMReader& esm) { while (esm.isNextSub("NAME")) { ActiveSpell spell; esm.getHT(spell.mIndex); esm.getHNT(spell.mSPDT, "SPDT"); spell.mTarget = esm.getHNOString("TNAM"); while (esm.isNextSub("NPDT")) { ActiveEffect effect; esm.getHT(effect.mNPDT); // Effect-specific subrecords can follow: // - INAM for disintegration and bound effects // - CNAM for summoning and command effects // - VNAM for vampirism // NOTE: There can be multiple INAMs per effect. // TODO: Needs more research. esm.skipHSubUntil("NAM0"); // sentinel esm.getSubName(); esm.skipHSub(); spell.mActiveEffects.push_back(effect); } unsigned char xnam; // sentinel esm.getHNT(xnam, "XNAM"); mActiveSpells.push_back(spell); } } } ================================================ FILE: apps/essimporter/importsplm.h ================================================ #ifndef OPENMW_ESSIMPORT_IMPORTSPLM_H #define OPENMW_ESSIMPORT_IMPORTSPLM_H #include #include #include namespace ESM { class ESMReader; } namespace ESSImport { struct SPLM { #pragma pack(push) #pragma pack(1) struct SPDT // 160 bytes { int mType; // 1 = spell, 2 = enchantment, 3 = potion ESM::NAME32 mId; // base ID of a spell/enchantment/potion unsigned char mUnknown[4*4]; ESM::NAME32 mCasterId; ESM::NAME32 mSourceId; // empty for spells unsigned char mUnknown2[4*11]; }; struct NPDT // 56 bytes { ESM::NAME32 mAffectedActorId; unsigned char mUnknown[4*2]; int mMagnitude; float mSecondsActive; unsigned char mUnknown2[4*2]; }; struct INAM // 40 bytes { int mUnknown; unsigned char mUnknown2; ESM::FIXED_STRING<35> mItemId; // disintegrated item / bound item / item to re-equip after expiration }; struct CNAM // 36 bytes { int mUnknown; // seems to always be 0 ESM::NAME32 mSummonedOrCommandedActor[32]; }; struct VNAM // 4 bytes { int mUnknown; }; #pragma pack(pop) struct ActiveEffect { NPDT mNPDT; }; struct ActiveSpell { int mIndex; SPDT mSPDT; std::string mTarget; std::vector mActiveEffects; }; std::vector mActiveSpells; void load(ESM::ESMReader& esm); }; } #endif ================================================ FILE: apps/essimporter/main.cpp ================================================ #include #include #include #include #include "importer.hpp" namespace bpo = boost::program_options; namespace bfs = boost::filesystem; int main(int argc, char** argv) { try { bpo::options_description desc("Syntax: openmw-essimporter infile.ess outfile.omwsave\nAllowed options"); bpo::positional_options_description p_desc; desc.add_options() ("help,h", "produce help message") ("mwsave,m", bpo::value(), "morrowind .ess save file") ("output,o", bpo::value(), "output file (.omwsave)") ("compare,c", "compare two .ess files") ("encoding", boost::program_options::value()->default_value("win1252"), "encoding of the save file") ; p_desc.add("mwsave", 1).add("output", 1); bpo::variables_map variables; bpo::parsed_options parsed = bpo::command_line_parser(argc, argv) .options(desc) .positional(p_desc) .run(); bpo::store(parsed, variables); if(variables.count("help") || !variables.count("mwsave") || !variables.count("output")) { std::cout << desc; return 0; } bpo::notify(variables); Files::ConfigurationManager cfgManager(true); cfgManager.readConfiguration(variables, desc); std::string essFile = variables["mwsave"].as(); std::string outputFile = variables["output"].as(); std::string encoding = variables["encoding"].as(); ESSImport::Importer importer(essFile, outputFile, encoding); if (variables.count("compare")) importer.compare(); else { const std::string& ext = ".omwsave"; if (bfs::exists(bfs::path(outputFile)) && (outputFile.size() < ext.size() || outputFile.substr(outputFile.size()-ext.size()) != ext)) { throw std::runtime_error("Output file already exists and does not end in .omwsave. Did you mean to use --compare?"); } importer.run(); } } catch (std::exception& e) { std::cerr << "Error: " << e.what() << std::endl; return 1; } return 0; } ================================================ FILE: apps/launcher/CMakeLists.txt ================================================ set(LAUNCHER datafilespage.cpp graphicspage.cpp sdlinit.cpp main.cpp maindialog.cpp playpage.cpp textslotmsgbox.cpp settingspage.cpp advancedpage.cpp utils/cellnameloader.cpp utils/profilescombobox.cpp utils/textinputdialog.cpp utils/lineedit.cpp utils/openalutil.cpp ${CMAKE_SOURCE_DIR}/files/windows/launcher.rc ) set(LAUNCHER_HEADER datafilespage.hpp graphicspage.hpp sdlinit.hpp maindialog.hpp playpage.hpp textslotmsgbox.hpp settingspage.hpp advancedpage.hpp utils/cellnameloader.hpp utils/profilescombobox.hpp utils/textinputdialog.hpp utils/lineedit.hpp utils/openalutil.hpp ) # Headers that must be pre-processed set(LAUNCHER_HEADER_MOC datafilespage.hpp graphicspage.hpp maindialog.hpp playpage.hpp textslotmsgbox.hpp settingspage.hpp advancedpage.hpp utils/cellnameloader.hpp utils/textinputdialog.hpp utils/profilescombobox.hpp utils/lineedit.hpp utils/openalutil.hpp ) set(LAUNCHER_UI ${CMAKE_SOURCE_DIR}/files/ui/datafilespage.ui ${CMAKE_SOURCE_DIR}/files/ui/graphicspage.ui ${CMAKE_SOURCE_DIR}/files/ui/mainwindow.ui ${CMAKE_SOURCE_DIR}/files/ui/playpage.ui ${CMAKE_SOURCE_DIR}/files/ui/contentselector.ui ${CMAKE_SOURCE_DIR}/files/ui/settingspage.ui ${CMAKE_SOURCE_DIR}/files/ui/advancedpage.ui ) source_group(launcher FILES ${LAUNCHER} ${LAUNCHER_HEADER}) set(QT_USE_QTGUI 1) # Set some platform specific settings if(WIN32) set(GUI_TYPE WIN32) set(QT_USE_QTMAIN TRUE) endif(WIN32) QT5_ADD_RESOURCES(RCC_SRCS ${CMAKE_SOURCE_DIR}/files/launcher/launcher.qrc) QT5_WRAP_CPP(MOC_SRCS ${LAUNCHER_HEADER_MOC}) QT5_WRAP_UI(UI_HDRS ${LAUNCHER_UI}) include_directories(${CMAKE_CURRENT_BINARY_DIR}) if(NOT WIN32) include_directories(${LIBUNSHIELD_INCLUDE_DIR}) endif(NOT WIN32) # Main executable openmw_add_executable(openmw-launcher ${GUI_TYPE} ${LAUNCHER} ${LAUNCHER_HEADER} ${RCC_SRCS} ${MOC_SRCS} ${UI_HDRS} ) if (WIN32) INSTALL(TARGETS openmw-launcher RUNTIME DESTINATION ".") endif (WIN32) target_link_libraries(openmw-launcher ${SDL2_LIBRARY_ONLY} ${OPENAL_LIBRARY} components ) target_link_libraries(openmw-launcher Qt5::Widgets Qt5::Core) if (BUILD_WITH_CODE_COVERAGE) add_definitions (--coverage) target_link_libraries(openmw-launcher gcov) endif() ================================================ FILE: apps/launcher/advancedpage.cpp ================================================ #include "advancedpage.hpp" #include #include #include #include #include #include #include #include #include "utils/openalutil.hpp" Launcher::AdvancedPage::AdvancedPage(Config::GameSettings &gameSettings, QWidget *parent) : QWidget(parent) , mGameSettings(gameSettings) { setObjectName ("AdvancedPage"); setupUi(this); for(const char * name : Launcher::enumerateOpenALDevices()) { audioDeviceSelectorComboBox->addItem(QString::fromUtf8(name), QString::fromUtf8(name)); } for(const char * name : Launcher::enumerateOpenALDevicesHrtf()) { hrtfProfileSelectorComboBox->addItem(QString::fromUtf8(name), QString::fromUtf8(name)); } loadSettings(); mCellNameCompleter.setModel(&mCellNameCompleterModel); startDefaultCharacterAtField->setCompleter(&mCellNameCompleter); } void Launcher::AdvancedPage::loadCellsForAutocomplete(QStringList cellNames) { // Update the list of suggestions for the "Start default character at" field mCellNameCompleterModel.setStringList(cellNames); mCellNameCompleter.setCompletionMode(QCompleter::PopupCompletion); mCellNameCompleter.setCaseSensitivity(Qt::CaseSensitivity::CaseInsensitive); } void Launcher::AdvancedPage::on_skipMenuCheckBox_stateChanged(int state) { startDefaultCharacterAtLabel->setEnabled(state == Qt::Checked); startDefaultCharacterAtField->setEnabled(state == Qt::Checked); } void Launcher::AdvancedPage::on_runScriptAfterStartupBrowseButton_clicked() { QString scriptFile = QFileDialog::getOpenFileName( this, QObject::tr("Select script file"), QDir::currentPath(), QString(tr("Text file (*.txt)"))); if (scriptFile.isEmpty()) return; QFileInfo info(scriptFile); if (!info.exists() || !info.isReadable()) return; const QString path(QDir::toNativeSeparators(info.absoluteFilePath())); runScriptAfterStartupField->setText(path); } namespace { constexpr double CellSizeInUnits = 8192; double convertToCells(double unitRadius) { return std::round((unitRadius + 1024) / CellSizeInUnits); } double convertToUnits(double CellGridRadius) { return CellSizeInUnits * CellGridRadius - 1024; } } bool Launcher::AdvancedPage::loadSettings() { // Game mechanics { loadSettingBool(toggleSneakCheckBox, "toggle sneak", "Input"); loadSettingBool(canLootDuringDeathAnimationCheckBox, "can loot during death animation", "Game"); loadSettingBool(followersAttackOnSightCheckBox, "followers attack on sight", "Game"); loadSettingBool(rebalanceSoulGemValuesCheckBox, "rebalance soul gem values", "Game"); loadSettingBool(enchantedWeaponsMagicalCheckBox, "enchanted weapons are magical", "Game"); loadSettingBool(permanentBarterDispositionChangeCheckBox, "barter disposition change is permanent", "Game"); loadSettingBool(classicReflectedAbsorbSpellsCheckBox, "classic reflected absorb spells behavior", "Game"); loadSettingBool(requireAppropriateAmmunitionCheckBox, "only appropriate ammunition bypasses resistance", "Game"); loadSettingBool(uncappedDamageFatigueCheckBox, "uncapped damage fatigue", "Game"); loadSettingBool(normaliseRaceSpeedCheckBox, "normalise race speed", "Game"); loadSettingBool(swimUpwardCorrectionCheckBox, "swim upward correction", "Game"); loadSettingBool(avoidCollisionsCheckBox, "NPCs avoid collisions", "Game"); int unarmedFactorsStrengthIndex = Settings::Manager::getInt("strength influences hand to hand", "Game"); if (unarmedFactorsStrengthIndex >= 0 && unarmedFactorsStrengthIndex <= 2) unarmedFactorsStrengthComboBox->setCurrentIndex(unarmedFactorsStrengthIndex); loadSettingBool(stealingFromKnockedOutCheckBox, "always allow stealing from knocked out actors", "Game"); loadSettingBool(enableNavigatorCheckBox, "enable", "Navigator"); int numPhysicsThreads = Settings::Manager::getInt("async num threads", "Physics"); if (numPhysicsThreads >= 0) physicsThreadsSpinBox->setValue(numPhysicsThreads); loadSettingBool(allowNPCToFollowOverWaterSurfaceCheckBox, "allow actors to follow over water surface", "Game"); } // Visuals { loadSettingBool(autoUseObjectNormalMapsCheckBox, "auto use object normal maps", "Shaders"); loadSettingBool(autoUseObjectSpecularMapsCheckBox, "auto use object specular maps", "Shaders"); loadSettingBool(autoUseTerrainNormalMapsCheckBox, "auto use terrain normal maps", "Shaders"); loadSettingBool(autoUseTerrainSpecularMapsCheckBox, "auto use terrain specular maps", "Shaders"); loadSettingBool(bumpMapLocalLightingCheckBox, "apply lighting to environment maps", "Shaders"); loadSettingBool(radialFogCheckBox, "radial fog", "Shaders"); loadSettingBool(magicItemAnimationsCheckBox, "use magic item animations", "Game"); connect(animSourcesCheckBox, SIGNAL(toggled(bool)), this, SLOT(slotAnimSourcesToggled(bool))); loadSettingBool(animSourcesCheckBox, "use additional anim sources", "Game"); if (animSourcesCheckBox->checkState() != Qt::Unchecked) { loadSettingBool(weaponSheathingCheckBox, "weapon sheathing", "Game"); loadSettingBool(shieldSheathingCheckBox, "shield sheathing", "Game"); } loadSettingBool(turnToMovementDirectionCheckBox, "turn to movement direction", "Game"); loadSettingBool(smoothMovementCheckBox, "smooth movement", "Game"); const bool distantTerrain = Settings::Manager::getBool("distant terrain", "Terrain"); const bool objectPaging = Settings::Manager::getBool("object paging", "Terrain"); if (distantTerrain && objectPaging) { distantLandCheckBox->setCheckState(Qt::Checked); } 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")); } // Audio { std::string selectedAudioDevice = Settings::Manager::getString("device", "Sound"); if (selectedAudioDevice.empty() == false) { int audioDeviceIndex = audioDeviceSelectorComboBox->findData(QString::fromStdString(selectedAudioDevice)); if (audioDeviceIndex != -1) { audioDeviceSelectorComboBox->setCurrentIndex(audioDeviceIndex); } } int hrtfEnabledIndex = Settings::Manager::getInt("hrtf enable", "Sound"); if (hrtfEnabledIndex >= -1 && hrtfEnabledIndex <= 1) { enableHRTFComboBox->setCurrentIndex(hrtfEnabledIndex + 1); } std::string selectedHRTFProfile = Settings::Manager::getString("hrtf", "Sound"); if (selectedHRTFProfile.empty() == false) { int hrtfProfileIndex = hrtfProfileSelectorComboBox->findData(QString::fromStdString(selectedHRTFProfile)); if (hrtfProfileIndex != -1) { hrtfProfileSelectorComboBox->setCurrentIndex(hrtfProfileIndex); } } } // Camera { loadSettingBool(viewOverShoulderCheckBox, "view over shoulder", "Camera"); connect(viewOverShoulderCheckBox, SIGNAL(toggled(bool)), this, SLOT(slotViewOverShoulderToggled(bool))); viewOverShoulderVerticalLayout->setEnabled(viewOverShoulderCheckBox->checkState()); loadSettingBool(autoSwitchShoulderCheckBox, "auto switch shoulder", "Camera"); loadSettingBool(previewIfStandStillCheckBox, "preview if stand still", "Camera"); loadSettingBool(deferredPreviewRotationCheckBox, "deferred preview rotation", "Camera"); loadSettingBool(headBobbingCheckBox, "head bobbing", "Camera"); defaultShoulderComboBox->setCurrentIndex( Settings::Manager::getVector2("view over shoulder offset", "Camera").x() >= 0 ? 0 : 1); } // Interface Changes { loadSettingBool(showEffectDurationCheckBox, "show effect duration", "Game"); loadSettingBool(showEnchantChanceCheckBox, "show enchant chance", "Game"); loadSettingBool(showMeleeInfoCheckBox, "show melee info", "Game"); loadSettingBool(showProjectileDamageCheckBox, "show projectile damage", "Game"); loadSettingBool(changeDialogTopicsCheckBox, "color topic enable", "GUI"); int showOwnedIndex = Settings::Manager::getInt("show owned", "Game"); // Match the index with the option (only 0, 1, 2, or 3 are valid). Will default to 0 if invalid. if (showOwnedIndex >= 0 && showOwnedIndex <= 3) showOwnedComboBox->setCurrentIndex(showOwnedIndex); loadSettingBool(stretchBackgroundCheckBox, "stretch menu background", "GUI"); loadSettingBool(graphicHerbalismCheckBox, "graphic herbalism", "Game"); scalingSpinBox->setValue(Settings::Manager::getFloat("scaling factor", "GUI")); } // Bug fixes { loadSettingBool(preventMerchantEquippingCheckBox, "prevent merchant equipping", "Game"); loadSettingBool(trainersTrainingSkillsBasedOnBaseSkillCheckBox, "trainers training skills based on base skill", "Game"); } // Miscellaneous { // Saves loadSettingBool(timePlayedCheckbox, "timeplayed", "Saves"); maximumQuicksavesComboBox->setValue(Settings::Manager::getInt("max quicksaves", "Saves")); // Other Settings QString screenshotFormatString = QString::fromStdString(Settings::Manager::getString("screenshot format", "General")).toUpper(); if (screenshotFormatComboBox->findText(screenshotFormatString) == -1) screenshotFormatComboBox->addItem(screenshotFormatString); screenshotFormatComboBox->setCurrentIndex(screenshotFormatComboBox->findText(screenshotFormatString)); } // Testing { loadSettingBool(grabCursorCheckBox, "grab cursor", "Input"); bool skipMenu = mGameSettings.value("skip-menu").toInt() == 1; if (skipMenu) { skipMenuCheckBox->setCheckState(Qt::Checked); } startDefaultCharacterAtLabel->setEnabled(skipMenu); startDefaultCharacterAtField->setEnabled(skipMenu); startDefaultCharacterAtField->setText(mGameSettings.value("start")); runScriptAfterStartupField->setText(mGameSettings.value("script-run")); } return true; } void Launcher::AdvancedPage::saveSettings() { // Game mechanics { saveSettingBool(toggleSneakCheckBox, "toggle sneak", "Input"); saveSettingBool(canLootDuringDeathAnimationCheckBox, "can loot during death animation", "Game"); saveSettingBool(followersAttackOnSightCheckBox, "followers attack on sight", "Game"); saveSettingBool(rebalanceSoulGemValuesCheckBox, "rebalance soul gem values", "Game"); saveSettingBool(enchantedWeaponsMagicalCheckBox, "enchanted weapons are magical", "Game"); saveSettingBool(permanentBarterDispositionChangeCheckBox, "barter disposition change is permanent", "Game"); saveSettingBool(classicReflectedAbsorbSpellsCheckBox, "classic reflected absorb spells behavior", "Game"); saveSettingBool(requireAppropriateAmmunitionCheckBox, "only appropriate ammunition bypasses resistance", "Game"); saveSettingBool(uncappedDamageFatigueCheckBox, "uncapped damage fatigue", "Game"); 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); 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); } // Visuals { saveSettingBool(autoUseObjectNormalMapsCheckBox, "auto use object normal maps", "Shaders"); saveSettingBool(autoUseObjectSpecularMapsCheckBox, "auto use object specular maps", "Shaders"); saveSettingBool(autoUseTerrainNormalMapsCheckBox, "auto use terrain normal maps", "Shaders"); saveSettingBool(autoUseTerrainSpecularMapsCheckBox, "auto use terrain specular maps", "Shaders"); saveSettingBool(bumpMapLocalLightingCheckBox, "apply lighting to environment maps", "Shaders"); saveSettingBool(radialFogCheckBox, "radial fog", "Shaders"); saveSettingBool(magicItemAnimationsCheckBox, "use magic item animations", "Game"); saveSettingBool(animSourcesCheckBox, "use additional anim sources", "Game"); saveSettingBool(weaponSheathingCheckBox, "weapon sheathing", "Game"); saveSettingBool(shieldSheathingCheckBox, "shield sheathing", "Game"); saveSettingBool(turnToMovementDirectionCheckBox, "turn to movement direction", "Game"); saveSettingBool(smoothMovementCheckBox, "smooth movement", "Game"); const bool distantTerrain = Settings::Manager::getBool("distant terrain", "Terrain"); const bool objectPaging = Settings::Manager::getBool("object paging", "Terrain"); const bool wantDistantLand = distantLandCheckBox->checkState(); if (wantDistantLand != (distantTerrain && objectPaging)) { Settings::Manager::setBool("distant terrain", "Terrain", wantDistantLand); Settings::Manager::setBool("object paging", "Terrain", wantDistantLand); } saveSettingBool(activeGridObjectPagingCheckBox, "object paging active grid", "Terrain"); double viewingDistance = viewingDistanceComboBox->value(); if (viewingDistance != convertToCells(Settings::Manager::getInt("viewing distance", "Camera"))) { Settings::Manager::setInt("viewing distance", "Camera", convertToUnits(viewingDistance)); } double objectPagingMinSize = objectPagingMinSizeComboBox->value(); if (objectPagingMinSize != Settings::Manager::getDouble("object paging min size", "Terrain")) Settings::Manager::setDouble("object paging min size", "Terrain", objectPagingMinSize); } // Audio { int audioDeviceIndex = audioDeviceSelectorComboBox->currentIndex(); if (audioDeviceIndex != 0) { Settings::Manager::setString("device", "Sound", audioDeviceSelectorComboBox->currentText().toUtf8().constData()); } else { Settings::Manager::setString("device", "Sound", ""); } int hrtfEnabledIndex = enableHRTFComboBox->currentIndex() - 1; if (hrtfEnabledIndex != Settings::Manager::getInt("hrtf enable", "Sound")) { Settings::Manager::setInt("hrtf enable", "Sound", hrtfEnabledIndex); } int selectedHRTFProfileIndex = hrtfProfileSelectorComboBox->currentIndex(); if (selectedHRTFProfileIndex != 0) { Settings::Manager::setString("hrtf", "Sound", hrtfProfileSelectorComboBox->currentText().toUtf8().constData()); } else { Settings::Manager::setString("hrtf", "Sound", ""); } } // Camera { saveSettingBool(viewOverShoulderCheckBox, "view over shoulder", "Camera"); saveSettingBool(autoSwitchShoulderCheckBox, "auto switch shoulder", "Camera"); saveSettingBool(previewIfStandStillCheckBox, "preview if stand still", "Camera"); saveSettingBool(deferredPreviewRotationCheckBox, "deferred preview rotation", "Camera"); saveSettingBool(headBobbingCheckBox, "head bobbing", "Camera"); osg::Vec2f shoulderOffset = Settings::Manager::getVector2("view over shoulder offset", "Camera"); if (defaultShoulderComboBox->currentIndex() != (shoulderOffset.x() >= 0 ? 0 : 1)) { if (defaultShoulderComboBox->currentIndex() == 0) shoulderOffset.x() = std::abs(shoulderOffset.x()); else shoulderOffset.x() = -std::abs(shoulderOffset.x()); Settings::Manager::setVector2("view over shoulder offset", "Camera", shoulderOffset); } } // Interface Changes { saveSettingBool(showEffectDurationCheckBox, "show effect duration", "Game"); saveSettingBool(showEnchantChanceCheckBox, "show enchant chance", "Game"); 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); saveSettingBool(stretchBackgroundCheckBox, "stretch menu background", "GUI"); saveSettingBool(graphicHerbalismCheckBox, "graphic herbalism", "Game"); float uiScalingFactor = scalingSpinBox->value(); if (uiScalingFactor != Settings::Manager::getFloat("scaling factor", "GUI")) Settings::Manager::setFloat("scaling factor", "GUI", uiScalingFactor); } // Bug fixes { saveSettingBool(preventMerchantEquippingCheckBox, "prevent merchant equipping", "Game"); saveSettingBool(trainersTrainingSkillsBasedOnBaseSkillCheckBox, "trainers training skills based on base skill", "Game"); } // Miscellaneous { // 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); } // Other Settings std::string screenshotFormatString = screenshotFormatComboBox->currentText().toLower().toStdString(); if (screenshotFormatString != Settings::Manager::getString("screenshot format", "General")) Settings::Manager::setString("screenshot format", "General", screenshotFormatString); } // Testing { saveSettingBool(grabCursorCheckBox, "grab cursor", "Input"); int skipMenu = skipMenuCheckBox->checkState() == Qt::Checked; if (skipMenu != mGameSettings.value("skip-menu").toInt()) mGameSettings.setValue("skip-menu", QString::number(skipMenu)); QString startCell = startDefaultCharacterAtField->text(); if (startCell != mGameSettings.value("start")) { mGameSettings.setValue("start", startCell); } QString scriptRun = runScriptAfterStartupField->text(); if (scriptRun != mGameSettings.value("script-run")) mGameSettings.setValue("script-run", scriptRun); } } void Launcher::AdvancedPage::loadSettingBool(QCheckBox *checkbox, const std::string &setting, const std::string &group) { if (Settings::Manager::getBool(setting, group)) checkbox->setCheckState(Qt::Checked); } void Launcher::AdvancedPage::saveSettingBool(QCheckBox *checkbox, const std::string &setting, const std::string &group) { bool cValue = checkbox->checkState(); if (cValue != Settings::Manager::getBool(setting, group)) Settings::Manager::setBool(setting, group, cValue); } void Launcher::AdvancedPage::slotLoadedCellsChanged(QStringList cellNames) { loadCellsForAutocomplete(cellNames); } void Launcher::AdvancedPage::slotAnimSourcesToggled(bool checked) { weaponSheathingCheckBox->setEnabled(checked); shieldSheathingCheckBox->setEnabled(checked); if (!checked) { weaponSheathingCheckBox->setCheckState(Qt::Unchecked); shieldSheathingCheckBox->setCheckState(Qt::Unchecked); } } void Launcher::AdvancedPage::slotViewOverShoulderToggled(bool checked) { viewOverShoulderVerticalLayout->setEnabled(viewOverShoulderCheckBox->checkState()); } ================================================ FILE: apps/launcher/advancedpage.hpp ================================================ #ifndef ADVANCEDPAGE_H #define ADVANCEDPAGE_H #include #include #include "ui_advancedpage.h" #include namespace Config { class GameSettings; } namespace Launcher { class AdvancedPage : public QWidget, private Ui::AdvancedPage { Q_OBJECT public: explicit AdvancedPage(Config::GameSettings &gameSettings, QWidget *parent = nullptr); bool loadSettings(); void saveSettings(); public slots: void slotLoadedCellsChanged(QStringList cellNames); private slots: void on_skipMenuCheckBox_stateChanged(int state); void on_runScriptAfterStartupBrowseButton_clicked(); void slotAnimSourcesToggled(bool checked); void slotViewOverShoulderToggled(bool checked); private: Config::GameSettings &mGameSettings; QCompleter mCellNameCompleter; QStringListModel mCellNameCompleterModel; /** * Load the cells associated with the given content files for use in autocomplete * @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); }; } #endif ================================================ FILE: apps/launcher/datafilespage.cpp ================================================ #include "datafilespage.hpp" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "utils/textinputdialog.hpp" #include "utils/profilescombobox.hpp" const char *Launcher::DataFilesPage::mDefaultContentListName = "Default"; Launcher::DataFilesPage::DataFilesPage(Files::ConfigurationManager &cfg, Config::GameSettings &gameSettings, Config::LauncherSettings &launcherSettings, QWidget *parent) : QWidget(parent) , mCfgMgr(cfg) , mGameSettings(gameSettings) , mLauncherSettings(launcherSettings) { ui.setupUi (this); setObjectName ("DataFilesPage"); mSelector = new ContentSelectorView::ContentSelector (ui.contentSelectorWidget); const QString encoding = mGameSettings.value("encoding", "win1252"); mSelector->setEncoding(encoding); mNewProfileDialog = new TextInputDialog(tr("New Content List"), tr("Content List name:"), this); mCloneProfileDialog = new TextInputDialog(tr("Clone Content List"), tr("Content List name:"), this); connect(mNewProfileDialog->lineEdit(), SIGNAL(textChanged(QString)), this, SLOT(updateNewProfileOkButton(QString))); connect(mCloneProfileDialog->lineEdit(), SIGNAL(textChanged(QString)), this, SLOT(updateCloneProfileOkButton(QString))); buildView(); loadSettings(); // Connect signal and slot after the settings have been loaded. We only care about the user changing // the addons and don't want to get signals of the system doing it during startup. connect(mSelector, SIGNAL(signalAddonDataChanged(QModelIndex,QModelIndex)), this, SLOT(slotAddonDataChanged())); // Call manually to indicate all changes to addon data during startup. slotAddonDataChanged(); } void Launcher::DataFilesPage::buildView() { ui.verticalLayout->insertWidget (0, mSelector->uiWidget()); QToolButton * refreshButton = mSelector->refreshButton(); //tool buttons ui.newProfileButton->setToolTip ("Create a new Content List"); ui.cloneProfileButton->setToolTip ("Clone the current Content List"); ui.deleteProfileButton->setToolTip ("Delete an existing Content List"); refreshButton->setToolTip("Refresh Data Files"); //combo box ui.profilesComboBox->addItem(mDefaultContentListName); ui.profilesComboBox->setPlaceholderText (QString("Select a Content List...")); ui.profilesComboBox->setCurrentIndex(ui.profilesComboBox->findText(QLatin1String(mDefaultContentListName))); // Add the actions to the toolbuttons ui.newProfileButton->setDefaultAction (ui.newProfileAction); ui.cloneProfileButton->setDefaultAction (ui.cloneProfileAction); ui.deleteProfileButton->setDefaultAction (ui.deleteProfileAction); refreshButton->setDefaultAction(ui.refreshDataFilesAction); //establish connections connect (ui.profilesComboBox, SIGNAL (currentIndexChanged(int)), this, SLOT (slotProfileChanged(int))); connect (ui.profilesComboBox, SIGNAL (profileRenamed(QString, QString)), this, SLOT (slotProfileRenamed(QString, QString))); connect (ui.profilesComboBox, SIGNAL (signalProfileChanged(QString, QString)), this, SLOT (slotProfileChangedByUser(QString, QString))); connect(ui.refreshDataFilesAction, SIGNAL(triggered()),this, SLOT(slotRefreshButtonClicked())); } bool Launcher::DataFilesPage::loadSettings() { QStringList profiles = mLauncherSettings.getContentLists(); QString currentProfile = mLauncherSettings.getCurrentContentListName(); qDebug() << "The current profile is: " << currentProfile; for (const QString &item : profiles) addProfile (item, false); // Hack: also add the current profile if (!currentProfile.isEmpty()) addProfile(currentProfile, true); return true; } void Launcher::DataFilesPage::populateFileViews(const QString& contentModelName) { QStringList paths = mGameSettings.getDataDirs(); mDataLocal = mGameSettings.getDataLocal(); if (!mDataLocal.isEmpty()) paths.insert(0, mDataLocal); mSelector->clearFiles(); for (const QString &path : paths) mSelector->addFiles(path); PathIterator pathIterator(paths); mSelector->setProfileContent(filesInProfile(contentModelName, pathIterator)); } QStringList Launcher::DataFilesPage::filesInProfile(const QString& profileName, PathIterator& pathIterator) { QStringList files = mLauncherSettings.getContentListFiles(profileName); QStringList filepaths; for (const QString& file : files) { QString filepath = pathIterator.findFirstPath(file); if (!filepath.isEmpty()) filepaths << filepath; } return filepaths; } void Launcher::DataFilesPage::saveSettings(const QString &profile) { QString profileName = profile; if (profileName.isEmpty()) profileName = ui.profilesComboBox->currentText(); //retrieve the files selected for the profile ContentSelectorModel::ContentFileList items = mSelector->selectedFiles(); //set the value of the current profile (not necessarily the profile being saved!) mLauncherSettings.setCurrentContentListName(ui.profilesComboBox->currentText()); QStringList fileNames; for (const ContentSelectorModel::EsmFile *item : items) { fileNames.append(item->fileName()); } mLauncherSettings.setContentList(profileName, fileNames); mGameSettings.setContentList(fileNames); } QStringList Launcher::DataFilesPage::selectedFilePaths() { //retrieve the files selected for the profile ContentSelectorModel::ContentFileList items = mSelector->selectedFiles(); QStringList filePaths; for (const ContentSelectorModel::EsmFile *item : items) { QFile file(item->filePath()); if(file.exists()) { filePaths.append(item->filePath()); } else { slotRefreshButtonClicked(); } } return filePaths; } void Launcher::DataFilesPage::removeProfile(const QString &profile) { mLauncherSettings.removeContentList(profile); } QAbstractItemModel *Launcher::DataFilesPage::profilesModel() const { return ui.profilesComboBox->model(); } int Launcher::DataFilesPage::profilesIndex() const { return ui.profilesComboBox->currentIndex(); } void Launcher::DataFilesPage::setProfile(int index, bool savePrevious) { if (index >= -1 && index < ui.profilesComboBox->count()) { QString previous = mPreviousProfile; QString current = ui.profilesComboBox->itemText(index); mPreviousProfile = current; setProfile (previous, current, savePrevious); } } void Launcher::DataFilesPage::setProfile (const QString &previous, const QString ¤t, bool savePrevious) { //abort if no change (poss. duplicate signal) if (previous == current) return; if (!previous.isEmpty() && savePrevious) saveSettings (previous); ui.profilesComboBox->setCurrentProfile (ui.profilesComboBox->findText (current)); populateFileViews(current); checkForDefaultProfile(); } void Launcher::DataFilesPage::slotProfileDeleted (const QString &item) { removeProfile (item); } void Launcher::DataFilesPage:: refreshDataFilesView () { QString currentProfile = ui.profilesComboBox->currentText(); saveSettings(currentProfile); populateFileViews(currentProfile); } void Launcher::DataFilesPage::slotRefreshButtonClicked () { refreshDataFilesView(); } void Launcher::DataFilesPage::slotProfileChangedByUser(const QString &previous, const QString ¤t) { setProfile(previous, current, true); emit signalProfileChanged (ui.profilesComboBox->findText(current)); } void Launcher::DataFilesPage::slotProfileRenamed(const QString &previous, const QString ¤t) { if (previous.isEmpty()) return; // Save the new profile name saveSettings(); // Remove the old one removeProfile (previous); loadSettings(); } void Launcher::DataFilesPage::slotProfileChanged(int index) { // in case the event was triggered externally if (ui.profilesComboBox->currentIndex() != index) ui.profilesComboBox->setCurrentIndex(index); setProfile (index, true); } void Launcher::DataFilesPage::on_newProfileAction_triggered() { if (mNewProfileDialog->exec() != QDialog::Accepted) return; QString profile = mNewProfileDialog->lineEdit()->text(); if (profile.isEmpty()) return; saveSettings(); mLauncherSettings.setCurrentContentListName(profile); addProfile(profile, true); } void Launcher::DataFilesPage::addProfile (const QString &profile, bool setAsCurrent) { if (profile.isEmpty()) return; if (ui.profilesComboBox->findText (profile) == -1) ui.profilesComboBox->addItem (profile); if (setAsCurrent) setProfile (ui.profilesComboBox->findText (profile), false); } void Launcher::DataFilesPage::on_cloneProfileAction_triggered() { if (mCloneProfileDialog->exec() != QDialog::Accepted) return; QString profile = mCloneProfileDialog->lineEdit()->text(); if (profile.isEmpty()) return; mLauncherSettings.setContentList(profile, selectedFilePaths()); addProfile(profile, true); } void Launcher::DataFilesPage::on_deleteProfileAction_triggered() { QString profile = ui.profilesComboBox->currentText(); if (profile.isEmpty()) return; if (!showDeleteMessageBox (profile)) return; // this should work since the Default profile can't be deleted and is always index 0 int next = ui.profilesComboBox->currentIndex()-1; // changing the profile forces a reload of plugin file views. ui.profilesComboBox->setCurrentIndex(next); removeProfile(profile); ui.profilesComboBox->removeItem(ui.profilesComboBox->findText(profile)); checkForDefaultProfile(); } void Launcher::DataFilesPage::updateNewProfileOkButton(const QString &text) { // We do this here because we need the profiles combobox text mNewProfileDialog->setOkButtonEnabled(!text.isEmpty() && ui.profilesComboBox->findText(text) == -1); } void Launcher::DataFilesPage::updateCloneProfileOkButton(const QString &text) { // We do this here because we need the profiles combobox text mCloneProfileDialog->setOkButtonEnabled(!text.isEmpty() && ui.profilesComboBox->findText(text) == -1); } void Launcher::DataFilesPage::checkForDefaultProfile() { //don't allow deleting "Default" profile bool success = (ui.profilesComboBox->currentText() != mDefaultContentListName); ui.deleteProfileAction->setEnabled (success); ui.profilesComboBox->setEditEnabled (success); } bool Launcher::DataFilesPage::showDeleteMessageBox (const QString &text) { QMessageBox msgBox(this); msgBox.setWindowTitle(tr("Delete Content List")); msgBox.setIcon(QMessageBox::Warning); msgBox.setStandardButtons(QMessageBox::Cancel); msgBox.setText(tr("Are you sure you want to delete %1?").arg(text)); QAbstractButton *deleteButton = msgBox.addButton(tr("Delete"), QMessageBox::ActionRole); msgBox.exec(); return (msgBox.clickedButton() == deleteButton); } void Launcher::DataFilesPage::slotAddonDataChanged() { QStringList selectedFiles = selectedFilePaths(); if (previousSelectedFiles != selectedFiles) { previousSelectedFiles = selectedFiles; // Loading cells for core Morrowind + Expansions takes about 0.2 seconds, which is enough to cause a // barely perceptible UI lag. Splitting into its own thread to alleviate that. std::thread loadCellsThread(&DataFilesPage::reloadCells, this, selectedFiles); loadCellsThread.detach(); } } // Mutex lock to run reloadCells synchronously. std::mutex _reloadCellsMutex; void Launcher::DataFilesPage::reloadCells(QStringList selectedFiles) { // Use a mutex lock so that we can prevent two threads from executing the rest of this code at the same time // Based on https://stackoverflow.com/a/5429695/531762 std::unique_lock lock(_reloadCellsMutex); // The following code will run only if there is not another thread currently running it CellNameLoader cellNameLoader; #if QT_VERSION >= QT_VERSION_CHECK(5,14,0) QSet set = cellNameLoader.getCellNames(selectedFiles); QStringList cellNamesList(set.begin(), set.end()); #else QStringList cellNamesList = QStringList::fromSet(cellNameLoader.getCellNames(selectedFiles)); #endif std::sort(cellNamesList.begin(), cellNamesList.end()); emit signalLoadedCellsChanged(cellNamesList); } ================================================ FILE: apps/launcher/datafilespage.hpp ================================================ #ifndef DATAFILESPAGE_H #define DATAFILESPAGE_H #include "ui_datafilespage.h" #include #include #include class QSortFilterProxyModel; class QAbstractItemModel; class QMenu; namespace Files { struct ConfigurationManager; } namespace ContentSelectorView { class ContentSelector; } namespace Config { class GameSettings; class LauncherSettings; } namespace Launcher { class TextInputDialog; class ProfilesComboBox; class DataFilesPage : public QWidget { Q_OBJECT ContentSelectorView::ContentSelector *mSelector; Ui::DataFilesPage ui; public: explicit DataFilesPage (Files::ConfigurationManager &cfg, Config::GameSettings &gameSettings, Config::LauncherSettings &launcherSettings, QWidget *parent = nullptr); QAbstractItemModel* profilesModel() const; int profilesIndex() const; //void writeConfig(QString profile = QString()); void saveSettings(const QString &profile = ""); bool loadSettings(); /** * Returns the file paths of all selected content files * @return the file paths of all selected content files */ QStringList selectedFilePaths(); signals: void signalProfileChanged (int index); void signalLoadedCellsChanged(QStringList selectedFiles); public slots: void slotProfileChanged (int index); private slots: void slotProfileChangedByUser(const QString &previous, const QString ¤t); void slotProfileRenamed(const QString &previous, const QString ¤t); void slotProfileDeleted(const QString &item); void slotAddonDataChanged (); void slotRefreshButtonClicked (); void updateNewProfileOkButton(const QString &text); void updateCloneProfileOkButton(const QString &text); void on_newProfileAction_triggered(); void on_cloneProfileAction_triggered(); void on_deleteProfileAction_triggered(); public: /// Content List that is always present const static char *mDefaultContentListName; private: TextInputDialog *mNewProfileDialog; TextInputDialog *mCloneProfileDialog; Files::ConfigurationManager &mCfgMgr; Config::GameSettings &mGameSettings; Config::LauncherSettings &mLauncherSettings; QString mPreviousProfile; QStringList previousSelectedFiles; QString mDataLocal; void buildView(); void setProfile (int index, bool savePrevious); void setProfile (const QString &previous, const QString ¤t, bool savePrevious); void removeProfile (const QString &profile); bool showDeleteMessageBox (const QString &text); void addProfile (const QString &profile, bool setAsCurrent); void checkForDefaultProfile(); void populateFileViews(const QString& contentModelName); void reloadCells(QStringList selectedFiles); void refreshDataFilesView (); class PathIterator { QStringList::ConstIterator mCitEnd; QStringList::ConstIterator mCitCurrent; QStringList::ConstIterator mCitBegin; QString mFile; QString mFilePath; public: PathIterator (const QStringList &list) { mCitBegin = list.constBegin(); mCitCurrent = mCitBegin; mCitEnd = list.constEnd(); } QString findFirstPath (const QString &file) { mCitCurrent = mCitBegin; mFile = file; return path(); } QString findNextPath () { return path(); } private: QString path () { bool success = false; QDir dir; QFileInfo file; while (!success) { if (mCitCurrent == mCitEnd) break; dir.setPath (*(mCitCurrent++)); file.setFile (dir.absoluteFilePath (mFile)); success = file.exists(); } if (success) return file.absoluteFilePath(); return ""; } }; QStringList filesInProfile(const QString& profileName, PathIterator& pathIterator); }; } #endif ================================================ FILE: apps/launcher/graphicspage.cpp ================================================ #include "graphicspage.hpp" #include #include #include #include #ifdef MAC_OS_X_VERSION_MIN_REQUIRED #undef MAC_OS_X_VERSION_MIN_REQUIRED // We need to do this because of Qt: https://bugreports.qt-project.org/browse/QTBUG-22154 #define MAC_OS_X_VERSION_MIN_REQUIRED __ENVIRONMENT_MAC_OS_X_VERSION_MIN_REQUIRED__ #endif // MAC_OS_X_VERSION_MIN_REQUIRED #include #include #include QString getAspect(int x, int y) { int gcd = std::gcd (x, y); if (gcd == 0) return QString(); int xaspect = x / gcd; int yaspect = y / gcd; // special case: 8 : 5 is usually referred to as 16:10 if (xaspect == 8 && yaspect == 5) return QString("16:10"); return QString(QString::number(xaspect) + ":" + QString::number(yaspect)); } Launcher::GraphicsPage::GraphicsPage(QWidget *parent) : QWidget(parent) { setObjectName ("GraphicsPage"); setupUi(this); // Set the maximum res we can set in windowed mode QRect res = getMaximumResolution(); customWidthSpinBox->setMaximum(res.width()); customHeightSpinBox->setMaximum(res.height()); connect(fullScreenCheckBox, SIGNAL(stateChanged(int)), this, SLOT(slotFullScreenChanged(int))); connect(standardRadioButton, SIGNAL(toggled(bool)), this, SLOT(slotStandardToggled(bool))); 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() { bool sdlConnectSuccessful = initSDL(); if (!sdlConnectSuccessful) { return false; } int displays = SDL_GetNumVideoDisplays(); if (displays < 0) { QMessageBox msgBox; msgBox.setWindowTitle(tr("Error receiving number of screens")); msgBox.setIcon(QMessageBox::Critical); msgBox.setStandardButtons(QMessageBox::Ok); msgBox.setText(tr("
SDL_GetNumVideoDisplays failed:

") + QString::fromUtf8(SDL_GetError()) + "
"); msgBox.exec(); return false; } screenComboBox->clear(); mResolutionsPerScreen.clear(); for (int i = 0; i < displays; i++) { mResolutionsPerScreen.append(getAvailableResolutions(i)); screenComboBox->addItem(QString(tr("Screen ")) + QString::number(i + 1)); } screenChanged(0); // Disconnect from SDL processes quitSDL(); return true; } bool Launcher::GraphicsPage::loadSettings() { if (!setupSDL()) return false; // Visuals if (Settings::Manager::getBool("vsync", "Video")) vSyncCheckBox->setCheckState(Qt::Checked); if (Settings::Manager::getBool("fullscreen", "Video")) fullScreenCheckBox->setCheckState(Qt::Checked); if (Settings::Manager::getBool("window border", "Video")) windowBorderCheckBox->setCheckState(Qt::Checked); // aaValue is the actual value (0, 1, 2, 4, 8, 16) int aaValue = Settings::Manager::getInt("antialiasing", "Video"); // aaIndex is the index into the allowed values in the pull down. int aaIndex = antiAliasingComboBox->findText(QString::number(aaValue)); if (aaIndex != -1) antiAliasingComboBox->setCurrentIndex(aaIndex); int width = Settings::Manager::getInt("resolution x", "Video"); int height = Settings::Manager::getInt("resolution y", "Video"); QString resolution = QString::number(width) + QString(" x ") + QString::number(height); screenComboBox->setCurrentIndex(Settings::Manager::getInt("screen", "Video")); int resIndex = resolutionComboBox->findText(resolution, Qt::MatchStartsWith); if (resIndex != -1) { standardRadioButton->toggle(); resolutionComboBox->setCurrentIndex(resIndex); } else { customRadioButton->toggle(); customWidthSpinBox->setValue(width); customHeightSpinBox->setValue(height); } float fpsLimit = Settings::Manager::getFloat("framerate limit", "Video"); if (fpsLimit != 0) { framerateLimitCheckBox->setCheckState(Qt::Checked); framerateLimitSpinBox->setValue(fpsLimit); } // Lighting int lightingMethod = 1; if (Settings::Manager::getString("lighting method", "Shaders") == "legacy") lightingMethod = 0; else if (Settings::Manager::getString("lighting method", "Shaders") == "shaders") lightingMethod = 2; lightingMethodComboBox->setCurrentIndex(lightingMethod); // Shadows if (Settings::Manager::getBool("actor shadows", "Shadows")) actorShadowsCheckBox->setCheckState(Qt::Checked); if (Settings::Manager::getBool("player shadows", "Shadows")) playerShadowsCheckBox->setCheckState(Qt::Checked); if (Settings::Manager::getBool("terrain shadows", "Shadows")) terrainShadowsCheckBox->setCheckState(Qt::Checked); if (Settings::Manager::getBool("object shadows", "Shadows")) objectShadowsCheckBox->setCheckState(Qt::Checked); if (Settings::Manager::getBool("enable indoor shadows", "Shadows")) indoorShadowsCheckBox->setCheckState(Qt::Checked); shadowComputeSceneBoundsComboBox->setCurrentIndex( shadowComputeSceneBoundsComboBox->findText( QString(tr(Settings::Manager::getString("compute scene bounds", "Shadows").c_str())))); int shadowDistLimit = Settings::Manager::getInt("maximum shadow map distance", "Shadows"); if (shadowDistLimit > 0) { shadowDistanceCheckBox->setCheckState(Qt::Checked); shadowDistanceSpinBox->setValue(shadowDistLimit); } float shadowFadeStart = Settings::Manager::getFloat("shadow fade start", "Shadows"); if (shadowFadeStart != 0) fadeStartSpinBox->setValue(shadowFadeStart); int shadowRes = Settings::Manager::getInt("shadow map resolution", "Shadows"); int shadowResIndex = shadowResolutionComboBox->findText(QString::number(shadowRes)); if (shadowResIndex != -1) shadowResolutionComboBox->setCurrentIndex(shadowResIndex); return true; } void Launcher::GraphicsPage::saveSettings() { // Visuals // Ensure we only set the new settings if they changed. This is to avoid cluttering the // user settings file (which by definition should only contain settings the user has touched) bool cVSync = vSyncCheckBox->checkState(); if (cVSync != Settings::Manager::getBool("vsync", "Video")) Settings::Manager::setBool("vsync", "Video", cVSync); bool cFullScreen = fullScreenCheckBox->checkState(); if (cFullScreen != Settings::Manager::getBool("fullscreen", "Video")) Settings::Manager::setBool("fullscreen", "Video", cFullScreen); bool cWindowBorder = windowBorderCheckBox->checkState(); if (cWindowBorder != Settings::Manager::getBool("window border", "Video")) Settings::Manager::setBool("window border", "Video", cWindowBorder); int cAAValue = antiAliasingComboBox->currentText().toInt(); if (cAAValue != Settings::Manager::getInt("antialiasing", "Video")) Settings::Manager::setInt("antialiasing", "Video", cAAValue); int cWidth = 0; int cHeight = 0; if (standardRadioButton->isChecked()) { QRegExp resolutionRe(QString("(\\d+) x (\\d+).*")); if (resolutionRe.exactMatch(resolutionComboBox->currentText().simplified())) { cWidth = resolutionRe.cap(1).toInt(); cHeight = resolutionRe.cap(2).toInt(); } } else { cWidth = customWidthSpinBox->value(); cHeight = customHeightSpinBox->value(); } if (cWidth != Settings::Manager::getInt("resolution x", "Video")) Settings::Manager::setInt("resolution x", "Video", cWidth); if (cHeight != Settings::Manager::getInt("resolution y", "Video")) Settings::Manager::setInt("resolution y", "Video", cHeight); int cScreen = screenComboBox->currentIndex(); if (cScreen != Settings::Manager::getInt("screen", "Video")) Settings::Manager::setInt("screen", "Video", cScreen); if (framerateLimitCheckBox->checkState() != Qt::Unchecked) { float cFpsLimit = framerateLimitSpinBox->value(); if (cFpsLimit != Settings::Manager::getFloat("framerate limit", "Video")) Settings::Manager::setFloat("framerate limit", "Video", cFpsLimit); } else if (Settings::Manager::getFloat("framerate limit", "Video") != 0) { Settings::Manager::setFloat("framerate limit", "Video", 0); } // Lighting static std::array lightingMethodMap = {"legacy", "shaders compatibility", "shaders"}; Settings::Manager::setString("lighting method", "Shaders", lightingMethodMap[lightingMethodComboBox->currentIndex()]); // Shadows int cShadowDist = shadowDistanceCheckBox->checkState() != Qt::Unchecked ? shadowDistanceSpinBox->value() : 0; if (Settings::Manager::getInt("maximum shadow map distance", "Shadows") != cShadowDist) Settings::Manager::setInt("maximum shadow map distance", "Shadows", cShadowDist); float cFadeStart = fadeStartSpinBox->value(); if (cShadowDist > 0 && Settings::Manager::getFloat("shadow fade start", "Shadows") != cFadeStart) Settings::Manager::setFloat("shadow fade start", "Shadows", cFadeStart); bool cActorShadows = actorShadowsCheckBox->checkState(); bool cObjectShadows = objectShadowsCheckBox->checkState(); bool cTerrainShadows = terrainShadowsCheckBox->checkState(); bool cPlayerShadows = playerShadowsCheckBox->checkState(); if (cActorShadows || cObjectShadows || cTerrainShadows || cPlayerShadows) { if (!Settings::Manager::getBool("enable shadows", "Shadows")) Settings::Manager::setBool("enable shadows", "Shadows", true); if (Settings::Manager::getBool("actor shadows", "Shadows") != cActorShadows) Settings::Manager::setBool("actor shadows", "Shadows", cActorShadows); if (Settings::Manager::getBool("player shadows", "Shadows") != cPlayerShadows) Settings::Manager::setBool("player shadows", "Shadows", cPlayerShadows); if (Settings::Manager::getBool("object shadows", "Shadows") != cObjectShadows) Settings::Manager::setBool("object shadows", "Shadows", cObjectShadows); if (Settings::Manager::getBool("terrain shadows", "Shadows") != cTerrainShadows) Settings::Manager::setBool("terrain shadows", "Shadows", cTerrainShadows); } else { if (Settings::Manager::getBool("enable shadows", "Shadows")) Settings::Manager::setBool("enable shadows", "Shadows", false); if (Settings::Manager::getBool("actor shadows", "Shadows")) Settings::Manager::setBool("actor shadows", "Shadows", false); if (Settings::Manager::getBool("player shadows", "Shadows")) Settings::Manager::setBool("player shadows", "Shadows", false); if (Settings::Manager::getBool("object shadows", "Shadows")) Settings::Manager::setBool("object shadows", "Shadows", false); if (Settings::Manager::getBool("terrain shadows", "Shadows")) Settings::Manager::setBool("terrain shadows", "Shadows", false); } bool cIndoorShadows = indoorShadowsCheckBox->checkState(); if (Settings::Manager::getBool("enable indoor shadows", "Shadows") != cIndoorShadows) Settings::Manager::setBool("enable indoor shadows", "Shadows", cIndoorShadows); int cShadowRes = shadowResolutionComboBox->currentText().toInt(); if (cShadowRes != Settings::Manager::getInt("shadow map resolution", "Shadows")) Settings::Manager::setInt("shadow map resolution", "Shadows", cShadowRes); auto cComputeSceneBounds = shadowComputeSceneBoundsComboBox->currentText().toStdString(); if (cComputeSceneBounds != Settings::Manager::getString("compute scene bounds", "Shadows")) Settings::Manager::setString("compute scene bounds", "Shadows", cComputeSceneBounds); } QStringList Launcher::GraphicsPage::getAvailableResolutions(int screen) { QStringList result; SDL_DisplayMode mode; int modeIndex, modes = SDL_GetNumDisplayModes(screen); if (modes < 0) { QMessageBox msgBox; msgBox.setWindowTitle(tr("Error receiving resolutions")); msgBox.setIcon(QMessageBox::Critical); msgBox.setStandardButtons(QMessageBox::Ok); msgBox.setText(tr("
SDL_GetNumDisplayModes failed:

") + QString::fromUtf8(SDL_GetError()) + "
"); msgBox.exec(); return result; } for (modeIndex = 0; modeIndex < modes; modeIndex++) { if (SDL_GetDisplayMode(screen, modeIndex, &mode) < 0) { QMessageBox msgBox; msgBox.setWindowTitle(tr("Error receiving resolutions")); msgBox.setIcon(QMessageBox::Critical); msgBox.setStandardButtons(QMessageBox::Ok); msgBox.setText(tr("
SDL_GetDisplayMode failed:

") + QString::fromUtf8(SDL_GetError()) + "
"); msgBox.exec(); return result; } QString resolution = QString::number(mode.w) + QString(" x ") + QString::number(mode.h); QString aspect = getAspect(mode.w, mode.h); if (aspect == QLatin1String("16:9") || aspect == QLatin1String("16:10")) { resolution.append(tr("\t(Wide ") + aspect + ")"); } else if (aspect == QLatin1String("4:3")) { resolution.append(tr("\t(Standard 4:3)")); } result.append(resolution); } result.removeDuplicates(); return result; } QRect Launcher::GraphicsPage::getMaximumResolution() { QRect max; for (QScreen* screen : QGuiApplication::screens()) { QRect res = screen->geometry(); if (res.width() > max.width()) max.setWidth(res.width()); if (res.height() > max.height()) max.setHeight(res.height()); } return max; } void Launcher::GraphicsPage::screenChanged(int screen) { if (screen >= 0) { resolutionComboBox->clear(); resolutionComboBox->addItems(mResolutionsPerScreen[screen]); } } void Launcher::GraphicsPage::slotFullScreenChanged(int state) { if (state == Qt::Checked) { standardRadioButton->toggle(); customRadioButton->setEnabled(false); customWidthSpinBox->setEnabled(false); customHeightSpinBox->setEnabled(false); windowBorderCheckBox->setEnabled(false); } else { customRadioButton->setEnabled(true); customWidthSpinBox->setEnabled(true); customHeightSpinBox->setEnabled(true); windowBorderCheckBox->setEnabled(true); } } void Launcher::GraphicsPage::slotStandardToggled(bool checked) { if (checked) { resolutionComboBox->setEnabled(true); customWidthSpinBox->setEnabled(false); customHeightSpinBox->setEnabled(false); } else { resolutionComboBox->setEnabled(false); customWidthSpinBox->setEnabled(true); customHeightSpinBox->setEnabled(true); } } void Launcher::GraphicsPage::slotFramerateLimitToggled(bool checked) { framerateLimitSpinBox->setEnabled(checked); } void Launcher::GraphicsPage::slotShadowDistLimitToggled(bool checked) { shadowDistanceSpinBox->setEnabled(checked); fadeStartSpinBox->setEnabled(checked); } ================================================ FILE: apps/launcher/graphicspage.hpp ================================================ #ifndef GRAPHICSPAGE_H #define GRAPHICSPAGE_H #include "ui_graphicspage.h" #include #include "sdlinit.hpp" namespace Files { struct ConfigurationManager; } namespace Launcher { class GraphicsSettings; class GraphicsPage : public QWidget, private Ui::GraphicsPage { Q_OBJECT public: explicit GraphicsPage(QWidget *parent = nullptr); void saveSettings(); bool loadSettings(); public slots: void screenChanged(int screen); private slots: void slotFullScreenChanged(int state); void slotStandardToggled(bool checked); void slotFramerateLimitToggled(bool checked); void slotShadowDistLimitToggled(bool checked); private: QVector mResolutionsPerScreen; static QStringList getAvailableResolutions(int screen); static QRect getMaximumResolution(); bool setupSDL(); }; } #endif ================================================ FILE: apps/launcher/main.cpp ================================================ #include #include #include #include #ifdef MAC_OS_X_VERSION_MIN_REQUIRED #undef MAC_OS_X_VERSION_MIN_REQUIRED // We need to do this because of Qt: https://bugreports.qt-project.org/browse/QTBUG-22154 #define MAC_OS_X_VERSION_MIN_REQUIRED __ENVIRONMENT_MAC_OS_X_VERSION_MIN_REQUIRED__ #endif // MAC_OS_X_VERSION_MIN_REQUIRED #include "maindialog.hpp" int main(int argc, char *argv[]) { try { QApplication app(argc, argv); // Internationalization QString locale = QLocale::system().name().section('_', 0, 0); QTranslator appTranslator; appTranslator.load(":/translations/" + locale + ".qm"); app.installTranslator(&appTranslator); // Now we make sure the current dir is set to application path QDir dir(QCoreApplication::applicationDirPath()); QDir::setCurrent(dir.absolutePath()); Launcher::MainDialog mainWin; Launcher::FirstRunDialogResult result = mainWin.showFirstRunDialog(); if (result == Launcher::FirstRunDialogResultFailure) return 0; if (result == Launcher::FirstRunDialogResultContinue) mainWin.show(); int exitCode = app.exec(); return exitCode; } catch (std::exception& e) { std::cerr << "ERROR: " << e.what() << std::endl; return 0; } } ================================================ FILE: apps/launcher/maindialog.cpp ================================================ #include "maindialog.hpp" #include #include #include #include #include #include #include #include #include #include "playpage.hpp" #include "graphicspage.hpp" #include "datafilespage.hpp" #include "settingspage.hpp" #include "advancedpage.hpp" using namespace Process; void cfgError(const QString& title, const QString& msg) { QMessageBox msgBox; msgBox.setWindowTitle(title); msgBox.setIcon(QMessageBox::Critical); msgBox.setStandardButtons(QMessageBox::Ok); msgBox.setText(msg); msgBox.exec(); } Launcher::MainDialog::MainDialog(QWidget *parent) : QMainWindow(parent), mGameSettings (mCfgMgr) { setupUi(this); mGameInvoker = new ProcessInvoker(); mWizardInvoker = new ProcessInvoker(); connect(mWizardInvoker->getProcess(), SIGNAL(started()), this, SLOT(wizardStarted())); connect(mWizardInvoker->getProcess(), SIGNAL(finished(int,QProcess::ExitStatus)), this, SLOT(wizardFinished(int,QProcess::ExitStatus))); iconWidget->setViewMode(QListView::IconMode); iconWidget->setWrapping(false); iconWidget->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff); // Just to be sure iconWidget->setIconSize(QSize(48, 48)); iconWidget->setMovement(QListView::Static); iconWidget->setSpacing(4); iconWidget->setCurrentRow(0); iconWidget->setFlow(QListView::LeftToRight); QPushButton *helpButton = new QPushButton(tr("Help")); QPushButton *playButton = new QPushButton(tr("Play")); buttonBox->button(QDialogButtonBox::Close)->setText(tr("Close")); buttonBox->addButton(helpButton, QDialogButtonBox::HelpRole); buttonBox->addButton(playButton, QDialogButtonBox::AcceptRole); connect(buttonBox, SIGNAL(rejected()), this, SLOT(close())); connect(buttonBox, SIGNAL(accepted()), this, SLOT(play())); connect(buttonBox, SIGNAL(helpRequested()), this, SLOT(help())); // Remove what's this? button setWindowFlags(this->windowFlags() & ~Qt::WindowContextHelpButtonHint); createIcons(); } Launcher::MainDialog::~MainDialog() { delete mGameInvoker; delete mWizardInvoker; } void Launcher::MainDialog::createIcons() { if (!QIcon::hasThemeIcon("document-new")) QIcon::setThemeName("tango"); QListWidgetItem *playButton = new QListWidgetItem(iconWidget); playButton->setIcon(QIcon(":/images/openmw.png")); playButton->setText(tr("Play")); playButton->setTextAlignment(Qt::AlignCenter); playButton->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled); QListWidgetItem *dataFilesButton = new QListWidgetItem(iconWidget); dataFilesButton->setIcon(QIcon(":/images/openmw-plugin.png")); dataFilesButton->setText(tr("Data Files")); dataFilesButton->setTextAlignment(Qt::AlignHCenter | Qt::AlignBottom); dataFilesButton->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled); QListWidgetItem *graphicsButton = new QListWidgetItem(iconWidget); graphicsButton->setIcon(QIcon(":/images/preferences-video.png")); graphicsButton->setText(tr("Graphics")); graphicsButton->setTextAlignment(Qt::AlignHCenter | Qt::AlignBottom | Qt::AlignAbsolute); graphicsButton->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled); QListWidgetItem *settingsButton = new QListWidgetItem(iconWidget); settingsButton->setIcon(QIcon(":/images/preferences.png")); settingsButton->setText(tr("Settings")); settingsButton->setTextAlignment(Qt::AlignHCenter | Qt::AlignBottom); settingsButton->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled); QListWidgetItem *advancedButton = new QListWidgetItem(iconWidget); advancedButton->setIcon(QIcon(":/images/preferences-advanced.png")); advancedButton->setText(tr("Advanced")); advancedButton->setTextAlignment(Qt::AlignHCenter | Qt::AlignBottom); advancedButton->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled); connect(iconWidget, SIGNAL(currentItemChanged(QListWidgetItem*,QListWidgetItem*)), this, SLOT(changePage(QListWidgetItem*,QListWidgetItem*))); } void Launcher::MainDialog::createPages() { // Avoid creating the widgets twice if (pagesWidget->count() != 0) return; mPlayPage = new PlayPage(this); mDataFilesPage = new DataFilesPage(mCfgMgr, mGameSettings, mLauncherSettings, this); mGraphicsPage = new GraphicsPage(this); mSettingsPage = new SettingsPage(mCfgMgr, mGameSettings, mLauncherSettings, this); mAdvancedPage = new AdvancedPage(mGameSettings, this); // Set the combobox of the play page to imitate the combobox on the datafilespage mPlayPage->setProfilesModel(mDataFilesPage->profilesModel()); mPlayPage->setProfilesIndex(mDataFilesPage->profilesIndex()); // Add the pages to the stacked widget pagesWidget->addWidget(mPlayPage); pagesWidget->addWidget(mDataFilesPage); pagesWidget->addWidget(mGraphicsPage); pagesWidget->addWidget(mSettingsPage); pagesWidget->addWidget(mAdvancedPage); // Select the first page iconWidget->setCurrentItem(iconWidget->item(0), QItemSelectionModel::Select); connect(mPlayPage, SIGNAL(playButtonClicked()), this, SLOT(play())); connect(mPlayPage, SIGNAL(signalProfileChanged(int)), mDataFilesPage, SLOT(slotProfileChanged(int))); 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() { if (!setupLauncherSettings()) return FirstRunDialogResultFailure; if (mLauncherSettings.value(QString("General/firstrun"), QString("true")) == QLatin1String("true")) { QMessageBox msgBox; msgBox.setWindowTitle(tr("First run")); msgBox.setIcon(QMessageBox::Question); msgBox.setStandardButtons(QMessageBox::NoButton); msgBox.setText(tr("

Welcome to OpenMW!

\

It is recommended to run the Installation Wizard.

\

The Wizard will let you select an existing Morrowind installation, \ or install Morrowind for OpenMW to use.

")); QAbstractButton *wizardButton = msgBox.addButton(tr("Run &Installation Wizard"), QMessageBox::AcceptRole); // ActionRole doesn't work?! QAbstractButton *skipButton = msgBox.addButton(tr("Skip"), QMessageBox::RejectRole); msgBox.exec(); if (msgBox.clickedButton() == wizardButton) { if (mWizardInvoker->startProcess(QLatin1String("openmw-wizard"), false)) return FirstRunDialogResultWizard; } else if (msgBox.clickedButton() == skipButton) { // Don't bother setting up absent game data. if (setup()) return FirstRunDialogResultContinue; } return FirstRunDialogResultFailure; } if (!setup() || !setupGameData()) { return FirstRunDialogResultFailure; } return FirstRunDialogResultContinue; } void Launcher::MainDialog::setVersionLabel() { // Add version information to bottom of the window Version::Version v = Version::getOpenmwVersion(mGameSettings.value("resources").toUtf8().constData()); QString revision(QString::fromUtf8(v.mCommitHash.c_str())); QString tag(QString::fromUtf8(v.mTagHash.c_str())); versionLabel->setTextInteractionFlags(Qt::TextSelectableByMouse); if (!v.mVersion.empty() && (revision.isEmpty() || revision == tag)) versionLabel->setText(tr("OpenMW %1 release").arg(QString::fromUtf8(v.mVersion.c_str()))); else versionLabel->setText(tr("OpenMW development (%1)").arg(revision.left(10))); // Add the compile date and time auto compileDate = QLocale(QLocale::C).toDate(QString(__DATE__).simplified(), QLatin1String("MMM d yyyy")); auto compileTime = QLocale(QLocale::C).toTime(QString(__TIME__).simplified(), QLatin1String("hh:mm:ss")); versionLabel->setToolTip(tr("Compiled on %1 %2").arg(QLocale::system().toString(compileDate, QLocale::LongFormat), QLocale::system().toString(compileTime, QLocale::ShortFormat))); } bool Launcher::MainDialog::setup() { if (!setupGameSettings()) return false; setVersionLabel(); mLauncherSettings.setContentList(mGameSettings); if (!setupGraphicsSettings()) return false; // Now create the pages as they need the settings createPages(); // Call this so we can exit on SDL errors before mainwindow is shown if (!mGraphicsPage->loadSettings()) return false; loadSettings(); return true; } bool Launcher::MainDialog::reloadSettings() { if (!setupLauncherSettings()) return false; if (!setupGameSettings()) return false; mLauncherSettings.setContentList(mGameSettings); if (!setupGraphicsSettings()) return false; if (!mSettingsPage->loadSettings()) return false; if (!mDataFilesPage->loadSettings()) return false; if (!mGraphicsPage->loadSettings()) return false; if (!mAdvancedPage->loadSettings()) return false; return true; } void Launcher::MainDialog::changePage(QListWidgetItem *current, QListWidgetItem *previous) { if (!current) current = previous; int currentIndex = iconWidget->row(current); pagesWidget->setCurrentIndex(currentIndex); mSettingsPage->resetProgressBar(); } bool Launcher::MainDialog::setupLauncherSettings() { mLauncherSettings.clear(); mLauncherSettings.setMultiValueEnabled(true); QString userPath = QString::fromUtf8(mCfgMgr.getUserConfigPath().string().c_str()); QStringList paths; paths.append(QString(Config::LauncherSettings::sLauncherConfigFileName)); paths.append(userPath + QString(Config::LauncherSettings::sLauncherConfigFileName)); for (const QString &path : paths) { qDebug() << "Loading config file:" << path.toUtf8().constData(); QFile file(path); if (file.exists()) { if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { cfgError(tr("Error opening OpenMW configuration file"), tr("
Could not open %0 for reading

\ Please make sure you have the right permissions \ and try again.
").arg(file.fileName())); return false; } QTextStream stream(&file); stream.setCodec(QTextCodec::codecForName("UTF-8")); mLauncherSettings.readFile(stream); } file.close(); } return true; } bool Launcher::MainDialog::setupGameSettings() { mGameSettings.clear(); QString localPath = QString::fromUtf8(mCfgMgr.getLocalPath().string().c_str()); QString userPath = QString::fromUtf8(mCfgMgr.getUserConfigPath().string().c_str()); QString globalPath = QString::fromUtf8(mCfgMgr.getGlobalPath().string().c_str()); // Load the user config file first, separately // So we can write it properly, uncontaminated QString path = userPath + QLatin1String("openmw.cfg"); QFile file(path); qDebug() << "Loading config file:" << path.toUtf8().constData(); if (file.exists()) { if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { cfgError(tr("Error opening OpenMW configuration file"), tr("
Could not open %0 for reading

\ Please make sure you have the right permissions \ and try again.
").arg(file.fileName())); return false; } QTextStream stream(&file); stream.setCodec(QTextCodec::codecForName("UTF-8")); mGameSettings.readUserFile(stream); file.close(); } // Now the rest - priority: user > local > global QStringList paths; paths.append(globalPath + QString("openmw.cfg")); paths.append(localPath + QString("openmw.cfg")); paths.append(userPath + QString("openmw.cfg")); for (const QString &path2 : paths) { qDebug() << "Loading config file:" << path2.toUtf8().constData(); file.setFileName(path2); if (file.exists()) { if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { cfgError(tr("Error opening OpenMW configuration file"), tr("
Could not open %0 for reading

\ Please make sure you have the right permissions \ and try again.
").arg(file.fileName())); return false; } QTextStream stream(&file); stream.setCodec(QTextCodec::codecForName("UTF-8")); mGameSettings.readFile(stream); file.close(); } } return true; } bool Launcher::MainDialog::setupGameData() { QStringList dataDirs; // Check if the paths actually contain data files for (const QString& path3 : mGameSettings.getDataDirs()) { QDir dir(path3); QStringList filters; filters << "*.esp" << "*.esm" << "*.omwgame" << "*.omwaddon"; if (!dir.entryList(filters).isEmpty()) dataDirs.append(path3); } if (dataDirs.isEmpty()) { QMessageBox msgBox; msgBox.setWindowTitle(tr("Error detecting Morrowind installation")); msgBox.setIcon(QMessageBox::Warning); msgBox.setStandardButtons(QMessageBox::NoButton); msgBox.setText(tr("
Could not find the Data Files location

\ The directory containing the data files was not found.")); QAbstractButton *wizardButton = msgBox.addButton(tr("Run &Installation Wizard..."), QMessageBox::ActionRole); QAbstractButton *skipButton = msgBox.addButton(tr("Skip"), QMessageBox::RejectRole); Q_UNUSED(skipButton); // Suppress compiler unused warning msgBox.exec(); if (msgBox.clickedButton() == wizardButton) { if (!mWizardInvoker->startProcess(QLatin1String("openmw-wizard"), false)) return false; } } return true; } bool Launcher::MainDialog::setupGraphicsSettings() { // This method is almost a copy of OMW::Engine::loadSettings(). They should definitely // remain consistent, and possibly be merged into a shared component. At the very least // the filenames should be in the CfgMgr component. // Ensure to clear previous settings in case we had already loaded settings. mEngineSettings.clear(); // Create the settings manager and load default settings file const std::string localDefault = (mCfgMgr.getLocalPath() / "defaults.bin").string(); const std::string globalDefault = (mCfgMgr.getGlobalPath() / "defaults.bin").string(); std::string defaultPath; // Prefer the defaults.bin in the current directory. if (boost::filesystem::exists(localDefault)) defaultPath = localDefault; else if (boost::filesystem::exists(globalDefault)) defaultPath = globalDefault; // Something's very wrong if we can't find the file at all. else { cfgError(tr("Error reading OpenMW configuration file"), tr("
Could not find defaults.bin

\ The problem may be due to an incomplete installation of OpenMW.
\ Reinstalling OpenMW may resolve the problem.")); return false; } // Load the default settings, report any parsing errors. try { mEngineSettings.loadDefault(defaultPath); } catch (std::exception& e) { std::string msg = std::string("
Error reading defaults.bin

") + e.what(); cfgError(tr("Error reading OpenMW configuration file"), tr(msg.c_str())); return false; } // Load user settings if they exist const std::string userPath = (mCfgMgr.getUserConfigPath() / "settings.cfg").string(); // User settings are not required to exist, so if they don't we're done. if (!boost::filesystem::exists(userPath)) return true; try { mEngineSettings.loadUser(userPath); } catch (std::exception& e) { std::string msg = std::string("
Error reading settings.cfg

") + e.what(); cfgError(tr("Error reading OpenMW configuration file"), tr(msg.c_str())); return false; } return true; } void Launcher::MainDialog::loadSettings() { int width = mLauncherSettings.value(QString("General/MainWindow/width")).toInt(); int height = mLauncherSettings.value(QString("General/MainWindow/height")).toInt(); int posX = mLauncherSettings.value(QString("General/MainWindow/posx")).toInt(); int posY = mLauncherSettings.value(QString("General/MainWindow/posy")).toInt(); resize(width, height); move(posX, posY); } void Launcher::MainDialog::saveSettings() { QString width = QString::number(this->width()); QString height = QString::number(this->height()); mLauncherSettings.setValue(QString("General/MainWindow/width"), width); mLauncherSettings.setValue(QString("General/MainWindow/height"), height); QString posX = QString::number(this->pos().x()); QString posY = QString::number(this->pos().y()); mLauncherSettings.setValue(QString("General/MainWindow/posx"), posX); mLauncherSettings.setValue(QString("General/MainWindow/posy"), posY); mLauncherSettings.setValue(QString("General/firstrun"), QString("false")); } bool Launcher::MainDialog::writeSettings() { // Now write all config files saveSettings(); mDataFilesPage->saveSettings(); mGraphicsPage->saveSettings(); mSettingsPage->saveSettings(); mAdvancedPage->saveSettings(); QString userPath = QString::fromUtf8(mCfgMgr.getUserConfigPath().string().c_str()); QDir dir(userPath); if (!dir.exists()) { if (!dir.mkpath(userPath)) { cfgError(tr("Error creating OpenMW configuration directory"), tr("
Could not create %0

\ Please make sure you have the right permissions \ and try again.
").arg(userPath)); return false; } } // Game settings QFile file(userPath + QString("openmw.cfg")); if (!file.open(QIODevice::ReadWrite | QIODevice::Text)) { // File cannot be opened or created cfgError(tr("Error writing OpenMW configuration file"), tr("
Could not open or create %0 for writing

\ Please make sure you have the right permissions \ and try again.
").arg(file.fileName())); return false; } mGameSettings.writeFileWithComments(file); file.close(); // Graphics settings const std::string settingsPath = (mCfgMgr.getUserConfigPath() / "settings.cfg").string(); try { mEngineSettings.saveUser(settingsPath); } catch (std::exception& e) { std::string msg = "
Error writing settings.cfg

" + settingsPath + "

" + e.what(); cfgError(tr("Error writing user settings file"), tr(msg.c_str())); return false; } // Launcher settings file.setFileName(userPath + QString(Config::LauncherSettings::sLauncherConfigFileName)); if (!file.open(QIODevice::ReadWrite | QIODevice::Text | QIODevice::Truncate)) { // File cannot be opened or created cfgError(tr("Error writing Launcher configuration file"), tr("
Could not open or create %0 for writing

\ Please make sure you have the right permissions \ and try again.
").arg(file.fileName())); return false; } QTextStream stream(&file); stream.setDevice(&file); stream.setCodec(QTextCodec::codecForName("UTF-8")); mLauncherSettings.writeFile(stream); file.close(); return true; } void Launcher::MainDialog::closeEvent(QCloseEvent *event) { writeSettings(); event->accept(); } void Launcher::MainDialog::wizardStarted() { hide(); } void Launcher::MainDialog::wizardFinished(int exitCode, QProcess::ExitStatus exitStatus) { if (exitCode != 0 || exitStatus == QProcess::CrashExit) return qApp->quit(); // HACK: Ensure the pages are created, else segfault setup(); if (setupGameData() && reloadSettings()) show(); } void Launcher::MainDialog::play() { if (!writeSettings()) return qApp->quit(); if (!mGameSettings.hasMaster()) { QMessageBox msgBox; msgBox.setWindowTitle(tr("No game file selected")); msgBox.setIcon(QMessageBox::Warning); msgBox.setStandardButtons(QMessageBox::Ok); msgBox.setText(tr("
You do not have a game file selected.

\ OpenMW will not start without a game file selected.
")); msgBox.exec(); return; } // Launch the game detached if (mGameInvoker->startProcess(QLatin1String("tes3mp-browser"), true)) return qApp->quit(); } void Launcher::MainDialog::help() { Misc::HelpViewer::openHelp("reference/index.html"); } ================================================ FILE: apps/launcher/maindialog.hpp ================================================ #ifndef MAINDIALOG_H #define MAINDIALOG_H #ifndef Q_MOC_RUN #include #include #include #include #include #endif #include "ui_mainwindow.h" class QListWidgetItem; class QStackedWidget; class QStringList; class QStringListModel; class QString; namespace Launcher { class PlayPage; class GraphicsPage; class DataFilesPage; class UnshieldThread; class SettingsPage; class AdvancedPage; enum FirstRunDialogResult { FirstRunDialogResultFailure, FirstRunDialogResultContinue, FirstRunDialogResultWizard }; #ifndef WIN32 bool expansions(Launcher::UnshieldThread& cd); #endif class MainDialog : public QMainWindow, private Ui::MainWindow { Q_OBJECT public: explicit MainDialog(QWidget *parent = nullptr); ~MainDialog(); FirstRunDialogResult showFirstRunDialog(); bool reloadSettings(); bool writeSettings(); public slots: void changePage(QListWidgetItem *current, QListWidgetItem *previous); void play(); void help(); private slots: void wizardStarted(); void wizardFinished(int exitCode, QProcess::ExitStatus exitStatus); private: bool setup(); void createIcons(); void createPages(); bool setupLauncherSettings(); bool setupGameSettings(); bool setupGraphicsSettings(); bool setupGameData(); void setVersionLabel(); void loadSettings(); void saveSettings(); inline bool startProgram(const QString &name, bool detached = false) { return startProgram(name, QStringList(), detached); } bool startProgram(const QString &name, const QStringList &arguments, bool detached = false); void closeEvent(QCloseEvent *event) override; PlayPage *mPlayPage; GraphicsPage *mGraphicsPage; DataFilesPage *mDataFilesPage; SettingsPage *mSettingsPage; AdvancedPage *mAdvancedPage; Process::ProcessInvoker *mGameInvoker; Process::ProcessInvoker *mWizardInvoker; Files::ConfigurationManager mCfgMgr; Config::GameSettings mGameSettings; Settings::Manager mEngineSettings; Config::LauncherSettings mLauncherSettings; }; } #endif ================================================ FILE: apps/launcher/playpage.cpp ================================================ #include "playpage.hpp" #include Launcher::PlayPage::PlayPage(QWidget *parent) : QWidget(parent) { setObjectName ("PlayPage"); setupUi(this); profilesComboBox->setView(new QListView()); connect(profilesComboBox, SIGNAL(activated(int)), this, SIGNAL (signalProfileChanged(int))); connect(playButton, SIGNAL(clicked()), this, SLOT(slotPlayClicked())); } void Launcher::PlayPage::setProfilesModel(QAbstractItemModel *model) { profilesComboBox->setModel(model); } void Launcher::PlayPage::setProfilesIndex(int index) { profilesComboBox->setCurrentIndex(index); } void Launcher::PlayPage::slotPlayClicked() { emit playButtonClicked(); } ================================================ FILE: apps/launcher/playpage.hpp ================================================ #ifndef PLAYPAGE_H #define PLAYPAGE_H #include "ui_playpage.h" class QComboBox; class QPushButton; class QAbstractItemModel; namespace Launcher { class PlayPage : public QWidget, private Ui::PlayPage { Q_OBJECT public: PlayPage(QWidget *parent = nullptr); void setProfilesModel(QAbstractItemModel *model); signals: void signalProfileChanged(int index); void playButtonClicked(); public slots: void setProfilesIndex(int index); private slots: void slotPlayClicked(); }; } #endif ================================================ FILE: apps/launcher/sdlinit.cpp ================================================ #include #include bool initSDL() { SDL_SetHint(SDL_HINT_RENDER_DRIVER, "software"); SDL_SetMainReady(); // Required for determining screen resolution and such on the Graphics tab if (SDL_Init(SDL_INIT_VIDEO) != 0) { return false; } signal(SIGINT, SIG_DFL); // We don't want to use the SDL event loop in the launcher, // so reset SIGINT which SDL wants to redirect to an SDL_Quit event. return true; } void quitSDL() { // Disconnect from SDL processes SDL_Quit(); } ================================================ FILE: apps/launcher/sdlinit.hpp ================================================ #ifndef SDLINIT_H #define SDLINIT_H bool initSDL(); void quitSDL(); #endif ================================================ FILE: apps/launcher/settingspage.cpp ================================================ #include "settingspage.hpp" #include #include #include #include #include #include #include "utils/textinputdialog.hpp" #include "datafilespage.hpp" using namespace Process; Launcher::SettingsPage::SettingsPage(Files::ConfigurationManager &cfg, Config::GameSettings &gameSettings, Config::LauncherSettings &launcherSettings, MainDialog *parent) : QWidget(parent) , mCfgMgr(cfg) , mGameSettings(gameSettings) , mLauncherSettings(launcherSettings) , mMain(parent) { setupUi(this); QStringList languages; languages << tr("English") << tr("French") << tr("German") << tr("Italian") << tr("Polish") << tr("Russian") << tr("Spanish"); languageComboBox->addItems(languages); mWizardInvoker = new ProcessInvoker(); mImporterInvoker = new ProcessInvoker(); resetProgressBar(); connect(mWizardInvoker->getProcess(), SIGNAL(started()), this, SLOT(wizardStarted())); connect(mWizardInvoker->getProcess(), SIGNAL(finished(int,QProcess::ExitStatus)), this, SLOT(wizardFinished(int,QProcess::ExitStatus))); connect(mImporterInvoker->getProcess(), SIGNAL(started()), this, SLOT(importerStarted())); connect(mImporterInvoker->getProcess(), SIGNAL(finished(int,QProcess::ExitStatus)), this, SLOT(importerFinished(int,QProcess::ExitStatus))); mProfileDialog = new TextInputDialog(tr("New Content List"), tr("Content List name:"), this); connect(mProfileDialog->lineEdit(), SIGNAL(textChanged(QString)), this, SLOT(updateOkButton(QString))); // Detect Morrowind configuration files QStringList iniPaths; for (const QString &path : mGameSettings.getDataDirs()) { QDir dir(path); dir.setPath(dir.canonicalPath()); // Resolve symlinks if (dir.exists(QString("Morrowind.ini"))) iniPaths.append(dir.absoluteFilePath(QString("Morrowind.ini"))); else { if (!dir.cdUp()) continue; // Cannot move from Data Files if (dir.exists(QString("Morrowind.ini"))) iniPaths.append(dir.absoluteFilePath(QString("Morrowind.ini"))); } } if (!iniPaths.isEmpty()) { settingsComboBox->addItems(iniPaths); importerButton->setEnabled(true); } else { importerButton->setEnabled(false); } loadSettings(); } Launcher::SettingsPage::~SettingsPage() { delete mWizardInvoker; delete mImporterInvoker; } void Launcher::SettingsPage::on_wizardButton_clicked() { mMain->writeSettings(); if (!mWizardInvoker->startProcess(QLatin1String("openmw-wizard"), false)) return; } void Launcher::SettingsPage::on_importerButton_clicked() { mMain->writeSettings(); // Create the file if it doesn't already exist, else the importer will fail QString path(QString::fromUtf8(mCfgMgr.getUserConfigPath().string().c_str())); path.append(QLatin1String("openmw.cfg")); QFile file(path); if (!file.exists()) { if (!file.open(QIODevice::ReadWrite)) { // File cannot be created QMessageBox msgBox; msgBox.setWindowTitle(tr("Error writing OpenMW configuration file")); msgBox.setIcon(QMessageBox::Critical); msgBox.setStandardButtons(QMessageBox::Ok); msgBox.setText(tr("

Could not open or create %1 for writing

\

Please make sure you have the right permissions \ and try again.

").arg(file.fileName())); msgBox.exec(); return; } file.close(); } // Construct the arguments to run the importer QStringList arguments; if (addonsCheckBox->isChecked()) arguments.append(QString("--game-files")); arguments.append(QString("--encoding")); arguments.append(mGameSettings.value(QString("encoding"), QString("win1252"))); arguments.append(QString("--ini")); arguments.append(settingsComboBox->currentText()); arguments.append(QString("--cfg")); arguments.append(path); qDebug() << "arguments " << arguments; // start the progress bar as a "bouncing ball" progressBar->setMaximum(0); progressBar->setValue(0); if (!mImporterInvoker->startProcess(QLatin1String("openmw-iniimporter"), arguments, false)) { resetProgressBar(); } } void Launcher::SettingsPage::on_browseButton_clicked() { QString iniFile = QFileDialog::getOpenFileName( this, QObject::tr("Select configuration file"), QDir::currentPath(), QString(tr("Morrowind configuration file (*.ini)"))); if (iniFile.isEmpty()) return; QFileInfo info(iniFile); if (!info.exists() || !info.isReadable()) return; const QString path(QDir::toNativeSeparators(info.absoluteFilePath())); if (settingsComboBox->findText(path) == -1) { settingsComboBox->addItem(path); settingsComboBox->setCurrentIndex(settingsComboBox->findText(path)); importerButton->setEnabled(true); } } void Launcher::SettingsPage::wizardStarted() { mMain->hide(); // Hide the launcher wizardButton->setEnabled(false); } void Launcher::SettingsPage::wizardFinished(int exitCode, QProcess::ExitStatus exitStatus) { if (exitCode != 0 || exitStatus == QProcess::CrashExit) return qApp->quit(); mMain->reloadSettings(); wizardButton->setEnabled(true); mMain->show(); // Show the launcher again } void Launcher::SettingsPage::importerStarted() { importerButton->setEnabled(false); } void Launcher::SettingsPage::importerFinished(int exitCode, QProcess::ExitStatus exitStatus) { if (exitCode != 0 || exitStatus == QProcess::CrashExit) { resetProgressBar(); QMessageBox msgBox; msgBox.setWindowTitle(tr("Importer finished")); msgBox.setStandardButtons(QMessageBox::Ok); msgBox.setIcon(QMessageBox::Warning); msgBox.setText(tr("Failed to import settings from INI file.")); msgBox.exec(); } else { // indicate progress finished progressBar->setMaximum(1); progressBar->setValue(1); // Importer may have changed settings, so refresh mMain->reloadSettings(); } importerButton->setEnabled(true); } void Launcher::SettingsPage::resetProgressBar() { // set progress bar to 0 % progressBar->reset(); } void Launcher::SettingsPage::updateOkButton(const QString &text) { // We do this here because we need to access the profiles if (text.isEmpty()) { mProfileDialog->setOkButtonEnabled(false); return; } const QStringList profiles(mLauncherSettings.getContentLists()); (profiles.contains(text)) ? mProfileDialog->setOkButtonEnabled(false) : mProfileDialog->setOkButtonEnabled(true); } void Launcher::SettingsPage::saveSettings() { QString language(languageComboBox->currentText()); mLauncherSettings.setValue(QLatin1String("Settings/language"), language); if (language == QLatin1String("Polish")) { mGameSettings.setValue(QLatin1String("encoding"), QLatin1String("win1250")); } else if (language == QLatin1String("Russian")) { mGameSettings.setValue(QLatin1String("encoding"), QLatin1String("win1251")); } else { mGameSettings.setValue(QLatin1String("encoding"), QLatin1String("win1252")); } } bool Launcher::SettingsPage::loadSettings() { QString language(mLauncherSettings.value(QLatin1String("Settings/language"))); int index = languageComboBox->findText(language); if (index != -1) languageComboBox->setCurrentIndex(index); return true; } ================================================ FILE: apps/launcher/settingspage.hpp ================================================ #ifndef SETTINGSPAGE_HPP #define SETTINGSPAGE_HPP #include #include "ui_settingspage.h" #include "maindialog.hpp" namespace Files { struct ConfigurationManager; } namespace Config { class GameSettings; class LauncherSettings; } namespace Launcher { class TextInputDialog; class SettingsPage : public QWidget, private Ui::SettingsPage { Q_OBJECT public: SettingsPage(Files::ConfigurationManager &cfg, Config::GameSettings &gameSettings, Config::LauncherSettings &launcherSettings, MainDialog *parent = nullptr); ~SettingsPage(); void saveSettings(); bool loadSettings(); /// set progress bar on page to 0% void resetProgressBar(); private slots: void on_wizardButton_clicked(); void on_importerButton_clicked(); void on_browseButton_clicked(); void wizardStarted(); void wizardFinished(int exitCode, QProcess::ExitStatus exitStatus); void importerStarted(); void importerFinished(int exitCode, QProcess::ExitStatus exitStatus); void updateOkButton(const QString &text); private: Process::ProcessInvoker *mWizardInvoker; Process::ProcessInvoker *mImporterInvoker; Files::ConfigurationManager &mCfgMgr; Config::GameSettings &mGameSettings; Config::LauncherSettings &mLauncherSettings; MainDialog *mMain; TextInputDialog *mProfileDialog; }; } #endif // SETTINGSPAGE_HPP ================================================ FILE: apps/launcher/textslotmsgbox.cpp ================================================ #include "textslotmsgbox.hpp" void Launcher::TextSlotMsgBox::setTextSlot(const QString& string) { setText(string); } ================================================ FILE: apps/launcher/textslotmsgbox.hpp ================================================ #ifndef TEXT_SLOT_MSG_BOX #define TEXT_SLOT_MSG_BOX #include namespace Launcher { class TextSlotMsgBox : public QMessageBox { Q_OBJECT public slots: void setTextSlot(const QString& string); }; } #endif ================================================ FILE: apps/launcher/utils/cellnameloader.cpp ================================================ #include "cellnameloader.hpp" #include #include QSet CellNameLoader::getCellNames(QStringList &contentPaths) { QSet cellNames; ESM::ESMReader esmReader; // Loop through all content files for (auto &contentPath : contentPaths) { esmReader.open(contentPath.toStdString()); // Loop through all records while(esmReader.hasMoreRecs()) { ESM::NAME recordName = esmReader.getRecName(); esmReader.getRecHeader(); if (isCellRecord(recordName)) { QString cellName = getCellName(esmReader); if (!cellName.isEmpty()) { cellNames.insert(cellName); } } // Stop loading content for this record and continue to the next esmReader.skipRecord(); } } return cellNames; } bool CellNameLoader::isCellRecord(ESM::NAME &recordName) { return recordName.intval == ESM::REC_CELL; } QString CellNameLoader::getCellName(ESM::ESMReader &esmReader) { ESM::Cell cell; bool isDeleted = false; cell.loadNameAndData(esmReader, isDeleted); return QString::fromStdString(cell.mName); } ================================================ FILE: apps/launcher/utils/cellnameloader.hpp ================================================ #ifndef OPENMW_CELLNAMELOADER_H #define OPENMW_CELLNAMELOADER_H #include #include #include namespace ESM {class ESMReader; struct Cell;} namespace ContentSelectorView {class ContentSelector;} class CellNameLoader { public: /** * Returns the names of all cells contained within the given content files * @param contentPaths the file paths of each content file to be examined * @return the names of all cells */ QSet getCellNames(QStringList &contentPaths); private: /** * Returns whether or not the given record is of type "Cell" * @param name The name associated with the record * @return whether or not the given record is of type "Cell" */ bool isCellRecord(ESM::NAME &name); /** * Returns the name of the cell * @param esmReader the reader currently pointed to a loaded cell * @return the name of the cell */ QString getCellName(ESM::ESMReader &esmReader); }; #endif //OPENMW_CELLNAMELOADER_H ================================================ FILE: apps/launcher/utils/lineedit.cpp ================================================ #include "lineedit.hpp" LineEdit::LineEdit(QWidget *parent) : QLineEdit(parent) { setupClearButton(); } void LineEdit::setupClearButton() { mClearButton = new QToolButton(this); QPixmap pixmap(":images/clear.png"); mClearButton->setIcon(QIcon(pixmap)); mClearButton->setIconSize(pixmap.size()); mClearButton->setCursor(Qt::ArrowCursor); mClearButton->setStyleSheet("QToolButton { border: none; padding: 0px; }"); mClearButton->hide(); connect(mClearButton, SIGNAL(clicked()), this, SLOT(clear())); connect(this, SIGNAL(textChanged(const QString&)), this, SLOT(updateClearButton(const QString&))); } void LineEdit::resizeEvent(QResizeEvent *) { QSize sz = mClearButton->sizeHint(); int frameWidth = style()->pixelMetric(QStyle::PM_DefaultFrameWidth); mClearButton->move(rect().right() - frameWidth - sz.width(), (rect().bottom() + 1 - sz.height())/2); } void LineEdit::updateClearButton(const QString& text) { mClearButton->setVisible(!text.isEmpty()); } ================================================ FILE: apps/launcher/utils/lineedit.hpp ================================================ /**************************************************************************** ** ** Copyright (c) 2007 Trolltech ASA ** ** Use, modification and distribution is allowed without limitation, ** warranty, liability or support of any kind. ** ****************************************************************************/ #ifndef LINEEDIT_H #define LINEEDIT_H #include #include #include class QToolButton; class LineEdit : public QLineEdit { Q_OBJECT QString mPlaceholderText; public: LineEdit(QWidget *parent = nullptr); protected: void resizeEvent(QResizeEvent *) override; private slots: void updateClearButton(const QString &text); protected: QToolButton *mClearButton; void setupClearButton(); }; #endif // LIENEDIT_H ================================================ FILE: apps/launcher/utils/openalutil.cpp ================================================ #include #include #include #include #include "openalutil.hpp" #ifndef ALC_ALL_DEVICES_SPECIFIER #define ALC_ALL_DEVICES_SPECIFIER 0x1013 #endif std::vector Launcher::enumerateOpenALDevices() { std::vector devlist; const ALCchar *devnames; if(alcIsExtensionPresent(nullptr, "ALC_ENUMERATE_ALL_EXT")) { devnames = alcGetString(nullptr, ALC_ALL_DEVICES_SPECIFIER); } else { devnames = alcGetString(nullptr, ALC_DEVICE_SPECIFIER); } while(devnames && *devnames) { devlist.emplace_back(devnames); devnames += strlen(devnames)+1; } return devlist; } std::vector Launcher::enumerateOpenALDevicesHrtf() { std::vector ret; ALCdevice *device = alcOpenDevice(nullptr); if(device) { if(alcIsExtensionPresent(device, "ALC_SOFT_HRTF")) { LPALCGETSTRINGISOFT alcGetStringiSOFT = nullptr; void* funcPtr = alcGetProcAddress(device, "alcGetStringiSOFT"); memcpy(&alcGetStringiSOFT, &funcPtr, sizeof(funcPtr)); ALCint num_hrtf; alcGetIntegerv(device, ALC_NUM_HRTF_SPECIFIERS_SOFT, 1, &num_hrtf); ret.reserve(num_hrtf); for(ALCint i = 0;i < num_hrtf;++i) { const ALCchar *entry = alcGetStringiSOFT(device, ALC_HRTF_SPECIFIER_SOFT, i); if(strcmp(entry, "") == 0) break; ret.emplace_back(entry); } } alcCloseDevice(device); } return ret; } ================================================ FILE: apps/launcher/utils/openalutil.hpp ================================================ #include namespace Launcher { std::vector enumerateOpenALDevices(); std::vector enumerateOpenALDevicesHrtf(); } ================================================ FILE: apps/launcher/utils/profilescombobox.cpp ================================================ #include #include #include #include #include "profilescombobox.hpp" ProfilesComboBox::ProfilesComboBox(QWidget *parent) : ContentSelectorView::ComboBox(parent) { connect(this, SIGNAL(activated(int)), this, SLOT(slotIndexChangedByUser(int))); setInsertPolicy(QComboBox::NoInsert); } void ProfilesComboBox::setEditEnabled(bool editable) { if (isEditable() == editable) return; if (!editable) { disconnect(lineEdit(), SIGNAL(editingFinished()), this, SLOT(slotEditingFinished())); disconnect(lineEdit(), SIGNAL(textChanged(QString)), this, SLOT(slotTextChanged(QString))); return setEditable(false); } // Reset the completer and validator setEditable(true); setValidator(mValidator); ComboBoxLineEdit *edit = new ComboBoxLineEdit(this); setLineEdit(edit); setCompleter(nullptr); connect(lineEdit(), SIGNAL(editingFinished()), this, SLOT(slotEditingFinished())); connect(lineEdit(), SIGNAL(textChanged(QString)), this, SLOT(slotTextChanged(QString))); connect (lineEdit(), SIGNAL(textChanged(QString)), this, SIGNAL (signalProfileTextChanged (QString))); } void ProfilesComboBox::slotTextChanged(const QString &text) { QPalette palette; palette.setColor(QPalette::Text,Qt::red); int index = findText(text); if (text.isEmpty() || (index != -1 && index != currentIndex())) { lineEdit()->setPalette(palette); } else { lineEdit()->setPalette(QApplication::palette()); } } void ProfilesComboBox::slotEditingFinished() { QString current = currentText(); QString previous = itemText(currentIndex()); if (currentIndex() == -1) return; if (current.isEmpty()) return; if (current == previous) return; if (findText(current) != -1) return; setItemText(currentIndex(), current); emit(profileRenamed(previous, current)); } void ProfilesComboBox::slotIndexChangedByUser(int index) { if (index == -1) return; emit (signalProfileChanged(mOldProfile, currentText())); mOldProfile = currentText(); } ProfilesComboBox::ComboBoxLineEdit::ComboBoxLineEdit (QWidget *parent) : LineEdit (parent) { int frameWidth = style()->pixelMetric(QStyle::PM_DefaultFrameWidth); setObjectName(QString("ComboBoxLineEdit")); setStyleSheet(QString("ComboBoxLineEdit { background-color: transparent; padding-right: %1px; } ").arg(mClearButton->sizeHint().width() + frameWidth + 1)); } ================================================ FILE: apps/launcher/utils/profilescombobox.hpp ================================================ #ifndef PROFILESCOMBOBOX_HPP #define PROFILESCOMBOBOX_HPP #include "components/contentselector/view/combobox.hpp" #include "lineedit.hpp" #include class QString; class ProfilesComboBox : public ContentSelectorView::ComboBox { Q_OBJECT public: class ComboBoxLineEdit : public LineEdit { public: explicit ComboBoxLineEdit (QWidget *parent = nullptr); }; public: explicit ProfilesComboBox(QWidget *parent = nullptr); void setEditEnabled(bool editable); void setCurrentProfile(int index) { ComboBox::setCurrentIndex(index); mOldProfile = currentText(); } signals: void signalProfileTextChanged(const QString &item); void signalProfileChanged(const QString &previous, const QString ¤t); void signalProfileChanged(int index); void profileRenamed(const QString &oldName, const QString &newName); private slots: void slotEditingFinished(); void slotIndexChangedByUser(int index); void slotTextChanged(const QString &text); private: QString mOldProfile; }; #endif // PROFILESCOMBOBOX_HPP ================================================ FILE: apps/launcher/utils/textinputdialog.cpp ================================================ #include "textinputdialog.hpp" #include #include #include #include #include #include Launcher::TextInputDialog::TextInputDialog(const QString& title, const QString &text, QWidget *parent) : QDialog(parent) { setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint); mButtonBox = new QDialogButtonBox(this); mButtonBox->addButton(QDialogButtonBox::Ok); mButtonBox->addButton(QDialogButtonBox::Cancel); mButtonBox->button(QDialogButtonBox::Ok)->setEnabled (false); QLabel *label = new QLabel(this); label->setText(text); // Line edit QValidator *validator = new QRegExpValidator(QRegExp("^[a-zA-Z0-9_]*$"), this); // Alpha-numeric + underscore mLineEdit = new LineEdit(this); mLineEdit->setValidator(validator); mLineEdit->setCompleter(nullptr); QVBoxLayout *dialogLayout = new QVBoxLayout(this); dialogLayout->addWidget(label); dialogLayout->addWidget(mLineEdit); dialogLayout->addWidget(mButtonBox); // Messageboxes on mac have no title #ifndef Q_OS_MAC setWindowTitle(title); #else Q_UNUSED(title); #endif setModal(true); connect(mButtonBox, SIGNAL(accepted()), this, SLOT(accept())); connect(mButtonBox, SIGNAL(rejected()), this, SLOT(reject())); } Launcher::TextInputDialog::~TextInputDialog() { } int Launcher::TextInputDialog::exec() { mLineEdit->clear(); mLineEdit->setFocus(); return QDialog::exec(); } void Launcher::TextInputDialog::setOkButtonEnabled(bool enabled) { QPushButton *okButton = mButtonBox->button(QDialogButtonBox::Ok); okButton->setEnabled(enabled); QPalette palette; palette.setColor(QPalette::Text, Qt::red); if (enabled) { mLineEdit->setPalette(QApplication::palette()); } else { // Existing profile name, make the text red mLineEdit->setPalette(palette); } } ================================================ FILE: apps/launcher/utils/textinputdialog.hpp ================================================ #ifndef TEXTINPUTDIALOG_HPP #define TEXTINPUTDIALOG_HPP #include #include "lineedit.hpp" class QDialogButtonBox; namespace Launcher { class TextInputDialog : public QDialog { Q_OBJECT public: explicit TextInputDialog(const QString& title, const QString &text, QWidget *parent = nullptr); ~TextInputDialog (); inline LineEdit *lineEdit() { return mLineEdit; } void setOkButtonEnabled(bool enabled); int exec() override; private: QDialogButtonBox *mButtonBox; LineEdit *mLineEdit; }; } #endif // TEXTINPUTDIALOG_HPP ================================================ FILE: apps/master/CMakeLists.txt ================================================ project(masterserver) #set(CMAKE_CXX_STANDARD 14) add_definitions(-std=gnu++14) include_directories("./") set(SOURCE_FILES main.cpp MasterServer.cpp MasterServer.hpp RestServer.cpp RestServer.hpp) add_executable(masterserver ${SOURCE_FILES}) target_link_libraries(masterserver ${RakNet_LIBRARY} components) option(BUILD_MASTER_TEST "build master server test program" OFF) if(BUILD_MASTER_TEST) add_executable(ServerTest ServerTest.cpp) target_link_libraries(ServerTest ${RakNet_LIBRARY} components) endif() if (UNIX) # Fix for not visible pthreads functions for linker with glibc 2.15 if(NOT APPLE) target_link_libraries(masterserver ${CMAKE_THREAD_LIBS_INIT}) if(BUILD_MASTER_TEST) target_link_libraries(ServerTest ${CMAKE_THREAD_LIBS_INIT}) endif() endif(NOT APPLE) endif(UNIX) if(WIN32) target_link_libraries(masterserver wsock32) endif(WIN32) ================================================ FILE: apps/master/MasterServer.cpp ================================================ #include #include #include #include #include "MasterServer.hpp" #include #include #include #include using namespace RakNet; using namespace std; using namespace mwmp; using namespace chrono; MasterServer::MasterServer(unsigned short maxConnections, unsigned short port) { peer = RakPeerInterface::GetInstance(); sockdescr = SocketDescriptor(port, 0); peer->Startup(maxConnections, &sockdescr, 1, 1000); peer->SetMaximumIncomingConnections(maxConnections); peer->SetIncomingPassword(TES3MP_MASTERSERVER_PASSW, (int) strlen(TES3MP_MASTERSERVER_PASSW)); run = false; } MasterServer::~MasterServer() { Stop(true); } using namespace chrono; void MasterServer::Thread() { unsigned char packetId = 0; auto startTime = chrono::steady_clock::now(); BitStream send; PacketMasterQuery pmq(peer); pmq.SetSendStream(&send); PacketMasterUpdate pmu(peer); pmu.SetSendStream(&send); PacketMasterAnnounce pma(peer); pma.SetSendStream(&send); while (run) { Packet *packet = peer->Receive(); auto now = steady_clock::now(); if (now - startTime >= 60s) { startTime = steady_clock::now(); for (auto it = servers.begin(); it != servers.end();) { if (it->second.lastUpdate + 60s <= now) servers.erase(it++); else ++it; } for(auto id = pendingACKs.begin(); id != pendingACKs.end();) { if(now - id->second >= 30s) { cout << "timeout: " << peer->GetSystemAddressFromGuid(id->first).ToString() << endl; peer->CloseConnection(id->first, true); id = pendingACKs.erase(id); } else ++id; } } if (packet == nullptr) RakSleep(10); else for (; packet; peer->DeallocatePacket(packet), packet = peer->Receive()) { BitStream data(packet->data, packet->length, false); data.Read(packetId); switch (packetId) { case ID_NEW_INCOMING_CONNECTION: cout << "New incoming connection: " << packet->systemAddress.ToString() << endl; break; case ID_DISCONNECTION_NOTIFICATION: cout << "Disconnected: " << packet->systemAddress.ToString() << endl; break; case ID_CONNECTION_LOST: cout << "Connection lost: " << packet->systemAddress.ToString() << endl; break; case ID_MASTER_QUERY: { pmq.SetServers(reinterpret_cast *>(&servers)); pmq.Send(packet->systemAddress); pendingACKs[packet->guid] = steady_clock::now(); cout << "Sent info about all " << servers.size() << " servers to " << packet->systemAddress.ToString() << endl; break; } case ID_MASTER_UPDATE: { SystemAddress addr; data.Read(addr); // update 1 server ServerIter it = servers.find(addr); if (it != servers.end()) { pair pairPtr(it->first, static_cast(it->second)); pmu.SetServer(&pairPtr); pmu.Send(packet->systemAddress); pendingACKs[packet->guid] = steady_clock::now(); cout << "Sent info about " << addr.ToString() << " to " << packet->systemAddress.ToString() << endl; } break; } case ID_MASTER_ANNOUNCE: { ServerIter iter = servers.find(packet->systemAddress); pma.SetReadStream(&data); SServer server; pma.SetServer(&server); pma.Read(); auto keepAliveFunc = [&]() { iter->second.lastUpdate = now; pma.SetFunc(PacketMasterAnnounce::FUNCTION_KEEP); pma.Send(packet->systemAddress); pendingACKs[packet->guid] = steady_clock::now(); }; if (iter != servers.end()) { if (pma.GetFunc() == PacketMasterAnnounce::FUNCTION_DELETE) { servers.erase(iter); cout << "Deleted"; pma.Send(packet->systemAddress); pendingACKs[packet->guid] = steady_clock::now(); } else if (pma.GetFunc() == PacketMasterAnnounce::FUNCTION_ANNOUNCE) { cout << "Updated"; iter->second = server; keepAliveFunc(); } else { cout << "Keeping alive"; keepAliveFunc(); } } else if (pma.GetFunc() == PacketMasterAnnounce::FUNCTION_ANNOUNCE) { cout << "Added"; iter = servers.insert({packet->systemAddress, server}).first; keepAliveFunc(); } else { cout << "Unknown"; pma.SetFunc(PacketMasterAnnounce::FUNCTION_DELETE); pma.Send(packet->systemAddress); pendingACKs[packet->guid] = steady_clock::now(); } cout << " server " << packet->systemAddress.ToString() << endl; break; } case ID_SND_RECEIPT_ACKED: uint32_t num; memcpy(&num, packet->data+1, 4); cout << "Packet with id " << num << " was delivered." << endl; pendingACKs.erase(packet->guid); peer->CloseConnection(packet->systemAddress, true); break; default: cout << "Wrong packet. id " << (unsigned) packet->data[0] << " packet length " << packet->length << " from " << packet->systemAddress.ToString() << endl; peer->CloseConnection(packet->systemAddress, true); } } } peer->Shutdown(1000); RakPeerInterface::DestroyInstance(peer); cout << "Server thread stopped" << endl; } void MasterServer::Start() { if (!run) { run = true; tMasterThread = thread(&MasterServer::Thread, this); cout << "Started" << endl; } } void MasterServer::Stop(bool wait) { if (run) { run = false; if (wait && tMasterThread.joinable()) tMasterThread.join(); } } bool MasterServer::isRunning() { return run; } void MasterServer::Wait() { if (run) { if (tMasterThread.joinable()) tMasterThread.join(); } } MasterServer::ServerMap *MasterServer::GetServers() { return &servers; } ================================================ FILE: apps/master/MasterServer.hpp ================================================ #ifndef NEWMASTERPROTO_MASTERSERVER_HPP #define NEWMASTERPROTO_MASTERSERVER_HPP #include #include #include #include class MasterServer { public: struct Ban { RakNet::SystemAddress sa; bool permanent; struct Date { } date; }; struct SServer : QueryData { std::chrono::steady_clock::time_point lastUpdate; }; typedef std::map ServerMap; //typedef ServerMap::const_iterator ServerCIter; typedef ServerMap::iterator ServerIter; MasterServer(unsigned short maxConnections, unsigned short port); ~MasterServer(); void Start(); void Stop(bool wait = false); bool isRunning(); void Wait(); ServerMap* GetServers(); private: void Thread(); private: std::thread tMasterThread; RakNet::RakPeerInterface* peer; RakNet::SocketDescriptor sockdescr; ServerMap servers; bool run; std::map pendingACKs; }; #endif //NEWMASTERPROTO_MASTERSERVER_HPP ================================================ FILE: apps/master/RestServer.cpp ================================================ #include "RestServer.hpp" #include #include using namespace std; using namespace chrono; using namespace boost::property_tree; static string response201 = "HTTP/1.1 201 Created\r\nContent-Length: 7\r\n\r\nCreated"; static string response202 = "HTTP/1.1 202 Accepted\r\nContent-Length: 8\r\n\r\nAccepted"; static string response400 = "HTTP/1.1 400 Bad Request\r\nContent-Length: 11\r\n\r\nbad request"; inline void ResponseStr(HttpServer::Response &response, string content, string type = "", string code = "200 OK") { response << "HTTP/1.1 " << code << "\r\n"; if (!type.empty()) response << "Content-Type: " << type <<"\r\n"; response << "Content-Length: " << content.length() << "\r\n\r\n" << content; } inline void ptreeToServer(boost::property_tree::ptree &pt, MasterServer::SServer &server) { server.SetName(pt.get("hostname").c_str()); server.SetGameMode(pt.get("modname").c_str()); server.SetVersion(pt.get("version").c_str()); server.SetPassword(pt.get("passw")); //server.query_port = pt.get("query_port"); server.SetPlayers(pt.get("players")); server.SetMaxPlayers(pt.get("max_players")); } inline void queryToStringStream(stringstream &ss, string addr, MasterServer::SServer &query) { ss <<"\"" << addr << "\":{"; ss << "\"modname\": \"" << query.GetGameMode() << "\"" << ", "; ss << "\"passw\": " << (query.GetPassword() ? "true" : "false") << ", "; ss << "\"hostname\": \"" << query.GetName() << "\"" << ", "; ss << "\"query_port\": " << 0 << ", "; ss << "\"last_update\": " << duration_cast(steady_clock::now() - query.lastUpdate).count() << ", "; ss << "\"players\": " << query.GetPlayers() << ", "; ss << "\"version\": \"" << query.GetVersion() << "\"" << ", "; ss << "\"max_players\": " << query.GetMaxPlayers(); ss << "}"; } RestServer::RestServer(unsigned short port, MasterServer::ServerMap *pMap) : serverMap(pMap) { httpServer.config.port = port; } void RestServer::start() { static const string ValidIpAddressRegex = "(?:[0-9]{1,3}\\.){3}[0-9]{1,3}"; static const string ValidPortRegex = "(?:[0-9]{1,4}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])$"; static const string ServersRegex = "^/api/servers(?:/(" + ValidIpAddressRegex + "\\:" + ValidPortRegex + "))?"; httpServer.resource[ServersRegex]["GET"] = [this](auto response, auto request) { if (request->path_match[1].length() > 0) { try { stringstream ss; ss << "{"; auto addr = request->path_match[1].str(); auto port = (unsigned short)stoi(&(addr[addr.find(':')+1])); queryToStringStream(ss, "server", serverMap->at(RakNet::SystemAddress(addr.c_str(), port))); ss << "}"; ResponseStr(*response, ss.str(), "application/json"); } catch(out_of_range e) { *response << response400; } } else { static string str; //if (updatedCache) { stringstream ss; ss << "{"; ss << "\"list servers\":{"; for (auto query = serverMap->begin(); query != serverMap->end(); query++) { queryToStringStream(ss, query->first.ToString(true, ':'), query->second); if (next(query) != serverMap->end()) ss << ", "; } ss << "}}"; ResponseStr(*response, ss.str(), "application/json"); updatedCache = false; } *response << str; } }; //Add query for < 0.6 servers httpServer.resource[ServersRegex]["POST"] = [this](auto response, auto request) { try { ptree pt; read_json(request->content, pt); MasterServer::SServer server; ptreeToServer(pt, server); unsigned short port = pt.get("port"); server.lastUpdate = steady_clock::now(); serverMap->insert({RakNet::SystemAddress(request->remote_endpoint_address.c_str(), port), server}); updatedCache = true; *response << response201; } catch (exception& e) { cout << e.what() << endl; *response << response400; } }; //Update query for < 0.6 servers httpServer.resource[ServersRegex]["PUT"] = [this](auto response, auto request) { auto addr = request->path_match[1].str(); auto port = (unsigned short)stoi(&(addr[addr.find(':')+1])); auto query = serverMap->find(RakNet::SystemAddress(request->remote_endpoint_address.c_str(), port)); if (query == serverMap->end()) { cout << request->remote_endpoint_address + ": Trying to update a non-existent server or without permissions." << endl; *response << response400; return; } if (request->content.size() != 0) { try { ptree pt; read_json(request->content, pt); ptreeToServer(pt, query->second); updatedCache = true; } catch(exception &e) { cout << e.what() << endl; *response << response400; } } query->second.lastUpdate = steady_clock::now(); *response << response202; }; httpServer.resource["/api/servers/info"]["GET"] = [this](auto response, auto /*request*/) { stringstream ss; ss << '{'; ss << "\"servers\": " << serverMap->size(); unsigned int players = 0; for (auto s : *serverMap) players += s.second.GetPlayers(); ss << ", \"players\": " << players; ss << "}"; ResponseStr(*response, ss.str(), "application/json"); }; httpServer.default_resource["GET"]=[](auto response, auto /*request*/) { *response << response400; }; httpServer.start(); } void RestServer::cacheUpdated() { updatedCache = true; } void RestServer::stop() { httpServer.stop(); } ================================================ FILE: apps/master/RestServer.hpp ================================================ #ifndef NEWRESTAPI_RESTSERVER_HPP #define NEWRESTAPI_RESTSERVER_HPP #include #include #include "MasterServer.hpp" #include "SimpleWeb/http_server.hpp" typedef SimpleWeb::Server HttpServer; class RestServer { public: RestServer(unsigned short port, MasterServer::ServerMap *pMap); void start(); void stop(); void cacheUpdated(); private: HttpServer httpServer; MasterServer::ServerMap *serverMap; bool updatedCache = true; }; #endif //NEWRESTAPI_RESTSERVER_HPP ================================================ FILE: apps/master/ServerTest.cpp ================================================ #include #include #include #include #include #include #include #include #include #include #include using namespace std; using namespace RakNet; using namespace mwmp; int main() { cout << "Server test" << endl; SystemAddress masterAddr("127.0.0.1", 25560); RakPeerInterface *peer = RakNet::RakPeerInterface::GetInstance(); RakNet::SocketDescriptor sd(25565, 0); peer->Startup(8, &sd, 1); ConnectionAttemptResult result = peer->Connect(masterAddr.ToString(false), masterAddr.GetPort(), "pass", (int)(strlen("pass")), 0, 0, 5, 500); assert(result == RakNet::CONNECTION_ATTEMPT_STARTED); char message[2048]; BitStream send; PacketMasterQuery pmq(peer); pmq.SetSendStream(&send); PacketMasterAnnounce pma(peer); pma.SetSendStream(&send); while (true) { RakSleep(30); if (kbhit()) { Gets(message, sizeof(message)); if (strcmp(message, "quit") == 0) { puts("Quitting."); break; } else if (strcmp(message, "send") == 0) { puts("Sending data about server"); QueryData server; server.SetName("Super Server"); server.SetPlayers(0); server.SetMaxPlayers(0); pma.SetServer(&server); pma.SetFunc(PacketMasterAnnounce::FUNCTION_ANNOUNCE); pma.Send(masterAddr); } else if (strcmp(message, "get") == 0) { puts("Request query info"); send.Reset(); send.Write((unsigned char) (ID_MASTER_QUERY)); peer->Send(&send, HIGH_PRIORITY, RELIABLE_ORDERED, CHANNEL_MASTER, masterAddr, false); } else if (strcmp(message, "getme") == 0) { send.Reset(); send.Write((unsigned char) (ID_MASTER_UPDATE)); send.Write(SystemAddress("127.0.0.1", 25565)); peer->Send(&send, HIGH_PRIORITY, RELIABLE_ORDERED, CHANNEL_MASTER, masterAddr, false); } else if (strcmp(message, "status") == 0) { cout << (peer->GetConnectionState(masterAddr) == IS_CONNECTED ? "Connected" : "Not connected") << endl; } else if (strcmp(message, "keep") == 0) { cout << "Sending keep alive" << endl; pma.SetFunc(PacketMasterAnnounce::FUNCTION_KEEP); pma.Send(masterAddr); } } for (RakNet::Packet *packet = peer->Receive(); packet; peer->DeallocatePacket(packet), packet = peer->Receive()) { BitStream data(packet->data, packet->length, false); unsigned char packetID; data.Read(packetID); switch (packetID) { case ID_DISCONNECTION_NOTIFICATION: // Connection lost normally printf("ID_DISCONNECTION_NOTIFICATION\n"); break; case ID_ALREADY_CONNECTED: // Connection lost normally printf("ID_ALREADY_CONNECTED with guid %lu\n", packet->guid.g); break; case ID_INCOMPATIBLE_PROTOCOL_VERSION: printf("ID_INCOMPATIBLE_PROTOCOL_VERSION\n"); break; case ID_REMOTE_DISCONNECTION_NOTIFICATION: // Server telling the clients of another client disconnecting gracefully. You can manually broadcast this in a peer to peer enviroment if you want. printf("ID_REMOTE_DISCONNECTION_NOTIFICATION\n"); break; case ID_REMOTE_CONNECTION_LOST: // Server telling the clients of another client disconnecting forcefully. You can manually broadcast this in a peer to peer enviroment if you want. printf("ID_REMOTE_CONNECTION_LOST\n"); break; case ID_REMOTE_NEW_INCOMING_CONNECTION: // Server telling the clients of another client connecting. You can manually broadcast this in a peer to peer enviroment if you want. printf("ID_REMOTE_NEW_INCOMING_CONNECTION\n"); break; case ID_CONNECTION_BANNED: // Banned from this server printf("We are banned from this server.\n"); break; case ID_CONNECTION_ATTEMPT_FAILED: printf("Connection attempt failed\n"); break; case ID_NO_FREE_INCOMING_CONNECTIONS: // Sorry, the server is full. I don't do anything here but // A real app should tell the user printf("ID_NO_FREE_INCOMING_CONNECTIONS\n"); break; case ID_INVALID_PASSWORD: printf("ID_INVALID_PASSWORD\n"); break; case ID_CONNECTION_LOST: // Couldn't deliver a reliable packet - i.e. the other system was abnormally // terminated printf("ID_CONNECTION_LOST\n"); return 0; break; case ID_CONNECTION_REQUEST_ACCEPTED: // This tells the client they have connected printf("ID_CONNECTION_REQUEST_ACCEPTED to %s with GUID %s\n", packet->systemAddress.ToString(true), packet->guid.ToString()); printf("My external address is %s\n", peer->GetExternalID(packet->systemAddress).ToString(true)); break; case ID_MASTER_QUERY: { map servers; pmq.SetReadStream(&data); pmq.SetServers(&servers); pmq.Read(); cout << "Received query data about " << servers.size() << " servers" << endl; for (auto serv : servers) cout << serv.second.GetName() << endl; break; } case ID_MASTER_UPDATE: { pair serverPair; PacketMasterUpdate pmu(peer); pmu.SetReadStream(&data); pmu.SetServer(&serverPair); pmu.Read(); cout << "Received info about " << serverPair.first.ToString() << endl; cout << serverPair.second.GetName() << endl; break; } default: cout << "Wrong packet" << endl; } } } peer->Shutdown(1000); RakPeerInterface::DestroyInstance(peer); } ================================================ FILE: apps/master/SimpleWeb/base_server.hpp ================================================ #ifndef BASE_SERVER_HPP #define BASE_SERVER_HPP #include #include #include #include #include #include #include #include #include #include #ifndef CASE_INSENSITIVE_EQUALS_AND_HASH #define CASE_INSENSITIVE_EQUALS_AND_HASH //Based on http://www.boost.org/doc/libs/1_60_0/doc/html/unordered/hash_equality.html struct case_insensitive_equals { bool operator()(const std::string &key1, const std::string &key2) const { return boost::algorithm::iequals(key1, key2); } }; struct case_insensitive_hash { size_t operator()(const std::string &key) const { std::size_t seed = 0; for (auto &c: key) boost::hash_combine(seed, std::tolower(c)); return seed; } }; #endif namespace SimpleWeb { template class Server; template class ServerBase { public: virtual ~ServerBase() {} class Response : public std::ostream { friend class ServerBase; boost::asio::streambuf streambuf; std::shared_ptr socket; Response(const std::shared_ptr &socket) : std::ostream(&streambuf), socket(socket) {} public: size_t size() { return streambuf.size(); } /// If true, force server to close the connection after the response have been sent. /// /// This is useful when implementing a HTTP/1.0-server sending content /// without specifying the content length. bool close_connection_after_response = false; }; class Content : public std::istream { friend class ServerBase; public: size_t size() { return streambuf.size(); } std::string string() { std::stringstream ss; ss << rdbuf(); return ss.str(); } private: boost::asio::streambuf &streambuf; Content(boost::asio::streambuf &streambuf) : std::istream(&streambuf), streambuf(streambuf) {} }; class Request { friend class ServerBase; friend class Server; public: std::string method, path, http_version; Content content; std::unordered_multimap header; std::smatch path_match; std::string remote_endpoint_address; unsigned short remote_endpoint_port; private: Request(const socket_type &socket) : content(streambuf) { try { remote_endpoint_address = socket.lowest_layer().remote_endpoint().address().to_string(); remote_endpoint_port = socket.lowest_layer().remote_endpoint().port(); } catch (...) {} } boost::asio::streambuf streambuf; }; class Config { friend class ServerBase; Config(unsigned short port) : port(port) {} public: /// Port number to use. Defaults to 80 for HTTP and 443 for HTTPS. unsigned short port; /// Number of threads that the server will use when start() is called. Defaults to 1 thread. size_t thread_pool_size = 1; /// Timeout on request handling. Defaults to 5 seconds. size_t timeout_request = 5; /// Timeout on content handling. Defaults to 300 seconds. size_t timeout_content = 300; /// IPv4 address in dotted decimal form or IPv6 address in hexadecimal notation. /// If empty, the address will be any address. std::string address; /// Set to false to avoid binding the socket to an address that is already in use. Defaults to true. bool reuse_address = true; }; ///Set before calling start(). Config config; private: class regex_orderable : public std::regex { std::string str; public: regex_orderable(const char *regex_cstr) : std::regex(regex_cstr), str(regex_cstr) {} regex_orderable(const std::string ®ex_str) : std::regex(regex_str), str(regex_str) {} bool operator<(const regex_orderable &rhs) const { return str < rhs.str; } }; public: /// Warning: do not add or remove resources after start() is called std::map::Response>, std::shared_ptr::Request>)>>> resource; std::map::Response>, std::shared_ptr::Request>)>> default_resource; std::function< void(std::shared_ptr::Request>, const boost::system::error_code &)> on_error; std::function socket, std::shared_ptr::Request>)> on_upgrade; virtual void start() { if (!io_service) io_service = std::make_shared(); if (io_service->stopped()) io_service->reset(); boost::asio::ip::tcp::endpoint endpoint; if (config.address.size() > 0) endpoint = boost::asio::ip::tcp::endpoint(boost::asio::ip::address::from_string(config.address), config.port); else endpoint = boost::asio::ip::tcp::endpoint(boost::asio::ip::tcp::v4(), config.port); if (!acceptor) acceptor = std::unique_ptr( new boost::asio::ip::tcp::acceptor(*io_service)); acceptor->open(endpoint.protocol()); acceptor->set_option(boost::asio::socket_base::reuse_address(config.reuse_address)); acceptor->bind(endpoint); acceptor->listen(); accept(); //If thread_pool_size>1, start m_io_service.run() in (thread_pool_size-1) threads for thread-pooling threads.clear(); for (size_t c = 1; c < config.thread_pool_size; c++) { threads.emplace_back([this]() { io_service->run(); }); } //Main thread if (config.thread_pool_size > 0) io_service->run(); //Wait for the rest of the threads, if any, to finish as well for (auto &t: threads) { t.join(); } } void stop() { acceptor->close(); if (config.thread_pool_size > 0) io_service->stop(); } ///Use this function if you need to recursively send parts of a longer message void send(const std::shared_ptr &response, const std::function &callback = nullptr) const { boost::asio::async_write(*response->socket, response->streambuf, [this, response, callback] (const boost::system::error_code &ec, size_t /*bytes_transferred*/) { if (callback) callback(ec); }); } /// If you have your own boost::asio::io_service, store its pointer here before running start(). /// You might also want to set config.thread_pool_size to 0. std::shared_ptr io_service; protected: std::unique_ptr acceptor; std::vector threads; ServerBase(unsigned short port) : config(port) {} virtual void accept()=0; std::shared_ptr get_timeout_timer(const std::shared_ptr &socket, long seconds) { if (seconds == 0) return nullptr; auto timer = std::make_shared(*io_service); timer->expires_from_now(boost::posix_time::seconds(seconds)); timer->async_wait([socket](const boost::system::error_code &ec) { if (!ec) { boost::system::error_code ec; socket->lowest_layer().shutdown(boost::asio::ip::tcp::socket::shutdown_both, ec); socket->lowest_layer().close(); } }); return timer; } void read_request_and_content(const std::shared_ptr &socket) { //Create new streambuf (Request::streambuf) for async_read_until() //shared_ptr is used to pass temporary objects to the asynchronous functions std::shared_ptr request(new Request(*socket)); //Set timeout on the following boost::asio::async-read or write function auto timer = this->get_timeout_timer(socket, config.timeout_request); boost::asio::async_read_until(*socket, request->streambuf, "\r\n\r\n", [this, socket, request, timer] (const boost::system::error_code &ec, size_t bytes_transferred) { if (timer) timer->cancel(); if (!ec) { //request->streambuf.size() is not necessarily the same as bytes_transferred, from Boost-docs: //"After a successful async_read_until operation, the streambuf may contain additional data beyond the delimiter" //The chosen solution is to extract lines from the stream directly when parsing the header. What is left of the //streambuf (maybe some bytes of the content) is appended to in the async_read-function below (for retrieving content). size_t num_additional_bytes = request->streambuf.size() - bytes_transferred; if (!this->parse_request(request)) return; //If content, read that as well auto it = request->header.find("Content-Length"); if (it != request->header.end()) { unsigned long long content_length; try { content_length = stoull(it->second); } catch (const std::exception &e) { if (on_error) on_error(request, boost::system::error_code( boost::system::errc::protocol_error, boost::system::generic_category())); return; } if (content_length > num_additional_bytes) { //Set timeout on the following boost::asio::async-read or write function auto timer = this->get_timeout_timer(socket, config.timeout_content); boost::asio::async_read(*socket, request->streambuf, boost::asio::transfer_exactly( content_length - num_additional_bytes), [this, socket, request, timer] (const boost::system::error_code &ec, size_t /*bytes_transferred*/) { if (timer) timer->cancel(); if (!ec) this->find_resource(socket, request); else if (on_error) on_error(request, ec); }); } else this->find_resource(socket, request); } else this->find_resource(socket, request); } else if (on_error) on_error(request, ec); }); } bool parse_request(const std::shared_ptr &request) const { std::string line; getline(request->content, line); size_t method_end; if ((method_end = line.find(' ')) != std::string::npos) { size_t path_end; if ((path_end = line.find(' ', method_end + 1)) != std::string::npos) { request->method = line.substr(0, method_end); request->path = line.substr(method_end + 1, path_end - method_end - 1); size_t protocol_end; if ((protocol_end = line.find('/', path_end + 1)) != std::string::npos) { if (line.compare(path_end + 1, protocol_end - path_end - 1, "HTTP") != 0) return false; request->http_version = line.substr(protocol_end + 1, line.size() - protocol_end - 2); } else return false; getline(request->content, line); size_t param_end; while ((param_end = line.find(':')) != std::string::npos) { size_t value_start = param_end + 1; if ((value_start) < line.size()) { if (line[value_start] == ' ') value_start++; if (value_start < line.size()) request->header.emplace(line.substr(0, param_end), line.substr(value_start, line.size() - value_start - 1)); } getline(request->content, line); } } else return false; } else return false; return true; } void find_resource(const std::shared_ptr &socket, const std::shared_ptr &request) { //Upgrade connection if (on_upgrade) { auto it = request->header.find("Upgrade"); if (it != request->header.end()) { on_upgrade(socket, request); return; } } //Find path- and method-match, and call write_response for (auto ®ex_method: resource) { auto it = regex_method.second.find(request->method); if (it != regex_method.second.end()) { std::smatch sm_res; if (std::regex_match(request->path, sm_res, regex_method.first)) { request->path_match = std::move(sm_res); write_response(socket, request, it->second); return; } } } auto it = default_resource.find(request->method); if (it != default_resource.end()) { write_response(socket, request, it->second); } } void write_response(const std::shared_ptr &socket, const std::shared_ptr &request, std::function::Response>, std::shared_ptr< typename ServerBase::Request>)> &resource_function) { //Set timeout on the following boost::asio::async-read or write function auto timer = this->get_timeout_timer(socket, config.timeout_content); auto response = std::shared_ptr(new Response(socket), [this, request, timer] (Response *response_ptr) { auto response = std::shared_ptr(response_ptr); this->send(response, [this, response, request, timer]( const boost::system::error_code &ec) { if (timer) timer->cancel(); if (!ec) { if (response->close_connection_after_response) return; auto range = request->header.equal_range( "Connection"); for (auto it = range.first; it != range.second; it++) { if (boost::iequals(it->second, "close")) { return; } else if (boost::iequals(it->second, "keep-alive")) { this->read_request_and_content( response->socket); return; } } if (request->http_version >= "1.1") this->read_request_and_content(response->socket); } else if (on_error) on_error(request, ec); }); }); try { resource_function(response, request); } catch (const std::exception &e) { if (on_error) on_error(request, boost::system::error_code(boost::system::errc::operation_canceled, boost::system::generic_category())); return; } } }; } #endif //BASE_SERVER_HPP ================================================ FILE: apps/master/SimpleWeb/http_server.hpp ================================================ /* * https://github.com/eidheim/Simple-Web-Server/ * * The MIT License (MIT) * Copyright (c) 2014-2016 Ole Christian Eidheim */ #ifndef SERVER_HTTP_HPP #define SERVER_HTTP_HPP #include "base_server.hpp" namespace SimpleWeb { template class Server : public ServerBase {}; typedef boost::asio::ip::tcp::socket HTTP; template<> class Server : public ServerBase { public: Server() : ServerBase::ServerBase(80) {} protected: virtual void accept() { //Create new socket for this connection //Shared_ptr is used to pass temporary objects to the asynchronous functions auto socket = std::make_shared(*io_service); acceptor->async_accept(*socket, [this, socket](const boost::system::error_code &ec) { //Immediately start accepting a new connection (if io_service hasn't been stopped) if (ec != boost::asio::error::operation_aborted) accept(); if (!ec) { boost::asio::ip::tcp::no_delay option(true); socket->set_option(option); this->read_request_and_content(socket); } else if (on_error) on_error(std::shared_ptr(new Request(*socket)), ec); }); } }; } #endif //SERVER_HTTP_HPP ================================================ FILE: apps/master/SimpleWeb/https_server.hpp ================================================ #ifndef HTTPS_SERVER_HPP #define HTTPS_SERVER_HPP #include "base_server.hpp" #include #include #include namespace SimpleWeb { typedef boost::asio::ssl::stream HTTPS; template<> class Server : public ServerBase { std::string session_id_context; bool set_session_id_context = false; public: Server(const std::string &cert_file, const std::string &private_key_file, const std::string &verify_file = std::string()) : ServerBase::ServerBase(443), context(boost::asio::ssl::context::tlsv12) { context.use_certificate_chain_file(cert_file); context.use_private_key_file(private_key_file, boost::asio::ssl::context::pem); if (verify_file.size() > 0) { context.load_verify_file(verify_file); context.set_verify_mode(boost::asio::ssl::verify_peer | boost::asio::ssl::verify_fail_if_no_peer_cert | boost::asio::ssl::verify_client_once); set_session_id_context = true; } } void start() { if (set_session_id_context) { // Creating session_id_context from address:port but reversed due to small SSL_MAX_SSL_SESSION_ID_LENGTH session_id_context = std::to_string(config.port) + ':'; session_id_context.append(config.address.rbegin(), config.address.rend()); SSL_CTX_set_session_id_context(context.native_handle(), reinterpret_cast(session_id_context.data()), std::min(session_id_context.size(), SSL_MAX_SSL_SESSION_ID_LENGTH)); } ServerBase::start(); } protected: boost::asio::ssl::context context; virtual void accept() { //Create new socket for this connection //Shared_ptr is used to pass temporary objects to the asynchronous functions auto socket = std::make_shared(*io_service, context); acceptor->async_accept((*socket).lowest_layer(), [this, socket](const boost::system::error_code &ec) { //Immediately start accepting a new connection (if io_service hasn't been stopped) if (ec != boost::asio::error::operation_aborted) accept(); if (!ec) { boost::asio::ip::tcp::no_delay option(true); socket->lowest_layer().set_option(option); //Set timeout on the following boost::asio::ssl::stream::async_handshake auto timer = get_timeout_timer(socket, config.timeout_request); socket->async_handshake(boost::asio::ssl::stream_base::server, [this, socket, timer] (const boost::system::error_code &ec) { if (timer) timer->cancel(); if (!ec) read_request_and_content(socket); else if (on_error) on_error(std::shared_ptr(new Request(*socket)), ec); }); } else if (on_error) on_error(std::shared_ptr(new Request(*socket)), ec); }); } }; } #endif //HTTPS_SERVER_HPP ================================================ FILE: apps/master/main.cpp ================================================ #include #include #include #include "MasterServer.hpp" #include "RestServer.hpp" using namespace RakNet; using namespace std; unique_ptr restServer; unique_ptr masterServer; bool run = true; int main() { masterServer.reset(new MasterServer(2000, 25560)); restServer.reset(new RestServer(8080, masterServer->GetServers())); auto onExit = [](int /*sig*/){ restServer->stop(); masterServer->Stop(false); masterServer->Wait(); run = false; }; signal(SIGINT, onExit); signal(SIGTERM, onExit); masterServer->Start(); thread server_thread([]() { restServer->start(); }); server_thread.join(); masterServer->Wait(); return 0; } ================================================ FILE: apps/mwiniimporter/CMakeLists.txt ================================================ set(MWINIIMPORT main.cpp importer.cpp ) set(MWINIIMPORT_HEADER importer.hpp ) source_group(launcher FILES ${MWINIIMPORT} ${MWINIIMPORT_HEADER}) openmw_add_executable(openmw-iniimporter ${MWINIIMPORT} ) target_link_libraries(openmw-iniimporter ${Boost_PROGRAM_OPTIONS_LIBRARY} ${Boost_FILESYSTEM_LIBRARY} components ) if (WIN32) target_link_libraries(openmw-iniimporter ${Boost_LOCALE_LIBRARY}) INSTALL(TARGETS openmw-iniimporter RUNTIME DESTINATION ".") endif(WIN32) if (MINGW) set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -municode") endif() if (BUILD_WITH_CODE_COVERAGE) add_definitions (--coverage) target_link_libraries(openmw-iniimporter gcov) endif() ================================================ FILE: apps/mwiniimporter/importer.cpp ================================================ #include "importer.hpp" #include #include #include #include #include #include #include namespace bfs = boost::filesystem; MwIniImporter::MwIniImporter() : mVerbose(false) , mEncoding(ToUTF8::WINDOWS_1250) { const char *map[][2] = { { "no-sound", "General:Disable Audio" }, { 0, 0 } }; const char *fallback[] = { // light "LightAttenuation:UseConstant", "LightAttenuation:ConstantValue", "LightAttenuation:UseLinear", "LightAttenuation:LinearMethod", "LightAttenuation:LinearValue", "LightAttenuation:LinearRadiusMult", "LightAttenuation:UseQuadratic", "LightAttenuation:QuadraticMethod", "LightAttenuation:QuadraticValue", "LightAttenuation:QuadraticRadiusMult", "LightAttenuation:OutQuadInLin", // inventory "Inventory:DirectionalDiffuseR", "Inventory:DirectionalDiffuseG", "Inventory:DirectionalDiffuseB", "Inventory:DirectionalAmbientR", "Inventory:DirectionalAmbientG", "Inventory:DirectionalAmbientB", "Inventory:DirectionalRotationX", "Inventory:DirectionalRotationY", "Inventory:UniformScaling", // map "Map:Travel Siltstrider Red", "Map:Travel Siltstrider Green", "Map:Travel Siltstrider Blue", "Map:Travel Boat Red", "Map:Travel Boat Green", "Map:Travel Boat Blue", "Map:Travel Magic Red", "Map:Travel Magic Green", "Map:Travel Magic Blue", "Map:Show Travel Lines", // water "Water:Map Alpha", "Water:World Alpha", "Water:SurfaceTextureSize", "Water:SurfaceTileCount", "Water:SurfaceFPS", "Water:SurfaceTexture", "Water:SurfaceFrameCount", "Water:TileTextureDivisor", "Water:RippleTexture", "Water:RippleFrameCount", "Water:RippleLifetime", "Water:MaxNumberRipples", "Water:RippleScale", "Water:RippleRotSpeed", "Water:RippleAlphas", "Water:PSWaterReflectTerrain", "Water:PSWaterReflectUpdate", "Water:NearWaterRadius", "Water:NearWaterPoints", "Water:NearWaterUnderwaterFreq", "Water:NearWaterUnderwaterVolume", "Water:NearWaterIndoorTolerance", "Water:NearWaterOutdoorTolerance", "Water:NearWaterIndoorID", "Water:NearWaterOutdoorID", "Water:UnderwaterSunriseFog", "Water:UnderwaterDayFog", "Water:UnderwaterSunsetFog", "Water:UnderwaterNightFog", "Water:UnderwaterIndoorFog", "Water:UnderwaterColor", "Water:UnderwaterColorWeight", // pixelwater "PixelWater:SurfaceFPS", "PixelWater:TileCount", "PixelWater:Resolution", // fonts "Fonts:Font 0", "Fonts:Font 1", "Fonts:Font 2", // UI colors "FontColor:color_normal", "FontColor:color_normal_over", "FontColor:color_normal_pressed", "FontColor:color_active", "FontColor:color_active_over", "FontColor:color_active_pressed", "FontColor:color_disabled", "FontColor:color_disabled_over", "FontColor:color_disabled_pressed", "FontColor:color_link", "FontColor:color_link_over", "FontColor:color_link_pressed", "FontColor:color_journal_link", "FontColor:color_journal_link_over", "FontColor:color_journal_link_pressed", "FontColor:color_journal_topic", "FontColor:color_journal_topic_over", "FontColor:color_journal_topic_pressed", "FontColor:color_answer", "FontColor:color_answer_over", "FontColor:color_answer_pressed", "FontColor:color_header", "FontColor:color_notify", "FontColor:color_big_normal", "FontColor:color_big_normal_over", "FontColor:color_big_normal_pressed", "FontColor:color_big_link", "FontColor:color_big_link_over", "FontColor:color_big_link_pressed", "FontColor:color_big_answer", "FontColor:color_big_answer_over", "FontColor:color_big_answer_pressed", "FontColor:color_big_header", "FontColor:color_big_notify", "FontColor:color_background", "FontColor:color_focus", "FontColor:color_health", "FontColor:color_magic", "FontColor:color_fatigue", "FontColor:color_misc", "FontColor:color_weapon_fill", "FontColor:color_magic_fill", "FontColor:color_positive", "FontColor:color_negative", "FontColor:color_count", // level up messages "Level Up:Level2", "Level Up:Level3", "Level Up:Level4", "Level Up:Level5", "Level Up:Level6", "Level Up:Level7", "Level Up:Level8", "Level Up:Level9", "Level Up:Level10", "Level Up:Level11", "Level Up:Level12", "Level Up:Level13", "Level Up:Level14", "Level Up:Level15", "Level Up:Level16", "Level Up:Level17", "Level Up:Level18", "Level Up:Level19", "Level Up:Level20", "Level Up:Default", // character creation multiple choice test "Question 1:Question", "Question 1:AnswerOne", "Question 1:AnswerTwo", "Question 1:AnswerThree", "Question 1:Sound", "Question 2:Question", "Question 2:AnswerOne", "Question 2:AnswerTwo", "Question 2:AnswerThree", "Question 2:Sound", "Question 3:Question", "Question 3:AnswerOne", "Question 3:AnswerTwo", "Question 3:AnswerThree", "Question 3:Sound", "Question 4:Question", "Question 4:AnswerOne", "Question 4:AnswerTwo", "Question 4:AnswerThree", "Question 4:Sound", "Question 5:Question", "Question 5:AnswerOne", "Question 5:AnswerTwo", "Question 5:AnswerThree", "Question 5:Sound", "Question 6:Question", "Question 6:AnswerOne", "Question 6:AnswerTwo", "Question 6:AnswerThree", "Question 6:Sound", "Question 7:Question", "Question 7:AnswerOne", "Question 7:AnswerTwo", "Question 7:AnswerThree", "Question 7:Sound", "Question 8:Question", "Question 8:AnswerOne", "Question 8:AnswerTwo", "Question 8:AnswerThree", "Question 8:Sound", "Question 9:Question", "Question 9:AnswerOne", "Question 9:AnswerTwo", "Question 9:AnswerThree", "Question 9:Sound", "Question 10:Question", "Question 10:AnswerOne", "Question 10:AnswerTwo", "Question 10:AnswerThree", "Question 10:Sound", // blood textures and models "Blood:Model 0", "Blood:Model 1", "Blood:Model 2", "Blood:Texture 0", "Blood:Texture 1", "Blood:Texture 2", "Blood:Texture 3", "Blood:Texture 4", "Blood:Texture 5", "Blood:Texture 6", "Blood:Texture 7", "Blood:Texture Name 0", "Blood:Texture Name 1", "Blood:Texture Name 2", "Blood:Texture Name 3", "Blood:Texture Name 4", "Blood:Texture Name 5", "Blood:Texture Name 6", "Blood:Texture Name 7", // movies "Movies:Company Logo", "Movies:Morrowind Logo", "Movies:New Game", "Movies:Loading", "Movies:Options Menu", // weather related values "Weather Thunderstorm:Thunder Sound ID 0", "Weather Thunderstorm:Thunder Sound ID 1", "Weather Thunderstorm:Thunder Sound ID 2", "Weather Thunderstorm:Thunder Sound ID 3", "Weather:Sunrise Time", "Weather:Sunset Time", "Weather:Sunrise Duration", "Weather:Sunset Duration", "Weather:Hours Between Weather Changes", // AKA weather update time "Weather Thunderstorm:Thunder Frequency", "Weather Thunderstorm:Thunder Threshold", "Weather:EnvReduceColor", "Weather:LerpCloseColor", "Weather:BumpFadeColor", "Weather:AlphaReduce", "Weather:Minimum Time Between Environmental Sounds", "Weather:Maximum Time Between Environmental Sounds", "Weather:Sun Glare Fader Max", "Weather:Sun Glare Fader Angle Max", "Weather:Sun Glare Fader Color", "Weather:Timescale Clouds", "Weather:Precip Gravity", "Weather:Rain Ripples", "Weather:Rain Ripple Radius", "Weather:Rain Ripples Per Drop", "Weather:Rain Ripple Scale", "Weather:Rain Ripple Speed", "Weather:Fog Depth Change Speed", "Weather:Sky Pre-Sunrise Time", "Weather:Sky Post-Sunrise Time", "Weather:Sky Pre-Sunset Time", "Weather:Sky Post-Sunset Time", "Weather:Ambient Pre-Sunrise Time", "Weather:Ambient Post-Sunrise Time", "Weather:Ambient Pre-Sunset Time", "Weather:Ambient Post-Sunset Time", "Weather:Fog Pre-Sunrise Time", "Weather:Fog Post-Sunrise Time", "Weather:Fog Pre-Sunset Time", "Weather:Fog Post-Sunset Time", "Weather:Sun Pre-Sunrise Time", "Weather:Sun Post-Sunrise Time", "Weather:Sun Pre-Sunset Time", "Weather:Sun Post-Sunset Time", "Weather:Stars Post-Sunset Start", "Weather:Stars Pre-Sunrise Finish", "Weather:Stars Fading Duration", "Weather:Snow Ripples", "Weather:Snow Ripple Radius", "Weather:Snow Ripples Per Flake", "Weather:Snow Ripple Scale", "Weather:Snow Ripple Speed", "Weather:Snow Gravity Scale", "Weather:Snow High Kill", "Weather:Snow Low Kill", "Weather Clear:Cloud Texture", "Weather Clear:Clouds Maximum Percent", "Weather Clear:Transition Delta", "Weather Clear:Sky Sunrise Color", "Weather Clear:Sky Day Color", "Weather Clear:Sky Sunset Color", "Weather Clear:Sky Night Color", "Weather Clear:Fog Sunrise Color", "Weather Clear:Fog Day Color", "Weather Clear:Fog Sunset Color", "Weather Clear:Fog Night Color", "Weather Clear:Ambient Sunrise Color", "Weather Clear:Ambient Day Color", "Weather Clear:Ambient Sunset Color", "Weather Clear:Ambient Night Color", "Weather Clear:Sun Sunrise Color", "Weather Clear:Sun Day Color", "Weather Clear:Sun Sunset Color", "Weather Clear:Sun Night Color", "Weather Clear:Sun Disc Sunset Color", "Weather Clear:Land Fog Day Depth", "Weather Clear:Land Fog Night Depth", "Weather Clear:Wind Speed", "Weather Clear:Cloud Speed", "Weather Clear:Glare View", "Weather Clear:Ambient Loop Sound ID", "Weather Cloudy:Cloud Texture", "Weather Cloudy:Clouds Maximum Percent", "Weather Cloudy:Transition Delta", "Weather Cloudy:Sky Sunrise Color", "Weather Cloudy:Sky Day Color", "Weather Cloudy:Sky Sunset Color", "Weather Cloudy:Sky Night Color", "Weather Cloudy:Fog Sunrise Color", "Weather Cloudy:Fog Day Color", "Weather Cloudy:Fog Sunset Color", "Weather Cloudy:Fog Night Color", "Weather Cloudy:Ambient Sunrise Color", "Weather Cloudy:Ambient Day Color", "Weather Cloudy:Ambient Sunset Color", "Weather Cloudy:Ambient Night Color", "Weather Cloudy:Sun Sunrise Color", "Weather Cloudy:Sun Day Color", "Weather Cloudy:Sun Sunset Color", "Weather Cloudy:Sun Night Color", "Weather Cloudy:Sun Disc Sunset Color", "Weather Cloudy:Land Fog Day Depth", "Weather Cloudy:Land Fog Night Depth", "Weather Cloudy:Wind Speed", "Weather Cloudy:Cloud Speed", "Weather Cloudy:Glare View", "Weather Cloudy:Ambient Loop Sound ID", "Weather Foggy:Cloud Texture", "Weather Foggy:Clouds Maximum Percent", "Weather Foggy:Transition Delta", "Weather Foggy:Sky Sunrise Color", "Weather Foggy:Sky Day Color", "Weather Foggy:Sky Sunset Color", "Weather Foggy:Sky Night Color", "Weather Foggy:Fog Sunrise Color", "Weather Foggy:Fog Day Color", "Weather Foggy:Fog Sunset Color", "Weather Foggy:Fog Night Color", "Weather Foggy:Ambient Sunrise Color", "Weather Foggy:Ambient Day Color", "Weather Foggy:Ambient Sunset Color", "Weather Foggy:Ambient Night Color", "Weather Foggy:Sun Sunrise Color", "Weather Foggy:Sun Day Color", "Weather Foggy:Sun Sunset Color", "Weather Foggy:Sun Night Color", "Weather Foggy:Sun Disc Sunset Color", "Weather Foggy:Land Fog Day Depth", "Weather Foggy:Land Fog Night Depth", "Weather Foggy:Wind Speed", "Weather Foggy:Cloud Speed", "Weather Foggy:Glare View", "Weather Foggy:Ambient Loop Sound ID", "Weather Thunderstorm:Cloud Texture", "Weather Thunderstorm:Clouds Maximum Percent", "Weather Thunderstorm:Transition Delta", "Weather Thunderstorm:Sky Sunrise Color", "Weather Thunderstorm:Sky Day Color", "Weather Thunderstorm:Sky Sunset Color", "Weather Thunderstorm:Sky Night Color", "Weather Thunderstorm:Fog Sunrise Color", "Weather Thunderstorm:Fog Day Color", "Weather Thunderstorm:Fog Sunset Color", "Weather Thunderstorm:Fog Night Color", "Weather Thunderstorm:Ambient Sunrise Color", "Weather Thunderstorm:Ambient Day Color", "Weather Thunderstorm:Ambient Sunset Color", "Weather Thunderstorm:Ambient Night Color", "Weather Thunderstorm:Sun Sunrise Color", "Weather Thunderstorm:Sun Day Color", "Weather Thunderstorm:Sun Sunset Color", "Weather Thunderstorm:Sun Night Color", "Weather Thunderstorm:Sun Disc Sunset Color", "Weather Thunderstorm:Land Fog Day Depth", "Weather Thunderstorm:Land Fog Night Depth", "Weather Thunderstorm:Wind Speed", "Weather Thunderstorm:Cloud Speed", "Weather Thunderstorm:Glare View", "Weather Thunderstorm:Rain Loop Sound ID", "Weather Thunderstorm:Using Precip", "Weather Thunderstorm:Rain Diameter", "Weather Thunderstorm:Rain Height Min", "Weather Thunderstorm:Rain Height Max", "Weather Thunderstorm:Rain Threshold", "Weather Thunderstorm:Max Raindrops", "Weather Thunderstorm:Rain Entrance Speed", "Weather Thunderstorm:Ambient Loop Sound ID", "Weather Thunderstorm:Flash Decrement", "Weather Rain:Cloud Texture", "Weather Rain:Clouds Maximum Percent", "Weather Rain:Transition Delta", "Weather Rain:Sky Sunrise Color", "Weather Rain:Sky Day Color", "Weather Rain:Sky Sunset Color", "Weather Rain:Sky Night Color", "Weather Rain:Fog Sunrise Color", "Weather Rain:Fog Day Color", "Weather Rain:Fog Sunset Color", "Weather Rain:Fog Night Color", "Weather Rain:Ambient Sunrise Color", "Weather Rain:Ambient Day Color", "Weather Rain:Ambient Sunset Color", "Weather Rain:Ambient Night Color", "Weather Rain:Sun Sunrise Color", "Weather Rain:Sun Day Color", "Weather Rain:Sun Sunset Color", "Weather Rain:Sun Night Color", "Weather Rain:Sun Disc Sunset Color", "Weather Rain:Land Fog Day Depth", "Weather Rain:Land Fog Night Depth", "Weather Rain:Wind Speed", "Weather Rain:Cloud Speed", "Weather Rain:Glare View", "Weather Rain:Rain Loop Sound ID", "Weather Rain:Using Precip", "Weather Rain:Rain Diameter", "Weather Rain:Rain Height Min", "Weather Rain:Rain Height Max", "Weather Rain:Rain Threshold", "Weather Rain:Rain Entrance Speed", "Weather Rain:Ambient Loop Sound ID", "Weather Rain:Max Raindrops", "Weather Overcast:Cloud Texture", "Weather Overcast:Clouds Maximum Percent", "Weather Overcast:Transition Delta", "Weather Overcast:Sky Sunrise Color", "Weather Overcast:Sky Day Color", "Weather Overcast:Sky Sunset Color", "Weather Overcast:Sky Night Color", "Weather Overcast:Fog Sunrise Color", "Weather Overcast:Fog Day Color", "Weather Overcast:Fog Sunset Color", "Weather Overcast:Fog Night Color", "Weather Overcast:Ambient Sunrise Color", "Weather Overcast:Ambient Day Color", "Weather Overcast:Ambient Sunset Color", "Weather Overcast:Ambient Night Color", "Weather Overcast:Sun Sunrise Color", "Weather Overcast:Sun Day Color", "Weather Overcast:Sun Sunset Color", "Weather Overcast:Sun Night Color", "Weather Overcast:Sun Disc Sunset Color", "Weather Overcast:Land Fog Day Depth", "Weather Overcast:Land Fog Night Depth", "Weather Overcast:Wind Speed", "Weather Overcast:Cloud Speed", "Weather Overcast:Glare View", "Weather Overcast:Ambient Loop Sound ID", "Weather Ashstorm:Cloud Texture", "Weather Ashstorm:Clouds Maximum Percent", "Weather Ashstorm:Transition Delta", "Weather Ashstorm:Sky Sunrise Color", "Weather Ashstorm:Sky Day Color", "Weather Ashstorm:Sky Sunset Color", "Weather Ashstorm:Sky Night Color", "Weather Ashstorm:Fog Sunrise Color", "Weather Ashstorm:Fog Day Color", "Weather Ashstorm:Fog Sunset Color", "Weather Ashstorm:Fog Night Color", "Weather Ashstorm:Ambient Sunrise Color", "Weather Ashstorm:Ambient Day Color", "Weather Ashstorm:Ambient Sunset Color", "Weather Ashstorm:Ambient Night Color", "Weather Ashstorm:Sun Sunrise Color", "Weather Ashstorm:Sun Day Color", "Weather Ashstorm:Sun Sunset Color", "Weather Ashstorm:Sun Night Color", "Weather Ashstorm:Sun Disc Sunset Color", "Weather Ashstorm:Land Fog Day Depth", "Weather Ashstorm:Land Fog Night Depth", "Weather Ashstorm:Wind Speed", "Weather Ashstorm:Cloud Speed", "Weather Ashstorm:Glare View", "Weather Ashstorm:Ambient Loop Sound ID", "Weather Ashstorm:Storm Threshold", "Weather Blight:Cloud Texture", "Weather Blight:Clouds Maximum Percent", "Weather Blight:Transition Delta", "Weather Blight:Sky Sunrise Color", "Weather Blight:Sky Day Color", "Weather Blight:Sky Sunset Color", "Weather Blight:Sky Night Color", "Weather Blight:Fog Sunrise Color", "Weather Blight:Fog Day Color", "Weather Blight:Fog Sunset Color", "Weather Blight:Fog Night Color", "Weather Blight:Ambient Sunrise Color", "Weather Blight:Ambient Day Color", "Weather Blight:Ambient Sunset Color", "Weather Blight:Ambient Night Color", "Weather Blight:Sun Sunrise Color", "Weather Blight:Sun Day Color", "Weather Blight:Sun Sunset Color", "Weather Blight:Sun Night Color", "Weather Blight:Sun Disc Sunset Color", "Weather Blight:Land Fog Day Depth", "Weather Blight:Land Fog Night Depth", "Weather Blight:Wind Speed", "Weather Blight:Cloud Speed", "Weather Blight:Glare View", "Weather Blight:Ambient Loop Sound ID", "Weather Blight:Storm Threshold", "Weather Blight:Disease Chance", // for Bloodmoon "Weather Snow:Cloud Texture", "Weather Snow:Clouds Maximum Percent", "Weather Snow:Transition Delta", "Weather Snow:Sky Sunrise Color", "Weather Snow:Sky Day Color", "Weather Snow:Sky Sunset Color", "Weather Snow:Sky Night Color", "Weather Snow:Fog Sunrise Color", "Weather Snow:Fog Day Color", "Weather Snow:Fog Sunset Color", "Weather Snow:Fog Night Color", "Weather Snow:Ambient Sunrise Color", "Weather Snow:Ambient Day Color", "Weather Snow:Ambient Sunset Color", "Weather Snow:Ambient Night Color", "Weather Snow:Sun Sunrise Color", "Weather Snow:Sun Day Color", "Weather Snow:Sun Sunset Color", "Weather Snow:Sun Night Color", "Weather Snow:Sun Disc Sunset Color", "Weather Snow:Land Fog Day Depth", "Weather Snow:Land Fog Night Depth", "Weather Snow:Wind Speed", "Weather Snow:Cloud Speed", "Weather Snow:Glare View", "Weather Snow:Snow Diameter", "Weather Snow:Snow Height Min", "Weather Snow:Snow Height Max", "Weather Snow:Snow Entrance Speed", "Weather Snow:Max Snowflakes", "Weather Snow:Ambient Loop Sound ID", "Weather Snow:Snow Threshold", // for Bloodmoon "Weather Blizzard:Cloud Texture", "Weather Blizzard:Clouds Maximum Percent", "Weather Blizzard:Transition Delta", "Weather Blizzard:Sky Sunrise Color", "Weather Blizzard:Sky Day Color", "Weather Blizzard:Sky Sunset Color", "Weather Blizzard:Sky Night Color", "Weather Blizzard:Fog Sunrise Color", "Weather Blizzard:Fog Day Color", "Weather Blizzard:Fog Sunset Color", "Weather Blizzard:Fog Night Color", "Weather Blizzard:Ambient Sunrise Color", "Weather Blizzard:Ambient Day Color", "Weather Blizzard:Ambient Sunset Color", "Weather Blizzard:Ambient Night Color", "Weather Blizzard:Sun Sunrise Color", "Weather Blizzard:Sun Day Color", "Weather Blizzard:Sun Sunset Color", "Weather Blizzard:Sun Night Color", "Weather Blizzard:Sun Disc Sunset Color", "Weather Blizzard:Land Fog Day Depth", "Weather Blizzard:Land Fog Night Depth", "Weather Blizzard:Wind Speed", "Weather Blizzard:Cloud Speed", "Weather Blizzard:Glare View", "Weather Blizzard:Ambient Loop Sound ID", "Weather Blizzard:Storm Threshold", // moons "Moons:Secunda Size", "Moons:Secunda Axis Offset", "Moons:Secunda Speed", "Moons:Secunda Daily Increment", "Moons:Secunda Moon Shadow Early Fade Angle", "Moons:Secunda Fade Start Angle", "Moons:Secunda Fade End Angle", "Moons:Secunda Fade In Start", "Moons:Secunda Fade In Finish", "Moons:Secunda Fade Out Start", "Moons:Secunda Fade Out Finish", "Moons:Masser Size", "Moons:Masser Axis Offset", "Moons:Masser Speed", "Moons:Masser Daily Increment", "Moons:Masser Moon Shadow Early Fade Angle", "Moons:Masser Fade Start Angle", "Moons:Masser Fade End Angle", "Moons:Masser Fade In Start", "Moons:Masser Fade In Finish", "Moons:Masser Fade Out Start", "Moons:Masser Fade Out Finish", "Moons:Script Color", // werewolf (Bloodmoon) "General:Werewolf FOV", 0 }; for(int i=0; map[i][0]; i++) { mMergeMap.insert(std::make_pair(map[i][0], map[i][1])); } for(int i=0; fallback[i]; i++) { mMergeFallback.emplace_back(fallback[i]); } } void MwIniImporter::setVerbose(bool verbose) { mVerbose = verbose; } MwIniImporter::multistrmap MwIniImporter::loadIniFile(const boost::filesystem::path& filename) const { std::cout << "load ini file: " << filename << std::endl; std::string section(""); MwIniImporter::multistrmap map; bfs::ifstream file((bfs::path(filename))); ToUTF8::Utf8Encoder encoder(mEncoding); std::string line; while (std::getline(file, line)) { line = encoder.getUtf8(line); // unify Unix-style and Windows file ending if (!(line.empty()) && (line[line.length()-1]) == '\r') { line = line.substr(0, line.length()-1); } if(line.empty()) { continue; } if(line[0] == '[') { int pos = static_cast(line.find(']')); if(pos < 2) { std::cout << "Warning: ini file wrongly formatted (" << line << "). Line ignored." << std::endl; continue; } section = line.substr(1, line.find(']')-1); continue; } int comment_pos = static_cast(line.find(';')); if(comment_pos > 0) { line = line.substr(0,comment_pos); } int pos = static_cast(line.find('=')); if(pos < 1) { continue; } std::string key(section + ":" + line.substr(0,pos)); std::string value(line.substr(pos+1)); if(value.empty()) { std::cout << "Warning: ignored empty value for key '" << key << "'." << std::endl; continue; } if(map.find(key) == map.end()) { map.insert( std::make_pair (key, std::vector() ) ); } map[key].push_back(value); } return map; } MwIniImporter::multistrmap MwIniImporter::loadCfgFile(const boost::filesystem::path& filename) { std::cout << "load cfg file: " << filename << std::endl; MwIniImporter::multistrmap map; bfs::ifstream file((bfs::path(filename))); std::string line; while (std::getline(file, line)) { // we cant say comment by only looking at first char anymore int comment_pos = static_cast(line.find('#')); if(comment_pos > 0) { line = line.substr(0,comment_pos); } if(line.empty()) { continue; } int pos = static_cast(line.find('=')); if(pos < 1) { continue; } std::string key(line.substr(0,pos)); std::string value(line.substr(pos+1)); if(map.find(key) == map.end()) { map.insert( std::make_pair (key, std::vector() ) ); } map[key].push_back(value); } return map; } void MwIniImporter::merge(multistrmap &cfg, const multistrmap &ini) const { multistrmap::const_iterator iniIt; for(strmap::const_iterator it=mMergeMap.begin(); it!=mMergeMap.end(); ++it) { if((iniIt = ini.find(it->second)) != ini.end()) { for(std::vector::const_iterator vc = iniIt->second.begin(); vc != iniIt->second.end(); ++vc) { cfg.erase(it->first); insertMultistrmap(cfg, it->first, *vc); } } } } void MwIniImporter::mergeFallback(multistrmap &cfg, const multistrmap &ini) const { cfg.erase("fallback"); multistrmap::const_iterator iniIt; for(std::vector::const_iterator it=mMergeFallback.begin(); it!=mMergeFallback.end(); ++it) { if((iniIt = ini.find(*it)) != ini.end()) { for(std::vector::const_iterator vc = iniIt->second.begin(); vc != iniIt->second.end(); ++vc) { std::string value(*it); std::replace( value.begin(), value.end(), ' ', '_' ); std::replace( value.begin(), value.end(), ':', '_' ); value.append(",").append(vc->substr(0,vc->length())); insertMultistrmap(cfg, "fallback", value); } } } } void MwIniImporter::insertMultistrmap(multistrmap &cfg, const std::string& key, const std::string& value) { const multistrmap::const_iterator it = cfg.find(key); if(it == cfg.end()) { cfg.insert(std::make_pair (key, std::vector() )); } cfg[key].push_back(value); } void MwIniImporter::importArchives(multistrmap &cfg, const multistrmap &ini) const { std::vector archives; std::string baseArchive("Archives:Archive "); std::string archive; // Search archives listed in ini file multistrmap::const_iterator it = ini.begin(); for(int i=0; it != ini.end(); i++) { archive = baseArchive; archive.append(std::to_string(i)); it = ini.find(archive); if(it == ini.end()) { break; } for(std::vector::const_iterator entry = it->second.begin(); entry!=it->second.end(); ++entry) { archives.push_back(*entry); } } cfg.erase("fallback-archive"); cfg.insert( std::make_pair > ("fallback-archive", std::vector())); // Add Morrowind.bsa by default, since Vanilla loads this archive even if it // does not appears in the ini file cfg["fallback-archive"].push_back("Morrowind.bsa"); for(std::vector::const_iterator iter=archives.begin(); iter!=archives.end(); ++iter) { cfg["fallback-archive"].push_back(*iter); } } void MwIniImporter::dependencySortStep(std::string& element, MwIniImporter::dependencyList& source, std::vector& result) { auto iter = std::find_if( source.begin(), source.end(), [&element](std::pair< std::string, std::vector >& sourceElement) { return sourceElement.first == element; } ); if (iter != source.end()) { auto foundElement = std::move(*iter); source.erase(iter); for (auto name : foundElement.second) { MwIniImporter::dependencySortStep(name, source, result); } result.push_back(std::move(foundElement.first)); } } std::vector MwIniImporter::dependencySort(MwIniImporter::dependencyList source) { std::vector result; while (!source.empty()) { MwIniImporter::dependencySortStep(source.begin()->first, source, result); } return result; } std::vector::iterator MwIniImporter::findString(std::vector& source, const std::string& string) { return std::find_if(source.begin(), source.end(), [&string](const std::string& sourceString) { return Misc::StringUtils::ciEqual(sourceString, string); }); } void MwIniImporter::addPaths(std::vector& output, std::vector input) { for (auto& path : input) { if (path.front() == '"') { // Drop first and last characters - quotation marks path = path.substr(1, path.size() - 2); } output.emplace_back(path); } } void MwIniImporter::importGameFiles(multistrmap &cfg, const multistrmap &ini, const boost::filesystem::path& iniFilename) const { std::vector> contentFiles; std::string baseGameFile("Game Files:GameFile"); std::time_t defaultTime = 0; ToUTF8::Utf8Encoder encoder(mEncoding); std::vector dataPaths; if (cfg.count("data")) addPaths(dataPaths, cfg["data"]); if (cfg.count("data-local")) addPaths(dataPaths, cfg["data-local"]); dataPaths.push_back(iniFilename.parent_path() /= "Data Files"); multistrmap::const_iterator it = ini.begin(); for (int i=0; it != ini.end(); i++) { std::string gameFile = baseGameFile; gameFile.append(std::to_string(i)); it = ini.find(gameFile); if(it == ini.end()) break; for(std::vector::const_iterator entry = it->second.begin(); entry!=it->second.end(); ++entry) { std::string filetype(entry->substr(entry->length()-3)); Misc::StringUtils::lowerCaseInPlace(filetype); if(filetype.compare("esm") == 0 || filetype.compare("esp") == 0) { bool found = false; for (auto & dataPath : dataPaths) { boost::filesystem::path path = dataPath / *entry; std::time_t time = lastWriteTime(path, defaultTime); if (time != defaultTime) { contentFiles.emplace_back(time, std::move(path)); found = true; break; } } if (!found) std::cout << "Warning: " << *entry << " not found, ignoring" << std::endl; } } } cfg.erase("content"); cfg.insert( std::make_pair("content", std::vector() ) ); // sort by timestamp sort(contentFiles.begin(), contentFiles.end()); MwIniImporter::dependencyList unsortedFiles; ESM::ESMReader reader; reader.setEncoder(&encoder); for (auto& file : contentFiles) { reader.open(file.second.string()); std::vector dependencies; for (auto& gameFile : reader.getGameFiles()) { dependencies.push_back(gameFile.name); } unsortedFiles.emplace_back(boost::filesystem::path(reader.getName()).filename().string(), dependencies); reader.close(); } auto sortedFiles = dependencySort(unsortedFiles); // hard-coded dependency Morrowind - Tribunal - Bloodmoon if(findString(sortedFiles, "Morrowind.esm") != sortedFiles.end()) { auto tribunalIter = findString(sortedFiles, "Tribunal.esm"); auto bloodmoonIter = findString(sortedFiles, "Bloodmoon.esm"); if (bloodmoonIter != sortedFiles.end() && tribunalIter != sortedFiles.end()) { size_t bloodmoonIndex = std::distance(sortedFiles.begin(), bloodmoonIter); size_t tribunalIndex = std::distance(sortedFiles.begin(), tribunalIter); if (bloodmoonIndex < tribunalIndex) tribunalIndex++; sortedFiles.insert(bloodmoonIter, *tribunalIter); sortedFiles.erase(sortedFiles.begin() + tribunalIndex); } } for (auto& file : sortedFiles) cfg["content"].push_back(file); } void MwIniImporter::writeToFile(std::ostream &out, const multistrmap &cfg) { for(multistrmap::const_iterator it=cfg.begin(); it != cfg.end(); ++it) { for(std::vector::const_iterator entry=it->second.begin(); entry != it->second.end(); ++entry) { out << (it->first) << "=" << (*entry) << std::endl; } } } void MwIniImporter::setInputEncoding(const ToUTF8::FromType &encoding) { mEncoding = encoding; } std::time_t MwIniImporter::lastWriteTime(const boost::filesystem::path& filename, std::time_t defaultTime) { std::time_t writeTime(defaultTime); if (boost::filesystem::exists(filename)) { boost::filesystem::path resolved = boost::filesystem::canonical(filename); writeTime = boost::filesystem::last_write_time(resolved); // print timestamp const int size=1024; char timeStrBuffer[size]; if (std::strftime(timeStrBuffer, size, "%x %X", localtime(&writeTime)) > 0) std::cout << "content file: " << resolved << " timestamp = (" << writeTime << ") " << timeStrBuffer << std::endl; } return writeTime; } ================================================ FILE: apps/mwiniimporter/importer.hpp ================================================ #ifndef MWINIIMPORTER_IMPORTER #define MWINIIMPORTER_IMPORTER 1 #include #include #include #include #include #include #include class MwIniImporter { public: typedef std::map strmap; typedef std::map > multistrmap; typedef std::vector< std::pair< std::string, std::vector > > dependencyList; MwIniImporter(); void setInputEncoding(const ToUTF8::FromType& encoding); void setVerbose(bool verbose); multistrmap loadIniFile(const boost::filesystem::path& filename) const; static multistrmap loadCfgFile(const boost::filesystem::path& filename); void merge(multistrmap &cfg, const multistrmap &ini) const; void mergeFallback(multistrmap &cfg, const multistrmap &ini) const; void importGameFiles(multistrmap &cfg, const multistrmap &ini, const boost::filesystem::path& iniFilename) const; void importArchives(multistrmap &cfg, const multistrmap &ini) const; static void writeToFile(std::ostream &out, const multistrmap &cfg); static std::vector dependencySort(MwIniImporter::dependencyList source); private: static void dependencySortStep(std::string& element, MwIniImporter::dependencyList& source, std::vector& result); static std::vector::iterator findString(std::vector& source, const std::string& string); static void insertMultistrmap(multistrmap &cfg, const std::string& key, const std::string& value); static void addPaths(std::vector& output, std::vector input); /// \return file's "last modified time", used in original MW to determine plug-in load order static std::time_t lastWriteTime(const boost::filesystem::path& filename, std::time_t defaultTime); bool mVerbose; strmap mMergeMap; std::vector mMergeFallback; ToUTF8::FromType mEncoding; }; #endif ================================================ FILE: apps/mwiniimporter/main.cpp ================================================ #include "importer.hpp" #include #include #include #include namespace bpo = boost::program_options; namespace bfs = boost::filesystem; #ifndef _WIN32 int main(int argc, char *argv[]) { #else // Include on Windows only #include class utf8argv { public: utf8argv(int argc, wchar_t *wargv[]) { args.reserve(argc); argv = new const char *[argc]; for (int i = 0; i < argc; ++i) { args.push_back(boost::locale::conv::utf_to_utf(wargv[i])); argv[i] = args.back().c_str(); } } ~utf8argv() { delete[] argv; } char **get() const { return const_cast(argv); } private: utf8argv(const utf8argv&); utf8argv& operator=(const utf8argv&); const char **argv; std::vector args; }; /* The only way to pass Unicode on Winodws with CLI is to use wide characters interface which presents UTF-16 encoding. The rest of OpenMW application stack assumes UTF-8 encoding, therefore this conversion. For boost::filesystem::path::imbue see components/files/windowspath.cpp */ int wmain(int argc, wchar_t *wargv[]) { utf8argv converter(argc, wargv); char **argv = converter.get(); boost::filesystem::path::imbue(boost::locale::generator().generate("")); #endif try { bpo::options_description desc("Syntax: openmw-iniimporter inifile configfile\nAllowed options"); bpo::positional_options_description p_desc; desc.add_options() ("help,h", "produce help message") ("verbose,v", "verbose output") ("ini,i", bpo::value(), "morrowind.ini file") ("cfg,c", bpo::value(), "openmw.cfg file") ("output,o", bpo::value()->default_value(""), "openmw.cfg file") ("game-files,g", "import esm and esp files") ("no-archives,A", "disable bsa archives import") ("encoding,e", bpo::value()-> 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") ; p_desc.add("ini", 1).add("cfg", 1); bpo::variables_map vm; bpo::parsed_options parsed = bpo::command_line_parser(argc, argv) .options(desc) .positional(p_desc) .run(); bpo::store(parsed, vm); if(vm.count("help") || !vm.count("ini") || !vm.count("cfg")) { std::cout << desc; return 0; } bpo::notify(vm); boost::filesystem::path iniFile(vm["ini"].as()); boost::filesystem::path cfgFile(vm["cfg"].as()); // if no output is given, write back to cfg file std::string outputFile(vm["output"].as()); if(vm["output"].defaulted()) { outputFile = vm["cfg"].as(); } if(!boost::filesystem::exists(iniFile)) { std::cerr << "ini file does not exist" << std::endl; return -3; } if(!boost::filesystem::exists(cfgFile)) std::cerr << "cfg file does not exist" << std::endl; MwIniImporter importer; importer.setVerbose(vm.count("verbose") != 0); // Font encoding settings std::string encoding(vm["encoding"].as()); importer.setInputEncoding(ToUTF8::calculateEncoding(encoding)); MwIniImporter::multistrmap ini = importer.loadIniFile(iniFile); MwIniImporter::multistrmap cfg = importer.loadCfgFile(cfgFile); importer.merge(cfg, ini); importer.mergeFallback(cfg, ini); if(vm.count("game-files")) { importer.importGameFiles(cfg, ini, iniFile); } if(!vm.count("no-archives")) { importer.importArchives(cfg, ini); } std::cout << "write to: " << outputFile << std::endl; bfs::ofstream file((bfs::path(outputFile))); importer.writeToFile(file, cfg); } catch (std::exception& e) { std::cerr << "ERROR: " << e.what() << std::endl; } return 0; } ================================================ FILE: apps/niftest/CMakeLists.txt ================================================ set(NIFTEST niftest.cpp ) source_group(components\\nif\\tests FILES ${NIFTEST}) # Main executable openmw_add_executable(niftest ${NIFTEST} ) target_link_libraries(niftest ${Boost_FILESYSTEM_LIBRARY} components ) if (BUILD_WITH_CODE_COVERAGE) add_definitions (--coverage) target_link_libraries(niftest gcov) endif() ================================================ FILE: apps/niftest/niftest.cpp ================================================ ///Program to test .nif files both on the FileSystem and in BSA archives. #include #include #include #include #include #include #include #include #include #include // Create local aliases for brevity 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) { 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; } ///See if the file has the "nif" extension. bool isNIF(const std::string & filename) { return hasExtension(filename,"nif"); } ///See if the file has the "bsa" extension. bool isBSA(const std::string & filename) { return hasExtension(filename,"bsa"); } /// Check all the nif files in a given VFS::Archive /// \note Takes ownership! /// \note Can not read a bsa file inside of a bsa file. void readVFS(VFS::Archive* anArchive,std::string archivePath = "") { VFS::Manager myManager(true); myManager.addArchive(anArchive); myManager.buildIndex(); std::map files=myManager.getIndex(); for(std::map::const_iterator it=files.begin(); it!=files.end(); ++it) { std::string name = it->first; try{ if(isNIF(name)) { // std::cout << "Decoding: " << name << std::endl; Nif::NIFFile temp_nif(myManager.get(name),archivePath+name); } else if(isBSA(name)) { if(!archivePath.empty() && !isBSA(archivePath)) { // std::cout << "Reading BSA File: " << name << std::endl; readVFS(new VFS::BsaArchive(archivePath+name),archivePath+name+"/"); // std::cout << "Done with BSA File: " << name << std::endl; } } } catch (std::exception& e) { std::cerr << "ERROR, an exception has occurred: " << e.what() << std::endl; } } } bool parseOptions (int argc, char** argv, std::vector& files) { bpo::options_description desc("Ensure that OpenMW can use the provided NIF and BSA files\n\n" "Usages:\n" " niftool \n" " Scan the file or directories for nif errors.\n\n" "Allowed options"); desc.add_options() ("help,h", "print help message.") ("input-file", bpo::value< std::vector >(), "input file") ; //Default option if none provided bpo::positional_options_description p; p.add("input-file", -1); bpo::variables_map variables; try { bpo::parsed_options valid_opts = bpo::command_line_parser(argc, argv). options(desc).positional(p).run(); bpo::store(valid_opts, variables); bpo::notify(variables); if (variables.count ("help")) { std::cout << desc << std::endl; return false; } if (variables.count("input-file")) { files = variables["input-file"].as< std::vector >(); return true; } } catch(std::exception &e) { std::cout << "ERROR parsing arguments: " << e.what() << "\n\n" << desc << std::endl; return false; } std::cout << "No input files or directories specified!" << std::endl; std::cout << desc << std::endl; return false; } int main(int argc, char **argv) { std::vector files; if(!parseOptions (argc, argv, files)) return 1; Nif::NIFFile::setLoadUnsupportedFiles(true); // std::cout << "Reading Files" << std::endl; for(std::vector::const_iterator it=files.begin(); it!=files.end(); ++it) { std::string name = *it; try { if(isNIF(name)) { //std::cout << "Decoding: " << name << std::endl; Nif::NIFFile temp_nif(Files::openConstrainedFileStream(name.c_str()),name); } else if(isBSA(name)) { // std::cout << "Reading BSA File: " << name << std::endl; readVFS(new VFS::BsaArchive(name)); } else if(bfs::is_directory(bfs::path(name))) { // std::cout << "Reading All Files in: " << name << std::endl; readVFS(new VFS::FileSystemArchive(name),name); } else { std::cerr << "ERROR: \"" << name << "\" is not a nif file, bsa file, or directory!" << std::endl; } } catch (std::exception& e) { std::cerr << "ERROR, an exception has occurred: " << e.what() << std::endl; } } return 0; } ================================================ FILE: apps/opencs/CMakeLists.txt ================================================ set (OPENCS_SRC main.cpp ${CMAKE_SOURCE_DIR}/files/windows/opencs.rc ) opencs_units (. editor) opencs_units (model/doc document operation saving documentmanager loader runner operationholder ) opencs_units_noqt (model/doc stage savingstate savingstages blacklist messages ) opencs_hdrs_noqt (model/doc state ) opencs_units (model/world idtable idtableproxymodel regionmap data commanddispatcher idtablebase resourcetable nestedtableproxymodel idtree infotableproxymodel landtexturetableproxymodel actoradapter ) opencs_units_noqt (model/world universalid record commands columnbase columnimp scriptcontext cell refidcollection refidadapter refiddata refidadapterimp ref collectionbase refcollection columns infocollection tablemimedata cellcoordinates cellselection resources resourcesmanager scope pathgrid landtexture land nestedtablewrapper nestedcollection nestedcoladapterimp nestedinfocollection idcompletionmanager metadata defaultgmsts infoselectwrapper commandmacro ) opencs_hdrs_noqt (model/world columnimp idcollection collection info subcellcollection ) opencs_units (model/tools tools reportmodel mergeoperation ) opencs_units_noqt (model/tools mandatoryid skillcheck classcheck factioncheck racecheck soundcheck regioncheck birthsigncheck spellcheck referencecheck referenceablecheck scriptcheck bodypartcheck startscriptcheck search searchoperation searchstage pathgridcheck soundgencheck magiceffectcheck mergestages gmstcheck topicinfocheck journalcheck enchantmentcheck ) opencs_hdrs_noqt (model/tools mergestate ) opencs_units (view/doc viewmanager view operations operation subview startup filedialog newgame filewidget adjusterwidget loader globaldebugprofilemenu runlogsubview sizehint ) opencs_units_noqt (view/doc subviewfactory ) opencs_hdrs_noqt (view/doc subviewfactoryimp ) opencs_units (view/world table tablesubview scriptsubview util regionmapsubview tablebottombox creator genericcreator globalcreator cellcreator pathgridcreator referenceablecreator startscriptcreator referencecreator scenesubview infocreator scriptedit dialoguesubview previewsubview regionmap dragrecordtable nestedtable dialoguespinbox recordbuttonbar tableeditidaction scripterrortable extendedcommandconfigurator bodypartcreator landtexturecreator landcreator ) opencs_units_noqt (view/world subviews enumdelegate vartypedelegate recordstatusdelegate idtypedelegate datadisplaydelegate scripthighlighter idvalidator dialoguecreator idcompletiondelegate colordelegate dragdroputils ) opencs_units (view/widget scenetoolbar scenetool scenetoolmode pushbutton scenetooltoggle scenetoolrun modebutton scenetooltoggle2 scenetooltexturebrush scenetoolshapebrush completerpopup coloreditor colorpickerpopup droplineedit ) opencs_units (view/render scenewidget worldspacewidget pagedworldspacewidget unpagedworldspacewidget previewwidget editmode instancemode instanceselectionmode instancemovemode orbitcameramode pathgridmode selectionmode pathgridselectionmode cameracontroller cellwater terraintexturemode actor terrainselection terrainshapemode brushdraw commands ) opencs_units_noqt (view/render lighting lightingday lightingnight lightingbright object cell terrainstorage tagbase cellarrow cellmarker cellborder pathgrid ) opencs_hdrs_noqt (view/render mask ) opencs_units (view/tools reportsubview reporttable searchsubview searchbox merge ) opencs_units_noqt (view/tools subviews ) opencs_units (view/prefs dialogue pagebase page keybindingpage contextmenulist ) opencs_units (model/prefs state setting intsetting doublesetting boolsetting enumsetting coloursetting shortcut shortcuteventhandler shortcutmanager shortcutsetting modifiersetting stringsetting ) opencs_units_noqt (model/prefs category ) opencs_units_noqt (model/filter node unarynode narynode leafnode booleannode parser andnode ornode notnode textnode valuenode ) opencs_units (view/filter filterbox recordfilterbox editwidget ) set (OPENCS_US ) set (OPENCS_RES ${CMAKE_SOURCE_DIR}/files/opencs/resources.qrc ${CMAKE_SOURCE_DIR}/files/launcher/launcher.qrc ) set (OPENCS_UI ${CMAKE_SOURCE_DIR}/files/ui/contentselector.ui ${CMAKE_SOURCE_DIR}/files/ui/filedialog.ui ) source_group (openmw-cs FILES ${OPENCS_SRC} ${OPENCS_HDR}) if(WIN32) set(QT_USE_QTMAIN TRUE) endif(WIN32) qt5_wrap_ui(OPENCS_UI_HDR ${OPENCS_UI}) qt5_wrap_cpp(OPENCS_MOC_SRC ${OPENCS_HDR_QT}) qt5_add_resources(OPENCS_RES_SRC ${OPENCS_RES}) # for compiled .ui files include_directories(${CMAKE_CURRENT_BINARY_DIR}) if(APPLE) set (OPENCS_MAC_ICON "${CMAKE_SOURCE_DIR}/files/mac/openmw-cs.icns") set (OPENCS_CFG "${OpenMW_BINARY_DIR}/defaults-cs.bin") set (OPENCS_DEFAULT_FILTERS_FILE "${OpenMW_BINARY_DIR}/resources/defaultfilters") set (OPENCS_OPENMW_CFG "${OpenMW_BINARY_DIR}/openmw.cfg") else() set (OPENCS_MAC_ICON "") set (OPENCS_CFG "") set (OPENCS_DEFAULT_FILTERS_FILE "") set (OPENCS_OPENMW_CFG "") endif(APPLE) openmw_add_executable(openmw-cs MACOSX_BUNDLE ${OPENCS_SRC} ${OPENCS_UI_HDR} ${OPENCS_MOC_SRC} ${OPENCS_RES_SRC} ${OPENCS_MAC_ICON} ${OPENCS_CFG} ${OPENCS_DEFAULT_FILTERS_FILE} ${OPENCS_OPENMW_CFG} ) 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}) add_subdirectory(../../files/ ${CMAKE_CURRENT_BINARY_DIR}/files) set_target_properties(openmw-cs PROPERTIES RUNTIME_OUTPUT_DIRECTORY "${OpenMW_BINARY_DIR}" OUTPUT_NAME ${OPENCS_BUNDLE_NAME} MACOSX_BUNDLE_ICON_FILE "openmw-cs.icns" MACOSX_BUNDLE_BUNDLE_NAME "OpenMW-CS" MACOSX_BUNDLE_GUI_IDENTIFIER "org.openmw.opencs" MACOSX_BUNDLE_SHORT_VERSION_STRING ${OPENMW_VERSION} MACOSX_BUNDLE_BUNDLE_VERSION ${OPENMW_VERSION} MACOSX_BUNDLE_INFO_PLIST "${CMAKE_SOURCE_DIR}/files/mac/openmw-cs-Info.plist.in" ) set_source_files_properties(${OPENCS_MAC_ICON} PROPERTIES MACOSX_PACKAGE_LOCATION Resources) set_source_files_properties(${OPENCS_CFG} PROPERTIES MACOSX_PACKAGE_LOCATION Resources) set_source_files_properties(${OPENCS_DEFAULT_FILTERS_FILE} PROPERTIES MACOSX_PACKAGE_LOCATION Resources/resources) set_source_files_properties(${OPENCS_OPENMW_CFG} PROPERTIES MACOSX_PACKAGE_LOCATION Resources) add_custom_command(TARGET openmw-cs POST_BUILD COMMAND cp "${OpenMW_BINARY_DIR}/resources/version" "${OPENCS_BUNDLE_RESOURCES_DIR}/resources") endif(APPLE) target_link_libraries(openmw-cs # CMake's built-in OSG finder does not use pkgconfig, so we have to # manually ensure the order is correct for inter-library dependencies. # This only makes a difference with `-DOPENMW_USE_SYSTEM_OSG=ON -DOSG_STATIC=ON`. # https://gitlab.kitware.com/cmake/cmake/-/issues/21701 ${OSGVIEWER_LIBRARIES} ${OSGFX_LIBRARIES} ${OSGGA_LIBRARIES} ${OSGUTIL_LIBRARIES} ${OSGTEXT_LIBRARIES} ${OSG_LIBRARIES} ${EXTERN_OSGQT_LIBRARY} ${Boost_SYSTEM_LIBRARY} ${Boost_FILESYSTEM_LIBRARY} ${Boost_PROGRAM_OPTIONS_LIBRARY} components ) if(OSG_STATIC) unset(_osg_plugins_static_files) add_library(openmw_cs_osg_plugins INTERFACE) foreach(_plugin ${USED_OSG_PLUGINS}) string(TOUPPER ${_plugin} _plugin_uc) if(OPENMW_USE_SYSTEM_OSG) list(APPEND _osg_plugins_static_files ${${_plugin_uc}_LIBRARY}) else() list(APPEND _osg_plugins_static_files $) target_link_libraries(openmw_cs_osg_plugins INTERFACE $) add_dependencies(openmw_cs_osg_plugins ${${_plugin_uc}_LIBRARY}) endif() endforeach() # We use --whole-archive because OSG plugins use registration. get_whole_archive_options(_opts ${_osg_plugins_static_files}) target_link_options(openmw_cs_osg_plugins INTERFACE ${_opts}) target_link_libraries(openmw-cs openmw_cs_osg_plugins) if(OPENMW_USE_SYSTEM_OSG) # OSG plugin pkgconfig files are missing these dependencies. # https://github.com/openscenegraph/OpenSceneGraph/issues/1052 target_link_libraries(openmw freetype jpeg png) endif() endif(OSG_STATIC) target_link_libraries(openmw-cs Qt5::Widgets Qt5::Core Qt5::Network Qt5::OpenGL) if (WIN32) target_link_libraries(openmw-cs ${Boost_LOCALE_LIBRARY}) INSTALL(TARGETS openmw-cs RUNTIME DESTINATION ".") get_generator_is_multi_config(multi_config) if (multi_config) SET(INSTALL_SOURCE "${OpenMW_BINARY_DIR}/$") else () SET(INSTALL_SOURCE "${OpenMW_BINARY_DIR}") endif () INSTALL(FILES "${INSTALL_SOURCE}/defaults-cs.bin" DESTINATION ".") endif() if (MSVC) # Debug version needs increased number of sections beyond 2^16 if (CMAKE_CL_64) set (CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} /bigobj") endif (CMAKE_CL_64) endif (MSVC) if(APPLE) INSTALL(TARGETS openmw-cs BUNDLE DESTINATION "." COMPONENT Bundle) endif() ================================================ FILE: apps/opencs/Networking.cpp ================================================ #include "editor.hpp" #include #include #include #include #include #include #include "model/doc/messages.hpp" #include "model/world/universalid.hpp" #ifdef Q_OS_MAC #include #endif Q_DECLARE_METATYPE (std::string) class Application : public QApplication { private: bool notify (QObject *receiver, QEvent *event) { try { return QApplication::notify (receiver, event); } catch (const std::exception& exception) { std::cerr << "An exception has been caught: " << exception.what() << std::endl; } return false; } public: Application (int& argc, char *argv[]) : QApplication (argc, argv) {} }; int main(int argc, char *argv[]) { #ifdef Q_OS_MAC setenv("OSG_GL_TEXTURE_STORAGE", "OFF", 0); #endif try { // To allow background thread drawing in OSG QApplication::setAttribute(Qt::AA_X11InitThreads, true); Q_INIT_RESOURCE (resources); qRegisterMetaType ("std::string"); qRegisterMetaType ("CSMWorld::UniversalId"); qRegisterMetaType ("CSMDoc::Message"); Application application (argc, argv); #ifdef Q_OS_MAC QDir dir(QCoreApplication::applicationDirPath()); if (dir.dirName() == "MacOS") { dir.cdUp(); dir.cdUp(); dir.cdUp(); } QDir::setCurrent(dir.absolutePath()); #endif application.setWindowIcon (QIcon (":./openmw-cs.png")); CS::Editor editor; if(!editor.makeIPCServer()) { editor.connectToIPCServer(); return 0; } return editor.run(); } catch (std::exception& e) { std::cerr << "ERROR: " << e.what() << std::endl; return 0; } } ================================================ FILE: apps/opencs/editor.cpp ================================================ #include "editor.hpp" #include #include #include #include #include #include #include #include #include "model/doc/document.hpp" #include "model/world/data.hpp" #ifdef _WIN32 #include #endif using namespace Fallback; CS::Editor::Editor (int argc, char **argv) : mSettingsState (mCfgMgr), mDocumentManager (mCfgMgr), mPid(""), mLock(), mMerge (mDocumentManager), mIpcServerName ("org.openmw.OpenCS"), mServer(nullptr), mClientSocket(nullptr) { std::pair > config = readConfig(); mViewManager = new CSVDoc::ViewManager(mDocumentManager); if (argc > 1) { mFileToLoad = argv[1]; mDataDirs = config.first; } NifOsg::Loader::setShowMarkers(true); mDocumentManager.setFileData(mFsStrict, config.first, config.second); mNewGame.setLocalData (mLocal); mFileDialog.setLocalData (mLocal); mMerge.setLocalData (mLocal); connect (&mDocumentManager, SIGNAL (documentAdded (CSMDoc::Document *)), this, SLOT (documentAdded (CSMDoc::Document *))); connect (&mDocumentManager, SIGNAL (documentAboutToBeRemoved (CSMDoc::Document *)), this, SLOT (documentAboutToBeRemoved (CSMDoc::Document *))); connect (&mDocumentManager, SIGNAL (lastDocumentDeleted()), this, SLOT (lastDocumentDeleted())); connect (mViewManager, SIGNAL (newGameRequest ()), this, SLOT (createGame ())); connect (mViewManager, SIGNAL (newAddonRequest ()), this, SLOT (createAddon ())); connect (mViewManager, SIGNAL (loadDocumentRequest ()), this, SLOT (loadDocument ())); connect (mViewManager, SIGNAL (editSettingsRequest()), this, SLOT (showSettings ())); connect (mViewManager, SIGNAL (mergeDocument (CSMDoc::Document *)), this, SLOT (mergeDocument (CSMDoc::Document *))); connect (&mStartup, SIGNAL (createGame()), this, SLOT (createGame ())); connect (&mStartup, SIGNAL (createAddon()), this, SLOT (createAddon ())); connect (&mStartup, SIGNAL (loadDocument()), this, SLOT (loadDocument ())); connect (&mStartup, SIGNAL (editConfig()), this, SLOT (showSettings ())); connect (&mFileDialog, SIGNAL(signalOpenFiles (const boost::filesystem::path&)), this, SLOT(openFiles (const boost::filesystem::path&))); connect (&mFileDialog, SIGNAL(signalCreateNewFile (const boost::filesystem::path&)), this, SLOT(createNewFile (const boost::filesystem::path&))); connect (&mFileDialog, SIGNAL (rejected()), this, SLOT (cancelFileDialog ())); connect (&mNewGame, SIGNAL (createRequest (const boost::filesystem::path&)), this, SLOT (createNewGame (const boost::filesystem::path&))); connect (&mNewGame, SIGNAL (cancelCreateGame()), this, SLOT (cancelCreateGame ())); } CS::Editor::~Editor () { delete mViewManager; mPidFile.close(); if(mServer && boost::filesystem::exists(mPid)) static_cast ( // silence coverity warning remove(mPid.string().c_str())); // ignore any error } std::pair > CS::Editor::readConfig(bool quiet) { boost::program_options::variables_map variables; boost::program_options::options_description desc("Syntax: openmw-cs \nAllowed options"); desc.add_options() ("data", boost::program_options::value()->default_value(Files::EscapePathContainer(), "data")->multitoken()->composing()) ("data-local", boost::program_options::value()->default_value(Files::EscapePath(), "")) ("fs-strict", boost::program_options::value()->implicit_value(true)->default_value(false)) ("encoding", boost::program_options::value()->default_value("win1252")) ("resources", boost::program_options::value()->default_value(Files::EscapePath(), "resources")) ("fallback-archive", boost::program_options::value()-> default_value(Files::EscapeStringVector(), "fallback-archive")->multitoken()) ("fallback", boost::program_options::value()->default_value(FallbackMap(), "") ->multitoken()->composing(), "fallback values") ("script-blacklist", boost::program_options::value()->default_value(Files::EscapeStringVector(), "") ->multitoken(), "exclude specified script from the verifier (if the use of the blacklist is enabled)") ("script-blacklist-use", boost::program_options::value()->implicit_value(true) ->default_value(true), "enable script blacklisting"); boost::program_options::notify(variables); mCfgMgr.readConfiguration(variables, desc, false); Fallback::Map::init(variables["fallback"].as().mMap); mEncodingName = variables["encoding"].as().toStdString(); mDocumentManager.setEncoding(ToUTF8::calculateEncoding(mEncodingName)); mFileDialog.setEncoding (QString::fromUtf8(mEncodingName.c_str())); mDocumentManager.setResourceDir (mResources = variables["resources"].as().mPath); if (variables["script-blacklist-use"].as()) mDocumentManager.setBlacklistedScripts ( variables["script-blacklist"].as().toStdStringVector()); mFsStrict = variables["fs-strict"].as(); Files::PathContainer dataDirs, dataLocal; if (!variables["data"].empty()) { dataDirs = Files::PathContainer(Files::EscapePath::toPathContainer(variables["data"].as())); } Files::PathContainer::value_type local(variables["data-local"].as().mPath); if (!local.empty()) dataLocal.push_back(local); mCfgMgr.processPaths (dataDirs); mCfgMgr.processPaths (dataLocal, true); if (!dataLocal.empty()) mLocal = dataLocal[0]; else { QMessageBox messageBox; messageBox.setWindowTitle (tr ("No local data path available")); messageBox.setIcon (QMessageBox::Critical); messageBox.setStandardButtons (QMessageBox::Ok); messageBox.setText(tr("
OpenCS is unable to access the local data directory. This may indicate a faulty configuration or a broken install.")); messageBox.exec(); QApplication::exit (1); } 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); } return std::make_pair (dataDirs, variables["fallback-archive"].as().toStdStringVector()); } void CS::Editor::createGame() { mStartup.hide(); if (mNewGame.isHidden()) mNewGame.show(); mNewGame.raise(); mNewGame.activateWindow(); } void CS::Editor::cancelCreateGame() { if (!mDocumentManager.isEmpty()) return; mNewGame.hide(); if (mStartup.isHidden()) mStartup.show(); mStartup.raise(); mStartup.activateWindow(); } void CS::Editor::createAddon() { mStartup.hide(); mFileDialog.clearFiles(); readConfig(/*quiet*/true); mFileDialog.showDialog (CSVDoc::ContentAction_New); } void CS::Editor::cancelFileDialog() { if (!mDocumentManager.isEmpty()) return; mFileDialog.hide(); if (mStartup.isHidden()) mStartup.show(); mStartup.raise(); mStartup.activateWindow(); } void CS::Editor::loadDocument() { mStartup.hide(); mFileDialog.clearFiles(); readConfig(/*quiet*/true); mFileDialog.showDialog (CSVDoc::ContentAction_Edit); } void CS::Editor::openFiles (const boost::filesystem::path &savePath, const std::vector &discoveredFiles) { std::vector files; if(discoveredFiles.empty()) { for (const QString &path : mFileDialog.selectedFilePaths()) files.emplace_back(path.toUtf8().constData()); } else { files = discoveredFiles; } mDocumentManager.addDocument (files, savePath, false); mFileDialog.hide(); } void CS::Editor::createNewFile (const boost::filesystem::path &savePath) { std::vector files; for (const QString &path : mFileDialog.selectedFilePaths()) { files.emplace_back(path.toUtf8().constData()); } files.push_back (savePath); mDocumentManager.addDocument (files, savePath, true); mFileDialog.hide(); } void CS::Editor::createNewGame (const boost::filesystem::path& file) { std::vector files; files.push_back (file); mDocumentManager.addDocument (files, file, true); mNewGame.hide(); } void CS::Editor::showStartup() { if(mStartup.isHidden()) mStartup.show(); mStartup.raise(); mStartup.activateWindow(); } void CS::Editor::showSettings() { if (mSettings.isHidden()) mSettings.show(); mSettings.move (QCursor::pos()); mSettings.raise(); mSettings.activateWindow(); } bool CS::Editor::makeIPCServer() { try { mPid = boost::filesystem::temp_directory_path(); mPid /= "openmw-cs.pid"; bool pidExists = boost::filesystem::exists(mPid); mPidFile.open(mPid); mLock = boost::interprocess::file_lock(mPid.string().c_str()); if(!mLock.try_lock()) { Log(Debug::Error) << "Error: OpenMW-CS is already running."; return false; } #ifdef _WIN32 mPidFile << GetCurrentProcessId() << std::endl; #else mPidFile << getpid() << std::endl; #endif mServer = new QLocalServer(this); if(pidExists) { // hack to get the temp directory path mServer->listen("dummy"); QString fullPath = mServer->fullServerName(); mServer->close(); fullPath.remove(QRegExp("dummy$")); fullPath += mIpcServerName; if(boost::filesystem::exists(fullPath.toUtf8().constData())) { // TODO: compare pid of the current process with that in the file Log(Debug::Info) << "Detected unclean shutdown."; // delete the stale file if(remove(fullPath.toUtf8().constData())) Log(Debug::Error) << "Error: can not remove stale connection file."; } } } catch(const std::exception& e) { Log(Debug::Error) << "Error: " << e.what(); return false; } if(mServer->listen(mIpcServerName)) { connect(mServer, SIGNAL(newConnection()), this, SLOT(showStartup())); return true; } mServer->close(); mServer = nullptr; return false; } void CS::Editor::connectToIPCServer() { mClientSocket = new QLocalSocket(this); mClientSocket->connectToServer(mIpcServerName); mClientSocket->close(); } int CS::Editor::run() { if (mLocal.empty()) return 1; Misc::Rng::init(); QApplication::setQuitOnLastWindowClosed(true); if (mFileToLoad.empty()) { mStartup.show(); } else { ESM::ESMReader fileReader; ToUTF8::Utf8Encoder encoder = ToUTF8::calculateEncoding(mEncodingName); fileReader.setEncoder(&encoder); fileReader.open(mFileToLoad.string()); std::vector discoveredFiles; for (std::vector::const_iterator itemIter = fileReader.getGameFiles().begin(); itemIter != fileReader.getGameFiles().end(); ++itemIter) { for (Files::PathContainer::const_iterator pathIter = mDataDirs.begin(); pathIter != mDataDirs.end(); ++pathIter) { const boost::filesystem::path masterPath = *pathIter / itemIter->name; if (boost::filesystem::exists(masterPath)) { discoveredFiles.push_back(masterPath); break; } } } discoveredFiles.push_back(mFileToLoad); QString extension = QString::fromStdString(mFileToLoad.extension().string()).toLower(); if (extension == ".esm") { mFileToLoad.replace_extension(".omwgame"); mDocumentManager.addDocument(discoveredFiles, mFileToLoad, false); } else if (extension == ".esp") { mFileToLoad.replace_extension(".omwaddon"); mDocumentManager.addDocument(discoveredFiles, mFileToLoad, false); } else { openFiles(mFileToLoad, discoveredFiles); } } return QApplication::exec(); } void CS::Editor::documentAdded (CSMDoc::Document *document) { mViewManager->addView (document); } void CS::Editor::documentAboutToBeRemoved (CSMDoc::Document *document) { if (mMerge.getDocument()==document) mMerge.cancel(); } void CS::Editor::lastDocumentDeleted() { QApplication::quit(); } void CS::Editor::mergeDocument (CSMDoc::Document *document) { mMerge.configure (document); mMerge.show(); mMerge.raise(); mMerge.activateWindow(); } ================================================ FILE: apps/opencs/editor.hpp ================================================ #ifndef CS_EDITOR_H #define CS_EDITOR_H #include #include #include #include #include #include #ifndef Q_MOC_RUN #include #endif #include #include "model/doc/documentmanager.hpp" #include "model/prefs/state.hpp" #include "view/doc/viewmanager.hpp" #include "view/doc/startup.hpp" #include "view/doc/filedialog.hpp" #include "view/doc/newgame.hpp" #include "view/prefs/dialogue.hpp" #include "view/tools/merge.hpp" namespace CSMDoc { class Document; } namespace CS { class Editor : public QObject { Q_OBJECT Files::ConfigurationManager mCfgMgr; CSMPrefs::State mSettingsState; CSMDoc::DocumentManager mDocumentManager; CSVDoc::StartupDialogue mStartup; CSVDoc::NewGameDialogue mNewGame; CSVPrefs::Dialogue mSettings; CSVDoc::FileDialog mFileDialog; boost::filesystem::path mLocal; boost::filesystem::path mResources; boost::filesystem::path mPid; boost::interprocess::file_lock mLock; boost::filesystem::ofstream mPidFile; bool mFsStrict; CSVTools::Merge mMerge; CSVDoc::ViewManager* mViewManager; boost::filesystem::path mFileToLoad; Files::PathContainer mDataDirs; std::string mEncodingName; std::pair > readConfig(bool quiet=false); ///< \return data paths // not implemented Editor (const Editor&); Editor& operator= (const Editor&); public: Editor (int argc, char **argv); ~Editor (); bool makeIPCServer(); void connectToIPCServer(); int run(); ///< \return error status private slots: void createGame(); void createAddon(); void cancelCreateGame(); void cancelFileDialog(); void loadDocument(); void openFiles (const boost::filesystem::path &path, const std::vector &discoveredFiles = std::vector()); void createNewFile (const boost::filesystem::path& path); void createNewGame (const boost::filesystem::path& file); void showStartup(); void showSettings(); void documentAdded (CSMDoc::Document *document); void documentAboutToBeRemoved (CSMDoc::Document *document); void lastDocumentDeleted(); void mergeDocument (CSMDoc::Document *document); private: QString mIpcServerName; QLocalServer *mServer; QLocalSocket *mClientSocket; }; } #endif ================================================ FILE: apps/opencs/main.cpp ================================================ #include "editor.hpp" #include #include #include #include #include #include #include "model/doc/messages.hpp" #include "model/world/universalid.hpp" #ifdef Q_OS_MAC #include #endif Q_DECLARE_METATYPE (std::string) class Application : public QApplication { private: bool notify (QObject *receiver, QEvent *event) override { try { return QApplication::notify (receiver, event); } catch (const std::exception& exception) { Log(Debug::Error) << "An exception has been caught: " << exception.what(); } return false; } public: Application (int& argc, char *argv[]) : QApplication (argc, argv) {} }; int runApplication(int argc, char *argv[]) { #ifdef Q_OS_MAC setenv("OSG_GL_TEXTURE_STORAGE", "OFF", 0); #endif Q_INIT_RESOURCE (resources); qRegisterMetaType ("std::string"); qRegisterMetaType ("CSMWorld::UniversalId"); qRegisterMetaType ("CSMDoc::Message"); Application application (argc, argv); #ifdef Q_OS_MAC QDir dir(QCoreApplication::applicationDirPath()); QDir::setCurrent(dir.absolutePath()); #endif application.setWindowIcon (QIcon (":./openmw-cs.png")); CS::Editor editor(argc, argv); #ifdef __linux__ setlocale(LC_NUMERIC,"C"); #endif if(!editor.makeIPCServer()) { editor.connectToIPCServer(); return 0; } return editor.run(); } int main(int argc, char *argv[]) { return wrapApplication(&runApplication, argc, argv, "OpenMW-CS"); } ================================================ FILE: apps/opencs/model/doc/blacklist.cpp ================================================ #include "blacklist.hpp" #include #include bool CSMDoc::Blacklist::isBlacklisted (const CSMWorld::UniversalId& id) const { std::map >::const_iterator iter = mIds.find (id.getType()); if (iter==mIds.end()) return false; return std::binary_search (iter->second.begin(), iter->second.end(), Misc::StringUtils::lowerCase (id.getId())); } void CSMDoc::Blacklist::add (CSMWorld::UniversalId::Type type, const std::vector& ids) { std::vector& list = mIds[type]; size_t size = list.size(); list.resize (size+ids.size()); std::transform (ids.begin(), ids.end(), list.begin()+size, Misc::StringUtils::lowerCase); std::sort (list.begin(), list.end()); } ================================================ FILE: apps/opencs/model/doc/blacklist.hpp ================================================ #ifndef CSM_DOC_BLACKLIST_H #define CSM_DOC_BLACKLIST_H #include #include #include #include "../world/universalid.hpp" namespace CSMDoc { /// \brief ID blacklist sorted by UniversalId type class Blacklist { std::map > mIds; public: bool isBlacklisted (const CSMWorld::UniversalId& id) const; void add (CSMWorld::UniversalId::Type type, const std::vector& ids); }; } #endif ================================================ FILE: apps/opencs/model/doc/document.cpp ================================================ #include "document.hpp" #include #include #include #include "../world/defaultgmsts.hpp" #ifndef Q_MOC_RUN #include #endif #include void CSMDoc::Document::addGmsts() { for (size_t i=0; i < CSMWorld::DefaultGmsts::FloatCount; ++i) { ESM::GameSetting gmst; gmst.mId = CSMWorld::DefaultGmsts::Floats[i]; gmst.mValue.setType (ESM::VT_Float); gmst.mValue.setFloat (CSMWorld::DefaultGmsts::FloatsDefaultValues[i]); getData().getGmsts().add (gmst); } for (size_t i=0; i < CSMWorld::DefaultGmsts::IntCount; ++i) { ESM::GameSetting gmst; gmst.mId = CSMWorld::DefaultGmsts::Ints[i]; gmst.mValue.setType (ESM::VT_Int); gmst.mValue.setInteger (CSMWorld::DefaultGmsts::IntsDefaultValues[i]); getData().getGmsts().add (gmst); } for (size_t i=0; i < CSMWorld::DefaultGmsts::StringCount; ++i) { ESM::GameSetting gmst; gmst.mId = CSMWorld::DefaultGmsts::Strings[i]; gmst.mValue.setType (ESM::VT_String); gmst.mValue.setString (""); getData().getGmsts().add (gmst); } } void CSMDoc::Document::addOptionalGmsts() { for (size_t i=0; i < CSMWorld::DefaultGmsts::OptionalFloatCount; ++i) { ESM::GameSetting gmst; gmst.mId = CSMWorld::DefaultGmsts::OptionalFloats[i]; gmst.blank(); gmst.mValue.setType (ESM::VT_Float); addOptionalGmst (gmst); } for (size_t i=0; i < CSMWorld::DefaultGmsts::OptionalIntCount; ++i) { ESM::GameSetting gmst; gmst.mId = CSMWorld::DefaultGmsts::OptionalInts[i]; gmst.blank(); gmst.mValue.setType (ESM::VT_Int); addOptionalGmst (gmst); } for (size_t i=0; i < CSMWorld::DefaultGmsts::OptionalStringCount; ++i) { ESM::GameSetting gmst; gmst.mId = CSMWorld::DefaultGmsts::OptionalStrings[i]; gmst.blank(); gmst.mValue.setType (ESM::VT_String); gmst.mValue.setString (""); addOptionalGmst (gmst); } } void CSMDoc::Document::addOptionalGlobals() { static const char *sGlobals[] = { "DaysPassed", "PCWerewolf", "PCYear", 0 }; for (int i=0; sGlobals[i]; ++i) { ESM::Global global; global.mId = sGlobals[i]; global.blank(); global.mValue.setType (ESM::VT_Long); if (i==0) global.mValue.setInteger (1); // dayspassed starts counting at 1 addOptionalGlobal (global); } } void CSMDoc::Document::addOptionalMagicEffects() { for (int i=ESM::MagicEffect::SummonFabricant; i<=ESM::MagicEffect::SummonCreature05; ++i) { ESM::MagicEffect effect; effect.mIndex = i; effect.mId = ESM::MagicEffect::indexToId (i); effect.blank(); addOptionalMagicEffect (effect); } } void CSMDoc::Document::addOptionalGmst (const ESM::GameSetting& gmst) { if (getData().getGmsts().searchId (gmst.mId)==-1) { CSMWorld::Record record; record.mBase = gmst; record.mState = CSMWorld::RecordBase::State_BaseOnly; getData().getGmsts().appendRecord (record); } } void CSMDoc::Document::addOptionalGlobal (const ESM::Global& global) { if (getData().getGlobals().searchId (global.mId)==-1) { CSMWorld::Record record; record.mBase = global; record.mState = CSMWorld::RecordBase::State_BaseOnly; getData().getGlobals().appendRecord (record); } } void CSMDoc::Document::addOptionalMagicEffect (const ESM::MagicEffect& magicEffect) { if (getData().getMagicEffects().searchId (magicEffect.mId)==-1) { CSMWorld::Record record; record.mBase = magicEffect; record.mState = CSMWorld::RecordBase::State_BaseOnly; getData().getMagicEffects().appendRecord (record); } } void CSMDoc::Document::createBase() { static const char *sGlobals[] = { "Day", "DaysPassed", "GameHour", "Month", "PCRace", "PCVampire", "PCWerewolf", "PCYear", 0 }; for (int i=0; sGlobals[i]; ++i) { ESM::Global record; record.mId = sGlobals[i]; record.mValue.setType (i==2 ? ESM::VT_Float : ESM::VT_Long); if (i==0 || i==1) record.mValue.setInteger (1); getData().getGlobals().add (record); } addGmsts(); for (int i=0; i<27; ++i) { ESM::Skill record; record.mIndex = i; record.mId = ESM::Skill::indexToId (record.mIndex); record.blank(); getData().getSkills().add (record); } static const char *sVoice[] = { "Intruder", "Attack", "Hello", "Thief", "Alarm", "Idle", "Flee", "Hit", 0 }; for (int i=0; sVoice[i]; ++i) { ESM::Dialogue record; record.mId = sVoice[i]; record.mType = ESM::Dialogue::Voice; record.blank(); getData().getTopics().add (record); } static const char *sGreetings[] = { "Greeting 0", "Greeting 1", "Greeting 2", "Greeting 3", "Greeting 4", "Greeting 5", "Greeting 6", "Greeting 7", "Greeting 8", "Greeting 9", 0 }; for (int i=0; sGreetings[i]; ++i) { ESM::Dialogue record; record.mId = sGreetings[i]; record.mType = ESM::Dialogue::Greeting; record.blank(); getData().getTopics().add (record); } static const char *sPersuasion[] = { "Intimidate Success", "Intimidate Fail", "Service Refusal", "Admire Success", "Taunt Success", "Bribe Success", "Info Refusal", "Admire Fail", "Taunt Fail", "Bribe Fail", 0 }; for (int i=0; sPersuasion[i]; ++i) { ESM::Dialogue record; record.mId = sPersuasion[i]; record.mType = ESM::Dialogue::Persuasion; record.blank(); getData().getTopics().add (record); } for (int i=0; i& files,bool new_, const boost::filesystem::path& savePath, const boost::filesystem::path& resDir, ToUTF8::FromType encoding, const std::vector& blacklistedScripts, bool fsStrict, const Files::PathContainer& dataPaths, const std::vector& archives) : mSavePath (savePath), mContentFiles (files), mNew (new_), mData (encoding, fsStrict, dataPaths, archives, resDir), mTools (*this, encoding), mProjectPath ((configuration.getUserDataPath() / "projects") / (savePath.filename().string() + ".project")), mSavingOperation (*this, mProjectPath, encoding), mSaving (&mSavingOperation), mResDir(resDir), mRunner (mProjectPath), mDirty (false), mIdCompletionManager(mData) { if (mContentFiles.empty()) throw std::runtime_error ("Empty content file sequence"); if (mNew || !boost::filesystem::exists (mProjectPath)) { boost::filesystem::path filtersPath (configuration.getUserDataPath() / "defaultfilters"); boost::filesystem::ofstream destination(mProjectPath, std::ios::out | std::ios::binary); if (!destination.is_open()) throw std::runtime_error("Can not create project file: " + mProjectPath.string()); destination.exceptions(std::ios::failbit | std::ios::badbit); if (!boost::filesystem::exists (filtersPath)) filtersPath = mResDir / "defaultfilters"; boost::filesystem::ifstream source(filtersPath, std::ios::in | std::ios::binary); if (!source.is_open()) throw std::runtime_error("Can not read filters file: " + filtersPath.string()); source.exceptions(std::ios::failbit | std::ios::badbit); destination << source.rdbuf(); } if (mNew) { if (mContentFiles.size()==1) createBase(); } mBlacklist.add (CSMWorld::UniversalId::Type_Script, blacklistedScripts); addOptionalGmsts(); addOptionalGlobals(); addOptionalMagicEffects(); connect (&mUndoStack, SIGNAL (cleanChanged (bool)), this, SLOT (modificationStateChanged (bool))); connect (&mTools, SIGNAL (progress (int, int, int)), this, SLOT (progress (int, int, int))); connect (&mTools, SIGNAL (done (int, bool)), this, SIGNAL (operationDone (int, bool))); connect (&mTools, SIGNAL (done (int, bool)), this, SLOT (operationDone2 (int, bool))); connect (&mTools, SIGNAL (mergeDone (CSMDoc::Document*)), this, SIGNAL (mergeDone (CSMDoc::Document*))); connect (&mSaving, SIGNAL (progress (int, int, int)), this, SLOT (progress (int, int, int))); connect (&mSaving, SIGNAL (done (int, bool)), this, SLOT (operationDone2 (int, bool))); connect ( &mSaving, SIGNAL (reportMessage (const CSMDoc::Message&, int)), this, SLOT (reportMessage (const CSMDoc::Message&, int))); connect (&mRunner, SIGNAL (runStateChanged()), this, SLOT (runStateChanged())); } CSMDoc::Document::~Document() { } QUndoStack& CSMDoc::Document::getUndoStack() { return mUndoStack; } int CSMDoc::Document::getState() const { int state = 0; if (!mUndoStack.isClean() || mDirty) state |= State_Modified; if (mSaving.isRunning()) state |= State_Locked | State_Saving | State_Operation; if (mRunner.isRunning()) state |= State_Locked | State_Running; if (int operations = mTools.getRunningOperations()) state |= State_Locked | State_Operation | operations; return state; } const boost::filesystem::path& CSMDoc::Document::getResourceDir() const { return mResDir; } const boost::filesystem::path& CSMDoc::Document::getSavePath() const { return mSavePath; } const boost::filesystem::path& CSMDoc::Document::getProjectPath() const { return mProjectPath; } const std::vector& CSMDoc::Document::getContentFiles() const { return mContentFiles; } bool CSMDoc::Document::isNew() const { return mNew; } void CSMDoc::Document::save() { if (mSaving.isRunning()) throw std::logic_error ( "Failed to initiate save, because a save operation is already running."); mSaving.start(); emit stateChanged (getState(), this); } CSMWorld::UniversalId CSMDoc::Document::verify (const CSMWorld::UniversalId& reportId) { CSMWorld::UniversalId id = mTools.runVerifier (reportId); emit stateChanged (getState(), this); return id; } CSMWorld::UniversalId CSMDoc::Document::newSearch() { return mTools.newSearch(); } void CSMDoc::Document::runSearch (const CSMWorld::UniversalId& searchId, const CSMTools::Search& search) { mTools.runSearch (searchId, search); emit stateChanged (getState(), this); } void CSMDoc::Document::runMerge (std::unique_ptr target) { mTools.runMerge (std::move(target)); emit stateChanged (getState(), this); } void CSMDoc::Document::abortOperation (int type) { if (type==State_Saving) mSaving.abort(); else mTools.abortOperation (type); } void CSMDoc::Document::modificationStateChanged (bool clean) { emit stateChanged (getState(), this); } void CSMDoc::Document::reportMessage (const CSMDoc::Message& message, int type) { /// \todo find a better way to get these messages to the user. Log(Debug::Info) << message.mMessage; } void CSMDoc::Document::operationDone2 (int type, bool failed) { if (type==CSMDoc::State_Saving && !failed) mDirty = false; emit stateChanged (getState(), this); } const CSMWorld::Data& CSMDoc::Document::getData() const { return mData; } CSMWorld::Data& CSMDoc::Document::getData() { return mData; } CSMTools::ReportModel *CSMDoc::Document::getReport (const CSMWorld::UniversalId& id) { return mTools.getReport (id); } bool CSMDoc::Document::isBlacklisted (const CSMWorld::UniversalId& id) const { return mBlacklist.isBlacklisted (id); } void CSMDoc::Document::startRunning (const std::string& profile, const std::string& startupInstruction) { std::vector contentFiles; for (std::vector::const_iterator iter (mContentFiles.begin()); iter!=mContentFiles.end(); ++iter) contentFiles.push_back (iter->filename().string()); mRunner.configure (getData().getDebugProfiles().getRecord (profile).get(), contentFiles, startupInstruction); int state = getState(); if (state & State_Modified) { // need to save first mRunner.start (true); new SaveWatcher (&mRunner, &mSaving); // no, that is not a memory leak. Qt is weird. if (!(state & State_Saving)) save(); } else mRunner.start(); } void CSMDoc::Document::stopRunning() { mRunner.stop(); } QTextDocument *CSMDoc::Document::getRunLog() { return mRunner.getLog(); } void CSMDoc::Document::runStateChanged() { emit stateChanged (getState(), this); } void CSMDoc::Document::progress (int current, int max, int type) { emit progress (current, max, type, 1, this); } CSMWorld::IdCompletionManager &CSMDoc::Document::getIdCompletionManager() { return mIdCompletionManager; } void CSMDoc::Document::flagAsDirty() { mDirty = true; } ================================================ FILE: apps/opencs/model/doc/document.hpp ================================================ #ifndef CSM_DOC_DOCUMENT_H #define CSM_DOC_DOCUMENT_H #include #include #include #include #include #include #include #include "../world/data.hpp" #include "../world/idcompletionmanager.hpp" #include "../tools/tools.hpp" #include "state.hpp" #include "saving.hpp" #include "blacklist.hpp" #include "runner.hpp" #include "operationholder.hpp" class QAbstractItemModel; namespace Fallback { class Map; } namespace VFS { class Manager; } namespace ESM { struct GameSetting; struct Global; struct MagicEffect; } namespace Files { struct ConfigurationManager; } namespace CSMWorld { class ResourcesManager; } namespace CSMDoc { class Document : public QObject { Q_OBJECT private: boost::filesystem::path mSavePath; std::vector mContentFiles; bool mNew; CSMWorld::Data mData; CSMTools::Tools mTools; boost::filesystem::path mProjectPath; Saving mSavingOperation; OperationHolder mSaving; boost::filesystem::path mResDir; Blacklist mBlacklist; Runner mRunner; bool mDirty; CSMWorld::IdCompletionManager mIdCompletionManager; // It is important that the undo stack is declared last, because on desctruction it fires a signal, that is connected to a slot, that is // using other member variables. Unfortunately this connection is cut only in the QObject destructor, which is way too late. QUndoStack mUndoStack; // not implemented Document (const Document&); Document& operator= (const Document&); void createBase(); void addGmsts(); void addOptionalGmsts(); void addOptionalGlobals(); void addOptionalMagicEffects(); void addOptionalGmst (const ESM::GameSetting& gmst); void addOptionalGlobal (const ESM::Global& global); void addOptionalMagicEffect (const ESM::MagicEffect& effect); public: Document (const Files::ConfigurationManager& configuration, const std::vector< boost::filesystem::path >& files, bool new_, const boost::filesystem::path& savePath, const boost::filesystem::path& resDir, ToUTF8::FromType encoding, const std::vector& blacklistedScripts, bool fsStrict, const Files::PathContainer& dataPaths, const std::vector& archives); ~Document(); QUndoStack& getUndoStack(); int getState() const; const boost::filesystem::path& getResourceDir() const; const boost::filesystem::path& getSavePath() const; const boost::filesystem::path& getProjectPath() const; const std::vector& getContentFiles() const; ///< \attention The last element in this collection is the file that is being edited, /// but with its original path instead of the save path. bool isNew() const; ///< Is this a newly created content file? void save(); CSMWorld::UniversalId verify (const CSMWorld::UniversalId& reportId = CSMWorld::UniversalId()); CSMWorld::UniversalId newSearch(); void runSearch (const CSMWorld::UniversalId& searchId, const CSMTools::Search& search); void runMerge (std::unique_ptr target); void abortOperation (int type); const CSMWorld::Data& getData() const; CSMWorld::Data& getData(); CSMTools::ReportModel *getReport (const CSMWorld::UniversalId& id); ///< The ownership of the returned report is not transferred. bool isBlacklisted (const CSMWorld::UniversalId& id) const; void startRunning (const std::string& profile, const std::string& startupInstruction = ""); void stopRunning(); QTextDocument *getRunLog(); CSMWorld::IdCompletionManager &getIdCompletionManager(); void flagAsDirty(); signals: void stateChanged (int state, CSMDoc::Document *document); void progress (int current, int max, int type, int threads, CSMDoc::Document *document); /// \attention When this signal is emitted, *this hands over the ownership of the /// document. This signal must be handled to avoid a leak. void mergeDone (CSMDoc::Document *document); void operationDone (int type, bool failed); private slots: void modificationStateChanged (bool clean); void reportMessage (const CSMDoc::Message& message, int type); void operationDone2 (int type, bool failed); void runStateChanged(); public slots: void progress (int current, int max, int type); }; } #endif ================================================ FILE: apps/opencs/model/doc/documentmanager.cpp ================================================ #include "documentmanager.hpp" #include #ifndef Q_MOC_RUN #include #endif #include "document.hpp" CSMDoc::DocumentManager::DocumentManager (const Files::ConfigurationManager& configuration) : mConfiguration (configuration), mEncoding (ToUTF8::WINDOWS_1252), mFsStrict(false) { boost::filesystem::path projectPath = configuration.getUserDataPath() / "projects"; if (!boost::filesystem::is_directory (projectPath)) boost::filesystem::create_directories (projectPath); mLoader.moveToThread (&mLoaderThread); mLoaderThread.start(); connect (&mLoader, SIGNAL (documentLoaded (Document *)), this, SLOT (documentLoaded (Document *))); connect (&mLoader, SIGNAL (documentNotLoaded (Document *, const std::string&)), this, SLOT (documentNotLoaded (Document *, const std::string&))); connect (this, SIGNAL (loadRequest (CSMDoc::Document *)), &mLoader, SLOT (loadDocument (CSMDoc::Document *))); connect (&mLoader, SIGNAL (nextStage (CSMDoc::Document *, const std::string&, int)), this, SIGNAL (nextStage (CSMDoc::Document *, const std::string&, int))); connect (&mLoader, SIGNAL (nextRecord (CSMDoc::Document *, int)), this, SIGNAL (nextRecord (CSMDoc::Document *, int))); connect (this, SIGNAL (cancelLoading (CSMDoc::Document *)), &mLoader, SLOT (abortLoading (CSMDoc::Document *))); connect (&mLoader, SIGNAL (loadMessage (CSMDoc::Document *, const std::string&)), this, SIGNAL (loadMessage (CSMDoc::Document *, const std::string&))); } CSMDoc::DocumentManager::~DocumentManager() { mLoaderThread.quit(); mLoader.stop(); mLoader.hasThingsToDo().wakeAll(); mLoaderThread.wait(); for (std::vector::iterator iter (mDocuments.begin()); iter!=mDocuments.end(); ++iter) delete *iter; } bool CSMDoc::DocumentManager::isEmpty() { return mDocuments.empty(); } void CSMDoc::DocumentManager::addDocument (const std::vector& files, const boost::filesystem::path& savePath, bool new_) { Document *document = makeDocument (files, savePath, new_); insertDocument (document); } CSMDoc::Document *CSMDoc::DocumentManager::makeDocument ( const std::vector< boost::filesystem::path >& files, const boost::filesystem::path& savePath, bool new_) { return new Document (mConfiguration, files, new_, savePath, mResDir, mEncoding, mBlacklistedScripts, mFsStrict, mDataPaths, mArchives); } void CSMDoc::DocumentManager::insertDocument (CSMDoc::Document *document) { mDocuments.push_back (document); connect (document, SIGNAL (mergeDone (CSMDoc::Document*)), this, SLOT (insertDocument (CSMDoc::Document*))); emit loadRequest (document); mLoader.hasThingsToDo().wakeAll(); } void CSMDoc::DocumentManager::removeDocument (CSMDoc::Document *document) { std::vector::iterator iter = std::find (mDocuments.begin(), mDocuments.end(), document); if (iter==mDocuments.end()) throw std::runtime_error ("removing invalid document"); emit documentAboutToBeRemoved (document); mDocuments.erase (iter); document->deleteLater(); if (mDocuments.empty()) emit lastDocumentDeleted(); } void CSMDoc::DocumentManager::setResourceDir (const boost::filesystem::path& parResDir) { mResDir = boost::filesystem::system_complete(parResDir); } void CSMDoc::DocumentManager::setEncoding (ToUTF8::FromType encoding) { mEncoding = encoding; } void CSMDoc::DocumentManager::setBlacklistedScripts (const std::vector& scriptIds) { mBlacklistedScripts = scriptIds; } void CSMDoc::DocumentManager::documentLoaded (Document *document) { emit documentAdded (document); emit loadingStopped (document, true, ""); } void CSMDoc::DocumentManager::documentNotLoaded (Document *document, const std::string& error) { emit loadingStopped (document, false, error); if (error.empty()) // do not remove the document yet, if we have an error removeDocument (document); } void CSMDoc::DocumentManager::setFileData(bool strict, const Files::PathContainer& dataPaths, const std::vector& archives) { mFsStrict = strict; mDataPaths = dataPaths; mArchives = archives; } ================================================ FILE: apps/opencs/model/doc/documentmanager.hpp ================================================ #ifndef CSM_DOC_DOCUMENTMGR_H #define CSM_DOC_DOCUMENTMGR_H #include #include #include #include #include #include #include #include #include "loader.hpp" namespace VFS { class Manager; } namespace Files { struct ConfigurationManager; } namespace CSMDoc { class Document; class DocumentManager : public QObject { Q_OBJECT std::vector mDocuments; const Files::ConfigurationManager& mConfiguration; QThread mLoaderThread; Loader mLoader; ToUTF8::FromType mEncoding; std::vector mBlacklistedScripts; boost::filesystem::path mResDir; bool mFsStrict; Files::PathContainer mDataPaths; std::vector mArchives; DocumentManager (const DocumentManager&); DocumentManager& operator= (const DocumentManager&); public: DocumentManager (const Files::ConfigurationManager& configuration); ~DocumentManager(); void addDocument (const std::vector< boost::filesystem::path >& files, const boost::filesystem::path& savePath, bool new_); ///< \param new_ Do not load the last content file in \a files and instead create in an /// appropriate way. /// Create a new document. The ownership of the created document is transferred to /// the calling function. The DocumentManager does not manage it. Loading has not /// taken place at the point when the document is returned. /// /// \param new_ Do not load the last content file in \a files and instead create in an /// appropriate way. Document *makeDocument (const std::vector< boost::filesystem::path >& files, const boost::filesystem::path& savePath, bool new_); void setResourceDir (const boost::filesystem::path& parResDir); void setEncoding (ToUTF8::FromType encoding); void setBlacklistedScripts (const std::vector& scriptIds); /// Sets the file data that gets passed to newly created documents. void setFileData(bool strict, const Files::PathContainer& dataPaths, const std::vector& archives); bool isEmpty(); private slots: void documentLoaded (Document *document); ///< The ownership of \a document is not transferred. void documentNotLoaded (Document *document, const std::string& error); ///< Document load has been interrupted either because of a call to abortLoading /// or a problem during loading). In the former case error will be an empty string. public slots: void removeDocument (CSMDoc::Document *document); ///< Emits the lastDocumentDeleted signal, if applicable. /// Hand over document to *this. The ownership is transferred. The DocumentManager /// will initiate the load procedure, if necessary void insertDocument (CSMDoc::Document *document); signals: void documentAdded (CSMDoc::Document *document); void documentAboutToBeRemoved (CSMDoc::Document *document); void loadRequest (CSMDoc::Document *document); void lastDocumentDeleted(); void loadingStopped (CSMDoc::Document *document, bool completed, const std::string& error); void nextStage (CSMDoc::Document *document, const std::string& name, int totalRecords); void nextRecord (CSMDoc::Document *document, int records); void cancelLoading (CSMDoc::Document *document); void loadMessage (CSMDoc::Document *document, const std::string& message); }; } #endif ================================================ FILE: apps/opencs/model/doc/loader.cpp ================================================ #include "loader.hpp" #include #include "../tools/reportmodel.hpp" #include "document.hpp" CSMDoc::Loader::Stage::Stage() : mFile (0), mRecordsLoaded (0), mRecordsLeft (false) {} CSMDoc::Loader::Loader() : mShouldStop(false) { mTimer = new QTimer (this); connect (mTimer, SIGNAL (timeout()), this, SLOT (load())); mTimer->start(); } QWaitCondition& CSMDoc::Loader::hasThingsToDo() { return mThingsToDo; } void CSMDoc::Loader::stop() { mShouldStop = true; } void CSMDoc::Loader::load() { if (mDocuments.empty()) { mMutex.lock(); mThingsToDo.wait (&mMutex); mMutex.unlock(); if (mShouldStop) mTimer->stop(); return; } std::vector >::iterator iter = mDocuments.begin(); Document *document = iter->first; int size = static_cast (document->getContentFiles().size()); int editedIndex = size-1; // index of the file to be edited/created if (document->isNew()) --size; bool done = false; try { if (iter->second.mRecordsLeft) { Messages messages (Message::Severity_Error); const int batchingSize = 50; for (int i=0; igetData().continueLoading (messages)) { iter->second.mRecordsLeft = false; break; } else ++(iter->second.mRecordsLoaded); CSMWorld::UniversalId log (CSMWorld::UniversalId::Type_LoadErrorLog, 0); { // silence a g++ warning for (CSMDoc::Messages::Iterator messageIter (messages.begin()); messageIter!=messages.end(); ++messageIter) { document->getReport (log)->add (*messageIter); emit loadMessage (document, messageIter->mMessage); } } emit nextRecord (document, iter->second.mRecordsLoaded); return; } if (iter->second.mFilegetContentFiles()[iter->second.mFile]; int steps = document->getData().startLoading (path, iter->second.mFile!=editedIndex, false); iter->second.mRecordsLeft = true; iter->second.mRecordsLoaded = 0; emit nextStage (document, path.filename().string(), steps); } else if (iter->second.mFile==size) { int steps = document->getData().startLoading (document->getProjectPath(), false, true); iter->second.mRecordsLeft = true; iter->second.mRecordsLoaded = 0; emit nextStage (document, "Project File", steps); } else { done = true; } ++(iter->second.mFile); } catch (const std::exception& e) { mDocuments.erase (iter); emit documentNotLoaded (document, e.what()); return; } if (done) { mDocuments.erase (iter); emit documentLoaded (document); } } void CSMDoc::Loader::loadDocument (CSMDoc::Document *document) { mDocuments.emplace_back (document, Stage()); } void CSMDoc::Loader::abortLoading (CSMDoc::Document *document) { for (std::vector >::iterator iter = mDocuments.begin(); iter!=mDocuments.end(); ++iter) { if (iter->first==document) { mDocuments.erase (iter); emit documentNotLoaded (document, ""); break; } } } ================================================ FILE: apps/opencs/model/doc/loader.hpp ================================================ #ifndef CSM_DOC_LOADER_H #define CSM_DOC_LOADER_H #include #include #include #include #include namespace CSMDoc { class Document; class Loader : public QObject { Q_OBJECT struct Stage { int mFile; int mRecordsLoaded; bool mRecordsLeft; Stage(); }; QMutex mMutex; QWaitCondition mThingsToDo; std::vector > mDocuments; QTimer* mTimer; bool mShouldStop; public: Loader(); QWaitCondition& hasThingsToDo(); void stop(); private slots: void load(); public slots: void loadDocument (CSMDoc::Document *document); ///< The ownership of \a document is not transferred. void abortLoading (CSMDoc::Document *document); ///< Abort loading \a docuemnt (ignored if \a document has already finished being /// loaded). Will result in a documentNotLoaded signal, once the Loader has finished /// cleaning up. signals: void documentLoaded (Document *document); ///< The ownership of \a document is not transferred. void documentNotLoaded (Document *document, const std::string& error); ///< Document load has been interrupted either because of a call to abortLoading /// or a problem during loading). In the former case error will be an empty string. void nextStage (CSMDoc::Document *document, const std::string& name, int totalRecords); void nextRecord (CSMDoc::Document *document, int records); ///< \note This signal is only given once per group of records. The group size is /// approximately the total number of records divided by the steps value of the /// previous nextStage signal. void loadMessage (CSMDoc::Document *document, const std::string& message); ///< Non-critical load error or warning }; } #endif ================================================ FILE: apps/opencs/model/doc/messages.cpp ================================================ #include "messages.hpp" CSMDoc::Message::Message() : mSeverity(Severity_Default){} CSMDoc::Message::Message (const CSMWorld::UniversalId& id, const std::string& message, const std::string& hint, Severity severity) : mId (id), mMessage (message), mHint (hint), mSeverity (severity) {} std::string CSMDoc::Message::toString (Severity severity) { switch (severity) { case CSMDoc::Message::Severity_Info: return "Information"; case CSMDoc::Message::Severity_Warning: return "Warning"; case CSMDoc::Message::Severity_Error: return "Error"; case CSMDoc::Message::Severity_SeriousError: return "Serious Error"; case CSMDoc::Message::Severity_Default: break; } return ""; } CSMDoc::Messages::Messages (Message::Severity default_) : mDefault (default_) {} void CSMDoc::Messages::add (const CSMWorld::UniversalId& id, const std::string& message, const std::string& hint, Message::Severity severity) { if (severity==Message::Severity_Default) severity = mDefault; mMessages.push_back (Message (id, message, hint, severity)); } CSMDoc::Messages::Iterator CSMDoc::Messages::begin() const { return mMessages.begin(); } CSMDoc::Messages::Iterator CSMDoc::Messages::end() const { return mMessages.end(); } ================================================ FILE: apps/opencs/model/doc/messages.hpp ================================================ #ifndef CSM_DOC_MESSAGES_H #define CSM_DOC_MESSAGES_H #include #include #include #include "../world/universalid.hpp" namespace CSMDoc { struct Message { enum Severity { Severity_Info = 0, // no problem Severity_Warning = 1, // a potential problem, but we are probably fine Severity_Error = 2, // an error; we are not fine Severity_SeriousError = 3, // an error so bad we can't even be sure if we are // reporting it correctly Severity_Default = 4 }; CSMWorld::UniversalId mId; std::string mMessage; std::string mHint; Severity mSeverity; Message(); Message (const CSMWorld::UniversalId& id, const std::string& message, const std::string& hint, Severity severity); static std::string toString (Severity severity); }; class Messages { public: typedef std::vector Collection; typedef Collection::const_iterator Iterator; private: Collection mMessages; Message::Severity mDefault; public: Messages (Message::Severity default_); void add (const CSMWorld::UniversalId& id, const std::string& message, const std::string& hint = "", Message::Severity severity = Message::Severity_Default); Iterator begin() const; Iterator end() const; }; } Q_DECLARE_METATYPE (CSMDoc::Message) #endif ================================================ FILE: apps/opencs/model/doc/operation.cpp ================================================ #include "operation.hpp" #include #include #include #include "../world/universalid.hpp" #include "stage.hpp" void CSMDoc::Operation::prepareStages() { mCurrentStage = mStages.begin(); mCurrentStep = 0; mCurrentStepTotal = 0; mTotalSteps = 0; mError = false; for (std::vector >::iterator iter (mStages.begin()); iter!=mStages.end(); ++iter) { iter->second = iter->first->setup(); mTotalSteps += iter->second; } } CSMDoc::Operation::Operation (int type, bool ordered, bool finalAlways) : mType (type), mStages(std::vector >()), mCurrentStage(mStages.begin()), mCurrentStep(0), mCurrentStepTotal(0), mTotalSteps(0), mOrdered (ordered), mFinalAlways (finalAlways), mError(false), mConnected (false), mPrepared (false), mDefaultSeverity (Message::Severity_Error) { mTimer = new QTimer (this); } CSMDoc::Operation::~Operation() { for (std::vector >::iterator iter (mStages.begin()); iter!=mStages.end(); ++iter) delete iter->first; } void CSMDoc::Operation::run() { mTimer->stop(); if (!mConnected) { connect (mTimer, SIGNAL (timeout()), this, SLOT (executeStage())); mConnected = true; } mPrepared = false; mTimer->start (0); } void CSMDoc::Operation::appendStage (Stage *stage) { mStages.emplace_back (stage, 0); } void CSMDoc::Operation::setDefaultSeverity (Message::Severity severity) { mDefaultSeverity = severity; } bool CSMDoc::Operation::hasError() const { return mError; } void CSMDoc::Operation::abort() { if (!mTimer->isActive()) return; mError = true; if (mFinalAlways) { if (mStages.begin()!=mStages.end() && mCurrentStage!=--mStages.end()) { mCurrentStep = 0; mCurrentStage = --mStages.end(); } } else mCurrentStage = mStages.end(); } void CSMDoc::Operation::executeStage() { if (!mPrepared) { prepareStages(); mPrepared = true; } Messages messages (mDefaultSeverity); while (mCurrentStage!=mStages.end()) { if (mCurrentStep>=mCurrentStage->second) { mCurrentStep = 0; ++mCurrentStage; } else { try { mCurrentStage->first->perform (mCurrentStep++, messages); } catch (const std::exception& e) { emit reportMessage (Message (CSMWorld::UniversalId(), e.what(), "", Message::Severity_SeriousError), mType); abort(); } ++mCurrentStepTotal; break; } } emit progress (mCurrentStepTotal, mTotalSteps ? mTotalSteps : 1, mType); for (Messages::Iterator iter (messages.begin()); iter!=messages.end(); ++iter) emit reportMessage (*iter, mType); if (mCurrentStage==mStages.end()) operationDone(); } void CSMDoc::Operation::operationDone() { mTimer->stop(); emit done (mType, mError); } ================================================ FILE: apps/opencs/model/doc/operation.hpp ================================================ #ifndef CSM_DOC_OPERATION_H #define CSM_DOC_OPERATION_H #include #include #include #include #include #include "messages.hpp" namespace CSMWorld { class UniversalId; } namespace CSMDoc { class Stage; class Operation : public QObject { Q_OBJECT int mType; std::vector > mStages; // stage, number of steps std::vector >::iterator mCurrentStage; int mCurrentStep; int mCurrentStepTotal; int mTotalSteps; int mOrdered; bool mFinalAlways; bool mError; bool mConnected; QTimer *mTimer; bool mPrepared; Message::Severity mDefaultSeverity; void prepareStages(); public: Operation (int type, bool ordered, bool finalAlways = false); ///< \param ordered Stages must be executed in the given order. /// \param finalAlways Execute last stage even if an error occurred during earlier stages. virtual ~Operation(); void appendStage (Stage *stage); ///< The ownership of \a stage is transferred to *this. /// /// \attention Do no call this function while this Operation is running. /// \attention Do no call this function while this Operation is running. void setDefaultSeverity (Message::Severity severity); bool hasError() const; signals: void progress (int current, int max, int type); void reportMessage (const CSMDoc::Message& message, int type); void done (int type, bool failed); public slots: void abort(); void run(); private slots: void executeStage(); protected slots: virtual void operationDone(); }; } #endif ================================================ FILE: apps/opencs/model/doc/operationholder.cpp ================================================ #include "operationholder.hpp" #include "operation.hpp" CSMDoc::OperationHolder::OperationHolder (Operation *operation) : mOperation(nullptr) , mRunning (false) { if (operation) setOperation (operation); } void CSMDoc::OperationHolder::setOperation (Operation *operation) { mOperation = operation; mOperation->moveToThread (&mThread); connect ( mOperation, SIGNAL (progress (int, int, int)), this, SIGNAL (progress (int, int, int))); connect ( mOperation, SIGNAL (reportMessage (const CSMDoc::Message&, int)), this, SIGNAL (reportMessage (const CSMDoc::Message&, int))); connect ( mOperation, SIGNAL (done (int, bool)), this, SLOT (doneSlot (int, bool))); connect (this, SIGNAL (abortSignal()), mOperation, SLOT (abort())); connect (&mThread, SIGNAL (started()), mOperation, SLOT (run())); } bool CSMDoc::OperationHolder::isRunning() const { return mRunning; } void CSMDoc::OperationHolder::start() { mRunning = true; mThread.start(); } void CSMDoc::OperationHolder::abort() { mRunning = false; emit abortSignal(); } void CSMDoc::OperationHolder::abortAndWait() { if (mRunning) { mThread.quit(); mThread.wait(); } } void CSMDoc::OperationHolder::doneSlot (int type, bool failed) { mRunning = false; mThread.quit(); emit done (type, failed); } ================================================ FILE: apps/opencs/model/doc/operationholder.hpp ================================================ #ifndef CSM_DOC_OPERATIONHOLDER_H #define CSM_DOC_OPERATIONHOLDER_H #include #include #include "messages.hpp" namespace CSMWorld { class UniversalId; } namespace CSMDoc { class Operation; class OperationHolder : public QObject { Q_OBJECT QThread mThread; Operation *mOperation; bool mRunning; public: OperationHolder (Operation *operation = nullptr); void setOperation (Operation *operation); bool isRunning() const; void start(); void abort(); // Abort and wait until thread has finished. void abortAndWait(); private slots: void doneSlot (int type, bool failed); signals: void progress (int current, int max, int type); void reportMessage (const CSMDoc::Message& message, int type); void done (int type, bool failed); void abortSignal(); }; } #endif ================================================ FILE: apps/opencs/model/doc/runner.cpp ================================================ #include "runner.hpp" #include #include #include #include #include "operationholder.hpp" CSMDoc::Runner::Runner (const boost::filesystem::path& projectPath) : mRunning (false), mStartup (nullptr), mProjectPath (projectPath) { connect (&mProcess, SIGNAL (finished (int, QProcess::ExitStatus)), this, SLOT (finished (int, QProcess::ExitStatus))); connect (&mProcess, SIGNAL (readyReadStandardOutput()), this, SLOT (readyReadStandardOutput())); mProcess.setProcessChannelMode (QProcess::MergedChannels); mProfile.blank(); } CSMDoc::Runner::~Runner() { if (mRunning) { disconnect (&mProcess, nullptr, this, nullptr); mProcess.kill(); mProcess.waitForFinished(); } } void CSMDoc::Runner::start (bool delayed) { if (mStartup) { delete mStartup; mStartup = nullptr; } if (!delayed) { mLog.clear(); QString path = "openmw"; #ifdef Q_OS_WIN path.append(QString(".exe")); #elif defined(Q_OS_MAC) QDir dir(QCoreApplication::applicationDirPath()); dir.cdUp(); dir.cdUp(); dir.cdUp(); path = dir.absoluteFilePath(path.prepend("OpenMW.app/Contents/MacOS/")); #else path.prepend(QString("./")); #endif mStartup = new QTemporaryFile (this); mStartup->open(); { QTextStream stream (mStartup); if (!mStartupInstruction.empty()) stream << QString::fromUtf8 (mStartupInstruction.c_str()) << '\n'; stream << QString::fromUtf8 (mProfile.mScriptText.c_str()); } mStartup->close(); QStringList arguments; arguments << "--skip-menu"; if (mProfile.mFlags & ESM::DebugProfile::Flag_BypassNewGame) arguments << "--new-game=0"; else arguments << "--new-game=1"; arguments << ("--script-run="+mStartup->fileName());; arguments << QString::fromUtf8 (("--data=\""+mProjectPath.parent_path().string()+"\"").c_str()); arguments << "--replace=content"; for (std::vector::const_iterator iter (mContentFiles.begin()); iter!=mContentFiles.end(); ++iter) { arguments << QString::fromUtf8 (("--content="+*iter).c_str()); } arguments << QString::fromUtf8 (("--content="+mProjectPath.filename().string()).c_str()); mProcess.start (path, arguments); } mRunning = true; emit runStateChanged(); } void CSMDoc::Runner::stop() { delete mStartup; mStartup = nullptr; if (mProcess.state()==QProcess::NotRunning) { mRunning = false; emit runStateChanged(); } else mProcess.kill(); } bool CSMDoc::Runner::isRunning() const { return mRunning; } void CSMDoc::Runner::configure (const ESM::DebugProfile& profile, const std::vector& contentFiles, const std::string& startupInstruction) { mProfile = profile; mContentFiles = contentFiles; mStartupInstruction = startupInstruction; } void CSMDoc::Runner::finished (int exitCode, QProcess::ExitStatus exitStatus) { mRunning = false; emit runStateChanged(); } QTextDocument *CSMDoc::Runner::getLog() { return &mLog; } void CSMDoc::Runner::readyReadStandardOutput() { mLog.setPlainText ( mLog.toPlainText() + QString::fromUtf8 (mProcess.readAllStandardOutput())); } CSMDoc::SaveWatcher::SaveWatcher (Runner *runner, OperationHolder *operation) : QObject (runner), mRunner (runner) { connect (operation, SIGNAL (done (int, bool)), this, SLOT (saveDone (int, bool))); } void CSMDoc::SaveWatcher::saveDone (int type, bool failed) { if (failed) mRunner->stop(); else mRunner->start(); deleteLater(); } ================================================ FILE: apps/opencs/model/doc/runner.hpp ================================================ #ifndef CSM_DOC_RUNNER_H #define CSM_DOC_RUNNER_H #include #include #include #include #include #include #include class QTemporaryFile; namespace CSMDoc { class OperationHolder; class Runner : public QObject { Q_OBJECT QProcess mProcess; bool mRunning; ESM::DebugProfile mProfile; std::vector mContentFiles; std::string mStartupInstruction; QTemporaryFile *mStartup; QTextDocument mLog; boost::filesystem::path mProjectPath; public: Runner (const boost::filesystem::path& projectPath); ~Runner(); /// \param delayed Flag as running but do not start the OpenMW process yet (the /// process must be started by another call of start with delayed==false) void start (bool delayed = false); void stop(); /// \note Running state is entered when the start function is called. This /// is not necessarily identical to the moment the child process is started. bool isRunning() const; void configure (const ESM::DebugProfile& profile, const std::vector& contentFiles, const std::string& startupInstruction); QTextDocument *getLog(); signals: void runStateChanged(); private slots: void finished (int exitCode, QProcess::ExitStatus exitStatus); void readyReadStandardOutput(); }; class Operation; /// \brief Watch for end of save operation and restart or stop runner class SaveWatcher : public QObject { Q_OBJECT Runner *mRunner; public: /// *this attaches itself to runner SaveWatcher (Runner *runner, OperationHolder *operation); private slots: void saveDone (int type, bool failed); }; } #endif ================================================ FILE: apps/opencs/model/doc/saving.cpp ================================================ #include "saving.hpp" #include "../world/data.hpp" #include "../world/idcollection.hpp" #include "state.hpp" #include "savingstages.hpp" #include "document.hpp" CSMDoc::Saving::Saving (Document& document, const boost::filesystem::path& projectPath, ToUTF8::FromType encoding) : Operation (State_Saving, true, true), mDocument (document), mState (*this, projectPath, encoding) { // save project file appendStage (new OpenSaveStage (mDocument, mState, true)); appendStage (new WriteHeaderStage (mDocument, mState, true)); appendStage (new WriteCollectionStage > ( mDocument.getData().getFilters(), mState, CSMWorld::Scope_Project)); appendStage (new WriteCollectionStage > ( mDocument.getData().getDebugProfiles(), mState, CSMWorld::Scope_Project)); appendStage (new WriteCollectionStage > ( mDocument.getData().getScripts(), mState, CSMWorld::Scope_Project)); appendStage (new CloseSaveStage (mState)); // save content file appendStage (new OpenSaveStage (mDocument, mState, false)); appendStage (new WriteHeaderStage (mDocument, mState, false)); appendStage (new WriteCollectionStage > (mDocument.getData().getGlobals(), mState)); appendStage (new WriteCollectionStage > (mDocument.getData().getGmsts(), mState)); appendStage (new WriteCollectionStage > (mDocument.getData().getSkills(), mState)); appendStage (new WriteCollectionStage > (mDocument.getData().getClasses(), mState)); appendStage (new WriteCollectionStage > (mDocument.getData().getFactions(), mState)); appendStage (new WriteCollectionStage > (mDocument.getData().getRaces(), mState)); appendStage (new WriteCollectionStage > (mDocument.getData().getSounds(), mState)); appendStage (new WriteCollectionStage > (mDocument.getData().getScripts(), mState)); appendStage (new WriteCollectionStage > (mDocument.getData().getRegions(), mState)); appendStage (new WriteCollectionStage > (mDocument.getData().getBirthsigns(), mState)); appendStage (new WriteCollectionStage > (mDocument.getData().getSpells(), mState)); appendStage (new WriteCollectionStage > (mDocument.getData().getEnchantments(), mState)); appendStage (new WriteCollectionStage > (mDocument.getData().getBodyParts(), mState)); appendStage (new WriteCollectionStage > (mDocument.getData().getSoundGens(), mState)); appendStage (new WriteCollectionStage > (mDocument.getData().getMagicEffects(), mState)); appendStage (new WriteCollectionStage > (mDocument.getData().getStartScripts(), mState)); appendStage (new WriteRefIdCollectionStage (mDocument, mState)); appendStage (new CollectionReferencesStage (mDocument, mState)); appendStage (new WriteCellCollectionStage (mDocument, mState)); // Dialogue can reference objects and cells so must be written after these records for vanilla-compatible files appendStage (new WriteDialogueCollectionStage (mDocument, mState, false)); appendStage (new WriteDialogueCollectionStage (mDocument, mState, true)); appendStage (new WritePathgridCollectionStage (mDocument, mState)); appendStage (new WriteLandTextureCollectionStage (mDocument, mState)); // references Land Textures appendStage (new WriteLandCollectionStage (mDocument, mState)); // close file and clean up appendStage (new CloseSaveStage (mState)); appendStage (new FinalSavingStage (mDocument, mState)); } ================================================ FILE: apps/opencs/model/doc/saving.hpp ================================================ #ifndef CSM_DOC_SAVING_H #define CSM_DOC_SAVING_H #include #include #include "operation.hpp" #include "savingstate.hpp" namespace CSMDoc { class Document; class Saving : public Operation { Q_OBJECT Document& mDocument; SavingState mState; public: Saving (Document& document, const boost::filesystem::path& projectPath, ToUTF8::FromType encoding); }; } #endif ================================================ FILE: apps/opencs/model/doc/savingstages.cpp ================================================ #include "savingstages.hpp" #include #include #include #include "../world/infocollection.hpp" #include "../world/cellcoordinates.hpp" #include "document.hpp" CSMDoc::OpenSaveStage::OpenSaveStage (Document& document, SavingState& state, bool projectFile) : mDocument (document), mState (state), mProjectFile (projectFile) {} int CSMDoc::OpenSaveStage::setup() { return 1; } void CSMDoc::OpenSaveStage::perform (int stage, Messages& messages) { mState.start (mDocument, mProjectFile); mState.getStream().open ( mProjectFile ? mState.getPath() : mState.getTmpPath(), std::ios::binary); if (!mState.getStream().is_open()) throw std::runtime_error ("failed to open stream for saving"); } CSMDoc::WriteHeaderStage::WriteHeaderStage (Document& document, SavingState& state, bool simple) : mDocument (document), mState (state), mSimple (simple) {} int CSMDoc::WriteHeaderStage::setup() { return 1; } void CSMDoc::WriteHeaderStage::perform (int stage, Messages& messages) { mState.getWriter().setVersion(); mState.getWriter().clearMaster(); if (mSimple) { mState.getWriter().setAuthor (""); mState.getWriter().setDescription (""); mState.getWriter().setRecordCount (0); mState.getWriter().setFormat (ESM::Header::CurrentFormat); } else { mDocument.getData().getMetaData().save (mState.getWriter()); mState.getWriter().setRecordCount ( mDocument.getData().count (CSMWorld::RecordBase::State_Modified) + mDocument.getData().count (CSMWorld::RecordBase::State_ModifiedOnly) + mDocument.getData().count (CSMWorld::RecordBase::State_Deleted)); /// \todo refine dependency list (at least remove redundant dependencies) std::vector dependencies = mDocument.getContentFiles(); std::vector::const_iterator end (--dependencies.end()); for (std::vector::const_iterator iter (dependencies.begin()); iter!=end; ++iter) { std::string name = iter->filename().string(); uint64_t size = boost::filesystem::file_size (*iter); mState.getWriter().addMaster (name, size); } } mState.getWriter().save (mState.getStream()); } CSMDoc::WriteDialogueCollectionStage::WriteDialogueCollectionStage (Document& document, SavingState& state, bool journal) : mState (state), mTopics (journal ? document.getData().getJournals() : document.getData().getTopics()), mInfos (journal ? document.getData().getJournalInfos() : document.getData().getTopicInfos()) {} int CSMDoc::WriteDialogueCollectionStage::setup() { return mTopics.getSize(); } void CSMDoc::WriteDialogueCollectionStage::perform (int stage, Messages& messages) { ESM::ESMWriter& writer = mState.getWriter(); const CSMWorld::Record& topic = mTopics.getRecord (stage); if (topic.mState == CSMWorld::RecordBase::State_Deleted) { // if the topic is deleted, we do not need to bother with INFO records. ESM::Dialogue dialogue = topic.get(); writer.startRecord(dialogue.sRecordId); dialogue.save(writer, true); writer.endRecord(dialogue.sRecordId); return; } // Test, if we need to save anything associated info records. bool infoModified = false; CSMWorld::InfoCollection::Range range = mInfos.getTopicRange (topic.get().mId); for (CSMWorld::InfoCollection::RecordConstIterator iter (range.first); iter!=range.second; ++iter) { if (iter->isModified() || iter->mState == CSMWorld::RecordBase::State_Deleted) { infoModified = true; break; } } if (topic.isModified() || infoModified) { if (infoModified && topic.mState != CSMWorld::RecordBase::State_Modified && topic.mState != CSMWorld::RecordBase::State_ModifiedOnly) { mState.getWriter().startRecord (topic.mBase.sRecordId); topic.mBase.save (mState.getWriter(), topic.mState == CSMWorld::RecordBase::State_Deleted); mState.getWriter().endRecord (topic.mBase.sRecordId); } else { mState.getWriter().startRecord (topic.mModified.sRecordId); topic.mModified.save (mState.getWriter(), topic.mState == CSMWorld::RecordBase::State_Deleted); mState.getWriter().endRecord (topic.mModified.sRecordId); } // write modified selected info records for (CSMWorld::InfoCollection::RecordConstIterator iter (range.first); iter!=range.second; ++iter) { if (iter->isModified() || iter->mState == CSMWorld::RecordBase::State_Deleted) { ESM::DialInfo info = iter->get(); info.mId = info.mId.substr (info.mId.find_last_of ('#')+1); info.mPrev = ""; if (iter!=range.first) { CSMWorld::InfoCollection::RecordConstIterator prev = iter; --prev; info.mPrev = prev->get().mId.substr (prev->get().mId.find_last_of ('#')+1); } CSMWorld::InfoCollection::RecordConstIterator next = iter; ++next; info.mNext = ""; if (next!=range.second) { info.mNext = next->get().mId.substr (next->get().mId.find_last_of ('#')+1); } writer.startRecord (info.sRecordId); info.save (writer, iter->mState == CSMWorld::RecordBase::State_Deleted); writer.endRecord (info.sRecordId); } } } } CSMDoc::WriteRefIdCollectionStage::WriteRefIdCollectionStage (Document& document, SavingState& state) : mDocument (document), mState (state) {} int CSMDoc::WriteRefIdCollectionStage::setup() { return mDocument.getData().getReferenceables().getSize(); } void CSMDoc::WriteRefIdCollectionStage::perform (int stage, Messages& messages) { mDocument.getData().getReferenceables().save (stage, mState.getWriter()); } CSMDoc::CollectionReferencesStage::CollectionReferencesStage (Document& document, SavingState& state) : mDocument (document), mState (state) {} int CSMDoc::CollectionReferencesStage::setup() { mState.getSubRecords().clear(); int size = mDocument.getData().getReferences().getSize(); int steps = size/100; if (size%100) ++steps; return steps; } void CSMDoc::CollectionReferencesStage::perform (int stage, Messages& messages) { int size = mDocument.getData().getReferences().getSize(); for (int i=stage*100; i& record = mDocument.getData().getReferences().getRecord (i); if (record.isModified() || record.mState == CSMWorld::RecordBase::State_Deleted) { std::string cellId = record.get().mOriginalCell.empty() ? record.get().mCell : record.get().mOriginalCell; std::deque& indices = mState.getSubRecords()[Misc::StringUtils::lowerCase (cellId)]; // collect moved references at the end of the container bool interior = cellId.substr (0, 1)!="#"; std::ostringstream stream; if (!interior) { // recalculate the ref's cell location std::pair index = record.get().getCellIndex(); stream << "#" << index.first << " " << index.second; } // An empty mOriginalCell is meant to indicate that it is the same as // the current cell. It is possible that a moved ref is moved again. if ((record.get().mOriginalCell.empty() ? record.get().mCell : record.get().mOriginalCell) != stream.str() && !interior && record.mState!=CSMWorld::RecordBase::State_ModifiedOnly && !record.get().mNew) indices.push_back (i); else indices.push_front (i); } } } CSMDoc::WriteCellCollectionStage::WriteCellCollectionStage (Document& document, SavingState& state) : mDocument (document), mState (state) {} int CSMDoc::WriteCellCollectionStage::setup() { return mDocument.getData().getCells().getSize(); } void CSMDoc::WriteCellCollectionStage::perform (int stage, Messages& messages) { ESM::ESMWriter& writer = mState.getWriter(); const CSMWorld::Record& cell = mDocument.getData().getCells().getRecord (stage); std::map >::const_iterator references = mState.getSubRecords().find (Misc::StringUtils::lowerCase (cell.get().mId)); if (cell.isModified() || cell.mState == CSMWorld::RecordBase::State_Deleted || references!=mState.getSubRecords().end()) { CSMWorld::Cell cellRecord = cell.get(); bool interior = cellRecord.mId.substr (0, 1)!="#"; // count new references and adjust RefNumCount accordingsly unsigned int newRefNum = cellRecord.mRefNumCounter; if (references!=mState.getSubRecords().end()) { for (std::deque::const_iterator iter (references->second.begin()); iter!=references->second.end(); ++iter) { const CSMWorld::Record& ref = mDocument.getData().getReferences().getRecord (*iter); CSMWorld::CellRef refRecord = ref.get(); if (refRecord.mNew || (!interior && ref.mState==CSMWorld::RecordBase::State_ModifiedOnly && /// \todo consider worldspace CSMWorld::CellCoordinates (refRecord.getCellIndex()).getId("") != refRecord.mCell)) ++cellRecord.mRefNumCounter; if (refRecord.mRefNum.mIndex >= newRefNum) newRefNum = refRecord.mRefNum.mIndex + 1; } } // write cell data writer.startRecord (cellRecord.sRecordId); if (interior) cellRecord.mData.mFlags |= ESM::Cell::Interior; else { cellRecord.mData.mFlags &= ~ESM::Cell::Interior; std::istringstream stream (cellRecord.mId.c_str()); char ignore; stream >> ignore >> cellRecord.mData.mX >> cellRecord.mData.mY; } cellRecord.save (writer, cell.mState == CSMWorld::RecordBase::State_Deleted); // write references if (references!=mState.getSubRecords().end()) { for (std::deque::const_iterator iter (references->second.begin()); iter!=references->second.end(); ++iter) { const CSMWorld::Record& ref = mDocument.getData().getReferences().getRecord (*iter); if (ref.isModified() || ref.mState == CSMWorld::RecordBase::State_Deleted) { CSMWorld::CellRef refRecord = ref.get(); // Check for uninitialized content file if (!refRecord.mRefNum.hasContentFile()) refRecord.mRefNum.mContentFile = 0; // recalculate the ref's cell location std::ostringstream stream; if (!interior) { std::pair index = refRecord.getCellIndex(); stream << "#" << index.first << " " << index.second; } if (refRecord.mNew || refRecord.mRefNum.mIndex == 0 || (!interior && ref.mState==CSMWorld::RecordBase::State_ModifiedOnly && refRecord.mCell!=stream.str())) { refRecord.mRefNum.mIndex = newRefNum++; } else if ((refRecord.mOriginalCell.empty() ? refRecord.mCell : refRecord.mOriginalCell) != stream.str() && !interior) { // An empty mOriginalCell is meant to indicate that it is the same as // the current cell. It is possible that a moved ref is moved again. ESM::MovedCellRef moved; moved.mRefNum = refRecord.mRefNum; // Need to fill mTarget with the ref's new position. std::istringstream istream (stream.str().c_str()); char ignore; istream >> ignore >> moved.mTarget[0] >> moved.mTarget[1]; refRecord.mRefNum.save (writer, false, "MVRF"); writer.writeHNT ("CNDT", moved.mTarget); } refRecord.save (writer, false, false, ref.mState == CSMWorld::RecordBase::State_Deleted); } } } writer.endRecord (cellRecord.sRecordId); } } CSMDoc::WritePathgridCollectionStage::WritePathgridCollectionStage (Document& document, SavingState& state) : mDocument (document), mState (state) {} int CSMDoc::WritePathgridCollectionStage::setup() { return mDocument.getData().getPathgrids().getSize(); } void CSMDoc::WritePathgridCollectionStage::perform (int stage, Messages& messages) { ESM::ESMWriter& writer = mState.getWriter(); const CSMWorld::Record& pathgrid = mDocument.getData().getPathgrids().getRecord (stage); if (pathgrid.isModified() || pathgrid.mState == CSMWorld::RecordBase::State_Deleted) { CSMWorld::Pathgrid record = pathgrid.get(); if (record.mId.substr (0, 1)=="#") { std::istringstream stream (record.mId.c_str()); char ignore; stream >> ignore >> record.mData.mX >> record.mData.mY; } else record.mCell = record.mId; writer.startRecord (record.sRecordId); record.save (writer, pathgrid.mState == CSMWorld::RecordBase::State_Deleted); writer.endRecord (record.sRecordId); } } CSMDoc::WriteLandCollectionStage::WriteLandCollectionStage (Document& document, SavingState& state) : mDocument (document), mState (state) {} int CSMDoc::WriteLandCollectionStage::setup() { return mDocument.getData().getLand().getSize(); } void CSMDoc::WriteLandCollectionStage::perform (int stage, Messages& messages) { ESM::ESMWriter& writer = mState.getWriter(); const CSMWorld::Record& land = mDocument.getData().getLand().getRecord (stage); if (land.isModified() || land.mState == CSMWorld::RecordBase::State_Deleted) { CSMWorld::Land record = land.get(); writer.startRecord (record.sRecordId); record.save (writer, land.mState == CSMWorld::RecordBase::State_Deleted); writer.endRecord (record.sRecordId); } } CSMDoc::WriteLandTextureCollectionStage::WriteLandTextureCollectionStage (Document& document, SavingState& state) : mDocument (document), mState (state) {} int CSMDoc::WriteLandTextureCollectionStage::setup() { return mDocument.getData().getLandTextures().getSize(); } void CSMDoc::WriteLandTextureCollectionStage::perform (int stage, Messages& messages) { ESM::ESMWriter& writer = mState.getWriter(); const CSMWorld::Record& landTexture = mDocument.getData().getLandTextures().getRecord (stage); if (landTexture.isModified() || landTexture.mState == CSMWorld::RecordBase::State_Deleted) { CSMWorld::LandTexture record = landTexture.get(); writer.startRecord (record.sRecordId); record.save (writer, landTexture.mState == CSMWorld::RecordBase::State_Deleted); writer.endRecord (record.sRecordId); } } CSMDoc::CloseSaveStage::CloseSaveStage (SavingState& state) : mState (state) {} int CSMDoc::CloseSaveStage::setup() { return 1; } void CSMDoc::CloseSaveStage::perform (int stage, Messages& messages) { mState.getStream().close(); if (!mState.getStream()) throw std::runtime_error ("saving failed"); } CSMDoc::FinalSavingStage::FinalSavingStage (Document& document, SavingState& state) : mDocument (document), mState (state) {} int CSMDoc::FinalSavingStage::setup() { return 1; } void CSMDoc::FinalSavingStage::perform (int stage, Messages& messages) { if (mState.hasError()) { mState.getWriter().close(); mState.getStream().close(); if (boost::filesystem::exists (mState.getTmpPath())) boost::filesystem::remove (mState.getTmpPath()); } else if (!mState.isProjectFile()) { if (boost::filesystem::exists (mState.getPath())) boost::filesystem::remove (mState.getPath()); boost::filesystem::rename (mState.getTmpPath(), mState.getPath()); mDocument.getUndoStack().setClean(); } } ================================================ FILE: apps/opencs/model/doc/savingstages.hpp ================================================ #ifndef CSM_DOC_SAVINGSTAGES_H #define CSM_DOC_SAVINGSTAGES_H #include "stage.hpp" #include "../world/record.hpp" #include "../world/idcollection.hpp" #include "../world/scope.hpp" #include #include "savingstate.hpp" namespace ESM { struct Dialogue; } namespace CSMWorld { class InfoCollection; } namespace CSMDoc { class Document; class SavingState; class OpenSaveStage : public Stage { Document& mDocument; SavingState& mState; bool mProjectFile; public: OpenSaveStage (Document& document, SavingState& state, bool projectFile); ///< \param projectFile Saving the project file instead of the content file. int setup() override; ///< \return number of steps void perform (int stage, Messages& messages) override; ///< Messages resulting from this stage will be appended to \a messages. }; class WriteHeaderStage : public Stage { Document& mDocument; SavingState& mState; bool mSimple; public: WriteHeaderStage (Document& document, SavingState& state, bool simple); ///< \param simple Simplified header (used for project files). int setup() override; ///< \return number of steps void perform (int stage, Messages& messages) override; ///< Messages resulting from this stage will be appended to \a messages. }; template class WriteCollectionStage : public Stage { const CollectionT& mCollection; SavingState& mState; CSMWorld::Scope mScope; public: WriteCollectionStage (const CollectionT& collection, SavingState& state, CSMWorld::Scope scope = CSMWorld::Scope_Content); int setup() override; ///< \return number of steps void perform (int stage, Messages& messages) override; ///< Messages resulting from this stage will be appended to \a messages. }; template WriteCollectionStage::WriteCollectionStage (const CollectionT& collection, SavingState& state, CSMWorld::Scope scope) : mCollection (collection), mState (state), mScope (scope) {} template int WriteCollectionStage::setup() { return mCollection.getSize(); } template void WriteCollectionStage::perform (int stage, Messages& messages) { if (CSMWorld::getScopeFromId (mCollection.getRecord (stage).get().mId)!=mScope) return; ESM::ESMWriter& writer = mState.getWriter(); CSMWorld::RecordBase::State state = mCollection.getRecord (stage).mState; typename CollectionT::ESXRecord record = mCollection.getRecord (stage).get(); if (state == CSMWorld::RecordBase::State_Modified || state == CSMWorld::RecordBase::State_ModifiedOnly || state == CSMWorld::RecordBase::State_Deleted) { writer.startRecord (record.sRecordId); record.save (writer, state == CSMWorld::RecordBase::State_Deleted); writer.endRecord (record.sRecordId); } } class WriteDialogueCollectionStage : public Stage { SavingState& mState; const CSMWorld::IdCollection& mTopics; CSMWorld::InfoCollection& mInfos; public: WriteDialogueCollectionStage (Document& document, SavingState& state, bool journal); int setup() override; ///< \return number of steps void perform (int stage, Messages& messages) override; ///< Messages resulting from this stage will be appended to \a messages. }; class WriteRefIdCollectionStage : public Stage { Document& mDocument; SavingState& mState; public: WriteRefIdCollectionStage (Document& document, SavingState& state); int setup() override; ///< \return number of steps void perform (int stage, Messages& messages) override; ///< Messages resulting from this stage will be appended to \a messages. }; class CollectionReferencesStage : public Stage { Document& mDocument; SavingState& mState; public: CollectionReferencesStage (Document& document, SavingState& state); int setup() override; ///< \return number of steps void perform (int stage, Messages& messages) override; ///< Messages resulting from this stage will be appended to \a messages. }; class WriteCellCollectionStage : public Stage { Document& mDocument; SavingState& mState; public: WriteCellCollectionStage (Document& document, SavingState& state); int setup() override; ///< \return number of steps void perform (int stage, Messages& messages) override; ///< Messages resulting from this stage will be appended to \a messages. }; class WritePathgridCollectionStage : public Stage { Document& mDocument; SavingState& mState; public: WritePathgridCollectionStage (Document& document, SavingState& state); int setup() override; ///< \return number of steps void perform (int stage, Messages& messages) override; ///< Messages resulting from this stage will be appended to \a messages. }; class WriteLandCollectionStage : public Stage { Document& mDocument; SavingState& mState; public: WriteLandCollectionStage (Document& document, SavingState& state); int setup() override; ///< \return number of steps void perform (int stage, Messages& messages) override; ///< Messages resulting from this stage will be appended to \a messages. }; class WriteLandTextureCollectionStage : public Stage { Document& mDocument; SavingState& mState; public: WriteLandTextureCollectionStage (Document& document, SavingState& state); int setup() override; ///< \return number of steps void perform (int stage, Messages& messages) override; ///< Messages resulting from this stage will be appended to \a messages. }; class CloseSaveStage : public Stage { SavingState& mState; public: CloseSaveStage (SavingState& state); int setup() override; ///< \return number of steps void perform (int stage, Messages& messages) override; ///< Messages resulting from this stage will be appended to \a messages. }; class FinalSavingStage : public Stage { Document& mDocument; SavingState& mState; public: FinalSavingStage (Document& document, SavingState& state); int setup() override; ///< \return number of steps void perform (int stage, Messages& messages) override; ///< Messages resulting from this stage will be appended to \a messages. }; } #endif ================================================ FILE: apps/opencs/model/doc/savingstate.cpp ================================================ #include "savingstate.hpp" #include #include "operation.hpp" #include "document.hpp" CSMDoc::SavingState::SavingState (Operation& operation, const boost::filesystem::path& projectPath, ToUTF8::FromType encoding) : mOperation (operation), mEncoder (encoding), mProjectPath (projectPath), mProjectFile (false) { mWriter.setEncoder (&mEncoder); } bool CSMDoc::SavingState::hasError() const { return mOperation.hasError(); } void CSMDoc::SavingState::start (Document& document, bool project) { mProjectFile = project; if (mStream.is_open()) mStream.close(); mStream.clear(); mSubRecords.clear(); if (project) mPath = mProjectPath; else mPath = document.getSavePath(); boost::filesystem::path file (mPath.filename().string() + ".tmp"); mTmpPath = mPath.parent_path(); mTmpPath /= file; } const boost::filesystem::path& CSMDoc::SavingState::getPath() const { return mPath; } const boost::filesystem::path& CSMDoc::SavingState::getTmpPath() const { return mTmpPath; } boost::filesystem::ofstream& CSMDoc::SavingState::getStream() { return mStream; } ESM::ESMWriter& CSMDoc::SavingState::getWriter() { return mWriter; } bool CSMDoc::SavingState::isProjectFile() const { return mProjectFile; } std::map >& CSMDoc::SavingState::getSubRecords() { return mSubRecords; } ================================================ FILE: apps/opencs/model/doc/savingstate.hpp ================================================ #ifndef CSM_DOC_SAVINGSTATE_H #define CSM_DOC_SAVINGSTATE_H #include #include #include #include #include #include #include namespace CSMDoc { class Operation; class Document; class SavingState { Operation& mOperation; boost::filesystem::path mPath; boost::filesystem::path mTmpPath; ToUTF8::Utf8Encoder mEncoder; boost::filesystem::ofstream mStream; ESM::ESMWriter mWriter; boost::filesystem::path mProjectPath; bool mProjectFile; std::map > mSubRecords; // record ID, list of subrecords public: SavingState (Operation& operation, const boost::filesystem::path& projectPath, ToUTF8::FromType encoding); bool hasError() const; void start (Document& document, bool project); ///< \param project Save project file instead of content file. const boost::filesystem::path& getPath() const; const boost::filesystem::path& getTmpPath() const; boost::filesystem::ofstream& getStream(); ESM::ESMWriter& getWriter(); bool isProjectFile() const; ///< Currently saving project file? (instead of content file) std::map >& getSubRecords(); }; } #endif ================================================ FILE: apps/opencs/model/doc/stage.cpp ================================================ #include "stage.hpp" CSMDoc::Stage::~Stage() {} ================================================ FILE: apps/opencs/model/doc/stage.hpp ================================================ #ifndef CSM_DOC_STAGE_H #define CSM_DOC_STAGE_H #include #include #include "../world/universalid.hpp" #include "messages.hpp" class QString; namespace CSMDoc { class Stage { public: virtual ~Stage(); virtual int setup() = 0; ///< \return number of steps virtual void perform (int stage, Messages& messages) = 0; ///< Messages resulting from this stage will be appended to \a messages. }; } #endif ================================================ FILE: apps/opencs/model/doc/state.hpp ================================================ #ifndef CSM_DOC_STATE_H #define CSM_DOC_STATE_H namespace CSMDoc { enum State { State_Modified = 1, State_Locked = 2, State_Operation = 4, State_Running = 8, State_Saving = 16, State_Verifying = 32, State_Merging = 64, State_Searching = 128, State_Loading = 256 // pseudo-state; can not be encountered in a loaded document }; } #endif ================================================ FILE: apps/opencs/model/filter/andnode.cpp ================================================ #include "andnode.hpp" #include CSMFilter::AndNode::AndNode (const std::vector >& nodes) : NAryNode (nodes, "and") {} bool CSMFilter::AndNode::test (const CSMWorld::IdTableBase& table, int row, const std::map& columns) const { int size = getSize(); for (int i=0; i >& nodes); bool test (const CSMWorld::IdTableBase& table, int row, const std::map& columns) const override; ///< \return Can the specified table row pass through to filter? /// \param columns column ID to column index mapping }; } #endif ================================================ FILE: apps/opencs/model/filter/booleannode.cpp ================================================ #include "booleannode.hpp" CSMFilter::BooleanNode::BooleanNode (bool true_) : mTrue (true_) {} bool CSMFilter::BooleanNode::test (const CSMWorld::IdTableBase& table, int row, const std::map& columns) const { return mTrue; } std::string CSMFilter::BooleanNode::toString (bool numericColumns) const { return mTrue ? "true" : "false"; } ================================================ FILE: apps/opencs/model/filter/booleannode.hpp ================================================ #ifndef CSM_FILTER_BOOLEANNODE_H #define CSM_FILTER_BOOLEANNODE_H #include "leafnode.hpp" namespace CSMFilter { class BooleanNode : public LeafNode { bool mTrue; public: BooleanNode (bool true_); bool test (const CSMWorld::IdTableBase& table, int row, const std::map& columns) const override; ///< \return Can the specified table row pass through to filter? /// \param columns column ID to column index mapping std::string toString (bool numericColumns) const override; ///< Return a string that represents this node. /// /// \param numericColumns Use numeric IDs instead of string to represent columns. }; } #endif ================================================ FILE: apps/opencs/model/filter/leafnode.cpp ================================================ #include "leafnode.hpp" std::vector CSMFilter::LeafNode::getReferencedColumns() const { return std::vector(); } ================================================ FILE: apps/opencs/model/filter/leafnode.hpp ================================================ #ifndef CSM_FILTER_LEAFNODE_H #define CSM_FILTER_LEAFNODE_H #include #include "node.hpp" namespace CSMFilter { class LeafNode : public Node { public: std::vector getReferencedColumns() const override; ///< Return a list of the IDs of the columns referenced by this node. The column mapping /// passed into test as columns must contain all columns listed here. }; } #endif ================================================ FILE: apps/opencs/model/filter/narynode.cpp ================================================ #include "narynode.hpp" #include CSMFilter::NAryNode::NAryNode (const std::vector >& nodes, const std::string& name) : mNodes (nodes), mName (name) {} int CSMFilter::NAryNode::getSize() const { return static_cast(mNodes.size()); } const CSMFilter::Node& CSMFilter::NAryNode::operator[] (int index) const { return *mNodes.at (index); } std::vector CSMFilter::NAryNode::getReferencedColumns() const { std::vector columns; for (std::vector >::const_iterator iter (mNodes.begin()); iter!=mNodes.end(); ++iter) { std::vector columns2 = (*iter)->getReferencedColumns(); columns.insert (columns.end(), columns2.begin(), columns2.end()); } return columns; } std::string CSMFilter::NAryNode::toString (bool numericColumns) const { std::ostringstream stream; stream << mName << " ("; bool first = true; int size = getSize(); for (int i=0; i #include #include "node.hpp" namespace CSMFilter { class NAryNode : public Node { std::vector > mNodes; std::string mName; public: NAryNode (const std::vector >& nodes, const std::string& name); int getSize() const; const Node& operator[] (int index) const; std::vector getReferencedColumns() const override; ///< Return a list of the IDs of the columns referenced by this node. The column mapping /// passed into test as columns must contain all columns listed here. std::string toString (bool numericColumns) const override; ///< Return a string that represents this node. /// /// \param numericColumns Use numeric IDs instead of string to represent columns. }; } #endif ================================================ FILE: apps/opencs/model/filter/node.cpp ================================================ #include "node.hpp" CSMFilter::Node::Node() {} CSMFilter::Node::~Node() {} ================================================ FILE: apps/opencs/model/filter/node.hpp ================================================ #ifndef CSM_FILTER_NODE_H #define CSM_FILTER_NODE_H #include #include #include #include #include namespace CSMWorld { class IdTableBase; } namespace CSMFilter { /// \brief Root class for the filter node hierarchy /// /// \note When the function documentation for this class mentions "this node", this should be /// interpreted as "the node and all its children". class Node { // not implemented Node (const Node&); Node& operator= (const Node&); public: Node(); virtual ~Node(); virtual bool test (const CSMWorld::IdTableBase& table, int row, const std::map& columns) const = 0; ///< \return Can the specified table row pass through to filter? /// \param columns column ID to column index mapping virtual std::vector getReferencedColumns() const = 0; ///< Return a list of the IDs of the columns referenced by this node. The column mapping /// passed into test as columns must contain all columns listed here. virtual std::string toString (bool numericColumns) const = 0; ///< Return a string that represents this node. /// /// \param numericColumns Use numeric IDs instead of string to represent columns. }; } Q_DECLARE_METATYPE (std::shared_ptr) #endif ================================================ FILE: apps/opencs/model/filter/notnode.cpp ================================================ #include "notnode.hpp" CSMFilter::NotNode::NotNode (std::shared_ptr child) : UnaryNode (child, "not") {} bool CSMFilter::NotNode::test (const CSMWorld::IdTableBase& table, int row, const std::map& columns) const { return !getChild().test (table, row, columns); } ================================================ FILE: apps/opencs/model/filter/notnode.hpp ================================================ #ifndef CSM_FILTER_NOTNODE_H #define CSM_FILTER_NOTNODE_H #include "unarynode.hpp" namespace CSMFilter { class NotNode : public UnaryNode { public: NotNode (std::shared_ptr child); bool test (const CSMWorld::IdTableBase& table, int row, const std::map& columns) const override; ///< \return Can the specified table row pass through to filter? /// \param columns column ID to column index mapping }; } #endif ================================================ FILE: apps/opencs/model/filter/ornode.cpp ================================================ #include "ornode.hpp" #include CSMFilter::OrNode::OrNode (const std::vector >& nodes) : NAryNode (nodes, "or") {} bool CSMFilter::OrNode::test (const CSMWorld::IdTableBase& table, int row, const std::map& columns) const { int size = getSize(); for (int i=0; i >& nodes); bool test (const CSMWorld::IdTableBase& table, int row, const std::map& columns) const override; ///< \return Can the specified table row pass through to filter? /// \param columns column ID to column index mapping }; } #endif ================================================ FILE: apps/opencs/model/filter/parser.cpp ================================================ #include "parser.hpp" #include #include #include #include #include "../world/columns.hpp" #include "../world/data.hpp" #include "../world/idcollection.hpp" #include "booleannode.hpp" #include "ornode.hpp" #include "andnode.hpp" #include "notnode.hpp" #include "textnode.hpp" #include "valuenode.hpp" namespace CSMFilter { struct Token { enum Type { Type_EOS, Type_None, Type_String, Type_Number, Type_Open, Type_Close, Type_OpenSquare, Type_CloseSquare, Type_Comma, Type_OneShot, Type_Keyword_True, ///< \attention Keyword enums must be arranged continuously. Type_Keyword_False, Type_Keyword_And, Type_Keyword_Or, Type_Keyword_Not, Type_Keyword_Text, Type_Keyword_Value }; Type mType; std::string mString; double mNumber; Token (Type type = Type_None); Token (Type type, const std::string& string); ///< Non-string type that can also be interpreted as a string. Token (const std::string& string); Token (double number); operator bool() const; bool isString() const; }; Token::Token (Type type) : mType (type), mNumber(0.0) {} Token::Token (Type type, const std::string& string) : mType (type), mString (string), mNumber(0.0) {} Token::Token (const std::string& string) : mType (Type_String), mString (string), mNumber(0.0) {} Token::Token (double number) : mType (Type_Number), mNumber (number) {} bool Token::isString() const { return mType==Type_String || mType>=Type_Keyword_True; } Token::operator bool() const { return mType!=Type_None; } bool operator== (const Token& left, const Token& right) { if (left.mType!=right.mType) return false; switch (left.mType) { case Token::Type_String: return left.mString==right.mString; case Token::Type_Number: return left.mNumber==right.mNumber; default: return true; } } } CSMFilter::Token CSMFilter::Parser::getStringToken() { std::string string; int size = static_cast (mInput.size()); for (; mIndex1) { ++mIndex; break; } }; if (!string.empty()) { if (string[0]=='"' && (string[string.size()-1]!='"' || string.size()<2) ) { error(); return Token (Token::Type_None); } if (string[0]!='"' && string[string.size()-1]=='"') { error(); return Token (Token::Type_None); } if (string[0]=='"') return string.substr (1, string.size()-2); } return checkKeywords (string); } CSMFilter::Token CSMFilter::Parser::getNumberToken() { std::string string; int size = static_cast (mInput.size()); bool hasDecimalPoint = false; bool hasDigit = false; for (; mIndex> value; return value; } CSMFilter::Token CSMFilter::Parser::checkKeywords (const Token& token) { static const char *sKeywords[] = { "true", "false", "and", "or", "not", "string", "value", 0 }; std::string string = Misc::StringUtils::lowerCase (token.mString); for (int i=0; sKeywords[i]; ++i) if (sKeywords[i]==string || (string.size()==1 && sKeywords[i][0]==string[0])) return Token (static_cast (i+Token::Type_Keyword_True), token.mString); return token; } CSMFilter::Token CSMFilter::Parser::getNextToken() { int size = static_cast (mInput.size()); char c = 0; for (; mIndex=size) return Token (Token::Type_EOS); switch (c) { case '(': ++mIndex; return Token (Token::Type_Open); case ')': ++mIndex; return Token (Token::Type_Close); case '[': ++mIndex; return Token (Token::Type_OpenSquare); case ']': ++mIndex; return Token (Token::Type_CloseSquare); case ',': ++mIndex; return Token (Token::Type_Comma); case '!': ++mIndex; return Token (Token::Type_OneShot); } if (c=='"' || c=='_' || std::isalpha (c) || c==':') return getStringToken(); if (c=='-' || c=='.' || std::isdigit (c)) return getNumberToken(); error(); return Token (Token::Type_None); } std::shared_ptr CSMFilter::Parser::parseImp (bool allowEmpty, bool ignoreOneShot) { if (Token token = getNextToken()) { if (token==Token (Token::Type_OneShot)) token = getNextToken(); if (token) switch (token.mType) { case Token::Type_Keyword_True: return std::shared_ptr (new BooleanNode (true)); case Token::Type_Keyword_False: return std::shared_ptr (new BooleanNode (false)); case Token::Type_Keyword_And: case Token::Type_Keyword_Or: return parseNAry (token); case Token::Type_Keyword_Not: { std::shared_ptr node = parseImp(); if (mError) return std::shared_ptr(); return std::shared_ptr (new NotNode (node)); } case Token::Type_Keyword_Text: return parseText(); case Token::Type_Keyword_Value: return parseValue(); case Token::Type_EOS: if (!allowEmpty) error(); return std::shared_ptr(); default: error(); } } return std::shared_ptr(); } std::shared_ptr CSMFilter::Parser::parseNAry (const Token& keyword) { std::vector > nodes; Token token = getNextToken(); if (token.mType!=Token::Type_Open) { error(); return std::shared_ptr(); } for (;;) { std::shared_ptr node = parseImp(); if (mError) return std::shared_ptr(); nodes.push_back (node); token = getNextToken(); if (!token || (token.mType!=Token::Type_Close && token.mType!=Token::Type_Comma)) { error(); return std::shared_ptr(); } if (token.mType==Token::Type_Close) break; } switch (keyword.mType) { case Token::Type_Keyword_And: return std::shared_ptr (new AndNode (nodes)); case Token::Type_Keyword_Or: return std::shared_ptr (new OrNode (nodes)); default: error(); return std::shared_ptr(); } } std::shared_ptr CSMFilter::Parser::parseText() { Token token = getNextToken(); if (token.mType!=Token::Type_Open) { error(); return std::shared_ptr(); } token = getNextToken(); if (!token) return std::shared_ptr(); // parse column ID int columnId = -1; if (token.mType==Token::Type_Number) { if (static_cast (token.mNumber)==token.mNumber) columnId = static_cast (token.mNumber); } else if (token.isString()) { columnId = CSMWorld::Columns::getId (token.mString); } if (columnId<0) { error(); return std::shared_ptr(); } token = getNextToken(); if (token.mType!=Token::Type_Comma) { error(); return std::shared_ptr(); } // parse text pattern token = getNextToken(); if (!token.isString()) { error(); return std::shared_ptr(); } std::string text = token.mString; token = getNextToken(); if (token.mType!=Token::Type_Close) { error(); return std::shared_ptr(); } return std::shared_ptr (new TextNode (columnId, text)); } std::shared_ptr CSMFilter::Parser::parseValue() { Token token = getNextToken(); if (token.mType!=Token::Type_Open) { error(); return std::shared_ptr(); } token = getNextToken(); if (!token) return std::shared_ptr(); // parse column ID int columnId = -1; if (token.mType==Token::Type_Number) { if (static_cast (token.mNumber)==token.mNumber) columnId = static_cast (token.mNumber); } else if (token.isString()) { columnId = CSMWorld::Columns::getId (token.mString); } if (columnId<0) { error(); return std::shared_ptr(); } token = getNextToken(); if (token.mType!=Token::Type_Comma) { error(); return std::shared_ptr(); } // parse value double lower = 0; double upper = 0; ValueNode::Type lowerType = ValueNode::Type_Open; ValueNode::Type upperType = ValueNode::Type_Open; token = getNextToken(); if (token.mType==Token::Type_Number) { // single value lower = upper = token.mNumber; lowerType = upperType = ValueNode::Type_Closed; } else { // interval if (token.mType==Token::Type_OpenSquare) lowerType = ValueNode::Type_Closed; else if (token.mType!=Token::Type_CloseSquare && token.mType!=Token::Type_Open) { error(); return std::shared_ptr(); } token = getNextToken(); if (token.mType==Token::Type_Number) { lower = token.mNumber; token = getNextToken(); if (token.mType!=Token::Type_Comma) { error(); return std::shared_ptr(); } } else if (token.mType==Token::Type_Comma) { lowerType = ValueNode::Type_Infinite; } else { error(); return std::shared_ptr(); } token = getNextToken(); if (token.mType==Token::Type_Number) { upper = token.mNumber; token = getNextToken(); } else upperType = ValueNode::Type_Infinite; if (token.mType==Token::Type_CloseSquare) { if (upperType!=ValueNode::Type_Infinite) upperType = ValueNode::Type_Closed; } else if (token.mType!=Token::Type_OpenSquare && token.mType!=Token::Type_Close) { error(); return std::shared_ptr(); } } token = getNextToken(); if (token.mType!=Token::Type_Close) { error(); return std::shared_ptr(); } return std::shared_ptr (new ValueNode (columnId, lowerType, upperType, lower, upper)); } void CSMFilter::Parser::error() { mError = true; } CSMFilter::Parser::Parser (const CSMWorld::Data& data) : mIndex (0), mError (false), mData (data) {} bool CSMFilter::Parser::parse (const std::string& filter, bool allowPredefined) { // reset mFilter.reset(); mError = false; mInput = filter; mIndex = 0; Token token; if (allowPredefined) token = getNextToken(); if (allowPredefined && token==Token (Token::Type_EOS)) { mFilter.reset(); return true; } else if (!allowPredefined || token==Token (Token::Type_OneShot)) { std::shared_ptr node = parseImp (true, token!=Token (Token::Type_OneShot)); if (mError) return false; if (getNextToken()!=Token (Token::Type_EOS)) { error(); return false; } if (node) mFilter = node; else { // Empty filter string equals to filter "true". mFilter.reset (new BooleanNode (true)); } return true; } // We do not use isString() here, because there could be a pre-defined filter with an ID that is // equal a filter keyword. else if (token.mType==Token::Type_String) { if (getNextToken()!=Token (Token::Type_EOS)) { error(); return false; } int index = mData.getFilters().searchId (token.mString); if (index==-1) { error(); return false; } const CSMWorld::Record& record = mData.getFilters().getRecord (index); if (record.isDeleted()) { error(); return false; } return parse (record.get().mFilter, false); } else { error(); return false; } } std::shared_ptr CSMFilter::Parser::getFilter() const { if (mError) throw std::logic_error ("No filter available"); return mFilter; } ================================================ FILE: apps/opencs/model/filter/parser.hpp ================================================ #ifndef CSM_FILTER_PARSER_H #define CSM_FILTER_PARSER_H #include "node.hpp" namespace CSMWorld { class Data; } namespace CSMFilter { struct Token; class Parser { std::shared_ptr mFilter; std::string mInput; int mIndex; bool mError; const CSMWorld::Data& mData; Token getStringToken(); Token getNumberToken(); Token getNextToken(); Token checkKeywords (const Token& token); ///< Turn string token into keyword token, if possible. std::shared_ptr parseImp (bool allowEmpty = false, bool ignoreOneShot = false); ///< Will return a null-pointer, if there is nothing more to parse. std::shared_ptr parseNAry (const Token& keyword); std::shared_ptr parseText(); std::shared_ptr parseValue(); void error(); public: Parser (const CSMWorld::Data& data); bool parse (const std::string& filter, bool allowPredefined = true); ///< Discards any previous calls to parse /// /// \return Success? std::shared_ptr getFilter() const; ///< Throws an exception if the last call to parse did not return true. }; } #endif ================================================ FILE: apps/opencs/model/filter/textnode.cpp ================================================ #include "textnode.hpp" #include #include #include #include "../world/columns.hpp" #include "../world/idtablebase.hpp" CSMFilter::TextNode::TextNode (int columnId, const std::string& text) : mColumnId (columnId), mText (text) {} bool CSMFilter::TextNode::test (const CSMWorld::IdTableBase& table, int row, const std::map& columns) const { const std::map::const_iterator iter = columns.find (mColumnId); if (iter==columns.end()) throw std::logic_error ("invalid column in text node test"); if (iter->second==-1) return true; QModelIndex index = table.index (row, iter->second); QVariant data = table.data (index); QString string; if (data.type()==QVariant::String) { string = data.toString(); } else if ((data.type()==QVariant::Int || data.type()==QVariant::UInt) && CSMWorld::Columns::hasEnums (static_cast (mColumnId))) { int value = data.toInt(); std::vector> enums = CSMWorld::Columns::getEnums (static_cast (mColumnId)); if (value>=0 && value (enums.size())) string = QString::fromUtf8 (enums[value].second.c_str()); } else if (data.type()==QVariant::Bool) { string = data.toBool() ? "true" : "false"; } else if (mText.empty() && !data.isValid()) return true; else return false; /// \todo make pattern syntax configurable QRegExp regExp (QString::fromUtf8 (mText.c_str()), Qt::CaseInsensitive); return regExp.exactMatch (string); } std::vector CSMFilter::TextNode::getReferencedColumns() const { return std::vector (1, mColumnId); } std::string CSMFilter::TextNode::toString (bool numericColumns) const { std::ostringstream stream; stream << "text ("; if (numericColumns) stream << mColumnId; else stream << "\"" << CSMWorld::Columns::getName (static_cast (mColumnId)) << "\""; stream << ", \"" << mText << "\")"; return stream.str(); } ================================================ FILE: apps/opencs/model/filter/textnode.hpp ================================================ #ifndef CSM_FILTER_TEXTNODE_H #define CSM_FILTER_TEXTNODE_H #include "leafnode.hpp" namespace CSMFilter { class TextNode : public LeafNode { int mColumnId; std::string mText; public: TextNode (int columnId, const std::string& text); bool test (const CSMWorld::IdTableBase& table, int row, const std::map& columns) const override; ///< \return Can the specified table row pass through to filter? /// \param columns column ID to column index mapping std::vector getReferencedColumns() const override; ///< Return a list of the IDs of the columns referenced by this node. The column mapping /// passed into test as columns must contain all columns listed here. std::string toString (bool numericColumns) const override; ///< Return a string that represents this node. /// /// \param numericColumns Use numeric IDs instead of string to represent columns. }; } #endif ================================================ FILE: apps/opencs/model/filter/unarynode.cpp ================================================ #include "unarynode.hpp" CSMFilter::UnaryNode::UnaryNode (std::shared_ptr child, const std::string& name) : mChild (child), mName (name) {} const CSMFilter::Node& CSMFilter::UnaryNode::getChild() const { return *mChild; } CSMFilter::Node& CSMFilter::UnaryNode::getChild() { return *mChild; } std::vector CSMFilter::UnaryNode::getReferencedColumns() const { return mChild->getReferencedColumns(); } std::string CSMFilter::UnaryNode::toString (bool numericColumns) const { return mName + " " + mChild->toString (numericColumns); } ================================================ FILE: apps/opencs/model/filter/unarynode.hpp ================================================ #ifndef CSM_FILTER_UNARYNODE_H #define CSM_FILTER_UNARYNODE_H #include "node.hpp" namespace CSMFilter { class UnaryNode : public Node { std::shared_ptr mChild; std::string mName; public: UnaryNode (std::shared_ptr child, const std::string& name); const Node& getChild() const; Node& getChild(); std::vector getReferencedColumns() const override; ///< Return a list of the IDs of the columns referenced by this node. The column mapping /// passed into test as columns must contain all columns listed here. std::string toString (bool numericColumns) const override; ///< Return a string that represents this node. /// /// \param numericColumns Use numeric IDs instead of string to represent columns. }; } #endif ================================================ FILE: apps/opencs/model/filter/valuenode.cpp ================================================ #include "valuenode.hpp" #include #include #include "../world/columns.hpp" #include "../world/idtablebase.hpp" CSMFilter::ValueNode::ValueNode (int columnId, Type lowerType, Type upperType, double lower, double upper) : mColumnId (columnId), mLower (lower), mUpper (upper), mLowerType (lowerType), mUpperType (upperType){} bool CSMFilter::ValueNode::test (const CSMWorld::IdTableBase& table, int row, const std::map& columns) const { const std::map::const_iterator iter = columns.find (mColumnId); if (iter==columns.end()) throw std::logic_error ("invalid column in value node test"); if (iter->second==-1) return true; QModelIndex index = table.index (row, iter->second); QVariant data = table.data (index); if (data.type()!=QVariant::Double && data.type()!=QVariant::Bool && data.type()!=QVariant::Int && data.type()!=QVariant::UInt && data.type()!=static_cast (QMetaType::Float)) return false; double value = data.toDouble(); switch (mLowerType) { case Type_Closed: if (valuemUpper) return false; break; case Type_Open: if (value>=mUpper) return false; break; case Type_Infinite: break; } return true; } std::vector CSMFilter::ValueNode::getReferencedColumns() const { return std::vector (1, mColumnId); } std::string CSMFilter::ValueNode::toString (bool numericColumns) const { std::ostringstream stream; stream << "value ("; if (numericColumns) stream << mColumnId; else stream << "\"" << CSMWorld::Columns::getName (static_cast (mColumnId)) << "\""; stream << ", "; if (mLower==mUpper && mLowerType!=Type_Infinite && mUpperType!=Type_Infinite) stream << mLower; else { switch (mLowerType) { case Type_Closed: stream << "[" << mLower; break; case Type_Open: stream << "(" << mLower; break; case Type_Infinite: stream << "("; break; } stream << ", "; switch (mUpperType) { case Type_Closed: stream << mUpper << "]"; break; case Type_Open: stream << mUpper << ")"; break; case Type_Infinite: stream << ")"; break; } } stream << ")"; return stream.str(); } ================================================ FILE: apps/opencs/model/filter/valuenode.hpp ================================================ #ifndef CSM_FILTER_VALUENODE_H #define CSM_FILTER_VALUENODE_H #include "leafnode.hpp" namespace CSMFilter { class ValueNode : public LeafNode { public: enum Type { Type_Closed, Type_Open, Type_Infinite }; private: int mColumnId; std::string mText; double mLower; double mUpper; Type mLowerType; Type mUpperType; public: ValueNode (int columnId, Type lowerType, Type upperType, double lower, double upper); bool test (const CSMWorld::IdTableBase& table, int row, const std::map& columns) const override; ///< \return Can the specified table row pass through to filter? /// \param columns column ID to column index mapping std::vector getReferencedColumns() const override; ///< Return a list of the IDs of the columns referenced by this node. The column mapping /// passed into test as columns must contain all columns listed here. std::string toString (bool numericColumns) const override; ///< Return a string that represents this node. /// /// \param numericColumns Use numeric IDs instead of string to represent columns. }; } #endif ================================================ FILE: apps/opencs/model/prefs/boolsetting.cpp ================================================ #include "boolsetting.hpp" #include #include #include #include "category.hpp" #include "state.hpp" CSMPrefs::BoolSetting::BoolSetting (Category *parent, Settings::Manager *values, QMutex *mutex, const std::string& key, const std::string& label, bool default_) : Setting (parent, values, mutex, key, label), mDefault (default_), mWidget(nullptr) {} CSMPrefs::BoolSetting& CSMPrefs::BoolSetting::setTooltip (const std::string& tooltip) { mTooltip = tooltip; return *this; } std::pair CSMPrefs::BoolSetting::makeWidgets (QWidget *parent) { mWidget = new QCheckBox (QString::fromUtf8 (getLabel().c_str()), parent); mWidget->setCheckState (mDefault ? Qt::Checked : Qt::Unchecked); if (!mTooltip.empty()) { QString tooltip = QString::fromUtf8 (mTooltip.c_str()); mWidget->setToolTip (tooltip); } connect (mWidget, SIGNAL (stateChanged (int)), this, SLOT (valueChanged (int))); return std::make_pair (static_cast (nullptr), mWidget); } void CSMPrefs::BoolSetting::updateWidget() { if (mWidget) { mWidget->setCheckState(getValues().getBool(getKey(), getParent()->getKey()) ? Qt::Checked : Qt::Unchecked); } } void CSMPrefs::BoolSetting::valueChanged (int value) { { QMutexLocker lock (getMutex()); getValues().setBool (getKey(), getParent()->getKey(), value); } getParent()->getState()->update (*this); } ================================================ FILE: apps/opencs/model/prefs/boolsetting.hpp ================================================ #ifndef CSM_PREFS_BOOLSETTING_H #define CSM_PREFS_BOOLSETTING_H #include "setting.hpp" class QCheckBox; namespace CSMPrefs { class BoolSetting : public Setting { Q_OBJECT std::string mTooltip; bool mDefault; QCheckBox* mWidget; public: BoolSetting (Category *parent, Settings::Manager *values, QMutex *mutex, const std::string& key, const std::string& label, bool default_); BoolSetting& setTooltip (const std::string& tooltip); /// Return label, input widget. std::pair makeWidgets (QWidget *parent) override; void updateWidget() override; private slots: void valueChanged (int value); }; } #endif ================================================ FILE: apps/opencs/model/prefs/category.cpp ================================================ #include "category.hpp" #include #include "setting.hpp" #include "state.hpp" CSMPrefs::Category::Category (State *parent, const std::string& key) : mParent (parent), mKey (key) {} const std::string& CSMPrefs::Category::getKey() const { return mKey; } CSMPrefs::State *CSMPrefs::Category::getState() const { return mParent; } void CSMPrefs::Category::addSetting (Setting *setting) { mSettings.push_back (setting); } CSMPrefs::Category::Iterator CSMPrefs::Category::begin() { return mSettings.begin(); } CSMPrefs::Category::Iterator CSMPrefs::Category::end() { return mSettings.end(); } CSMPrefs::Setting& CSMPrefs::Category::operator[] (const std::string& key) { for (Iterator iter = mSettings.begin(); iter!=mSettings.end(); ++iter) if ((*iter)->getKey()==key) return **iter; throw std::logic_error ("Invalid user setting: " + key); } void CSMPrefs::Category::update() { for (Iterator iter = mSettings.begin(); iter!=mSettings.end(); ++iter) mParent->update (**iter); } ================================================ FILE: apps/opencs/model/prefs/category.hpp ================================================ #ifndef CSM_PREFS_CATEGORY_H #define CSM_PREFS_CATEGORY_H #include #include namespace CSMPrefs { class State; class Setting; class Category { public: typedef std::vector Container; typedef Container::iterator Iterator; private: State *mParent; std::string mKey; Container mSettings; public: Category (State *parent, const std::string& key); const std::string& getKey() const; State *getState() const; void addSetting (Setting *setting); Iterator begin(); Iterator end(); Setting& operator[] (const std::string& key); void update(); }; } #endif ================================================ FILE: apps/opencs/model/prefs/coloursetting.cpp ================================================ #include "coloursetting.hpp" #include #include #include #include #include "../../view/widget/coloreditor.hpp" #include "category.hpp" #include "state.hpp" CSMPrefs::ColourSetting::ColourSetting (Category *parent, Settings::Manager *values, QMutex *mutex, const std::string& key, const std::string& label, QColor default_) : Setting (parent, values, mutex, key, label), mDefault (default_), mWidget(nullptr) {} CSMPrefs::ColourSetting& CSMPrefs::ColourSetting::setTooltip (const std::string& tooltip) { mTooltip = tooltip; return *this; } std::pair CSMPrefs::ColourSetting::makeWidgets (QWidget *parent) { QLabel *label = new QLabel (QString::fromUtf8 (getLabel().c_str()), parent); mWidget = new CSVWidget::ColorEditor (mDefault, parent); if (!mTooltip.empty()) { QString tooltip = QString::fromUtf8 (mTooltip.c_str()); label->setToolTip (tooltip); mWidget->setToolTip (tooltip); } connect (mWidget, SIGNAL (pickingFinished()), this, SLOT (valueChanged())); return std::make_pair (label, mWidget); } void CSMPrefs::ColourSetting::updateWidget() { if (mWidget) { mWidget->setColor(QString::fromStdString (getValues().getString(getKey(), getParent()->getKey()))); } } void CSMPrefs::ColourSetting::valueChanged() { CSVWidget::ColorEditor& widget = dynamic_cast (*sender()); { QMutexLocker lock (getMutex()); getValues().setString (getKey(), getParent()->getKey(), widget.color().name().toUtf8().data()); } getParent()->getState()->update (*this); } ================================================ FILE: apps/opencs/model/prefs/coloursetting.hpp ================================================ #ifndef CSM_PREFS_COLOURSETTING_H #define CSM_PREFS_COLOURSETTING_H #include "setting.hpp" #include namespace CSVWidget { class ColorEditor; } namespace CSMPrefs { class ColourSetting : public Setting { Q_OBJECT std::string mTooltip; QColor mDefault; CSVWidget::ColorEditor* mWidget; public: ColourSetting (Category *parent, Settings::Manager *values, QMutex *mutex, const std::string& key, const std::string& label, QColor default_); ColourSetting& setTooltip (const std::string& tooltip); /// Return label, input widget. std::pair makeWidgets (QWidget *parent) override; void updateWidget() override; private slots: void valueChanged(); }; } #endif ================================================ FILE: apps/opencs/model/prefs/doublesetting.cpp ================================================ #include "doublesetting.hpp" #include #include #include #include #include #include "category.hpp" #include "state.hpp" CSMPrefs::DoubleSetting::DoubleSetting (Category *parent, Settings::Manager *values, QMutex *mutex, const std::string& key, const std::string& label, double default_) : Setting (parent, values, mutex, key, label), mPrecision(2), mMin (0), mMax (std::numeric_limits::max()), mDefault (default_), mWidget(nullptr) {} CSMPrefs::DoubleSetting& CSMPrefs::DoubleSetting::setPrecision(int precision) { mPrecision = precision; return *this; } CSMPrefs::DoubleSetting& CSMPrefs::DoubleSetting::setRange (double min, double max) { mMin = min; mMax = max; return *this; } CSMPrefs::DoubleSetting& CSMPrefs::DoubleSetting::setMin (double min) { mMin = min; return *this; } CSMPrefs::DoubleSetting& CSMPrefs::DoubleSetting::setMax (double max) { mMax = max; return *this; } CSMPrefs::DoubleSetting& CSMPrefs::DoubleSetting::setTooltip (const std::string& tooltip) { mTooltip = tooltip; return *this; } std::pair CSMPrefs::DoubleSetting::makeWidgets (QWidget *parent) { QLabel *label = new QLabel (QString::fromUtf8 (getLabel().c_str()), parent); mWidget = new QDoubleSpinBox (parent); mWidget->setDecimals(mPrecision); mWidget->setRange (mMin, mMax); mWidget->setValue (mDefault); if (!mTooltip.empty()) { QString tooltip = QString::fromUtf8 (mTooltip.c_str()); label->setToolTip (tooltip); mWidget->setToolTip (tooltip); } connect (mWidget, SIGNAL (valueChanged (double)), this, SLOT (valueChanged (double))); return std::make_pair (label, mWidget); } void CSMPrefs::DoubleSetting::updateWidget() { if (mWidget) { mWidget->setValue(getValues().getFloat(getKey(), getParent()->getKey())); } } void CSMPrefs::DoubleSetting::valueChanged (double value) { { QMutexLocker lock (getMutex()); getValues().setFloat (getKey(), getParent()->getKey(), value); } getParent()->getState()->update (*this); } ================================================ FILE: apps/opencs/model/prefs/doublesetting.hpp ================================================ #ifndef CSM_PREFS_DOUBLESETTING_H #define CSM_PREFS_DOUBLESETTING_H #include "setting.hpp" class QDoubleSpinBox; namespace CSMPrefs { class DoubleSetting : public Setting { Q_OBJECT int mPrecision; double mMin; double mMax; std::string mTooltip; double mDefault; QDoubleSpinBox* mWidget; public: DoubleSetting (Category *parent, Settings::Manager *values, QMutex *mutex, const std::string& key, const std::string& label, double default_); DoubleSetting& setPrecision (int precision); // defaults to [0, std::numeric_limits::max()] DoubleSetting& setRange (double min, double max); DoubleSetting& setMin (double min); DoubleSetting& setMax (double max); DoubleSetting& setTooltip (const std::string& tooltip); /// Return label, input widget. std::pair makeWidgets (QWidget *parent) override; void updateWidget() override; private slots: void valueChanged (double value); }; } #endif ================================================ FILE: apps/opencs/model/prefs/enumsetting.cpp ================================================ #include "enumsetting.hpp" #include #include #include #include #include #include "category.hpp" #include "state.hpp" CSMPrefs::EnumValue::EnumValue (const std::string& value, const std::string& tooltip) : mValue (value), mTooltip (tooltip) {} CSMPrefs::EnumValue::EnumValue (const char *value) : mValue (value) {} CSMPrefs::EnumValues& CSMPrefs::EnumValues::add (const EnumValues& values) { mValues.insert (mValues.end(), values.mValues.begin(), values.mValues.end()); return *this; } CSMPrefs::EnumValues& CSMPrefs::EnumValues::add (const EnumValue& value) { mValues.push_back (value); return *this; } CSMPrefs::EnumValues& CSMPrefs::EnumValues::add (const std::string& value, const std::string& tooltip) { mValues.emplace_back(value, tooltip); return *this; } CSMPrefs::EnumSetting::EnumSetting (Category *parent, Settings::Manager *values, QMutex *mutex, const std::string& key, const std::string& label, const EnumValue& default_) : Setting (parent, values, mutex, key, label), mDefault (default_), mWidget(nullptr) {} CSMPrefs::EnumSetting& CSMPrefs::EnumSetting::setTooltip (const std::string& tooltip) { mTooltip = tooltip; return *this; } CSMPrefs::EnumSetting& CSMPrefs::EnumSetting::addValues (const EnumValues& values) { mValues.add (values); return *this; } CSMPrefs::EnumSetting& CSMPrefs::EnumSetting::addValue (const EnumValue& value) { mValues.add (value); return *this; } CSMPrefs::EnumSetting& CSMPrefs::EnumSetting::addValue (const std::string& value, const std::string& tooltip) { mValues.add (value, tooltip); return *this; } std::pair CSMPrefs::EnumSetting::makeWidgets (QWidget *parent) { QLabel *label = new QLabel (QString::fromUtf8 (getLabel().c_str()), parent); mWidget = new QComboBox (parent); int index = 0; for (int i=0; i (mValues.mValues.size()); ++i) { if (mDefault.mValue==mValues.mValues[i].mValue) index = i; mWidget->addItem (QString::fromUtf8 (mValues.mValues[i].mValue.c_str())); if (!mValues.mValues[i].mTooltip.empty()) mWidget->setItemData (i, QString::fromUtf8 (mValues.mValues[i].mTooltip.c_str()), Qt::ToolTipRole); } mWidget->setCurrentIndex (index); if (!mTooltip.empty()) { QString tooltip = QString::fromUtf8 (mTooltip.c_str()); label->setToolTip (tooltip); } connect (mWidget, SIGNAL (currentIndexChanged (int)), this, SLOT (valueChanged (int))); return std::make_pair (label, mWidget); } void CSMPrefs::EnumSetting::updateWidget() { if (mWidget) { int index = mWidget->findText(QString::fromStdString (getValues().getString(getKey(), getParent()->getKey()))); mWidget->setCurrentIndex(index); } } void CSMPrefs::EnumSetting::valueChanged (int value) { { QMutexLocker lock (getMutex()); getValues().setString (getKey(), getParent()->getKey(), mValues.mValues.at (value).mValue); } getParent()->getState()->update (*this); } ================================================ FILE: apps/opencs/model/prefs/enumsetting.hpp ================================================ #ifndef CSM_PREFS_ENUMSETTING_H #define CSM_PREFS_ENUMSETTING_H #include #include "setting.hpp" class QComboBox; namespace CSMPrefs { struct EnumValue { std::string mValue; std::string mTooltip; EnumValue (const std::string& value, const std::string& tooltip = ""); EnumValue (const char *value); }; struct EnumValues { std::vector mValues; EnumValues& add (const EnumValues& values); EnumValues& add (const EnumValue& value); EnumValues& add (const std::string& value, const std::string& tooltip); }; class EnumSetting : public Setting { Q_OBJECT std::string mTooltip; EnumValue mDefault; EnumValues mValues; QComboBox* mWidget; public: EnumSetting (Category *parent, Settings::Manager *values, QMutex *mutex, const std::string& key, const std::string& label, const EnumValue& default_); EnumSetting& setTooltip (const std::string& tooltip); EnumSetting& addValues (const EnumValues& values); EnumSetting& addValue (const EnumValue& value); EnumSetting& addValue (const std::string& value, const std::string& tooltip); /// Return label, input widget. std::pair makeWidgets (QWidget *parent) override; void updateWidget() override; private slots: void valueChanged (int value); }; } #endif ================================================ FILE: apps/opencs/model/prefs/intsetting.cpp ================================================ #include "intsetting.hpp" #include #include #include #include #include #include "category.hpp" #include "state.hpp" CSMPrefs::IntSetting::IntSetting (Category *parent, Settings::Manager *values, QMutex *mutex, const std::string& key, const std::string& label, int default_) : Setting (parent, values, mutex, key, label), mMin (0), mMax (std::numeric_limits::max()), mDefault (default_), mWidget(nullptr) {} CSMPrefs::IntSetting& CSMPrefs::IntSetting::setRange (int min, int max) { mMin = min; mMax = max; return *this; } CSMPrefs::IntSetting& CSMPrefs::IntSetting::setMin (int min) { mMin = min; return *this; } CSMPrefs::IntSetting& CSMPrefs::IntSetting::setMax (int max) { mMax = max; return *this; } CSMPrefs::IntSetting& CSMPrefs::IntSetting::setTooltip (const std::string& tooltip) { mTooltip = tooltip; return *this; } std::pair CSMPrefs::IntSetting::makeWidgets (QWidget *parent) { QLabel *label = new QLabel (QString::fromUtf8 (getLabel().c_str()), parent); mWidget = new QSpinBox (parent); mWidget->setRange (mMin, mMax); mWidget->setValue (mDefault); if (!mTooltip.empty()) { QString tooltip = QString::fromUtf8 (mTooltip.c_str()); label->setToolTip (tooltip); mWidget->setToolTip (tooltip); } connect (mWidget, SIGNAL (valueChanged (int)), this, SLOT (valueChanged (int))); return std::make_pair (label, mWidget); } void CSMPrefs::IntSetting::updateWidget() { if (mWidget) { mWidget->setValue(getValues().getInt(getKey(), getParent()->getKey())); } } void CSMPrefs::IntSetting::valueChanged (int value) { { QMutexLocker lock (getMutex()); getValues().setInt (getKey(), getParent()->getKey(), value); } getParent()->getState()->update (*this); } ================================================ FILE: apps/opencs/model/prefs/intsetting.hpp ================================================ #ifndef CSM_PREFS_INTSETTING_H #define CSM_PREFS_INTSETTING_H #include "setting.hpp" class QSpinBox; namespace CSMPrefs { class IntSetting : public Setting { Q_OBJECT int mMin; int mMax; std::string mTooltip; int mDefault; QSpinBox* mWidget; public: IntSetting (Category *parent, Settings::Manager *values, QMutex *mutex, const std::string& key, const std::string& label, int default_); // defaults to [0, std::numeric_limits::max()] IntSetting& setRange (int min, int max); IntSetting& setMin (int min); IntSetting& setMax (int max); IntSetting& setTooltip (const std::string& tooltip); /// Return label, input widget. std::pair makeWidgets (QWidget *parent) override; void updateWidget() override; private slots: void valueChanged (int value); }; } #endif ================================================ FILE: apps/opencs/model/prefs/modifiersetting.cpp ================================================ #include "modifiersetting.hpp" #include #include #include #include #include #include #include "state.hpp" #include "shortcutmanager.hpp" namespace CSMPrefs { ModifierSetting::ModifierSetting(Category* parent, Settings::Manager* values, QMutex* mutex, const std::string& key, const std::string& label) : Setting(parent, values, mutex, key, label) , mButton(nullptr) , mEditorActive(false) { } std::pair ModifierSetting::makeWidgets(QWidget* parent) { int modifier = 0; State::get().getShortcutManager().getModifier(getKey(), modifier); QString text = QString::fromUtf8(State::get().getShortcutManager().convertToString(modifier).c_str()); QLabel* label = new QLabel(QString::fromUtf8(getLabel().c_str()), parent); QPushButton* widget = new QPushButton(text, parent); widget->setCheckable(true); widget->installEventFilter(this); // right clicking on button sets shortcut to RMB, so context menu should not appear widget->setContextMenuPolicy(Qt::PreventContextMenu); mButton = widget; connect(widget, SIGNAL(toggled(bool)), this, SLOT(buttonToggled(bool))); return std::make_pair(label, widget); } void ModifierSetting::updateWidget() { if (mButton) { std::string shortcut = getValues().getString(getKey(), getParent()->getKey()); int modifier; State::get().getShortcutManager().convertFromString(shortcut, modifier); State::get().getShortcutManager().setModifier(getKey(), modifier); resetState(); } } bool ModifierSetting::eventFilter(QObject* target, QEvent* event) { if (event->type() == QEvent::KeyPress) { QKeyEvent* keyEvent = static_cast(event); if (keyEvent->isAutoRepeat()) return true; int mod = keyEvent->modifiers(); int key = keyEvent->key(); return handleEvent(target, mod, key); } else if (event->type() == QEvent::MouseButtonPress) { QMouseEvent* mouseEvent = static_cast(event); int mod = mouseEvent->modifiers(); int button = mouseEvent->button(); return handleEvent(target, mod, button); } else if (event->type() == QEvent::FocusOut) { resetState(); } return false; } bool ModifierSetting::handleEvent(QObject* target, int mod, int value) { // For potential future exceptions const int Blacklist[] = { 0 }; const size_t BlacklistSize = sizeof(Blacklist) / sizeof(int); if (!mEditorActive) { if (value == Qt::RightButton) { // Clear modifier int modifier = 0; storeValue(modifier); resetState(); } return false; } // Handle blacklist for (size_t i = 0; i < BlacklistSize; ++i) { if (value == Blacklist[i]) return true; } // Update modifier int modifier = value; storeValue(modifier); resetState(); return true; } void ModifierSetting::storeValue(int modifier) { State::get().getShortcutManager().setModifier(getKey(), modifier); // Convert to string and assign std::string value = State::get().getShortcutManager().convertToString(modifier); { QMutexLocker lock(getMutex()); getValues().setString(getKey(), getParent()->getKey(), value); } getParent()->getState()->update(*this); } void ModifierSetting::resetState() { mButton->setChecked(false); mEditorActive = false; // Button text int modifier = 0; State::get().getShortcutManager().getModifier(getKey(), modifier); QString text = QString::fromUtf8(State::get().getShortcutManager().convertToString(modifier).c_str()); mButton->setText(text); } void ModifierSetting::buttonToggled(bool checked) { if (checked) mButton->setText("Press keys or click here..."); mEditorActive = checked; } } ================================================ FILE: apps/opencs/model/prefs/modifiersetting.hpp ================================================ #ifndef CSM_PREFS_MODIFIERSETTING_H #define CSM_PREFS_MODIFIERSETTING_H #include #include "setting.hpp" class QEvent; class QPushButton; namespace CSMPrefs { class ModifierSetting : public Setting { Q_OBJECT public: ModifierSetting(Category* parent, Settings::Manager* values, QMutex* mutex, const std::string& key, const std::string& label); std::pair makeWidgets(QWidget* parent) override; void updateWidget() override; protected: bool eventFilter(QObject* target, QEvent* event) override; private: bool handleEvent(QObject* target, int mod, int value); void storeValue(int modifier); void resetState(); QPushButton* mButton; bool mEditorActive; private slots: void buttonToggled(bool checked); }; } #endif ================================================ FILE: apps/opencs/model/prefs/setting.cpp ================================================ #include "setting.hpp" #include #include #include "category.hpp" #include "state.hpp" Settings::Manager& CSMPrefs::Setting::getValues() { return *mValues; } QMutex *CSMPrefs::Setting::getMutex() { return mMutex; } CSMPrefs::Setting::Setting (Category *parent, Settings::Manager *values, QMutex *mutex, const std::string& key, const std::string& label) : QObject (parent->getState()), mParent (parent), mValues (values), mMutex (mutex), mKey (key), mLabel (label) {} CSMPrefs::Setting:: ~Setting() {} std::pair CSMPrefs::Setting::makeWidgets (QWidget *parent) { return std::pair (0, 0); } void CSMPrefs::Setting::updateWidget() { } const CSMPrefs::Category *CSMPrefs::Setting::getParent() const { return mParent; } const std::string& CSMPrefs::Setting::getKey() const { return mKey; } const std::string& CSMPrefs::Setting::getLabel() const { return mLabel; } int CSMPrefs::Setting::toInt() const { QMutexLocker lock (mMutex); return mValues->getInt (mKey, mParent->getKey()); } double CSMPrefs::Setting::toDouble() const { QMutexLocker lock (mMutex); return mValues->getFloat (mKey, mParent->getKey()); } std::string CSMPrefs::Setting::toString() const { QMutexLocker lock (mMutex); return mValues->getString (mKey, mParent->getKey()); } bool CSMPrefs::Setting::isTrue() const { QMutexLocker lock (mMutex); return mValues->getBool (mKey, mParent->getKey()); } QColor CSMPrefs::Setting::toColor() const { // toString() handles lock return QColor (QString::fromUtf8 (toString().c_str())); } bool CSMPrefs::operator== (const Setting& setting, const std::string& key) { std::string fullKey = setting.getParent()->getKey() + "/" + setting.getKey(); return fullKey==key; } bool CSMPrefs::operator== (const std::string& key, const Setting& setting) { return setting==key; } bool CSMPrefs::operator!= (const Setting& setting, const std::string& key) { return !(setting==key); } bool CSMPrefs::operator!= (const std::string& key, const Setting& setting) { return !(key==setting); } ================================================ FILE: apps/opencs/model/prefs/setting.hpp ================================================ #ifndef CSM_PREFS_SETTING_H #define CSM_PREFS_SETTING_H #include #include #include class QWidget; class QColor; class QMutex; namespace Settings { class Manager; } namespace CSMPrefs { class Category; class Setting : public QObject { Q_OBJECT Category *mParent; Settings::Manager *mValues; QMutex *mMutex; std::string mKey; std::string mLabel; protected: Settings::Manager& getValues(); QMutex *getMutex(); public: Setting (Category *parent, Settings::Manager *values, QMutex *mutex, const std::string& key, const std::string& label); virtual ~Setting(); /// Return label, input widget. /// /// \note first can be a 0-pointer, which means that the label is part of the input /// widget. virtual std::pair makeWidgets (QWidget *parent); /// Updates the widget returned by makeWidgets() to the current setting. /// /// \note If make_widgets() has not been called yet then nothing happens. virtual void updateWidget(); const Category *getParent() const; const std::string& getKey() const; const std::string& getLabel() const; int toInt() const; double toDouble() const; std::string toString() const; bool isTrue() const; QColor toColor() const; }; // note: fullKeys have the format categoryKey/settingKey bool operator== (const Setting& setting, const std::string& fullKey); bool operator== (const std::string& fullKey, const Setting& setting); bool operator!= (const Setting& setting, const std::string& fullKey); bool operator!= (const std::string& fullKey, const Setting& setting); } #endif ================================================ FILE: apps/opencs/model/prefs/shortcut.cpp ================================================ #include "shortcut.hpp" #include #include #include #include #include "state.hpp" #include "shortcutmanager.hpp" namespace CSMPrefs { Shortcut::Shortcut(const std::string& name, QWidget* parent) : QObject(parent) , mEnabled(true) , mName(name) , mModName("") , mSecondaryMode(SM_Ignore) , mModifier(0) , mCurrentPos(0) , mLastPos(0) , mActivationStatus(AS_Inactive) , mModifierStatus(false) , mAction(nullptr) { assert (parent); State::get().getShortcutManager().addShortcut(this); State::get().getShortcutManager().getSequence(name, mSequence); } Shortcut::Shortcut(const std::string& name, const std::string& modName, QWidget* parent) : QObject(parent) , mEnabled(true) , mName(name) , mModName(modName) , mSecondaryMode(SM_Ignore) , mModifier(0) , mCurrentPos(0) , mLastPos(0) , mActivationStatus(AS_Inactive) , mModifierStatus(false) , mAction(nullptr) { assert (parent); State::get().getShortcutManager().addShortcut(this); State::get().getShortcutManager().getSequence(name, mSequence); State::get().getShortcutManager().getModifier(modName, mModifier); } Shortcut::Shortcut(const std::string& name, const std::string& modName, SecondaryMode secMode, QWidget* parent) : QObject(parent) , mEnabled(true) , mName(name) , mModName(modName) , mSecondaryMode(secMode) , mModifier(0) , mCurrentPos(0) , mLastPos(0) , mActivationStatus(AS_Inactive) , mModifierStatus(false) , mAction(nullptr) { assert (parent); State::get().getShortcutManager().addShortcut(this); State::get().getShortcutManager().getSequence(name, mSequence); State::get().getShortcutManager().getModifier(modName, mModifier); } Shortcut::~Shortcut() { try { State::get().getShortcutManager().removeShortcut(this); } catch(const std::exception& e) { Log(Debug::Error) << "Error in the destructor: " << e.what(); } } bool Shortcut::isEnabled() const { return mEnabled; } const std::string& Shortcut::getName() const { return mName; } const std::string& Shortcut::getModifierName() const { return mModName; } Shortcut::SecondaryMode Shortcut::getSecondaryMode() const { return mSecondaryMode; } const QKeySequence& Shortcut::getSequence() const { return mSequence; } int Shortcut::getModifier() const { return mModifier; } int Shortcut::getPosition() const { return mCurrentPos; } int Shortcut::getLastPosition() const { return mLastPos; } Shortcut::ActivationStatus Shortcut::getActivationStatus() const { return mActivationStatus; } bool Shortcut::getModifierStatus() const { return mModifierStatus; } void Shortcut::enable(bool state) { mEnabled = state; } void Shortcut::setSequence(const QKeySequence& sequence) { mSequence = sequence; mCurrentPos = 0; mLastPos = sequence.count() - 1; if (mAction) { mAction->setText(mActionText + "\t" + State::get().getShortcutManager().convertToString(mSequence).data()); } } void Shortcut::setModifier(int modifier) { mModifier = modifier; } void Shortcut::setPosition(int pos) { mCurrentPos = pos; } void Shortcut::setActivationStatus(ActivationStatus status) { mActivationStatus = status; } void Shortcut::setModifierStatus(bool status) { mModifierStatus = status; } void Shortcut::associateAction(QAction* action) { if (mAction) { mAction->setText(mActionText); disconnect(this, SIGNAL(activated()), mAction, SLOT(trigger())); disconnect(mAction, SIGNAL(destroyed()), this, SLOT(actionDeleted())); } mAction = action; if (mAction) { mActionText = mAction->text(); mAction->setText(mActionText + "\t" + State::get().getShortcutManager().convertToString(mSequence).data()); connect(this, SIGNAL(activated()), mAction, SLOT(trigger())); connect(mAction, SIGNAL(destroyed()), this, SLOT(actionDeleted())); } } void Shortcut::signalActivated(bool state) { emit activated(state); } void Shortcut::signalActivated() { emit activated(); } void Shortcut::signalSecondary(bool state) { emit secondary(state); } void Shortcut::signalSecondary() { emit secondary(); } QString Shortcut::toString() const { return QString(State::get().getShortcutManager().convertToString(mSequence, mModifier).data()); } void Shortcut::actionDeleted() { mAction = nullptr; } } ================================================ FILE: apps/opencs/model/prefs/shortcut.hpp ================================================ #ifndef CSM_PREFS_SHORTCUT_H #define CSM_PREFS_SHORTCUT_H #include #include #include #include class QAction; class QWidget; namespace CSMPrefs { /// A class similar in purpose to QShortcut, but with the ability to use mouse buttons class Shortcut : public QObject { Q_OBJECT public: enum ActivationStatus { AS_Regular, AS_Secondary, AS_Inactive }; enum SecondaryMode { SM_Replace, ///< The secondary signal replaces the regular signal when the modifier is active SM_Detach, ///< The secondary signal is emitted independent of the regular signal, even if not active SM_Ignore ///< The secondary signal will not ever be emitted }; Shortcut(const std::string& name, QWidget* parent); Shortcut(const std::string& name, const std::string& modName, QWidget* parent); Shortcut(const std::string& name, const std::string& modName, SecondaryMode secMode, QWidget* parent); ~Shortcut(); bool isEnabled() const; const std::string& getName() const; const std::string& getModifierName() const; SecondaryMode getSecondaryMode() const; const QKeySequence& getSequence() const; int getModifier() const; /// The position in the sequence int getPosition() const; /// The position in the sequence int getLastPosition() const; ActivationStatus getActivationStatus() const; bool getModifierStatus() const; void enable(bool state); void setSequence(const QKeySequence& sequence); void setModifier(int modifier); /// The position in the sequence void setPosition(int pos); void setActivationStatus(ActivationStatus status); void setModifierStatus(bool status); /// Appends the sequence to the QAction text, also keeps it up to date void associateAction(QAction* action); // Workaround for Qt4 signals being "protected" void signalActivated(bool state); void signalActivated(); void signalSecondary(bool state); void signalSecondary(); QString toString() const; private: bool mEnabled; std::string mName; std::string mModName; SecondaryMode mSecondaryMode; QKeySequence mSequence; int mModifier; int mCurrentPos; int mLastPos; ActivationStatus mActivationStatus; bool mModifierStatus; QAction* mAction; QString mActionText; private slots: void actionDeleted(); signals: /// Triggered when the shortcut is activated or deactivated; can be determined from \p state void activated(bool state); /// Convenience signal. void activated(); /// Triggered depending on SecondaryMode void secondary(bool state); /// Convenience signal. void secondary(); }; } #endif ================================================ FILE: apps/opencs/model/prefs/shortcuteventhandler.cpp ================================================ #include "shortcuteventhandler.hpp" #include #include #include #include #include #include #include "shortcut.hpp" namespace CSMPrefs { ShortcutEventHandler::ShortcutEventHandler(QObject* parent) : QObject(parent) { } void ShortcutEventHandler::addShortcut(Shortcut* shortcut) { // Enforced by shortcut class QWidget* widget = static_cast(shortcut->parent()); // Check if widget setup is needed ShortcutMap::iterator shortcutListIt = mWidgetShortcuts.find(widget); if (shortcutListIt == mWidgetShortcuts.end()) { // Create list shortcutListIt = mWidgetShortcuts.insert(std::make_pair(widget, ShortcutList())).first; // Check if widget has a parent with shortcuts, unfortunately it is not typically set yet updateParent(widget); // Intercept widget events widget->installEventFilter(this); connect(widget, SIGNAL(destroyed()), this, SLOT(widgetDestroyed())); } // Add to list shortcutListIt->second.push_back(shortcut); } void ShortcutEventHandler::removeShortcut(Shortcut* shortcut) { // Enforced by shortcut class QWidget* widget = static_cast(shortcut->parent()); ShortcutMap::iterator shortcutListIt = mWidgetShortcuts.find(widget); if (shortcutListIt != mWidgetShortcuts.end()) { shortcutListIt->second.erase(std::remove(shortcutListIt->second.begin(), shortcutListIt->second.end(), shortcut), shortcutListIt->second.end()); } } bool ShortcutEventHandler::eventFilter(QObject* watched, QEvent* event) { // Process event if (event->type() == QEvent::KeyPress) { QWidget* widget = static_cast(watched); QKeyEvent* keyEvent = static_cast(event); unsigned int mod = (unsigned int) keyEvent->modifiers(); unsigned int key = (unsigned int) keyEvent->key(); if (!keyEvent->isAutoRepeat()) return activate(widget, mod, key); } else if (event->type() == QEvent::KeyRelease) { QWidget* widget = static_cast(watched); QKeyEvent* keyEvent = static_cast(event); unsigned int mod = (unsigned int) keyEvent->modifiers(); unsigned int key = (unsigned int) keyEvent->key(); if (!keyEvent->isAutoRepeat()) return deactivate(widget, mod, key); } else if (event->type() == QEvent::MouseButtonPress) { QWidget* widget = static_cast(watched); QMouseEvent* mouseEvent = static_cast(event); unsigned int mod = (unsigned int) mouseEvent->modifiers(); unsigned int button = (unsigned int) mouseEvent->button(); return activate(widget, mod, button); } else if (event->type() == QEvent::MouseButtonRelease) { QWidget* widget = static_cast(watched); QMouseEvent* mouseEvent = static_cast(event); unsigned int mod = (unsigned int) mouseEvent->modifiers(); unsigned int button = (unsigned int) mouseEvent->button(); return deactivate(widget, mod, button); } else if (event->type() == QEvent::FocusOut) { QWidget* widget = static_cast(watched); ShortcutMap::iterator shortcutListIt = mWidgetShortcuts.find(widget); // Deactivate in case events are missed for (ShortcutList::iterator it = shortcutListIt->second.begin(); it != shortcutListIt->second.end(); ++it) { Shortcut* shortcut = *it; shortcut->setPosition(0); shortcut->setModifierStatus(false); if (shortcut->getActivationStatus() == Shortcut::AS_Regular) { shortcut->setActivationStatus(Shortcut::AS_Inactive); shortcut->signalActivated(false); } else if (shortcut->getActivationStatus() == Shortcut::AS_Secondary) { shortcut->setActivationStatus(Shortcut::AS_Inactive); shortcut->signalSecondary(false); } } } else if (event->type() == QEvent::FocusIn) { QWidget* widget = static_cast(watched); updateParent(widget); } return false; } void ShortcutEventHandler::updateParent(QWidget* widget) { QWidget* parent = widget->parentWidget(); while (parent) { ShortcutMap::iterator parentIt = mWidgetShortcuts.find(parent); if (parentIt != mWidgetShortcuts.end()) { mChildParentRelations.insert(std::make_pair(widget, parent)); updateParent(parent); break; } // Check next parent = parent->parentWidget(); } } bool ShortcutEventHandler::activate(QWidget* widget, unsigned int mod, unsigned int button) { std::vector > potentials; bool used = false; while (widget) { ShortcutMap::iterator shortcutListIt = mWidgetShortcuts.find(widget); assert(shortcutListIt != mWidgetShortcuts.end()); // Find potential activations for (ShortcutList::iterator it = shortcutListIt->second.begin(); it != shortcutListIt->second.end(); ++it) { Shortcut* shortcut = *it; if (!shortcut->isEnabled()) continue; if (checkModifier(mod, button, shortcut, true)) used = true; if (shortcut->getActivationStatus() != Shortcut::AS_Inactive) continue; int pos = shortcut->getPosition(); int lastPos = shortcut->getLastPosition(); MatchResult result = match(mod, button, shortcut->getSequence()[pos]); if (result == Matches_WithMod || result == Matches_NoMod) { if (pos < lastPos && (result == Matches_WithMod || pos > 0)) { shortcut->setPosition(pos+1); } else if (pos == lastPos) { potentials.emplace_back(result, shortcut); } } } // Move on to parent WidgetMap::iterator widgetIt = mChildParentRelations.find(widget); widget = (widgetIt != mChildParentRelations.end()) ? widgetIt->second : 0; } // Only activate the best match; in exact conflicts, this will favor the first shortcut added. if (!potentials.empty()) { std::stable_sort(potentials.begin(), potentials.end(), ShortcutEventHandler::sort); Shortcut* shortcut = potentials.front().second; if (shortcut->getModifierStatus() && shortcut->getSecondaryMode() == Shortcut::SM_Replace) { shortcut->setActivationStatus(Shortcut::AS_Secondary); shortcut->signalSecondary(true); shortcut->signalSecondary(); } else { shortcut->setActivationStatus(Shortcut::AS_Regular); shortcut->signalActivated(true); shortcut->signalActivated(); } used = true; } return used; } bool ShortcutEventHandler::deactivate(QWidget* widget, unsigned int mod, unsigned int button) { const int KeyMask = 0x01FFFFFF; bool used = false; while (widget) { ShortcutMap::iterator shortcutListIt = mWidgetShortcuts.find(widget); assert(shortcutListIt != mWidgetShortcuts.end()); for (ShortcutList::iterator it = shortcutListIt->second.begin(); it != shortcutListIt->second.end(); ++it) { Shortcut* shortcut = *it; if (checkModifier(mod, button, shortcut, false)) used = true; int pos = shortcut->getPosition(); MatchResult result = match(0, button, shortcut->getSequence()[pos] & KeyMask); if (result != Matches_Not) { shortcut->setPosition(0); if (shortcut->getActivationStatus() == Shortcut::AS_Regular) { shortcut->setActivationStatus(Shortcut::AS_Inactive); shortcut->signalActivated(false); used = true; } else if (shortcut->getActivationStatus() == Shortcut::AS_Secondary) { shortcut->setActivationStatus(Shortcut::AS_Inactive); shortcut->signalSecondary(false); used = true; } } } // Move on to parent WidgetMap::iterator widgetIt = mChildParentRelations.find(widget); widget = (widgetIt != mChildParentRelations.end()) ? widgetIt->second : 0; } return used; } bool ShortcutEventHandler::checkModifier(unsigned int mod, unsigned int button, Shortcut* shortcut, bool activate) { if (!shortcut->isEnabled() || !shortcut->getModifier() || shortcut->getSecondaryMode() == Shortcut::SM_Ignore || shortcut->getModifierStatus() == activate) return false; MatchResult result = match(mod, button, shortcut->getModifier()); bool used = false; if (result != Matches_Not) { shortcut->setModifierStatus(activate); if (shortcut->getSecondaryMode() == Shortcut::SM_Detach) { if (activate) { shortcut->signalSecondary(true); shortcut->signalSecondary(); } else { shortcut->signalSecondary(false); } } else if (!activate && shortcut->getActivationStatus() == Shortcut::AS_Secondary) { shortcut->setActivationStatus(Shortcut::AS_Inactive); shortcut->setPosition(0); shortcut->signalSecondary(false); used = true; } } return used; } ShortcutEventHandler::MatchResult ShortcutEventHandler::match(unsigned int mod, unsigned int button, unsigned int value) { if ((mod | button) == value) { return Matches_WithMod; } else if (button == value) { return Matches_NoMod; } else { return Matches_Not; } } bool ShortcutEventHandler::sort(const std::pair& left, const std::pair& right) { if (left.first == Matches_WithMod && right.first == Matches_NoMod) return true; else return left.second->getPosition() > right.second->getPosition(); } void ShortcutEventHandler::widgetDestroyed() { QWidget* widget = static_cast(sender()); mWidgetShortcuts.erase(widget); mChildParentRelations.erase(widget); } } ================================================ FILE: apps/opencs/model/prefs/shortcuteventhandler.hpp ================================================ #ifndef CSM_PREFS_SHORTCUT_EVENT_HANDLER_H #define CSM_PREFS_SHORTCUT_EVENT_HANDLER_H #include #include #include class QEvent; class QWidget; namespace CSMPrefs { class Shortcut; /// Users of this class should install it as an event handler class ShortcutEventHandler : public QObject { Q_OBJECT public: ShortcutEventHandler(QObject* parent); void addShortcut(Shortcut* shortcut); void removeShortcut(Shortcut* shortcut); protected: bool eventFilter(QObject* watched, QEvent* event) override; private: typedef std::vector ShortcutList; // Child, Parent typedef std::map WidgetMap; typedef std::map ShortcutMap; enum MatchResult { Matches_WithMod, Matches_NoMod, Matches_Not }; void updateParent(QWidget* widget); bool activate(QWidget* widget, unsigned int mod, unsigned int button); bool deactivate(QWidget* widget, unsigned int mod, unsigned int button); bool checkModifier(unsigned int mod, unsigned int button, Shortcut* shortcut, bool activate); MatchResult match(unsigned int mod, unsigned int button, unsigned int value); // Prefers Matches_WithMod and a larger number of buttons static bool sort(const std::pair& left, const std::pair& right); WidgetMap mChildParentRelations; ShortcutMap mWidgetShortcuts; private slots: void widgetDestroyed(); }; } #endif ================================================ FILE: apps/opencs/model/prefs/shortcutmanager.cpp ================================================ #include "shortcutmanager.hpp" #include #include #include #include "shortcut.hpp" #include "shortcuteventhandler.hpp" namespace CSMPrefs { ShortcutManager::ShortcutManager() { createLookupTables(); mEventHandler = new ShortcutEventHandler(this); } void ShortcutManager::addShortcut(Shortcut* shortcut) { mShortcuts.insert(std::make_pair(shortcut->getName(), shortcut)); mShortcuts.insert(std::make_pair(shortcut->getModifierName(), shortcut)); mEventHandler->addShortcut(shortcut); } void ShortcutManager::removeShortcut(Shortcut* shortcut) { std::pair range = mShortcuts.equal_range(shortcut->getName()); for (ShortcutMap::iterator it = range.first; it != range.second;) { if (it->second == shortcut) { mShortcuts.erase(it++); } else { ++it; } } mEventHandler->removeShortcut(shortcut); } bool ShortcutManager::getSequence(const std::string& name, QKeySequence& sequence) const { SequenceMap::const_iterator item = mSequences.find(name); if (item != mSequences.end()) { sequence = item->second; return true; } else return false; } void ShortcutManager::setSequence(const std::string& name, const QKeySequence& sequence) { // Add to map/modify SequenceMap::iterator item = mSequences.find(name); if (item != mSequences.end()) { item->second = sequence; } else { mSequences.insert(std::make_pair(name, sequence)); } // Change active shortcuts std::pair rangeS = mShortcuts.equal_range(name); for (ShortcutMap::iterator it = rangeS.first; it != rangeS.second; ++it) { it->second->setSequence(sequence); } } bool ShortcutManager::getModifier(const std::string& name, int& modifier) const { ModifierMap::const_iterator item = mModifiers.find(name); if (item != mModifiers.end()) { modifier = item->second; return true; } else return false; } void ShortcutManager::setModifier(const std::string& name, int modifier) { // Add to map/modify ModifierMap::iterator item = mModifiers.find(name); if (item != mModifiers.end()) { item->second = modifier; } else { mModifiers.insert(std::make_pair(name, modifier)); } // Change active shortcuts std::pair rangeS = mShortcuts.equal_range(name); for (ShortcutMap::iterator it = rangeS.first; it != rangeS.second; ++it) { it->second->setModifier(modifier); } } std::string ShortcutManager::convertToString(const QKeySequence& sequence) const { const int MouseKeyMask = 0x01FFFFFF; const int ModMask = 0x7E000000; std::string result; for (int i = 0; i < (int)sequence.count(); ++i) { int mods = sequence[i] & ModMask; int key = sequence[i] & MouseKeyMask; if (key) { NameMap::const_iterator searchResult = mNames.find(key); if (searchResult != mNames.end()) { if (mods && i == 0) { if (mods & Qt::ControlModifier) result.append("Ctrl+"); if (mods & Qt::ShiftModifier) result.append("Shift+"); if (mods & Qt::AltModifier) result.append("Alt+"); if (mods & Qt::MetaModifier) result.append("Meta+"); if (mods & Qt::KeypadModifier) result.append("Keypad+"); if (mods & Qt::GroupSwitchModifier) result.append("GroupSwitch+"); } else if (i > 0) { result.append("+"); } result.append(searchResult->second); } } } return result; } std::string ShortcutManager::convertToString(int modifier) const { NameMap::const_iterator searchResult = mNames.find(modifier); if (searchResult != mNames.end()) { return searchResult->second; } else return ""; } std::string ShortcutManager::convertToString(const QKeySequence& sequence, int modifier) const { std::string concat = convertToString(sequence) + ";" + convertToString(modifier); return concat; } void ShortcutManager::convertFromString(const std::string& data, QKeySequence& sequence) const { const int MaxKeys = 4; // A limitation of QKeySequence size_t end = data.find(';'); size_t size = std::min(end, data.size()); std::string value = data.substr(0, size); size_t start = 0; int keyPos = 0; int mods = 0; int keys[MaxKeys] = {}; while (start < value.size()) { end = data.find('+', start); end = std::min(end, value.size()); std::string name = value.substr(start, end - start); if (name == "Ctrl") { mods |= Qt::ControlModifier; } else if (name == "Shift") { mods |= Qt::ShiftModifier; } else if (name == "Alt") { mods |= Qt::AltModifier; } else if (name == "Meta") { mods |= Qt::MetaModifier; } else if (name == "Keypad") { mods |= Qt::KeypadModifier; } else if (name == "GroupSwitch") { mods |= Qt::GroupSwitchModifier; } else { KeyMap::const_iterator searchResult = mKeys.find(name); if (searchResult != mKeys.end()) { keys[keyPos] = mods | searchResult->second; mods = 0; keyPos += 1; if (keyPos >= MaxKeys) break; } } start = end + 1; } sequence = QKeySequence(keys[0], keys[1], keys[2], keys[3]); } void ShortcutManager::convertFromString(const std::string& data, int& modifier) const { size_t start = data.find(';') + 1; start = std::min(start, data.size()); std::string name = data.substr(start); KeyMap::const_iterator searchResult = mKeys.find(name); if (searchResult != mKeys.end()) { modifier = searchResult->second; } else { modifier = 0; } } void ShortcutManager::convertFromString(const std::string& data, QKeySequence& sequence, int& modifier) const { convertFromString(data, sequence); convertFromString(data, modifier); } void ShortcutManager::createLookupTables() { // Mouse buttons mNames.insert(std::make_pair(Qt::LeftButton, "LMB")); mNames.insert(std::make_pair(Qt::RightButton, "RMB")); mNames.insert(std::make_pair(Qt::MiddleButton, "MMB")); mNames.insert(std::make_pair(Qt::XButton1, "Mouse4")); mNames.insert(std::make_pair(Qt::XButton2, "Mouse5")); // Keyboard buttons for (size_t i = 0; QtKeys[i].first != 0; ++i) { mNames.insert(QtKeys[i]); } // Generate inverse map for (NameMap::const_iterator it = mNames.begin(); it != mNames.end(); ++it) { mKeys.insert(std::make_pair(it->second, it->first)); } } QString ShortcutManager::processToolTip(const QString& toolTip) const { const QChar SequenceStart = '{'; const QChar SequenceEnd = '}'; QStringList substrings; int prevIndex = 0; int startIndex = toolTip.indexOf(SequenceStart); int endIndex = (startIndex != -1) ? toolTip.indexOf(SequenceEnd, startIndex) : -1; // Process every valid shortcut escape sequence while (startIndex != -1 && endIndex != -1) { int count = startIndex - prevIndex; if (count > 0) { substrings.push_back(toolTip.mid(prevIndex, count)); } // Find sequence name startIndex += 1; // '{' character count = endIndex - startIndex; if (count > 0) { QString settingName = toolTip.mid(startIndex, count); QKeySequence sequence; int modifier; if (getSequence(settingName.toUtf8().data(), sequence)) { QString value = QString::fromUtf8(convertToString(sequence).c_str()); substrings.push_back(value); } else if (getModifier(settingName.toUtf8().data(), modifier)) { QString value = QString::fromUtf8(convertToString(modifier).c_str()); substrings.push_back(value); } prevIndex = endIndex + 1; // '}' character } startIndex = toolTip.indexOf(SequenceStart, endIndex); endIndex = (startIndex != -1) ? toolTip.indexOf(SequenceEnd, startIndex) : -1; } if (prevIndex < toolTip.size()) { substrings.push_back(toolTip.mid(prevIndex)); } return substrings.join(""); } const std::pair ShortcutManager::QtKeys[] = { std::make_pair((int)Qt::Key_Space , "Space"), std::make_pair((int)Qt::Key_Exclam , "Exclam"), std::make_pair((int)Qt::Key_QuoteDbl , "QuoteDbl"), std::make_pair((int)Qt::Key_NumberSign , "NumberSign"), std::make_pair((int)Qt::Key_Dollar , "Dollar"), std::make_pair((int)Qt::Key_Percent , "Percent"), std::make_pair((int)Qt::Key_Ampersand , "Ampersand"), std::make_pair((int)Qt::Key_Apostrophe , "Apostrophe"), std::make_pair((int)Qt::Key_ParenLeft , "ParenLeft"), std::make_pair((int)Qt::Key_ParenRight , "ParenRight"), std::make_pair((int)Qt::Key_Asterisk , "Asterisk"), std::make_pair((int)Qt::Key_Plus , "Plus"), std::make_pair((int)Qt::Key_Comma , "Comma"), std::make_pair((int)Qt::Key_Minus , "Minus"), std::make_pair((int)Qt::Key_Period , "Period"), std::make_pair((int)Qt::Key_Slash , "Slash"), std::make_pair((int)Qt::Key_0 , "0"), std::make_pair((int)Qt::Key_1 , "1"), std::make_pair((int)Qt::Key_2 , "2"), std::make_pair((int)Qt::Key_3 , "3"), std::make_pair((int)Qt::Key_4 , "4"), std::make_pair((int)Qt::Key_5 , "5"), std::make_pair((int)Qt::Key_6 , "6"), std::make_pair((int)Qt::Key_7 , "7"), std::make_pair((int)Qt::Key_8 , "8"), std::make_pair((int)Qt::Key_9 , "9"), std::make_pair((int)Qt::Key_Colon , "Colon"), std::make_pair((int)Qt::Key_Semicolon , "Semicolon"), std::make_pair((int)Qt::Key_Less , "Less"), std::make_pair((int)Qt::Key_Equal , "Equal"), std::make_pair((int)Qt::Key_Greater , "Greater"), std::make_pair((int)Qt::Key_Question , "Question"), std::make_pair((int)Qt::Key_At , "At"), std::make_pair((int)Qt::Key_A , "A"), std::make_pair((int)Qt::Key_B , "B"), std::make_pair((int)Qt::Key_C , "C"), std::make_pair((int)Qt::Key_D , "D"), std::make_pair((int)Qt::Key_E , "E"), std::make_pair((int)Qt::Key_F , "F"), std::make_pair((int)Qt::Key_G , "G"), std::make_pair((int)Qt::Key_H , "H"), std::make_pair((int)Qt::Key_I , "I"), std::make_pair((int)Qt::Key_J , "J"), std::make_pair((int)Qt::Key_K , "K"), std::make_pair((int)Qt::Key_L , "L"), std::make_pair((int)Qt::Key_M , "M"), std::make_pair((int)Qt::Key_N , "N"), std::make_pair((int)Qt::Key_O , "O"), std::make_pair((int)Qt::Key_P , "P"), std::make_pair((int)Qt::Key_Q , "Q"), std::make_pair((int)Qt::Key_R , "R"), std::make_pair((int)Qt::Key_S , "S"), std::make_pair((int)Qt::Key_T , "T"), std::make_pair((int)Qt::Key_U , "U"), std::make_pair((int)Qt::Key_V , "V"), std::make_pair((int)Qt::Key_W , "W"), std::make_pair((int)Qt::Key_X , "X"), std::make_pair((int)Qt::Key_Y , "Y"), std::make_pair((int)Qt::Key_Z , "Z"), std::make_pair((int)Qt::Key_BracketLeft , "BracketLeft"), std::make_pair((int)Qt::Key_Backslash , "Backslash"), std::make_pair((int)Qt::Key_BracketRight , "BracketRight"), std::make_pair((int)Qt::Key_AsciiCircum , "AsciiCircum"), std::make_pair((int)Qt::Key_Underscore , "Underscore"), std::make_pair((int)Qt::Key_QuoteLeft , "QuoteLeft"), std::make_pair((int)Qt::Key_BraceLeft , "BraceLeft"), std::make_pair((int)Qt::Key_Bar , "Bar"), std::make_pair((int)Qt::Key_BraceRight , "BraceRight"), std::make_pair((int)Qt::Key_AsciiTilde , "AsciiTilde"), std::make_pair((int)Qt::Key_nobreakspace , "nobreakspace"), std::make_pair((int)Qt::Key_exclamdown , "exclamdown"), std::make_pair((int)Qt::Key_cent , "cent"), std::make_pair((int)Qt::Key_sterling , "sterling"), std::make_pair((int)Qt::Key_currency , "currency"), std::make_pair((int)Qt::Key_yen , "yen"), std::make_pair((int)Qt::Key_brokenbar , "brokenbar"), std::make_pair((int)Qt::Key_section , "section"), std::make_pair((int)Qt::Key_diaeresis , "diaeresis"), std::make_pair((int)Qt::Key_copyright , "copyright"), std::make_pair((int)Qt::Key_ordfeminine , "ordfeminine"), std::make_pair((int)Qt::Key_guillemotleft , "guillemotleft"), std::make_pair((int)Qt::Key_notsign , "notsign"), std::make_pair((int)Qt::Key_hyphen , "hyphen"), std::make_pair((int)Qt::Key_registered , "registered"), std::make_pair((int)Qt::Key_macron , "macron"), std::make_pair((int)Qt::Key_degree , "degree"), std::make_pair((int)Qt::Key_plusminus , "plusminus"), std::make_pair((int)Qt::Key_twosuperior , "twosuperior"), std::make_pair((int)Qt::Key_threesuperior , "threesuperior"), std::make_pair((int)Qt::Key_acute , "acute"), std::make_pair((int)Qt::Key_mu , "mu"), std::make_pair((int)Qt::Key_paragraph , "paragraph"), std::make_pair((int)Qt::Key_periodcentered , "periodcentered"), std::make_pair((int)Qt::Key_cedilla , "cedilla"), std::make_pair((int)Qt::Key_onesuperior , "onesuperior"), std::make_pair((int)Qt::Key_masculine , "masculine"), std::make_pair((int)Qt::Key_guillemotright , "guillemotright"), std::make_pair((int)Qt::Key_onequarter , "onequarter"), std::make_pair((int)Qt::Key_onehalf , "onehalf"), std::make_pair((int)Qt::Key_threequarters , "threequarters"), std::make_pair((int)Qt::Key_questiondown , "questiondown"), std::make_pair((int)Qt::Key_Agrave , "Agrave"), std::make_pair((int)Qt::Key_Aacute , "Aacute"), std::make_pair((int)Qt::Key_Acircumflex , "Acircumflex"), std::make_pair((int)Qt::Key_Atilde , "Atilde"), std::make_pair((int)Qt::Key_Adiaeresis , "Adiaeresis"), std::make_pair((int)Qt::Key_Aring , "Aring"), std::make_pair((int)Qt::Key_AE , "AE"), std::make_pair((int)Qt::Key_Ccedilla , "Ccedilla"), std::make_pair((int)Qt::Key_Egrave , "Egrave"), std::make_pair((int)Qt::Key_Eacute , "Eacute"), std::make_pair((int)Qt::Key_Ecircumflex , "Ecircumflex"), std::make_pair((int)Qt::Key_Ediaeresis , "Ediaeresis"), std::make_pair((int)Qt::Key_Igrave , "Igrave"), std::make_pair((int)Qt::Key_Iacute , "Iacute"), std::make_pair((int)Qt::Key_Icircumflex , "Icircumflex"), std::make_pair((int)Qt::Key_Idiaeresis , "Idiaeresis"), std::make_pair((int)Qt::Key_ETH , "ETH"), std::make_pair((int)Qt::Key_Ntilde , "Ntilde"), std::make_pair((int)Qt::Key_Ograve , "Ograve"), std::make_pair((int)Qt::Key_Oacute , "Oacute"), std::make_pair((int)Qt::Key_Ocircumflex , "Ocircumflex"), std::make_pair((int)Qt::Key_Otilde , "Otilde"), std::make_pair((int)Qt::Key_Odiaeresis , "Odiaeresis"), std::make_pair((int)Qt::Key_multiply , "multiply"), std::make_pair((int)Qt::Key_Ooblique , "Ooblique"), std::make_pair((int)Qt::Key_Ugrave , "Ugrave"), std::make_pair((int)Qt::Key_Uacute , "Uacute"), std::make_pair((int)Qt::Key_Ucircumflex , "Ucircumflex"), std::make_pair((int)Qt::Key_Udiaeresis , "Udiaeresis"), std::make_pair((int)Qt::Key_Yacute , "Yacute"), std::make_pair((int)Qt::Key_THORN , "THORN"), std::make_pair((int)Qt::Key_ssharp , "ssharp"), std::make_pair((int)Qt::Key_division , "division"), std::make_pair((int)Qt::Key_ydiaeresis , "ydiaeresis"), std::make_pair((int)Qt::Key_Escape , "Escape"), std::make_pair((int)Qt::Key_Tab , "Tab"), std::make_pair((int)Qt::Key_Backtab , "Backtab"), std::make_pair((int)Qt::Key_Backspace , "Backspace"), std::make_pair((int)Qt::Key_Return , "Return"), std::make_pair((int)Qt::Key_Enter , "Enter"), std::make_pair((int)Qt::Key_Insert , "Insert"), std::make_pair((int)Qt::Key_Delete , "Delete"), std::make_pair((int)Qt::Key_Pause , "Pause"), std::make_pair((int)Qt::Key_Print , "Print"), std::make_pair((int)Qt::Key_SysReq , "SysReq"), std::make_pair((int)Qt::Key_Clear , "Clear"), std::make_pair((int)Qt::Key_Home , "Home"), std::make_pair((int)Qt::Key_End , "End"), std::make_pair((int)Qt::Key_Left , "Left"), std::make_pair((int)Qt::Key_Up , "Up"), std::make_pair((int)Qt::Key_Right , "Right"), std::make_pair((int)Qt::Key_Down , "Down"), std::make_pair((int)Qt::Key_PageUp , "PageUp"), std::make_pair((int)Qt::Key_PageDown , "PageDown"), std::make_pair((int)Qt::Key_Shift , "Shift"), std::make_pair((int)Qt::Key_Control , "Control"), std::make_pair((int)Qt::Key_Meta , "Meta"), std::make_pair((int)Qt::Key_Alt , "Alt"), std::make_pair((int)Qt::Key_CapsLock , "CapsLock"), std::make_pair((int)Qt::Key_NumLock , "NumLock"), std::make_pair((int)Qt::Key_ScrollLock , "ScrollLock"), std::make_pair((int)Qt::Key_F1 , "F1"), std::make_pair((int)Qt::Key_F2 , "F2"), std::make_pair((int)Qt::Key_F3 , "F3"), std::make_pair((int)Qt::Key_F4 , "F4"), std::make_pair((int)Qt::Key_F5 , "F5"), std::make_pair((int)Qt::Key_F6 , "F6"), std::make_pair((int)Qt::Key_F7 , "F7"), std::make_pair((int)Qt::Key_F8 , "F8"), std::make_pair((int)Qt::Key_F9 , "F9"), std::make_pair((int)Qt::Key_F10 , "F10"), std::make_pair((int)Qt::Key_F11 , "F11"), std::make_pair((int)Qt::Key_F12 , "F12"), std::make_pair((int)Qt::Key_F13 , "F13"), std::make_pair((int)Qt::Key_F14 , "F14"), std::make_pair((int)Qt::Key_F15 , "F15"), std::make_pair((int)Qt::Key_F16 , "F16"), std::make_pair((int)Qt::Key_F17 , "F17"), std::make_pair((int)Qt::Key_F18 , "F18"), std::make_pair((int)Qt::Key_F19 , "F19"), std::make_pair((int)Qt::Key_F20 , "F20"), std::make_pair((int)Qt::Key_F21 , "F21"), std::make_pair((int)Qt::Key_F22 , "F22"), std::make_pair((int)Qt::Key_F23 , "F23"), std::make_pair((int)Qt::Key_F24 , "F24"), std::make_pair((int)Qt::Key_F25 , "F25"), std::make_pair((int)Qt::Key_F26 , "F26"), std::make_pair((int)Qt::Key_F27 , "F27"), std::make_pair((int)Qt::Key_F28 , "F28"), std::make_pair((int)Qt::Key_F29 , "F29"), std::make_pair((int)Qt::Key_F30 , "F30"), std::make_pair((int)Qt::Key_F31 , "F31"), std::make_pair((int)Qt::Key_F32 , "F32"), std::make_pair((int)Qt::Key_F33 , "F33"), std::make_pair((int)Qt::Key_F34 , "F34"), std::make_pair((int)Qt::Key_F35 , "F35"), std::make_pair((int)Qt::Key_Super_L , "Super_L"), std::make_pair((int)Qt::Key_Super_R , "Super_R"), std::make_pair((int)Qt::Key_Menu , "Menu"), std::make_pair((int)Qt::Key_Hyper_L , "Hyper_L"), std::make_pair((int)Qt::Key_Hyper_R , "Hyper_R"), std::make_pair((int)Qt::Key_Help , "Help"), std::make_pair((int)Qt::Key_Direction_L , "Direction_L"), std::make_pair((int)Qt::Key_Direction_R , "Direction_R"), std::make_pair((int)Qt::Key_Back , "Back"), std::make_pair((int)Qt::Key_Forward , "Forward"), std::make_pair((int)Qt::Key_Stop , "Stop"), std::make_pair((int)Qt::Key_Refresh , "Refresh"), std::make_pair((int)Qt::Key_VolumeDown , "VolumeDown"), std::make_pair((int)Qt::Key_VolumeMute , "VolumeMute"), std::make_pair((int)Qt::Key_VolumeUp , "VolumeUp"), std::make_pair((int)Qt::Key_BassBoost , "BassBoost"), std::make_pair((int)Qt::Key_BassUp , "BassUp"), std::make_pair((int)Qt::Key_BassDown , "BassDown"), std::make_pair((int)Qt::Key_TrebleUp , "TrebleUp"), std::make_pair((int)Qt::Key_TrebleDown , "TrebleDown"), std::make_pair((int)Qt::Key_MediaPlay , "MediaPlay"), std::make_pair((int)Qt::Key_MediaStop , "MediaStop"), std::make_pair((int)Qt::Key_MediaPrevious , "MediaPrevious"), std::make_pair((int)Qt::Key_MediaNext , "MediaNext"), std::make_pair((int)Qt::Key_MediaRecord , "MediaRecord"), std::make_pair((int)Qt::Key_MediaPause , "MediaPause"), std::make_pair((int)Qt::Key_MediaTogglePlayPause , "MediaTogglePlayPause"), std::make_pair((int)Qt::Key_HomePage , "HomePage"), std::make_pair((int)Qt::Key_Favorites , "Favorites"), std::make_pair((int)Qt::Key_Search , "Search"), std::make_pair((int)Qt::Key_Standby , "Standby"), std::make_pair((int)Qt::Key_OpenUrl , "OpenUrl"), std::make_pair((int)Qt::Key_LaunchMail , "LaunchMail"), std::make_pair((int)Qt::Key_LaunchMedia , "LaunchMedia"), std::make_pair((int)Qt::Key_Launch0 , "Launch0"), std::make_pair((int)Qt::Key_Launch1 , "Launch1"), std::make_pair((int)Qt::Key_Launch2 , "Launch2"), std::make_pair((int)Qt::Key_Launch3 , "Launch3"), std::make_pair((int)Qt::Key_Launch4 , "Launch4"), std::make_pair((int)Qt::Key_Launch5 , "Launch5"), std::make_pair((int)Qt::Key_Launch6 , "Launch6"), std::make_pair((int)Qt::Key_Launch7 , "Launch7"), std::make_pair((int)Qt::Key_Launch8 , "Launch8"), std::make_pair((int)Qt::Key_Launch9 , "Launch9"), std::make_pair((int)Qt::Key_LaunchA , "LaunchA"), std::make_pair((int)Qt::Key_LaunchB , "LaunchB"), std::make_pair((int)Qt::Key_LaunchC , "LaunchC"), std::make_pair((int)Qt::Key_LaunchD , "LaunchD"), std::make_pair((int)Qt::Key_LaunchE , "LaunchE"), std::make_pair((int)Qt::Key_LaunchF , "LaunchF"), std::make_pair((int)Qt::Key_MonBrightnessUp , "MonBrightnessUp"), std::make_pair((int)Qt::Key_MonBrightnessDown , "MonBrightnessDown"), std::make_pair((int)Qt::Key_KeyboardLightOnOff , "KeyboardLightOnOff"), std::make_pair((int)Qt::Key_KeyboardBrightnessUp , "KeyboardBrightnessUp"), std::make_pair((int)Qt::Key_KeyboardBrightnessDown , "KeyboardBrightnessDown"), std::make_pair((int)Qt::Key_PowerOff , "PowerOff"), std::make_pair((int)Qt::Key_WakeUp , "WakeUp"), std::make_pair((int)Qt::Key_Eject , "Eject"), std::make_pair((int)Qt::Key_ScreenSaver , "ScreenSaver"), std::make_pair((int)Qt::Key_WWW , "WWW"), std::make_pair((int)Qt::Key_Memo , "Memo"), std::make_pair((int)Qt::Key_LightBulb , "LightBulb"), std::make_pair((int)Qt::Key_Shop , "Shop"), std::make_pair((int)Qt::Key_History , "History"), std::make_pair((int)Qt::Key_AddFavorite , "AddFavorite"), std::make_pair((int)Qt::Key_HotLinks , "HotLinks"), std::make_pair((int)Qt::Key_BrightnessAdjust , "BrightnessAdjust"), std::make_pair((int)Qt::Key_Finance , "Finance"), std::make_pair((int)Qt::Key_Community , "Community"), std::make_pair((int)Qt::Key_AudioRewind , "AudioRewind"), std::make_pair((int)Qt::Key_BackForward , "BackForward"), std::make_pair((int)Qt::Key_ApplicationLeft , "ApplicationLeft"), std::make_pair((int)Qt::Key_ApplicationRight , "ApplicationRight"), std::make_pair((int)Qt::Key_Book , "Book"), std::make_pair((int)Qt::Key_CD , "CD"), std::make_pair((int)Qt::Key_Calculator , "Calculator"), std::make_pair((int)Qt::Key_ToDoList , "ToDoList"), std::make_pair((int)Qt::Key_ClearGrab , "ClearGrab"), std::make_pair((int)Qt::Key_Close , "Close"), std::make_pair((int)Qt::Key_Copy , "Copy"), std::make_pair((int)Qt::Key_Cut , "Cut"), std::make_pair((int)Qt::Key_Display , "Display"), std::make_pair((int)Qt::Key_DOS , "DOS"), std::make_pair((int)Qt::Key_Documents , "Documents"), std::make_pair((int)Qt::Key_Excel , "Excel"), std::make_pair((int)Qt::Key_Explorer , "Explorer"), std::make_pair((int)Qt::Key_Game , "Game"), std::make_pair((int)Qt::Key_Go , "Go"), std::make_pair((int)Qt::Key_iTouch , "iTouch"), std::make_pair((int)Qt::Key_LogOff , "LogOff"), std::make_pair((int)Qt::Key_Market , "Market"), std::make_pair((int)Qt::Key_Meeting , "Meeting"), std::make_pair((int)Qt::Key_MenuKB , "MenuKB"), std::make_pair((int)Qt::Key_MenuPB , "MenuPB"), std::make_pair((int)Qt::Key_MySites , "MySites"), std::make_pair((int)Qt::Key_News , "News"), std::make_pair((int)Qt::Key_OfficeHome , "OfficeHome"), std::make_pair((int)Qt::Key_Option , "Option"), std::make_pair((int)Qt::Key_Paste , "Paste"), std::make_pair((int)Qt::Key_Phone , "Phone"), std::make_pair((int)Qt::Key_Calendar , "Calendar"), std::make_pair((int)Qt::Key_Reply , "Reply"), std::make_pair((int)Qt::Key_Reload , "Reload"), std::make_pair((int)Qt::Key_RotateWindows , "RotateWindows"), std::make_pair((int)Qt::Key_RotationPB , "RotationPB"), std::make_pair((int)Qt::Key_RotationKB , "RotationKB"), std::make_pair((int)Qt::Key_Save , "Save"), std::make_pair((int)Qt::Key_Send , "Send"), std::make_pair((int)Qt::Key_Spell , "Spell"), std::make_pair((int)Qt::Key_SplitScreen , "SplitScreen"), std::make_pair((int)Qt::Key_Support , "Support"), std::make_pair((int)Qt::Key_TaskPane , "TaskPane"), std::make_pair((int)Qt::Key_Terminal , "Terminal"), std::make_pair((int)Qt::Key_Tools , "Tools"), std::make_pair((int)Qt::Key_Travel , "Travel"), std::make_pair((int)Qt::Key_Video , "Video"), std::make_pair((int)Qt::Key_Word , "Word"), std::make_pair((int)Qt::Key_Xfer , "Xfer"), std::make_pair((int)Qt::Key_ZoomIn , "ZoomIn"), std::make_pair((int)Qt::Key_ZoomOut , "ZoomOut"), std::make_pair((int)Qt::Key_Away , "Away"), std::make_pair((int)Qt::Key_Messenger , "Messenger"), std::make_pair((int)Qt::Key_WebCam , "WebCam"), std::make_pair((int)Qt::Key_MailForward , "MailForward"), std::make_pair((int)Qt::Key_Pictures , "Pictures"), std::make_pair((int)Qt::Key_Music , "Music"), std::make_pair((int)Qt::Key_Battery , "Battery"), std::make_pair((int)Qt::Key_Bluetooth , "Bluetooth"), std::make_pair((int)Qt::Key_WLAN , "WLAN"), std::make_pair((int)Qt::Key_UWB , "UWB"), std::make_pair((int)Qt::Key_AudioForward , "AudioForward"), std::make_pair((int)Qt::Key_AudioRepeat , "AudioRepeat"), std::make_pair((int)Qt::Key_AudioRandomPlay , "AudioRandomPlay"), std::make_pair((int)Qt::Key_Subtitle , "Subtitle"), std::make_pair((int)Qt::Key_AudioCycleTrack , "AudioCycleTrack"), std::make_pair((int)Qt::Key_Time , "Time"), std::make_pair((int)Qt::Key_Hibernate , "Hibernate"), std::make_pair((int)Qt::Key_View , "View"), std::make_pair((int)Qt::Key_TopMenu , "TopMenu"), std::make_pair((int)Qt::Key_PowerDown , "PowerDown"), std::make_pair((int)Qt::Key_Suspend , "Suspend"), std::make_pair((int)Qt::Key_ContrastAdjust , "ContrastAdjust"), std::make_pair((int)Qt::Key_LaunchG , "LaunchG"), std::make_pair((int)Qt::Key_LaunchH , "LaunchH"), std::make_pair((int)Qt::Key_TouchpadToggle , "TouchpadToggle"), std::make_pair((int)Qt::Key_TouchpadOn , "TouchpadOn"), std::make_pair((int)Qt::Key_TouchpadOff , "TouchpadOff"), std::make_pair((int)Qt::Key_MicMute , "MicMute"), std::make_pair((int)Qt::Key_Red , "Red"), std::make_pair((int)Qt::Key_Green , "Green"), std::make_pair((int)Qt::Key_Yellow , "Yellow"), std::make_pair((int)Qt::Key_Blue , "Blue"), std::make_pair((int)Qt::Key_ChannelUp , "ChannelUp"), std::make_pair((int)Qt::Key_ChannelDown , "ChannelDown"), std::make_pair((int)Qt::Key_Guide , "Guide"), std::make_pair((int)Qt::Key_Info , "Info"), std::make_pair((int)Qt::Key_Settings , "Settings"), std::make_pair((int)Qt::Key_MicVolumeUp , "MicVolumeUp"), std::make_pair((int)Qt::Key_MicVolumeDown , "MicVolumeDown"), std::make_pair((int)Qt::Key_New , "New"), std::make_pair((int)Qt::Key_Open , "Open"), std::make_pair((int)Qt::Key_Find , "Find"), std::make_pair((int)Qt::Key_Undo , "Undo"), std::make_pair((int)Qt::Key_Redo , "Redo"), std::make_pair((int)Qt::Key_AltGr , "AltGr"), std::make_pair((int)Qt::Key_Multi_key , "Multi_key"), std::make_pair((int)Qt::Key_Kanji , "Kanji"), std::make_pair((int)Qt::Key_Muhenkan , "Muhenkan"), std::make_pair((int)Qt::Key_Henkan , "Henkan"), std::make_pair((int)Qt::Key_Romaji , "Romaji"), std::make_pair((int)Qt::Key_Hiragana , "Hiragana"), std::make_pair((int)Qt::Key_Katakana , "Katakana"), std::make_pair((int)Qt::Key_Hiragana_Katakana , "Hiragana_Katakana"), std::make_pair((int)Qt::Key_Zenkaku , "Zenkaku"), std::make_pair((int)Qt::Key_Hankaku , "Hankaku"), std::make_pair((int)Qt::Key_Zenkaku_Hankaku , "Zenkaku_Hankaku"), std::make_pair((int)Qt::Key_Touroku , "Touroku"), std::make_pair((int)Qt::Key_Massyo , "Massyo"), std::make_pair((int)Qt::Key_Kana_Lock , "Kana_Lock"), std::make_pair((int)Qt::Key_Kana_Shift , "Kana_Shift"), std::make_pair((int)Qt::Key_Eisu_Shift , "Eisu_Shift"), std::make_pair((int)Qt::Key_Eisu_toggle , "Eisu_toggle"), std::make_pair((int)Qt::Key_Hangul , "Hangul"), std::make_pair((int)Qt::Key_Hangul_Start , "Hangul_Start"), std::make_pair((int)Qt::Key_Hangul_End , "Hangul_End"), std::make_pair((int)Qt::Key_Hangul_Hanja , "Hangul_Hanja"), std::make_pair((int)Qt::Key_Hangul_Jamo , "Hangul_Jamo"), std::make_pair((int)Qt::Key_Hangul_Romaja , "Hangul_Romaja"), std::make_pair((int)Qt::Key_Codeinput , "Codeinput"), std::make_pair((int)Qt::Key_Hangul_Jeonja , "Hangul_Jeonja"), std::make_pair((int)Qt::Key_Hangul_Banja , "Hangul_Banja"), std::make_pair((int)Qt::Key_Hangul_PreHanja , "Hangul_PreHanja"), std::make_pair((int)Qt::Key_Hangul_PostHanja , "Hangul_PostHanja"), std::make_pair((int)Qt::Key_SingleCandidate , "SingleCandidate"), std::make_pair((int)Qt::Key_MultipleCandidate , "MultipleCandidate"), std::make_pair((int)Qt::Key_PreviousCandidate , "PreviousCandidate"), std::make_pair((int)Qt::Key_Hangul_Special , "Hangul_Special"), std::make_pair((int)Qt::Key_Mode_switch , "Mode_switch"), std::make_pair((int)Qt::Key_Dead_Grave , "Dead_Grave"), std::make_pair((int)Qt::Key_Dead_Acute , "Dead_Acute"), std::make_pair((int)Qt::Key_Dead_Circumflex , "Dead_Circumflex"), std::make_pair((int)Qt::Key_Dead_Tilde , "Dead_Tilde"), std::make_pair((int)Qt::Key_Dead_Macron , "Dead_Macron"), std::make_pair((int)Qt::Key_Dead_Breve , "Dead_Breve"), std::make_pair((int)Qt::Key_Dead_Abovedot , "Dead_Abovedot"), std::make_pair((int)Qt::Key_Dead_Diaeresis , "Dead_Diaeresis"), std::make_pair((int)Qt::Key_Dead_Abovering , "Dead_Abovering"), std::make_pair((int)Qt::Key_Dead_Doubleacute , "Dead_Doubleacute"), std::make_pair((int)Qt::Key_Dead_Caron , "Dead_Caron"), std::make_pair((int)Qt::Key_Dead_Cedilla , "Dead_Cedilla"), std::make_pair((int)Qt::Key_Dead_Ogonek , "Dead_Ogonek"), std::make_pair((int)Qt::Key_Dead_Iota , "Dead_Iota"), std::make_pair((int)Qt::Key_Dead_Voiced_Sound , "Dead_Voiced_Sound"), std::make_pair((int)Qt::Key_Dead_Semivoiced_Sound , "Dead_Semivoiced_Sound"), std::make_pair((int)Qt::Key_Dead_Belowdot , "Dead_Belowdot"), std::make_pair((int)Qt::Key_Dead_Hook , "Dead_Hook"), std::make_pair((int)Qt::Key_Dead_Horn , "Dead_Horn"), std::make_pair((int)Qt::Key_MediaLast , "MediaLast"), std::make_pair((int)Qt::Key_Select , "Select"), std::make_pair((int)Qt::Key_Yes , "Yes"), std::make_pair((int)Qt::Key_No , "No"), std::make_pair((int)Qt::Key_Cancel , "Cancel"), std::make_pair((int)Qt::Key_Printer , "Printer"), std::make_pair((int)Qt::Key_Execute , "Execute"), std::make_pair((int)Qt::Key_Sleep , "Sleep"), std::make_pair((int)Qt::Key_Play , "Play"), std::make_pair((int)Qt::Key_Zoom , "Zoom"), std::make_pair((int)Qt::Key_Exit , "Exit"), std::make_pair((int)Qt::Key_Context1 , "Context1"), std::make_pair((int)Qt::Key_Context2 , "Context2"), std::make_pair((int)Qt::Key_Context3 , "Context3"), std::make_pair((int)Qt::Key_Context4 , "Context4"), std::make_pair((int)Qt::Key_Call , "Call"), std::make_pair((int)Qt::Key_Hangup , "Hangup"), std::make_pair((int)Qt::Key_Flip , "Flip"), std::make_pair((int)Qt::Key_ToggleCallHangup , "ToggleCallHangup"), std::make_pair((int)Qt::Key_VoiceDial , "VoiceDial"), std::make_pair((int)Qt::Key_LastNumberRedial , "LastNumberRedial"), std::make_pair((int)Qt::Key_Camera , "Camera"), std::make_pair((int)Qt::Key_CameraFocus , "CameraFocus"), std::make_pair(0 , (const char*) nullptr) }; } ================================================ FILE: apps/opencs/model/prefs/shortcutmanager.hpp ================================================ #ifndef CSM_PREFS_SHORTCUTMANAGER_H #define CSM_PREFS_SHORTCUTMANAGER_H #include #include #include #include namespace CSMPrefs { class Shortcut; class ShortcutEventHandler; /// Class used to track and update shortcuts/sequences class ShortcutManager : public QObject { Q_OBJECT public: ShortcutManager(); /// The shortcut class will do this automatically void addShortcut(Shortcut* shortcut); /// The shortcut class will do this automatically void removeShortcut(Shortcut* shortcut); bool getSequence(const std::string& name, QKeySequence& sequence) const; void setSequence(const std::string& name, const QKeySequence& sequence); bool getModifier(const std::string& name, int& modifier) const; void setModifier(const std::string& name, int modifier); std::string convertToString(const QKeySequence& sequence) const; std::string convertToString(int modifier) const; std::string convertToString(const QKeySequence& sequence, int modifier) const; void convertFromString(const std::string& data, QKeySequence& sequence) const; void convertFromString(const std::string& data, int& modifier) const; void convertFromString(const std::string& data, QKeySequence& sequence, int& modifier) const; /// Replaces "{sequence-name}" or "{modifier-name}" with the appropriate text QString processToolTip(const QString& toolTip) const; private: // Need a multimap in case multiple shortcuts share the same name typedef std::multimap ShortcutMap; typedef std::map SequenceMap; typedef std::map ModifierMap; typedef std::map NameMap; typedef std::map KeyMap; ShortcutMap mShortcuts; SequenceMap mSequences; ModifierMap mModifiers; NameMap mNames; KeyMap mKeys; ShortcutEventHandler* mEventHandler; void createLookupTables(); static const std::pair QtKeys[]; }; } #endif ================================================ FILE: apps/opencs/model/prefs/shortcutsetting.cpp ================================================ #include "shortcutsetting.hpp" #include #include #include #include #include #include #include #include "state.hpp" #include "shortcutmanager.hpp" namespace CSMPrefs { ShortcutSetting::ShortcutSetting(Category* parent, Settings::Manager* values, QMutex* mutex, const std::string& key, const std::string& label) : Setting(parent, values, mutex, key, label) , mButton(nullptr) , mEditorActive(false) , mEditorPos(0) { for (int i = 0; i < MaxKeys; ++i) { mEditorKeys[i] = 0; } } std::pair ShortcutSetting::makeWidgets(QWidget* parent) { QKeySequence sequence; State::get().getShortcutManager().getSequence(getKey(), sequence); QString text = QString::fromUtf8(State::get().getShortcutManager().convertToString(sequence).c_str()); QLabel* label = new QLabel(QString::fromUtf8(getLabel().c_str()), parent); QPushButton* widget = new QPushButton(text, parent); widget->setCheckable(true); widget->installEventFilter(this); // right clicking on button sets shortcut to RMB, so context menu should not appear widget->setContextMenuPolicy(Qt::PreventContextMenu); mButton = widget; connect(widget, SIGNAL(toggled(bool)), this, SLOT(buttonToggled(bool))); return std::make_pair(label, widget); } void ShortcutSetting::updateWidget() { if (mButton) { std::string shortcut = getValues().getString(getKey(), getParent()->getKey()); QKeySequence sequence; State::get().getShortcutManager().convertFromString(shortcut, sequence); State::get().getShortcutManager().setSequence(getKey(), sequence); resetState(); } } bool ShortcutSetting::eventFilter(QObject* target, QEvent* event) { if (event->type() == QEvent::KeyPress) { QKeyEvent* keyEvent = static_cast(event); if (keyEvent->isAutoRepeat()) return true; int mod = keyEvent->modifiers(); int key = keyEvent->key(); return handleEvent(target, mod, key, true); } else if (event->type() == QEvent::KeyRelease) { QKeyEvent* keyEvent = static_cast(event); if (keyEvent->isAutoRepeat()) return true; int mod = keyEvent->modifiers(); int key = keyEvent->key(); return handleEvent(target, mod, key, false); } else if (event->type() == QEvent::MouseButtonPress) { QMouseEvent* mouseEvent = static_cast(event); int mod = mouseEvent->modifiers(); int key = mouseEvent->button(); return handleEvent(target, mod, key, true); } else if (event->type() == QEvent::MouseButtonRelease) { QMouseEvent* mouseEvent = static_cast(event); int mod = mouseEvent->modifiers(); int key = mouseEvent->button(); return handleEvent(target, mod, key, false); } else if (event->type() == QEvent::FocusOut) { resetState(); } return false; } bool ShortcutSetting::handleEvent(QObject* target, int mod, int value, bool active) { // Modifiers are handled differently const int Blacklist[] = { Qt::Key_Shift, Qt::Key_Control, Qt::Key_Meta, Qt::Key_Alt, Qt::Key_AltGr }; const size_t BlacklistSize = sizeof(Blacklist) / sizeof(int); if (!mEditorActive) { if (value == Qt::RightButton && !active) { // Clear sequence QKeySequence sequence = QKeySequence(0, 0, 0, 0); storeValue(sequence); resetState(); } return false; } // Handle blacklist for (size_t i = 0; i < BlacklistSize; ++i) { if (value == Blacklist[i]) return true; } if (!active || mEditorPos >= MaxKeys) { // Update key QKeySequence sequence = QKeySequence(mEditorKeys[0], mEditorKeys[1], mEditorKeys[2], mEditorKeys[3]); storeValue(sequence); resetState(); } else { if (mEditorPos == 0) { mEditorKeys[0] = mod | value; } else { mEditorKeys[mEditorPos] = value; } mEditorPos += 1; } return true; } void ShortcutSetting::storeValue(const QKeySequence& sequence) { State::get().getShortcutManager().setSequence(getKey(), sequence); // Convert to string and assign std::string value = State::get().getShortcutManager().convertToString(sequence); { QMutexLocker lock(getMutex()); getValues().setString(getKey(), getParent()->getKey(), value); } getParent()->getState()->update(*this); } void ShortcutSetting::resetState() { mButton->setChecked(false); mEditorActive = false; mEditorPos = 0; for (int i = 0; i < MaxKeys; ++i) { mEditorKeys[i] = 0; } // Button text QKeySequence sequence; State::get().getShortcutManager().getSequence(getKey(), sequence); QString text = QString::fromUtf8(State::get().getShortcutManager().convertToString(sequence).c_str()); mButton->setText(text); } void ShortcutSetting::buttonToggled(bool checked) { if (checked) mButton->setText("Press keys or click here..."); mEditorActive = checked; } } ================================================ FILE: apps/opencs/model/prefs/shortcutsetting.hpp ================================================ #ifndef CSM_PREFS_SHORTCUTSETTING_H #define CSM_PREFS_SHORTCUTSETTING_H #include #include "setting.hpp" class QEvent; class QPushButton; namespace CSMPrefs { class ShortcutSetting : public Setting { Q_OBJECT public: ShortcutSetting(Category* parent, Settings::Manager* values, QMutex* mutex, const std::string& key, const std::string& label); std::pair makeWidgets(QWidget* parent) override; void updateWidget() override; protected: bool eventFilter(QObject* target, QEvent* event) override; private: bool handleEvent(QObject* target, int mod, int value, bool active); void storeValue(const QKeySequence& sequence); void resetState(); static constexpr int MaxKeys = 4; QPushButton* mButton; bool mEditorActive; int mEditorPos; int mEditorKeys[MaxKeys]; private slots: void buttonToggled(bool checked); }; } #endif ================================================ FILE: apps/opencs/model/prefs/state.cpp ================================================ #include "state.hpp" #include #include #include #include "intsetting.hpp" #include "doublesetting.hpp" #include "boolsetting.hpp" #include "coloursetting.hpp" #include "shortcutsetting.hpp" #include "modifiersetting.hpp" CSMPrefs::State *CSMPrefs::State::sThis = nullptr; void CSMPrefs::State::load() { // default settings file boost::filesystem::path local = mConfigurationManager.getLocalPath() / mDefaultConfigFile; boost::filesystem::path global = mConfigurationManager.getGlobalPath() / mDefaultConfigFile; if (boost::filesystem::exists (local)) mSettings.loadDefault (local.string()); else if (boost::filesystem::exists (global)) mSettings.loadDefault (global.string()); else throw std::runtime_error ("No default settings file found! Make sure the file \"" + mDefaultConfigFile + "\" was properly installed."); // user settings file boost::filesystem::path user = mConfigurationManager.getUserConfigPath() / mConfigFile; if (boost::filesystem::exists (user)) mSettings.loadUser (user.string()); } void CSMPrefs::State::declare() { declareCategory ("Windows"); declareInt ("default-width", "Default window width", 800). setTooltip ("Newly opened top-level windows will open with this width."). setMin (80); declareInt ("default-height", "Default window height", 600). setTooltip ("Newly opened top-level windows will open with this height."). setMin (80); declareBool ("show-statusbar", "Show Status Bar", true). setTooltip ("If a newly open top level window is showing status bars or not. " " Note that this does not affect existing windows."); declareSeparator(); declareBool ("reuse", "Reuse Subviews", true). setTooltip ("When a new subview is requested and a matching subview already " " exist, do not open a new subview and use the existing one instead."); declareInt ("max-subviews", "Maximum number of subviews per top-level window", 256). setTooltip ("If the maximum number is reached and a new subview is opened " "it will be placed into a new top-level window."). setRange (1, 256); declareBool ("hide-subview", "Hide single subview", false). setTooltip ("When a view contains only a single subview, hide the subview title " "bar and if this subview is closed also close the view (unless it is the last " "view for this document)"); declareInt ("minimum-width", "Minimum subview width", 325). setTooltip ("Minimum width of subviews."). setRange (50, 10000); declareSeparator(); EnumValue scrollbarOnly ("Scrollbar Only", "Simple addition of scrollbars, the view window " "does not grow automatically."); declareEnum ("mainwindow-scrollbar", "Horizontal scrollbar mode for main window.", scrollbarOnly). addValue (scrollbarOnly). addValue ("Grow Only", "The view window grows as subviews are added. No scrollbars."). addValue ("Grow then Scroll", "The view window grows. The scrollbar appears once it cannot grow any further."); declareBool ("grow-limit", "Grow Limit Screen", false). setTooltip ("When \"Grow then Scroll\" option is selected, the window size grows to" " the width of the virtual desktop. \nIf this option is selected the the window growth" "is limited to the current screen."); declareCategory ("Records"); EnumValue iconAndText ("Icon and Text"); EnumValues recordValues; recordValues.add (iconAndText).add ("Icon Only").add ("Text Only"); declareEnum ("status-format", "Modification status display format", iconAndText). addValues (recordValues); declareEnum ("type-format", "ID type display format", iconAndText). addValues (recordValues); declareCategory ("ID Tables"); EnumValue inPlaceEdit ("Edit in Place", "Edit the clicked cell"); EnumValue editRecord ("Edit Record", "Open a dialogue subview for the clicked record"); EnumValue view ("View", "Open a scene subview for the clicked record (not available everywhere)"); EnumValue editRecordAndClose ("Edit Record and Close"); EnumValues doubleClickValues; doubleClickValues.add (inPlaceEdit).add (editRecord).add (view).add ("Revert"). add ("Delete").add (editRecordAndClose). add ("View and Close", "Open a scene subview for the clicked record and close the table subview"); declareEnum ("double", "Double Click", inPlaceEdit).addValues (doubleClickValues); declareEnum ("double-s", "Shift Double Click", editRecord).addValues (doubleClickValues); declareEnum ("double-c", "Control Double Click", view).addValues (doubleClickValues); declareEnum ("double-sc", "Shift Control Double Click", editRecordAndClose).addValues (doubleClickValues); declareSeparator(); EnumValue jumpAndSelect ("Jump and Select", "Scroll new record into view and make it the selection"); declareEnum ("jump-to-added", "Action on adding or cloning a record", jumpAndSelect). addValue (jumpAndSelect). addValue ("Jump Only", "Scroll new record into view"). addValue ("No Jump", "No special action"); declareBool ("extended-config", "Manually specify affected record types for an extended delete/revert", false). setTooltip ("Delete and revert commands have an extended form that also affects " "associated records.\n\n" "If this option is enabled, types of affected records are selected " "manually before a command execution.\nOtherwise, all associated " "records are deleted/reverted immediately."); declareCategory ("ID Dialogues"); declareBool ("toolbar", "Show toolbar", true); declareCategory ("Reports"); EnumValue actionNone ("None"); EnumValue actionEdit ("Edit", "Open a table or dialogue suitable for addressing the listed report"); EnumValue actionRemove ("Remove", "Remove the report from the report table"); EnumValue actionEditAndRemove ("Edit And Remove", "Open a table or dialogue suitable for addressing the listed report, then remove the report from the report table"); EnumValues reportValues; reportValues.add (actionNone).add (actionEdit).add (actionRemove).add (actionEditAndRemove); declareEnum ("double", "Double Click", actionEdit).addValues (reportValues); declareEnum ("double-s", "Shift Double Click", actionRemove).addValues (reportValues); declareEnum ("double-c", "Control Double Click", actionEditAndRemove).addValues (reportValues); declareEnum ("double-sc", "Shift Control Double Click", actionNone).addValues (reportValues); declareBool("ignore-base-records", "Ignore base records in verifier", false); declareCategory ("Search & Replace"); declareInt ("char-before", "Characters before search string", 10). setTooltip ("Maximum number of character to display in search result before the searched text"); declareInt ("char-after", "Characters after search string", 10). setTooltip ("Maximum number of character to display in search result after the searched text"); declareBool ("auto-delete", "Delete row from result table after a successful replace", true); declareCategory ("Scripts"); declareBool ("show-linenum", "Show Line Numbers", true). setTooltip ("Show line numbers to the left of the script editor window." "The current row and column numbers of the text cursor are shown at the bottom."); declareBool ("wrap-lines", "Wrap Lines", false). setTooltip ("Wrap lines longer than width of script editor."); declareBool ("mono-font", "Use monospace font", true); declareInt ("tab-width", "Tab Width", 4). setTooltip ("Number of characters for tab width"). setRange (1, 10); EnumValue warningsNormal ("Normal", "Report warnings as warning"); declareEnum ("warnings", "Warning Mode", warningsNormal). addValue ("Ignore", "Do not report warning"). addValue (warningsNormal). addValue ("Strict", "Promote warning to an error"); declareBool ("toolbar", "Show toolbar", true); declareInt ("compile-delay", "Delay between updating of source errors", 100). setTooltip ("Delay in milliseconds"). setRange (0, 10000); declareInt ("error-height", "Initial height of the error panel", 100). setRange (100, 10000); declareBool ("highlight-occurrences", "Highlight other occurrences of selected names", true); declareColour ("colour-highlight", "Colour of highlighted occurrences", QColor("lightcyan")); declareSeparator(); declareColour ("colour-int", "Highlight Colour: Integer Literals", QColor ("darkmagenta")); declareColour ("colour-float", "Highlight Colour: Float Literals", QColor ("magenta")); declareColour ("colour-name", "Highlight Colour: Names", QColor ("grey")); declareColour ("colour-keyword", "Highlight Colour: Keywords", QColor ("red")); declareColour ("colour-special", "Highlight Colour: Special Characters", QColor ("darkorange")); declareColour ("colour-comment", "Highlight Colour: Comments", QColor ("green")); declareColour ("colour-id", "Highlight Colour: IDs", QColor ("blue")); declareCategory ("General Input"); declareBool ("cycle", "Cyclic next/previous", false). setTooltip ("When using next/previous functions at the last/first item of a " "list go to the first/last item"); declareCategory ("3D Scene Input"); declareDouble ("navi-wheel-factor", "Camera Zoom Sensitivity", 8).setRange(-100.0, 100.0); declareDouble ("s-navi-sensitivity", "Secondary Camera Movement Sensitivity", 50.0).setRange(-1000.0, 1000.0); declareSeparator(); declareDouble ("p-navi-free-sensitivity", "Free Camera Sensitivity", 1/650.).setPrecision(5).setRange(0.0, 1.0); declareBool ("p-navi-free-invert", "Invert Free Camera Mouse Input", false); declareDouble ("navi-free-lin-speed", "Free Camera Linear Speed", 1000.0).setRange(1.0, 10000.0); declareDouble ("navi-free-rot-speed", "Free Camera Rotational Speed", 3.14 / 2).setRange(0.001, 6.28); declareDouble ("navi-free-speed-mult", "Free Camera Speed Multiplier (from Modifier)", 8).setRange(0.001, 1000.0); declareSeparator(); declareDouble ("p-navi-orbit-sensitivity", "Orbit Camera Sensitivity", 1/650.).setPrecision(5).setRange(0.0, 1.0); declareBool ("p-navi-orbit-invert", "Invert Orbit Camera Mouse Input", false); declareDouble ("navi-orbit-rot-speed", "Orbital Camera Rotational Speed", 3.14 / 4).setRange(0.001, 6.28); declareDouble ("navi-orbit-speed-mult", "Orbital Camera Speed Multiplier (from Modifier)", 4).setRange(0.001, 1000.0); declareBool ("navi-orbit-const-roll", "Keep camera roll constant for orbital camera", true); declareSeparator(); declareBool ("context-select", "Context Sensitive Selection", false); declareDouble ("drag-factor", "Mouse sensitivity during drag operations", 1.0). setRange (0.001, 100.0); declareDouble ("drag-wheel-factor", "Mouse wheel sensitivity during drag operations", 1.0). setRange (0.001, 100.0); declareDouble ("drag-shift-factor", "Shift-acceleration factor during drag operations", 4.0). setTooltip ("Acceleration factor during drag operations while holding down shift"). setRange (0.001, 100.0); declareDouble ("rotate-factor", "Free rotation factor", 0.007).setPrecision(4).setRange(0.0001, 0.1); declareCategory ("Rendering"); declareInt ("framerate-limit", "FPS limit", 60). setTooltip("Framerate limit in 3D preview windows. Zero value means \"unlimited\"."). setRange(0, 10000); declareInt ("camera-fov", "Camera FOV", 90).setRange(10, 170); declareBool ("camera-ortho", "Orthographic projection for camera", false); declareInt ("camera-ortho-size", "Orthographic projection size parameter", 100). setTooltip("Size of the orthographic frustum, greater value will allow the camera to see more of the world."). setRange(10, 10000); declareDouble ("object-marker-alpha", "Object Marker Transparency", 0.5).setPrecision(2).setRange(0,1); declareBool("scene-use-gradient", "Use Gradient Background", true); declareColour ("scene-day-background-colour", "Day Background Colour", QColor (110, 120, 128, 255)); declareColour ("scene-day-gradient-colour", "Day Gradient Colour", QColor (47, 51, 51, 255)). setTooltip("Sets the gradient color to use in conjunction with the day background color. Ignored if " "the gradient option is disabled."); declareColour ("scene-bright-background-colour", "Scene Bright Background Colour", QColor (79, 87, 92, 255)); declareColour ("scene-bright-gradient-colour", "Scene Bright Gradient Colour", QColor (47, 51, 51, 255)). setTooltip("Sets the gradient color to use in conjunction with the bright background color. Ignored if " "the gradient option is disabled."); declareColour ("scene-night-background-colour", "Scene Night Background Colour", QColor (64, 77, 79, 255)); 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."); declareCategory ("Tooltips"); declareBool ("scene", "Show Tooltips in 3D scenes", true); declareBool ("scene-hide-basic", "Hide basic 3D scenes tooltips", false); declareInt ("scene-delay", "Tooltip delay in milliseconds", 500). setMin (1); EnumValue createAndInsert ("Create cell and insert"); EnumValue showAndInsert ("Show cell and insert"); EnumValue dontInsert ("Discard"); EnumValue insertAnyway ("Insert anyway"); EnumValues insertOutsideCell; insertOutsideCell.add (createAndInsert).add (dontInsert).add (insertAnyway); EnumValues insertOutsideVisibleCell; insertOutsideVisibleCell.add (showAndInsert).add (dontInsert).add (insertAnyway); EnumValue createAndLandEdit ("Create cell and land, then edit"); EnumValue showAndLandEdit ("Show cell and edit"); EnumValue dontLandEdit ("Discard"); EnumValues landeditOutsideCell; landeditOutsideCell.add (createAndLandEdit).add (dontLandEdit); EnumValues landeditOutsideVisibleCell; landeditOutsideVisibleCell.add (showAndLandEdit).add (dontLandEdit); EnumValue SelectOnly ("Select only"); EnumValue SelectAdd ("Add to selection"); EnumValue SelectRemove ("Remove from selection"); EnumValue selectInvert ("Invert selection"); EnumValues primarySelectAction; primarySelectAction.add (SelectOnly).add (SelectAdd).add (SelectRemove).add (selectInvert); EnumValues secondarySelectAction; secondarySelectAction.add (SelectOnly).add (SelectAdd).add (SelectRemove).add (selectInvert); declareCategory ("3D Scene Editing"); declareInt ("distance", "Drop Distance", 50). setTooltip ("If an instance drop can not be placed against another object at the " "insert point, it will be placed by this distance from the insert point instead"); declareEnum ("outside-drop", "Handling drops outside of cells", createAndInsert). addValues (insertOutsideCell); declareEnum ("outside-visible-drop", "Handling drops outside of visible cells", showAndInsert). addValues (insertOutsideVisibleCell); declareEnum ("outside-landedit", "Handling terrain edit outside of cells", createAndLandEdit). setTooltip("Behavior of terrain editing, if land editing brush reaches an area without cell record."). addValues (landeditOutsideCell); declareEnum ("outside-visible-landedit", "Handling terrain edit outside of visible cells", showAndLandEdit). setTooltip("Behavior of terrain editing, if land editing brush reaches an area that is not currently visible."). addValues (landeditOutsideVisibleCell); declareInt ("texturebrush-maximumsize", "Maximum texture brush size", 50). setMin (1); declareInt ("shapebrush-maximumsize", "Maximum height edit brush size", 100). setTooltip("Setting for the slider range of brush size in terrain height editing."). setMin (1); declareBool ("landedit-post-smoothpainting", "Smooth land after painting height", false). setTooltip("Raise and lower tools will leave bumpy finish without this option"); declareDouble ("landedit-post-smoothstrength", "Smoothing strength (post-edit)", 0.25). setTooltip("If smoothing land after painting height is used, this is the percentage of smooth applied afterwards. " "Negative values may be used to roughen instead of smooth."). setMin (-1). setMax (1); declareBool ("open-list-view", "Open displays list view", false). setTooltip ("When opening a reference from the scene view, it will open the" " instance list view instead of the individual instance record view."); declareEnum ("primary-select-action", "Action for primary select", SelectOnly). setTooltip("Selection can be chosen between select only, add to selection, remove from selection and invert selection."). addValues (primarySelectAction); declareEnum ("secondary-select-action", "Action for secondary select", SelectAdd). setTooltip("Selection can be chosen between select only, add to selection, remove from selection and invert selection."). addValues (secondarySelectAction); declareCategory ("Key Bindings"); declareSubcategory ("Document"); declareShortcut ("document-file-newgame", "New Game", QKeySequence(Qt::ControlModifier | Qt::Key_N)); declareShortcut ("document-file-newaddon", "New Addon", QKeySequence()); declareShortcut ("document-file-open", "Open", QKeySequence(Qt::ControlModifier | Qt::Key_O)); declareShortcut ("document-file-save", "Save", QKeySequence(Qt::ControlModifier | Qt::Key_S)); declareShortcut ("document-help-help", "Help", QKeySequence(Qt::Key_F1)); declareShortcut ("document-help-tutorial", "Tutorial", QKeySequence()); declareShortcut ("document-file-verify", "Verify", QKeySequence()); declareShortcut ("document-file-merge", "Merge", QKeySequence()); declareShortcut ("document-file-errorlog", "Open Load Error Log", QKeySequence()); declareShortcut ("document-file-metadata", "Meta Data", QKeySequence()); declareShortcut ("document-file-close", "Close Document", QKeySequence(Qt::ControlModifier | Qt::Key_W)); declareShortcut ("document-file-exit", "Exit Application", QKeySequence(Qt::ControlModifier | Qt::Key_Q)); declareShortcut ("document-edit-undo", "Undo", QKeySequence(Qt::ControlModifier | Qt::Key_Z)); declareShortcut ("document-edit-redo", "Redo", QKeySequence(Qt::ControlModifier | Qt::ShiftModifier | Qt::Key_Z)); declareShortcut ("document-edit-preferences", "Open Preferences", QKeySequence()); declareShortcut ("document-edit-search", "Search", QKeySequence(Qt::ControlModifier | Qt::Key_F)); declareShortcut ("document-view-newview", "New View", QKeySequence()); declareShortcut ("document-view-statusbar", "Toggle Status Bar", QKeySequence()); declareShortcut ("document-view-filters", "Open Filter List", QKeySequence()); declareShortcut ("document-world-regions", "Open Region List", QKeySequence()); declareShortcut ("document-world-cells", "Open Cell List", QKeySequence()); declareShortcut ("document-world-referencables", "Open Object List", QKeySequence()); declareShortcut ("document-world-references", "Open Instance List", QKeySequence()); declareShortcut ("document-world-lands", "Open Lands List", QKeySequence()); declareShortcut ("document-world-landtextures", "Open Land Textures List", QKeySequence()); declareShortcut ("document-world-pathgrid", "Open Pathgrid List", QKeySequence()); declareShortcut ("document-world-regionmap", "Open Region Map", QKeySequence()); declareShortcut ("document-mechanics-globals", "Open Global List", QKeySequence()); declareShortcut ("document-mechanics-gamesettings", "Open Game Settings", QKeySequence()); declareShortcut ("document-mechanics-scripts", "Open Script List", QKeySequence()); declareShortcut ("document-mechanics-spells", "Open Spell List", QKeySequence()); declareShortcut ("document-mechanics-enchantments", "Open Enchantment List", QKeySequence()); declareShortcut ("document-mechanics-magiceffects", "Open Magic Effect List", QKeySequence()); declareShortcut ("document-mechanics-startscripts", "Open Start Script List", QKeySequence()); declareShortcut ("document-character-skills", "Open Skill List", QKeySequence()); declareShortcut ("document-character-classes", "Open Class List", QKeySequence()); declareShortcut ("document-character-factions", "Open Faction List", QKeySequence()); declareShortcut ("document-character-races", "Open Race List", QKeySequence()); declareShortcut ("document-character-birthsigns", "Open Birthsign List", QKeySequence()); declareShortcut ("document-character-topics", "Open Topic List", QKeySequence()); declareShortcut ("document-character-journals", "Open Journal List", QKeySequence()); declareShortcut ("document-character-topicinfos", "Open Topic Info List", QKeySequence()); declareShortcut ("document-character-journalinfos", "Open Journal Info List", QKeySequence()); declareShortcut ("document-character-bodyparts", "Open Body Part List", QKeySequence()); declareShortcut ("document-assets-reload", "Reload Assets", QKeySequence(Qt::Key_F5)); declareShortcut ("document-assets-sounds", "Open Sound Asset List", QKeySequence()); declareShortcut ("document-assets-soundgens", "Open Sound Generator List", QKeySequence()); declareShortcut ("document-assets-meshes", "Open Mesh Asset List", QKeySequence()); declareShortcut ("document-assets-icons", "Open Icon Asset List", QKeySequence()); declareShortcut ("document-assets-music", "Open Music Asset List", QKeySequence()); declareShortcut ("document-assets-soundres", "Open Sound File List", QKeySequence()); declareShortcut ("document-assets-textures", "Open Texture Asset List", QKeySequence()); declareShortcut ("document-assets-videos", "Open Video Asset List", QKeySequence()); declareShortcut ("document-debug-run", "Run Debug", QKeySequence()); declareShortcut ("document-debug-shutdown", "Stop Debug", QKeySequence()); declareShortcut ("document-debug-profiles", "Debug Profiles", QKeySequence()); declareShortcut ("document-debug-runlog", "Open Run Log", QKeySequence()); declareSubcategory ("Table"); declareShortcut ("table-edit", "Edit Record", QKeySequence()); declareShortcut ("table-add", "Add Row/Record", QKeySequence(Qt::ShiftModifier | Qt::Key_A)); declareShortcut ("table-clone", "Clone Record", QKeySequence(Qt::ShiftModifier | Qt::Key_D)); declareShortcut ("touch-record", "Touch Record", QKeySequence()); declareShortcut ("table-revert", "Revert Record", QKeySequence()); declareShortcut ("table-remove", "Remove Row/Record", QKeySequence(Qt::Key_Delete)); declareShortcut ("table-moveup", "Move Record Up", QKeySequence()); declareShortcut ("table-movedown", "Move Record Down", QKeySequence()); declareShortcut ("table-view", "View Record", QKeySequence(Qt::ShiftModifier | Qt::Key_C)); declareShortcut ("table-preview", "Preview Record", QKeySequence(Qt::ShiftModifier | Qt::Key_V)); declareShortcut ("table-extendeddelete", "Extended Record Deletion", QKeySequence()); declareShortcut ("table-extendedrevert", "Extended Record Revertion", QKeySequence()); declareSubcategory ("Report Table"); declareShortcut ("reporttable-show", "Show Report", QKeySequence()); declareShortcut ("reporttable-remove", "Remove Report", QKeySequence(Qt::Key_Delete)); declareShortcut ("reporttable-replace", "Replace Report", QKeySequence()); declareShortcut ("reporttable-refresh", "Refresh Report", QKeySequence()); declareSubcategory ("Scene"); declareShortcut ("scene-navi-primary", "Camera Rotation From Mouse Movement", QKeySequence(Qt::LeftButton)); declareShortcut ("scene-navi-secondary", "Camera Translation From Mouse Movement", QKeySequence(Qt::ControlModifier | (int)Qt::LeftButton)); declareShortcut ("scene-open-primary", "Primary Open", QKeySequence(Qt::ShiftModifier | (int)Qt::LeftButton)); declareShortcut ("scene-edit-primary", "Primary Edit", QKeySequence(Qt::RightButton)); declareShortcut ("scene-edit-secondary", "Secondary Edit", QKeySequence(Qt::ControlModifier | (int)Qt::RightButton)); declareShortcut ("scene-select-primary", "Primary Select", QKeySequence(Qt::MiddleButton)); declareShortcut ("scene-select-secondary", "Secondary Select", QKeySequence(Qt::ControlModifier | (int)Qt::MiddleButton)); declareModifier ("scene-speed-modifier", "Speed Modifier", Qt::Key_Shift); declareShortcut ("scene-delete", "Delete Instance", QKeySequence(Qt::Key_Delete)); declareShortcut ("scene-instance-drop-terrain", "Drop to terrain level", QKeySequence(Qt::Key_G)); declareShortcut ("scene-instance-drop-collision", "Drop to collision", QKeySequence(Qt::Key_H)); declareShortcut ("scene-instance-drop-terrain-separately", "Drop to terrain level separately", QKeySequence()); declareShortcut ("scene-instance-drop-collision-separately", "Drop to collision separately", QKeySequence()); declareShortcut ("scene-load-cam-cell", "Load Camera Cell", QKeySequence(Qt::KeypadModifier | Qt::Key_5)); declareShortcut ("scene-load-cam-eastcell", "Load East Cell", QKeySequence(Qt::KeypadModifier | Qt::Key_6)); declareShortcut ("scene-load-cam-northcell", "Load North Cell", QKeySequence(Qt::KeypadModifier | Qt::Key_8)); declareShortcut ("scene-load-cam-westcell", "Load West Cell", QKeySequence(Qt::KeypadModifier | Qt::Key_4)); declareShortcut ("scene-load-cam-southcell", "Load South Cell", QKeySequence(Qt::KeypadModifier | Qt::Key_2)); declareShortcut ("scene-edit-abort", "Abort", QKeySequence(Qt::Key_Escape)); declareShortcut ("scene-focus-toolbar", "Toggle Toolbar Focus", QKeySequence(Qt::Key_T)); declareShortcut ("scene-render-stats", "Debug Rendering Stats", QKeySequence(Qt::Key_F3)); declareSubcategory ("1st/Free Camera"); declareShortcut ("free-forward", "Forward", QKeySequence(Qt::Key_W)); declareShortcut ("free-backward", "Backward", QKeySequence(Qt::Key_S)); declareShortcut ("free-left", "Left", QKeySequence(Qt::Key_A)); declareShortcut ("free-right", "Right", QKeySequence(Qt::Key_D)); declareShortcut ("free-roll-left", "Roll Left", QKeySequence(Qt::Key_Q)); declareShortcut ("free-roll-right", "Roll Right", QKeySequence(Qt::Key_E)); declareShortcut ("free-speed-mode", "Toggle Speed Mode", QKeySequence(Qt::Key_F)); declareSubcategory ("Orbit Camera"); declareShortcut ("orbit-up", "Up", QKeySequence(Qt::Key_W)); declareShortcut ("orbit-down", "Down", QKeySequence(Qt::Key_S)); declareShortcut ("orbit-left", "Left", QKeySequence(Qt::Key_A)); declareShortcut ("orbit-right", "Right", QKeySequence(Qt::Key_D)); declareShortcut ("orbit-roll-left", "Roll Left", QKeySequence(Qt::Key_Q)); declareShortcut ("orbit-roll-right", "Roll Right", QKeySequence(Qt::Key_E)); declareShortcut ("orbit-speed-mode", "Toggle Speed Mode", QKeySequence(Qt::Key_F)); declareShortcut ("orbit-center-selection", "Center On Selected", QKeySequence(Qt::Key_C)); declareSubcategory ("Script Editor"); declareShortcut ("script-editor-comment", "Comment Selection", QKeySequence()); declareShortcut ("script-editor-uncomment", "Uncomment Selection", QKeySequence()); declareCategory ("Models"); declareString ("baseanim", "base animations", "meshes/base_anim.nif"). setTooltip("3rd person base model with textkeys-data"); declareString ("baseanimkna", "base animations, kna", "meshes/base_animkna.nif"). setTooltip("3rd person beast race base model with textkeys-data"); declareString ("baseanimfemale", "base animations, female", "meshes/base_anim_female.nif"). setTooltip("3rd person female base model with textkeys-data"); declareString ("wolfskin", "base animations, wolf", "meshes/wolf/skin.nif"). setTooltip("3rd person werewolf skin"); } void CSMPrefs::State::declareCategory (const std::string& key) { std::map::iterator iter = mCategories.find (key); if (iter!=mCategories.end()) { mCurrentCategory = iter; } else { mCurrentCategory = mCategories.insert (std::make_pair (key, Category (this, key))).first; } } CSMPrefs::IntSetting& CSMPrefs::State::declareInt (const std::string& key, const std::string& label, int default_) { if (mCurrentCategory==mCategories.end()) throw std::logic_error ("no category for setting"); setDefault(key, std::to_string(default_)); default_ = mSettings.getInt (key, mCurrentCategory->second.getKey()); CSMPrefs::IntSetting *setting = new CSMPrefs::IntSetting (&mCurrentCategory->second, &mSettings, &mMutex, key, label, default_); mCurrentCategory->second.addSetting (setting); return *setting; } CSMPrefs::DoubleSetting& CSMPrefs::State::declareDouble (const std::string& key, const std::string& label, double default_) { if (mCurrentCategory==mCategories.end()) throw std::logic_error ("no category for setting"); std::ostringstream stream; stream << default_; setDefault(key, stream.str()); default_ = mSettings.getFloat (key, mCurrentCategory->second.getKey()); CSMPrefs::DoubleSetting *setting = new CSMPrefs::DoubleSetting (&mCurrentCategory->second, &mSettings, &mMutex, key, label, default_); mCurrentCategory->second.addSetting (setting); return *setting; } CSMPrefs::BoolSetting& CSMPrefs::State::declareBool (const std::string& key, const std::string& label, bool default_) { if (mCurrentCategory==mCategories.end()) throw std::logic_error ("no category for setting"); setDefault (key, default_ ? "true" : "false"); default_ = mSettings.getBool (key, mCurrentCategory->second.getKey()); CSMPrefs::BoolSetting *setting = new CSMPrefs::BoolSetting (&mCurrentCategory->second, &mSettings, &mMutex, key, label, default_); mCurrentCategory->second.addSetting (setting); return *setting; } CSMPrefs::EnumSetting& CSMPrefs::State::declareEnum (const std::string& key, const std::string& label, EnumValue default_) { if (mCurrentCategory==mCategories.end()) throw std::logic_error ("no category for setting"); setDefault (key, default_.mValue); default_.mValue = mSettings.getString (key, mCurrentCategory->second.getKey()); CSMPrefs::EnumSetting *setting = new CSMPrefs::EnumSetting (&mCurrentCategory->second, &mSettings, &mMutex, key, label, default_); mCurrentCategory->second.addSetting (setting); return *setting; } CSMPrefs::ColourSetting& CSMPrefs::State::declareColour (const std::string& key, const std::string& label, QColor default_) { if (mCurrentCategory==mCategories.end()) throw std::logic_error ("no category for setting"); setDefault (key, default_.name().toUtf8().data()); default_.setNamedColor (QString::fromUtf8 (mSettings.getString (key, mCurrentCategory->second.getKey()).c_str())); CSMPrefs::ColourSetting *setting = new CSMPrefs::ColourSetting (&mCurrentCategory->second, &mSettings, &mMutex, key, label, default_); mCurrentCategory->second.addSetting (setting); return *setting; } CSMPrefs::ShortcutSetting& CSMPrefs::State::declareShortcut (const std::string& key, const std::string& label, const QKeySequence& default_) { if (mCurrentCategory==mCategories.end()) throw std::logic_error ("no category for setting"); std::string seqStr = getShortcutManager().convertToString(default_); setDefault (key, seqStr); // Setup with actual data QKeySequence sequence; getShortcutManager().convertFromString(mSettings.getString(key, mCurrentCategory->second.getKey()), sequence); getShortcutManager().setSequence(key, sequence); CSMPrefs::ShortcutSetting *setting = new CSMPrefs::ShortcutSetting (&mCurrentCategory->second, &mSettings, &mMutex, key, label); mCurrentCategory->second.addSetting (setting); return *setting; } CSMPrefs::StringSetting& CSMPrefs::State::declareString (const std::string& key, const std::string& label, std::string default_) { if (mCurrentCategory==mCategories.end()) throw std::logic_error ("no category for setting"); setDefault (key, default_); default_ = mSettings.getString (key, mCurrentCategory->second.getKey()); CSMPrefs::StringSetting *setting = new CSMPrefs::StringSetting (&mCurrentCategory->second, &mSettings, &mMutex, key, label, default_); mCurrentCategory->second.addSetting (setting); return *setting; } CSMPrefs::ModifierSetting& CSMPrefs::State::declareModifier(const std::string& key, const std::string& label, int default_) { if (mCurrentCategory==mCategories.end()) throw std::logic_error ("no category for setting"); std::string modStr = getShortcutManager().convertToString(default_); setDefault (key, modStr); // Setup with actual data int modifier; getShortcutManager().convertFromString(mSettings.getString(key, mCurrentCategory->second.getKey()), modifier); getShortcutManager().setModifier(key, modifier); CSMPrefs::ModifierSetting *setting = new CSMPrefs::ModifierSetting (&mCurrentCategory->second, &mSettings, &mMutex, key, label); mCurrentCategory->second.addSetting (setting); return *setting; } void CSMPrefs::State::declareSeparator() { if (mCurrentCategory==mCategories.end()) throw std::logic_error ("no category for setting"); CSMPrefs::Setting *setting = new CSMPrefs::Setting (&mCurrentCategory->second, &mSettings, &mMutex, "", ""); mCurrentCategory->second.addSetting (setting); } void CSMPrefs::State::declareSubcategory(const std::string& label) { if (mCurrentCategory==mCategories.end()) throw std::logic_error ("no category for setting"); CSMPrefs::Setting *setting = new CSMPrefs::Setting (&mCurrentCategory->second, &mSettings, &mMutex, "", label); mCurrentCategory->second.addSetting (setting); } void CSMPrefs::State::setDefault (const std::string& key, const std::string& default_) { Settings::CategorySetting fullKey (mCurrentCategory->second.getKey(), key); Settings::CategorySettingValueMap::iterator iter = mSettings.mDefaultSettings.find (fullKey); if (iter==mSettings.mDefaultSettings.end()) mSettings.mDefaultSettings.insert (std::make_pair (fullKey, default_)); } CSMPrefs::State::State (const Files::ConfigurationManager& configurationManager) : mConfigFile ("openmw-cs.cfg"), mDefaultConfigFile("defaults-cs.bin"), mConfigurationManager (configurationManager), mCurrentCategory (mCategories.end()) { if (sThis) throw std::logic_error ("An instance of CSMPRefs::State already exists"); sThis = this; load(); declare(); } CSMPrefs::State::~State() { sThis = nullptr; } void CSMPrefs::State::save() { boost::filesystem::path user = mConfigurationManager.getUserConfigPath() / mConfigFile; mSettings.saveUser (user.string()); } CSMPrefs::State::Iterator CSMPrefs::State::begin() { return mCategories.begin(); } CSMPrefs::State::Iterator CSMPrefs::State::end() { return mCategories.end(); } CSMPrefs::ShortcutManager& CSMPrefs::State::getShortcutManager() { return mShortcutManager; } CSMPrefs::Category& CSMPrefs::State::operator[] (const std::string& key) { Iterator iter = mCategories.find (key); if (iter==mCategories.end()) throw std::logic_error ("Invalid user settings category: " + key); return iter->second; } void CSMPrefs::State::update (const Setting& setting) { emit (settingChanged (&setting)); } CSMPrefs::State& CSMPrefs::State::get() { if (!sThis) throw std::logic_error ("No instance of CSMPrefs::State"); return *sThis; } void CSMPrefs::State::resetCategory(const std::string& category) { for (Settings::CategorySettingValueMap::iterator i = mSettings.mUserSettings.begin(); i != mSettings.mUserSettings.end(); ++i) { // if the category matches if (i->first.first == category) { // mark the setting as changed mSettings.mChangedSettings.insert(std::make_pair(i->first.first, i->first.second)); // reset the value to the default i->second = mSettings.mDefaultSettings[i->first]; } } Collection::iterator container = mCategories.find(category); if (container != mCategories.end()) { Category settings = container->second; for (Category::Iterator i = settings.begin(); i != settings.end(); ++i) { (*i)->updateWidget(); update(**i); } } } void CSMPrefs::State::resetAll() { for (Collection::iterator iter = mCategories.begin(); iter != mCategories.end(); ++iter) { resetCategory(iter->first); } } CSMPrefs::State& CSMPrefs::get() { return State::get(); } ================================================ FILE: apps/opencs/model/prefs/state.hpp ================================================ #ifndef CSM_PREFS_STATE_H #define CSM_PREFS_STATE_H #include #include #include #include #ifndef Q_MOC_RUN #include #endif #include #include "category.hpp" #include "setting.hpp" #include "enumsetting.hpp" #include "stringsetting.hpp" #include "shortcutmanager.hpp" class QColor; namespace CSMPrefs { class IntSetting; class DoubleSetting; class BoolSetting; class ColourSetting; class ShortcutSetting; class ModifierSetting; /// \brief User settings state /// /// \note Access to the user settings is thread-safe once all declarations and loading has /// been completed. class State : public QObject { Q_OBJECT static State *sThis; public: typedef std::map Collection; typedef Collection::iterator Iterator; private: const std::string mConfigFile; const std::string mDefaultConfigFile; const Files::ConfigurationManager& mConfigurationManager; ShortcutManager mShortcutManager; Settings::Manager mSettings; Collection mCategories; Iterator mCurrentCategory; QMutex mMutex; // not implemented State (const State&); State& operator= (const State&); private: void load(); void declare(); void declareCategory (const std::string& key); IntSetting& declareInt (const std::string& key, const std::string& label, int default_); DoubleSetting& declareDouble (const std::string& key, const std::string& label, double default_); BoolSetting& declareBool (const std::string& key, const std::string& label, bool default_); EnumSetting& declareEnum (const std::string& key, const std::string& label, EnumValue default_); ColourSetting& declareColour (const std::string& key, const std::string& label, QColor default_); ShortcutSetting& declareShortcut (const std::string& key, const std::string& label, const QKeySequence& default_); StringSetting& declareString (const std::string& key, const std::string& label, std::string default_); ModifierSetting& declareModifier(const std::string& key, const std::string& label, int modifier_); void declareSeparator(); void declareSubcategory(const std::string& label); void setDefault (const std::string& key, const std::string& default_); public: State (const Files::ConfigurationManager& configurationManager); ~State(); void save(); Iterator begin(); Iterator end(); ShortcutManager& getShortcutManager(); Category& operator[](const std::string& key); void update (const Setting& setting); static State& get(); void resetCategory(const std::string& category); void resetAll(); signals: void settingChanged (const CSMPrefs::Setting *setting); }; // convenience function State& get(); } #endif ================================================ FILE: apps/opencs/model/prefs/stringsetting.cpp ================================================ #include "stringsetting.hpp" #include #include #include #include "category.hpp" #include "state.hpp" CSMPrefs::StringSetting::StringSetting (Category *parent, Settings::Manager *values, QMutex *mutex, const std::string& key, const std::string& label, std::string default_) : Setting (parent, values, mutex, key, label), mDefault (default_), mWidget(nullptr) {} CSMPrefs::StringSetting& CSMPrefs::StringSetting::setTooltip (const std::string& tooltip) { mTooltip = tooltip; return *this; } std::pair CSMPrefs::StringSetting::makeWidgets (QWidget *parent) { mWidget = new QLineEdit (QString::fromUtf8 (mDefault.c_str()), parent); if (!mTooltip.empty()) { QString tooltip = QString::fromUtf8 (mTooltip.c_str()); mWidget->setToolTip (tooltip); } connect (mWidget, SIGNAL (textChanged (QString)), this, SLOT (textChanged (QString))); return std::make_pair (static_cast (nullptr), mWidget); } void CSMPrefs::StringSetting::updateWidget() { if (mWidget) { mWidget->setText(QString::fromStdString(getValues().getString(getKey(), getParent()->getKey()))); } } void CSMPrefs::StringSetting::textChanged (const QString& text) { { QMutexLocker lock (getMutex()); getValues().setString (getKey(), getParent()->getKey(), text.toStdString()); } getParent()->getState()->update (*this); } ================================================ FILE: apps/opencs/model/prefs/stringsetting.hpp ================================================ #ifndef CSM_PREFS_StringSetting_H #define CSM_PREFS_StringSetting_H #include "setting.hpp" class QLineEdit; namespace CSMPrefs { class StringSetting : public Setting { Q_OBJECT std::string mTooltip; std::string mDefault; QLineEdit* mWidget; public: StringSetting (Category *parent, Settings::Manager *values, QMutex *mutex, const std::string& key, const std::string& label, std::string default_); StringSetting& setTooltip (const std::string& tooltip); /// Return label, input widget. std::pair makeWidgets (QWidget *parent) override; void updateWidget() override; private slots: void textChanged (const QString& text); }; } #endif ================================================ FILE: apps/opencs/model/tools/birthsigncheck.cpp ================================================ #include "birthsigncheck.hpp" #include #include "../prefs/state.hpp" #include "../world/universalid.hpp" CSMTools::BirthsignCheckStage::BirthsignCheckStage (const CSMWorld::IdCollection& birthsigns, const CSMWorld::Resources &textures) : mBirthsigns(birthsigns), mTextures(textures) { mIgnoreBaseRecords = false; } int CSMTools::BirthsignCheckStage::setup() { mIgnoreBaseRecords = CSMPrefs::get()["Reports"]["ignore-base-records"].isTrue(); return mBirthsigns.getSize(); } void CSMTools::BirthsignCheckStage::perform (int stage, CSMDoc::Messages& messages) { const CSMWorld::Record& record = mBirthsigns.getRecord (stage); // Skip "Base" records (setting!) and "Deleted" records if ((mIgnoreBaseRecords && record.mState == CSMWorld::RecordBase::State_BaseOnly) || record.isDeleted()) return; const ESM::BirthSign& birthsign = record.get(); CSMWorld::UniversalId id (CSMWorld::UniversalId::Type_Birthsign, birthsign.mId); if (birthsign.mName.empty()) messages.add(id, "Name is missing", "", CSMDoc::Message::Severity_Error); if (birthsign.mDescription.empty()) messages.add(id, "Description is missing", "", CSMDoc::Message::Severity_Warning); if (birthsign.mTexture.empty()) messages.add(id, "Image is missing", "", CSMDoc::Message::Severity_Error); else if (mTextures.searchId(birthsign.mTexture) == -1) { std::string ddsTexture = birthsign.mTexture; if (!(Misc::ResourceHelpers::changeExtensionToDds(ddsTexture) && mTextures.searchId(ddsTexture) != -1)) messages.add(id, "Image '" + birthsign.mTexture + "' does not exist", "", CSMDoc::Message::Severity_Error); } /// \todo check data members that can't be edited in the table view } ================================================ FILE: apps/opencs/model/tools/birthsigncheck.hpp ================================================ #ifndef CSM_TOOLS_BIRTHSIGNCHECK_H #define CSM_TOOLS_BIRTHSIGNCHECK_H #include #include "../world/idcollection.hpp" #include "../world/resources.hpp" #include "../doc/stage.hpp" namespace CSMTools { /// \brief VerifyStage: make sure that birthsign records are internally consistent class BirthsignCheckStage : public CSMDoc::Stage { const CSMWorld::IdCollection &mBirthsigns; const CSMWorld::Resources &mTextures; bool mIgnoreBaseRecords; public: BirthsignCheckStage (const CSMWorld::IdCollection &birthsigns, const CSMWorld::Resources &textures); int setup() override; ///< \return number of steps void perform (int stage, CSMDoc::Messages& messages) override; ///< Messages resulting from this tage will be appended to \a messages. }; } #endif ================================================ FILE: apps/opencs/model/tools/bodypartcheck.cpp ================================================ #include "bodypartcheck.hpp" #include "../prefs/state.hpp" CSMTools::BodyPartCheckStage::BodyPartCheckStage( const CSMWorld::IdCollection &bodyParts, const CSMWorld::Resources &meshes, const CSMWorld::IdCollection &races ) : mBodyParts(bodyParts), mMeshes(meshes), mRaces(races) { mIgnoreBaseRecords = false; } int CSMTools::BodyPartCheckStage::setup() { mIgnoreBaseRecords = CSMPrefs::get()["Reports"]["ignore-base-records"].isTrue(); return mBodyParts.getSize(); } void CSMTools::BodyPartCheckStage::perform (int stage, CSMDoc::Messages &messages) { const CSMWorld::Record &record = mBodyParts.getRecord(stage); // Skip "Base" records (setting!) and "Deleted" records if ((mIgnoreBaseRecords && record.mState == CSMWorld::RecordBase::State_BaseOnly) || record.isDeleted()) return; const ESM::BodyPart &bodyPart = record.get(); CSMWorld::UniversalId id( CSMWorld::UniversalId::Type_BodyPart, bodyPart.mId ); // Check BYDT if (bodyPart.mData.mPart >= ESM::BodyPart::MP_Count ) messages.add(id, "Invalid part", "", CSMDoc::Message::Severity_Error); if (bodyPart.mData.mType > ESM::BodyPart::MT_Armor ) messages.add(id, "Invalid type", "", CSMDoc::Message::Severity_Error); // Check MODL if ( bodyPart.mModel.empty() ) messages.add(id, "Model is missing", "", CSMDoc::Message::Severity_Error); else if ( mMeshes.searchId( bodyPart.mModel ) == -1 ) messages.add(id, "Model '" + bodyPart.mModel + "' does not exist", "", CSMDoc::Message::Severity_Error); // Check FNAM for skin body parts (for non-skin body parts it's meaningless) if ( bodyPart.mData.mType == ESM::BodyPart::MT_Skin ) { if ( bodyPart.mRace.empty() ) messages.add(id, "Race is missing", "", CSMDoc::Message::Severity_Error); else if ( mRaces.searchId( bodyPart.mRace ) == -1 ) messages.add(id, "Race '" + bodyPart.mRace + "' does not exist", "", CSMDoc::Message::Severity_Error); } } ================================================ FILE: apps/opencs/model/tools/bodypartcheck.hpp ================================================ #ifndef CSM_TOOLS_BODYPARTCHECK_H #define CSM_TOOLS_BODYPARTCHECK_H #include #include #include "../world/resources.hpp" #include "../world/idcollection.hpp" #include "../doc/stage.hpp" namespace CSMTools { /// \brief VerifyStage: make sure that body part records are internally consistent class BodyPartCheckStage : public CSMDoc::Stage { const CSMWorld::IdCollection &mBodyParts; const CSMWorld::Resources &mMeshes; const CSMWorld::IdCollection &mRaces; bool mIgnoreBaseRecords; public: BodyPartCheckStage( const CSMWorld::IdCollection &bodyParts, const CSMWorld::Resources &meshes, const CSMWorld::IdCollection &races ); int setup() override; ///< \return number of steps void perform(int stage, CSMDoc::Messages &messages) override; ///< Messages resulting from this tage will be appended to \a messages. }; } #endif ================================================ FILE: apps/opencs/model/tools/classcheck.cpp ================================================ #include "classcheck.hpp" #include #include #include #include "../prefs/state.hpp" #include "../world/universalid.hpp" CSMTools::ClassCheckStage::ClassCheckStage (const CSMWorld::IdCollection& classes) : mClasses (classes) { mIgnoreBaseRecords = false; } int CSMTools::ClassCheckStage::setup() { mIgnoreBaseRecords = CSMPrefs::get()["Reports"]["ignore-base-records"].isTrue(); return mClasses.getSize(); } void CSMTools::ClassCheckStage::perform (int stage, CSMDoc::Messages& messages) { const CSMWorld::Record& record = mClasses.getRecord (stage); // Skip "Base" records (setting!) and "Deleted" records if ((mIgnoreBaseRecords && record.mState == CSMWorld::RecordBase::State_BaseOnly) || record.isDeleted()) return; const ESM::Class& class_ = record.get(); CSMWorld::UniversalId id (CSMWorld::UniversalId::Type_Class, class_.mId); // A class should have a name if (class_.mName.empty()) messages.add(id, "Name is missing", "", CSMDoc::Message::Severity_Error); // A playable class should have a description if (class_.mData.mIsPlayable != 0 && class_.mDescription.empty()) messages.add(id, "Description of a playable class is missing", "", CSMDoc::Message::Severity_Warning); // test for invalid attributes for (int i=0; i<2; ++i) if (class_.mData.mAttribute[i]==-1) { messages.add(id, "Attribute #" + std::to_string(i) + " is not set", "", CSMDoc::Message::Severity_Error); } if (class_.mData.mAttribute[0]==class_.mData.mAttribute[1] && class_.mData.mAttribute[0]!=-1) { messages.add(id, "Same attribute is listed twice", "", CSMDoc::Message::Severity_Error); } // test for non-unique skill std::map skills; // ID, number of occurrences for (int i=0; i<5; ++i) for (int i2=0; i2<2; ++i2) ++skills[class_.mData.mSkills[i][i2]]; for (auto &skill : skills) if (skill.second>1) { messages.add(id, "Skill " + ESM::Skill::indexToId (skill.first) + " is listed more than once", "", CSMDoc::Message::Severity_Error); } } ================================================ FILE: apps/opencs/model/tools/classcheck.hpp ================================================ #ifndef CSM_TOOLS_CLASSCHECK_H #define CSM_TOOLS_CLASSCHECK_H #include #include "../world/idcollection.hpp" #include "../doc/stage.hpp" namespace CSMTools { /// \brief VerifyStage: make sure that class records are internally consistent class ClassCheckStage : public CSMDoc::Stage { const CSMWorld::IdCollection& mClasses; bool mIgnoreBaseRecords; public: ClassCheckStage (const CSMWorld::IdCollection& classes); int setup() override; ///< \return number of steps void perform (int stage, CSMDoc::Messages& messages) override; ///< Messages resulting from this tage will be appended to \a messages. }; } #endif ================================================ FILE: apps/opencs/model/tools/enchantmentcheck.cpp ================================================ #include "enchantmentcheck.hpp" #include "../prefs/state.hpp" #include "../world/universalid.hpp" CSMTools::EnchantmentCheckStage::EnchantmentCheckStage (const CSMWorld::IdCollection& enchantments) : mEnchantments (enchantments) { mIgnoreBaseRecords = false; } int CSMTools::EnchantmentCheckStage::setup() { mIgnoreBaseRecords = CSMPrefs::get()["Reports"]["ignore-base-records"].isTrue(); return mEnchantments.getSize(); } void CSMTools::EnchantmentCheckStage::perform (int stage, CSMDoc::Messages& messages) { const CSMWorld::Record& record = mEnchantments.getRecord (stage); // Skip "Base" records (setting!) and "Deleted" records if ((mIgnoreBaseRecords && record.mState == CSMWorld::RecordBase::State_BaseOnly) || record.isDeleted()) return; const ESM::Enchantment& enchantment = record.get(); CSMWorld::UniversalId id (CSMWorld::UniversalId::Type_Enchantment, enchantment.mId); if (enchantment.mData.mType < 0 || enchantment.mData.mType > 3) messages.add(id, "Invalid type", "", CSMDoc::Message::Severity_Error); if (enchantment.mData.mCost < 0) messages.add(id, "Cost is negative", "", CSMDoc::Message::Severity_Error); if (enchantment.mData.mCharge < 0) messages.add(id, "Charge is negative", "", CSMDoc::Message::Severity_Error); if (enchantment.mData.mCost > enchantment.mData.mCharge) messages.add(id, "Cost is higher than charge", "", CSMDoc::Message::Severity_Error); if (enchantment.mEffects.mList.empty()) { messages.add(id, "Enchantment doesn't have any magic effects", "", CSMDoc::Message::Severity_Warning); } else { std::vector::const_iterator effect = enchantment.mEffects.mList.begin(); for (size_t i = 1; i <= enchantment.mEffects.mList.size(); i++) { const std::string number = std::to_string(i); // At the time of writing this effects, attributes and skills are hardcoded if (effect->mEffectID < 0 || effect->mEffectID > 142) { messages.add(id, "Effect #" + number + " is invalid", "", CSMDoc::Message::Severity_Error); ++effect; continue; } if (effect->mSkill < -1 || effect->mSkill > 26) messages.add(id, "Effect #" + number + " affected skill is invalid", "", CSMDoc::Message::Severity_Error); if (effect->mAttribute < -1 || effect->mAttribute > 7) messages.add(id, "Effect #" + number + " affected attribute is invalid", "", CSMDoc::Message::Severity_Error); if (effect->mRange < 0 || effect->mRange > 2) messages.add(id, "Effect #" + number + " range is invalid", "", CSMDoc::Message::Severity_Error); if (effect->mArea < 0) messages.add(id, "Effect #" + number + " area is negative", "", CSMDoc::Message::Severity_Error); if (effect->mDuration < 0) messages.add(id, "Effect #" + number + " duration is negative", "", CSMDoc::Message::Severity_Error); if (effect->mMagnMin < 0) messages.add(id, "Effect #" + number + " minimum magnitude is negative", "", CSMDoc::Message::Severity_Error); if (effect->mMagnMax < 0) messages.add(id, "Effect #" + number + " maximum magnitude is negative", "", CSMDoc::Message::Severity_Error); if (effect->mMagnMin > effect->mMagnMax) messages.add(id, "Effect #" + number + " minimum magnitude is higher than maximum magnitude", "", CSMDoc::Message::Severity_Error); ++effect; } } } ================================================ FILE: apps/opencs/model/tools/enchantmentcheck.hpp ================================================ #ifndef CSM_TOOLS_ENCHANTMENTCHECK_H #define CSM_TOOLS_ENCHANTMENTCHECK_H #include #include "../world/idcollection.hpp" #include "../doc/stage.hpp" namespace CSMTools { /// \brief Make sure that enchantment records are correct class EnchantmentCheckStage : public CSMDoc::Stage { const CSMWorld::IdCollection& mEnchantments; bool mIgnoreBaseRecords; public: EnchantmentCheckStage (const CSMWorld::IdCollection& enchantments); int setup() override; ///< \return number of steps void perform (int stage, CSMDoc::Messages& messages) override; ///< Messages resulting from this tage will be appended to \a messages. }; } #endif ================================================ FILE: apps/opencs/model/tools/factioncheck.cpp ================================================ #include "factioncheck.hpp" #include #include #include "../prefs/state.hpp" #include "../world/universalid.hpp" CSMTools::FactionCheckStage::FactionCheckStage (const CSMWorld::IdCollection& factions) : mFactions (factions) { mIgnoreBaseRecords = false; } int CSMTools::FactionCheckStage::setup() { mIgnoreBaseRecords = CSMPrefs::get()["Reports"]["ignore-base-records"].isTrue(); return mFactions.getSize(); } void CSMTools::FactionCheckStage::perform (int stage, CSMDoc::Messages& messages) { const CSMWorld::Record& record = mFactions.getRecord (stage); // Skip "Base" records (setting!) and "Deleted" records if ((mIgnoreBaseRecords && record.mState == CSMWorld::RecordBase::State_BaseOnly) || record.isDeleted()) return; const ESM::Faction& faction = record.get(); CSMWorld::UniversalId id (CSMWorld::UniversalId::Type_Faction, faction.mId); // test for empty name if (faction.mName.empty()) messages.add(id, "Name is missing", "", CSMDoc::Message::Severity_Error); // test for invalid attributes if (faction.mData.mAttribute[0]==faction.mData.mAttribute[1] && faction.mData.mAttribute[0]!=-1) { messages.add(id, "Same attribute is listed twice", "", CSMDoc::Message::Severity_Error); } // test for non-unique skill std::map skills; // ID, number of occurrences for (int i=0; i<7; ++i) if (faction.mData.mSkills[i]!=-1) ++skills[faction.mData.mSkills[i]]; for (auto &skill : skills) if (skill.second>1) { messages.add(id, "Skill " + ESM::Skill::indexToId (skill.first) + " is listed more than once", "", CSMDoc::Message::Severity_Error); } /// \todo check data members that can't be edited in the table view } ================================================ FILE: apps/opencs/model/tools/factioncheck.hpp ================================================ #ifndef CSM_TOOLS_FACTIONCHECK_H #define CSM_TOOLS_FACTIONCHECK_H #include #include "../world/idcollection.hpp" #include "../doc/stage.hpp" namespace CSMTools { /// \brief VerifyStage: make sure that faction records are internally consistent class FactionCheckStage : public CSMDoc::Stage { const CSMWorld::IdCollection& mFactions; bool mIgnoreBaseRecords; public: FactionCheckStage (const CSMWorld::IdCollection& factions); int setup() override; ///< \return number of steps void perform (int stage, CSMDoc::Messages& messages) override; ///< Messages resulting from this tage will be appended to \a messages. }; } #endif ================================================ FILE: apps/opencs/model/tools/gmstcheck.cpp ================================================ #include "gmstcheck.hpp" #include #include "../prefs/state.hpp" #include "../world/defaultgmsts.hpp" CSMTools::GmstCheckStage::GmstCheckStage(const CSMWorld::IdCollection& gameSettings) : mGameSettings(gameSettings) { mIgnoreBaseRecords = false; } int CSMTools::GmstCheckStage::setup() { mIgnoreBaseRecords = CSMPrefs::get()["Reports"]["ignore-base-records"].isTrue(); return mGameSettings.getSize(); } void CSMTools::GmstCheckStage::perform(int stage, CSMDoc::Messages& messages) { const CSMWorld::Record& record = mGameSettings.getRecord (stage); // Skip "Base" records (setting!) and "Deleted" records if ((mIgnoreBaseRecords && record.mState == CSMWorld::RecordBase::State_BaseOnly) || record.isDeleted()) return; const ESM::GameSetting& gmst = record.get(); CSMWorld::UniversalId id (CSMWorld::UniversalId::Type_Gmst, gmst.mId); // Test for empty string if (gmst.mValue.getType() == ESM::VT_String && gmst.mValue.getString().empty()) messages.add(id, gmst.mId + " is an empty string", "", CSMDoc::Message::Severity_Warning); // Checking type and limits // optimization - compare it to lists based on naming convention (f-float,i-int,s-string) if (gmst.mId[0] == 'f') { for (size_t i = 0; i < CSMWorld::DefaultGmsts::FloatCount; ++i) { if (gmst.mId == CSMWorld::DefaultGmsts::Floats[i]) { if (gmst.mValue.getType() != ESM::VT_Float) { std::ostringstream stream; stream << "Expected float type for " << gmst.mId << " but found " << varTypeToString(gmst.mValue.getType()) << " type"; messages.add(id, stream.str(), "", CSMDoc::Message::Severity_Error); } if (gmst.mValue.getFloat() < CSMWorld::DefaultGmsts::FloatLimits[i*2]) messages.add(id, gmst.mId + " is less than the suggested range", "", CSMDoc::Message::Severity_Warning); if (gmst.mValue.getFloat() > CSMWorld::DefaultGmsts::FloatLimits[i*2+1]) messages.add(id, gmst.mId + " is more than the suggested range", "", CSMDoc::Message::Severity_Warning); break; // for loop } } } else if (gmst.mId[0] == 'i') { for (size_t i = 0; i < CSMWorld::DefaultGmsts::IntCount; ++i) { if (gmst.mId == CSMWorld::DefaultGmsts::Ints[i]) { if (gmst.mValue.getType() != ESM::VT_Int) { std::ostringstream stream; stream << "Expected int type for " << gmst.mId << " but found " << varTypeToString(gmst.mValue.getType()) << " type"; messages.add(id, stream.str(), "", CSMDoc::Message::Severity_Error); } if (gmst.mValue.getInteger() < CSMWorld::DefaultGmsts::IntLimits[i*2]) messages.add(id, gmst.mId + " is less than the suggested range", "", CSMDoc::Message::Severity_Warning); if (gmst.mValue.getInteger() > CSMWorld::DefaultGmsts::IntLimits[i*2+1]) messages.add(id, gmst.mId + " is more than the suggested range", "", CSMDoc::Message::Severity_Warning); break; // for loop } } } else if (gmst.mId[0] == 's') { for (size_t i = 0; i < CSMWorld::DefaultGmsts::StringCount; ++i) { if (gmst.mId == CSMWorld::DefaultGmsts::Strings[i]) { ESM::VarType type = gmst.mValue.getType(); if (type != ESM::VT_String && type != ESM::VT_None) { std::ostringstream stream; stream << "Expected string or none type for " << gmst.mId << " but found " << varTypeToString(gmst.mValue.getType()) << " type"; messages.add(id, stream.str(), "", CSMDoc::Message::Severity_Error); } break; // for loop } } } } std::string CSMTools::GmstCheckStage::varTypeToString(ESM::VarType type) { switch (type) { case ESM::VT_Unknown: return "unknown"; case ESM::VT_None: return "none"; case ESM::VT_Short: return "short"; case ESM::VT_Int: return "int"; case ESM::VT_Long: return "long"; case ESM::VT_Float: return "float"; case ESM::VT_String: return "string"; default: return "unhandled"; } } ================================================ FILE: apps/opencs/model/tools/gmstcheck.hpp ================================================ #ifndef CSM_TOOLS_GMSTCHECK_H #define CSM_TOOLS_GMSTCHECK_H #include #include "../world/idcollection.hpp" #include "../doc/stage.hpp" namespace CSMTools { /// \brief VerifyStage: make sure that GMSTs are alright class GmstCheckStage : public CSMDoc::Stage { public: GmstCheckStage(const CSMWorld::IdCollection& gameSettings); int setup() override; ///< \return number of steps void perform(int stage, CSMDoc::Messages& messages) override; ///< Messages resulting from this stage will be appended to \a messages private: const CSMWorld::IdCollection& mGameSettings; bool mIgnoreBaseRecords; std::string varTypeToString(ESM::VarType); }; } #endif ================================================ FILE: apps/opencs/model/tools/journalcheck.cpp ================================================ #include "journalcheck.hpp" #include #include "../prefs/state.hpp" CSMTools::JournalCheckStage::JournalCheckStage(const CSMWorld::IdCollection &journals, const CSMWorld::InfoCollection& journalInfos) : mJournals(journals), mJournalInfos(journalInfos) { mIgnoreBaseRecords = false; } int CSMTools::JournalCheckStage::setup() { mIgnoreBaseRecords = CSMPrefs::get()["Reports"]["ignore-base-records"].isTrue(); return mJournals.getSize(); } void CSMTools::JournalCheckStage::perform(int stage, CSMDoc::Messages& messages) { const CSMWorld::Record &journalRecord = mJournals.getRecord(stage); // Skip "Base" records (setting!) and "Deleted" records if ((mIgnoreBaseRecords && journalRecord.mState == CSMWorld::RecordBase::State_BaseOnly) || journalRecord.isDeleted()) return; const ESM::Dialogue &journal = journalRecord.get(); int statusNamedCount = 0; int totalInfoCount = 0; std::set questIndices; CSMWorld::InfoCollection::Range range = mJournalInfos.getTopicRange(journal.mId); for (CSMWorld::InfoCollection::RecordConstIterator it = range.first; it != range.second; ++it) { const CSMWorld::Record infoRecord = (*it); if (infoRecord.isDeleted()) continue; const CSMWorld::Info& journalInfo = infoRecord.get(); totalInfoCount += 1; if (journalInfo.mQuestStatus == ESM::DialInfo::QS_Name) { statusNamedCount += 1; } // Skip "Base" records (setting!) if (mIgnoreBaseRecords && infoRecord.mState == CSMWorld::RecordBase::State_BaseOnly) continue; if (journalInfo.mResponse.empty()) { CSMWorld::UniversalId id(CSMWorld::UniversalId::Type_JournalInfo, journalInfo.mId); messages.add(id, "Missing journal entry text", "", CSMDoc::Message::Severity_Warning); } std::pair::iterator, bool> result = questIndices.insert(journalInfo.mData.mJournalIndex); // Duplicate index if (!result.second) { CSMWorld::UniversalId id(CSMWorld::UniversalId::Type_JournalInfo, journalInfo.mId); messages.add(id, "Duplicated quest index " + std::to_string(journalInfo.mData.mJournalIndex), "", CSMDoc::Message::Severity_Error); } } if (totalInfoCount == 0) { CSMWorld::UniversalId id(CSMWorld::UniversalId::Type_Journal, journal.mId); messages.add(id, "No related journal entry", "", CSMDoc::Message::Severity_Warning); } else if (statusNamedCount > 1) { CSMWorld::UniversalId id(CSMWorld::UniversalId::Type_Journal, journal.mId); messages.add(id, "Multiple entries with quest status 'Named'", "", CSMDoc::Message::Severity_Error); } } ================================================ FILE: apps/opencs/model/tools/journalcheck.hpp ================================================ #ifndef CSM_TOOLS_JOURNALCHECK_H #define CSM_TOOLS_JOURNALCHECK_H #include #include "../world/idcollection.hpp" #include "../world/infocollection.hpp" #include "../doc/stage.hpp" namespace CSMTools { /// \brief VerifyStage: make sure that journal infos are good class JournalCheckStage : public CSMDoc::Stage { public: JournalCheckStage(const CSMWorld::IdCollection& journals, const CSMWorld::InfoCollection& journalInfos); int setup() override; ///< \return number of steps void perform(int stage, CSMDoc::Messages& messages) override; ///< Messages resulting from this stage will be appended to \a messages private: const CSMWorld::IdCollection& mJournals; const CSMWorld::InfoCollection& mJournalInfos; bool mIgnoreBaseRecords; }; } #endif ================================================ FILE: apps/opencs/model/tools/magiceffectcheck.cpp ================================================ #include "magiceffectcheck.hpp" #include #include "../prefs/state.hpp" std::string CSMTools::MagicEffectCheckStage::checkObject(const std::string &id, const CSMWorld::UniversalId &type, const std::string &column) const { CSMWorld::RefIdData::LocalIndex index = mObjects.getDataSet().searchId(id); if (index.first == -1) return (column + " '" + id + "' does not exist"); else if (index.second != type.getType()) return (column + " '" + id + "' does not have " + type.getTypeName() + " type"); return std::string(); } CSMTools::MagicEffectCheckStage::MagicEffectCheckStage(const CSMWorld::IdCollection &effects, const CSMWorld::IdCollection &sounds, const CSMWorld::RefIdCollection &objects, const CSMWorld::Resources &icons, const CSMWorld::Resources &textures) : mMagicEffects(effects), mSounds(sounds), mObjects(objects), mIcons(icons), mTextures(textures) { mIgnoreBaseRecords = false; } int CSMTools::MagicEffectCheckStage::setup() { mIgnoreBaseRecords = CSMPrefs::get()["Reports"]["ignore-base-records"].isTrue(); return mMagicEffects.getSize(); } void CSMTools::MagicEffectCheckStage::perform(int stage, CSMDoc::Messages &messages) { const CSMWorld::Record &record = mMagicEffects.getRecord(stage); // Skip "Base" records (setting!) and "Deleted" records if ((mIgnoreBaseRecords && record.mState == CSMWorld::RecordBase::State_BaseOnly) || record.isDeleted()) return; ESM::MagicEffect effect = record.get(); CSMWorld::UniversalId id(CSMWorld::UniversalId::Type_MagicEffect, effect.mId); if (effect.mDescription.empty()) { messages.add(id, "Description is missing", "", CSMDoc::Message::Severity_Warning); } if (effect.mData.mBaseCost < 0.0f) { messages.add(id, "Base cost is negative", "", CSMDoc::Message::Severity_Error); } if (effect.mIcon.empty()) { messages.add(id, "Icon is missing", "", CSMDoc::Message::Severity_Error); } else { if (mIcons.searchId(effect.mIcon) == -1) { std::string ddsIcon = effect.mIcon; if (!(Misc::ResourceHelpers::changeExtensionToDds(ddsIcon) && mIcons.searchId(ddsIcon) != -1)) messages.add(id, "Icon '" + effect.mIcon + "' does not exist", "", CSMDoc::Message::Severity_Error); } } if (!effect.mParticle.empty()) { if (mTextures.searchId(effect.mParticle) == -1) { std::string ddsParticle = effect.mParticle; if (!(Misc::ResourceHelpers::changeExtensionToDds(ddsParticle) && mTextures.searchId(ddsParticle) != -1)) messages.add(id, "Particle texture '" + effect.mParticle + "' does not exist", "", CSMDoc::Message::Severity_Error); } } if (!effect.mCasting.empty()) { const std::string error = checkObject(effect.mCasting, CSMWorld::UniversalId::Type_Static, "Casting object"); if (!error.empty()) messages.add(id, error, "", CSMDoc::Message::Severity_Error); } if (!effect.mHit.empty()) { const std::string error = checkObject(effect.mHit, CSMWorld::UniversalId::Type_Static, "Hit object"); if (!error.empty()) messages.add(id, error, "", CSMDoc::Message::Severity_Error); } if (!effect.mArea.empty()) { const std::string error = checkObject(effect.mArea, CSMWorld::UniversalId::Type_Static, "Area object"); if (!error.empty()) messages.add(id, error, "", CSMDoc::Message::Severity_Error); } if (!effect.mBolt.empty()) { const std::string error = checkObject(effect.mBolt, CSMWorld::UniversalId::Type_Weapon, "Bolt object"); if (!error.empty()) messages.add(id, error, "", CSMDoc::Message::Severity_Error); } if (!effect.mCastSound.empty() && mSounds.searchId(effect.mCastSound) == -1) messages.add(id, "Casting sound '" + effect.mCastSound + "' does not exist", "", CSMDoc::Message::Severity_Error); if (!effect.mHitSound.empty() && mSounds.searchId(effect.mHitSound) == -1) messages.add(id, "Hit sound '" + effect.mHitSound + "' does not exist", "", CSMDoc::Message::Severity_Error); if (!effect.mAreaSound.empty() && mSounds.searchId(effect.mAreaSound) == -1) messages.add(id, "Area sound '" + effect.mAreaSound + "' does not exist", "", CSMDoc::Message::Severity_Error); if (!effect.mBoltSound.empty() && mSounds.searchId(effect.mBoltSound) == -1) messages.add(id, "Bolt sound '" + effect.mBoltSound + "' does not exist", "", CSMDoc::Message::Severity_Error); } ================================================ FILE: apps/opencs/model/tools/magiceffectcheck.hpp ================================================ #ifndef CSM_TOOLS_MAGICEFFECTCHECK_HPP #define CSM_TOOLS_MAGICEFFECTCHECK_HPP #include #include #include "../world/idcollection.hpp" #include "../world/refidcollection.hpp" #include "../world/resources.hpp" #include "../doc/stage.hpp" namespace CSMTools { /// \brief VerifyStage: make sure that magic effect records are internally consistent class MagicEffectCheckStage : public CSMDoc::Stage { const CSMWorld::IdCollection &mMagicEffects; const CSMWorld::IdCollection &mSounds; const CSMWorld::RefIdCollection &mObjects; const CSMWorld::Resources &mIcons; const CSMWorld::Resources &mTextures; bool mIgnoreBaseRecords; private: std::string checkObject(const std::string &id, const CSMWorld::UniversalId &type, const std::string &column) const; public: MagicEffectCheckStage(const CSMWorld::IdCollection &effects, const CSMWorld::IdCollection &sounds, const CSMWorld::RefIdCollection &objects, const CSMWorld::Resources &icons, const CSMWorld::Resources &textures); int setup() override; ///< \return number of steps void perform (int stage, CSMDoc::Messages &messages) override; ///< Messages resulting from this tage will be appended to \a messages. }; } #endif ================================================ FILE: apps/opencs/model/tools/mandatoryid.cpp ================================================ #include "mandatoryid.hpp" #include "../world/collectionbase.hpp" #include "../world/record.hpp" CSMTools::MandatoryIdStage::MandatoryIdStage (const CSMWorld::CollectionBase& idCollection, const CSMWorld::UniversalId& collectionId, const std::vector& ids) : mIdCollection (idCollection), mCollectionId (collectionId), mIds (ids) {} int CSMTools::MandatoryIdStage::setup() { return static_cast(mIds.size()); } void CSMTools::MandatoryIdStage::perform (int stage, CSMDoc::Messages& messages) { if (mIdCollection.searchId (mIds.at (stage))==-1 || mIdCollection.getRecord (mIds.at (stage)).isDeleted()) messages.add (mCollectionId, "Missing mandatory record: " + mIds.at (stage)); } ================================================ FILE: apps/opencs/model/tools/mandatoryid.hpp ================================================ #ifndef CSM_TOOLS_MANDATORYID_H #define CSM_TOOLS_MANDATORYID_H #include #include #include "../world/universalid.hpp" #include "../doc/stage.hpp" namespace CSMWorld { class CollectionBase; } namespace CSMTools { /// \brief Verify stage: make sure that records with specific IDs exist. class MandatoryIdStage : public CSMDoc::Stage { const CSMWorld::CollectionBase& mIdCollection; CSMWorld::UniversalId mCollectionId; std::vector mIds; public: MandatoryIdStage (const CSMWorld::CollectionBase& idCollection, const CSMWorld::UniversalId& collectionId, const std::vector& ids); int setup() override; ///< \return number of steps void perform (int stage, CSMDoc::Messages& messages) override; ///< Messages resulting from this tage will be appended to \a messages. }; } #endif ================================================ FILE: apps/opencs/model/tools/mergeoperation.cpp ================================================ #include "mergeoperation.hpp" #include "../doc/state.hpp" #include "../doc/document.hpp" #include "mergestages.hpp" CSMTools::MergeOperation::MergeOperation (CSMDoc::Document& document, ToUTF8::FromType encoding) : CSMDoc::Operation (CSMDoc::State_Merging, true), mState (document) { appendStage (new StartMergeStage (mState)); appendStage (new MergeIdCollectionStage (mState, &CSMWorld::Data::getGlobals)); appendStage (new MergeIdCollectionStage (mState, &CSMWorld::Data::getGmsts)); appendStage (new MergeIdCollectionStage (mState, &CSMWorld::Data::getSkills)); appendStage (new MergeIdCollectionStage (mState, &CSMWorld::Data::getClasses)); appendStage (new MergeIdCollectionStage (mState, &CSMWorld::Data::getFactions)); appendStage (new MergeIdCollectionStage (mState, &CSMWorld::Data::getRaces)); appendStage (new MergeIdCollectionStage (mState, &CSMWorld::Data::getSounds)); appendStage (new MergeIdCollectionStage (mState, &CSMWorld::Data::getScripts)); appendStage (new MergeIdCollectionStage (mState, &CSMWorld::Data::getRegions)); appendStage (new MergeIdCollectionStage (mState, &CSMWorld::Data::getBirthsigns)); appendStage (new MergeIdCollectionStage (mState, &CSMWorld::Data::getSpells)); appendStage (new MergeIdCollectionStage (mState, &CSMWorld::Data::getTopics)); appendStage (new MergeIdCollectionStage (mState, &CSMWorld::Data::getJournals)); appendStage (new MergeIdCollectionStage (mState, &CSMWorld::Data::getCells)); appendStage (new MergeIdCollectionStage (mState, &CSMWorld::Data::getFilters)); appendStage (new MergeIdCollectionStage (mState, &CSMWorld::Data::getEnchantments)); appendStage (new MergeIdCollectionStage (mState, &CSMWorld::Data::getBodyParts)); appendStage (new MergeIdCollectionStage (mState, &CSMWorld::Data::getDebugProfiles)); appendStage (new MergeIdCollectionStage (mState, &CSMWorld::Data::getSoundGens)); appendStage (new MergeIdCollectionStage (mState, &CSMWorld::Data::getMagicEffects)); appendStage (new MergeIdCollectionStage (mState, &CSMWorld::Data::getStartScripts)); appendStage (new MergeIdCollectionStage > (mState, &CSMWorld::Data::getPathgrids)); appendStage (new MergeIdCollectionStage (mState, &CSMWorld::Data::getTopicInfos)); appendStage (new MergeIdCollectionStage (mState, &CSMWorld::Data::getJournalInfos)); appendStage (new MergeRefIdsStage (mState)); appendStage (new MergeReferencesStage (mState)); appendStage (new MergeReferencesStage (mState)); appendStage (new PopulateLandTexturesMergeStage (mState)); appendStage (new MergeLandStage (mState)); appendStage (new FixLandsAndLandTexturesMergeStage (mState)); appendStage (new CleanupLandTexturesMergeStage (mState)); appendStage (new FinishMergedDocumentStage (mState, encoding)); } void CSMTools::MergeOperation::setTarget (std::unique_ptr document) { mState.mTarget = std::move(document); } void CSMTools::MergeOperation::operationDone() { CSMDoc::Operation::operationDone(); if (mState.mCompleted) emit mergeDone (mState.mTarget.release()); } ================================================ FILE: apps/opencs/model/tools/mergeoperation.hpp ================================================ #ifndef CSM_TOOLS_MERGEOPERATION_H #define CSM_TOOLS_MERGEOPERATION_H #include #include #include "../doc/operation.hpp" #include "mergestate.hpp" namespace CSMDoc { class Document; } namespace CSMTools { class MergeOperation : public CSMDoc::Operation { Q_OBJECT MergeState mState; public: MergeOperation (CSMDoc::Document& document, ToUTF8::FromType encoding); /// \attention Do not call this function while a merge is running. void setTarget (std::unique_ptr document); protected slots: void operationDone() override; signals: /// \attention When this signal is emitted, *this hands over the ownership of the /// document. This signal must be handled to avoid a leak. void mergeDone (CSMDoc::Document *document); }; } #endif ================================================ FILE: apps/opencs/model/tools/mergestages.cpp ================================================ #include "mergestages.hpp" #include #include #include "mergestate.hpp" #include "../doc/document.hpp" #include "../world/commands.hpp" #include "../world/data.hpp" #include "../world/idtable.hpp" CSMTools::StartMergeStage::StartMergeStage (MergeState& state) : mState (state) {} int CSMTools::StartMergeStage::setup() { return 1; } void CSMTools::StartMergeStage::perform (int stage, CSMDoc::Messages& messages) { mState.mCompleted = false; mState.mTextureIndices.clear(); } CSMTools::FinishMergedDocumentStage::FinishMergedDocumentStage (MergeState& state, ToUTF8::FromType encoding) : mState (state), mEncoder (encoding) {} int CSMTools::FinishMergedDocumentStage::setup() { return 1; } void CSMTools::FinishMergedDocumentStage::perform (int stage, CSMDoc::Messages& messages) { // We know that the content file list contains at least two entries and that the first one // does exist on disc (otherwise it would have been impossible to initiate a merge on that // document). boost::filesystem::path path = mState.mSource.getContentFiles()[0]; ESM::ESMReader reader; reader.setEncoder (&mEncoder); reader.open (path.string()); CSMWorld::MetaData source; source.mId = "sys::meta"; source.load (reader); CSMWorld::MetaData target = mState.mTarget->getData().getMetaData(); target.mAuthor = source.mAuthor; target.mDescription = source.mDescription; mState.mTarget->getData().setMetaData (target); mState.mCompleted = true; } CSMTools::MergeRefIdsStage::MergeRefIdsStage (MergeState& state) : mState (state) {} int CSMTools::MergeRefIdsStage::setup() { return mState.mSource.getData().getReferenceables().getSize(); } void CSMTools::MergeRefIdsStage::perform (int stage, CSMDoc::Messages& messages) { mState.mSource.getData().getReferenceables().copyTo ( stage, mState.mTarget->getData().getReferenceables()); } CSMTools::MergeReferencesStage::MergeReferencesStage (MergeState& state) : mState (state) {} int CSMTools::MergeReferencesStage::setup() { mIndex.clear(); return mState.mSource.getData().getReferences().getSize(); } void CSMTools::MergeReferencesStage::perform (int stage, CSMDoc::Messages& messages) { const CSMWorld::Record& record = mState.mSource.getData().getReferences().getRecord (stage); if (!record.isDeleted()) { CSMWorld::CellRef ref = record.get(); ref.mOriginalCell = ref.mCell; ref.mRefNum.mIndex = mIndex[Misc::StringUtils::lowerCase (ref.mCell)]++; ref.mRefNum.mContentFile = 0; ref.mNew = false; CSMWorld::Record newRecord ( CSMWorld::RecordBase::State_ModifiedOnly, nullptr, &ref); mState.mTarget->getData().getReferences().appendRecord (newRecord); } } CSMTools::PopulateLandTexturesMergeStage::PopulateLandTexturesMergeStage (MergeState& state) : mState (state) { } int CSMTools::PopulateLandTexturesMergeStage::setup() { return mState.mSource.getData().getLandTextures().getSize(); } void CSMTools::PopulateLandTexturesMergeStage::perform (int stage, CSMDoc::Messages& messages) { const CSMWorld::Record& record = mState.mSource.getData().getLandTextures().getRecord (stage); if (!record.isDeleted()) { mState.mTarget->getData().getLandTextures().appendRecord(record); } } CSMTools::MergeLandStage::MergeLandStage (MergeState& state) : mState (state) { } int CSMTools::MergeLandStage::setup() { return mState.mSource.getData().getLand().getSize(); } void CSMTools::MergeLandStage::perform (int stage, CSMDoc::Messages& messages) { const CSMWorld::Record& record = mState.mSource.getData().getLand().getRecord (stage); if (!record.isDeleted()) { mState.mTarget->getData().getLand().appendRecord (record); } } CSMTools::FixLandsAndLandTexturesMergeStage::FixLandsAndLandTexturesMergeStage (MergeState& state) : mState (state) { } int CSMTools::FixLandsAndLandTexturesMergeStage::setup() { // We will have no more than the source return mState.mSource.getData().getLand().getSize(); } void CSMTools::FixLandsAndLandTexturesMergeStage::perform (int stage, CSMDoc::Messages& messages) { if (stage < mState.mTarget->getData().getLand().getSize()) { CSMWorld::IdTable& landTable = dynamic_cast( *mState.mTarget->getData().getTableModel(CSMWorld::UniversalId::Type_Lands)); CSMWorld::IdTable& ltexTable = dynamic_cast( *mState.mTarget->getData().getTableModel(CSMWorld::UniversalId::Type_LandTextures)); std::string id = mState.mTarget->getData().getLand().getId(stage); CSMWorld::TouchLandCommand cmd(landTable, ltexTable, id); cmd.redo(); // Get rid of base data const CSMWorld::Record& oldRecord = mState.mTarget->getData().getLand().getRecord (stage); CSMWorld::Record newRecord(CSMWorld::RecordBase::State_ModifiedOnly, nullptr, &oldRecord.get()); mState.mTarget->getData().getLand().setRecord(stage, newRecord); } } CSMTools::CleanupLandTexturesMergeStage::CleanupLandTexturesMergeStage (MergeState& state) : mState (state) { } int CSMTools::CleanupLandTexturesMergeStage::setup() { return 1; } void CSMTools::CleanupLandTexturesMergeStage::perform (int stage, CSMDoc::Messages& messages) { auto& landTextures = mState.mTarget->getData().getLandTextures(); for (int i = 0; i < landTextures.getSize(); ) { if (!landTextures.getRecord(i).isModified()) landTextures.removeRows(i, 1); else ++i; } } ================================================ FILE: apps/opencs/model/tools/mergestages.hpp ================================================ #ifndef CSM_TOOLS_MERGESTAGES_H #define CSM_TOOLS_MERGESTAGES_H #include #include #include #include "../doc/stage.hpp" #include "../world/data.hpp" #include "mergestate.hpp" namespace CSMTools { class StartMergeStage : public CSMDoc::Stage { MergeState& mState; public: StartMergeStage (MergeState& state); int setup() override; ///< \return number of steps void perform (int stage, CSMDoc::Messages& messages) override; ///< Messages resulting from this stage will be appended to \a messages. }; class FinishMergedDocumentStage : public CSMDoc::Stage { MergeState& mState; ToUTF8::Utf8Encoder mEncoder; public: FinishMergedDocumentStage (MergeState& state, ToUTF8::FromType encoding); int setup() override; ///< \return number of steps void perform (int stage, CSMDoc::Messages& messages) override; ///< Messages resulting from this stage will be appended to \a messages. }; template > class MergeIdCollectionStage : public CSMDoc::Stage { MergeState& mState; Collection& (CSMWorld::Data::*mAccessor)(); public: MergeIdCollectionStage (MergeState& state, Collection& (CSMWorld::Data::*accessor)()); int setup() override; ///< \return number of steps void perform (int stage, CSMDoc::Messages& messages) override; ///< Messages resulting from this stage will be appended to \a messages. }; template MergeIdCollectionStage::MergeIdCollectionStage (MergeState& state, Collection& (CSMWorld::Data::*accessor)()) : mState (state), mAccessor (accessor) {} template int MergeIdCollectionStage::setup() { return (mState.mSource.getData().*mAccessor)().getSize(); } template void MergeIdCollectionStage::perform (int stage, CSMDoc::Messages& messages) { const Collection& source = (mState.mSource.getData().*mAccessor)(); Collection& target = (mState.mTarget->getData().*mAccessor)(); const CSMWorld::Record& record = source.getRecord (stage); if (!record.isDeleted()) target.appendRecord (CSMWorld::Record (CSMWorld::RecordBase::State_ModifiedOnly, nullptr, &record.get())); } class MergeRefIdsStage : public CSMDoc::Stage { MergeState& mState; public: MergeRefIdsStage (MergeState& state); int setup() override; ///< \return number of steps void perform (int stage, CSMDoc::Messages& messages) override; ///< Messages resulting from this stage will be appended to \a messages. }; class MergeReferencesStage : public CSMDoc::Stage { MergeState& mState; std::map mIndex; public: MergeReferencesStage (MergeState& state); int setup() override; ///< \return number of steps void perform (int stage, CSMDoc::Messages& messages) override; ///< Messages resulting from this stage will be appended to \a messages. }; /// Adds all land texture records that could potentially be referenced when merging class PopulateLandTexturesMergeStage : public CSMDoc::Stage { MergeState& mState; public: PopulateLandTexturesMergeStage (MergeState& state); int setup() override; ///< \return number of steps void perform (int stage, CSMDoc::Messages& messages) override; ///< Messages resulting from this stage will be appended to \a messages. }; class MergeLandStage : public CSMDoc::Stage { MergeState& mState; public: MergeLandStage (MergeState& state); int setup() override; ///< \return number of steps void perform (int stage, CSMDoc::Messages& messages) override; ///< Messages resulting from this stage will be appended to \a messages. }; /// During this stage, the complex process of combining LandTextures from /// potentially multiple plugins is undertaken. class FixLandsAndLandTexturesMergeStage : public CSMDoc::Stage { MergeState& mState; public: FixLandsAndLandTexturesMergeStage (MergeState& state); int setup() override; ///< \return number of steps void perform (int stage, CSMDoc::Messages& messages) override; ///< Messages resulting from this stage will be appended to \a messages. }; /// Removes base LandTexture records. This gets rid of the base records previously /// needed in FixLandsAndLandTexturesMergeStage. class CleanupLandTexturesMergeStage : public CSMDoc::Stage { MergeState& mState; public: CleanupLandTexturesMergeStage (MergeState& state); int setup() override; ///< \return number of steps void perform (int stage, CSMDoc::Messages& messages) override; ///< Messages resulting from this stage will be appended to \a messages. }; } #endif ================================================ FILE: apps/opencs/model/tools/mergestate.hpp ================================================ #ifndef CSM_TOOLS_MERGESTATE_H #define CSM_TOOLS_MERGESTATE_H #include #include #include #include "../doc/document.hpp" namespace CSMTools { struct MergeState { std::unique_ptr mTarget; CSMDoc::Document& mSource; bool mCompleted; std::map, int> mTextureIndices; // (texture, content file) -> new texture MergeState (CSMDoc::Document& source) : mSource (source), mCompleted (false) {} }; } #endif ================================================ FILE: apps/opencs/model/tools/pathgridcheck.cpp ================================================ #include "pathgridcheck.hpp" #include #include #include "../prefs/state.hpp" #include "../world/universalid.hpp" #include "../world/idcollection.hpp" #include "../world/subcellcollection.hpp" #include "../world/pathgrid.hpp" CSMTools::PathgridCheckStage::PathgridCheckStage (const CSMWorld::SubCellCollection& pathgrids) : mPathgrids (pathgrids) { mIgnoreBaseRecords = false; } int CSMTools::PathgridCheckStage::setup() { mIgnoreBaseRecords = CSMPrefs::get()["Reports"]["ignore-base-records"].isTrue(); return mPathgrids.getSize(); } void CSMTools::PathgridCheckStage::perform (int stage, CSMDoc::Messages& messages) { const CSMWorld::Record& record = mPathgrids.getRecord (stage); // Skip "Base" records (setting!) and "Deleted" records if ((mIgnoreBaseRecords && record.mState == CSMWorld::RecordBase::State_BaseOnly) || record.isDeleted()) return; const CSMWorld::Pathgrid& pathgrid = record.get(); CSMWorld::UniversalId id (CSMWorld::UniversalId::Type_Pathgrid, pathgrid.mId); // check the number of pathgrid points if (pathgrid.mData.mS2 < static_cast(pathgrid.mPoints.size())) messages.add (id, "Less points than expected", "", CSMDoc::Message::Severity_Error); else if (pathgrid.mData.mS2 > static_cast(pathgrid.mPoints.size())) messages.add (id, "More points than expected", "", CSMDoc::Message::Severity_Error); std::vector pointList(pathgrid.mPoints.size()); std::vector duplList; for (unsigned int i = 0; i < pathgrid.mEdges.size(); ++i) { if (pathgrid.mEdges[i].mV0 < static_cast(pathgrid.mPoints.size()) && pathgrid.mEdges[i].mV0 >= 0) { pointList[pathgrid.mEdges[i].mV0].mConnectionNum++; // first check for duplicate edges unsigned int j = 0; for (; j < pointList[pathgrid.mEdges[i].mV0].mOtherIndex.size(); ++j) { if (pointList[pathgrid.mEdges[i].mV0].mOtherIndex[j] == pathgrid.mEdges[i].mV1) { std::ostringstream ss; ss << "Duplicate edge between points #" << pathgrid.mEdges[i].mV0 << " and #" << pathgrid.mEdges[i].mV1; messages.add (id, ss.str(), "", CSMDoc::Message::Severity_Error); break; } } // only add if not a duplicate if (j == pointList[pathgrid.mEdges[i].mV0].mOtherIndex.size()) pointList[pathgrid.mEdges[i].mV0].mOtherIndex.push_back(pathgrid.mEdges[i].mV1); } else { std::ostringstream ss; ss << "An edge is connected to a non-existent point #" << pathgrid.mEdges[i].mV0; messages.add (id, ss.str(), "", CSMDoc::Message::Severity_Error); } } for (unsigned int i = 0; i < pathgrid.mPoints.size(); ++i) { // check that edges are bidirectional bool foundReverse = false; for (unsigned int j = 0; j < pointList[i].mOtherIndex.size(); ++j) { for (unsigned int k = 0; k < pointList[pointList[i].mOtherIndex[j]].mOtherIndex.size(); ++k) { if (pointList[pointList[i].mOtherIndex[j]].mOtherIndex[k] == static_cast(i)) { foundReverse = true; break; } } if (!foundReverse) { std::ostringstream ss; ss << "Missing edge between points #" << i << " and #" << pointList[i].mOtherIndex[j]; messages.add (id, ss.str(), "", CSMDoc::Message::Severity_Error); } } // check duplicate points // FIXME: how to do this efficiently? for (unsigned int j = 0; j != i; ++j) { if (pathgrid.mPoints[i].mX == pathgrid.mPoints[j].mX && pathgrid.mPoints[i].mY == pathgrid.mPoints[j].mY && pathgrid.mPoints[i].mZ == pathgrid.mPoints[j].mZ) { std::vector::const_iterator it = find(duplList.begin(), duplList.end(), static_cast(i)); if (it == duplList.end()) { std::ostringstream ss; ss << "Point #" << i << " duplicates point #" << j << " (" << pathgrid.mPoints[i].mX << ", " << pathgrid.mPoints[i].mY << ", " << pathgrid.mPoints[i].mZ << ")"; messages.add (id, ss.str(), "", CSMDoc::Message::Severity_Warning); duplList.push_back(i); break; } } } } // check pathgrid points that are not connected to anything for (unsigned int i = 0; i < pointList.size(); ++i) { if (pointList[i].mConnectionNum == 0) { std::ostringstream ss; ss << "Point #" << i << " (" << pathgrid.mPoints[i].mX << ", " << pathgrid.mPoints[i].mY << ", " << pathgrid.mPoints[i].mZ << ") is disconnected from other points"; messages.add (id, ss.str(), "", CSMDoc::Message::Severity_Warning); } } // TODO: check whether there are disconnected graphs } ================================================ FILE: apps/opencs/model/tools/pathgridcheck.hpp ================================================ #ifndef CSM_TOOLS_PATHGRIDCHECK_H #define CSM_TOOLS_PATHGRIDCHECK_H #include "../world/collection.hpp" #include "../doc/stage.hpp" namespace CSMWorld { struct Pathgrid; template class SubCellCollection; } namespace CSMTools { struct Point { unsigned char mConnectionNum; std::vector mOtherIndex; Point() : mConnectionNum(0), mOtherIndex(0) {} }; class PathgridCheckStage : public CSMDoc::Stage { const CSMWorld::SubCellCollection >& mPathgrids; bool mIgnoreBaseRecords; public: PathgridCheckStage (const CSMWorld::SubCellCollection >& pathgrids); int setup() override; void perform (int stage, CSMDoc::Messages& messages) override; }; } #endif // CSM_TOOLS_PATHGRIDCHECK_H ================================================ FILE: apps/opencs/model/tools/racecheck.cpp ================================================ #include "racecheck.hpp" #include "../prefs/state.hpp" #include "../world/universalid.hpp" void CSMTools::RaceCheckStage::performPerRecord (int stage, CSMDoc::Messages& messages) { const CSMWorld::Record& record = mRaces.getRecord (stage); if (record.isDeleted()) return; const ESM::Race& race = record.get(); // Consider mPlayable flag even when "Base" records are ignored if (race.mData.mFlags & 0x1) mPlayable = true; // Skip "Base" records (setting!) if (mIgnoreBaseRecords && record.mState == CSMWorld::RecordBase::State_BaseOnly) return; CSMWorld::UniversalId id (CSMWorld::UniversalId::Type_Race, race.mId); // test for empty name and description if (race.mName.empty()) messages.add(id, "Name is missing", "", (race.mData.mFlags & 0x1) ? CSMDoc::Message::Severity_Error : CSMDoc::Message::Severity_Warning); if (race.mDescription.empty()) messages.add(id, "Description is missing", "", CSMDoc::Message::Severity_Warning); // test for positive height if (race.mData.mHeight.mMale<=0) messages.add(id, "Male height is non-positive", "", CSMDoc::Message::Severity_Error); if (race.mData.mHeight.mFemale<=0) messages.add(id, "Female height is non-positive", "", CSMDoc::Message::Severity_Error); // test for non-negative weight if (race.mData.mWeight.mMale<0) messages.add(id, "Male weight is negative", "", CSMDoc::Message::Severity_Error); if (race.mData.mWeight.mFemale<0) messages.add(id, "Female weight is negative", "", CSMDoc::Message::Severity_Error); /// \todo check data members that can't be edited in the table view } void CSMTools::RaceCheckStage::performFinal (CSMDoc::Messages& messages) { CSMWorld::UniversalId id (CSMWorld::UniversalId::Type_Races); if (!mPlayable) messages.add(id, "No playable race", "", CSMDoc::Message::Severity_SeriousError); } CSMTools::RaceCheckStage::RaceCheckStage (const CSMWorld::IdCollection& races) : mRaces (races), mPlayable (false) { mIgnoreBaseRecords = false; } int CSMTools::RaceCheckStage::setup() { mPlayable = false; mIgnoreBaseRecords = CSMPrefs::get()["Reports"]["ignore-base-records"].isTrue(); return mRaces.getSize()+1; } void CSMTools::RaceCheckStage::perform (int stage, CSMDoc::Messages& messages) { if (stage==mRaces.getSize()) performFinal (messages); else performPerRecord (stage, messages); } ================================================ FILE: apps/opencs/model/tools/racecheck.hpp ================================================ #ifndef CSM_TOOLS_RACECHECK_H #define CSM_TOOLS_RACECHECK_H #include #include "../world/idcollection.hpp" #include "../doc/stage.hpp" namespace CSMTools { /// \brief VerifyStage: make sure that race records are internally consistent class RaceCheckStage : public CSMDoc::Stage { const CSMWorld::IdCollection& mRaces; bool mPlayable; bool mIgnoreBaseRecords; void performPerRecord (int stage, CSMDoc::Messages& messages); void performFinal (CSMDoc::Messages& messages); public: RaceCheckStage (const CSMWorld::IdCollection& races); int setup() override; ///< \return number of steps void perform (int stage, CSMDoc::Messages& messages) override; ///< Messages resulting from this tage will be appended to \a messages. }; } #endif ================================================ FILE: apps/opencs/model/tools/referenceablecheck.cpp ================================================ #include "referenceablecheck.hpp" #include #include #include "../prefs/state.hpp" #include "../world/record.hpp" #include "../world/universalid.hpp" CSMTools::ReferenceableCheckStage::ReferenceableCheckStage( const CSMWorld::RefIdData& referenceable, const CSMWorld::IdCollection& races, const CSMWorld::IdCollection& classes, const CSMWorld::IdCollection& faction, const CSMWorld::IdCollection& scripts, const CSMWorld::Resources& models, const CSMWorld::Resources& icons, const CSMWorld::IdCollection& bodyparts) :mReferencables(referenceable), mRaces(races), mClasses(classes), mFactions(faction), mScripts(scripts), mModels(models), mIcons(icons), mBodyParts(bodyparts), mPlayerPresent(false) { mIgnoreBaseRecords = false; } void CSMTools::ReferenceableCheckStage::perform (int stage, CSMDoc::Messages& messages) { //Checks for books, than, when stage is above mBooksSize goes to other checks, with (stage - PrevSum) as stage. const int bookSize(mReferencables.getBooks().getSize()); if (stage < bookSize) { bookCheck(stage, mReferencables.getBooks(), messages); return; } stage -= bookSize; const int activatorSize(mReferencables.getActivators().getSize()); if (stage < activatorSize) { activatorCheck(stage, mReferencables.getActivators(), messages); return; } stage -= activatorSize; const int potionSize(mReferencables.getPotions().getSize()); if (stage < potionSize) { potionCheck(stage, mReferencables.getPotions(), messages); return; } stage -= potionSize; const int apparatusSize(mReferencables.getApparati().getSize()); if (stage < apparatusSize) { apparatusCheck(stage, mReferencables.getApparati(), messages); return; } stage -= apparatusSize; const int armorSize(mReferencables.getArmors().getSize()); if (stage < armorSize) { armorCheck(stage, mReferencables.getArmors(), messages); return; } stage -= armorSize; const int clothingSize(mReferencables.getClothing().getSize()); if (stage < clothingSize) { clothingCheck(stage, mReferencables.getClothing(), messages); return; } stage -= clothingSize; const int containerSize(mReferencables.getContainers().getSize()); if (stage < containerSize) { containerCheck(stage, mReferencables.getContainers(), messages); return; } stage -= containerSize; const int doorSize(mReferencables.getDoors().getSize()); if (stage < doorSize) { doorCheck(stage, mReferencables.getDoors(), messages); return; } stage -= doorSize; const int ingredientSize(mReferencables.getIngredients().getSize()); if (stage < ingredientSize) { ingredientCheck(stage, mReferencables.getIngredients(), messages); return; } stage -= ingredientSize; const int creatureLevListSize(mReferencables.getCreatureLevelledLists().getSize()); if (stage < creatureLevListSize) { creaturesLevListCheck(stage, mReferencables.getCreatureLevelledLists(), messages); return; } stage -= creatureLevListSize; const int itemLevelledListSize(mReferencables.getItemLevelledList().getSize()); if (stage < itemLevelledListSize) { itemLevelledListCheck(stage, mReferencables.getItemLevelledList(), messages); return; } stage -= itemLevelledListSize; const int lightSize(mReferencables.getLights().getSize()); if (stage < lightSize) { lightCheck(stage, mReferencables.getLights(), messages); return; } stage -= lightSize; const int lockpickSize(mReferencables.getLocpicks().getSize()); if (stage < lockpickSize) { lockpickCheck(stage, mReferencables.getLocpicks(), messages); return; } stage -= lockpickSize; const int miscSize(mReferencables.getMiscellaneous().getSize()); if (stage < miscSize) { miscCheck(stage, mReferencables.getMiscellaneous(), messages); return; } stage -= miscSize; const int npcSize(mReferencables.getNPCs().getSize()); if (stage < npcSize) { npcCheck(stage, mReferencables.getNPCs(), messages); return; } stage -= npcSize; const int weaponSize(mReferencables.getWeapons().getSize()); if (stage < weaponSize) { weaponCheck(stage, mReferencables.getWeapons(), messages); return; } stage -= weaponSize; const int probeSize(mReferencables.getProbes().getSize()); if (stage < probeSize) { probeCheck(stage, mReferencables.getProbes(), messages); return; } stage -= probeSize; const int repairSize(mReferencables.getRepairs().getSize()); if (stage < repairSize) { repairCheck(stage, mReferencables.getRepairs(), messages); return; } stage -= repairSize; const int staticSize(mReferencables.getStatics().getSize()); if (stage < staticSize) { staticCheck(stage, mReferencables.getStatics(), messages); return; } stage -= staticSize; const int creatureSize(mReferencables.getCreatures().getSize()); if (stage < creatureSize) { creatureCheck(stage, mReferencables.getCreatures(), messages); return; } // if we come that far, we are about to perform our last, final check. finalCheck(messages); return; } int CSMTools::ReferenceableCheckStage::setup() { mPlayerPresent = false; mIgnoreBaseRecords = CSMPrefs::get()["Reports"]["ignore-base-records"].isTrue(); return mReferencables.getSize() + 1; } void CSMTools::ReferenceableCheckStage::bookCheck( int stage, const CSMWorld::RefIdDataContainer< ESM::Book >& records, CSMDoc::Messages& messages) { const CSMWorld::RecordBase& baseRecord = records.getRecord(stage); // Skip "Base" records (setting!) and "Deleted" records if ((mIgnoreBaseRecords && baseRecord.mState == CSMWorld::RecordBase::State_BaseOnly) || baseRecord.isDeleted()) return; const ESM::Book& book = (dynamic_cast& >(baseRecord)).get(); CSMWorld::UniversalId id(CSMWorld::UniversalId::Type_Book, book.mId); inventoryItemCheck(book, messages, id.toString(), true); // Check that mentioned scripts exist scriptCheck(book, messages, id.toString()); } void CSMTools::ReferenceableCheckStage::activatorCheck( int stage, const CSMWorld::RefIdDataContainer< ESM::Activator >& records, CSMDoc::Messages& messages) { const CSMWorld::RecordBase& baseRecord = records.getRecord(stage); // Skip "Base" records (setting!) and "Deleted" records if ((mIgnoreBaseRecords && baseRecord.mState == CSMWorld::RecordBase::State_BaseOnly) || baseRecord.isDeleted()) return; const ESM::Activator& activator = (dynamic_cast& >(baseRecord)).get(); CSMWorld::UniversalId id(CSMWorld::UniversalId::Type_Activator, activator.mId); if (activator.mModel.empty()) messages.add(id, "Model is missing", "", CSMDoc::Message::Severity_Error); else if (mModels.searchId(activator.mModel) == -1) messages.add(id, "Model '" + activator.mModel + "' does not exist", "", CSMDoc::Message::Severity_Error); // Check that mentioned scripts exist scriptCheck(activator, messages, id.toString()); } void CSMTools::ReferenceableCheckStage::potionCheck( int stage, const CSMWorld::RefIdDataContainer< ESM::Potion >& records, CSMDoc::Messages& messages) { const CSMWorld::RecordBase& baseRecord = records.getRecord(stage); // Skip "Base" records (setting!) and "Deleted" records if ((mIgnoreBaseRecords && baseRecord.mState == CSMWorld::RecordBase::State_BaseOnly) || baseRecord.isDeleted()) return; const ESM::Potion& potion = (dynamic_cast& >(baseRecord)).get(); CSMWorld::UniversalId id(CSMWorld::UniversalId::Type_Potion, potion.mId); inventoryItemCheck(potion, messages, id.toString()); /// \todo Check magic effects for validity // Check that mentioned scripts exist scriptCheck(potion, messages, id.toString()); } void CSMTools::ReferenceableCheckStage::apparatusCheck( int stage, const CSMWorld::RefIdDataContainer< ESM::Apparatus >& records, CSMDoc::Messages& messages) { const CSMWorld::RecordBase& baseRecord = records.getRecord(stage); // Skip "Base" records (setting!) and "Deleted" records if ((mIgnoreBaseRecords && baseRecord.mState == CSMWorld::RecordBase::State_BaseOnly) || baseRecord.isDeleted()) return; const ESM::Apparatus& apparatus = (dynamic_cast& >(baseRecord)).get(); CSMWorld::UniversalId id(CSMWorld::UniversalId::Type_Apparatus, apparatus.mId); inventoryItemCheck(apparatus, messages, id.toString()); toolCheck(apparatus, messages, id.toString()); // Check that mentioned scripts exist scriptCheck(apparatus, messages, id.toString()); } void CSMTools::ReferenceableCheckStage::armorCheck( int stage, const CSMWorld::RefIdDataContainer< ESM::Armor >& records, CSMDoc::Messages& messages) { const CSMWorld::RecordBase& baseRecord = records.getRecord(stage); // Skip "Base" records (setting!) and "Deleted" records if ((mIgnoreBaseRecords && baseRecord.mState == CSMWorld::RecordBase::State_BaseOnly) || baseRecord.isDeleted()) return; const ESM::Armor& armor = (dynamic_cast& >(baseRecord)).get(); CSMWorld::UniversalId id(CSMWorld::UniversalId::Type_Armor, armor.mId); inventoryItemCheck(armor, messages, id.toString(), true); // Armor should have positive armor class, but 0 class is not an error if (armor.mData.mArmor < 0) messages.add(id, "Armor class is negative", "", CSMDoc::Message::Severity_Error); // Armor durability must be a positive number if (armor.mData.mHealth <= 0) messages.add(id, "Durability is non-positive", "", CSMDoc::Message::Severity_Error); // Check that mentioned scripts exist scriptCheck(armor, messages, id.toString()); } void CSMTools::ReferenceableCheckStage::clothingCheck( int stage, const CSMWorld::RefIdDataContainer< ESM::Clothing >& records, CSMDoc::Messages& messages) { const CSMWorld::RecordBase& baseRecord = records.getRecord(stage); // Skip "Base" records (setting!) and "Deleted" records if ((mIgnoreBaseRecords && baseRecord.mState == CSMWorld::RecordBase::State_BaseOnly) || baseRecord.isDeleted()) return; const ESM::Clothing& clothing = (dynamic_cast& >(baseRecord)).get(); CSMWorld::UniversalId id(CSMWorld::UniversalId::Type_Clothing, clothing.mId); inventoryItemCheck(clothing, messages, id.toString(), true); // Check that mentioned scripts exist scriptCheck(clothing, messages, id.toString()); } void CSMTools::ReferenceableCheckStage::containerCheck( int stage, const CSMWorld::RefIdDataContainer< ESM::Container >& records, CSMDoc::Messages& messages) { const CSMWorld::RecordBase& baseRecord = records.getRecord(stage); // Skip "Base" records (setting!) and "Deleted" records if ((mIgnoreBaseRecords && baseRecord.mState == CSMWorld::RecordBase::State_BaseOnly) || baseRecord.isDeleted()) return; const ESM::Container& container = (dynamic_cast& >(baseRecord)).get(); CSMWorld::UniversalId id(CSMWorld::UniversalId::Type_Container, container.mId); //checking for name if (container.mName.empty()) messages.add(id, "Name is missing", "", CSMDoc::Message::Severity_Error); //Checking for model if (container.mModel.empty()) messages.add(id, "Model is missing", "", CSMDoc::Message::Severity_Error); else if (mModels.searchId(container.mModel) == -1) messages.add(id, "Model '" + container.mModel + "' does not exist", "", CSMDoc::Message::Severity_Error); //Checking for capacity (weight) if (container.mWeight < 0) //0 is allowed messages.add(id, "Capacity is negative", "", CSMDoc::Message::Severity_Error); //checking contained items inventoryListCheck(container.mInventory.mList, messages, id.toString()); // Check that mentioned scripts exist scriptCheck(container, messages, id.toString()); } void CSMTools::ReferenceableCheckStage::creatureCheck ( int stage, const CSMWorld::RefIdDataContainer< ESM::Creature >& records, CSMDoc::Messages& messages) { const CSMWorld::RecordBase& baseRecord = records.getRecord(stage); // Skip "Base" records (setting!) and "Deleted" records if ((mIgnoreBaseRecords && baseRecord.mState == CSMWorld::RecordBase::State_BaseOnly) || baseRecord.isDeleted()) return; const ESM::Creature& creature = (dynamic_cast&>(baseRecord)).get(); CSMWorld::UniversalId id(CSMWorld::UniversalId::Type_Creature, creature.mId); if (creature.mName.empty()) messages.add(id, "Name is missing", "", CSMDoc::Message::Severity_Error); if (creature.mModel.empty()) messages.add(id, "Model is missing", "", CSMDoc::Message::Severity_Error); else if (mModels.searchId(creature.mModel) == -1) messages.add(id, "Model '" + creature.mModel + "' does not exist", "", CSMDoc::Message::Severity_Error); //stats checks if (creature.mData.mLevel <= 0) messages.add(id, "Level is non-positive", "", CSMDoc::Message::Severity_Warning); if (creature.mData.mStrength < 0) messages.add(id, "Strength is negative", "", CSMDoc::Message::Severity_Warning); if (creature.mData.mIntelligence < 0) messages.add(id, "Intelligence is negative", "", CSMDoc::Message::Severity_Warning); if (creature.mData.mWillpower < 0) messages.add(id, "Willpower is negative", "", CSMDoc::Message::Severity_Warning); if (creature.mData.mAgility < 0) messages.add(id, "Agility is negative", "", CSMDoc::Message::Severity_Warning); if (creature.mData.mSpeed < 0) messages.add(id, "Speed is negative", "", CSMDoc::Message::Severity_Warning); if (creature.mData.mEndurance < 0) messages.add(id, "Endurance is negative", "", CSMDoc::Message::Severity_Warning); if (creature.mData.mPersonality < 0) messages.add(id, "Personality is negative", "", CSMDoc::Message::Severity_Warning); if (creature.mData.mLuck < 0) messages.add(id, "Luck is negative", "", CSMDoc::Message::Severity_Warning); if (creature.mData.mCombat < 0) messages.add(id, "Combat is negative", "", CSMDoc::Message::Severity_Warning); if (creature.mData.mMagic < 0) messages.add(id, "Magic is negative", "", CSMDoc::Message::Severity_Warning); if (creature.mData.mStealth < 0) messages.add(id, "Stealth is negative", "", CSMDoc::Message::Severity_Warning); if (creature.mData.mHealth < 0) messages.add(id, "Health is negative", "", CSMDoc::Message::Severity_Error); if (creature.mData.mMana < 0) messages.add(id, "Magicka is negative", "", CSMDoc::Message::Severity_Error); if (creature.mData.mFatigue < 0) messages.add(id, "Fatigue is negative", "", CSMDoc::Message::Severity_Error); if (creature.mData.mSoul < 0) messages.add(id, "Soul value is negative", "", CSMDoc::Message::Severity_Error); if (creature.mAiData.mAlarm > 100) messages.add(id, "Alarm rating is over 100", "", CSMDoc::Message::Severity_Warning); if (creature.mAiData.mFight > 100) messages.add(id, "Fight rating is over 100", "", CSMDoc::Message::Severity_Warning); if (creature.mAiData.mFlee > 100) messages.add(id, "Flee rating is over 100", "", CSMDoc::Message::Severity_Warning); for (int i = 0; i < 6; ++i) { if (creature.mData.mAttack[i] < 0) messages.add(id, "Attack " + std::to_string(i/2 + 1) + " has negative" + (i % 2 == 0 ? " minimum " : " maximum ") + "damage", "", CSMDoc::Message::Severity_Error); if (i % 2 == 0 && creature.mData.mAttack[i] > creature.mData.mAttack[i+1]) messages.add(id, "Attack " + std::to_string(i/2 + 1) + " has minimum damage higher than maximum damage", "", CSMDoc::Message::Severity_Error); } if (creature.mData.mGold < 0) messages.add(id, "Gold count is negative", "", CSMDoc::Message::Severity_Error); if (creature.mScale == 0) messages.add(id, "Scale is equal to zero", "", CSMDoc::Message::Severity_Error); if (!creature.mOriginal.empty()) { CSMWorld::RefIdData::LocalIndex index = mReferencables.searchId(creature.mOriginal); if (index.first == -1) messages.add(id, "Parent creature '" + creature.mOriginal + "' does not exist", "", CSMDoc::Message::Severity_Error); else if (index.second != CSMWorld::UniversalId::Type_Creature) messages.add(id, "'" + creature.mOriginal + "' is not a creature", "", CSMDoc::Message::Severity_Error); } // Check inventory inventoryListCheck(creature.mInventory.mList, messages, id.toString()); // Check that mentioned scripts exist scriptCheck(creature, messages, id.toString()); /// \todo Check spells, teleport table, AI data and AI packages for validity } void CSMTools::ReferenceableCheckStage::doorCheck( int stage, const CSMWorld::RefIdDataContainer< ESM::Door >& records, CSMDoc::Messages& messages) { const CSMWorld::RecordBase& baseRecord = records.getRecord(stage); // Skip "Base" records (setting!) and "Deleted" records if ((mIgnoreBaseRecords && baseRecord.mState == CSMWorld::RecordBase::State_BaseOnly) || baseRecord.isDeleted()) return; const ESM::Door& door = (dynamic_cast&>(baseRecord)).get(); CSMWorld::UniversalId id(CSMWorld::UniversalId::Type_Door, door.mId); //usual, name or model if (door.mName.empty()) messages.add(id, "Name is missing", "", CSMDoc::Message::Severity_Error); if (door.mModel.empty()) messages.add(id, "Model is missing", "", CSMDoc::Message::Severity_Error); else if (mModels.searchId(door.mModel) == -1) messages.add(id, "Model '" + door.mModel + "' does not exist", "", CSMDoc::Message::Severity_Error); // Check that mentioned scripts exist scriptCheck(door, messages, id.toString()); } void CSMTools::ReferenceableCheckStage::ingredientCheck( int stage, const CSMWorld::RefIdDataContainer< ESM::Ingredient >& records, CSMDoc::Messages& messages) { const CSMWorld::RecordBase& baseRecord = records.getRecord(stage); // Skip "Base" records (setting!) and "Deleted" records if ((mIgnoreBaseRecords && baseRecord.mState == CSMWorld::RecordBase::State_BaseOnly) || baseRecord.isDeleted()) return; const ESM::Ingredient& ingredient = (dynamic_cast& >(baseRecord)).get(); CSMWorld::UniversalId id(CSMWorld::UniversalId::Type_Ingredient, ingredient.mId); inventoryItemCheck(ingredient, messages, id.toString()); // Check that mentioned scripts exist scriptCheck(ingredient, messages, id.toString()); } void CSMTools::ReferenceableCheckStage::creaturesLevListCheck( int stage, const CSMWorld::RefIdDataContainer< ESM::CreatureLevList >& records, CSMDoc::Messages& messages) { const CSMWorld::RecordBase& baseRecord = records.getRecord(stage); // Skip "Base" records (setting!) and "Deleted" records if ((mIgnoreBaseRecords && baseRecord.mState == CSMWorld::RecordBase::State_BaseOnly) || baseRecord.isDeleted()) return; const ESM::CreatureLevList& CreatureLevList = (dynamic_cast& >(baseRecord)).get(); CSMWorld::UniversalId id(CSMWorld::UniversalId::Type_CreatureLevelledList, CreatureLevList.mId); //CreatureLevList but Type_CreatureLevelledList :/ listCheck(CreatureLevList, messages, id.toString()); } void CSMTools::ReferenceableCheckStage::itemLevelledListCheck( int stage, const CSMWorld::RefIdDataContainer< ESM::ItemLevList >& records, CSMDoc::Messages& messages) { const CSMWorld::RecordBase& baseRecord = records.getRecord(stage); // Skip "Base" records (setting!) and "Deleted" records if ((mIgnoreBaseRecords && baseRecord.mState == CSMWorld::RecordBase::State_BaseOnly) || baseRecord.isDeleted()) return; const ESM::ItemLevList& ItemLevList = (dynamic_cast& >(baseRecord)).get(); CSMWorld::UniversalId id(CSMWorld::UniversalId::Type_ItemLevelledList, ItemLevList.mId); listCheck(ItemLevList, messages, id.toString()); } void CSMTools::ReferenceableCheckStage::lightCheck( int stage, const CSMWorld::RefIdDataContainer< ESM::Light >& records, CSMDoc::Messages& messages) { const CSMWorld::RecordBase& baseRecord = records.getRecord(stage); // Skip "Base" records (setting!) and "Deleted" records if ((mIgnoreBaseRecords && baseRecord.mState == CSMWorld::RecordBase::State_BaseOnly) || baseRecord.isDeleted()) return; const ESM::Light& light = (dynamic_cast& >(baseRecord)).get(); CSMWorld::UniversalId id(CSMWorld::UniversalId::Type_Light, light.mId); if (light.mData.mRadius < 0) messages.add(id, "Light radius is negative", "", CSMDoc::Message::Severity_Error); if (light.mData.mFlags & ESM::Light::Carry) inventoryItemCheck(light, messages, id.toString()); // Check that mentioned scripts exist scriptCheck(light, messages, id.toString()); } void CSMTools::ReferenceableCheckStage::lockpickCheck( int stage, const CSMWorld::RefIdDataContainer< ESM::Lockpick >& records, CSMDoc::Messages& messages) { const CSMWorld::RecordBase& baseRecord = records.getRecord(stage); // Skip "Base" records (setting!) and "Deleted" records if ((mIgnoreBaseRecords && baseRecord.mState == CSMWorld::RecordBase::State_BaseOnly) || baseRecord.isDeleted()) return; const ESM::Lockpick& lockpick = (dynamic_cast& >(baseRecord)).get(); CSMWorld::UniversalId id(CSMWorld::UniversalId::Type_Lockpick, lockpick.mId); inventoryItemCheck(lockpick, messages, id.toString()); toolCheck(lockpick, messages, id.toString(), true); // Check that mentioned scripts exist scriptCheck(lockpick, messages, id.toString()); } void CSMTools::ReferenceableCheckStage::miscCheck( int stage, const CSMWorld::RefIdDataContainer< ESM::Miscellaneous >& records, CSMDoc::Messages& messages) { const CSMWorld::RecordBase& baseRecord = records.getRecord(stage); // Skip "Base" records (setting!) and "Deleted" records if ((mIgnoreBaseRecords && baseRecord.mState == CSMWorld::RecordBase::State_BaseOnly) || baseRecord.isDeleted()) return; const ESM::Miscellaneous& miscellaneous = (dynamic_cast& >(baseRecord)).get(); CSMWorld::UniversalId id(CSMWorld::UniversalId::Type_Miscellaneous, miscellaneous.mId); inventoryItemCheck(miscellaneous, messages, id.toString()); // Check that mentioned scripts exist scriptCheck(miscellaneous, messages, id.toString()); } void CSMTools::ReferenceableCheckStage::npcCheck ( int stage, const CSMWorld::RefIdDataContainer< ESM::NPC >& records, CSMDoc::Messages& messages) { const CSMWorld::RecordBase& baseRecord = records.getRecord(stage); if (baseRecord.isDeleted()) return; const ESM::NPC& npc = (dynamic_cast& >(baseRecord)).get(); CSMWorld::UniversalId id (CSMWorld::UniversalId::Type_Npc, npc.mId); //Detect if player is present if (Misc::StringUtils::ciEqual(npc.mId, "player")) //Happy now, scrawl? mPlayerPresent = true; // Skip "Base" records (setting!) if (mIgnoreBaseRecords && baseRecord.mState == CSMWorld::RecordBase::State_BaseOnly) return; short level(npc.mNpdt.mLevel); int gold(npc.mNpdt.mGold); if (npc.mNpdtType == ESM::NPC::NPC_WITH_AUTOCALCULATED_STATS) //12 = autocalculated { if ((npc.mFlags & ESM::NPC::Autocalc) == 0) //0x0010 = autocalculated flag { messages.add(id, "NPC with autocalculated stats doesn't have autocalc flag turned on", "", CSMDoc::Message::Severity_Error); //should not happen? return; } } else { if (npc.mNpdt.mStrength == 0) messages.add(id, "Strength is equal to zero", "", CSMDoc::Message::Severity_Warning); if (npc.mNpdt.mIntelligence == 0) messages.add(id, "Intelligence is equal to zero", "", CSMDoc::Message::Severity_Warning); if (npc.mNpdt.mWillpower == 0) messages.add(id, "Willpower is equal to zero", "", CSMDoc::Message::Severity_Warning); if (npc.mNpdt.mAgility == 0) messages.add(id, "Agility is equal to zero", "", CSMDoc::Message::Severity_Warning); if (npc.mNpdt.mSpeed == 0) messages.add(id, "Speed is equal to zero", "", CSMDoc::Message::Severity_Warning); if (npc.mNpdt.mEndurance == 0) messages.add(id, "Endurance is equal to zero", "", CSMDoc::Message::Severity_Warning); if (npc.mNpdt.mPersonality == 0) messages.add(id, "Personality is equal to zero", "", CSMDoc::Message::Severity_Warning); if (npc.mNpdt.mLuck == 0) messages.add(id, "Luck is equal to zero", "", CSMDoc::Message::Severity_Warning); } if (level <= 0) messages.add(id, "Level is non-positive", "", CSMDoc::Message::Severity_Warning); if (npc.mAiData.mAlarm > 100) messages.add(id, "Alarm rating is over 100", "", CSMDoc::Message::Severity_Warning); if (npc.mAiData.mFight > 100) messages.add(id, "Fight rating is over 100", "", CSMDoc::Message::Severity_Warning); if (npc.mAiData.mFlee > 100) messages.add(id, "Flee rating is over 100", "", CSMDoc::Message::Severity_Warning); if (gold < 0) messages.add(id, "Gold count is negative", "", CSMDoc::Message::Severity_Error); if (npc.mName.empty()) messages.add(id, "Name is missing", "", CSMDoc::Message::Severity_Error); if (npc.mClass.empty()) messages.add(id, "Class is missing", "", CSMDoc::Message::Severity_Error); else if (mClasses.searchId (npc.mClass) == -1) messages.add(id, "Class '" + npc.mClass + "' does not exist", "", CSMDoc::Message::Severity_Error); if (npc.mRace.empty()) messages.add(id, "Race is missing", "", CSMDoc::Message::Severity_Error); else if (mRaces.searchId (npc.mRace) == -1) messages.add(id, "Race '" + npc.mRace + "' does not exist", "", CSMDoc::Message::Severity_Error); if (!npc.mFaction.empty() && mFactions.searchId(npc.mFaction) == -1) messages.add(id, "Faction '" + npc.mFaction + "' does not exist", "", CSMDoc::Message::Severity_Error); if (npc.mHead.empty()) messages.add(id, "Head is missing", "", CSMDoc::Message::Severity_Error); else { if (mBodyParts.searchId(npc.mHead) == -1) messages.add(id, "Head body part '" + npc.mHead + "' does not exist", "", CSMDoc::Message::Severity_Error); /// \todo Check gender, race and other body parts stuff validity for the specific NPC } if (npc.mHair.empty()) messages.add(id, "Hair is missing", "", CSMDoc::Message::Severity_Error); else { if (mBodyParts.searchId(npc.mHair) == -1) messages.add(id, "Hair body part '" + npc.mHair + "' does not exist", "", CSMDoc::Message::Severity_Error); /// \todo Check gender, race and other body part stuff validity for the specific NPC } // Check inventory inventoryListCheck(npc.mInventory.mList, messages, id.toString()); // Check that mentioned scripts exist scriptCheck(npc, messages, id.toString()); } void CSMTools::ReferenceableCheckStage::weaponCheck( int stage, const CSMWorld::RefIdDataContainer< ESM::Weapon >& records, CSMDoc::Messages& messages) { const CSMWorld::RecordBase& baseRecord = records.getRecord (stage); // Skip "Base" records (setting!) and "Deleted" records if ((mIgnoreBaseRecords && baseRecord.mState == CSMWorld::RecordBase::State_BaseOnly) || baseRecord.isDeleted()) return; const ESM::Weapon& weapon = (dynamic_cast& >(baseRecord)).get(); CSMWorld::UniversalId id (CSMWorld::UniversalId::Type_Weapon, weapon.mId); //TODO, It seems that this stuff for spellcasting is obligatory and In fact We should check if records are present if ( //THOSE ARE HARDCODED! !(weapon.mId == "VFX_Hands" || weapon.mId == "VFX_Absorb" || weapon.mId == "VFX_Reflect" || weapon.mId == "VFX_DefaultBolt" || //TODO I don't know how to get full list of effects :/ //DANGER!, ACHTUNG! FIXME! The following is the list of the magical bolts, valid for Morrowind.esm. However those are not hardcoded. weapon.mId == "magic_bolt" || weapon.mId == "shock_bolt" || weapon.mId == "shield_bolt" || weapon.mId == "VFX_DestructBolt" || weapon.mId == "VFX_PoisonBolt" || weapon.mId == "VFX_RestoreBolt" || weapon.mId == "VFX_AlterationBolt" || weapon.mId == "VFX_ConjureBolt" || weapon.mId == "VFX_FrostBolt" || weapon.mId == "VFX_MysticismBolt" || weapon.mId == "VFX_IllusionBolt" || weapon.mId == "VFX_Multiple2" || weapon.mId == "VFX_Multiple3" || weapon.mId == "VFX_Multiple4" || weapon.mId == "VFX_Multiple5" || weapon.mId == "VFX_Multiple6" || weapon.mId == "VFX_Multiple7" || weapon.mId == "VFX_Multiple8" || weapon.mId == "VFX_Multiple9")) { inventoryItemCheck(weapon, messages, id.toString(), true); if (!(weapon.mData.mType == ESM::Weapon::MarksmanBow || weapon.mData.mType == ESM::Weapon::MarksmanCrossbow || weapon.mData.mType == ESM::Weapon::MarksmanThrown || weapon.mData.mType == ESM::Weapon::Arrow || weapon.mData.mType == ESM::Weapon::Bolt)) { if (weapon.mData.mSlash[0] > weapon.mData.mSlash[1]) messages.add(id, "Minimum slash damage higher than maximum", "", CSMDoc::Message::Severity_Warning); if (weapon.mData.mThrust[0] > weapon.mData.mThrust[1]) messages.add(id, "Minimum thrust damage higher than maximum", "", CSMDoc::Message::Severity_Warning); } if (weapon.mData.mChop[0] > weapon.mData.mChop[1]) messages.add(id, "Minimum chop damage higher than maximum", "", CSMDoc::Message::Severity_Warning); if (!(weapon.mData.mType == ESM::Weapon::Arrow || weapon.mData.mType == ESM::Weapon::Bolt || weapon.mData.mType == ESM::Weapon::MarksmanThrown)) { //checking of health if (weapon.mData.mHealth == 0) messages.add(id, "Durability is equal to zero", "", CSMDoc::Message::Severity_Warning); if (weapon.mData.mReach < 0) messages.add(id, "Reach is negative", "", CSMDoc::Message::Severity_Error); } } // Check that mentioned scripts exist scriptCheck(weapon, messages, id.toString()); } void CSMTools::ReferenceableCheckStage::probeCheck( int stage, const CSMWorld::RefIdDataContainer< ESM::Probe >& records, CSMDoc::Messages& messages) { const CSMWorld::RecordBase& baseRecord = records.getRecord(stage); // Skip "Base" records (setting!) and "Deleted" records if ((mIgnoreBaseRecords && baseRecord.mState == CSMWorld::RecordBase::State_BaseOnly) || baseRecord.isDeleted()) return; const ESM::Probe& probe = (dynamic_cast& >(baseRecord)).get(); CSMWorld::UniversalId id(CSMWorld::UniversalId::Type_Probe, probe.mId); inventoryItemCheck(probe, messages, id.toString()); toolCheck(probe, messages, id.toString(), true); // Check that mentioned scripts exist scriptCheck(probe, messages, id.toString()); } void CSMTools::ReferenceableCheckStage::repairCheck ( int stage, const CSMWorld::RefIdDataContainer< ESM::Repair >& records, CSMDoc::Messages& messages) { const CSMWorld::RecordBase& baseRecord = records.getRecord (stage); // Skip "Base" records (setting!) and "Deleted" records if ((mIgnoreBaseRecords && baseRecord.mState == CSMWorld::RecordBase::State_BaseOnly) || baseRecord.isDeleted()) return; const ESM::Repair& repair = (dynamic_cast& >(baseRecord)).get(); CSMWorld::UniversalId id (CSMWorld::UniversalId::Type_Repair, repair.mId); inventoryItemCheck (repair, messages, id.toString()); toolCheck (repair, messages, id.toString(), true); // Check that mentioned scripts exist scriptCheck(repair, messages, id.toString()); } void CSMTools::ReferenceableCheckStage::staticCheck ( int stage, const CSMWorld::RefIdDataContainer< ESM::Static >& records, CSMDoc::Messages& messages) { const CSMWorld::RecordBase& baseRecord = records.getRecord (stage); // Skip "Base" records (setting!) and "Deleted" records if ((mIgnoreBaseRecords && baseRecord.mState == CSMWorld::RecordBase::State_BaseOnly) || baseRecord.isDeleted()) return; const ESM::Static& staticElement = (dynamic_cast& >(baseRecord)).get(); CSMWorld::UniversalId id (CSMWorld::UniversalId::Type_Static, staticElement.mId); if (staticElement.mModel.empty()) messages.add(id, "Model is missing", "", CSMDoc::Message::Severity_Error); else if (mModels.searchId(staticElement.mModel) == -1) messages.add(id, "Model '" + staticElement.mModel + "' does not exist", "", CSMDoc::Message::Severity_Error); } //final check void CSMTools::ReferenceableCheckStage::finalCheck (CSMDoc::Messages& messages) { if (!mPlayerPresent) messages.add(CSMWorld::UniversalId::Type_Referenceables, "Player record is missing", "", CSMDoc::Message::Severity_SeriousError); } void CSMTools::ReferenceableCheckStage::inventoryListCheck( const std::vector& itemList, CSMDoc::Messages& messages, const std::string& id) { for (size_t i = 0; i < itemList.size(); ++i) { std::string itemName = itemList[i].mItem; CSMWorld::RefIdData::LocalIndex localIndex = mReferencables.searchId(itemName); if (localIndex.first == -1) messages.add(id, "Item '" + itemName + "' does not exist", "", CSMDoc::Message::Severity_Error); else { // Needs to accommodate containers, creatures, and NPCs switch (localIndex.second) { case CSMWorld::UniversalId::Type_Potion: case CSMWorld::UniversalId::Type_Apparatus: case CSMWorld::UniversalId::Type_Armor: case CSMWorld::UniversalId::Type_Book: case CSMWorld::UniversalId::Type_Clothing: case CSMWorld::UniversalId::Type_Ingredient: case CSMWorld::UniversalId::Type_Light: case CSMWorld::UniversalId::Type_Lockpick: case CSMWorld::UniversalId::Type_Miscellaneous: case CSMWorld::UniversalId::Type_Probe: case CSMWorld::UniversalId::Type_Repair: case CSMWorld::UniversalId::Type_Weapon: case CSMWorld::UniversalId::Type_ItemLevelledList: break; default: messages.add(id, "'" + itemName + "' is not an item", "", CSMDoc::Message::Severity_Error); } } } } //Templates begins here template void CSMTools::ReferenceableCheckStage::inventoryItemCheck ( const Item& someItem, CSMDoc::Messages& messages, const std::string& someID, bool enchantable) { if (someItem.mName.empty()) messages.add(someID, "Name is missing", "", CSMDoc::Message::Severity_Error); //Checking for weight if (someItem.mData.mWeight < 0) messages.add(someID, "Weight is negative", "", CSMDoc::Message::Severity_Error); //Checking for value if (someItem.mData.mValue < 0) messages.add(someID, "Value is negative", "", CSMDoc::Message::Severity_Error); //checking for model if (someItem.mModel.empty()) messages.add(someID, "Model is missing", "", CSMDoc::Message::Severity_Error); else if (mModels.searchId(someItem.mModel) == -1) messages.add(someID, "Model '" + someItem.mModel + "' does not exist", "", CSMDoc::Message::Severity_Error); //checking for icon if (someItem.mIcon.empty()) messages.add(someID, "Icon is missing", "", CSMDoc::Message::Severity_Error); else if (mIcons.searchId(someItem.mIcon) == -1) { std::string ddsIcon = someItem.mIcon; if (!(Misc::ResourceHelpers::changeExtensionToDds(ddsIcon) && mIcons.searchId(ddsIcon) != -1)) messages.add(someID, "Icon '" + someItem.mIcon + "' does not exist", "", CSMDoc::Message::Severity_Error); } if (enchantable && someItem.mData.mEnchant < 0) messages.add(someID, "Enchantment points number is negative", "", CSMDoc::Message::Severity_Error); } template void CSMTools::ReferenceableCheckStage::inventoryItemCheck ( const Item& someItem, CSMDoc::Messages& messages, const std::string& someID) { if (someItem.mName.empty()) messages.add(someID, "Name is missing", "", CSMDoc::Message::Severity_Error); //Checking for weight if (someItem.mData.mWeight < 0) messages.add(someID, "Weight is negative", "", CSMDoc::Message::Severity_Error); //Checking for value if (someItem.mData.mValue < 0) messages.add(someID, "Value is negative", "", CSMDoc::Message::Severity_Error); //checking for model if (someItem.mModel.empty()) messages.add(someID, "Model is missing", "", CSMDoc::Message::Severity_Error); else if (mModels.searchId(someItem.mModel) == -1) messages.add(someID, "Model '" + someItem.mModel + "' does not exist", "", CSMDoc::Message::Severity_Error); //checking for icon if (someItem.mIcon.empty()) messages.add(someID, "Icon is missing", "", CSMDoc::Message::Severity_Error); else if (mIcons.searchId(someItem.mIcon) == -1) { std::string ddsIcon = someItem.mIcon; if (!(Misc::ResourceHelpers::changeExtensionToDds(ddsIcon) && mIcons.searchId(ddsIcon) != -1)) messages.add(someID, "Icon '" + someItem.mIcon + "' does not exist", "", CSMDoc::Message::Severity_Error); } } template void CSMTools::ReferenceableCheckStage::toolCheck ( const Tool& someTool, CSMDoc::Messages& messages, const std::string& someID, bool canBeBroken) { if (someTool.mData.mQuality <= 0) messages.add(someID, "Quality is non-positive", "", CSMDoc::Message::Severity_Error); if (canBeBroken && someTool.mData.mUses<=0) messages.add(someID, "Number of uses is non-positive", "", CSMDoc::Message::Severity_Error); } template void CSMTools::ReferenceableCheckStage::toolCheck ( const Tool& someTool, CSMDoc::Messages& messages, const std::string& someID) { if (someTool.mData.mQuality <= 0) messages.add(someID, "Quality is non-positive", "", CSMDoc::Message::Severity_Error); } template void CSMTools::ReferenceableCheckStage::listCheck ( const List& someList, CSMDoc::Messages& messages, const std::string& someID) { if (someList.mChanceNone > 100) { messages.add(someID, "Chance that no object is used is over 100 percent", "", CSMDoc::Message::Severity_Warning); } for (unsigned i = 0; i < someList.mList.size(); ++i) { if (mReferencables.searchId(someList.mList[i].mId).first == -1) messages.add(someID, "Object '" + someList.mList[i].mId + "' does not exist", "", CSMDoc::Message::Severity_Error); if (someList.mList[i].mLevel < 1) messages.add(someID, "Level of item '" + someList.mList[i].mId + "' is non-positive", "", CSMDoc::Message::Severity_Error); } } template void CSMTools::ReferenceableCheckStage::scriptCheck ( const Tool& someTool, CSMDoc::Messages& messages, const std::string& someID) { if (!someTool.mScript.empty()) { if (mScripts.searchId(someTool.mScript) == -1) messages.add(someID, "Script '" + someTool.mScript + "' does not exist", "", CSMDoc::Message::Severity_Error); } } ================================================ FILE: apps/opencs/model/tools/referenceablecheck.hpp ================================================ #ifndef REFERENCEABLECHECKSTAGE_H #define REFERENCEABLECHECKSTAGE_H #include "../world/universalid.hpp" #include "../doc/stage.hpp" #include "../world/data.hpp" #include "../world/refiddata.hpp" #include "../world/resources.hpp" namespace CSMTools { class ReferenceableCheckStage : public CSMDoc::Stage { public: ReferenceableCheckStage (const CSMWorld::RefIdData& referenceable, const CSMWorld::IdCollection& races, const CSMWorld::IdCollection& classes, const CSMWorld::IdCollection& factions, const CSMWorld::IdCollection& scripts, const CSMWorld::Resources& models, const CSMWorld::Resources& icons, const CSMWorld::IdCollection& bodyparts); void perform(int stage, CSMDoc::Messages& messages) override; int setup() override; private: //CONCRETE CHECKS void bookCheck(int stage, const CSMWorld::RefIdDataContainer< ESM::Book >& records, CSMDoc::Messages& messages); void activatorCheck(int stage, const CSMWorld::RefIdDataContainer< ESM::Activator >& records, CSMDoc::Messages& messages); void potionCheck(int stage, const CSMWorld::RefIdDataContainer& records, CSMDoc::Messages& messages); void apparatusCheck(int stage, const CSMWorld::RefIdDataContainer& records, CSMDoc::Messages& messages); void armorCheck(int stage, const CSMWorld::RefIdDataContainer& records, CSMDoc::Messages& messages); void clothingCheck(int stage, const CSMWorld::RefIdDataContainer& records, CSMDoc::Messages& messages); void containerCheck(int stage, const CSMWorld::RefIdDataContainer& records, CSMDoc::Messages& messages); void creatureCheck(int stage, const CSMWorld::RefIdDataContainer& records, CSMDoc::Messages& messages); void doorCheck(int stage, const CSMWorld::RefIdDataContainer& records, CSMDoc::Messages& messages); void ingredientCheck(int stage, const CSMWorld::RefIdDataContainer& records, CSMDoc::Messages& messages); void creaturesLevListCheck(int stage, const CSMWorld::RefIdDataContainer& records, CSMDoc::Messages& messages); void itemLevelledListCheck(int stage, const CSMWorld::RefIdDataContainer& records, CSMDoc::Messages& messages); void lightCheck(int stage, const CSMWorld::RefIdDataContainer& records, CSMDoc::Messages& messages); void lockpickCheck(int stage, const CSMWorld::RefIdDataContainer& records, CSMDoc::Messages& messages); void miscCheck(int stage, const CSMWorld::RefIdDataContainer& records, CSMDoc::Messages& messages); void npcCheck(int stage, const CSMWorld::RefIdDataContainer& records, CSMDoc::Messages& messages); void weaponCheck(int stage, const CSMWorld::RefIdDataContainer& records, CSMDoc::Messages& messages); void probeCheck(int stage, const CSMWorld::RefIdDataContainer& records, CSMDoc::Messages& messages); void repairCheck(int stage, const CSMWorld::RefIdDataContainer& records, CSMDoc::Messages& messages); void staticCheck(int stage, const CSMWorld::RefIdDataContainer& records, CSMDoc::Messages& messages); //FINAL CHECK void finalCheck (CSMDoc::Messages& messages); //Convenience functions void inventoryListCheck(const std::vector& itemList, CSMDoc::Messages& messages, const std::string& id); template void inventoryItemCheck(const ITEM& someItem, CSMDoc::Messages& messages, const std::string& someID, bool enchantable); //for all enchantable items. template void inventoryItemCheck(const ITEM& someItem, CSMDoc::Messages& messages, const std::string& someID); //for non-enchantable items. template void toolCheck(const TOOL& someTool, CSMDoc::Messages& messages, const std::string& someID, bool canbebroken); //for tools with uses. template void toolCheck(const TOOL& someTool, CSMDoc::Messages& messages, const std::string& someID); //for tools without uses. template void listCheck(const LIST& someList, CSMDoc::Messages& messages, const std::string& someID); template void scriptCheck(const TOOL& someTool, CSMDoc::Messages& messages, const std::string& someID); const CSMWorld::RefIdData& mReferencables; const CSMWorld::IdCollection& mRaces; const CSMWorld::IdCollection& mClasses; const CSMWorld::IdCollection& mFactions; const CSMWorld::IdCollection& mScripts; const CSMWorld::Resources& mModels; const CSMWorld::Resources& mIcons; const CSMWorld::IdCollection& mBodyParts; bool mPlayerPresent; bool mIgnoreBaseRecords; }; } #endif // REFERENCEABLECHECKSTAGE_H ================================================ FILE: apps/opencs/model/tools/referencecheck.cpp ================================================ #include "referencecheck.hpp" #include "../prefs/state.hpp" CSMTools::ReferenceCheckStage::ReferenceCheckStage( const CSMWorld::RefCollection& references, const CSMWorld::RefIdCollection& referencables, const CSMWorld::IdCollection& cells, const CSMWorld::IdCollection& factions) : mReferences(references), mObjects(referencables), mDataSet(referencables.getDataSet()), mCells(cells), mFactions(factions) { mIgnoreBaseRecords = false; } void CSMTools::ReferenceCheckStage::perform(int stage, CSMDoc::Messages &messages) { const CSMWorld::Record& record = mReferences.getRecord(stage); // Skip "Base" records (setting!) and "Deleted" records if ((mIgnoreBaseRecords && record.mState == CSMWorld::RecordBase::State_BaseOnly) || record.isDeleted()) return; const CSMWorld::CellRef& cellRef = record.get(); const CSMWorld::UniversalId id(CSMWorld::UniversalId::Type_Reference, cellRef.mId); // Check reference id if (cellRef.mRefID.empty()) messages.add(id, "Instance is not based on an object", "", CSMDoc::Message::Severity_Error); else { // Check for non existing referenced object if (mObjects.searchId(cellRef.mRefID) == -1) messages.add(id, "Instance of a non-existent object '" + cellRef.mRefID + "'", "", CSMDoc::Message::Severity_Error); else { // Check if reference charge is valid for it's proper referenced type CSMWorld::RefIdData::LocalIndex localIndex = mDataSet.searchId(cellRef.mRefID); bool isLight = localIndex.second == CSMWorld::UniversalId::Type_Light; if ((isLight && cellRef.mChargeFloat < -1) || (!isLight && cellRef.mChargeInt < -1)) messages.add(id, "Invalid charge", "", CSMDoc::Message::Severity_Error); } } // If object have owner, check if that owner reference is valid if (!cellRef.mOwner.empty() && mObjects.searchId(cellRef.mOwner) == -1) messages.add(id, "Owner object '" + cellRef.mOwner + "' does not exist", "", CSMDoc::Message::Severity_Error); // If object have creature soul trapped, check if that creature reference is valid if (!cellRef.mSoul.empty()) if (mObjects.searchId(cellRef.mSoul) == -1) messages.add(id, "Trapped soul object '" + cellRef.mSoul + "' does not exist", "", CSMDoc::Message::Severity_Error); if (cellRef.mFaction.empty()) { if (cellRef.mFactionRank != -2) messages.add(id, "Reference without a faction has a faction rank", "", CSMDoc::Message::Severity_Error); } else { if (mFactions.searchId(cellRef.mFaction) == -1) messages.add(id, "Faction '" + cellRef.mFaction + "' does not exist", "", CSMDoc::Message::Severity_Error); else if (cellRef.mFactionRank < -1) messages.add(id, "Invalid faction rank", "", CSMDoc::Message::Severity_Error); } if (!cellRef.mDestCell.empty() && mCells.searchId(cellRef.mDestCell) == -1) messages.add(id, "Destination cell '" + cellRef.mDestCell + "' does not exist", "", CSMDoc::Message::Severity_Error); if (cellRef.mScale < 0) messages.add(id, "Negative scale", "", CSMDoc::Message::Severity_Error); // Check if enchantement points aren't negative or are at full (-1) if (cellRef.mEnchantmentCharge < -1) messages.add(id, "Negative number of enchantment points", "", CSMDoc::Message::Severity_Error); // Check if gold value isn't negative if (cellRef.mGoldValue < 0) messages.add(id, "Negative gold value", "", CSMDoc::Message::Severity_Error); } int CSMTools::ReferenceCheckStage::setup() { mIgnoreBaseRecords = CSMPrefs::get()["Reports"]["ignore-base-records"].isTrue(); return mReferences.getSize(); } ================================================ FILE: apps/opencs/model/tools/referencecheck.hpp ================================================ #ifndef CSM_TOOLS_REFERENCECHECK_H #define CSM_TOOLS_REFERENCECHECK_H #include "../doc/state.hpp" #include "../doc/document.hpp" namespace CSMTools { class ReferenceCheckStage : public CSMDoc::Stage { public: ReferenceCheckStage (const CSMWorld::RefCollection& references, const CSMWorld::RefIdCollection& referencables, const CSMWorld::IdCollection& cells, const CSMWorld::IdCollection& factions); void perform(int stage, CSMDoc::Messages& messages) override; int setup() override; private: const CSMWorld::RefCollection& mReferences; const CSMWorld::RefIdCollection& mObjects; const CSMWorld::RefIdData& mDataSet; const CSMWorld::IdCollection& mCells; const CSMWorld::IdCollection& mFactions; bool mIgnoreBaseRecords; }; } #endif // CSM_TOOLS_REFERENCECHECK_H ================================================ FILE: apps/opencs/model/tools/regioncheck.cpp ================================================ #include "regioncheck.hpp" #include "../prefs/state.hpp" #include "../world/universalid.hpp" CSMTools::RegionCheckStage::RegionCheckStage (const CSMWorld::IdCollection& regions) : mRegions (regions) { mIgnoreBaseRecords = false; } int CSMTools::RegionCheckStage::setup() { mIgnoreBaseRecords = CSMPrefs::get()["Reports"]["ignore-base-records"].isTrue(); return mRegions.getSize(); } void CSMTools::RegionCheckStage::perform (int stage, CSMDoc::Messages& messages) { const CSMWorld::Record& record = mRegions.getRecord (stage); // Skip "Base" records (setting!) and "Deleted" records if ((mIgnoreBaseRecords && record.mState == CSMWorld::RecordBase::State_BaseOnly) || record.isDeleted()) return; const ESM::Region& region = record.get(); CSMWorld::UniversalId id (CSMWorld::UniversalId::Type_Region, region.mId); // test for empty name if (region.mName.empty()) messages.add(id, "Name is missing", "", CSMDoc::Message::Severity_Error); /// \todo test that the ID in mSleeplist exists // test that chances add up to 100 int chances = region.mData.mClear + region.mData.mCloudy + region.mData.mFoggy + region.mData.mOvercast + region.mData.mRain + region.mData.mThunder + region.mData.mAsh + region.mData.mBlight + region.mData.mA + region.mData.mB; if (chances != 100) messages.add(id, "Weather chances do not add up to 100", "", CSMDoc::Message::Severity_Error); for (const ESM::Region::SoundRef& sound : region.mSoundList) { if (sound.mChance > 100) messages.add(id, "Chance of '" + sound.mSound + "' sound to play is over 100 percent", "", CSMDoc::Message::Severity_Warning); } /// \todo check data members that can't be edited in the table view } ================================================ FILE: apps/opencs/model/tools/regioncheck.hpp ================================================ #ifndef CSM_TOOLS_REGIONCHECK_H #define CSM_TOOLS_REGIONCHECK_H #include #include "../world/idcollection.hpp" #include "../doc/stage.hpp" namespace CSMTools { /// \brief VerifyStage: make sure that region records are internally consistent class RegionCheckStage : public CSMDoc::Stage { const CSMWorld::IdCollection& mRegions; bool mIgnoreBaseRecords; public: RegionCheckStage (const CSMWorld::IdCollection& regions); int setup() override; ///< \return number of steps void perform (int stage, CSMDoc::Messages& messages) override; ///< Messages resulting from this tage will be appended to \a messages. }; } #endif ================================================ FILE: apps/opencs/model/tools/reportmodel.cpp ================================================ #include "reportmodel.hpp" #include #include #include "../world/columns.hpp" CSMTools::ReportModel::ReportModel (bool fieldColumn, bool severityColumn) : mColumnField (-1), mColumnSeverity (-1) { int index = 3; if (severityColumn) mColumnSeverity = index++; if (fieldColumn) mColumnField = index++; mColumnDescription = index; } int CSMTools::ReportModel::rowCount (const QModelIndex & parent) const { if (parent.isValid()) return 0; return static_cast(mRows.size()); } int CSMTools::ReportModel::columnCount (const QModelIndex & parent) const { if (parent.isValid()) return 0; return mColumnDescription+1; } QVariant CSMTools::ReportModel::data (const QModelIndex & index, int role) const { if (role!=Qt::DisplayRole && role!=Qt::UserRole) return QVariant(); switch (index.column()) { case Column_Type: if(role == Qt::UserRole) return QString::fromUtf8 ( mRows.at (index.row()).mId.getTypeName().c_str()); else return static_cast (mRows.at (index.row()).mId.getType()); case Column_Id: { CSMWorld::UniversalId id = mRows.at (index.row()).mId; if (id.getArgumentType()==CSMWorld::UniversalId::ArgumentType_Id) return QString::fromUtf8 (id.getId().c_str()); return QString ("-"); } case Column_Hint: return QString::fromUtf8 (mRows.at (index.row()).mHint.c_str()); } if (index.column()==mColumnDescription) return QString::fromUtf8 (mRows.at (index.row()).mMessage.c_str()); if (index.column()==mColumnField) { std::string field; std::istringstream stream (mRows.at (index.row()).mHint); char type, ignore; int fieldIndex; if ((stream >> type >> ignore >> fieldIndex) && (type=='r' || type=='R')) { field = CSMWorld::Columns::getName ( static_cast (fieldIndex)); } return QString::fromUtf8 (field.c_str()); } if (index.column()==mColumnSeverity) { return QString::fromUtf8 ( CSMDoc::Message::toString (mRows.at (index.row()).mSeverity).c_str()); } return QVariant(); } QVariant CSMTools::ReportModel::headerData (int section, Qt::Orientation orientation, int role) const { if (role!=Qt::DisplayRole) return QVariant(); if (orientation==Qt::Vertical) return QVariant(); switch (section) { case Column_Type: return "Type"; case Column_Id: return "ID"; } if (section==mColumnDescription) return "Description"; if (section==mColumnField) return "Field"; if (section==mColumnSeverity) return "Severity"; return "-"; } bool CSMTools::ReportModel::removeRows (int row, int count, const QModelIndex& parent) { if (parent.isValid()) return false; if (count>0) { beginRemoveRows (parent, row, row+count-1); mRows.erase (mRows.begin()+row, mRows.begin()+row+count); endRemoveRows(); } return true; } void CSMTools::ReportModel::add (const CSMDoc::Message& message) { beginInsertRows (QModelIndex(), static_cast(mRows.size()), static_cast(mRows.size())); mRows.push_back (message); endInsertRows(); } void CSMTools::ReportModel::flagAsReplaced (int index) { CSMDoc::Message& line = mRows.at (index); std::string hint = line.mHint; if (hint.empty() || hint[0]!='R') throw std::logic_error ("trying to flag message as replaced that is not replaceable"); hint[0] = 'r'; line.mHint = hint; emit dataChanged (this->index (index, 0), this->index (index, columnCount())); } const CSMWorld::UniversalId& CSMTools::ReportModel::getUniversalId (int row) const { return mRows.at (row).mId; } std::string CSMTools::ReportModel::getHint (int row) const { return mRows.at (row).mHint; } void CSMTools::ReportModel::clear() { if (!mRows.empty()) { beginRemoveRows (QModelIndex(), 0, static_cast(mRows.size())-1); mRows.clear(); endRemoveRows(); } } int CSMTools::ReportModel::countErrors() const { int count = 0; for (std::vector::const_iterator iter (mRows.begin()); iter!=mRows.end(); ++iter) if (iter->mSeverity==CSMDoc::Message::Severity_Error || iter->mSeverity==CSMDoc::Message::Severity_SeriousError) ++count; return count; } ================================================ FILE: apps/opencs/model/tools/reportmodel.hpp ================================================ #ifndef CSM_TOOLS_REPORTMODEL_H #define CSM_TOOLS_REPORTMODEL_H #include #include #include #include "../doc/messages.hpp" #include "../world/universalid.hpp" namespace CSMTools { class ReportModel : public QAbstractTableModel { Q_OBJECT std::vector mRows; // Fixed columns enum Columns { Column_Type = 0, Column_Id = 1, Column_Hint = 2 }; // Configurable columns int mColumnDescription; int mColumnField; int mColumnSeverity; public: ReportModel (bool fieldColumn = false, bool severityColumn = true); int rowCount (const QModelIndex & parent = QModelIndex()) const override; int columnCount (const QModelIndex & parent = QModelIndex()) const override; QVariant data (const QModelIndex & index, int role = Qt::DisplayRole) const override; QVariant headerData (int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override; bool removeRows (int row, int count, const QModelIndex& parent = QModelIndex()) override; void add (const CSMDoc::Message& message); void flagAsReplaced (int index); const CSMWorld::UniversalId& getUniversalId (int row) const; std::string getHint (int row) const; void clear(); // Return number of messages with Error or SeriousError severity. int countErrors() const; }; } #endif ================================================ FILE: apps/opencs/model/tools/scriptcheck.cpp ================================================ #include "scriptcheck.hpp" #include #include #include #include #include #include "../doc/document.hpp" #include "../world/data.hpp" #include "../prefs/state.hpp" CSMDoc::Message::Severity CSMTools::ScriptCheckStage::getSeverity (Type type) { switch (type) { case WarningMessage: return CSMDoc::Message::Severity_Warning; case ErrorMessage: return CSMDoc::Message::Severity_Error; } return CSMDoc::Message::Severity_SeriousError; } void CSMTools::ScriptCheckStage::report (const std::string& message, const Compiler::TokenLoc& loc, Type type) { std::ostringstream stream; CSMWorld::UniversalId id (CSMWorld::UniversalId::Type_Script, mId); stream << message << " (" << loc.mLiteral << ")" << " @ line " << loc.mLine+1 << ", column " << loc.mColumn; std::ostringstream hintStream; hintStream << "l:" << loc.mLine+1 << " " << loc.mColumn; mMessages->add (id, stream.str(), hintStream.str(), getSeverity (type)); } void CSMTools::ScriptCheckStage::report (const std::string& message, Type type) { CSMWorld::UniversalId id (CSMWorld::UniversalId::Type_Script, mId); std::ostringstream stream; stream << message; mMessages->add (id, stream.str(), "", getSeverity (type)); } CSMTools::ScriptCheckStage::ScriptCheckStage (const CSMDoc::Document& document) : mDocument (document), mContext (document.getData()), mMessages (nullptr), mWarningMode (Mode_Ignore) { /// \todo add an option to configure warning mode setWarningsMode (0); Compiler::registerExtensions (mExtensions); mContext.setExtensions (&mExtensions); mIgnoreBaseRecords = false; } int CSMTools::ScriptCheckStage::setup() { std::string warnings = CSMPrefs::get()["Scripts"]["warnings"].toString(); if (warnings=="Ignore") mWarningMode = Mode_Ignore; else if (warnings=="Normal") mWarningMode = Mode_Normal; else if (warnings=="Strict") mWarningMode = Mode_Strict; mContext.clear(); mMessages = nullptr; mId.clear(); Compiler::ErrorHandler::reset(); mIgnoreBaseRecords = CSMPrefs::get()["Reports"]["ignore-base-records"].isTrue(); return mDocument.getData().getScripts().getSize(); } void CSMTools::ScriptCheckStage::perform (int stage, CSMDoc::Messages& messages) { const CSMWorld::Record &record = mDocument.getData().getScripts().getRecord(stage); mId = mDocument.getData().getScripts().getId (stage); if (mDocument.isBlacklisted ( CSMWorld::UniversalId (CSMWorld::UniversalId::Type_Script, mId))) return; // Skip "Base" records (setting!) and "Deleted" records if ((mIgnoreBaseRecords && record.mState == CSMWorld::RecordBase::State_BaseOnly) || record.isDeleted()) return; mMessages = &messages; switch (mWarningMode) { case Mode_Ignore: setWarningsMode (0); break; case Mode_Normal: setWarningsMode (1); break; case Mode_Strict: setWarningsMode (2); break; } try { mFile = record.get().mId; std::istringstream input (record.get().mScriptText); Compiler::Scanner scanner (*this, input, mContext.getExtensions()); Compiler::FileParser parser (*this, mContext); scanner.scan (parser); } catch (const Compiler::SourceException&) { // error has already been reported via error handler } catch (const std::exception& error) { CSMWorld::UniversalId id (CSMWorld::UniversalId::Type_Script, mId); std::ostringstream stream; stream << error.what(); messages.add (id, stream.str(), "", CSMDoc::Message::Severity_SeriousError); } mMessages = nullptr; } ================================================ FILE: apps/opencs/model/tools/scriptcheck.hpp ================================================ #ifndef CSM_TOOLS_SCRIPTCHECK_H #define CSM_TOOLS_SCRIPTCHECK_H #include #include #include "../doc/stage.hpp" #include "../world/scriptcontext.hpp" namespace CSMDoc { class Document; } namespace CSMTools { /// \brief VerifyStage: make sure that scripts compile class ScriptCheckStage : public CSMDoc::Stage, private Compiler::ErrorHandler { enum WarningMode { Mode_Ignore, Mode_Normal, Mode_Strict }; const CSMDoc::Document& mDocument; Compiler::Extensions mExtensions; CSMWorld::ScriptContext mContext; std::string mId; std::string mFile; CSMDoc::Messages *mMessages; WarningMode mWarningMode; bool mIgnoreBaseRecords; CSMDoc::Message::Severity getSeverity (Type type); void report (const std::string& message, const Compiler::TokenLoc& loc, Type type) override; ///< Report error to the user. void report (const std::string& message, Type type) override; ///< Report a file related error public: ScriptCheckStage (const CSMDoc::Document& document); int setup() override; ///< \return number of steps void perform (int stage, CSMDoc::Messages& messages) override; ///< Messages resulting from this tage will be appended to \a messages. }; } #endif ================================================ FILE: apps/opencs/model/tools/search.cpp ================================================ #include "search.hpp" #include #include #include "../doc/messages.hpp" #include "../doc/document.hpp" #include "../world/idtablebase.hpp" #include "../world/columnbase.hpp" #include "../world/universalid.hpp" #include "../world/commands.hpp" void CSMTools::Search::searchTextCell (const CSMWorld::IdTableBase *model, const QModelIndex& index, const CSMWorld::UniversalId& id, bool writable, CSMDoc::Messages& messages) const { // using QString here for easier handling of case folding. QString search = QString::fromUtf8 (mText.c_str()); QString text = model->data (index).toString(); int pos = 0; Qt::CaseSensitivity caseSensitivity = mCase ? Qt::CaseSensitive : Qt::CaseInsensitive; while ((pos = text.indexOf (search, pos, caseSensitivity))!=-1) { std::ostringstream hint; hint << (writable ? 'R' : 'r') <<": " << model->getColumnId (index.column()) << " " << pos << " " << search.length(); messages.add (id, formatDescription (text, pos, search.length()).toUtf8().data(), hint.str()); pos += search.length(); } } void CSMTools::Search::searchRegExCell (const CSMWorld::IdTableBase *model, const QModelIndex& index, const CSMWorld::UniversalId& id, bool writable, CSMDoc::Messages& messages) const { QString text = model->data (index).toString(); int pos = 0; while ((pos = mRegExp.indexIn (text, pos))!=-1) { int length = mRegExp.matchedLength(); std::ostringstream hint; hint << (writable ? 'R' : 'r') <<": " << model->getColumnId (index.column()) << " " << pos << " " << length; messages.add (id, formatDescription (text, pos, length).toUtf8().data(), hint.str()); pos += length; } } void CSMTools::Search::searchRecordStateCell (const CSMWorld::IdTableBase *model, const QModelIndex& index, const CSMWorld::UniversalId& id, bool writable, CSMDoc::Messages& messages) const { if (writable) throw std::logic_error ("Record state can not be modified by search and replace"); int data = model->data (index).toInt(); if (data==mValue) { std::vector> states = CSMWorld::Columns::getEnums (CSMWorld::Columns::ColumnId_Modification); const std::string hint = "r: " + std::to_string(model->getColumnId(index.column())); messages.add (id, states.at(data).second, hint); } } QString CSMTools::Search::formatDescription (const QString& description, int pos, int length) const { QString text (description); // split QString highlight = flatten (text.mid (pos, length)); QString before = flatten (mPaddingBefore>=pos ? text.mid (0, pos) : text.mid (pos-mPaddingBefore, mPaddingBefore)); QString after = flatten (text.mid (pos+length, mPaddingAfter)); // compensate for Windows nonsense text.remove ('\r'); // join text = before + "" + highlight + "" + after; // improve layout for single line display text.replace ("\n", "<CR>"); text.replace ('\t', ' '); return text; } QString CSMTools::Search::flatten (const QString& text) const { QString flat (text); flat.replace ("&", "&"); flat.replace ("<", "<"); return flat; } CSMTools::Search::Search() : mType (Type_None), mValue (0), mCase (false), mIdColumn (0), mTypeColumn (0), mPaddingBefore (10), mPaddingAfter (10) {} CSMTools::Search::Search (Type type, bool caseSensitive, const std::string& value) : mType (type), mText (value), mValue (0), mCase (caseSensitive), mIdColumn (0), mTypeColumn (0), mPaddingBefore (10), mPaddingAfter (10) { if (type!=Type_Text && type!=Type_Id) throw std::logic_error ("Invalid search parameter (string)"); } CSMTools::Search::Search (Type type, bool caseSensitive, const QRegExp& value) : mType (type), mRegExp (value), mValue (0), mCase (caseSensitive), mIdColumn (0), mTypeColumn (0), mPaddingBefore (10), mPaddingAfter (10) { mRegExp.setCaseSensitivity(mCase ? Qt::CaseSensitive : Qt::CaseInsensitive); if (type!=Type_TextRegEx && type!=Type_IdRegEx) throw std::logic_error ("Invalid search parameter (RegExp)"); } CSMTools::Search::Search (Type type, bool caseSensitive, int value) : mType (type), mValue (value), mCase (caseSensitive), mIdColumn (0), mTypeColumn (0), mPaddingBefore (10), mPaddingAfter (10) { if (type!=Type_RecordState) throw std::logic_error ("invalid search parameter (int)"); } void CSMTools::Search::configure (const CSMWorld::IdTableBase *model) { mColumns.clear(); int columns = model->columnCount(); for (int i=0; i ( model->headerData ( i, Qt::Horizontal, static_cast (CSMWorld::ColumnBase::Role_Display)).toInt()); bool consider = false; switch (mType) { case Type_Text: case Type_TextRegEx: if (CSMWorld::ColumnBase::isText (display) || CSMWorld::ColumnBase::isScript (display)) { consider = true; } break; case Type_Id: case Type_IdRegEx: if (CSMWorld::ColumnBase::isId (display) || CSMWorld::ColumnBase::isScript (display)) { consider = true; } break; case Type_RecordState: if (display==CSMWorld::ColumnBase::Display_RecordState) consider = true; break; case Type_None: break; } if (consider) mColumns.insert (i); } mIdColumn = model->findColumnIndex (CSMWorld::Columns::ColumnId_Id); mTypeColumn = model->findColumnIndex (CSMWorld::Columns::ColumnId_RecordType); } void CSMTools::Search::searchRow (const CSMWorld::IdTableBase *model, int row, CSMDoc::Messages& messages) const { for (std::set::const_iterator iter (mColumns.begin()); iter!=mColumns.end(); ++iter) { QModelIndex index = model->index (row, *iter); CSMWorld::UniversalId::Type type = static_cast ( model->data (model->index (row, mTypeColumn)).toInt()); CSMWorld::UniversalId id ( type, model->data (model->index (row, mIdColumn)).toString().toUtf8().data()); bool writable = model->flags (index) & Qt::ItemIsEditable; switch (mType) { case Type_Text: case Type_Id: searchTextCell (model, index, id, writable, messages); break; case Type_TextRegEx: case Type_IdRegEx: searchRegExCell (model, index, id, writable, messages); break; case Type_RecordState: searchRecordStateCell (model, index, id, writable, messages); break; case Type_None: break; } } } void CSMTools::Search::setPadding (int before, int after) { mPaddingBefore = before; mPaddingAfter = after; } void CSMTools::Search::replace (CSMDoc::Document& document, CSMWorld::IdTableBase *model, const CSMWorld::UniversalId& id, const std::string& messageHint, const std::string& replaceText) const { std::istringstream stream (messageHint.c_str()); char hint, ignore; int columnId, pos, length; if (stream >> hint >> ignore >> columnId >> pos >> length) { int column = model->findColumnIndex (static_cast (columnId)); QModelIndex index = model->getModelIndex (id.getId(), column); std::string text = model->data (index).toString().toUtf8().constData(); std::string before = text.substr (0, pos); std::string after = text.substr (pos+length); std::string newText = before + replaceText + after; document.getUndoStack().push ( new CSMWorld::ModifyCommand (*model, index, QString::fromUtf8 (newText.c_str()))); } } bool CSMTools::Search::verify (CSMDoc::Document& document, CSMWorld::IdTableBase *model, const CSMWorld::UniversalId& id, const std::string& messageHint) const { CSMDoc::Messages messages (CSMDoc::Message::Severity_Info); int row = model->getModelIndex (id.getId(), model->findColumnIndex (CSMWorld::Columns::ColumnId_Id)).row(); searchRow (model, row, messages); for (CSMDoc::Messages::Iterator iter (messages.begin()); iter!=messages.end(); ++iter) if (iter->mHint==messageHint) return true; return false; } ================================================ FILE: apps/opencs/model/tools/search.hpp ================================================ #ifndef CSM_TOOLS_SEARCH_H #define CSM_TOOLS_SEARCH_H #include #include #include #include class QModelIndex; namespace CSMDoc { class Messages; class Document; } namespace CSMWorld { class IdTableBase; class UniversalId; } namespace CSMTools { class Search { public: enum Type { Type_Text = 0, Type_TextRegEx = 1, Type_Id = 2, Type_IdRegEx = 3, Type_RecordState = 4, Type_None }; private: Type mType; std::string mText; QRegExp mRegExp; int mValue; bool mCase; std::set mColumns; int mIdColumn; int mTypeColumn; int mPaddingBefore; int mPaddingAfter; void searchTextCell (const CSMWorld::IdTableBase *model, const QModelIndex& index, const CSMWorld::UniversalId& id, bool writable, CSMDoc::Messages& messages) const; void searchRegExCell (const CSMWorld::IdTableBase *model, const QModelIndex& index, const CSMWorld::UniversalId& id, bool writable, CSMDoc::Messages& messages) const; void searchRecordStateCell (const CSMWorld::IdTableBase *model, const QModelIndex& index, const CSMWorld::UniversalId& id, bool writable, CSMDoc::Messages& messages) const; QString formatDescription (const QString& description, int pos, int length) const; QString flatten (const QString& text) const; public: Search(); Search (Type type, bool caseSensitive, const std::string& value); Search (Type type, bool caseSensitive, const QRegExp& value); Search (Type type, bool caseSensitive, int value); // Configure search for the specified model. void configure (const CSMWorld::IdTableBase *model); // Search row in \a model and store results in \a messages. // // \attention *this needs to be configured for \a model. void searchRow (const CSMWorld::IdTableBase *model, int row, CSMDoc::Messages& messages) const; void setPadding (int before, int after); // Configuring *this for the model is not necessary when calling this function. void replace (CSMDoc::Document& document, CSMWorld::IdTableBase *model, const CSMWorld::UniversalId& id, const std::string& messageHint, const std::string& replaceText) const; // Check if model still matches search results. bool verify (CSMDoc::Document& document, CSMWorld::IdTableBase *model, const CSMWorld::UniversalId& id, const std::string& messageHint) const; }; } Q_DECLARE_METATYPE (CSMTools::Search) #endif ================================================ FILE: apps/opencs/model/tools/searchoperation.cpp ================================================ #include "searchoperation.hpp" #include "../doc/state.hpp" #include "../doc/document.hpp" #include "../world/data.hpp" #include "../world/idtablebase.hpp" #include "searchstage.hpp" CSMTools::SearchOperation::SearchOperation (CSMDoc::Document& document) : CSMDoc::Operation (CSMDoc::State_Searching, false) { std::vector types = CSMWorld::UniversalId::listTypes ( CSMWorld::UniversalId::Class_RecordList | CSMWorld::UniversalId::Class_ResourceList ); for (std::vector::const_iterator iter (types.begin()); iter!=types.end(); ++iter) appendStage (new SearchStage (&dynamic_cast ( *document.getData().getTableModel (*iter)))); setDefaultSeverity (CSMDoc::Message::Severity_Info); } void CSMTools::SearchOperation::configure (const Search& search) { mSearch = search; } void CSMTools::SearchOperation::appendStage (SearchStage *stage) { CSMDoc::Operation::appendStage (stage); stage->setOperation (this); } const CSMTools::Search& CSMTools::SearchOperation::getSearch() const { return mSearch; } ================================================ FILE: apps/opencs/model/tools/searchoperation.hpp ================================================ #ifndef CSM_TOOLS_SEARCHOPERATION_H #define CSM_TOOLS_SEARCHOPERATION_H #include "../doc/operation.hpp" #include "search.hpp" namespace CSMDoc { class Document; } namespace CSMTools { class SearchStage; class SearchOperation : public CSMDoc::Operation { Search mSearch; public: SearchOperation (CSMDoc::Document& document); /// \attention Do not call this function while a search is running. void configure (const Search& search); void appendStage (SearchStage *stage); ///< The ownership of \a stage is transferred to *this. /// /// \attention Do no call this function while this Operation is running. const Search& getSearch() const; }; } #endif ================================================ FILE: apps/opencs/model/tools/searchstage.cpp ================================================ #include "searchstage.hpp" #include "../world/idtablebase.hpp" #include "searchoperation.hpp" CSMTools::SearchStage::SearchStage (const CSMWorld::IdTableBase *model) : mModel (model), mOperation (nullptr) {} int CSMTools::SearchStage::setup() { if (mOperation) mSearch = mOperation->getSearch(); mSearch.configure (mModel); return mModel->rowCount(); } void CSMTools::SearchStage::perform (int stage, CSMDoc::Messages& messages) { mSearch.searchRow (mModel, stage, messages); } void CSMTools::SearchStage::setOperation (const SearchOperation *operation) { mOperation = operation; } ================================================ FILE: apps/opencs/model/tools/searchstage.hpp ================================================ #ifndef CSM_TOOLS_SEARCHSTAGE_H #define CSM_TOOLS_SEARCHSTAGE_H #include "../doc/stage.hpp" #include "search.hpp" namespace CSMWorld { class IdTableBase; } namespace CSMTools { class SearchOperation; class SearchStage : public CSMDoc::Stage { const CSMWorld::IdTableBase *mModel; Search mSearch; const SearchOperation *mOperation; public: SearchStage (const CSMWorld::IdTableBase *model); int setup() override; ///< \return number of steps void perform (int stage, CSMDoc::Messages& messages) override; ///< Messages resulting from this stage will be appended to \a messages. void setOperation (const SearchOperation *operation); }; } #endif ================================================ FILE: apps/opencs/model/tools/skillcheck.cpp ================================================ #include "skillcheck.hpp" #include "../prefs/state.hpp" #include "../world/universalid.hpp" CSMTools::SkillCheckStage::SkillCheckStage (const CSMWorld::IdCollection& skills) : mSkills (skills) { mIgnoreBaseRecords = false; } int CSMTools::SkillCheckStage::setup() { mIgnoreBaseRecords = CSMPrefs::get()["Reports"]["ignore-base-records"].isTrue(); return mSkills.getSize(); } void CSMTools::SkillCheckStage::perform (int stage, CSMDoc::Messages& messages) { const CSMWorld::Record& record = mSkills.getRecord (stage); // Skip "Base" records (setting!) and "Deleted" records if ((mIgnoreBaseRecords && record.mState == CSMWorld::RecordBase::State_BaseOnly) || record.isDeleted()) return; const ESM::Skill& skill = record.get(); CSMWorld::UniversalId id (CSMWorld::UniversalId::Type_Skill, skill.mId); if (skill.mDescription.empty()) messages.add(id, "Description is missing", "", CSMDoc::Message::Severity_Warning); for (int i=0; i<4; ++i) if (skill.mData.mUseValue[i]<0) { messages.add(id, "Use value #" + std::to_string(i) + " is negative", "", CSMDoc::Message::Severity_Error); } } ================================================ FILE: apps/opencs/model/tools/skillcheck.hpp ================================================ #ifndef CSM_TOOLS_SKILLCHECK_H #define CSM_TOOLS_SKILLCHECK_H #include #include "../world/idcollection.hpp" #include "../doc/stage.hpp" namespace CSMTools { /// \brief VerifyStage: make sure that skill records are internally consistent class SkillCheckStage : public CSMDoc::Stage { const CSMWorld::IdCollection& mSkills; bool mIgnoreBaseRecords; public: SkillCheckStage (const CSMWorld::IdCollection& skills); int setup() override; ///< \return number of steps void perform (int stage, CSMDoc::Messages& messages) override; ///< Messages resulting from this tage will be appended to \a messages. }; } #endif ================================================ FILE: apps/opencs/model/tools/soundcheck.cpp ================================================ #include "soundcheck.hpp" #include "../prefs/state.hpp" #include "../world/universalid.hpp" CSMTools::SoundCheckStage::SoundCheckStage (const CSMWorld::IdCollection &sounds, const CSMWorld::Resources &soundfiles) : mSounds (sounds), mSoundFiles (soundfiles) { mIgnoreBaseRecords = false; } int CSMTools::SoundCheckStage::setup() { mIgnoreBaseRecords = CSMPrefs::get()["Reports"]["ignore-base-records"].isTrue(); return mSounds.getSize(); } void CSMTools::SoundCheckStage::perform (int stage, CSMDoc::Messages& messages) { const CSMWorld::Record& record = mSounds.getRecord (stage); // Skip "Base" records (setting!) and "Deleted" records if ((mIgnoreBaseRecords && record.mState == CSMWorld::RecordBase::State_BaseOnly) || record.isDeleted()) return; const ESM::Sound& sound = record.get(); CSMWorld::UniversalId id (CSMWorld::UniversalId::Type_Sound, sound.mId); if (sound.mData.mMinRange>sound.mData.mMaxRange) { messages.add(id, "Minimum range is larger than maximum range", "", CSMDoc::Message::Severity_Warning); } if (sound.mSound.empty()) { messages.add(id, "Sound file is missing", "", CSMDoc::Message::Severity_Error); } else if (mSoundFiles.searchId(sound.mSound) == -1) { messages.add(id, "Sound file '" + sound.mSound + "' does not exist", "", CSMDoc::Message::Severity_Error); } } ================================================ FILE: apps/opencs/model/tools/soundcheck.hpp ================================================ #ifndef CSM_TOOLS_SOUNDCHECK_H #define CSM_TOOLS_SOUNDCHECK_H #include #include "../world/resources.hpp" #include "../world/idcollection.hpp" #include "../doc/stage.hpp" namespace CSMTools { /// \brief VerifyStage: make sure that sound records are internally consistent class SoundCheckStage : public CSMDoc::Stage { const CSMWorld::IdCollection& mSounds; const CSMWorld::Resources &mSoundFiles; bool mIgnoreBaseRecords; public: SoundCheckStage (const CSMWorld::IdCollection& sounds, const CSMWorld::Resources &soundfiles); int setup() override; ///< \return number of steps void perform (int stage, CSMDoc::Messages& messages) override; ///< Messages resulting from this tage will be appended to \a messages. }; } #endif ================================================ FILE: apps/opencs/model/tools/soundgencheck.cpp ================================================ #include "soundgencheck.hpp" #include "../prefs/state.hpp" #include "../world/refiddata.hpp" #include "../world/universalid.hpp" CSMTools::SoundGenCheckStage::SoundGenCheckStage(const CSMWorld::IdCollection &soundGens, const CSMWorld::IdCollection &sounds, const CSMWorld::RefIdCollection &objects) : mSoundGens(soundGens), mSounds(sounds), mObjects(objects) { mIgnoreBaseRecords = false; } int CSMTools::SoundGenCheckStage::setup() { mIgnoreBaseRecords = CSMPrefs::get()["Reports"]["ignore-base-records"].isTrue(); return mSoundGens.getSize(); } void CSMTools::SoundGenCheckStage::perform(int stage, CSMDoc::Messages &messages) { const CSMWorld::Record &record = mSoundGens.getRecord(stage); // Skip "Base" records (setting!) and "Deleted" records if ((mIgnoreBaseRecords && record.mState == CSMWorld::RecordBase::State_BaseOnly) || record.isDeleted()) return; const ESM::SoundGenerator& soundGen = record.get(); CSMWorld::UniversalId id(CSMWorld::UniversalId::Type_SoundGen, soundGen.mId); if (!soundGen.mCreature.empty()) { CSMWorld::RefIdData::LocalIndex creatureIndex = mObjects.getDataSet().searchId(soundGen.mCreature); if (creatureIndex.first == -1) { messages.add(id, "Creature '" + soundGen.mCreature + "' doesn't exist", "", CSMDoc::Message::Severity_Error); } else if (creatureIndex.second != CSMWorld::UniversalId::Type_Creature) { messages.add(id, "'" + soundGen.mCreature + "' is not a creature", "", CSMDoc::Message::Severity_Error); } } if (soundGen.mSound.empty()) { messages.add(id, "Sound is missing", "", CSMDoc::Message::Severity_Error); } else if (mSounds.searchId(soundGen.mSound) == -1) { messages.add(id, "Sound '" + soundGen.mSound + "' doesn't exist", "", CSMDoc::Message::Severity_Error); } } ================================================ FILE: apps/opencs/model/tools/soundgencheck.hpp ================================================ #ifndef CSM_TOOLS_SOUNDGENCHECK_HPP #define CSM_TOOLS_SOUNDGENCHECK_HPP #include "../world/data.hpp" #include "../doc/stage.hpp" namespace CSMTools { /// \brief VerifyStage: make sure that sound gen records are internally consistent class SoundGenCheckStage : public CSMDoc::Stage { const CSMWorld::IdCollection &mSoundGens; const CSMWorld::IdCollection &mSounds; const CSMWorld::RefIdCollection &mObjects; bool mIgnoreBaseRecords; public: SoundGenCheckStage(const CSMWorld::IdCollection &soundGens, const CSMWorld::IdCollection &sounds, const CSMWorld::RefIdCollection &objects); int setup() override; ///< \return number of steps void perform(int stage, CSMDoc::Messages &messages) override; ///< Messages resulting from this stage will be appended to \a messages. }; } #endif ================================================ FILE: apps/opencs/model/tools/spellcheck.cpp ================================================ #include "spellcheck.hpp" #include #include #include #include "../prefs/state.hpp" #include "../world/universalid.hpp" CSMTools::SpellCheckStage::SpellCheckStage (const CSMWorld::IdCollection& spells) : mSpells (spells) { mIgnoreBaseRecords = false; } int CSMTools::SpellCheckStage::setup() { mIgnoreBaseRecords = CSMPrefs::get()["Reports"]["ignore-base-records"].isTrue(); return mSpells.getSize(); } void CSMTools::SpellCheckStage::perform (int stage, CSMDoc::Messages& messages) { const CSMWorld::Record& record = mSpells.getRecord (stage); // Skip "Base" records (setting!) and "Deleted" records if ((mIgnoreBaseRecords && record.mState == CSMWorld::RecordBase::State_BaseOnly) || record.isDeleted()) return; const ESM::Spell& spell = record.get(); CSMWorld::UniversalId id (CSMWorld::UniversalId::Type_Spell, spell.mId); // test for empty name if (spell.mName.empty()) messages.add(id, "Name is missing", "", CSMDoc::Message::Severity_Error); // test for invalid cost values if (spell.mData.mCost<0) messages.add(id, "Spell cost is negative", "", CSMDoc::Message::Severity_Error); /// \todo check data members that can't be edited in the table view } ================================================ FILE: apps/opencs/model/tools/spellcheck.hpp ================================================ #ifndef CSM_TOOLS_SPELLCHECK_H #define CSM_TOOLS_SPELLCHECK_H #include #include "../world/idcollection.hpp" #include "../doc/stage.hpp" namespace CSMTools { /// \brief VerifyStage: make sure that spell records are internally consistent class SpellCheckStage : public CSMDoc::Stage { const CSMWorld::IdCollection& mSpells; bool mIgnoreBaseRecords; public: SpellCheckStage (const CSMWorld::IdCollection& spells); int setup() override; ///< \return number of steps void perform (int stage, CSMDoc::Messages& messages) override; ///< Messages resulting from this tage will be appended to \a messages. }; } #endif ================================================ FILE: apps/opencs/model/tools/startscriptcheck.cpp ================================================ #include "startscriptcheck.hpp" #include "../prefs/state.hpp" #include CSMTools::StartScriptCheckStage::StartScriptCheckStage ( const CSMWorld::IdCollection& startScripts, const CSMWorld::IdCollection& scripts) : mStartScripts (startScripts), mScripts (scripts) { mIgnoreBaseRecords = false; } void CSMTools::StartScriptCheckStage::perform(int stage, CSMDoc::Messages& messages) { const CSMWorld::Record& record = mStartScripts.getRecord (stage); // Skip "Base" records (setting!) and "Deleted" records if ((mIgnoreBaseRecords && record.mState == CSMWorld::RecordBase::State_BaseOnly) || record.isDeleted()) return; std::string scriptId = record.get().mId; CSMWorld::UniversalId id (CSMWorld::UniversalId::Type_StartScript, scriptId); if (mScripts.searchId (Misc::StringUtils::lowerCase (scriptId))==-1) messages.add(id, "Start script " + scriptId + " does not exist", "", CSMDoc::Message::Severity_Error); } int CSMTools::StartScriptCheckStage::setup() { mIgnoreBaseRecords = CSMPrefs::get()["Reports"]["ignore-base-records"].isTrue(); return mStartScripts.getSize(); } ================================================ FILE: apps/opencs/model/tools/startscriptcheck.hpp ================================================ #ifndef CSM_TOOLS_STARTSCRIPTCHECK_H #define CSM_TOOLS_STARTSCRIPTCHECK_H #include #include #include "../doc/stage.hpp" #include "../world/idcollection.hpp" namespace CSMTools { class StartScriptCheckStage : public CSMDoc::Stage { const CSMWorld::IdCollection& mStartScripts; const CSMWorld::IdCollection& mScripts; bool mIgnoreBaseRecords; public: StartScriptCheckStage (const CSMWorld::IdCollection& startScripts, const CSMWorld::IdCollection& scripts); void perform(int stage, CSMDoc::Messages& messages) override; int setup() override; }; } #endif ================================================ FILE: apps/opencs/model/tools/tools.cpp ================================================ #include "tools.hpp" #include #include "../doc/state.hpp" #include "../doc/operation.hpp" #include "../doc/document.hpp" #include "../world/data.hpp" #include "../world/universalid.hpp" #include "reportmodel.hpp" #include "mandatoryid.hpp" #include "skillcheck.hpp" #include "classcheck.hpp" #include "factioncheck.hpp" #include "racecheck.hpp" #include "soundcheck.hpp" #include "regioncheck.hpp" #include "birthsigncheck.hpp" #include "spellcheck.hpp" #include "referenceablecheck.hpp" #include "scriptcheck.hpp" #include "bodypartcheck.hpp" #include "referencecheck.hpp" #include "startscriptcheck.hpp" #include "searchoperation.hpp" #include "pathgridcheck.hpp" #include "soundgencheck.hpp" #include "magiceffectcheck.hpp" #include "mergeoperation.hpp" #include "gmstcheck.hpp" #include "topicinfocheck.hpp" #include "journalcheck.hpp" #include "enchantmentcheck.hpp" CSMDoc::OperationHolder *CSMTools::Tools::get (int type) { switch (type) { case CSMDoc::State_Verifying: return &mVerifier; case CSMDoc::State_Searching: return &mSearch; case CSMDoc::State_Merging: return &mMerge; } return nullptr; } const CSMDoc::OperationHolder *CSMTools::Tools::get (int type) const { return const_cast (this)->get (type); } CSMDoc::OperationHolder *CSMTools::Tools::getVerifier() { if (!mVerifierOperation) { mVerifierOperation = new CSMDoc::Operation (CSMDoc::State_Verifying, false); connect (&mVerifier, SIGNAL (progress (int, int, int)), this, SIGNAL (progress (int, int, int))); connect (&mVerifier, SIGNAL (done (int, bool)), this, SIGNAL (done (int, bool))); connect (&mVerifier, SIGNAL (reportMessage (const CSMDoc::Message&, int)), this, SLOT (verifierMessage (const CSMDoc::Message&, int))); std::vector mandatoryIds {"Day", "DaysPassed", "GameHour", "Month", "PCRace"}; mVerifierOperation->appendStage (new MandatoryIdStage (mData.getGlobals(), CSMWorld::UniversalId (CSMWorld::UniversalId::Type_Globals), mandatoryIds)); mVerifierOperation->appendStage (new SkillCheckStage (mData.getSkills())); mVerifierOperation->appendStage (new ClassCheckStage (mData.getClasses())); mVerifierOperation->appendStage (new FactionCheckStage (mData.getFactions())); mVerifierOperation->appendStage (new RaceCheckStage (mData.getRaces())); mVerifierOperation->appendStage (new SoundCheckStage (mData.getSounds(), mData.getResources (CSMWorld::UniversalId::Type_SoundsRes))); mVerifierOperation->appendStage (new RegionCheckStage (mData.getRegions())); mVerifierOperation->appendStage (new BirthsignCheckStage (mData.getBirthsigns(), mData.getResources (CSMWorld::UniversalId::Type_Textures))); mVerifierOperation->appendStage (new SpellCheckStage (mData.getSpells())); mVerifierOperation->appendStage (new ReferenceableCheckStage (mData.getReferenceables().getDataSet(), mData.getRaces(), mData.getClasses(), mData.getFactions(), mData.getScripts(), mData.getResources (CSMWorld::UniversalId::Type_Meshes), mData.getResources (CSMWorld::UniversalId::Type_Icons), mData.getBodyParts())); mVerifierOperation->appendStage (new ReferenceCheckStage(mData.getReferences(), mData.getReferenceables(), mData.getCells(), mData.getFactions())); mVerifierOperation->appendStage (new ScriptCheckStage (mDocument)); mVerifierOperation->appendStage (new StartScriptCheckStage (mData.getStartScripts(), mData.getScripts())); mVerifierOperation->appendStage( new BodyPartCheckStage( mData.getBodyParts(), mData.getResources( CSMWorld::UniversalId( CSMWorld::UniversalId::Type_Meshes )), mData.getRaces() )); mVerifierOperation->appendStage (new PathgridCheckStage (mData.getPathgrids())); mVerifierOperation->appendStage (new SoundGenCheckStage (mData.getSoundGens(), mData.getSounds(), mData.getReferenceables())); mVerifierOperation->appendStage (new MagicEffectCheckStage (mData.getMagicEffects(), mData.getSounds(), mData.getReferenceables(), mData.getResources (CSMWorld::UniversalId::Type_Icons), mData.getResources (CSMWorld::UniversalId::Type_Textures))); mVerifierOperation->appendStage (new GmstCheckStage (mData.getGmsts())); mVerifierOperation->appendStage (new TopicInfoCheckStage (mData.getTopicInfos(), mData.getCells(), mData.getClasses(), mData.getFactions(), mData.getGmsts(), mData.getGlobals(), mData.getJournals(), mData.getRaces(), mData.getRegions(), mData.getTopics(), mData.getReferenceables().getDataSet(), mData.getResources (CSMWorld::UniversalId::Type_SoundsRes))); mVerifierOperation->appendStage (new JournalCheckStage(mData.getJournals(), mData.getJournalInfos())); mVerifierOperation->appendStage (new EnchantmentCheckStage(mData.getEnchantments())); mVerifier.setOperation (mVerifierOperation); } return &mVerifier; } CSMTools::Tools::Tools (CSMDoc::Document& document, ToUTF8::FromType encoding) : mDocument (document), mData (document.getData()), mVerifierOperation (nullptr), mSearchOperation (nullptr), mMergeOperation (nullptr), mNextReportNumber (0), mEncoding (encoding) { // index 0: load error log mReports.insert (std::make_pair (mNextReportNumber++, new ReportModel)); mActiveReports.insert (std::make_pair (CSMDoc::State_Loading, 0)); connect (&mSearch, SIGNAL (progress (int, int, int)), this, SIGNAL (progress (int, int, int))); connect (&mSearch, SIGNAL (done (int, bool)), this, SIGNAL (done (int, bool))); connect (&mSearch, SIGNAL (reportMessage (const CSMDoc::Message&, int)), this, SLOT (verifierMessage (const CSMDoc::Message&, int))); connect (&mMerge, SIGNAL (progress (int, int, int)), this, SIGNAL (progress (int, int, int))); connect (&mMerge, SIGNAL (done (int, bool)), this, SIGNAL (done (int, bool))); // don't need to connect report message, since there are no messages for merge } CSMTools::Tools::~Tools() { if (mVerifierOperation) { mVerifier.abortAndWait(); delete mVerifierOperation; } if (mSearchOperation) { mSearch.abortAndWait(); delete mSearchOperation; } if (mMergeOperation) { mMerge.abortAndWait(); delete mMergeOperation; } for (std::map::iterator iter (mReports.begin()); iter!=mReports.end(); ++iter) delete iter->second; } CSMWorld::UniversalId CSMTools::Tools::runVerifier (const CSMWorld::UniversalId& reportId) { int reportNumber = reportId.getType()==CSMWorld::UniversalId::Type_VerificationResults ? reportId.getIndex() : mNextReportNumber++; if (mReports.find (reportNumber)==mReports.end()) mReports.insert (std::make_pair (reportNumber, new ReportModel)); mActiveReports[CSMDoc::State_Verifying] = reportNumber; getVerifier()->start(); return CSMWorld::UniversalId (CSMWorld::UniversalId::Type_VerificationResults, reportNumber); } CSMWorld::UniversalId CSMTools::Tools::newSearch() { mReports.insert (std::make_pair (mNextReportNumber++, new ReportModel (true, false))); return CSMWorld::UniversalId (CSMWorld::UniversalId::Type_Search, mNextReportNumber-1); } void CSMTools::Tools::runSearch (const CSMWorld::UniversalId& searchId, const Search& search) { mActiveReports[CSMDoc::State_Searching] = searchId.getIndex(); if (!mSearchOperation) { mSearchOperation = new SearchOperation (mDocument); mSearch.setOperation (mSearchOperation); } mSearchOperation->configure (search); mSearch.start(); } void CSMTools::Tools::runMerge (std::unique_ptr target) { // not setting an active report, because merge does not produce messages if (!mMergeOperation) { mMergeOperation = new MergeOperation (mDocument, mEncoding); mMerge.setOperation (mMergeOperation); connect (mMergeOperation, SIGNAL (mergeDone (CSMDoc::Document*)), this, SIGNAL (mergeDone (CSMDoc::Document*))); } target->flagAsDirty(); mMergeOperation->setTarget (std::move(target)); mMerge.start(); } void CSMTools::Tools::abortOperation (int type) { if (CSMDoc::OperationHolder *operation = get (type)) operation->abort(); } int CSMTools::Tools::getRunningOperations() const { static const int sOperations[] = { CSMDoc::State_Verifying, CSMDoc::State_Searching, CSMDoc::State_Merging, -1 }; int result = 0; for (int i=0; sOperations[i]!=-1; ++i) if (const CSMDoc::OperationHolder *operation = get (sOperations[i])) if (operation->isRunning()) result |= sOperations[i]; return result; } CSMTools::ReportModel *CSMTools::Tools::getReport (const CSMWorld::UniversalId& id) { if (id.getType()!=CSMWorld::UniversalId::Type_VerificationResults && id.getType()!=CSMWorld::UniversalId::Type_LoadErrorLog && id.getType()!=CSMWorld::UniversalId::Type_Search) throw std::logic_error ("invalid request for report model: " + id.toString()); return mReports.at (id.getIndex()); } void CSMTools::Tools::verifierMessage (const CSMDoc::Message& message, int type) { std::map::iterator iter = mActiveReports.find (type); if (iter!=mActiveReports.end()) mReports[iter->second]->add (message); } ================================================ FILE: apps/opencs/model/tools/tools.hpp ================================================ #ifndef CSM_TOOLS_TOOLS_H #define CSM_TOOLS_TOOLS_H #include #include #include #include #include #include "../doc/operationholder.hpp" namespace CSMWorld { class Data; class UniversalId; } namespace CSMDoc { class Operation; class Document; } namespace CSMTools { class ReportModel; class Search; class SearchOperation; class MergeOperation; class Tools : public QObject { Q_OBJECT CSMDoc::Document& mDocument; CSMWorld::Data& mData; CSMDoc::Operation *mVerifierOperation; CSMDoc::OperationHolder mVerifier; SearchOperation *mSearchOperation; CSMDoc::OperationHolder mSearch; MergeOperation *mMergeOperation; CSMDoc::OperationHolder mMerge; std::map mReports; int mNextReportNumber; std::map mActiveReports; // type, report number ToUTF8::FromType mEncoding; // not implemented Tools (const Tools&); Tools& operator= (const Tools&); CSMDoc::OperationHolder *getVerifier(); CSMDoc::OperationHolder *get (int type); ///< Returns a 0-pointer, if operation hasn't been used yet. const CSMDoc::OperationHolder *get (int type) const; ///< Returns a 0-pointer, if operation hasn't been used yet. public: Tools (CSMDoc::Document& document, ToUTF8::FromType encoding); virtual ~Tools(); /// \param reportId If a valid VerificationResults ID, run verifier for the /// specified report instead of creating a new one. /// /// \return ID of the report for this verification run CSMWorld::UniversalId runVerifier (const CSMWorld::UniversalId& reportId = CSMWorld::UniversalId()); /// Return ID of the report for this search. CSMWorld::UniversalId newSearch(); void runSearch (const CSMWorld::UniversalId& searchId, const Search& search); void runMerge (std::unique_ptr target); void abortOperation (int type); ///< \attention The operation is not aborted immediately. int getRunningOperations() const; ReportModel *getReport (const CSMWorld::UniversalId& id); ///< The ownership of the returned report is not transferred. private slots: void verifierMessage (const CSMDoc::Message& message, int type); signals: void progress (int current, int max, int type); void done (int type, bool failed); /// \attention When this signal is emitted, *this hands over the ownership of the /// document. This signal must be handled to avoid a leak. void mergeDone (CSMDoc::Document *document); }; } #endif ================================================ FILE: apps/opencs/model/tools/topicinfocheck.cpp ================================================ #include "topicinfocheck.hpp" #include #include "../prefs/state.hpp" #include "../world/infoselectwrapper.hpp" CSMTools::TopicInfoCheckStage::TopicInfoCheckStage( const CSMWorld::InfoCollection& topicInfos, const CSMWorld::IdCollection& cells, const CSMWorld::IdCollection& classes, const CSMWorld::IdCollection& factions, const CSMWorld::IdCollection& gmsts, const CSMWorld::IdCollection& globals, const CSMWorld::IdCollection& journals, const CSMWorld::IdCollection& races, const CSMWorld::IdCollection& regions, const CSMWorld::IdCollection &topics, const CSMWorld::RefIdData& referencables, const CSMWorld::Resources& soundFiles) : mTopicInfos(topicInfos), mCells(cells), mClasses(classes), mFactions(factions), mGameSettings(gmsts), mGlobals(globals), mJournals(journals), mRaces(races), mRegions(regions), mTopics(topics), mReferencables(referencables), mSoundFiles(soundFiles) { mIgnoreBaseRecords = false; } int CSMTools::TopicInfoCheckStage::setup() { // Generate list of cell names for reference checking mCellNames.clear(); for (int i = 0; i < mCells.getSize(); ++i) { const CSMWorld::Record& cellRecord = mCells.getRecord(i); if (cellRecord.isDeleted()) continue; mCellNames.insert(cellRecord.get().mName); } // Cell names can also include region names for (int i = 0; i < mRegions.getSize(); ++i) { const CSMWorld::Record& regionRecord = mRegions.getRecord(i); if (regionRecord.isDeleted()) continue; mCellNames.insert(regionRecord.get().mName); } // Default cell name int index = mGameSettings.searchId("sDefaultCellname"); if (index != -1) { const CSMWorld::Record& gmstRecord = mGameSettings.getRecord(index); if (!gmstRecord.isDeleted() && gmstRecord.get().mValue.getType() == ESM::VT_String) { mCellNames.insert(gmstRecord.get().mValue.getString()); } } mIgnoreBaseRecords = CSMPrefs::get()["Reports"]["ignore-base-records"].isTrue(); return mTopicInfos.getSize(); } void CSMTools::TopicInfoCheckStage::perform(int stage, CSMDoc::Messages& messages) { const CSMWorld::Record& infoRecord = mTopicInfos.getRecord(stage); // Skip "Base" records (setting!) and "Deleted" records if ((mIgnoreBaseRecords && infoRecord.mState == CSMWorld::RecordBase::State_BaseOnly) || infoRecord.isDeleted()) return; const CSMWorld::Info& topicInfo = infoRecord.get(); // There should always be a topic that matches int topicIndex = mTopics.searchId(topicInfo.mTopicId); const CSMWorld::Record& topicRecord = mTopics.getRecord(topicIndex); if (topicRecord.isDeleted()) return; const ESM::Dialogue& topic = topicRecord.get(); CSMWorld::UniversalId id(CSMWorld::UniversalId::Type_TopicInfo, topicInfo.mId); // Check fields if (!topicInfo.mActor.empty()) { verifyActor(topicInfo.mActor, id, messages); } if (!topicInfo.mClass.empty()) { verifyId(topicInfo.mClass, mClasses, id, messages); } if (!topicInfo.mCell.empty()) { verifyCell(topicInfo.mCell, id, messages); } if (!topicInfo.mFaction.empty() && !topicInfo.mFactionLess) { if (verifyId(topicInfo.mFaction, mFactions, id, messages)) { verifyFactionRank(topicInfo.mFaction, topicInfo.mData.mRank, id, messages); } } if (!topicInfo.mPcFaction.empty()) { if (verifyId(topicInfo.mPcFaction, mFactions, id, messages)) { verifyFactionRank(topicInfo.mPcFaction, topicInfo.mData.mPCrank, id, messages); } } if (topicInfo.mData.mGender < -1 || topicInfo.mData.mGender > 1) { messages.add(id, "Gender is invalid", "", CSMDoc::Message::Severity_Error); } if (!topicInfo.mRace.empty()) { verifyId(topicInfo.mRace, mRaces, id, messages); } if (!topicInfo.mSound.empty()) { verifySound(topicInfo.mSound, id, messages); } if (topicInfo.mResponse.empty() && topic.mType != ESM::Dialogue::Voice) { messages.add(id, "Response is empty", "", CSMDoc::Message::Severity_Warning); } // Check info conditions for (std::vector::const_iterator it = topicInfo.mSelects.begin(); it != topicInfo.mSelects.end(); ++it) { verifySelectStruct((*it), id, messages); } } // Verification functions bool CSMTools::TopicInfoCheckStage::verifyActor(const std::string& actor, const CSMWorld::UniversalId& id, CSMDoc::Messages& messages) { CSMWorld::RefIdData::LocalIndex index = mReferencables.searchId(actor); if (index.first == -1) { messages.add(id, "Actor '" + actor + "' does not exist", "", CSMDoc::Message::Severity_Error); return false; } else if (mReferencables.getRecord(index).isDeleted()) { messages.add(id, "Deleted actor '" + actor + "' is being referenced", "", CSMDoc::Message::Severity_Error); return false; } else if (index.second != CSMWorld::UniversalId::Type_Npc && index.second != CSMWorld::UniversalId::Type_Creature) { CSMWorld::UniversalId tempId(index.second, actor); std::ostringstream stream; stream << "Object '" << actor << "' has invalid type " << tempId.getTypeName() << " (an actor must be an NPC or a creature)"; messages.add(id, stream.str(), "", CSMDoc::Message::Severity_Error); return false; } return true; } bool CSMTools::TopicInfoCheckStage::verifyCell(const std::string& cell, const CSMWorld::UniversalId& id, CSMDoc::Messages& messages) { if (mCellNames.find(cell) == mCellNames.end()) { messages.add(id, "Cell '" + cell + "' does not exist", "", CSMDoc::Message::Severity_Error); return false; } return true; } bool CSMTools::TopicInfoCheckStage::verifyFactionRank(const std::string& factionName, int rank, const CSMWorld::UniversalId& id, CSMDoc::Messages& messages) { if (rank < -1) { std::ostringstream stream; stream << "Faction rank is set to " << rank << ", but it should be set to -1 if there are no rank requirements"; messages.add(id, stream.str(), "", CSMDoc::Message::Severity_Warning); return false; } int index = mFactions.searchId(factionName); const ESM::Faction &faction = mFactions.getRecord(index).get(); int limit = 0; for (; limit < 10; ++limit) { if (faction.mRanks[limit].empty()) break; } if (rank >= limit) { std::ostringstream stream; stream << "Faction rank is set to " << rank << " which is more than the maximum of " << limit - 1 << " for the '" << factionName << "' faction"; messages.add(id, stream.str(), "", CSMDoc::Message::Severity_Error); return false; } return true; } bool CSMTools::TopicInfoCheckStage::verifyItem(const std::string& item, const CSMWorld::UniversalId& id, CSMDoc::Messages& messages) { CSMWorld::RefIdData::LocalIndex index = mReferencables.searchId(item); if (index.first == -1) { messages.add(id, ("Item '" + item + "' does not exist"), "", CSMDoc::Message::Severity_Error); return false; } else if (mReferencables.getRecord(index).isDeleted()) { messages.add(id, ("Deleted item '" + item + "' is being referenced"), "", CSMDoc::Message::Severity_Error); return false; } else { switch (index.second) { case CSMWorld::UniversalId::Type_Potion: case CSMWorld::UniversalId::Type_Apparatus: case CSMWorld::UniversalId::Type_Armor: case CSMWorld::UniversalId::Type_Book: case CSMWorld::UniversalId::Type_Clothing: case CSMWorld::UniversalId::Type_Ingredient: case CSMWorld::UniversalId::Type_Light: case CSMWorld::UniversalId::Type_Lockpick: case CSMWorld::UniversalId::Type_Miscellaneous: case CSMWorld::UniversalId::Type_Probe: case CSMWorld::UniversalId::Type_Repair: case CSMWorld::UniversalId::Type_Weapon: case CSMWorld::UniversalId::Type_ItemLevelledList: break; default: { CSMWorld::UniversalId tempId(index.second, item); std::ostringstream stream; stream << "Object '" << item << "' has invalid type " << tempId.getTypeName() << " (an item can be a potion, an armor piece, a book and so on)"; messages.add(id, stream.str(), "", CSMDoc::Message::Severity_Error); return false; } } } return true; } bool CSMTools::TopicInfoCheckStage::verifySelectStruct(const ESM::DialInfo::SelectStruct& select, const CSMWorld::UniversalId& id, CSMDoc::Messages& messages) { CSMWorld::ConstInfoSelectWrapper infoCondition(select); if (infoCondition.getFunctionName() == CSMWorld::ConstInfoSelectWrapper::Function_None) { messages.add(id, "Invalid condition '" + infoCondition.toString() + "'", "", CSMDoc::Message::Severity_Error); return false; } else if (!infoCondition.variantTypeIsValid()) { std::ostringstream stream; stream << "Value of condition '" << infoCondition.toString() << "' has invalid "; switch (select.mValue.getType()) { case ESM::VT_None: stream << "None"; break; case ESM::VT_Short: stream << "Short"; break; case ESM::VT_Int: stream << "Int"; break; case ESM::VT_Long: stream << "Long"; break; case ESM::VT_Float: stream << "Float"; break; case ESM::VT_String: stream << "String"; break; default: stream << "unknown"; break; } stream << " type"; messages.add(id, stream.str(), "", CSMDoc::Message::Severity_Error); return false; } else if (infoCondition.conditionIsAlwaysTrue()) { messages.add(id, "Condition '" + infoCondition.toString() + "' is always true", "", CSMDoc::Message::Severity_Warning); return false; } else if (infoCondition.conditionIsNeverTrue()) { messages.add(id, "Condition '" + infoCondition.toString() + "' is never true", "", CSMDoc::Message::Severity_Warning); return false; } // Id checks if (infoCondition.getFunctionName() == CSMWorld::ConstInfoSelectWrapper::Function_Global && !verifyId(infoCondition.getVariableName(), mGlobals, id, messages)) { return false; } else if (infoCondition.getFunctionName() == CSMWorld::ConstInfoSelectWrapper::Function_Journal && !verifyId(infoCondition.getVariableName(), mJournals, id, messages)) { return false; } else if (infoCondition.getFunctionName() == CSMWorld::ConstInfoSelectWrapper::Function_Item && !verifyItem(infoCondition.getVariableName(), id, messages)) { return false; } else if (infoCondition.getFunctionName() == CSMWorld::ConstInfoSelectWrapper::Function_Dead && !verifyActor(infoCondition.getVariableName(), id, messages)) { return false; } else if (infoCondition.getFunctionName() == CSMWorld::ConstInfoSelectWrapper::Function_NotId && !verifyActor(infoCondition.getVariableName(), id, messages)) { return false; } else if (infoCondition.getFunctionName() == CSMWorld::ConstInfoSelectWrapper::Function_NotFaction && !verifyId(infoCondition.getVariableName(), mFactions, id, messages)) { return false; } else if (infoCondition.getFunctionName() == CSMWorld::ConstInfoSelectWrapper::Function_NotClass && !verifyId(infoCondition.getVariableName(), mClasses, id, messages)) { return false; } else if (infoCondition.getFunctionName() == CSMWorld::ConstInfoSelectWrapper::Function_NotRace && !verifyId(infoCondition.getVariableName(), mRaces, id, messages)) { return false; } else if (infoCondition.getFunctionName() == CSMWorld::ConstInfoSelectWrapper::Function_NotCell && !verifyCell(infoCondition.getVariableName(), id, messages)) { return false; } return true; } bool CSMTools::TopicInfoCheckStage::verifySound(const std::string& sound, const CSMWorld::UniversalId& id, CSMDoc::Messages& messages) { if (mSoundFiles.searchId(sound) == -1) { messages.add(id, "Sound file '" + sound + "' does not exist", "", CSMDoc::Message::Severity_Error); return false; } return true; } template bool CSMTools::TopicInfoCheckStage::verifyId(const std::string& name, const CSMWorld::IdCollection& collection, const CSMWorld::UniversalId& id, CSMDoc::Messages& messages) { int index = collection.searchId(name); if (index == -1) { messages.add(id, T::getRecordType() + " '" + name + "' does not exist", "", CSMDoc::Message::Severity_Error); return false; } else if (collection.getRecord(index).isDeleted()) { messages.add(id, "Deleted " + T::getRecordType() + " record '" + name + "' is being referenced", "", CSMDoc::Message::Severity_Error); return false; } return true; } ================================================ FILE: apps/opencs/model/tools/topicinfocheck.hpp ================================================ #ifndef CSM_TOOLS_TOPICINFOCHECK_HPP #define CSM_TOOLS_TOPICINFOCHECK_HPP #include #include #include #include #include #include #include #include #include "../world/cell.hpp" #include "../world/idcollection.hpp" #include "../world/infocollection.hpp" #include "../world/refiddata.hpp" #include "../world/resources.hpp" #include "../doc/stage.hpp" namespace CSMTools { /// \brief VerifyStage: check topics class TopicInfoCheckStage : public CSMDoc::Stage { public: TopicInfoCheckStage( const CSMWorld::InfoCollection& topicInfos, const CSMWorld::IdCollection& cells, const CSMWorld::IdCollection& classes, const CSMWorld::IdCollection& factions, const CSMWorld::IdCollection& gmsts, const CSMWorld::IdCollection& globals, const CSMWorld::IdCollection& journals, const CSMWorld::IdCollection& races, const CSMWorld::IdCollection& regions, const CSMWorld::IdCollection& topics, const CSMWorld::RefIdData& referencables, const CSMWorld::Resources& soundFiles); int setup() override; ///< \return number of steps void perform(int step, CSMDoc::Messages& messages) override; ///< Messages resulting from this stage will be appended to \a messages private: const CSMWorld::InfoCollection& mTopicInfos; const CSMWorld::IdCollection& mCells; const CSMWorld::IdCollection& mClasses; const CSMWorld::IdCollection& mFactions; const CSMWorld::IdCollection& mGameSettings; const CSMWorld::IdCollection& mGlobals; const CSMWorld::IdCollection& mJournals; const CSMWorld::IdCollection& mRaces; const CSMWorld::IdCollection& mRegions; const CSMWorld::IdCollection& mTopics; const CSMWorld::RefIdData& mReferencables; const CSMWorld::Resources& mSoundFiles; std::set mCellNames; bool mIgnoreBaseRecords; // These return false when not successful and write an error bool verifyActor(const std::string& name, const CSMWorld::UniversalId& id, CSMDoc::Messages& messages); bool verifyCell(const std::string& name, const CSMWorld::UniversalId& id, CSMDoc::Messages& messages); bool verifyFactionRank(const std::string& name, int rank, const CSMWorld::UniversalId& id, CSMDoc::Messages& messages); bool verifyItem(const std::string& name, const CSMWorld::UniversalId& id, CSMDoc::Messages& messages); bool verifySelectStruct(const ESM::DialInfo::SelectStruct& select, const CSMWorld::UniversalId& id, CSMDoc::Messages& messages); bool verifySound(const std::string& name, const CSMWorld::UniversalId& id, CSMDoc::Messages& messages); template bool verifyId(const std::string& name, const CSMWorld::IdCollection& collection, const CSMWorld::UniversalId& id, CSMDoc::Messages& messages); }; } #endif ================================================ FILE: apps/opencs/model/world/actoradapter.cpp ================================================ #include "actoradapter.hpp" #include #include #include #include #include #include #include "data.hpp" namespace CSMWorld { const std::string& ActorAdapter::RaceData::getId() const { return mId; } bool ActorAdapter::RaceData::isBeast() const { return mIsBeast; } ActorAdapter::RaceData::RaceData() { mIsBeast = false; } bool ActorAdapter::RaceData::handlesPart(ESM::PartReferenceType type) const { switch (type) { case ESM::PRT_Skirt: case ESM::PRT_Shield: case ESM::PRT_RPauldron: case ESM::PRT_LPauldron: case ESM::PRT_Weapon: return false; default: return true; } } const std::string& ActorAdapter::RaceData::getFemalePart(ESM::PartReferenceType index) const { return mFemaleParts[ESM::getMeshPart(index)]; } const std::string& ActorAdapter::RaceData::getMalePart(ESM::PartReferenceType index) const { return mMaleParts[ESM::getMeshPart(index)]; } bool ActorAdapter::RaceData::hasDependency(const std::string& id) const { return mDependencies.find(id) != mDependencies.end(); } void ActorAdapter::RaceData::setFemalePart(ESM::BodyPart::MeshPart index, const std::string& partId) { mFemaleParts[index] = partId; addOtherDependency(partId); } void ActorAdapter::RaceData::setMalePart(ESM::BodyPart::MeshPart index, const std::string& partId) { mMaleParts[index] = partId; addOtherDependency(partId); } void ActorAdapter::RaceData::addOtherDependency(const std::string& id) { if (!id.empty()) mDependencies.emplace(id); } void ActorAdapter::RaceData::reset_data(const std::string& id, bool isBeast) { mId = id; mIsBeast = isBeast; for (auto& str : mFemaleParts) str.clear(); for (auto& str : mMaleParts) str.clear(); mDependencies.clear(); // Mark self as a dependency addOtherDependency(id); } ActorAdapter::ActorData::ActorData() { mCreature = false; mFemale = false; } const std::string& ActorAdapter::ActorData::getId() const { return mId; } bool ActorAdapter::ActorData::isCreature() const { return mCreature; } bool ActorAdapter::ActorData::isFemale() const { return mFemale; } std::string ActorAdapter::ActorData::getSkeleton() const { if (mCreature || !mSkeletonOverride.empty()) return "meshes\\" + mSkeletonOverride; bool firstPerson = false; bool beast = mRaceData ? mRaceData->isBeast() : false; bool werewolf = false; return SceneUtil::getActorSkeleton(firstPerson, mFemale, beast, werewolf); } const std::string ActorAdapter::ActorData::getPart(ESM::PartReferenceType index) const { auto it = mParts.find(index); if (it == mParts.end()) { if (mRaceData && mRaceData->handlesPart(index)) { if (mFemale) { // Note: we should use male parts for females as fallback const std::string femalePart = mRaceData->getFemalePart(index); if (!femalePart.empty()) return femalePart; } return mRaceData->getMalePart(index); } return ""; } const std::string& partName = it->second.first; return partName; } bool ActorAdapter::ActorData::hasDependency(const std::string& id) const { return mDependencies.find(id) != mDependencies.end(); } void ActorAdapter::ActorData::setPart(ESM::PartReferenceType index, const std::string& partId, int priority) { auto it = mParts.find(index); if (it != mParts.end()) { if (it->second.second >= priority) return; } mParts[index] = std::make_pair(partId, priority); addOtherDependency(partId); } void ActorAdapter::ActorData::addOtherDependency(const std::string& id) { if (!id.empty()) mDependencies.emplace(id); } void ActorAdapter::ActorData::reset_data(const std::string& id, const std::string& skeleton, bool isCreature, bool isFemale, RaceDataPtr raceData) { mId = id; mCreature = isCreature; mFemale = isFemale; mSkeletonOverride = skeleton; mRaceData = raceData; mParts.clear(); mDependencies.clear(); // Mark self and race as a dependency addOtherDependency(id); if (raceData) addOtherDependency(raceData->getId()); } ActorAdapter::ActorAdapter(Data& data) : mReferenceables(data.getReferenceables()) , mRaces(data.getRaces()) , mBodyParts(data.getBodyParts()) { // Setup qt slots and signals QAbstractItemModel* refModel = data.getTableModel(UniversalId::Type_Referenceable); connect(refModel, SIGNAL(rowsInserted(const QModelIndex&, int, int)), this, SLOT(handleReferenceablesInserted(const QModelIndex&, int, int))); connect(refModel, SIGNAL(dataChanged(const QModelIndex&, const QModelIndex&)), this, SLOT(handleReferenceableChanged(const QModelIndex&, const QModelIndex&))); connect(refModel, SIGNAL(rowsAboutToBeRemoved(const QModelIndex&, int, int)), this, SLOT(handleReferenceablesAboutToBeRemoved(const QModelIndex&, int, int))); QAbstractItemModel* raceModel = data.getTableModel(UniversalId::Type_Race); connect(raceModel, SIGNAL(rowsInserted(const QModelIndex&, int, int)), this, SLOT(handleRacesAboutToBeRemoved(const QModelIndex&, int, int))); connect(raceModel, SIGNAL(dataChanged(const QModelIndex&, const QModelIndex&)), this, SLOT(handleRaceChanged(const QModelIndex&, const QModelIndex&))); connect(raceModel, SIGNAL(rowsAboutToBeRemoved(const QModelIndex&, int, int)), this, SLOT(handleRacesAboutToBeRemoved(const QModelIndex&, int, int))); QAbstractItemModel* partModel = data.getTableModel(UniversalId::Type_BodyPart); connect(partModel, SIGNAL(rowsInserted(const QModelIndex&, int, int)), this, SLOT(handleBodyPartsInserted(const QModelIndex&, int, int))); connect(partModel, SIGNAL(dataChanged(const QModelIndex&, const QModelIndex&)), this, SLOT(handleBodyPartChanged(const QModelIndex&, const QModelIndex&))); connect(partModel, SIGNAL(rowsAboutToBeRemoved(const QModelIndex&, int, int)), this, SLOT(handleBodyPartsAboutToBeRemoved(const QModelIndex&, int, int))); } ActorAdapter::ActorDataPtr ActorAdapter::getActorData(const std::string& id) { // Return cached actor data if it exists ActorDataPtr data = mCachedActors.get(id); if (data) { return data; } // Create the actor data data.reset(new ActorData()); setupActor(id, data); mCachedActors.insert(id, data); return data; } void ActorAdapter::handleReferenceablesInserted(const QModelIndex& parent, int start, int end) { // Only rows added at the top level are pertinent. Others are caught by dataChanged handler. if (!parent.isValid()) { for (int row = start; row <= end; ++row) { std::string refId = mReferenceables.getId(row); markDirtyDependency(refId); } } // Update affected updateDirty(); } void ActorAdapter::handleReferenceableChanged(const QModelIndex& topLeft, const QModelIndex& botRight) { int start = getHighestIndex(topLeft).row(); int end = getHighestIndex(botRight).row(); // A change to record status (ex. Deleted) returns an invalid botRight if (end == -1) end = start; // Handle each record for (int row = start; row <= end; ++row) { std::string refId = mReferenceables.getId(row); markDirtyDependency(refId); } // Update affected updateDirty(); } void ActorAdapter::handleReferenceablesAboutToBeRemoved(const QModelIndex& parent, int start, int end) { // Only rows at the top are pertinent. if (!parent.isValid()) { for (int row = start; row <= end; ++row) { std::string refId = mReferenceables.getId(row); markDirtyDependency(refId); } } } void ActorAdapter::handleReferenceablesRemoved(const QModelIndex& parent, int start, int end) { // Changes specified in handleReferenceablesAboutToBeRemoved updateDirty(); } void ActorAdapter::handleRacesInserted(const QModelIndex& parent, int start, int end) { // Only rows added at the top are pertinent. if (!parent.isValid()) { for (int row = start; row <= end; ++row) { std::string raceId = mReferenceables.getId(row); markDirtyDependency(raceId); } } // Update affected updateDirty(); } void ActorAdapter::handleRaceChanged(const QModelIndex& topLeft, const QModelIndex& botRight) { int start = getHighestIndex(topLeft).row(); int end = getHighestIndex(botRight).row(); // A change to record status (ex. Deleted) returns an invalid botRight if (end == -1) end = start; for (int row = start; row <= end; ++row) { std::string raceId = mRaces.getId(row); markDirtyDependency(raceId); } // Update affected updateDirty(); } void ActorAdapter::handleRacesAboutToBeRemoved(const QModelIndex& parent, int start, int end) { // Only changes at the top are pertinent. if (!parent.isValid()) { for (int row = start; row <= end; ++row) { std::string raceId = mRaces.getId(row); markDirtyDependency(raceId); } } } void ActorAdapter::handleRacesRemoved(const QModelIndex& parent, int start, int end) { // Changes specified in handleRacesAboutToBeRemoved updateDirty(); } void ActorAdapter::handleBodyPartsInserted(const QModelIndex& parent, int start, int end) { // Only rows added at the top are pertinent. if (!parent.isValid()) { for (int row = start; row <= end; ++row) { // Race specified by part may need update auto& record = mBodyParts.getRecord(row); if (!record.isDeleted()) { markDirtyDependency(record.get().mRace); } std::string partId = mBodyParts.getId(row); markDirtyDependency(partId); } } // Update affected updateDirty(); } void ActorAdapter::handleBodyPartChanged(const QModelIndex& topLeft, const QModelIndex& botRight) { int start = getHighestIndex(topLeft).row(); int end = getHighestIndex(botRight).row(); // A change to record status (ex. Deleted) returns an invalid botRight if (end == -1) end = start; for (int row = start; row <= end; ++row) { // Race specified by part may need update auto& record = mBodyParts.getRecord(row); if (!record.isDeleted()) { markDirtyDependency(record.get().mRace); } // Update entries with a tracked dependency std::string partId = mBodyParts.getId(row); markDirtyDependency(partId); } // Update affected updateDirty(); } void ActorAdapter::handleBodyPartsAboutToBeRemoved(const QModelIndex& parent, int start, int end) { // Only changes at the top are pertinent. if (!parent.isValid()) { for (int row = start; row <= end; ++row) { std::string partId = mBodyParts.getId(row); markDirtyDependency(partId); } } } void ActorAdapter::handleBodyPartsRemoved(const QModelIndex& parent, int start, int end) { // Changes specified in handleBodyPartsAboutToBeRemoved updateDirty(); } QModelIndex ActorAdapter::getHighestIndex(QModelIndex index) const { while (index.parent().isValid()) index = index.parent(); return index; } bool ActorAdapter::is1stPersonPart(const std::string& name) const { return name.size() >= 4 && name.find(".1st", name.size() - 4) != std::string::npos; } ActorAdapter::RaceDataPtr ActorAdapter::getRaceData(const std::string& id) { // Return cached race data if it exists RaceDataPtr data = mCachedRaces.get(id); if (data) return data; // Create the race data data.reset(new RaceData()); setupRace(id, data); mCachedRaces.insert(id, data); return data; } void ActorAdapter::setupActor(const std::string& id, ActorDataPtr data) { int index = mReferenceables.searchId(id); if (index == -1) { // Record does not exist data->reset_data(id); emit actorChanged(id); return; } auto& record = mReferenceables.getRecord(index); if (record.isDeleted()) { // Record is deleted and therefore not accessible data->reset_data(id); emit actorChanged(id); return; } const int TypeColumn = mReferenceables.findColumnIndex(Columns::ColumnId_RecordType); int type = mReferenceables.getData(index, TypeColumn).toInt(); if (type == UniversalId::Type_Creature) { // Valid creature record setupCreature(id, data); emit actorChanged(id); } else if (type == UniversalId::Type_Npc) { // Valid npc record setupNpc(id, data); emit actorChanged(id); } else { // Wrong record type data->reset_data(id); emit actorChanged(id); } } void ActorAdapter::setupRace(const std::string& id, RaceDataPtr data) { int index = mRaces.searchId(id); if (index == -1) { // Record does not exist data->reset_data(id); return; } auto& raceRecord = mRaces.getRecord(index); if (raceRecord.isDeleted()) { // Record is deleted, so not accessible data->reset_data(id); return; } auto& race = raceRecord.get(); data->reset_data(id, race.mData.mFlags & ESM::Race::Beast); // Setup body parts for (int i = 0; i < mBodyParts.getSize(); ++i) { std::string partId = mBodyParts.getId(i); auto& partRecord = mBodyParts.getRecord(i); if (partRecord.isDeleted()) { // Record is deleted, so not accessible. continue; } auto& part = partRecord.get(); if (part.mRace == id && part.mData.mType == ESM::BodyPart::MT_Skin && !is1stPersonPart(part.mId)) { auto type = (ESM::BodyPart::MeshPart) part.mData.mPart; bool female = part.mData.mFlags & ESM::BodyPart::BPF_Female; if (female) data->setFemalePart(type, part.mId); else data->setMalePart(type, part.mId); } } } void ActorAdapter::setupNpc(const std::string& id, ActorDataPtr data) { // Common setup, record is known to exist and is not deleted int index = mReferenceables.searchId(id); auto& npc = dynamic_cast&>(mReferenceables.getRecord(index)).get(); RaceDataPtr raceData = getRaceData(npc.mRace); data->reset_data(id, "", false, !npc.isMale(), raceData); // Add head and hair data->setPart(ESM::PRT_Head, npc.mHead, 0); data->setPart(ESM::PRT_Hair, npc.mHair, 0); // Add inventory items for (auto& item : npc.mInventory.mList) { if (item.mCount <= 0) continue; std::string itemId = item.mItem; addNpcItem(itemId, data); } } void ActorAdapter::addNpcItem(const std::string& itemId, ActorDataPtr data) { int index = mReferenceables.searchId(itemId); if (index == -1) { // Item does not exist yet data->addOtherDependency(itemId); return; } auto& record = mReferenceables.getRecord(index); if (record.isDeleted()) { // Item cannot be accessed yet data->addOtherDependency(itemId); return; } // Convenience function to add a parts list to actor data auto addParts = [&](const std::vector& list, int priority) { for (auto& part : list) { std::string partId; auto partType = (ESM::PartReferenceType) part.mPart; if (data->isFemale()) partId = part.mFemale; if (partId.empty()) partId = part.mMale; data->setPart(partType, partId, priority); // An another vanilla quirk: hide hairs if an item replaces Head part if (partType == ESM::PRT_Head) data->setPart(ESM::PRT_Hair, "", priority); } }; int TypeColumn = mReferenceables.findColumnIndex(Columns::ColumnId_RecordType); int type = mReferenceables.getData(index, TypeColumn).toInt(); if (type == UniversalId::Type_Armor) { auto& armor = dynamic_cast&>(record).get(); addParts(armor.mParts.mParts, 1); // Changing parts could affect what is picked for rendering data->addOtherDependency(itemId); } else if (type == UniversalId::Type_Clothing) { auto& clothing = dynamic_cast&>(record).get(); std::vector parts; if (clothing.mData.mType == ESM::Clothing::Robe) { parts = { ESM::PRT_Groin, ESM::PRT_Skirt, ESM::PRT_RLeg, ESM::PRT_LLeg, ESM::PRT_RUpperarm, ESM::PRT_LUpperarm, ESM::PRT_RKnee, ESM::PRT_LKnee, ESM::PRT_RForearm, ESM::PRT_LForearm, ESM::PRT_Cuirass }; } else if (clothing.mData.mType == ESM::Clothing::Skirt) { parts = {ESM::PRT_Groin, ESM::PRT_RLeg, ESM::PRT_LLeg}; } std::vector reservedList; for (const auto& p : parts) { ESM::PartReference pr; pr.mPart = p; reservedList.emplace_back(pr); } int priority = parts.size(); addParts(clothing.mParts.mParts, priority); addParts(reservedList, priority); // Changing parts could affect what is picked for rendering data->addOtherDependency(itemId); } } void ActorAdapter::setupCreature(const std::string& id, ActorDataPtr data) { // Record is known to exist and is not deleted int index = mReferenceables.searchId(id); auto& creature = dynamic_cast&>(mReferenceables.getRecord(index)).get(); data->reset_data(id, creature.mModel, true); } void ActorAdapter::markDirtyDependency(const std::string& dep) { for (auto raceIt : mCachedRaces) { if (raceIt->hasDependency(dep)) mDirtyRaces.emplace(raceIt->getId()); } for (auto actorIt : mCachedActors) { if (actorIt->hasDependency(dep)) mDirtyActors.emplace(actorIt->getId()); } } void ActorAdapter::updateDirty() { // Handle races before actors, since actors are dependent on race for (auto& race : mDirtyRaces) { RaceDataPtr data = mCachedRaces.get(race); if (data) { setupRace(race, data); // Race was changed. Need to mark actor dependencies as dirty. // Cannot use markDirtyDependency because that would invalidate // the current iterator. for (auto actorIt : mCachedActors) { if (actorIt->hasDependency(race)) mDirtyActors.emplace(actorIt->getId()); } } } mDirtyRaces.clear(); for (auto& actor : mDirtyActors) { ActorDataPtr data = mCachedActors.get(actor); if (data) { setupActor(actor, data); } } mDirtyActors.clear(); } } ================================================ FILE: apps/opencs/model/world/actoradapter.hpp ================================================ #ifndef CSM_WOLRD_ACTORADAPTER_H #define CSM_WOLRD_ACTORADAPTER_H #include #include #include #include #include #include #include #include #include "refidcollection.hpp" #include "idcollection.hpp" namespace ESM { struct Race; } namespace CSMWorld { class Data; /// Adapts multiple collections to provide the data needed to render /// an npc or creature. class ActorAdapter : public QObject { Q_OBJECT public: /// A list indexed by ESM::PartReferenceType using ActorPartList = std::map>; /// A list indexed by ESM::BodyPart::MeshPart using RacePartList = std::array; /// Tracks unique strings using StringSet = std::unordered_set; /// Contains base race data shared between actors class RaceData { public: RaceData(); /// Retrieves the id of the race represented const std::string& getId() const; /// Checks if it's a beast race bool isBeast() const; /// Checks if a part could exist for the given type bool handlesPart(ESM::PartReferenceType type) const; /// Retrieves the associated body part const std::string& getFemalePart(ESM::PartReferenceType index) const; /// Retrieves the associated body part const std::string& getMalePart(ESM::PartReferenceType index) const; /// Checks if the race has a data dependency bool hasDependency(const std::string& id) const; /// Sets the associated part if it's empty and marks a dependency void setFemalePart(ESM::BodyPart::MeshPart partIndex, const std::string& partId); /// Sets the associated part if it's empty and marks a dependency void setMalePart(ESM::BodyPart::MeshPart partIndex, const std::string& partId); /// Marks an additional dependency void addOtherDependency(const std::string& id); /// Clears parts and dependencies void reset_data(const std::string& raceId, bool isBeast=false); private: bool handles(ESM::PartReferenceType type) const; std::string mId; bool mIsBeast; RacePartList mFemaleParts; RacePartList mMaleParts; StringSet mDependencies; }; using RaceDataPtr = std::shared_ptr; /// Contains all the data needed to render an actor. Tracks dependencies /// so that pertinent data changes can be checked. class ActorData { public: ActorData(); /// Retrieves the id of the actor represented const std::string& getId() const; /// Checks if the actor is a creature bool isCreature() const; /// Checks if the actor is female bool isFemale() const; /// Returns the skeleton the actor should use for attaching parts to std::string getSkeleton() const; /// Retrieves the associated actor part const std::string getPart(ESM::PartReferenceType index) const; /// Checks if the actor has a data dependency bool hasDependency(const std::string& id) const; /// Sets the actor part used and marks a dependency void setPart(ESM::PartReferenceType partIndex, const std::string& partId, int priority); /// Marks an additional dependency for the actor void addOtherDependency(const std::string& id); /// Clears race, parts, and dependencies void reset_data(const std::string& actorId, const std::string& skeleton="", bool isCreature=false, bool female=true, RaceDataPtr raceData=nullptr); private: std::string mId; bool mCreature; bool mFemale; std::string mSkeletonOverride; RaceDataPtr mRaceData; ActorPartList mParts; StringSet mDependencies; }; using ActorDataPtr = std::shared_ptr; ActorAdapter(Data& data); /// Obtains the shared data for a given actor ActorDataPtr getActorData(const std::string& refId); signals: void actorChanged(const std::string& refId); public slots: void handleReferenceablesInserted(const QModelIndex&, int, int); void handleReferenceableChanged(const QModelIndex&, const QModelIndex&); void handleReferenceablesAboutToBeRemoved(const QModelIndex&, int, int); void handleReferenceablesRemoved(const QModelIndex&, int, int); void handleRacesInserted(const QModelIndex&, int, int); void handleRaceChanged(const QModelIndex&, const QModelIndex&); void handleRacesAboutToBeRemoved(const QModelIndex&, int, int); void handleRacesRemoved(const QModelIndex&, int, int); void handleBodyPartsInserted(const QModelIndex&, int, int); void handleBodyPartChanged(const QModelIndex&, const QModelIndex&); void handleBodyPartsAboutToBeRemoved(const QModelIndex&, int, int); void handleBodyPartsRemoved(const QModelIndex&, int, int); private: ActorAdapter(const ActorAdapter&) = delete; ActorAdapter& operator=(const ActorAdapter&) = delete; QModelIndex getHighestIndex(QModelIndex) const; bool is1stPersonPart(const std::string& id) const; RaceDataPtr getRaceData(const std::string& raceId); void setupActor(const std::string& id, ActorDataPtr data); void setupRace(const std::string& id, RaceDataPtr data); void setupNpc(const std::string& id, ActorDataPtr data); void addNpcItem(const std::string& itemId, ActorDataPtr data); void setupCreature(const std::string& id, ActorDataPtr data); void markDirtyDependency(const std::string& dependency); void updateDirty(); RefIdCollection& mReferenceables; IdCollection& mRaces; IdCollection& mBodyParts; Misc::WeakCache mCachedActors; // Key: referenceable id Misc::WeakCache mCachedRaces; // Key: race id StringSet mDirtyActors; // Actors that need updating StringSet mDirtyRaces; // Races that need updating }; } #endif ================================================ FILE: apps/opencs/model/world/cell.cpp ================================================ #include "cell.hpp" #include void CSMWorld::Cell::load (ESM::ESMReader &esm, bool &isDeleted) { ESM::Cell::load (esm, isDeleted, false); mId = mName; if (isExterior()) { std::ostringstream stream; stream << "#" << mData.mX << " " << mData.mY; mId = stream.str(); } } ================================================ FILE: apps/opencs/model/world/cell.hpp ================================================ #ifndef CSM_WOLRD_CELL_H #define CSM_WOLRD_CELL_H #include #include #include namespace CSMWorld { /// \brief Wrapper for Cell record /// /// \attention The mData.mX and mData.mY fields of the ESM::Cell struct are not used. /// Exterior cell coordinates are encoded in the cell ID. struct Cell : public ESM::Cell { std::string mId; void load (ESM::ESMReader &esm, bool &isDeleted); }; } #endif ================================================ FILE: apps/opencs/model/world/cellcoordinates.cpp ================================================ #include "cellcoordinates.hpp" #include #include #include #include #include CSMWorld::CellCoordinates::CellCoordinates() : mX (0), mY (0) {} CSMWorld::CellCoordinates::CellCoordinates (int x, int y) : mX (x), mY (y) {} CSMWorld::CellCoordinates::CellCoordinates (const std::pair& coordinates) : mX (coordinates.first), mY (coordinates.second) {} int CSMWorld::CellCoordinates::getX() const { return mX; } int CSMWorld::CellCoordinates::getY() const { return mY; } CSMWorld::CellCoordinates CSMWorld::CellCoordinates::move (int x, int y) const { return CellCoordinates (mX + x, mY + y); } std::string CSMWorld::CellCoordinates::getId (const std::string& worldspace) const { // we ignore the worldspace for now, since there is only one (will change in 1.1) return generateId(mX, mY); } std::string CSMWorld::CellCoordinates::generateId (int x, int y) { std::string cellId = "#" + std::to_string(x) + " " + std::to_string(y); return cellId; } bool CSMWorld::CellCoordinates::isExteriorCell (const std::string& id) { return (!id.empty() && id[0]=='#'); } std::pair CSMWorld::CellCoordinates::fromId ( const std::string& id) { // no worldspace for now, needs to be changed for 1.1 if (isExteriorCell(id)) { int x, y; char ignore; std::istringstream stream (id); if (stream >> ignore >> x >> y) return std::make_pair (CellCoordinates (x, y), true); } return std::make_pair (CellCoordinates(), false); } std::pair CSMWorld::CellCoordinates::coordinatesToCellIndex (float x, float y) { return std::make_pair (std::floor (x / Constants::CellSizeInUnits), std::floor (y / Constants::CellSizeInUnits)); } std::pair CSMWorld::CellCoordinates::toTextureCoords(const osg::Vec3d& worldPos) { const auto xd = static_cast(worldPos.x() * ESM::Land::LAND_TEXTURE_SIZE / ESM::Land::REAL_SIZE - 0.25f); const auto yd = static_cast(worldPos.y() * ESM::Land::LAND_TEXTURE_SIZE / ESM::Land::REAL_SIZE + 0.25f); const auto x = static_cast(std::floor(xd)); const auto y = static_cast(std::floor(yd)); return std::make_pair(x, y); } std::pair CSMWorld::CellCoordinates::toVertexCoords(const osg::Vec3d& worldPos) { const auto xd = static_cast(worldPos.x() * (ESM::Land::LAND_SIZE - 1) / ESM::Land::REAL_SIZE + 0.5f); const auto yd = static_cast(worldPos.y() * (ESM::Land::LAND_SIZE - 1) / ESM::Land::REAL_SIZE + 0.5f); const auto x = static_cast(std::floor(xd)); const auto y = static_cast(std::floor(yd)); return std::make_pair(x, y); } float CSMWorld::CellCoordinates::textureGlobalXToWorldCoords(int textureGlobal) { return ESM::Land::REAL_SIZE * (static_cast(textureGlobal) + 0.25f) / ESM::Land::LAND_TEXTURE_SIZE; } float CSMWorld::CellCoordinates::textureGlobalYToWorldCoords(int textureGlobal) { return ESM::Land::REAL_SIZE * (static_cast(textureGlobal) - 0.25f) / ESM::Land::LAND_TEXTURE_SIZE; } float CSMWorld::CellCoordinates::vertexGlobalToWorldCoords(int vertexGlobal) { return ESM::Land::REAL_SIZE * static_cast(vertexGlobal) / (ESM::Land::LAND_SIZE - 1); } int CSMWorld::CellCoordinates::vertexGlobalToInCellCoords(int vertexGlobal) { return static_cast(vertexGlobal - std::floor(static_cast(vertexGlobal) / (ESM::Land::LAND_SIZE - 1)) * (ESM::Land::LAND_SIZE - 1)); } std::string CSMWorld::CellCoordinates::textureGlobalToCellId(const std::pair& textureGlobal) { int x = std::floor(static_cast(textureGlobal.first) / ESM::Land::LAND_TEXTURE_SIZE); int y = std::floor(static_cast(textureGlobal.second) / ESM::Land::LAND_TEXTURE_SIZE); return generateId(x, y); } std::string CSMWorld::CellCoordinates::vertexGlobalToCellId(const std::pair& vertexGlobal) { int x = std::floor(static_cast(vertexGlobal.first) / (ESM::Land::LAND_SIZE - 1)); int y = std::floor(static_cast(vertexGlobal.second) / (ESM::Land::LAND_SIZE - 1)); return generateId(x, y); } bool CSMWorld::operator== (const CellCoordinates& left, const CellCoordinates& right) { return left.getX()==right.getX() && left.getY()==right.getY(); } bool CSMWorld::operator!= (const CellCoordinates& left, const CellCoordinates& right) { return !(left==right); } bool CSMWorld::operator< (const CellCoordinates& left, const CellCoordinates& right) { if (left.getX()right.getX()) return false; return left.getY() #include #include #include #include namespace CSMWorld { class CellCoordinates { int mX; int mY; public: CellCoordinates(); CellCoordinates (int x, int y); CellCoordinates (const std::pair& coordinates); int getX() const; int getY() const; CellCoordinates move (int x, int y) const; ///< Return a copy of *this, moved by the given offset. ///Generate cell id string from x and y coordinates static std::string generateId (int x, int y); std::string getId (const std::string& worldspace) const; ///< Return the ID for the cell at these coordinates. static bool isExteriorCell (const std::string& id); /// \return first: CellCoordinates (or 0, 0 if cell does not have coordinates), /// second: is cell paged? /// /// \note The worldspace part of \a id is ignored static std::pair fromId (const std::string& id); /// \return cell coordinates such that given world coordinates are in it. static std::pair coordinatesToCellIndex (float x, float y); ///Converts worldspace coordinates to global texture selection, taking in account the texture offset. static std::pair toTextureCoords(const osg::Vec3d& worldPos); ///Converts worldspace coordinates to global vertex selection. static std::pair toVertexCoords(const osg::Vec3d& worldPos); ///Converts global texture coordinate X to worldspace coordinate, offset by 0.25f. static float textureGlobalXToWorldCoords(int textureGlobal); ///Converts global texture coordinate Y to worldspace coordinate, offset by 0.25f. static float textureGlobalYToWorldCoords(int textureGlobal); ///Converts global vertex coordinate to worldspace coordinate static float vertexGlobalToWorldCoords(int vertexGlobal); ///Converts global vertex coordinate to local cell's heightmap coordinates static int vertexGlobalToInCellCoords(int vertexGlobal); ///Converts global texture coordinates to cell id static std::string textureGlobalToCellId(const std::pair& textureGlobal); ///Converts global vertex coordinates to cell id static std::string vertexGlobalToCellId(const std::pair& vertexGlobal); }; bool operator== (const CellCoordinates& left, const CellCoordinates& right); bool operator!= (const CellCoordinates& left, const CellCoordinates& right); bool operator< (const CellCoordinates& left, const CellCoordinates& right); std::ostream& operator<< (std::ostream& stream, const CellCoordinates& coordiantes); } Q_DECLARE_METATYPE (CSMWorld::CellCoordinates) #endif ================================================ FILE: apps/opencs/model/world/cellselection.cpp ================================================ #include "cellselection.hpp" #include #include #include CSMWorld::CellSelection::Iterator CSMWorld::CellSelection::begin() const { return mCells.begin(); } CSMWorld::CellSelection::Iterator CSMWorld::CellSelection::end() const { return mCells.end(); } bool CSMWorld::CellSelection::add (const CellCoordinates& coordinates) { return mCells.insert (coordinates).second; } void CSMWorld::CellSelection::remove (const CellCoordinates& coordinates) { mCells.erase (coordinates); } bool CSMWorld::CellSelection::has (const CellCoordinates& coordinates) const { return mCells.find (coordinates)!=end(); } int CSMWorld::CellSelection::getSize() const { return mCells.size(); } CSMWorld::CellCoordinates CSMWorld::CellSelection::getCentre() const { if (mCells.empty()) throw std::logic_error ("call of getCentre on empty cell selection"); double x = 0; double y = 0; for (Iterator iter = begin(); iter!=end(); ++iter) { x += iter->getX(); y += iter->getY(); } x /= mCells.size(); y /= mCells.size(); Iterator closest = begin(); double distance = std::numeric_limits::max(); for (Iterator iter (begin()); iter!=end(); ++iter) { double deltaX = x - iter->getX(); double deltaY = y - iter->getY(); double delta = std::sqrt (deltaX * deltaX + deltaY * deltaY); if (deltamove (x, y)); mCells.swap (moved); } ================================================ FILE: apps/opencs/model/world/cellselection.hpp ================================================ #ifndef CSM_WOLRD_CELLSELECTION_H #define CSM_WOLRD_CELLSELECTION_H #include #include #include "cellcoordinates.hpp" namespace CSMWorld { /// \brief Selection of cells in a paged worldspace /// /// \note The CellSelection does not specify the worldspace it applies to. class CellSelection { public: typedef std::set Container; typedef Container::const_iterator Iterator; private: Container mCells; public: Iterator begin() const; Iterator end() const; bool add (const CellCoordinates& coordinates); ///< Ignored if the cell specified by \a coordinates is already part of the selection. /// /// \return Was a cell added to the collection? void remove (const CellCoordinates& coordinates); ///< ignored if the cell specified by \a coordinates is not part of the selection. bool has (const CellCoordinates& coordinates) const; ///< \return Is the cell specified by \a coordinates part of the selection? int getSize() const; ///< Return number of cells. CellCoordinates getCentre() const; ///< Return the selected cell that is closest to the geometric centre of the selection. /// /// \attention This function must not be called on selections that are empty. void move (int x, int y); }; } Q_DECLARE_METATYPE (CSMWorld::CellSelection) #endif ================================================ FILE: apps/opencs/model/world/collection.hpp ================================================ #ifndef CSM_WOLRD_COLLECTION_H #define CSM_WOLRD_COLLECTION_H #include #include #include #include #include #include #include #include #include #include "columnbase.hpp" #include "collectionbase.hpp" #include "land.hpp" #include "landtexture.hpp" #include "ref.hpp" namespace CSMWorld { /// \brief Access to ID field in records template struct IdAccessor { void setId(ESXRecordT& record, const std::string& id) const; const std::string getId (const ESXRecordT& record) const; }; template void IdAccessor::setId(ESXRecordT& record, const std::string& id) const { record.mId = id; } template const std::string IdAccessor::getId (const ESXRecordT& record) const { return record.mId; } template<> inline void IdAccessor::setId (Land& record, const std::string& id) const { int x=0, y=0; Land::parseUniqueRecordId(id, x, y); record.mX = x; record.mY = y; } template<> inline void IdAccessor::setId (LandTexture& record, const std::string& id) const { int plugin = 0; int index = 0; LandTexture::parseUniqueRecordId(id, plugin, index); record.mPluginIndex = plugin; record.mIndex = index; } template<> inline const std::string IdAccessor::getId (const Land& record) const { return Land::createUniqueRecordId(record.mX, record.mY); } template<> inline const std::string IdAccessor::getId (const LandTexture& record) const { return LandTexture::createUniqueRecordId(record.mPluginIndex, record.mIndex); } /// \brief Single-type record collection template > class Collection : public CollectionBase { public: typedef ESXRecordT ESXRecord; private: std::vector > mRecords; std::map mIndex; std::vector *> mColumns; // not implemented Collection (const Collection&); Collection& operator= (const Collection&); protected: const std::map& getIdMap() const; const std::vector >& getRecords() const; bool reorderRowsImp (int baseIndex, const std::vector& newOrder); ///< Reorder the rows [baseIndex, baseIndex+newOrder.size()) according to the indices /// given in \a newOrder (baseIndex+newOrder[0] specifies the new index of row baseIndex). /// /// \return Success? int cloneRecordImp (const std::string& origin, const std::string& dest, UniversalId::Type type); ///< Returns the index of the clone. int touchRecordImp (const std::string& id); ///< Returns the index of the record on success, -1 on failure. public: Collection(); virtual ~Collection(); void add (const ESXRecordT& record); ///< Add a new record (modified) int getSize() const override; std::string getId (int index) const override; int getIndex (const std::string& id) const override; int getColumns() const override; QVariant getData (int index, int column) const override; void setData (int index, int column, const QVariant& data) override; const ColumnBase& getColumn (int column) const override; virtual void merge(); ///< Merge modified into base. virtual void purge(); ///< Remove records that are flagged as erased. void removeRows (int index, int count) override; void appendBlankRecord (const std::string& id, UniversalId::Type type = UniversalId::Type_None) override; ///< \param type Will be ignored, unless the collection supports multiple record types void cloneRecord(const std::string& origin, const std::string& destination, const UniversalId::Type type) override; bool touchRecord(const std::string& id) override; ///< Change the state of a record from base to modified, if it is not already. /// \return True if the record was changed. int searchId (const std::string& id) const override; ////< Search record with \a id. /// \return index of record (if found) or -1 (not found) void replace (int index, const RecordBase& record) override; ///< If the record type does not match, an exception is thrown. /// /// \attention \a record must not change the ID. void appendRecord (const RecordBase& record, UniversalId::Type type = UniversalId::Type_None) override; ///< If the record type does not match, an exception is thrown. ///< \param type Will be ignored, unless the collection supports multiple record types const Record& getRecord (const std::string& id) const override; const Record& getRecord (int index) const override; int getAppendIndex (const std::string& id, UniversalId::Type type = UniversalId::Type_None) const override; ///< \param type Will be ignored, unless the collection supports multiple record types std::vector getIds (bool listDeleted = true) const override; ///< Return a sorted collection of all IDs /// /// \param listDeleted include deleted record in the list virtual void insertRecord (const RecordBase& record, int index, UniversalId::Type type = UniversalId::Type_None); ///< Insert record before index. /// /// If the record type does not match, an exception is thrown. /// /// If the index is invalid either generally (by being out of range) or for the particular /// record, an exception is thrown. bool reorderRows (int baseIndex, const std::vector& newOrder) override; ///< Reorder the rows [baseIndex, baseIndex+newOrder.size()) according to the indices /// given in \a newOrder (baseIndex+newOrder[0] specifies the new index of row baseIndex). /// /// \return Success? void addColumn (Column *column); void setRecord (int index, const Record& record); ///< \attention This function must not change the ID. NestableColumn *getNestableColumn (int column) const; }; template const std::map& Collection::getIdMap() const { return mIndex; } template const std::vector >& Collection::getRecords() const { return mRecords; } template bool Collection::reorderRowsImp (int baseIndex, const std::vector& newOrder) { if (!newOrder.empty()) { int size = static_cast (newOrder.size()); // check that all indices are present std::vector test (newOrder); std::sort (test.begin(), test.end()); if (*test.begin()!=0 || *--test.end()!=size-1) return false; // reorder records std::vector > buffer (size); for (int i=0; i::iterator iter (mIndex.begin()); iter!=mIndex.end(); ++iter) if (iter->second>=baseIndex && iter->secondsecond = newOrder.at (iter->second-baseIndex)+baseIndex; } return true; } template int Collection::cloneRecordImp(const std::string& origin, const std::string& destination, UniversalId::Type type) { Record copy; copy.mModified = getRecord(origin).get(); copy.mState = RecordBase::State_ModifiedOnly; IdAccessorT().setId(copy.get(), destination); if (type == UniversalId::Type_Reference) { CSMWorld::CellRef* ptr = (CSMWorld::CellRef*) ©.mModified; ptr->mRefNum.mIndex = 0; } int index = getAppendIndex(destination, type); insertRecord(copy, getAppendIndex(destination, type)); return index; } template int Collection::touchRecordImp(const std::string& id) { int index = getIndex(id); Record& record = mRecords.at(index); if (record.isDeleted()) { throw std::runtime_error("attempt to touch deleted record"); } if (!record.isModified()) { record.setModified(record.get()); return index; } return -1; } template void Collection::cloneRecord(const std::string& origin, const std::string& destination, const UniversalId::Type type) { cloneRecordImp(origin, destination, type); } template<> inline void Collection >::cloneRecord(const std::string& origin, const std::string& destination, const UniversalId::Type type) { int index = cloneRecordImp(origin, destination, type); mRecords.at(index).get().mPlugin = 0; } template bool Collection::touchRecord(const std::string& id) { return touchRecordImp(id) != -1; } template<> inline bool Collection >::touchRecord(const std::string& id) { int index = touchRecordImp(id); if (index >= 0) { mRecords.at(index).get().mPlugin = 0; return true; } return false; } template Collection::Collection() {} template Collection::~Collection() { for (typename std::vector *>::iterator iter (mColumns.begin()); iter!=mColumns.end(); ++iter) delete *iter; } template void Collection::add (const ESXRecordT& record) { std::string id = Misc::StringUtils::lowerCase (IdAccessorT().getId (record)); std::map::iterator iter = mIndex.find (id); if (iter==mIndex.end()) { Record record2; record2.mState = Record::State_ModifiedOnly; record2.mModified = record; insertRecord (record2, getAppendIndex (id)); } else { mRecords[iter->second].setModified (record); } } template int Collection::getSize() const { return mRecords.size(); } template std::string Collection::getId (int index) const { return IdAccessorT().getId (mRecords.at (index).get()); } template int Collection::getIndex (const std::string& id) const { int index = searchId (id); if (index==-1) throw std::runtime_error ("invalid ID: " + id); return index; } template int Collection::getColumns() const { return mColumns.size(); } template QVariant Collection::getData (int index, int column) const { return mColumns.at (column)->get (mRecords.at (index)); } template void Collection::setData (int index, int column, const QVariant& data) { return mColumns.at (column)->set (mRecords.at (index), data); } template const ColumnBase& Collection::getColumn (int column) const { return *mColumns.at (column); } template NestableColumn *Collection::getNestableColumn (int column) const { if (column < 0 || column >= static_cast(mColumns.size())) throw std::runtime_error("column index out of range"); return mColumns.at (column); } template void Collection::addColumn (Column *column) { mColumns.push_back (column); } template void Collection::merge() { for (typename std::vector >::iterator iter (mRecords.begin()); iter!=mRecords.end(); ++iter) iter->merge(); purge(); } template void Collection::purge() { int i = 0; while (i (mRecords.size())) { if (mRecords[i].isErased()) removeRows (i, 1); else ++i; } } template void Collection::removeRows (int index, int count) { mRecords.erase (mRecords.begin()+index, mRecords.begin()+index+count); typename std::map::iterator iter = mIndex.begin(); while (iter!=mIndex.end()) { if (iter->second>=index) { if (iter->second>=index+count) { iter->second -= count; ++iter; } else { mIndex.erase (iter++); } } else ++iter; } } template void Collection::appendBlankRecord (const std::string& id, UniversalId::Type type) { ESXRecordT record; IdAccessorT().setId(record, id); record.blank(); Record record2; record2.mState = Record::State_ModifiedOnly; record2.mModified = record; insertRecord (record2, getAppendIndex (id, type), type); } template int Collection::searchId (const std::string& id) const { std::string id2 = Misc::StringUtils::lowerCase(id); std::map::const_iterator iter = mIndex.find (id2); if (iter==mIndex.end()) return -1; return iter->second; } template void Collection::replace (int index, const RecordBase& record) { mRecords.at (index) = dynamic_cast&> (record); } template void Collection::appendRecord (const RecordBase& record, UniversalId::Type type) { insertRecord (record, getAppendIndex (IdAccessorT().getId ( dynamic_cast&> (record).get()), type), type); } template int Collection::getAppendIndex (const std::string& id, UniversalId::Type type) const { return static_cast (mRecords.size()); } template std::vector Collection::getIds (bool listDeleted) const { std::vector ids; for (typename std::map::const_iterator iter = mIndex.begin(); iter!=mIndex.end(); ++iter) { if (listDeleted || !mRecords[iter->second].isDeleted()) ids.push_back (IdAccessorT().getId (mRecords[iter->second].get())); } return ids; } template const Record& Collection::getRecord (const std::string& id) const { int index = getIndex (id); return mRecords.at (index); } template const Record& Collection::getRecord (int index) const { return mRecords.at (index); } template void Collection::insertRecord (const RecordBase& record, int index, UniversalId::Type type) { if (index<0 || index>static_cast (mRecords.size())) throw std::runtime_error ("index out of range"); const Record& record2 = dynamic_cast&> (record); mRecords.insert (mRecords.begin()+index, record2); if (index (mRecords.size())-1) { for (std::map::iterator iter (mIndex.begin()); iter!=mIndex.end(); ++iter) if (iter->second>=index) ++(iter->second); } mIndex.insert (std::make_pair (Misc::StringUtils::lowerCase (IdAccessorT().getId ( record2.get())), index)); } template void Collection::setRecord (int index, const Record& record) { if (Misc::StringUtils::lowerCase (IdAccessorT().getId (mRecords.at (index).get()))!= Misc::StringUtils::lowerCase (IdAccessorT().getId (record.get()))) throw std::runtime_error ("attempt to change the ID of a record"); mRecords.at (index) = record; } template bool Collection::reorderRows (int baseIndex, const std::vector& newOrder) { return false; } } #endif ================================================ FILE: apps/opencs/model/world/collectionbase.cpp ================================================ #include "collectionbase.hpp" #include #include "columnbase.hpp" CSMWorld::CollectionBase::CollectionBase() {} CSMWorld::CollectionBase::~CollectionBase() {} int CSMWorld::CollectionBase::searchColumnIndex (Columns::ColumnId id) const { int columns = getColumns(); for (int i=0; i #include #include "universalid.hpp" #include "columns.hpp" class QVariant; namespace CSMWorld { struct ColumnBase; struct RecordBase; /// \brief Base class for record collections /// /// \attention Modifying records through the interface does not update connected views. /// Such modifications should be done through the table model interface instead unless no views /// are connected to the model or special precautions have been taken to send update signals /// manually. class CollectionBase { // not implemented CollectionBase (const CollectionBase&); CollectionBase& operator= (const CollectionBase&); public: CollectionBase(); virtual ~CollectionBase(); virtual int getSize() const = 0; virtual std::string getId (int index) const = 0; virtual int getIndex (const std::string& id) const = 0; virtual int getColumns() const = 0; virtual const ColumnBase& getColumn (int column) const = 0; virtual QVariant getData (int index, int column) const = 0; virtual void setData (int index, int column, const QVariant& data) = 0; // Not in use. Temporarily removed so that the implementation of RefIdCollection can continue without // these functions for now. // virtual void merge() = 0; ///< Merge modified into base. // virtual void purge() = 0; ///< Remove records that are flagged as erased. virtual void removeRows (int index, int count) = 0; virtual void appendBlankRecord (const std::string& id, UniversalId::Type type = UniversalId::Type_None) = 0; ///< \param type Will be ignored, unless the collection supports multiple record types virtual int searchId (const std::string& id) const = 0; ////< Search record with \a id. /// \return index of record (if found) or -1 (not found) virtual void replace (int index, const RecordBase& record) = 0; ///< If the record type does not match, an exception is thrown. /// /// \attention \a record must not change the ID. ///< \param type Will be ignored, unless the collection supports multiple record types virtual void appendRecord (const RecordBase& record, UniversalId::Type type = UniversalId::Type_None) = 0; ///< If the record type does not match, an exception is thrown. virtual void cloneRecord(const std::string& origin, const std::string& destination, const UniversalId::Type type) = 0; virtual bool touchRecord(const std::string& id) = 0; virtual const RecordBase& getRecord (const std::string& id) const = 0; virtual const RecordBase& getRecord (int index) const = 0; virtual int getAppendIndex (const std::string& id, UniversalId::Type type = UniversalId::Type_None) const = 0; ///< \param type Will be ignored, unless the collection supports multiple record types virtual std::vector getIds (bool listDeleted = true) const = 0; ///< Return a sorted collection of all IDs /// /// \param listDeleted include deleted record in the list virtual bool reorderRows (int baseIndex, const std::vector& newOrder) = 0; ///< Reorder the rows [baseIndex, baseIndex+newOrder.size()) according to the indices /// given in \a newOrder (baseIndex+newOrder[0] specifies the new index of row baseIndex). /// /// \return Success? int searchColumnIndex (Columns::ColumnId id) const; ///< Return index of column with the given \a id. If no such column exists, -1 is returned. int findColumnIndex (Columns::ColumnId id) const; ///< Return index of column with the given \a id. If no such column exists, an exception is /// thrown. }; } #endif ================================================ FILE: apps/opencs/model/world/columnbase.cpp ================================================ #include "columnbase.hpp" #include "columns.hpp" CSMWorld::ColumnBase::ColumnBase (int columnId, Display displayType, int flags) : mColumnId (columnId), mFlags (flags), mDisplayType (displayType) {} CSMWorld::ColumnBase::~ColumnBase() {} bool CSMWorld::ColumnBase::isUserEditable() const { return isEditable(); } std::string CSMWorld::ColumnBase::getTitle() const { return Columns::getName (static_cast (mColumnId)); } int CSMWorld::ColumnBase::getId() const { return mColumnId; } bool CSMWorld::ColumnBase::isId (Display display) { static const Display ids[] = { Display_Skill, Display_Class, Display_Faction, Display_Rank, Display_Race, Display_Sound, Display_Region, Display_Birthsign, Display_Spell, Display_Cell, Display_Referenceable, Display_Activator, Display_Potion, Display_Apparatus, Display_Armor, Display_Book, Display_Clothing, Display_Container, Display_Creature, Display_Door, Display_Ingredient, Display_CreatureLevelledList, Display_ItemLevelledList, Display_Light, Display_Lockpick, Display_Miscellaneous, Display_Npc, Display_Probe, Display_Repair, Display_Static, Display_Weapon, Display_Reference, Display_Filter, Display_Topic, Display_Journal, Display_TopicInfo, Display_JournalInfo, Display_Scene, Display_GlobalVariable, Display_BodyPart, Display_Enchantment, Display_Script, Display_Mesh, Display_Icon, Display_Music, Display_SoundRes, Display_Texture, Display_Video, Display_Id, Display_SkillId, Display_EffectRange, Display_EffectId, Display_PartRefType, Display_AiPackageType, Display_InfoCondFunc, Display_InfoCondVar, Display_InfoCondComp, Display_EffectSkill, Display_EffectAttribute, Display_IngredEffectId, Display_None }; for (int i=0; ids[i]!=Display_None; ++i) if (ids[i]==display) return true; return false; } bool CSMWorld::ColumnBase::isText (Display display) { return display==Display_String || display==Display_LongString || display==Display_String32 || display==Display_LongString256; } bool CSMWorld::ColumnBase::isScript (Display display) { return display==Display_ScriptFile || display==Display_ScriptLines; } void CSMWorld::NestableColumn::addColumn(CSMWorld::NestableColumn *column) { mNestedColumns.push_back(column); } const CSMWorld::ColumnBase& CSMWorld::NestableColumn::nestedColumn(int subColumn) const { if (mNestedColumns.empty()) throw std::logic_error("Tried to access nested column of the non-nest column"); return *mNestedColumns.at(subColumn); } CSMWorld::NestableColumn::NestableColumn(int columnId, CSMWorld::ColumnBase::Display displayType, int flag) : CSMWorld::ColumnBase(columnId, displayType, flag) {} CSMWorld::NestableColumn::~NestableColumn() { for (unsigned int i = 0; i < mNestedColumns.size(); ++i) { delete mNestedColumns[i]; } } bool CSMWorld::NestableColumn::hasChildren() const { return !mNestedColumns.empty(); } CSMWorld::NestedChildColumn::NestedChildColumn (int id, CSMWorld::ColumnBase::Display display, int flags, bool isEditable) : NestableColumn (id, display, flags) , mIsEditable(isEditable) {} bool CSMWorld::NestedChildColumn::isEditable () const { return mIsEditable; } ================================================ FILE: apps/opencs/model/world/columnbase.hpp ================================================ #ifndef CSM_WOLRD_COLUMNBASE_H #define CSM_WOLRD_COLUMNBASE_H #include #include #include #include #include #include "record.hpp" namespace CSMWorld { struct ColumnBase { enum TableEditModes { TableEdit_None, // no editing TableEdit_Full, // edit cells and add/remove rows TableEdit_FixedRows // edit cells only }; enum Roles { Role_Flags = Qt::UserRole, Role_Display = Qt::UserRole+1, Role_ColumnId = Qt::UserRole+2 }; enum Flags { Flag_Table = 1, // column should be displayed in table view Flag_Dialogue = 2, // column should be displayed in dialogue view Flag_Dialogue_List = 4, // column should be diaplyed in dialogue view Flag_Dialogue_Refresh = 8 // refresh dialogue view if this column is modified }; enum Display { Display_None, //Do not use Display_String, Display_LongString, //CONCRETE TYPES STARTS HERE (for drag and drop) Display_Skill, Display_Class, Display_Faction, Display_Rank, Display_Race, Display_Sound, Display_Region, Display_Birthsign, Display_Spell, Display_Cell, Display_Referenceable, Display_Activator, Display_Potion, Display_Apparatus, Display_Armor, Display_Book, Display_Clothing, Display_Container, Display_Creature, Display_Door, Display_Ingredient, Display_CreatureLevelledList, Display_ItemLevelledList, Display_Light, Display_Lockpick, Display_Miscellaneous, Display_Npc, Display_Probe, Display_Repair, Display_Static, Display_Weapon, Display_Reference, Display_Filter, Display_Topic, Display_Journal, Display_TopicInfo, Display_JournalInfo, Display_Scene, Display_GlobalVariable, Display_BodyPart, Display_Enchantment, //CONCRETE TYPES ENDS HERE Display_SignedInteger8, Display_SignedInteger16, Display_UnsignedInteger8, Display_UnsignedInteger16, Display_Integer, Display_Float, Display_Double, Display_Var, Display_GmstVarType, Display_GlobalVarType, Display_Specialisation, Display_Attribute, Display_Boolean, Display_SpellType, Display_Script, Display_ApparatusType, Display_ArmorType, Display_ClothingType, Display_CreatureType, Display_WeaponType, Display_RecordState, Display_RefRecordType, Display_DialogueType, Display_QuestStatusType, Display_EnchantmentType, Display_BodyPartType, Display_MeshType, Display_Gender, Display_Mesh, Display_Icon, Display_Music, Display_SoundRes, Display_Texture, Display_Video, Display_Colour, Display_ScriptFile, Display_ScriptLines, // console context Display_SoundGeneratorType, Display_School, Display_Id, Display_SkillId, Display_EffectRange, Display_EffectId, Display_PartRefType, Display_AiPackageType, Display_InfoCondFunc, Display_InfoCondVar, Display_InfoCondComp, Display_String32, Display_LongString256, Display_BookType, Display_BloodType, Display_EmitterType, Display_EffectSkill, // must display at least one, unlike Display_Skill Display_EffectAttribute, // must display at least one, unlike Display_Attribute Display_IngredEffectId, // display none allowed, unlike Display_EffectId Display_GenderNpc, // must display at least one, unlike Display_Gender //top level columns that nest other columns Display_NestedHeader }; int mColumnId; int mFlags; Display mDisplayType; ColumnBase (int columnId, Display displayType, int flag); virtual ~ColumnBase(); virtual bool isEditable() const = 0; virtual bool isUserEditable() const; ///< Can this column be edited directly by the user? virtual std::string getTitle() const; virtual int getId() const; static bool isId (Display display); static bool isText (Display display); static bool isScript (Display display); }; class NestableColumn : public ColumnBase { std::vector mNestedColumns; public: NestableColumn(int columnId, Display displayType, int flag); ~NestableColumn(); void addColumn(CSMWorld::NestableColumn *column); const ColumnBase& nestedColumn(int subColumn) const; bool hasChildren() const; }; template struct Column : public NestableColumn { Column (int columnId, Display displayType, int flags = Flag_Table | Flag_Dialogue) : NestableColumn (columnId, displayType, flags) {} virtual QVariant get (const Record& record) const = 0; virtual void set (Record& record, const QVariant& data) { throw std::logic_error ("Column " + getTitle() + " is not editable"); } }; template struct NestedParentColumn : public Column { NestedParentColumn (int id, int flags = ColumnBase::Flag_Dialogue, bool fixedRows = false) : Column (id, ColumnBase::Display_NestedHeader, flags), mFixedRows(fixedRows) {} void set (Record& record, const QVariant& data) override { // There is nothing to do here. // This prevents exceptions from parent's implementation } QVariant get (const Record& record) const override { // by default editable; also see IdTree::hasChildren() if (mFixedRows) return QVariant::fromValue(ColumnBase::TableEdit_FixedRows); else return QVariant::fromValue(ColumnBase::TableEdit_Full); } bool isEditable() const override { return true; } private: bool mFixedRows; }; struct NestedChildColumn : public NestableColumn { NestedChildColumn (int id, Display display, int flags = ColumnBase::Flag_Dialogue, bool isEditable = true); bool isEditable() const override; private: bool mIsEditable; }; } Q_DECLARE_METATYPE(CSMWorld::ColumnBase::TableEditModes) #endif ================================================ FILE: apps/opencs/model/world/columnimp.cpp ================================================ #include "columnimp.hpp" #include #include namespace CSMWorld { /* LandTextureNicknameColumn */ LandTextureNicknameColumn::LandTextureNicknameColumn() : Column(Columns::ColumnId_TextureNickname, ColumnBase::Display_String) { } QVariant LandTextureNicknameColumn::get(const Record& record) const { return QString::fromUtf8(record.get().mId.c_str()); } void LandTextureNicknameColumn::set(Record& record, const QVariant& data) { LandTexture copy = record.get(); copy.mId = data.toString().toUtf8().constData(); record.setModified(copy); } bool LandTextureNicknameColumn::isEditable() const { return true; } /* LandTextureIndexColumn */ LandTextureIndexColumn::LandTextureIndexColumn() : Column(Columns::ColumnId_TextureIndex, ColumnBase::Display_Integer) { } QVariant LandTextureIndexColumn::get(const Record& record) const { return record.get().mIndex; } bool LandTextureIndexColumn::isEditable() const { return false; } /* LandPluginIndexColumn */ LandPluginIndexColumn::LandPluginIndexColumn() : Column(Columns::ColumnId_PluginIndex, ColumnBase::Display_Integer, 0) { } QVariant LandPluginIndexColumn::get(const Record& record) const { return record.get().mPlugin; } bool LandPluginIndexColumn::isEditable() const { return false; } /* LandTexturePluginIndexColumn */ LandTexturePluginIndexColumn::LandTexturePluginIndexColumn() : Column(Columns::ColumnId_PluginIndex, ColumnBase::Display_Integer, 0) { } QVariant LandTexturePluginIndexColumn::get(const Record& record) const { return record.get().mPluginIndex; } bool LandTexturePluginIndexColumn::isEditable() const { return false; } /* LandNormalsColumn */ LandNormalsColumn::LandNormalsColumn() : Column(Columns::ColumnId_LandNormalsIndex, ColumnBase::Display_String, 0) { } QVariant LandNormalsColumn::get(const Record& record) const { const int Size = Land::LAND_NUM_VERTS * 3; const Land& land = record.get(); DataType values(Size, 0); if (land.isDataLoaded(Land::DATA_VNML)) { for (int i = 0; i < Size; ++i) values[i] = land.getLandData()->mNormals[i]; } QVariant variant; variant.setValue(values); return variant; } void LandNormalsColumn::set(Record& record, const QVariant& data) { DataType values = data.value(); if (values.size() != Land::LAND_NUM_VERTS * 3) throw std::runtime_error("invalid land normals data"); Land copy = record.get(); copy.add(Land::DATA_VNML); for (int i = 0; i < values.size(); ++i) { copy.getLandData()->mNormals[i] = values[i]; } record.setModified(copy); } bool LandNormalsColumn::isEditable() const { return true; } /* LandHeightsColumn */ LandHeightsColumn::LandHeightsColumn() : Column(Columns::ColumnId_LandHeightsIndex, ColumnBase::Display_String, 0) { } QVariant LandHeightsColumn::get(const Record& record) const { const int Size = Land::LAND_NUM_VERTS; const Land& land = record.get(); DataType values(Size, 0); if (land.isDataLoaded(Land::DATA_VHGT)) { for (int i = 0; i < Size; ++i) values[i] = land.getLandData()->mHeights[i]; } QVariant variant; variant.setValue(values); return variant; } void LandHeightsColumn::set(Record& record, const QVariant& data) { DataType values = data.value(); if (values.size() != Land::LAND_NUM_VERTS) throw std::runtime_error("invalid land heights data"); Land copy = record.get(); copy.add(Land::DATA_VHGT); for (int i = 0; i < values.size(); ++i) { copy.getLandData()->mHeights[i] = values[i]; } record.setModified(copy); } bool LandHeightsColumn::isEditable() const { return true; } /* LandColoursColumn */ LandColoursColumn::LandColoursColumn() : Column(Columns::ColumnId_LandColoursIndex, ColumnBase::Display_String, 0) { } QVariant LandColoursColumn::get(const Record& record) const { const int Size = Land::LAND_NUM_VERTS * 3; const Land& land = record.get(); DataType values(Size, 0); if (land.isDataLoaded(Land::DATA_VCLR)) { for (int i = 0; i < Size; ++i) values[i] = land.getLandData()->mColours[i]; } QVariant variant; variant.setValue(values); return variant; } void LandColoursColumn::set(Record& record, const QVariant& data) { DataType values = data.value(); if (values.size() != Land::LAND_NUM_VERTS * 3) throw std::runtime_error("invalid land colours data"); Land copy = record.get(); copy.add(Land::DATA_VCLR); for (int i = 0; i < values.size(); ++i) { copy.getLandData()->mColours[i] = values[i]; } record.setModified(copy); } bool LandColoursColumn::isEditable() const { return true; } /* LandTexturesColumn */ LandTexturesColumn::LandTexturesColumn() : Column(Columns::ColumnId_LandTexturesIndex, ColumnBase::Display_String, 0) { } QVariant LandTexturesColumn::get(const Record& record) const { const int Size = Land::LAND_NUM_TEXTURES; const Land& land = record.get(); DataType values(Size, 0); if (land.isDataLoaded(Land::DATA_VTEX)) { for (int i = 0; i < Size; ++i) values[i] = land.getLandData()->mTextures[i]; } QVariant variant; variant.setValue(values); return variant; } void LandTexturesColumn::set(Record& record, const QVariant& data) { DataType values = data.value(); if (values.size() != Land::LAND_NUM_TEXTURES) throw std::runtime_error("invalid land textures data"); Land copy = record.get(); copy.add(Land::DATA_VTEX); for (int i = 0; i < values.size(); ++i) { copy.getLandData()->mTextures[i] = values[i]; } record.setModified(copy); } bool LandTexturesColumn::isEditable() const { return true; } /* BodyPartRaceColumn */ BodyPartRaceColumn::BodyPartRaceColumn(const MeshTypeColumn *meshType) : mMeshType(meshType) {} QVariant BodyPartRaceColumn::get(const Record &record) const { if (mMeshType != nullptr && mMeshType->get(record) == ESM::BodyPart::MT_Skin) { return QString::fromUtf8(record.get().mRace.c_str()); } return QVariant(QVariant::UserType); } void BodyPartRaceColumn::set(Record &record, const QVariant &data) { ESM::BodyPart record2 = record.get(); record2.mRace = data.toString().toUtf8().constData(); record.setModified(record2); } bool BodyPartRaceColumn::isEditable() const { return true; } } ================================================ FILE: apps/opencs/model/world/columnimp.hpp ================================================ #ifndef CSM_WOLRD_COLUMNIMP_H #define CSM_WOLRD_COLUMNIMP_H #include #include #include #include #include #include #include #include #include #include "columnbase.hpp" #include "columns.hpp" #include "info.hpp" #include "land.hpp" #include "landtexture.hpp" namespace CSMWorld { /// \note Shares ID with VarValueColumn. A table can not have both. template struct FloatValueColumn : public Column { FloatValueColumn() : Column (Columns::ColumnId_Value, ColumnBase::Display_Float) {} QVariant get (const Record& record) const override { return record.get().mValue.getFloat(); } void set (Record& record, const QVariant& data) override { ESXRecordT record2 = record.get(); record2.mValue.setFloat (data.toFloat()); record.setModified (record2); } bool isEditable() const override { return true; } }; template struct StringIdColumn : public Column { StringIdColumn (bool hidden = false) : Column (Columns::ColumnId_Id, ColumnBase::Display_Id, hidden ? 0 : ColumnBase::Flag_Table | ColumnBase::Flag_Dialogue) {} QVariant get (const Record& record) const override { return QString::fromUtf8 (record.get().mId.c_str()); } bool isEditable() const override { return false; } }; template<> inline QVariant StringIdColumn::get(const Record& record) const { const Land& land = record.get(); return QString::fromUtf8(Land::createUniqueRecordId(land.mX, land.mY).c_str()); } template<> inline QVariant StringIdColumn::get(const Record& record) const { const LandTexture& ltex = record.get(); return QString::fromUtf8(LandTexture::createUniqueRecordId(ltex.mPluginIndex, ltex.mIndex).c_str()); } template struct RecordStateColumn : public Column { RecordStateColumn() : Column (Columns::ColumnId_Modification, ColumnBase::Display_RecordState) {} QVariant get (const Record& record) const override { if (record.mState==Record::State_Erased) return static_cast (Record::State_Deleted); return static_cast (record.mState); } void set (Record& record, const QVariant& data) override { record.mState = static_cast (data.toInt()); } bool isEditable() const override { return true; } bool isUserEditable() const override { return false; } }; template struct FixedRecordTypeColumn : public Column { int mType; FixedRecordTypeColumn (int type) : Column (Columns::ColumnId_RecordType, ColumnBase::Display_Integer, 0), mType (type) {} QVariant get (const Record& record) const override { return mType; } bool isEditable() const override { return false; } }; /// \attention A var type column must be immediately followed by a suitable value column. template struct VarTypeColumn : public Column { VarTypeColumn (ColumnBase::Display display) : Column (Columns::ColumnId_ValueType, display, ColumnBase::Flag_Table | ColumnBase::Flag_Dialogue | ColumnBase::Flag_Dialogue_Refresh) {} QVariant get (const Record& record) const override { return static_cast (record.get().mValue.getType()); } void set (Record& record, const QVariant& data) override { ESXRecordT record2 = record.get(); record2.mValue.setType (static_cast (data.toInt())); record.setModified (record2); } bool isEditable() const override { return true; } }; /// \note Shares ID with FloatValueColumn. A table can not have both. template struct VarValueColumn : public Column { VarValueColumn() : Column (Columns::ColumnId_Value, ColumnBase::Display_Var, ColumnBase::Flag_Table | ColumnBase::Flag_Dialogue | ColumnBase::Flag_Dialogue_Refresh) {} QVariant get (const Record& record) const override { switch (record.get().mValue.getType()) { case ESM::VT_String: return QString::fromUtf8 (record.get().mValue.getString().c_str()); case ESM::VT_Int: case ESM::VT_Short: case ESM::VT_Long: return record.get().mValue.getInteger(); case ESM::VT_Float: return record.get().mValue.getFloat(); default: return QVariant(); } } void set (Record& record, const QVariant& data) override { ESXRecordT record2 = record.get(); switch (record2.mValue.getType()) { case ESM::VT_String: record2.mValue.setString (data.toString().toUtf8().constData()); break; case ESM::VT_Int: case ESM::VT_Short: case ESM::VT_Long: record2.mValue.setInteger (data.toInt()); break; case ESM::VT_Float: record2.mValue.setFloat (data.toFloat()); break; default: break; } record.setModified (record2); } bool isEditable() const override { return true; } }; template struct DescriptionColumn : public Column { DescriptionColumn() : Column (Columns::ColumnId_Description, ColumnBase::Display_LongString) {} QVariant get (const Record& record) const override { return QString::fromUtf8 (record.get().mDescription.c_str()); } void set (Record& record, const QVariant& data) override { ESXRecordT record2 = record.get(); record2.mDescription = data.toString().toUtf8().constData(); record.setModified (record2); } bool isEditable() const override { return true; } }; template struct SpecialisationColumn : public Column { SpecialisationColumn() : Column (Columns::ColumnId_Specialisation, ColumnBase::Display_Specialisation) {} QVariant get (const Record& record) const override { return record.get().mData.mSpecialization; } void set (Record& record, const QVariant& data) override { ESXRecordT record2 = record.get(); record2.mData.mSpecialization = data.toInt(); record.setModified (record2); } bool isEditable() const override { return true; } }; template struct UseValueColumn : public Column { int mIndex; UseValueColumn (int index) : Column (Columns::ColumnId_UseValue1 + index, ColumnBase::Display_Float), mIndex (index) {} QVariant get (const Record& record) const override { return record.get().mData.mUseValue[mIndex]; } void set (Record& record, const QVariant& data) override { ESXRecordT record2 = record.get(); record2.mData.mUseValue[mIndex] = data.toFloat(); record.setModified (record2); } bool isEditable() const override { return true; } }; template struct AttributeColumn : public Column { AttributeColumn() : Column (Columns::ColumnId_Attribute, ColumnBase::Display_Attribute) {} QVariant get (const Record& record) const override { return record.get().mData.mAttribute; } void set (Record& record, const QVariant& data) override { ESXRecordT record2 = record.get(); record2.mData.mAttribute = data.toInt(); record.setModified (record2); } bool isEditable() const override { return true; } }; template struct NameColumn : public Column { NameColumn() : Column (Columns::ColumnId_Name, ColumnBase::Display_String) {} QVariant get (const Record& record) const override { return QString::fromUtf8 (record.get().mName.c_str()); } void set (Record& record, const QVariant& data) override { ESXRecordT record2 = record.get(); record2.mName = data.toString().toUtf8().constData(); record.setModified (record2); } bool isEditable() const override { return true; } }; template struct AttributesColumn : public Column { int mIndex; AttributesColumn (int index) : Column (Columns::ColumnId_Attribute1 + index, ColumnBase::Display_Attribute), mIndex (index) {} QVariant get (const Record& record) const override { return record.get().mData.mAttribute[mIndex]; } void set (Record& record, const QVariant& data) override { ESXRecordT record2 = record.get(); record2.mData.mAttribute[mIndex] = data.toInt(); record.setModified (record2); } bool isEditable() const override { return true; } }; template struct SkillsColumn : public Column { int mIndex; bool mMajor; SkillsColumn (int index, bool typePrefix = false, bool major = false) : Column ((typePrefix ? ( major ? Columns::ColumnId_MajorSkill1 : Columns::ColumnId_MinorSkill1) : Columns::ColumnId_Skill1) + index, ColumnBase::Display_Skill), mIndex (index), mMajor (major) {} QVariant get (const Record& record) const override { int skill = record.get().mData.getSkill (mIndex, mMajor); return QString::fromUtf8 (ESM::Skill::indexToId (skill).c_str()); } void set (Record& record, const QVariant& data) override { std::istringstream stream (data.toString().toUtf8().constData()); int index = -1; char c; stream >> c >> index; if (index!=-1) { ESXRecordT record2 = record.get(); record2.mData.getSkill (mIndex, mMajor) = index; record.setModified (record2); } } bool isEditable() const override { return true; } }; template struct PlayableColumn : public Column { PlayableColumn() : Column (Columns::ColumnId_Playable, ColumnBase::Display_Boolean) {} QVariant get (const Record& record) const override { return record.get().mData.mIsPlayable!=0; } void set (Record& record, const QVariant& data) override { ESXRecordT record2 = record.get(); record2.mData.mIsPlayable = data.toInt(); record.setModified (record2); } bool isEditable() const override { return true; } }; template struct HiddenColumn : public Column { HiddenColumn() : Column (Columns::ColumnId_Hidden, ColumnBase::Display_Boolean) {} QVariant get (const Record& record) const override { return record.get().mData.mIsHidden!=0; } void set (Record& record, const QVariant& data) override { ESXRecordT record2 = record.get(); record2.mData.mIsHidden = data.toInt(); record.setModified (record2); } bool isEditable() const override { return true; } }; template struct FlagColumn : public Column { int mMask; bool mInverted; FlagColumn (int columnId, int mask, int flags = ColumnBase::Flag_Table | ColumnBase::Flag_Dialogue, bool inverted = false) : Column (columnId, ColumnBase::Display_Boolean, flags), mMask (mask), mInverted (inverted) {} QVariant get (const Record& record) const override { bool flag = (record.get().mData.mFlags & mMask)!=0; if (mInverted) flag = !flag; return flag; } void set (Record& record, const QVariant& data) override { ESXRecordT record2 = record.get(); int flags = record2.mData.mFlags & ~mMask; if ((data.toInt()!=0)!=mInverted) flags |= mMask; record2.mData.mFlags = flags; record.setModified (record2); } bool isEditable() const override { return true; } }; template struct FlagColumn2 : public Column { int mMask; bool mInverted; FlagColumn2 (int columnId, int mask, bool inverted = false) : Column (columnId, ColumnBase::Display_Boolean), mMask (mask), mInverted (inverted) {} QVariant get (const Record& record) const override { bool flag = (record.get().mFlags & mMask)!=0; if (mInverted) flag = !flag; return flag; } void set (Record& record, const QVariant& data) override { ESXRecordT record2 = record.get(); int flags = record2.mFlags & ~mMask; if ((data.toInt()!=0)!=mInverted) flags |= mMask; record2.mFlags = flags; record.setModified (record2); } bool isEditable() const override { return true; } }; template struct WeightHeightColumn : public Column { bool mMale; bool mWeight; WeightHeightColumn (bool male, bool weight) : Column (male ? (weight ? Columns::ColumnId_MaleWeight : Columns::ColumnId_MaleHeight) : (weight ? Columns::ColumnId_FemaleWeight : Columns::ColumnId_FemaleHeight), ColumnBase::Display_Float), mMale (male), mWeight (weight) {} QVariant get (const Record& record) const override { const ESM::Race::MaleFemaleF& value = mWeight ? record.get().mData.mWeight : record.get().mData.mHeight; return mMale ? value.mMale : value.mFemale; } void set (Record& record, const QVariant& data) override { ESXRecordT record2 = record.get(); ESM::Race::MaleFemaleF& value = mWeight ? record2.mData.mWeight : record2.mData.mHeight; (mMale ? value.mMale : value.mFemale) = data.toFloat(); record.setModified (record2); } bool isEditable() const override { return true; } }; template struct SoundParamColumn : public Column { enum Type { Type_Volume, Type_MinRange, Type_MaxRange }; Type mType; SoundParamColumn (Type type) : Column (type==Type_Volume ? Columns::ColumnId_Volume : (type==Type_MinRange ? Columns::ColumnId_MinRange : Columns::ColumnId_MaxRange), ColumnBase::Display_Integer), mType (type) {} QVariant get (const Record& record) const override { int value = 0; switch (mType) { case Type_Volume: value = record.get().mData.mVolume; break; case Type_MinRange: value = record.get().mData.mMinRange; break; case Type_MaxRange: value = record.get().mData.mMaxRange; break; } return value; } void set (Record& record, const QVariant& data) override { int value = data.toInt(); if (value<0) value = 0; else if (value>255) value = 255; ESXRecordT record2 = record.get(); switch (mType) { case Type_Volume: record2.mData.mVolume = static_cast (value); break; case Type_MinRange: record2.mData.mMinRange = static_cast (value); break; case Type_MaxRange: record2.mData.mMaxRange = static_cast (value); break; } record.setModified (record2); } bool isEditable() const override { return true; } }; template struct SoundFileColumn : public Column { SoundFileColumn() : Column (Columns::ColumnId_SoundFile, ColumnBase::Display_SoundRes) {} QVariant get (const Record& record) const override { return QString::fromUtf8 (record.get().mSound.c_str()); } void set (Record& record, const QVariant& data) override { ESXRecordT record2 = record.get(); record2.mSound = data.toString().toUtf8().constData(); record.setModified (record2); } bool isEditable() const override { return true; } }; template struct MapColourColumn : public Column { MapColourColumn() : Column (Columns::ColumnId_MapColour, ColumnBase::Display_Colour) {} QVariant get (const Record& record) const override { return record.get().mMapColor; } void set (Record& record, const QVariant& data) override { ESXRecordT copy = record.get(); copy.mMapColor = data.toInt(); record.setModified (copy); } bool isEditable() const override { return true; } }; template struct SleepListColumn : public Column { SleepListColumn() : Column (Columns::ColumnId_SleepEncounter, ColumnBase::Display_CreatureLevelledList) {} QVariant get (const Record& record) const override { return QString::fromUtf8 (record.get().mSleepList.c_str()); } void set (Record& record, const QVariant& data) override { ESXRecordT record2 = record.get(); record2.mSleepList = data.toString().toUtf8().constData(); record.setModified (record2); } bool isEditable() const override { return true; } }; template struct TextureColumn : public Column { TextureColumn() : Column (Columns::ColumnId_Texture, ColumnBase::Display_Texture) {} QVariant get (const Record& record) const override { return QString::fromUtf8 (record.get().mTexture.c_str()); } void set (Record& record, const QVariant& data) override { ESXRecordT record2 = record.get(); record2.mTexture = data.toString().toUtf8().constData(); record.setModified (record2); } bool isEditable() const override { return true; } }; template struct SpellTypeColumn : public Column { SpellTypeColumn() : Column (Columns::ColumnId_SpellType, ColumnBase::Display_SpellType) {} QVariant get (const Record& record) const override { return record.get().mData.mType; } void set (Record& record, const QVariant& data) override { ESXRecordT record2 = record.get(); record2.mData.mType = data.toInt(); record.setModified (record2); } bool isEditable() const override { return true; } }; template struct CostColumn : public Column { CostColumn() : Column (Columns::ColumnId_Cost, ColumnBase::Display_Integer) {} QVariant get (const Record& record) const override { return record.get().mData.mCost; } void set (Record& record, const QVariant& data) override { ESXRecordT record2 = record.get(); record2.mData.mCost = data.toInt(); record.setModified (record2); } bool isEditable() const override { return true; } }; template struct ScriptColumn : public Column { enum Type { Type_File, // regular script record Type_Lines, // console context Type_Info // dialogue context (not implemented yet) }; ScriptColumn (Type type) : Column (Columns::ColumnId_ScriptText, type==Type_File ? ColumnBase::Display_ScriptFile : ColumnBase::Display_ScriptLines, type==Type_File ? 0 : ColumnBase::Flag_Dialogue) {} QVariant get (const Record& record) const override { return QString::fromUtf8 (record.get().mScriptText.c_str()); } void set (Record& record, const QVariant& data) override { ESXRecordT record2 = record.get(); record2.mScriptText = data.toString().toUtf8().constData(); record.setModified (record2); } bool isEditable() const override { return true; } }; template struct RegionColumn : public Column { RegionColumn() : Column (Columns::ColumnId_Region, ColumnBase::Display_Region) {} QVariant get (const Record& record) const override { return QString::fromUtf8 (record.get().mRegion.c_str()); } void set (Record& record, const QVariant& data) override { ESXRecordT record2 = record.get(); record2.mRegion = data.toString().toUtf8().constData(); record.setModified (record2); } bool isEditable() const override { return true; } }; template struct CellColumn : public Column { bool mBlocked; /// \param blocked Do not allow user-modification CellColumn (bool blocked = false) : Column (Columns::ColumnId_Cell, ColumnBase::Display_Cell), mBlocked (blocked) {} QVariant get (const Record& record) const override { return QString::fromUtf8 (record.get().mCell.c_str()); } void set (Record& record, const QVariant& data) override { ESXRecordT record2 = record.get(); record2.mCell = data.toString().toUtf8().constData(); record.setModified (record2); } bool isEditable() const override { return true; } bool isUserEditable() const override { return !mBlocked; } }; template struct OriginalCellColumn : public Column { OriginalCellColumn() : Column (Columns::ColumnId_OriginalCell, ColumnBase::Display_Cell) {} QVariant get (const Record& record) const override { return QString::fromUtf8 (record.get().mOriginalCell.c_str()); } void set (Record& record, const QVariant& data) override { ESXRecordT record2 = record.get(); record2.mOriginalCell = data.toString().toUtf8().constData(); record.setModified (record2); } bool isEditable() const override { return true; } bool isUserEditable() const override { return false; } }; template struct IdColumn : public Column { IdColumn() : Column (Columns::ColumnId_ReferenceableId, ColumnBase::Display_Referenceable) {} QVariant get (const Record& record) const override { return QString::fromUtf8 (record.get().mRefID.c_str()); } void set (Record& record, const QVariant& data) override { ESXRecordT record2 = record.get(); record2.mRefID = data.toString().toUtf8().constData(); record.setModified (record2); } bool isEditable() const override { return true; } }; template struct ScaleColumn : public Column { ScaleColumn() : Column (Columns::ColumnId_Scale, ColumnBase::Display_Float) {} QVariant get (const Record& record) const override { return record.get().mScale; } void set (Record& record, const QVariant& data) override { ESXRecordT record2 = record.get(); record2.mScale = data.toFloat(); record.setModified (record2); } bool isEditable() const override { return true; } }; template struct OwnerColumn : public Column { OwnerColumn() : Column (Columns::ColumnId_Owner, ColumnBase::Display_Npc) {} QVariant get (const Record& record) const override { return QString::fromUtf8 (record.get().mOwner.c_str()); } void set (Record& record, const QVariant& data) override { ESXRecordT record2 = record.get(); record2.mOwner = data.toString().toUtf8().constData(); record.setModified (record2); } bool isEditable() const override { return true; } }; template struct SoulColumn : public Column { SoulColumn() : Column (Columns::ColumnId_Soul, ColumnBase::Display_Creature) {} QVariant get (const Record& record) const override { return QString::fromUtf8 (record.get().mSoul.c_str()); } void set (Record& record, const QVariant& data) override { ESXRecordT record2 = record.get(); record2.mSoul = data.toString().toUtf8().constData(); record.setModified (record2); } bool isEditable() const override { return true; } }; template struct FactionColumn : public Column { FactionColumn() : Column (Columns::ColumnId_Faction, ColumnBase::Display_Faction) {} QVariant get (const Record& record) const override { return QString::fromUtf8 (record.get().mFaction.c_str()); } void set (Record& record, const QVariant& data) override { ESXRecordT record2 = record.get(); record2.mFaction = data.toString().toUtf8().constData(); record.setModified (record2); } bool isEditable() const override { return true; } }; template struct FactionIndexColumn : public Column { FactionIndexColumn() : Column (Columns::ColumnId_FactionIndex, ColumnBase::Display_Integer) {} QVariant get (const Record& record) const override { return record.get().mFactionRank; } void set (Record& record, const QVariant& data) override { ESXRecordT record2 = record.get(); record2.mFactionRank = data.toInt(); record.setModified (record2); } bool isEditable() const override { return true; } }; template struct ChargesColumn : public Column { ChargesColumn() : Column (Columns::ColumnId_Charges, ColumnBase::Display_Integer) {} QVariant get (const Record& record) const override { return record.get().mChargeInt; } void set (Record& record, const QVariant& data) override { ESXRecordT record2 = record.get(); record2.mChargeInt = data.toInt(); record.setModified (record2); } bool isEditable() const override { return true; } }; template struct EnchantmentChargesColumn : public Column { EnchantmentChargesColumn() : Column (Columns::ColumnId_Enchantment, ColumnBase::Display_Float) {} QVariant get (const Record& record) const override { return record.get().mEnchantmentCharge; } void set (Record& record, const QVariant& data) override { ESXRecordT record2 = record.get(); record2.mEnchantmentCharge = data.toFloat(); record.setModified (record2); } bool isEditable() const override { return true; } }; template struct GoldValueColumn : public Column { GoldValueColumn() : Column (Columns::ColumnId_CoinValue, ColumnBase::Display_Integer) {} QVariant get (const Record& record) const override { return record.get().mGoldValue; } void set (Record& record, const QVariant& data) override { ESXRecordT record2 = record.get(); record2.mGoldValue = data.toInt(); record.setModified (record2); } bool isEditable() const override { return true; } }; template struct TeleportColumn : public Column { TeleportColumn() : Column (Columns::ColumnId_Teleport, ColumnBase::Display_Boolean) {} QVariant get (const Record& record) const override { return record.get().mTeleport; } void set (Record& record, const QVariant& data) override { ESXRecordT record2 = record.get(); record2.mTeleport = data.toInt(); record.setModified (record2); } bool isEditable() const override { return true; } }; template struct TeleportCellColumn : public Column { TeleportCellColumn() : Column (Columns::ColumnId_TeleportCell, ColumnBase::Display_Cell) {} QVariant get (const Record& record) const override { return QString::fromUtf8 (record.get().mDestCell.c_str()); } void set (Record& record, const QVariant& data) override { ESXRecordT record2 = record.get(); record2.mDestCell = data.toString().toUtf8().constData(); record.setModified (record2); } bool isEditable() const override { return true; } bool isUserEditable() const override { return true; } }; template struct LockLevelColumn : public Column { LockLevelColumn() : Column (Columns::ColumnId_LockLevel, ColumnBase::Display_Integer) {} QVariant get (const Record& record) const override { return record.get().mLockLevel; } void set (Record& record, const QVariant& data) override { ESXRecordT record2 = record.get(); record2.mLockLevel = data.toInt(); record.setModified (record2); } bool isEditable() const override { return true; } }; template struct KeyColumn : public Column { KeyColumn() : Column (Columns::ColumnId_Key, ColumnBase::Display_Miscellaneous) {} QVariant get (const Record& record) const override { return QString::fromUtf8 (record.get().mKey.c_str()); } void set (Record& record, const QVariant& data) override { ESXRecordT record2 = record.get(); record2.mKey = data.toString().toUtf8().constData(); record.setModified (record2); } bool isEditable() const override { return true; } }; template struct TrapColumn : public Column { TrapColumn() : Column (Columns::ColumnId_Trap, ColumnBase::Display_Spell) {} QVariant get (const Record& record) const override { return QString::fromUtf8 (record.get().mTrap.c_str()); } void set (Record& record, const QVariant& data) override { ESXRecordT record2 = record.get(); record2.mTrap = data.toString().toUtf8().constData(); record.setModified (record2); } bool isEditable() const override { return true; } }; template struct FilterColumn : public Column { FilterColumn() : Column (Columns::ColumnId_Filter, ColumnBase::Display_Filter) {} QVariant get (const Record& record) const override { return QString::fromUtf8 (record.get().mFilter.c_str()); } void set (Record& record, const QVariant& data) override { ESXRecordT record2 = record.get(); record2.mFilter = data.toString().toUtf8().constData(); record.setModified (record2); } bool isEditable() const override { return true; } }; template struct PosColumn : public Column { ESM::Position ESXRecordT::* mPosition; int mIndex; PosColumn (ESM::Position ESXRecordT::* position, int index, bool door) : Column ( (door ? Columns::ColumnId_DoorPositionXPos : Columns::ColumnId_PositionXPos)+index, ColumnBase::Display_Float), mPosition (position), mIndex (index) {} QVariant get (const Record& record) const override { const ESM::Position& position = record.get().*mPosition; return position.pos[mIndex]; } void set (Record& record, const QVariant& data) override { ESXRecordT record2 = record.get(); ESM::Position& position = record2.*mPosition; position.pos[mIndex] = data.toFloat(); record.setModified (record2); } bool isEditable() const override { return true; } }; template struct RotColumn : public Column { ESM::Position ESXRecordT::* mPosition; int mIndex; RotColumn (ESM::Position ESXRecordT::* position, int index, bool door) : Column ( (door ? Columns::ColumnId_DoorPositionXRot : Columns::ColumnId_PositionXRot)+index, ColumnBase::Display_Double), mPosition (position), mIndex (index) {} QVariant get (const Record& record) const override { const ESM::Position& position = record.get().*mPosition; return osg::RadiansToDegrees(position.rot[mIndex]); } void set (Record& record, const QVariant& data) override { ESXRecordT record2 = record.get(); ESM::Position& position = record2.*mPosition; position.rot[mIndex] = osg::DegreesToRadians(data.toFloat()); record.setModified (record2); } bool isEditable() const override { return true; } }; template struct DialogueTypeColumn : public Column { DialogueTypeColumn (bool hidden = false) : Column (Columns::ColumnId_DialogueType, ColumnBase::Display_DialogueType, hidden ? 0 : ColumnBase::Flag_Table | ColumnBase::Flag_Dialogue) {} QVariant get (const Record& record) const override { return static_cast (record.get().mType); } void set (Record& record, const QVariant& data) override { ESXRecordT record2 = record.get(); record2.mType = data.toInt(); record.setModified (record2); } bool isEditable() const override { return true; } bool isUserEditable() const override { return false; } }; template struct QuestStatusTypeColumn : public Column { QuestStatusTypeColumn() : Column (Columns::ColumnId_QuestStatusType, ColumnBase::Display_QuestStatusType) {} QVariant get (const Record& record) const override { return static_cast (record.get().mQuestStatus); } void set (Record& record, const QVariant& data) override { ESXRecordT record2 = record.get(); record2.mQuestStatus = static_cast (data.toInt()); record.setModified (record2); } bool isEditable() const override { return true; } }; template struct QuestDescriptionColumn : public Column { QuestDescriptionColumn() : Column (Columns::ColumnId_QuestDescription, ColumnBase::Display_LongString) {} QVariant get (const Record& record) const override { return QString::fromUtf8 (record.get().mResponse.c_str()); } void set (Record& record, const QVariant& data) override { ESXRecordT record2 = record.get(); record2.mResponse = data.toString().toUtf8().constData(); record.setModified (record2); } bool isEditable() const override { return true; } }; template struct QuestIndexColumn : public Column { QuestIndexColumn() : Column (Columns::ColumnId_QuestIndex, ColumnBase::Display_Integer) {} QVariant get (const Record& record) const override { return record.get().mData.mDisposition; } void set (Record& record, const QVariant& data) override { ESXRecordT record2 = record.get(); record2.mData.mDisposition = data.toInt(); record.setModified (record2); } bool isEditable() const override { return true; } }; template struct TopicColumn : public Column { TopicColumn (bool journal) : Column (journal ? Columns::ColumnId_Journal : Columns::ColumnId_Topic, journal ? ColumnBase::Display_Journal : ColumnBase::Display_Topic) {} QVariant get (const Record& record) const override { return QString::fromUtf8 (record.get().mTopicId.c_str()); } void set (Record& record, const QVariant& data) override { ESXRecordT record2 = record.get(); record2.mTopicId = data.toString().toUtf8().constData(); record.setModified (record2); } bool isEditable() const override { return true; } bool isUserEditable() const override { return false; } }; template struct ActorColumn : public Column { ActorColumn() : Column (Columns::ColumnId_Actor, ColumnBase::Display_Npc) {} QVariant get (const Record& record) const override { return QString::fromUtf8 (record.get().mActor.c_str()); } void set (Record& record, const QVariant& data) override { ESXRecordT record2 = record.get(); record2.mActor = data.toString().toUtf8().constData(); record.setModified (record2); } bool isEditable() const override { return true; } }; template struct RaceColumn : public Column { RaceColumn() : Column (Columns::ColumnId_Race, ColumnBase::Display_Race) {} QVariant get (const Record& record) const override { return QString::fromUtf8 (record.get().mRace.c_str()); } void set (Record& record, const QVariant& data) override { ESXRecordT record2 = record.get(); record2.mRace = data.toString().toUtf8().constData(); record.setModified (record2); } bool isEditable() const override { return true; } }; template struct ClassColumn : public Column { ClassColumn() : Column (Columns::ColumnId_Class, ColumnBase::Display_Class) {} QVariant get (const Record& record) const override { return QString::fromUtf8 (record.get().mClass.c_str()); } void set (Record& record, const QVariant& data) override { ESXRecordT record2 = record.get(); record2.mClass = data.toString().toUtf8().constData(); record.setModified (record2); } bool isEditable() const override { return true; } }; template struct PcFactionColumn : public Column { PcFactionColumn() : Column (Columns::ColumnId_PcFaction, ColumnBase::Display_Faction) {} QVariant get (const Record& record) const override { return QString::fromUtf8 (record.get().mPcFaction.c_str()); } void set (Record& record, const QVariant& data) override { ESXRecordT record2 = record.get(); record2.mPcFaction = data.toString().toUtf8().constData(); record.setModified (record2); } bool isEditable() const override { return true; } }; template struct ResponseColumn : public Column { ResponseColumn() : Column (Columns::ColumnId_Response, ColumnBase::Display_LongString) {} QVariant get (const Record& record) const override { return QString::fromUtf8 (record.get().mResponse.c_str()); } void set (Record& record, const QVariant& data) override { ESXRecordT record2 = record.get(); record2.mResponse = data.toString().toUtf8().constData(); record.setModified (record2); } bool isEditable() const override { return true; } }; template struct DispositionColumn : public Column { DispositionColumn() : Column (Columns::ColumnId_Disposition, ColumnBase::Display_Integer) {} QVariant get (const Record& record) const override { return record.get().mData.mDisposition; } void set (Record& record, const QVariant& data) override { ESXRecordT record2 = record.get(); record2.mData.mDisposition = data.toInt(); record.setModified (record2); } bool isEditable() const override { return true; } }; template struct RankColumn : public Column { RankColumn() : Column (Columns::ColumnId_Rank, ColumnBase::Display_Integer) {} QVariant get (const Record& record) const override { return static_cast (record.get().mData.mRank); } void set (Record& record, const QVariant& data) override { ESXRecordT record2 = record.get(); record2.mData.mRank = static_cast (data.toInt()); record.setModified (record2); } bool isEditable() const override { return true; } }; template struct PcRankColumn : public Column { PcRankColumn() : Column (Columns::ColumnId_PcRank, ColumnBase::Display_Integer) {} QVariant get (const Record& record) const override { return static_cast (record.get().mData.mPCrank); } void set (Record& record, const QVariant& data) override { ESXRecordT record2 = record.get(); record2.mData.mPCrank = static_cast (data.toInt()); record.setModified (record2); } bool isEditable() const override { return true; } }; template struct GenderColumn : public Column { GenderColumn() : Column (Columns::ColumnId_Gender, ColumnBase::Display_Gender) {} QVariant get (const Record& record) const override { return static_cast (record.get().mData.mGender); } void set (Record& record, const QVariant& data) override { ESXRecordT record2 = record.get(); record2.mData.mGender = data.toInt(); record.setModified (record2); } bool isEditable() const override { return true; } }; template struct GenderNpcColumn : public Column { GenderNpcColumn() : Column(Columns::ColumnId_Gender, ColumnBase::Display_GenderNpc) {} QVariant get(const Record& record) const override { // Implemented this way to allow additional gender types in the future. if ((record.get().mData.mFlags & ESM::BodyPart::BPF_Female) == ESM::BodyPart::BPF_Female) return 1; return 0; } void set(Record& record, const QVariant& data) override { ESXRecordT record2 = record.get(); // Implemented this way to allow additional gender types in the future. if (data.toInt() == 1) record2.mData.mFlags = (record2.mData.mFlags & ~ESM::BodyPart::BPF_Female) | ESM::BodyPart::BPF_Female; else record2.mData.mFlags = record2.mData.mFlags & ~ESM::BodyPart::BPF_Female; record.setModified(record2); } bool isEditable() const override { return true; } }; template struct EnchantmentTypeColumn : public Column { EnchantmentTypeColumn() : Column (Columns::ColumnId_EnchantmentType, ColumnBase::Display_EnchantmentType) {} QVariant get (const Record& record) const override { return static_cast (record.get().mData.mType); } void set (Record& record, const QVariant& data) override { ESXRecordT record2 = record.get(); record2.mData.mType = data.toInt(); record.setModified (record2); } bool isEditable() const override { return true; } }; template struct ChargesColumn2 : public Column { ChargesColumn2() : Column (Columns::ColumnId_Charges, ColumnBase::Display_Integer) {} QVariant get (const Record& record) const override { return record.get().mData.mCharge; } void set (Record& record, const QVariant& data) override { ESXRecordT record2 = record.get(); record2.mData.mCharge = data.toInt(); record.setModified (record2); } bool isEditable() const override { return true; } }; template struct AutoCalcColumn : public Column { AutoCalcColumn() : Column (Columns::ColumnId_AutoCalc, ColumnBase::Display_Boolean) {} QVariant get (const Record& record) const override { return record.get().mData.mAutocalc!=0; } void set (Record& record, const QVariant& data) override { ESXRecordT record2 = record.get(); record2.mData.mAutocalc = data.toInt(); record.setModified (record2); } bool isEditable() const override { return true; } }; template struct ModelColumn : public Column { ModelColumn() : Column (Columns::ColumnId_Model, ColumnBase::Display_Mesh) {} QVariant get (const Record& record) const override { return QString::fromUtf8 (record.get().mModel.c_str()); } void set (Record& record, const QVariant& data) override { ESXRecordT record2 = record.get(); record2.mModel = data.toString().toUtf8().constData(); record.setModified (record2); } bool isEditable() const override { return true; } }; template struct VampireColumn : public Column { VampireColumn() : Column (Columns::ColumnId_Vampire, ColumnBase::Display_Boolean) {} QVariant get (const Record& record) const override { return record.get().mData.mVampire!=0; } void set (Record& record, const QVariant& data) override { ESXRecordT record2 = record.get(); record2.mData.mVampire = data.toInt(); record.setModified (record2); } bool isEditable() const override { return true; } }; template struct BodyPartTypeColumn : public Column { BodyPartTypeColumn() : Column (Columns::ColumnId_BodyPartType, ColumnBase::Display_BodyPartType) {} QVariant get (const Record& record) const override { return static_cast (record.get().mData.mPart); } void set (Record& record, const QVariant& data) override { ESXRecordT record2 = record.get(); record2.mData.mPart = data.toInt(); record.setModified (record2); } bool isEditable() const override { return true; } }; template struct MeshTypeColumn : public Column { MeshTypeColumn(int flags = ColumnBase::Flag_Table | ColumnBase::Flag_Dialogue) : Column (Columns::ColumnId_MeshType, ColumnBase::Display_MeshType, flags) {} QVariant get (const Record& record) const override { return static_cast (record.get().mData.mType); } void set (Record& record, const QVariant& data) override { ESXRecordT record2 = record.get(); record2.mData.mType = data.toInt(); record.setModified (record2); } bool isEditable() const override { return true; } }; template struct OwnerGlobalColumn : public Column { OwnerGlobalColumn() : Column (Columns::ColumnId_OwnerGlobal, ColumnBase::Display_GlobalVariable) {} QVariant get (const Record& record) const override { return QString::fromUtf8 (record.get().mGlobalVariable.c_str()); } void set (Record& record, const QVariant& data) override { ESXRecordT record2 = record.get(); record2.mGlobalVariable = data.toString().toUtf8().constData(); record.setModified (record2); } bool isEditable() const override { return true; } }; template struct RefNumCounterColumn : public Column { RefNumCounterColumn() : Column (Columns::ColumnId_RefNumCounter, ColumnBase::Display_Integer, 0) {} QVariant get (const Record& record) const override { return static_cast (record.get().mRefNumCounter); } void set (Record& record, const QVariant& data) override { ESXRecordT record2 = record.get(); record2.mRefNumCounter = data.toInt(); record.setModified (record2); } bool isEditable() const override { return true; } bool isUserEditable() const override { return false; } }; template struct RefNumColumn : public Column { RefNumColumn() : Column (Columns::ColumnId_RefNum, ColumnBase::Display_Integer, 0) {} QVariant get (const Record& record) const override { return static_cast (record.get().mRefNum.mIndex); } void set (Record& record, const QVariant& data) override { ESXRecordT record2 = record.get(); record2.mRefNum.mIndex = data.toInt(); record.setModified (record2); } bool isEditable() const override { return true; } bool isUserEditable() const override { return false; } }; template struct SoundColumn : public Column { SoundColumn() : Column (Columns::ColumnId_Sound, ColumnBase::Display_Sound) {} QVariant get (const Record& record) const override { return QString::fromUtf8 (record.get().mSound.c_str()); } void set (Record& record, const QVariant& data) override { ESXRecordT record2 = record.get(); record2.mSound = data.toString().toUtf8().constData(); record.setModified (record2); } bool isEditable() const override { return true; } }; template struct CreatureColumn : public Column { CreatureColumn() : Column (Columns::ColumnId_Creature, ColumnBase::Display_Creature) {} QVariant get (const Record& record) const override { return QString::fromUtf8 (record.get().mCreature.c_str()); } void set (Record& record, const QVariant& data) override { ESXRecordT record2 = record.get(); record2.mCreature = data.toString().toUtf8().constData(); record.setModified (record2); } bool isEditable() const override { return true; } }; template struct SoundGeneratorTypeColumn : public Column { SoundGeneratorTypeColumn() : Column (Columns::ColumnId_SoundGeneratorType, ColumnBase::Display_SoundGeneratorType) {} QVariant get (const Record& record) const override { return static_cast (record.get().mType); } void set (Record& record, const QVariant& data) override { ESXRecordT record2 = record.get(); record2.mType = data.toInt(); record.setModified (record2); } bool isEditable() const override { return true; } }; template struct BaseCostColumn : public Column { BaseCostColumn() : Column (Columns::ColumnId_BaseCost, ColumnBase::Display_Float) {} QVariant get (const Record& record) const override { return record.get().mData.mBaseCost; } void set (Record& record, const QVariant& data) override { ESXRecordT record2 = record.get(); record2.mData.mBaseCost = data.toFloat(); record.setModified (record2); } bool isEditable() const override { return true; } }; template struct SchoolColumn : public Column { SchoolColumn() : Column (Columns::ColumnId_School, ColumnBase::Display_School) {} QVariant get (const Record& record) const override { return record.get().mData.mSchool; } void set (Record& record, const QVariant& data) override { ESXRecordT record2 = record.get(); record2.mData.mSchool = data.toInt(); record.setModified (record2); } bool isEditable() const override { return true; } }; template struct EffectTextureColumn : public Column { EffectTextureColumn (Columns::ColumnId columnId) : Column (columnId, columnId == Columns::ColumnId_Particle ? ColumnBase::Display_Texture : ColumnBase::Display_Icon) { assert (this->mColumnId==Columns::ColumnId_Icon || this->mColumnId==Columns::ColumnId_Particle); } QVariant get (const Record& record) const override { return QString::fromUtf8 ( (this->mColumnId==Columns::ColumnId_Icon ? record.get().mIcon : record.get().mParticle).c_str()); } void set (Record& record, const QVariant& data) override { ESXRecordT record2 = record.get(); (this->mColumnId==Columns::ColumnId_Icon ? record2.mIcon : record2.mParticle) = data.toString().toUtf8().constData(); record.setModified (record2); } bool isEditable() const override { return true; } }; template struct EffectObjectColumn : public Column { EffectObjectColumn (Columns::ColumnId columnId) : Column (columnId, columnId==Columns::ColumnId_BoltObject ? ColumnBase::Display_Weapon : ColumnBase::Display_Static) { assert (this->mColumnId==Columns::ColumnId_CastingObject || this->mColumnId==Columns::ColumnId_HitObject || this->mColumnId==Columns::ColumnId_AreaObject || this->mColumnId==Columns::ColumnId_BoltObject); } QVariant get (const Record& record) const override { const std::string *string = nullptr; switch (this->mColumnId) { case Columns::ColumnId_CastingObject: string = &record.get().mCasting; break; case Columns::ColumnId_HitObject: string = &record.get().mHit; break; case Columns::ColumnId_AreaObject: string = &record.get().mArea; break; case Columns::ColumnId_BoltObject: string = &record.get().mBolt; break; } if (!string) throw std::logic_error ("Unsupported column ID"); return QString::fromUtf8 (string->c_str()); } void set (Record& record, const QVariant& data) override { std::string *string = nullptr; ESXRecordT record2 = record.get(); switch (this->mColumnId) { case Columns::ColumnId_CastingObject: string = &record2.mCasting; break; case Columns::ColumnId_HitObject: string = &record2.mHit; break; case Columns::ColumnId_AreaObject: string = &record2.mArea; break; case Columns::ColumnId_BoltObject: string = &record2.mBolt; break; } if (!string) throw std::logic_error ("Unsupported column ID"); *string = data.toString().toUtf8().constData(); record.setModified (record2); } bool isEditable() const override { return true; } }; template struct EffectSoundColumn : public Column { EffectSoundColumn (Columns::ColumnId columnId) : Column (columnId, ColumnBase::Display_Sound) { assert (this->mColumnId==Columns::ColumnId_CastingSound || this->mColumnId==Columns::ColumnId_HitSound || this->mColumnId==Columns::ColumnId_AreaSound || this->mColumnId==Columns::ColumnId_BoltSound); } QVariant get (const Record& record) const override { const std::string *string = nullptr; switch (this->mColumnId) { case Columns::ColumnId_CastingSound: string = &record.get().mCastSound; break; case Columns::ColumnId_HitSound: string = &record.get().mHitSound; break; case Columns::ColumnId_AreaSound: string = &record.get().mAreaSound; break; case Columns::ColumnId_BoltSound: string = &record.get().mBoltSound; break; } if (!string) throw std::logic_error ("Unsupported column ID"); return QString::fromUtf8 (string->c_str()); } void set (Record& record, const QVariant& data) override { std::string *string = nullptr; ESXRecordT record2 = record.get(); switch (this->mColumnId) { case Columns::ColumnId_CastingSound: string = &record2.mCastSound; break; case Columns::ColumnId_HitSound: string = &record2.mHitSound; break; case Columns::ColumnId_AreaSound: string = &record2.mAreaSound; break; case Columns::ColumnId_BoltSound: string = &record2.mBoltSound; break; } if (!string) throw std::logic_error ("Unsupported column ID"); *string = data.toString().toUtf8().constData(); record.setModified (record2); } bool isEditable() const override { return true; } }; template struct FormatColumn : public Column { FormatColumn() : Column (Columns::ColumnId_FileFormat, ColumnBase::Display_Integer) {} QVariant get (const Record& record) const override { return record.get().mFormat; } bool isEditable() const override { return false; } }; template struct AuthorColumn : public Column { AuthorColumn() : Column (Columns::ColumnId_Author, ColumnBase::Display_String32) {} QVariant get (const Record& record) const override { return QString::fromUtf8 (record.get().mAuthor.c_str()); } void set (Record& record, const QVariant& data) override { ESXRecordT record2 = record.get(); record2.mAuthor = data.toString().toUtf8().constData(); record.setModified (record2); } bool isEditable() const override { return true; } }; template struct FileDescriptionColumn : public Column { FileDescriptionColumn() : Column (Columns::ColumnId_FileDescription, ColumnBase::Display_LongString256) {} QVariant get (const Record& record) const override { return QString::fromUtf8 (record.get().mDescription.c_str()); } void set (Record& record, const QVariant& data) override { ESXRecordT record2 = record.get(); record2.mDescription = data.toString().toUtf8().constData(); record.setModified (record2); } bool isEditable() const override { return true; } }; struct LandTextureNicknameColumn : public Column { LandTextureNicknameColumn(); QVariant get(const Record& record) const override; void set(Record& record, const QVariant& data) override; bool isEditable() const override; }; struct LandTextureIndexColumn : public Column { LandTextureIndexColumn(); QVariant get(const Record& record) const override; bool isEditable() const override; }; struct LandPluginIndexColumn : public Column { LandPluginIndexColumn(); QVariant get(const Record& record) const override; bool isEditable() const override; }; struct LandTexturePluginIndexColumn : public Column { LandTexturePluginIndexColumn(); QVariant get(const Record& record) const override; bool isEditable() const override; }; struct LandNormalsColumn : public Column { using DataType = QVector; LandNormalsColumn(); QVariant get(const Record& record) const override; void set(Record& record, const QVariant& data) override; bool isEditable() const override; }; struct LandHeightsColumn : public Column { using DataType = QVector; LandHeightsColumn(); QVariant get(const Record& record) const override; void set(Record& record, const QVariant& data) override; bool isEditable() const override; }; struct LandColoursColumn : public Column { using DataType = QVector; LandColoursColumn(); QVariant get(const Record& record) const override; void set(Record& record, const QVariant& data) override; bool isEditable() const override; }; struct LandTexturesColumn : public Column { using DataType = QVector; LandTexturesColumn(); QVariant get(const Record& record) const override; void set(Record& record, const QVariant& data) override; bool isEditable() const override; }; struct BodyPartRaceColumn : public RaceColumn { const MeshTypeColumn *mMeshType; BodyPartRaceColumn(const MeshTypeColumn *meshType); QVariant get(const Record &record) const override; void set(Record &record, const QVariant &data) override; bool isEditable() const override; }; } // This is required to access the type as a QVariant. Q_DECLARE_METATYPE(CSMWorld::LandNormalsColumn::DataType) Q_DECLARE_METATYPE(CSMWorld::LandHeightsColumn::DataType) Q_DECLARE_METATYPE(CSMWorld::LandColoursColumn::DataType) Q_DECLARE_METATYPE(CSMWorld::LandTexturesColumn::DataType) #endif ================================================ FILE: apps/opencs/model/world/columns.cpp ================================================ #include "columns.hpp" #include #include #include "universalid.hpp" #include "infoselectwrapper.hpp" namespace CSMWorld { namespace Columns { struct ColumnDesc { int mId; const char *mName; }; const ColumnDesc sNames[] = { { ColumnId_Value, "Value" }, { ColumnId_Id, "ID" }, { ColumnId_Modification, "Modified" }, { ColumnId_RecordType, "Record Type" }, { ColumnId_ValueType, "Value Type" }, { ColumnId_Description, "Description" }, { ColumnId_Specialisation, "Specialisation" }, { ColumnId_Attribute, "Attribute" }, { ColumnId_Name, "Name" }, { ColumnId_Playable, "Playable" }, { ColumnId_Hidden, "Hidden" }, { ColumnId_MaleWeight, "Male Weight" }, { ColumnId_FemaleWeight, "Female Weight" }, { ColumnId_MaleHeight, "Male Height" }, { ColumnId_FemaleHeight, "Female Height" }, { ColumnId_Volume, "Volume" }, { ColumnId_MinRange, "Min Range" }, { ColumnId_MaxRange, "Max Range" }, { ColumnId_MinMagnitude, "Min Magnitude" }, { ColumnId_MaxMagnitude, "Max Magnitude" }, { ColumnId_SoundFile, "Sound File" }, { ColumnId_MapColour, "Map Colour" }, { ColumnId_SleepEncounter, "Sleep Encounter" }, { ColumnId_Texture, "Texture" }, { ColumnId_SpellType, "Spell Type" }, { ColumnId_Cost, "Cost" }, { ColumnId_ScriptText, "Script Text" }, { ColumnId_Region, "Region" }, { ColumnId_Cell, "Cell" }, { ColumnId_Scale, "Scale" }, { ColumnId_Owner, "Owner" }, { ColumnId_Soul, "Soul" }, { ColumnId_Faction, "Faction" }, { ColumnId_FactionIndex, "Faction Index" }, { ColumnId_Charges, "Charges" }, { ColumnId_Enchantment, "Enchantment" }, { ColumnId_CoinValue, "Coin Value" }, { ColumnId_Teleport, "Teleport" }, { ColumnId_TeleportCell, "Teleport Cell" }, { ColumnId_LockLevel, "Lock Level" }, { ColumnId_Key, "Key" }, { ColumnId_Trap, "Trap" }, { ColumnId_BeastRace, "Beast Race" }, { ColumnId_AutoCalc, "Auto Calc" }, { ColumnId_StarterSpell, "Starter Spell" }, { ColumnId_AlwaysSucceeds, "Always Succeeds" }, { ColumnId_SleepForbidden, "Sleep Forbidden" }, { ColumnId_InteriorWater, "Interior Water" }, { ColumnId_InteriorSky, "Interior Sky" }, { ColumnId_Model, "Model/Animation" }, { ColumnId_Script, "Script" }, { ColumnId_Icon, "Icon" }, { ColumnId_Weight, "Weight" }, { ColumnId_EnchantmentPoints, "Enchantment Points" }, { ColumnId_Quality, "Quality" }, { ColumnId_AiHello, "AI Hello" }, { ColumnId_AiFlee, "AI Flee" }, { ColumnId_AiFight, "AI Fight" }, { ColumnId_AiAlarm, "AI Alarm" }, { ColumnId_BuysWeapons, "Buys Weapons" }, { ColumnId_BuysArmor, "Buys Armor" }, { ColumnId_BuysClothing, "Buys Clothing" }, { ColumnId_BuysBooks, "Buys Books" }, { ColumnId_BuysIngredients, "Buys Ingredients" }, { ColumnId_BuysLockpicks, "Buys Lockpicks" }, { ColumnId_BuysProbes, "Buys Probes" }, { ColumnId_BuysLights, "Buys Lights" }, { ColumnId_BuysApparati, "Buys Apparati" }, { ColumnId_BuysRepairItems, "Buys Repair Items" }, { ColumnId_BuysMiscItems, "Buys Misc Items" }, { ColumnId_BuysPotions, "Buys Potions" }, { ColumnId_BuysMagicItems, "Buys Magic Items" }, { ColumnId_SellsSpells, "Sells Spells" }, { ColumnId_Trainer, "Trainer" }, { ColumnId_Spellmaking, "Spellmaking" }, { ColumnId_EnchantingService, "Enchanting Service" }, { ColumnId_RepairService, "Repair Service" }, { ColumnId_ApparatusType, "Apparatus Type" }, { ColumnId_ArmorType, "Armor Type" }, { ColumnId_Health, "Health" }, { ColumnId_ArmorValue, "Armor Value" }, { ColumnId_BookType, "Book Type" }, { ColumnId_ClothingType, "Clothing Type" }, { ColumnId_WeightCapacity, "Weight Capacity" }, { ColumnId_OrganicContainer, "Organic Container" }, { ColumnId_Respawn, "Respawn" }, { ColumnId_CreatureType, "Creature Type" }, { ColumnId_SoulPoints, "Soul Points" }, { ColumnId_ParentCreature, "Parent Creature" }, { ColumnId_Biped, "Biped" }, { ColumnId_HasWeapon, "Has Weapon" }, { ColumnId_Swims, "Swims" }, { ColumnId_Flies, "Flies" }, { ColumnId_Walks, "Walks" }, { ColumnId_Essential, "Essential" }, { ColumnId_BloodType, "Blood Type" }, { ColumnId_OpenSound, "Open Sound" }, { ColumnId_CloseSound, "Close Sound" }, { ColumnId_Duration, "Duration" }, { ColumnId_Radius, "Radius" }, { ColumnId_Colour, "Colour" }, { ColumnId_Sound, "Sound" }, { ColumnId_Dynamic, "Dynamic" }, { ColumnId_Portable, "Portable" }, { ColumnId_NegativeLight, "Negative Light" }, { ColumnId_EmitterType, "Emitter Type" }, { ColumnId_Fire, "Fire" }, { ColumnId_OffByDefault, "Off by default" }, { ColumnId_IsKey, "Is Key" }, { ColumnId_Race, "Race" }, { ColumnId_Class, "Class" }, { Columnid_Hair, "Hair" }, { ColumnId_Head, "Head" }, { ColumnId_Female, "Female" }, { ColumnId_WeaponType, "Weapon Type" }, { ColumnId_WeaponSpeed, "Weapon Speed" }, { ColumnId_WeaponReach, "Weapon Reach" }, { ColumnId_MinChop, "Min Chop" }, { ColumnId_MaxChip, "Max Chop" }, { Columnid_MinSlash, "Min Slash" }, { ColumnId_MaxSlash, "Max Slash" }, { ColumnId_MinThrust, "Min Thrust" }, { ColumnId_MaxThrust, "Max Thrust" }, { ColumnId_Magical, "Magical" }, { ColumnId_Silver, "Silver" }, { ColumnId_Filter, "Filter" }, { ColumnId_PositionXPos, "Pos X" }, { ColumnId_PositionYPos, "Pos Y" }, { ColumnId_PositionZPos, "Pos Z" }, { ColumnId_PositionXRot, "Rot X" }, { ColumnId_PositionYRot, "Rot Y" }, { ColumnId_PositionZRot, "Rot Z" }, { ColumnId_DoorPositionXPos, "Teleport Pos X" }, { ColumnId_DoorPositionYPos, "Teleport Pos Y" }, { ColumnId_DoorPositionZPos, "Teleport Pos Z" }, { ColumnId_DoorPositionXRot, "Teleport Rot X" }, { ColumnId_DoorPositionYRot, "Teleport Rot Y" }, { ColumnId_DoorPositionZRot, "Teleport Rot Z" }, { ColumnId_DialogueType, "Dialogue Type" }, { ColumnId_QuestIndex, "Quest Index" }, { ColumnId_QuestStatusType, "Quest Status" }, { ColumnId_QuestDescription, "Quest Description" }, { ColumnId_Topic, "Topic" }, { ColumnId_Journal, "Journal" }, { ColumnId_Actor, "Actor" }, { ColumnId_PcFaction, "PC Faction" }, { ColumnId_Response, "Response" }, { ColumnId_Disposition, "Disposition" }, { ColumnId_Rank, "Rank" }, { ColumnId_Gender, "Gender" }, { ColumnId_PcRank, "PC Rank" }, { ColumnId_ReferenceableId, "Object ID" }, { ColumnId_ContainerContent, "Content" }, { ColumnId_ItemCount, "Count" }, { ColumnId_InventoryItemId, "Item ID"}, { ColumnId_CombatState, "Combat" }, { ColumnId_MagicState, "Magic" }, { ColumnId_StealthState, "Stealth" }, { ColumnId_EnchantmentType, "Enchantment Type" }, { ColumnId_Vampire, "Vampire" }, { ColumnId_BodyPartType, "Bodypart Type" }, { ColumnId_MeshType, "Mesh Type" }, { ColumnId_ActorInventory, "Inventory" }, { ColumnId_SpellList, "Spells" }, { ColumnId_SpellId, "Spell ID"}, { ColumnId_NpcDestinations, "Destinations" }, { ColumnId_DestinationCell, "Dest Cell"}, { ColumnId_PosX, "Dest X"}, { ColumnId_PosY, "Dest Y"}, { ColumnId_PosZ, "Dest Z"}, { ColumnId_RotX, "Rotation X"}, { ColumnId_RotY, "Rotation Y"}, { ColumnId_RotZ, "Rotation Z"}, { ColumnId_OwnerGlobal, "Owner Global" }, { ColumnId_DefaultProfile, "Default Profile" }, { ColumnId_BypassNewGame, "Bypass New Game" }, { ColumnId_GlobalProfile, "Global Profile" }, { ColumnId_RefNumCounter, "RefNum Counter" }, { ColumnId_RefNum, "RefNum" }, { ColumnId_Creature, "Creature" }, { ColumnId_SoundGeneratorType, "Sound Generator Type" }, { ColumnId_AllowSpellmaking, "Allow Spellmaking" }, { ColumnId_AllowEnchanting, "Allow Enchanting" }, { ColumnId_BaseCost, "Base Cost" }, { ColumnId_School, "School" }, { ColumnId_Particle, "Particle" }, { ColumnId_CastingObject, "Casting Object" }, { ColumnId_HitObject, "Hit Object" }, { ColumnId_AreaObject, "Area Object" }, { ColumnId_BoltObject, "Bolt Object" }, { ColumnId_CastingSound, "Casting Sound" }, { ColumnId_HitSound, "Hit Sound" }, { ColumnId_AreaSound, "Area Sound" }, { ColumnId_BoltSound, "Bolt Sound" }, { ColumnId_PathgridPoints, "Points" }, { ColumnId_PathgridIndex, "pIndex" }, { ColumnId_PathgridPosX, "X" }, { ColumnId_PathgridPosY, "Y" }, { ColumnId_PathgridPosZ, "Z" }, { ColumnId_PathgridEdges, "Edges" }, { ColumnId_PathgridEdgeIndex, "eIndex" }, { ColumnId_PathgridEdge0, "Point 0" }, { ColumnId_PathgridEdge1, "Point 1" }, { ColumnId_RegionSounds, "Sounds" }, { ColumnId_SoundName, "Sound Name" }, { ColumnId_SoundChance, "Chance" }, { ColumnId_FactionReactions, "Reactions" }, { ColumnId_FactionRanks, "Ranks" }, { ColumnId_FactionReaction, "Reaction" }, { ColumnId_FactionAttrib1, "Attrib 1" }, { ColumnId_FactionAttrib2, "Attrib 2" }, { ColumnId_FactionPrimSkill, "Prim Skill" }, { ColumnId_FactionFavSkill, "Fav Skill" }, { ColumnId_FactionRep, "Fact Rep" }, { ColumnId_RankName, "Rank Name" }, { ColumnId_EffectList, "Effects" }, { ColumnId_EffectId, "Effect" }, { ColumnId_EffectRange, "Range" }, { ColumnId_EffectArea, "Area" }, { ColumnId_AiPackageList, "Ai Packages" }, { ColumnId_AiPackageType, "Package" }, { ColumnId_AiWanderDist, "Wander Dist" }, { ColumnId_AiDuration, "Ai Duration" }, { ColumnId_AiWanderToD, "Wander ToD" }, { ColumnId_AiWanderRepeat, "Wander Repeat" }, { ColumnId_AiActivateName, "Activate" }, { ColumnId_AiTargetId, "Target ID" }, { ColumnId_AiTargetCell, "Target Cell" }, { ColumnId_PartRefList, "Part Reference" }, { ColumnId_PartRefType, "Type" }, { ColumnId_PartRefMale, "Male Part" }, { ColumnId_PartRefFemale, "Female Part" }, { ColumnId_LevelledList,"Levelled List" }, { ColumnId_LevelledItemId,"Levelled Item" }, { ColumnId_LevelledItemLevel,"Item Level" }, { ColumnId_LevelledItemType, "Calculate all levels <= player" }, { ColumnId_LevelledItemTypeEach, "Select a new item each instance" }, { ColumnId_LevelledItemChanceNone, "Chance None" }, { ColumnId_PowerList, "Powers" }, { ColumnId_Skill, "Skill" }, { ColumnId_InfoList, "Info List" }, { ColumnId_InfoCondition, "Info Conditions" }, { ColumnId_InfoCondFunc, "Function" }, { ColumnId_InfoCondVar, "Variable/Object" }, { ColumnId_InfoCondComp, "Relation" }, { ColumnId_InfoCondValue, "Values" }, { ColumnId_OriginalCell, "Original Cell" }, { ColumnId_NpcAttributes, "NPC Attributes" }, { ColumnId_NpcSkills, "NPC Skill" }, { ColumnId_UChar, "Value [0..255]" }, { ColumnId_NpcMisc, "NPC Misc" }, { ColumnId_Level, "Level" }, { ColumnId_Mana, "Mana" }, { ColumnId_Fatigue, "Fatigue" }, { ColumnId_NpcDisposition, "NPC Disposition" }, { ColumnId_NpcReputation, "Reputation" }, { ColumnId_NpcRank, "NPC Rank" }, { ColumnId_Gold, "Gold" }, { ColumnId_NpcPersistence, "Persistent" }, { ColumnId_RaceAttributes, "Race Attributes" }, { ColumnId_Male, "Male" }, { ColumnId_RaceSkillBonus, "Skill Bonus" }, { ColumnId_RaceBonus, "Bonus" }, { ColumnId_Interior, "Interior" }, { ColumnId_Ambient, "Ambient" }, { ColumnId_Sunlight, "Sunlight" }, { ColumnId_Fog, "Fog" }, { ColumnId_FogDensity, "Fog Density" }, { ColumnId_WaterLevel, "Water Level" }, { ColumnId_MapColor, "Map Color" }, { ColumnId_FileFormat, "File Format" }, { ColumnId_FileDescription, "File Description" }, { ColumnId_Author, "Author" }, { ColumnId_CreatureAttributes, "Creature Attributes" }, { ColumnId_AttributeValue, "Attrib Value" }, { ColumnId_CreatureAttack, "Creature Attack" }, { ColumnId_MinAttack, "Min Attack" }, { ColumnId_MaxAttack, "Max Attack" }, { ColumnId_CreatureMisc, "Creature Misc" }, { ColumnId_Idle1, "Idle 1" }, { ColumnId_Idle2, "Idle 2" }, { ColumnId_Idle3, "Idle 3" }, { ColumnId_Idle4, "Idle 4" }, { ColumnId_Idle5, "Idle 5" }, { ColumnId_Idle6, "Idle 6" }, { ColumnId_Idle7, "Idle 7" }, { ColumnId_Idle8, "Idle 8" }, { ColumnId_RegionWeather, "Weather" }, { ColumnId_WeatherName, "Type" }, { ColumnId_WeatherChance, "Percent Chance" }, { ColumnId_Text, "Text" }, { ColumnId_TextureNickname, "Texture Nickname" }, { ColumnId_PluginIndex, "Plugin Index" }, { ColumnId_TextureIndex, "Texture Index" }, { ColumnId_LandMapLodIndex, "Land map height LOD" }, { ColumnId_LandNormalsIndex, "Land normals" }, { ColumnId_LandHeightsIndex, "Land heights" }, { ColumnId_LandColoursIndex, "Land colors" }, { ColumnId_LandTexturesIndex, "Land textures" }, { ColumnId_UseValue1, "Use value 1" }, { ColumnId_UseValue2, "Use value 2" }, { ColumnId_UseValue3, "Use value 3" }, { ColumnId_UseValue4, "Use value 4" }, { ColumnId_Attribute1, "Attribute 1" }, { ColumnId_Attribute2, "Attribute 2" }, { ColumnId_MajorSkill1, "Major Skill 1" }, { ColumnId_MajorSkill2, "Major Skill 2" }, { ColumnId_MajorSkill3, "Major Skill 3" }, { ColumnId_MajorSkill4, "Major Skill 4" }, { ColumnId_MajorSkill5, "Major Skill 5" }, { ColumnId_MinorSkill1, "Minor Skill 1" }, { ColumnId_MinorSkill2, "Minor Skill 2" }, { ColumnId_MinorSkill3, "Minor Skill 3" }, { ColumnId_MinorSkill4, "Minor Skill 4" }, { ColumnId_MinorSkill5, "Minor Skill 5" }, { ColumnId_Skill1, "Skill 1" }, { ColumnId_Skill2, "Skill 2" }, { ColumnId_Skill3, "Skill 3" }, { ColumnId_Skill4, "Skill 4" }, { ColumnId_Skill5, "Skill 5" }, { ColumnId_Skill6, "Skill 6" }, { ColumnId_Skill7, "Skill 7" }, { -1, 0 } // end marker }; } } std::string CSMWorld::Columns::getName (ColumnId column) { for (int i=0; sNames[i].mName; ++i) if (column==sNames[i].mId) return sNames[i].mName; return ""; } int CSMWorld::Columns::getId (const std::string& name) { std::string name2 = Misc::StringUtils::lowerCase (name); for (int i=0; sNames[i].mName; ++i) if (Misc::StringUtils::ciEqual(sNames[i].mName, name2)) return sNames[i].mId; return -1; } namespace { static const char *sSpecialisations[] = { "Combat", "Magic", "Stealth", 0 }; // see ESM::Attribute::AttributeID in static const char *sAttributes[] = { "Strength", "Intelligence", "Willpower", "Agility", "Speed", "Endurance", "Personality", "Luck", 0 }; static const char *sSpellTypes[] = { "Spell", "Ability", "Blight", "Disease", "Curse", "Power", 0 }; static const char *sApparatusTypes[] = { "Mortar & Pestle", "Alembic", "Calcinator", "Retort", 0 }; static const char *sArmorTypes[] = { "Helmet", "Cuirass", "Left Pauldron", "Right Pauldron", "Greaves", "Boots", "Left Gauntlet", "Right Gauntlet", "Shield", "Left Bracer", "Right Bracer", 0 }; static const char *sClothingTypes[] = { "Pants", "Shoes", "Shirt", "Belt", "Robe", "Right Glove", "Left Glove", "Skirt", "Ring", "Amulet", 0 }; static const char *sCreatureTypes[] = { "Creature", "Daedra", "Undead", "Humanoid", 0 }; static const char *sWeaponTypes[] = { "Short Blade 1H", "Long Blade 1H", "Long Blade 2H", "Blunt 1H", "Blunt 2H Close", "Blunt 2H Wide", "Spear 2H", "Axe 1H", "Axe 2H", "Bow", "Crossbow", "Thrown", "Arrow", "Bolt", 0 }; static const char *sModificationEnums[] = { "Base", "Modified", "Added", "Deleted", "Deleted", 0 }; static const char *sVarTypeEnums[] = { "unknown", "none", "short", "integer", "long", "float", "string", 0 }; static const char *sDialogueTypeEnums[] = { "Topic", "Voice", "Greeting", "Persuasion", 0 }; static const char *sQuestStatusTypes[] = { "None", "Name", "Finished", "Restart", 0 }; static const char *sGenderEnums[] = { "Male", "Female", 0 }; static const char *sEnchantmentTypes[] = { "Cast Once", "When Strikes", "When Used", "Constant Effect", 0 }; static const char *sBodyPartTypes[] = { "Head", "Hair", "Neck", "Chest", "Groin", "Hand", "Wrist", "Forearm", "Upper Arm", "Foot", "Ankle", "Knee", "Upper Leg", "Clavicle", "Tail", 0 }; static const char *sMeshTypes[] = { "Skin", "Clothing", "Armour", 0 }; static const char *sSoundGeneratorType[] = { "Left Foot", "Right Foot", "Swim Left", "Swim Right", "Moan", "Roar", "Scream", "Land", 0 }; static const char *sSchools[] = { "Alteration", "Conjuration", "Destruction", "Illusion", "Mysticism", "Restoration", 0 }; // impact from magic effects, see ESM::Skill::SkillEnum in static const char *sSkills[] = { "Block", "Armorer", "MediumArmor", "HeavyArmor", "BluntWeapon", "LongBlade", "Axe", "Spear", "Athletics", "Enchant", "Destruction", "Alteration", "Illusion", "Conjuration", "Mysticism", "Restoration", "Alchemy", "Unarmored", "Security", "Sneak", "Acrobatics", "LightArmor", "ShortBlade", "Marksman", "Mercantile", "Speechcraft", "HandToHand", 0 }; // range of magic effects, see ESM::RangeType in static const char *sEffectRange[] = { "Self", "Touch", "Target", 0 }; // magic effect names, see ESM::MagicEffect::Effects in static const char *sEffectId[] = { "WaterBreathing", "SwiftSwim", "WaterWalking", "Shield", "FireShield", "LightningShield", "FrostShield", "Burden", "Feather", "Jump", "Levitate", "SlowFall", "Lock", "Open", "FireDamage", "ShockDamage", "FrostDamage", "DrainAttribute", "DrainHealth", "DrainMagicka", "DrainFatigue", "DrainSkill", "DamageAttribute", "DamageHealth", "DamageMagicka", "DamageFatigue", "DamageSkill", "Poison", "WeaknessToFire", "WeaknessToFrost", "WeaknessToShock", "WeaknessToMagicka", "WeaknessToCommonDisease", "WeaknessToBlightDisease", "WeaknessToCorprusDisease", "WeaknessToPoison", "WeaknessToNormalWeapons", "DisintegrateWeapon", "DisintegrateArmor", "Invisibility", "Chameleon", "Light", "Sanctuary", "NightEye", "Charm", "Paralyze", "Silence", "Blind", "Sound", "CalmHumanoid", "CalmCreature", "FrenzyHumanoid", "FrenzyCreature", "DemoralizeHumanoid", "DemoralizeCreature", "RallyHumanoid", "RallyCreature", "Dispel", "Soultrap", "Telekinesis", "Mark", "Recall", "DivineIntervention", "AlmsiviIntervention", "DetectAnimal", "DetectEnchantment", "DetectKey", "SpellAbsorption", "Reflect", "CureCommonDisease", "CureBlightDisease", "CureCorprusDisease", "CurePoison", "CureParalyzation", "RestoreAttribute", "RestoreHealth", "RestoreMagicka", "RestoreFatigue", "RestoreSkill", "FortifyAttribute", "FortifyHealth", "FortifyMagicka", "FortifyFatigue", "FortifySkill", "FortifyMaximumMagicka", "AbsorbAttribute", "AbsorbHealth", "AbsorbMagicka", "AbsorbFatigue", "AbsorbSkill", "ResistFire", "ResistFrost", "ResistShock", "ResistMagicka", "ResistCommonDisease", "ResistBlightDisease", "ResistCorprusDisease", "ResistPoison", "ResistNormalWeapons", "ResistParalysis", "RemoveCurse", "TurnUndead", "SummonScamp", "SummonClannfear", "SummonDaedroth", "SummonDremora", "SummonAncestralGhost", "SummonSkeletalMinion", "SummonBonewalker", "SummonGreaterBonewalker", "SummonBonelord", "SummonWingedTwilight", "SummonHunger", "SummonGoldenSaint", "SummonFlameAtronach", "SummonFrostAtronach", "SummonStormAtronach", "FortifyAttack", "CommandCreature", "CommandHumanoid", "BoundDagger", "BoundLongsword", "BoundMace", "BoundBattleAxe", "BoundSpear", "BoundLongbow", "ExtraSpell", "BoundCuirass", "BoundHelm", "BoundBoots", "BoundShield", "BoundGloves", "Corprus", "Vampirism", "SummonCenturionSphere", "SunDamage", "StuntedMagicka", "SummonFabricant", "SummonWolf", "SummonBear", "SummonBonewolf", "SummonCreature04", "SummonCreature05", 0 }; // see ESM::PartReferenceType in static const char *sPartRefType[] = { "Head", "Hair", "Neck", "Cuirass", "Groin", "Skirt", "Right Hand", "Left Hand", "Right Wrist", "Left Wrist", "Shield", "Right Forearm", "Left Forearm", "Right Upperarm", "Left Upperarm", "Right Foot", "Left Foot", "Right Ankle", "Left Ankle", "Right Knee", "Left Knee", "Right Leg", "Left Leg", "Right Pauldron", "Left Pauldron", "Weapon", "Tail", 0 }; // see the enums in static const char *sAiPackageType[] = { "AI Wander", "AI Travel", "AI Follow", "AI Escort", "AI Activate", 0 }; static const char *sBookType[] = { "Book", "Scroll", 0 }; static const char *sEmitterType[] = { "", "Flickering", "Flickering (Slow)", "Pulsing", "Pulsing (Slow)", 0 }; const char **getEnumNames (CSMWorld::Columns::ColumnId column) { switch (column) { case CSMWorld::Columns::ColumnId_Specialisation: return sSpecialisations; case CSMWorld::Columns::ColumnId_Attribute: return sAttributes; case CSMWorld::Columns::ColumnId_SpellType: return sSpellTypes; case CSMWorld::Columns::ColumnId_ApparatusType: return sApparatusTypes; case CSMWorld::Columns::ColumnId_ArmorType: return sArmorTypes; case CSMWorld::Columns::ColumnId_ClothingType: return sClothingTypes; case CSMWorld::Columns::ColumnId_CreatureType: return sCreatureTypes; case CSMWorld::Columns::ColumnId_WeaponType: return sWeaponTypes; case CSMWorld::Columns::ColumnId_Modification: return sModificationEnums; case CSMWorld::Columns::ColumnId_ValueType: return sVarTypeEnums; case CSMWorld::Columns::ColumnId_DialogueType: return sDialogueTypeEnums; case CSMWorld::Columns::ColumnId_QuestStatusType: return sQuestStatusTypes; case CSMWorld::Columns::ColumnId_Gender: return sGenderEnums; case CSMWorld::Columns::ColumnId_EnchantmentType: return sEnchantmentTypes; case CSMWorld::Columns::ColumnId_BodyPartType: return sBodyPartTypes; case CSMWorld::Columns::ColumnId_MeshType: return sMeshTypes; case CSMWorld::Columns::ColumnId_SoundGeneratorType: return sSoundGeneratorType; case CSMWorld::Columns::ColumnId_School: return sSchools; case CSMWorld::Columns::ColumnId_Skill: return sSkills; case CSMWorld::Columns::ColumnId_EffectRange: return sEffectRange; case CSMWorld::Columns::ColumnId_EffectId: return sEffectId; case CSMWorld::Columns::ColumnId_PartRefType: return sPartRefType; case CSMWorld::Columns::ColumnId_AiPackageType: return sAiPackageType; case CSMWorld::Columns::ColumnId_InfoCondFunc: return CSMWorld::ConstInfoSelectWrapper::FunctionEnumStrings; case CSMWorld::Columns::ColumnId_InfoCondComp: return CSMWorld::ConstInfoSelectWrapper::RelationEnumStrings; case CSMWorld::Columns::ColumnId_BookType: return sBookType; case CSMWorld::Columns::ColumnId_EmitterType: return sEmitterType; default: return 0; } } } bool CSMWorld::Columns::hasEnums (ColumnId column) { return getEnumNames (column)!=0 || column==ColumnId_RecordType; } std::vector>CSMWorld::Columns::getEnums (ColumnId column) { std::vector> enums; if (const char **table = getEnumNames (column)) for (int i=0; table[i]; ++i) enums.emplace_back(i, table[i]); else if (column==ColumnId_BloodType) { for (int i=0; i<8; i++) { const std::string& bloodName = Fallback::Map::getString("Blood_Texture_Name_" + std::to_string(i)); if (!bloodName.empty()) enums.emplace_back(i, bloodName); } } else if (column==ColumnId_RecordType) { enums.emplace_back(UniversalId::Type_None, ""); // none for (int i=UniversalId::Type_None+1; i (i)).getTypeName()); } return enums; } ================================================ FILE: apps/opencs/model/world/columns.hpp ================================================ #ifndef CSM_WOLRD_COLUMNS_H #define CSM_WOLRD_COLUMNS_H #include #include #include "columnbase.hpp" namespace CSMWorld { namespace Columns { enum ColumnId { ColumnId_Value = 0, ColumnId_Id = 1, ColumnId_Modification = 2, ColumnId_RecordType = 3, ColumnId_ValueType = 4, ColumnId_Description = 5, ColumnId_Specialisation = 6, ColumnId_Attribute = 7, ColumnId_Name = 8, ColumnId_Playable = 9, ColumnId_Hidden = 10, ColumnId_MaleWeight = 11, ColumnId_FemaleWeight = 12, ColumnId_MaleHeight = 13, ColumnId_FemaleHeight = 14, ColumnId_Volume = 15, ColumnId_MinRange = 16, ColumnId_MaxRange = 17, ColumnId_SoundFile = 18, ColumnId_MapColour = 19, ColumnId_SleepEncounter = 20, ColumnId_Texture = 21, ColumnId_SpellType = 22, ColumnId_Cost = 23, ColumnId_ScriptText = 24, ColumnId_Region = 25, ColumnId_Cell = 26, ColumnId_Scale = 27, ColumnId_Owner = 28, ColumnId_Soul = 29, ColumnId_Faction = 30, ColumnId_FactionIndex = 31, ColumnId_Charges = 32, ColumnId_Enchantment = 33, ColumnId_CoinValue = 34, ColumnId_Teleport = 35, ColumnId_TeleportCell = 36, ColumnId_LockLevel = 37, ColumnId_Key = 38, ColumnId_Trap = 39, ColumnId_BeastRace = 40, ColumnId_AutoCalc = 41, ColumnId_StarterSpell = 42, ColumnId_AlwaysSucceeds = 43, ColumnId_SleepForbidden = 44, ColumnId_InteriorWater = 45, ColumnId_InteriorSky = 46, ColumnId_Model = 47, ColumnId_Script = 48, ColumnId_Icon = 49, ColumnId_Weight = 50, ColumnId_EnchantmentPoints = 51, ColumnId_Quality = 52, // unused ColumnId_AiHello = 54, ColumnId_AiFlee = 55, ColumnId_AiFight = 56, ColumnId_AiAlarm = 57, ColumnId_BuysWeapons = 58, ColumnId_BuysArmor = 59, ColumnId_BuysClothing = 60, ColumnId_BuysBooks = 61, ColumnId_BuysIngredients = 62, ColumnId_BuysLockpicks = 63, ColumnId_BuysProbes = 64, ColumnId_BuysLights = 65, ColumnId_BuysApparati = 66, ColumnId_BuysRepairItems = 67, ColumnId_BuysMiscItems = 68, ColumnId_BuysPotions = 69, ColumnId_BuysMagicItems = 70, ColumnId_SellsSpells = 71, ColumnId_Trainer = 72, ColumnId_Spellmaking = 73, ColumnId_EnchantingService = 74, ColumnId_RepairService = 75, ColumnId_ApparatusType = 76, ColumnId_ArmorType = 77, ColumnId_Health = 78, ColumnId_ArmorValue = 79, ColumnId_BookType = 80, ColumnId_ClothingType = 81, ColumnId_WeightCapacity = 82, ColumnId_OrganicContainer = 83, ColumnId_Respawn = 84, ColumnId_CreatureType = 85, ColumnId_SoulPoints = 86, ColumnId_ParentCreature = 87, ColumnId_Biped = 88, ColumnId_HasWeapon = 89, // unused ColumnId_Swims = 91, ColumnId_Flies = 92, ColumnId_Walks = 93, ColumnId_Essential = 94, ColumnId_BloodType = 95, // unused ColumnId_OpenSound = 97, ColumnId_CloseSound = 98, ColumnId_Duration = 99, ColumnId_Radius = 100, ColumnId_Colour = 101, ColumnId_Sound = 102, ColumnId_Dynamic = 103, ColumnId_Portable = 104, ColumnId_NegativeLight = 105, ColumnId_EmitterType = 106, // unused (3x) ColumnId_Fire = 110, ColumnId_OffByDefault = 111, ColumnId_IsKey = 112, ColumnId_Race = 113, ColumnId_Class = 114, Columnid_Hair = 115, ColumnId_Head = 116, ColumnId_Female = 117, ColumnId_WeaponType = 118, ColumnId_WeaponSpeed = 119, ColumnId_WeaponReach = 120, ColumnId_MinChop = 121, ColumnId_MaxChip = 122, Columnid_MinSlash = 123, ColumnId_MaxSlash = 124, ColumnId_MinThrust = 125, ColumnId_MaxThrust = 126, ColumnId_Magical = 127, ColumnId_Silver = 128, ColumnId_Filter = 129, ColumnId_PositionXPos = 130, ColumnId_PositionYPos = 131, ColumnId_PositionZPos = 132, ColumnId_PositionXRot = 133, ColumnId_PositionYRot = 134, ColumnId_PositionZRot = 135, ColumnId_DoorPositionXPos = 136, ColumnId_DoorPositionYPos = 137, ColumnId_DoorPositionZPos = 138, ColumnId_DoorPositionXRot = 139, ColumnId_DoorPositionYRot = 140, ColumnId_DoorPositionZRot = 141, ColumnId_DialogueType = 142, ColumnId_QuestIndex = 143, ColumnId_QuestStatusType = 144, ColumnId_QuestDescription = 145, ColumnId_Topic = 146, ColumnId_Journal = 147, ColumnId_Actor = 148, ColumnId_PcFaction = 149, ColumnId_Response = 150, ColumnId_Disposition = 151, ColumnId_Rank = 152, ColumnId_Gender = 153, ColumnId_PcRank = 154, ColumnId_ReferenceableId = 155, ColumnId_ContainerContent = 156, ColumnId_ItemCount = 157, ColumnId_InventoryItemId = 158, ColumnId_CombatState = 159, ColumnId_MagicState = 160, ColumnId_StealthState = 161, ColumnId_EnchantmentType = 162, ColumnId_Vampire = 163, ColumnId_BodyPartType = 164, ColumnId_MeshType = 165, ColumnId_ActorInventory = 166, ColumnId_SpellList = 167, ColumnId_SpellId = 168, ColumnId_NpcDestinations = 169, ColumnId_DestinationCell = 170, ColumnId_PosX = 171, // these are float ColumnId_PosY = 172, // these are float ColumnId_PosZ = 173, // these are float ColumnId_RotX = 174, ColumnId_RotY = 175, ColumnId_RotZ = 176, // unused ColumnId_OwnerGlobal = 178, ColumnId_DefaultProfile = 179, ColumnId_BypassNewGame = 180, ColumnId_GlobalProfile = 181, ColumnId_RefNumCounter = 182, ColumnId_RefNum = 183, ColumnId_Creature = 184, ColumnId_SoundGeneratorType = 185, ColumnId_AllowSpellmaking = 186, ColumnId_AllowEnchanting = 187, ColumnId_BaseCost = 188, ColumnId_School = 189, ColumnId_Particle = 190, ColumnId_CastingObject = 191, ColumnId_HitObject = 192, ColumnId_AreaObject = 193, ColumnId_BoltObject = 194, ColumnId_CastingSound = 195, ColumnId_HitSound = 196, ColumnId_AreaSound = 197, ColumnId_BoltSound = 198, ColumnId_PathgridPoints = 199, ColumnId_PathgridIndex = 200, ColumnId_PathgridPosX = 201, // these are int ColumnId_PathgridPosY = 202, // these are int ColumnId_PathgridPosZ = 203, // these are int ColumnId_PathgridEdges = 204, ColumnId_PathgridEdgeIndex = 205, ColumnId_PathgridEdge0 = 206, ColumnId_PathgridEdge1 = 207, ColumnId_RegionSounds = 208, ColumnId_SoundName = 209, ColumnId_SoundChance = 210, ColumnId_FactionReactions = 211, ColumnId_FactionReaction = 213, ColumnId_EffectList = 214, ColumnId_EffectId = 215, ColumnId_EffectRange = 217, ColumnId_EffectArea = 218, ColumnId_AiPackageList = 219, ColumnId_AiPackageType = 220, ColumnId_AiWanderDist = 221, ColumnId_AiDuration = 222, ColumnId_AiWanderToD = 223, // unused ColumnId_AiWanderRepeat = 225, ColumnId_AiActivateName = 226, // use ColumnId_PosX, etc for AI destinations ColumnId_AiTargetId = 227, ColumnId_AiTargetCell = 228, ColumnId_PartRefList = 229, ColumnId_PartRefType = 230, ColumnId_PartRefMale = 231, ColumnId_PartRefFemale = 232, ColumnId_LevelledList = 233, ColumnId_LevelledItemId = 234, ColumnId_LevelledItemLevel = 235, ColumnId_LevelledItemType = 236, ColumnId_LevelledItemTypeEach = 237, ColumnId_LevelledItemChanceNone = 238, ColumnId_PowerList = 239, ColumnId_Skill = 240, ColumnId_InfoList = 241, ColumnId_InfoCondition = 242, ColumnId_InfoCondFunc = 243, ColumnId_InfoCondVar = 244, ColumnId_InfoCondComp = 245, ColumnId_InfoCondValue = 246, ColumnId_OriginalCell = 247, ColumnId_NpcAttributes = 248, ColumnId_NpcSkills = 249, ColumnId_UChar = 250, ColumnId_NpcMisc = 251, ColumnId_Level = 252, // unused ColumnId_Mana = 255, ColumnId_Fatigue = 256, ColumnId_NpcDisposition = 257, ColumnId_NpcReputation = 258, ColumnId_NpcRank = 259, ColumnId_Gold = 260, ColumnId_NpcPersistence = 261, ColumnId_RaceAttributes = 262, ColumnId_Male = 263, // unused ColumnId_RaceSkillBonus = 265, // unused ColumnId_RaceBonus = 267, ColumnId_Interior = 268, ColumnId_Ambient = 269, ColumnId_Sunlight = 270, ColumnId_Fog = 271, ColumnId_FogDensity = 272, ColumnId_WaterLevel = 273, ColumnId_MapColor = 274, ColumnId_FileFormat = 275, ColumnId_FileDescription = 276, ColumnId_Author = 277, ColumnId_MinMagnitude = 278, ColumnId_MaxMagnitude = 279, ColumnId_CreatureAttributes = 280, ColumnId_AttributeValue = 281, ColumnId_CreatureAttack = 282, ColumnId_MinAttack = 283, ColumnId_MaxAttack = 284, ColumnId_CreatureMisc = 285, ColumnId_Idle1 = 286, ColumnId_Idle2 = 287, ColumnId_Idle3 = 288, ColumnId_Idle4 = 289, ColumnId_Idle5 = 290, ColumnId_Idle6 = 291, ColumnId_Idle7 = 292, ColumnId_Idle8 = 293, ColumnId_RegionWeather = 294, ColumnId_WeatherName = 295, ColumnId_WeatherChance = 296, ColumnId_Text = 297, ColumnId_TextureNickname = 298, ColumnId_PluginIndex = 299, ColumnId_TextureIndex = 300, ColumnId_LandMapLodIndex = 301, ColumnId_LandNormalsIndex = 302, ColumnId_LandHeightsIndex = 303, ColumnId_LandColoursIndex = 304, ColumnId_LandTexturesIndex = 305, ColumnId_RankName = 306, ColumnId_FactionRanks = 307, ColumnId_FactionPrimSkill = 308, ColumnId_FactionFavSkill = 309, ColumnId_FactionRep = 310, ColumnId_FactionAttrib1 = 311, ColumnId_FactionAttrib2 = 312, // Allocated to a separate value range, so we don't get a collision should we ever need // to extend the number of use values. ColumnId_UseValue1 = 0x10000, ColumnId_UseValue2 = 0x10001, ColumnId_UseValue3 = 0x10002, ColumnId_UseValue4 = 0x10003, // Allocated to a separate value range, so we don't get a collision should we ever need // to extend the number of attributes. Note that this is not the number of different // attributes, but the number of attributes that can be references from a record. ColumnId_Attribute1 = 0x20000, ColumnId_Attribute2 = 0x20001, // Allocated to a separate value range, so we don't get a collision should we ever need // to extend the number of skills. Note that this is not the number of different // skills, but the number of skills that can be references from a record. ColumnId_MajorSkill1 = 0x30000, ColumnId_MajorSkill2 = 0x30001, ColumnId_MajorSkill3 = 0x30002, ColumnId_MajorSkill4 = 0x30003, ColumnId_MajorSkill5 = 0x30004, ColumnId_MinorSkill1 = 0x40000, ColumnId_MinorSkill2 = 0x40001, ColumnId_MinorSkill3 = 0x40002, ColumnId_MinorSkill4 = 0x40003, ColumnId_MinorSkill5 = 0x40004, ColumnId_Skill1 = 0x50000, ColumnId_Skill2 = 0x50001, ColumnId_Skill3 = 0x50002, ColumnId_Skill4 = 0x50003, ColumnId_Skill5 = 0x50004, ColumnId_Skill6 = 0x50005, ColumnId_Skill7 = 0x50006 }; std::string getName (ColumnId column); int getId (const std::string& name); ///< Will return -1 for an invalid name. bool hasEnums (ColumnId column); std::vector> getEnums (ColumnId column); ///< Returns an empty vector, if \a column isn't an enum type column. } } #endif ================================================ FILE: apps/opencs/model/world/commanddispatcher.cpp ================================================ #include "commanddispatcher.hpp" #include #include #include #include #include "../doc/document.hpp" #include "idtable.hpp" #include "record.hpp" #include "commands.hpp" #include "idtableproxymodel.hpp" #include "commandmacro.hpp" std::vector CSMWorld::CommandDispatcher::getDeletableRecords() const { std::vector result; IdTable& model = dynamic_cast (*mDocument.getData().getTableModel (mId)); int stateColumnIndex = model.findColumnIndex (Columns::ColumnId_Modification); for (std::vector::const_iterator iter (mSelection.begin()); iter!=mSelection.end(); ++iter) { int row = model.getModelIndex (*iter, 0).row(); // check record state RecordBase::State state = static_cast ( model.data (model.index (row, stateColumnIndex)).toInt()); if (state==RecordBase::State_Deleted) continue; // check other columns (only relevant for a subset of the tables) int dialogueTypeIndex = model.searchColumnIndex (Columns::ColumnId_DialogueType); if (dialogueTypeIndex!=-1) { int type = model.data (model.index (row, dialogueTypeIndex)).toInt(); if (type!=ESM::Dialogue::Topic && type!=ESM::Dialogue::Journal) continue; } result.push_back (*iter); } return result; } std::vector CSMWorld::CommandDispatcher::getRevertableRecords() const { std::vector result; IdTable& model = dynamic_cast (*mDocument.getData().getTableModel (mId)); /// \todo Reverting temporarily disabled on tables that support reordering, because /// revert logic currently can not handle reordering. if (model.getFeatures() & IdTable::Feature_ReorderWithinTopic) return result; int stateColumnIndex = model.findColumnIndex (Columns::ColumnId_Modification); for (std::vector::const_iterator iter (mSelection.begin()); iter!=mSelection.end(); ++iter) { int row = model.getModelIndex (*iter, 0).row(); // check record state RecordBase::State state = static_cast ( model.data (model.index (row, stateColumnIndex)).toInt()); if (state==RecordBase::State_BaseOnly) continue; result.push_back (*iter); } return result; } CSMWorld::CommandDispatcher::CommandDispatcher (CSMDoc::Document& document, const CSMWorld::UniversalId& id, QObject *parent) : QObject (parent), mLocked (false), mDocument (document), mId (id) {} void CSMWorld::CommandDispatcher::setEditLock (bool locked) { mLocked = locked; } void CSMWorld::CommandDispatcher::setSelection (const std::vector& selection) { mSelection = selection; std::for_each (mSelection.begin(), mSelection.end(), Misc::StringUtils::lowerCaseInPlace); std::sort (mSelection.begin(), mSelection.end()); } void CSMWorld::CommandDispatcher::setExtendedTypes (const std::vector& types) { mExtendedTypes = types; } bool CSMWorld::CommandDispatcher::canDelete() const { if (mLocked) return false; return getDeletableRecords().size()!=0; } bool CSMWorld::CommandDispatcher::canRevert() const { if (mLocked) return false; return getRevertableRecords().size()!=0; } std::vector CSMWorld::CommandDispatcher::getExtendedTypes() const { std::vector tables; if (mId==UniversalId::Type_Cells) { tables.push_back (mId); tables.emplace_back(UniversalId::Type_References); /// \todo add other cell-specific types } return tables; } void CSMWorld::CommandDispatcher::executeModify (QAbstractItemModel *model, const QModelIndex& index, const QVariant& new_) { if (mLocked) return; std::unique_ptr modifyCell; std::unique_ptr modifyDataRefNum; int columnId = model->data (index, ColumnBase::Role_ColumnId).toInt(); if (columnId==Columns::ColumnId_PositionXPos || columnId==Columns::ColumnId_PositionYPos) { const float oldPosition = model->data (index).toFloat(); // Modulate by cell size, update cell id if reference has been moved to a new cell if (std::abs(std::fmod(oldPosition, Constants::CellSizeInUnits)) - std::abs(std::fmod(new_.toFloat(), Constants::CellSizeInUnits)) >= 0.5f) { IdTableProxyModel *proxy = dynamic_cast (model); int row = proxy ? proxy->mapToSource (index).row() : index.row(); // This is not guaranteed to be the same as \a model, since a proxy could be used. IdTable& model2 = dynamic_cast (*mDocument.getData().getTableModel (mId)); int cellColumn = model2.searchColumnIndex (Columns::ColumnId_Cell); if (cellColumn!=-1) { QModelIndex cellIndex = model2.index (row, cellColumn); std::string cellId = model2.data (cellIndex).toString().toUtf8().data(); if (cellId.find ('#')!=std::string::npos) { // Need to recalculate the cell and (if necessary) clear the instance's refNum modifyCell.reset (new UpdateCellCommand (model2, row)); // Not sure which model this should be applied to int refNumColumn = model2.searchColumnIndex (Columns::ColumnId_RefNum); if (refNumColumn!=-1) modifyDataRefNum.reset (new ModifyCommand(*model, model->index(row, refNumColumn), 0)); } } } } std::unique_ptr modifyData ( new CSMWorld::ModifyCommand (*model, index, new_)); if (modifyCell.get()) { CommandMacro macro (mDocument.getUndoStack()); macro.push (modifyData.release()); macro.push (modifyCell.release()); if (modifyDataRefNum.get()) macro.push (modifyDataRefNum.release()); } else mDocument.getUndoStack().push (modifyData.release()); } void CSMWorld::CommandDispatcher::executeDelete() { if (mLocked) return; std::vector rows = getDeletableRecords(); if (rows.empty()) return; IdTable& model = dynamic_cast (*mDocument.getData().getTableModel (mId)); int columnIndex = model.findColumnIndex (Columns::ColumnId_Id); CommandMacro macro (mDocument.getUndoStack(), rows.size()>1 ? "Delete multiple records" : ""); for (std::vector::const_iterator iter (rows.begin()); iter!=rows.end(); ++iter) { std::string id = model.data (model.getModelIndex (*iter, columnIndex)). toString().toUtf8().constData(); if (mId.getType() == UniversalId::Type_Referenceables) { macro.push (new CSMWorld::DeleteCommand (model, id, static_cast(model.data (model.index ( model.getModelIndex (id, columnIndex).row(), model.findColumnIndex (CSMWorld::Columns::ColumnId_RecordType))).toInt()))); } else mDocument.getUndoStack().push (new CSMWorld::DeleteCommand (model, id)); } } void CSMWorld::CommandDispatcher::executeRevert() { if (mLocked) return; std::vector rows = getRevertableRecords(); if (rows.empty()) return; IdTable& model = dynamic_cast (*mDocument.getData().getTableModel (mId)); int columnIndex = model.findColumnIndex (Columns::ColumnId_Id); CommandMacro macro (mDocument.getUndoStack(), rows.size()>1 ? "Revert multiple records" : ""); for (std::vector::const_iterator iter (rows.begin()); iter!=rows.end(); ++iter) { std::string id = model.data (model.getModelIndex (*iter, columnIndex)). toString().toUtf8().constData(); macro.push (new CSMWorld::RevertCommand (model, id)); } } void CSMWorld::CommandDispatcher::executeExtendedDelete() { CommandMacro macro (mDocument.getUndoStack(), mExtendedTypes.size()>1 ? tr ("Extended delete of multiple records") : ""); for (std::vector::const_iterator iter (mExtendedTypes.begin()); iter!=mExtendedTypes.end(); ++iter) { if (*iter==mId) executeDelete(); else if (*iter==UniversalId::Type_References) { IdTable& model = dynamic_cast ( *mDocument.getData().getTableModel (*iter)); const RefCollection& collection = mDocument.getData().getReferences(); int size = collection.getSize(); for (int i=size-1; i>=0; --i) { const Record& record = collection.getRecord (i); if (record.mState==RecordBase::State_Deleted) continue; if (!std::binary_search (mSelection.begin(), mSelection.end(), Misc::StringUtils::lowerCase (record.get().mCell))) continue; macro.push (new CSMWorld::DeleteCommand (model, record.get().mId)); } } } } void CSMWorld::CommandDispatcher::executeExtendedRevert() { CommandMacro macro (mDocument.getUndoStack(), mExtendedTypes.size()>1 ? tr ("Extended revert of multiple records") : ""); for (std::vector::const_iterator iter (mExtendedTypes.begin()); iter!=mExtendedTypes.end(); ++iter) { if (*iter==mId) executeRevert(); else if (*iter==UniversalId::Type_References) { IdTable& model = dynamic_cast ( *mDocument.getData().getTableModel (*iter)); const RefCollection& collection = mDocument.getData().getReferences(); int size = collection.getSize(); for (int i=size-1; i>=0; --i) { const Record& record = collection.getRecord (i); if (!std::binary_search (mSelection.begin(), mSelection.end(), Misc::StringUtils::lowerCase (record.get().mCell))) continue; macro.push (new CSMWorld::RevertCommand (model, record.get().mId)); } } } } ================================================ FILE: apps/opencs/model/world/commanddispatcher.hpp ================================================ #ifndef CSM_WOLRD_COMMANDDISPATCHER_H #define CSM_WOLRD_COMMANDDISPATCHER_H #include #include #include "universalid.hpp" class QModelIndex; class QAbstractItemModel; namespace CSMDoc { class Document; } namespace CSMWorld { class CommandDispatcher : public QObject { Q_OBJECT bool mLocked; CSMDoc::Document& mDocument; UniversalId mId; std::vector mSelection; std::vector mExtendedTypes; std::vector getDeletableRecords() const; std::vector getRevertableRecords() const; public: CommandDispatcher (CSMDoc::Document& document, const CSMWorld::UniversalId& id, QObject *parent = nullptr); ///< \param id ID of the table the commands should operate on primarily. void setEditLock (bool locked); void setSelection (const std::vector& selection); void setExtendedTypes (const std::vector& types); ///< Set record lists selected by the user for extended operations. bool canDelete() const; bool canRevert() const; /// Return IDs of the record collection that can also be affected when /// operating on the record collection this dispatcher is used for. /// /// \note The returned collection contains the ID of the record collection this /// dispatcher is used for. However if that record collection does not support /// the extended mode, the returned vector will be empty instead. std::vector getExtendedTypes() const; /// Add a modify command to the undo stack. /// /// \attention model must either be a model for the table operated on by this /// dispatcher or a proxy of it. void executeModify (QAbstractItemModel *model, const QModelIndex& index, const QVariant& new_); public slots: void executeDelete(); void executeRevert(); void executeExtendedDelete(); void executeExtendedRevert(); }; } #endif ================================================ FILE: apps/opencs/model/world/commandmacro.cpp ================================================ #include "commandmacro.hpp" #include #include CSMWorld::CommandMacro::CommandMacro (QUndoStack& undoStack, const QString& description) : mUndoStack (undoStack), mDescription (description), mStarted (false) {} CSMWorld::CommandMacro::~CommandMacro() { if (mStarted) mUndoStack.endMacro(); } void CSMWorld::CommandMacro::push (QUndoCommand *command) { if (!mStarted) { mUndoStack.beginMacro (mDescription.isEmpty() ? command->text() : mDescription); mStarted = true; } mUndoStack.push (command); } ================================================ FILE: apps/opencs/model/world/commandmacro.hpp ================================================ #ifndef CSM_WOLRD_COMMANDMACRO_H #define CSM_WOLRD_COMMANDMACRO_H class QUndoStack; class QUndoCommand; #include namespace CSMWorld { class CommandMacro { QUndoStack& mUndoStack; QString mDescription; bool mStarted; /// not implemented CommandMacro (const CommandMacro&); /// not implemented CommandMacro& operator= (const CommandMacro&); public: /// If \a description is empty, the description of the first command is used. CommandMacro (QUndoStack& undoStack, const QString& description = ""); ~CommandMacro(); void push (QUndoCommand *command); }; } #endif ================================================ FILE: apps/opencs/model/world/commands.cpp ================================================ #include "commands.hpp" #include #include #include #include #include #include #include "cellcoordinates.hpp" #include "idcollection.hpp" #include "idtable.hpp" #include "idtree.hpp" #include "nestedtablewrapper.hpp" #include "pathgrid.hpp" CSMWorld::TouchCommand::TouchCommand(IdTable& table, const std::string& id, QUndoCommand* parent) : QUndoCommand(parent) , mTable(table) , mId(id) , mOld(nullptr) , mChanged(false) { setText(("Touch " + mId).c_str()); mOld.reset(mTable.getRecord(mId).clone()); } void CSMWorld::TouchCommand::redo() { mChanged = mTable.touchRecord(mId); } void CSMWorld::TouchCommand::undo() { if (mChanged) { mTable.setRecord(mId, *mOld); mChanged = false; } } CSMWorld::ImportLandTexturesCommand::ImportLandTexturesCommand(IdTable& landTable, IdTable& ltexTable, QUndoCommand* parent) : QUndoCommand(parent) , mLands(landTable) , mLtexs(ltexTable) , mOldState(0) { setText("Copy land textures to current plugin"); } void CSMWorld::ImportLandTexturesCommand::redo() { int pluginColumn = mLands.findColumnIndex(Columns::ColumnId_PluginIndex); int oldPlugin = mLands.data(mLands.getModelIndex(getOriginId(), pluginColumn)).toInt(); // Original data int textureColumn = mLands.findColumnIndex(Columns::ColumnId_LandTexturesIndex); mOld = mLands.data(mLands.getModelIndex(getOriginId(), textureColumn)).value(); // Need to make a copy so the old values can be looked up DataType copy(mOld); // Perform touch/copy/etc... onRedo(); // Find all indices used std::unordered_set texIndices; for (int i = 0; i < mOld.size(); ++i) { // All indices are offset by 1 for a default texture if (mOld[i] > 0) texIndices.insert(mOld[i] - 1); } std::vector oldTextures; oldTextures.reserve(texIndices.size()); for (int index : texIndices) { oldTextures.push_back(LandTexture::createUniqueRecordId(oldPlugin, index)); } // Import the textures, replace old values LandTextureIdTable::ImportResults results = dynamic_cast(mLtexs).importTextures(oldTextures); mCreatedTextures = std::move(results.createdRecords); for (const auto& it : results.recordMapping) { int plugin = 0, newIndex = 0, oldIndex = 0; LandTexture::parseUniqueRecordId(it.first, plugin, oldIndex); LandTexture::parseUniqueRecordId(it.second, plugin, newIndex); if (newIndex != oldIndex) { for (int i = 0; i < Land::LAND_NUM_TEXTURES; ++i) { // All indices are offset by 1 for a default texture if (mOld[i] == oldIndex + 1) copy[i] = newIndex + 1; } } } // Apply modification int stateColumn = mLands.findColumnIndex(Columns::ColumnId_Modification); mOldState = mLands.data(mLands.getModelIndex(getDestinationId(), stateColumn)).toInt(); QVariant variant; variant.setValue(copy); mLands.setData(mLands.getModelIndex(getDestinationId(), textureColumn), variant); } void CSMWorld::ImportLandTexturesCommand::undo() { // Restore to previous int textureColumn = mLands.findColumnIndex(Columns::ColumnId_LandTexturesIndex); QVariant variant; variant.setValue(mOld); mLands.setData(mLands.getModelIndex(getDestinationId(), textureColumn), variant); int stateColumn = mLands.findColumnIndex(Columns::ColumnId_Modification); mLands.setData(mLands.getModelIndex(getDestinationId(), stateColumn), mOldState); // Undo copy/touch/etc... onUndo(); for (const std::string& id : mCreatedTextures) { int row = mLtexs.getModelIndex(id, 0).row(); mLtexs.removeRows(row, 1); } mCreatedTextures.clear(); } CSMWorld::CopyLandTexturesCommand::CopyLandTexturesCommand(IdTable& landTable, IdTable& ltexTable, const std::string& origin, const std::string& dest, QUndoCommand* parent) : ImportLandTexturesCommand(landTable, ltexTable, parent) , mOriginId(origin) , mDestId(dest) { } const std::string& CSMWorld::CopyLandTexturesCommand::getOriginId() const { return mOriginId; } const std::string& CSMWorld::CopyLandTexturesCommand::getDestinationId() const { return mDestId; } CSMWorld::TouchLandCommand::TouchLandCommand(IdTable& landTable, IdTable& ltexTable, const std::string& id, QUndoCommand* parent) : ImportLandTexturesCommand(landTable, ltexTable, parent) , mId(id) , mOld(nullptr) , mChanged(false) { setText(("Touch " + mId).c_str()); mOld.reset(mLands.getRecord(mId).clone()); } const std::string& CSMWorld::TouchLandCommand::getOriginId() const { return mId; } const std::string& CSMWorld::TouchLandCommand::getDestinationId() const { return mId; } void CSMWorld::TouchLandCommand::onRedo() { mChanged = mLands.touchRecord(mId); } void CSMWorld::TouchLandCommand::onUndo() { if (mChanged) { mLands.setRecord(mId, *mOld); mChanged = false; } } CSMWorld::ModifyCommand::ModifyCommand (QAbstractItemModel& model, const QModelIndex& index, const QVariant& new_, QUndoCommand* parent) : QUndoCommand (parent), mModel (&model), mIndex (index), mNew (new_), mHasRecordState(false), mOldRecordState(CSMWorld::RecordBase::State_BaseOnly) { if (QAbstractProxyModel *proxy = dynamic_cast (&model)) { // Replace proxy with actual model mIndex = proxy->mapToSource (index); mModel = proxy->sourceModel(); } if (mIndex.parent().isValid()) { CSMWorld::IdTree* tree = &dynamic_cast(*mModel); setText ("Modify " + tree->nestedHeaderData ( mIndex.parent().column(), mIndex.column(), Qt::Horizontal, Qt::DisplayRole).toString()); } else { setText ("Modify " + mModel->headerData (mIndex.column(), Qt::Horizontal, Qt::DisplayRole).toString()); } // Remember record state before the modification if (CSMWorld::IdTable *table = dynamic_cast(mModel)) { mHasRecordState = true; int stateColumnIndex = table->findColumnIndex(Columns::ColumnId_Modification); int rowIndex = mIndex.row(); if (mIndex.parent().isValid()) { rowIndex = mIndex.parent().row(); } mRecordStateIndex = table->index(rowIndex, stateColumnIndex); mOldRecordState = static_cast(table->data(mRecordStateIndex).toInt()); } } void CSMWorld::ModifyCommand::redo() { mOld = mModel->data (mIndex, Qt::EditRole); mModel->setData (mIndex, mNew); } void CSMWorld::ModifyCommand::undo() { mModel->setData (mIndex, mOld); if (mHasRecordState) { mModel->setData(mRecordStateIndex, mOldRecordState); } } void CSMWorld::CreateCommand::applyModifications() { if (!mNestedValues.empty()) { CSMWorld::IdTree* tree = &dynamic_cast(mModel); std::map >::const_iterator current = mNestedValues.begin(); std::map >::const_iterator end = mNestedValues.end(); for (; current != end; ++current) { QModelIndex index = tree->index(0, current->second.first, tree->getNestedModelIndex(mId, current->first)); tree->setData(index, current->second.second); } } } CSMWorld::CreateCommand::CreateCommand (IdTable& model, const std::string& id, QUndoCommand* parent) : QUndoCommand (parent), mModel (model), mId (id), mType (UniversalId::Type_None) { setText (("Create record " + id).c_str()); } void CSMWorld::CreateCommand::addValue (int column, const QVariant& value) { mValues[column] = value; } void CSMWorld::CreateCommand::addNestedValue(int parentColumn, int nestedColumn, const QVariant &value) { mNestedValues[parentColumn] = std::make_pair(nestedColumn, value); } void CSMWorld::CreateCommand::setType (UniversalId::Type type) { mType = type; } void CSMWorld::CreateCommand::redo() { mModel.addRecordWithData (mId, mValues, mType); applyModifications(); } void CSMWorld::CreateCommand::undo() { mModel.removeRow (mModel.getModelIndex (mId, 0).row()); } CSMWorld::RevertCommand::RevertCommand (IdTable& model, const std::string& id, QUndoCommand* parent) : QUndoCommand (parent), mModel (model), mId (id), mOld (nullptr) { setText (("Revert record " + id).c_str()); mOld = model.getRecord (id).clone(); } CSMWorld::RevertCommand::~RevertCommand() { delete mOld; } void CSMWorld::RevertCommand::redo() { int column = mModel.findColumnIndex (Columns::ColumnId_Modification); QModelIndex index = mModel.getModelIndex (mId, column); RecordBase::State state = static_cast (mModel.data (index).toInt()); if (state==RecordBase::State_ModifiedOnly) { mModel.removeRows (index.row(), 1); } else { mModel.setData (index, static_cast (RecordBase::State_BaseOnly)); } } void CSMWorld::RevertCommand::undo() { mModel.setRecord (mId, *mOld); } CSMWorld::DeleteCommand::DeleteCommand (IdTable& model, const std::string& id, CSMWorld::UniversalId::Type type, QUndoCommand* parent) : QUndoCommand (parent), mModel (model), mId (id), mOld (nullptr), mType(type) { setText (("Delete record " + id).c_str()); mOld = model.getRecord (id).clone(); } CSMWorld::DeleteCommand::~DeleteCommand() { delete mOld; } void CSMWorld::DeleteCommand::redo() { int column = mModel.findColumnIndex (Columns::ColumnId_Modification); QModelIndex index = mModel.getModelIndex (mId, column); RecordBase::State state = static_cast (mModel.data (index).toInt()); if (state==RecordBase::State_ModifiedOnly) { mModel.removeRows (index.row(), 1); } else { mModel.setData (index, static_cast (RecordBase::State_Deleted)); } } void CSMWorld::DeleteCommand::undo() { mModel.setRecord (mId, *mOld, mType); } CSMWorld::ReorderRowsCommand::ReorderRowsCommand (IdTable& model, int baseIndex, const std::vector& newOrder) : mModel (model), mBaseIndex (baseIndex), mNewOrder (newOrder) {} void CSMWorld::ReorderRowsCommand::redo() { mModel.reorderRows (mBaseIndex, mNewOrder); } void CSMWorld::ReorderRowsCommand::undo() { int size = static_cast (mNewOrder.size()); std::vector reverse (size); for (int i=0; i< size; ++i) reverse.at (mNewOrder[i]) = i; mModel.reorderRows (mBaseIndex, reverse); } CSMWorld::CloneCommand::CloneCommand (CSMWorld::IdTable& model, const std::string& idOrigin, const std::string& idDestination, const CSMWorld::UniversalId::Type type, QUndoCommand* parent) : CreateCommand (model, idDestination, parent), mIdOrigin (idOrigin) { setType (type); setText ( ("Clone record " + idOrigin + " to the " + idDestination).c_str()); } void CSMWorld::CloneCommand::redo() { mModel.cloneRecord (mIdOrigin, mId, mType); applyModifications(); for (auto& value : mOverrideValues) { mModel.setData(mModel.getModelIndex (mId, value.first), value.second); } } void CSMWorld::CloneCommand::undo() { mModel.removeRow (mModel.getModelIndex (mId, 0).row()); } void CSMWorld::CloneCommand::setOverrideValue(int column, QVariant value) { mOverrideValues.emplace_back(std::make_pair(column, value)); } CSMWorld::CreatePathgridCommand::CreatePathgridCommand(IdTable& model, const std::string& id, QUndoCommand *parent) : CreateCommand(model, id, parent) { setType(UniversalId::Type_Pathgrid); } void CSMWorld::CreatePathgridCommand::redo() { CreateCommand::redo(); Record record = static_cast& >(mModel.getRecord(mId)); record.get().blank(); record.get().mCell = mId; std::pair coords = CellCoordinates::fromId(mId); if (coords.second) { record.get().mData.mX = coords.first.getX(); record.get().mData.mY = coords.first.getY(); } mModel.setRecord(mId, record, mType); } CSMWorld::UpdateCellCommand::UpdateCellCommand (IdTable& model, int row, QUndoCommand *parent) : QUndoCommand (parent), mModel (model), mRow (row) { setText ("Update cell ID"); } void CSMWorld::UpdateCellCommand::redo() { if (!mNew.isValid()) { int cellColumn = mModel.searchColumnIndex (Columns::ColumnId_Cell); mIndex = mModel.index (mRow, cellColumn); QModelIndex xIndex = mModel.index ( mRow, mModel.findColumnIndex (Columns::ColumnId_PositionXPos)); QModelIndex yIndex = mModel.index ( mRow, mModel.findColumnIndex (Columns::ColumnId_PositionYPos)); int x = std::floor (mModel.data (xIndex).toFloat() / Constants::CellSizeInUnits); int y = std::floor (mModel.data (yIndex).toFloat() / Constants::CellSizeInUnits); std::ostringstream stream; stream << "#" << x << " " << y; mNew = QString::fromUtf8 (stream.str().c_str()); } mModel.setData (mIndex, mNew); } void CSMWorld::UpdateCellCommand::undo() { mModel.setData (mIndex, mOld); } CSMWorld::DeleteNestedCommand::DeleteNestedCommand (IdTree& model, const std::string& id, int nestedRow, int parentColumn, QUndoCommand* parent) : QUndoCommand(parent), NestedTableStoring(model, id, parentColumn), mModel(model), mId(id), mParentColumn(parentColumn), mNestedRow(nestedRow) { std::string title = model.headerData(parentColumn, Qt::Horizontal, Qt::DisplayRole).toString().toUtf8().constData(); setText (("Delete row in " + title + " sub-table of " + mId).c_str()); QModelIndex parentIndex = mModel.getModelIndex(mId, mParentColumn); mModifyParentCommand = new ModifyCommand(mModel, parentIndex, parentIndex.data(Qt::EditRole), this); } void CSMWorld::DeleteNestedCommand::redo() { QModelIndex parentIndex = mModel.getModelIndex(mId, mParentColumn); mModel.removeRows (mNestedRow, 1, parentIndex); mModifyParentCommand->redo(); } void CSMWorld::DeleteNestedCommand::undo() { QModelIndex parentIndex = mModel.getModelIndex(mId, mParentColumn); mModel.setNestedTable(parentIndex, getOld()); mModifyParentCommand->undo(); } CSMWorld::AddNestedCommand::AddNestedCommand(IdTree& model, const std::string& id, int nestedRow, int parentColumn, QUndoCommand* parent) : QUndoCommand(parent), NestedTableStoring(model, id, parentColumn), mModel(model), mId(id), mNewRow(nestedRow), mParentColumn(parentColumn) { std::string title = model.headerData(parentColumn, Qt::Horizontal, Qt::DisplayRole).toString().toUtf8().constData(); setText (("Add row in " + title + " sub-table of " + mId).c_str()); QModelIndex parentIndex = mModel.getModelIndex(mId, mParentColumn); mModifyParentCommand = new ModifyCommand(mModel, parentIndex, parentIndex.data(Qt::EditRole), this); } void CSMWorld::AddNestedCommand::redo() { QModelIndex parentIndex = mModel.getModelIndex(mId, mParentColumn); mModel.addNestedRow (parentIndex, mNewRow); mModifyParentCommand->redo(); } void CSMWorld::AddNestedCommand::undo() { QModelIndex parentIndex = mModel.getModelIndex(mId, mParentColumn); mModel.setNestedTable(parentIndex, getOld()); mModifyParentCommand->undo(); } CSMWorld::NestedTableStoring::NestedTableStoring(const IdTree& model, const std::string& id, int parentColumn) : mOld(model.nestedTable(model.getModelIndex(id, parentColumn))) {} CSMWorld::NestedTableStoring::~NestedTableStoring() { delete mOld; } const CSMWorld::NestedTableWrapperBase& CSMWorld::NestedTableStoring::getOld() const { return *mOld; } ================================================ FILE: apps/opencs/model/world/commands.hpp ================================================ #ifndef CSM_WOLRD_COMMANDS_H #define CSM_WOLRD_COMMANDS_H #include "record.hpp" #include #include #include #include #include #include #include #include "columnimp.hpp" #include "universalid.hpp" #include "nestedtablewrapper.hpp" class QModelIndex; class QAbstractItemModel; namespace CSMWorld { class IdTable; class IdTree; struct RecordBase; struct NestedTableWrapperBase; class TouchCommand : public QUndoCommand { public: TouchCommand(IdTable& model, const std::string& id, QUndoCommand* parent=nullptr); void redo() override; void undo() override; private: IdTable& mTable; std::string mId; std::unique_ptr mOld; bool mChanged; }; /// \brief Adds LandTexture records and modifies texture indices as needed. /// /// LandTexture records are different from other types of records, because /// they only effect the current plugin. Thus, when modifying or copying /// a Land record, all of the LandTexture records referenced need to be /// added to the current plugin. Since these newly added LandTextures could /// have indices that conflict with pre-existing LandTextures in the current /// plugin, the indices might have to be changed, both for the newly added /// LandRecord and within the Land record. class ImportLandTexturesCommand : public QUndoCommand { public: ImportLandTexturesCommand(IdTable& landTable, IdTable& ltexTable, QUndoCommand* parent); void redo() override; void undo() override; protected: using DataType = LandTexturesColumn::DataType; virtual const std::string& getOriginId() const = 0; virtual const std::string& getDestinationId() const = 0; virtual void onRedo() = 0; virtual void onUndo() = 0; IdTable& mLands; IdTable& mLtexs; DataType mOld; int mOldState; std::vector mCreatedTextures; }; /// \brief This command is used to fix LandTexture records and texture /// indices after cloning a Land. See ImportLandTexturesCommand for /// details. class CopyLandTexturesCommand : public ImportLandTexturesCommand { public: CopyLandTexturesCommand(IdTable& landTable, IdTable& ltexTable, const std::string& origin, const std::string& dest, QUndoCommand* parent = nullptr); private: const std::string& getOriginId() const override; const std::string& getDestinationId() const override; void onRedo() override {} void onUndo() override {} std::string mOriginId; std::string mDestId; }; /// \brief This command brings a land record into the current plugin, adding /// LandTexture records and modifying texture indices as needed. /// \note See ImportLandTextures for more details. class TouchLandCommand : public ImportLandTexturesCommand { public: TouchLandCommand(IdTable& landTable, IdTable& ltexTable, const std::string& id, QUndoCommand* parent = nullptr); private: const std::string& getOriginId() const override; const std::string& getDestinationId() const override; void onRedo() override; void onUndo() override; std::string mId; std::unique_ptr mOld; bool mChanged; }; class ModifyCommand : public QUndoCommand { QAbstractItemModel *mModel; QModelIndex mIndex; QVariant mNew; QVariant mOld; bool mHasRecordState; QModelIndex mRecordStateIndex; CSMWorld::RecordBase::State mOldRecordState; public: ModifyCommand (QAbstractItemModel& model, const QModelIndex& index, const QVariant& new_, QUndoCommand *parent = nullptr); void redo() override; void undo() override; }; class CreateCommand : public QUndoCommand { std::map mValues; std::map > mNestedValues; ///< Parameter order: a parent column, a nested column, a data. ///< A nested row has index of 0. protected: IdTable& mModel; std::string mId; UniversalId::Type mType; protected: /// Apply modifications set via addValue. void applyModifications(); public: CreateCommand (IdTable& model, const std::string& id, QUndoCommand *parent = nullptr); void setType (UniversalId::Type type); void addValue (int column, const QVariant& value); void addNestedValue(int parentColumn, int nestedColumn, const QVariant &value); void redo() override; void undo() override; }; class CloneCommand : public CreateCommand { std::string mIdOrigin; std::vector> mOverrideValues; public: CloneCommand (IdTable& model, const std::string& idOrigin, const std::string& IdDestination, const UniversalId::Type type, QUndoCommand* parent = nullptr); void redo() override; void undo() override; void setOverrideValue(int column, QVariant value); }; class RevertCommand : public QUndoCommand { IdTable& mModel; std::string mId; RecordBase *mOld; // not implemented RevertCommand (const RevertCommand&); RevertCommand& operator= (const RevertCommand&); public: RevertCommand (IdTable& model, const std::string& id, QUndoCommand *parent = nullptr); virtual ~RevertCommand(); void redo() override; void undo() override; }; class DeleteCommand : public QUndoCommand { IdTable& mModel; std::string mId; RecordBase *mOld; UniversalId::Type mType; // not implemented DeleteCommand (const DeleteCommand&); DeleteCommand& operator= (const DeleteCommand&); public: DeleteCommand (IdTable& model, const std::string& id, UniversalId::Type type = UniversalId::Type_None, QUndoCommand *parent = nullptr); virtual ~DeleteCommand(); void redo() override; void undo() override; }; class ReorderRowsCommand : public QUndoCommand { IdTable& mModel; int mBaseIndex; std::vector mNewOrder; public: ReorderRowsCommand (IdTable& model, int baseIndex, const std::vector& newOrder); void redo() override; void undo() override; }; class CreatePathgridCommand : public CreateCommand { public: CreatePathgridCommand(IdTable& model, const std::string& id, QUndoCommand *parent = nullptr); void redo() override; }; /// \brief Update cell ID according to x/y-coordinates /// /// \note The new value will be calculated in the first call to redo instead of the /// constructor to accommodate multiple coordinate-affecting commands being executed /// in a macro. class UpdateCellCommand : public QUndoCommand { IdTable& mModel; int mRow; QModelIndex mIndex; QVariant mNew; // invalid, if new cell ID has not been calculated yet QVariant mOld; public: UpdateCellCommand (IdTable& model, int row, QUndoCommand *parent = nullptr); void redo() override; void undo() override; }; class NestedTableStoring { NestedTableWrapperBase* mOld; public: NestedTableStoring(const IdTree& model, const std::string& id, int parentColumn); ~NestedTableStoring(); protected: const NestedTableWrapperBase& getOld() const; }; class DeleteNestedCommand : public QUndoCommand, private NestedTableStoring { IdTree& mModel; std::string mId; int mParentColumn; int mNestedRow; // The command to redo/undo the Modified status of a record ModifyCommand *mModifyParentCommand; public: DeleteNestedCommand (IdTree& model, const std::string& id, int nestedRow, int parentColumn, QUndoCommand* parent = nullptr); void redo() override; void undo() override; }; class AddNestedCommand : public QUndoCommand, private NestedTableStoring { IdTree& mModel; std::string mId; int mNewRow; int mParentColumn; // The command to redo/undo the Modified status of a record ModifyCommand *mModifyParentCommand; public: AddNestedCommand(IdTree& model, const std::string& id, int nestedRow, int parentColumn, QUndoCommand* parent = nullptr); void redo() override; void undo() override; }; } #endif ================================================ FILE: apps/opencs/model/world/data.cpp ================================================ #include "data.hpp" #include #include #include #include #include #include #include #include #include #include #include #include #include "idtable.hpp" #include "idtree.hpp" #include "columnimp.hpp" #include "regionmap.hpp" #include "columns.hpp" #include "resourcesmanager.hpp" #include "resourcetable.hpp" #include "nestedcoladapterimp.hpp" void CSMWorld::Data::addModel (QAbstractItemModel *model, UniversalId::Type type, bool update) { mModels.push_back (model); mModelIndex.insert (std::make_pair (type, model)); UniversalId::Type type2 = UniversalId::getParentType (type); if (type2!=UniversalId::Type_None) mModelIndex.insert (std::make_pair (type2, model)); if (update) { connect (model, SIGNAL (dataChanged (const QModelIndex&, const QModelIndex&)), this, SLOT (dataChanged (const QModelIndex&, const QModelIndex&))); connect (model, SIGNAL (rowsInserted (const QModelIndex&, int, int)), this, SLOT (rowsChanged (const QModelIndex&, int, int))); connect (model, SIGNAL (rowsRemoved (const QModelIndex&, int, int)), this, SLOT (rowsChanged (const QModelIndex&, int, int))); } } void CSMWorld::Data::appendIds (std::vector& ids, const CollectionBase& collection, bool listDeleted) { std::vector ids2 = collection.getIds (listDeleted); ids.insert (ids.end(), ids2.begin(), ids2.end()); } int CSMWorld::Data::count (RecordBase::State state, const CollectionBase& collection) { int number = 0; for (int i=0; i& archives, const boost::filesystem::path& resDir) : mEncoder (encoding), mPathgrids (mCells), mRefs (mCells), mReader (nullptr), mDialogue (nullptr), mReaderIndex(1), mFsStrict(fsStrict), mDataPaths(dataPaths), mArchives(archives) { mVFS.reset(new VFS::Manager(mFsStrict)); VFS::registerArchives(mVFS.get(), Files::Collections(mDataPaths, !mFsStrict), mArchives, true); mResourcesManager.setVFS(mVFS.get()); mResourceSystem.reset(new Resource::ResourceSystem(mVFS.get())); Shader::ShaderManager::DefineMap defines = mResourceSystem->getSceneManager()->getShaderManager().getGlobalDefines(); Shader::ShaderManager::DefineMap shadowDefines = SceneUtil::ShadowManager::getShadowsDisabledDefines(); defines["forcePPL"] = "0"; // Don't force per-pixel lighting defines["clamp"] = "1"; // Clamp lighting defines["preLightEnv"] = "0"; // Apply environment maps after lighting like Morrowind defines["radialFog"] = "0"; defines["lightingModel"] = "0"; for (const auto& define : shadowDefines) defines[define.first] = define.second; mResourceSystem->getSceneManager()->getShaderManager().setGlobalDefines(defines); mResourceSystem->getSceneManager()->setShaderPath((resDir / "shaders").string()); int index = 0; mGlobals.addColumn (new StringIdColumn); mGlobals.addColumn (new RecordStateColumn); mGlobals.addColumn (new FixedRecordTypeColumn (UniversalId::Type_Global)); mGlobals.addColumn (new VarTypeColumn (ColumnBase::Display_GlobalVarType)); mGlobals.addColumn (new VarValueColumn); mGmsts.addColumn (new StringIdColumn); mGmsts.addColumn (new RecordStateColumn); mGmsts.addColumn (new FixedRecordTypeColumn (UniversalId::Type_Gmst)); mGmsts.addColumn (new VarTypeColumn (ColumnBase::Display_GmstVarType)); mGmsts.addColumn (new VarValueColumn); mSkills.addColumn (new StringIdColumn); mSkills.addColumn (new RecordStateColumn); mSkills.addColumn (new FixedRecordTypeColumn (UniversalId::Type_Skill)); mSkills.addColumn (new AttributeColumn); mSkills.addColumn (new SpecialisationColumn); for (int i=0; i<4; ++i) mSkills.addColumn (new UseValueColumn (i)); mSkills.addColumn (new DescriptionColumn); mClasses.addColumn (new StringIdColumn); mClasses.addColumn (new RecordStateColumn); mClasses.addColumn (new FixedRecordTypeColumn (UniversalId::Type_Class)); mClasses.addColumn (new NameColumn); mClasses.addColumn (new AttributesColumn (0)); mClasses.addColumn (new AttributesColumn (1)); mClasses.addColumn (new SpecialisationColumn); for (int i=0; i<5; ++i) mClasses.addColumn (new SkillsColumn (i, true, true)); for (int i=0; i<5; ++i) mClasses.addColumn (new SkillsColumn (i, true, false)); mClasses.addColumn (new PlayableColumn); mClasses.addColumn (new DescriptionColumn); mFactions.addColumn (new StringIdColumn); mFactions.addColumn (new RecordStateColumn); mFactions.addColumn (new FixedRecordTypeColumn (UniversalId::Type_Faction)); mFactions.addColumn (new NameColumn); mFactions.addColumn (new AttributesColumn (0)); mFactions.addColumn (new AttributesColumn (1)); mFactions.addColumn (new HiddenColumn); for (int i=0; i<7; ++i) mFactions.addColumn (new SkillsColumn (i)); // Faction Reactions mFactions.addColumn (new NestedParentColumn (Columns::ColumnId_FactionReactions)); index = mFactions.getColumns()-1; mFactions.addAdapter (std::make_pair(&mFactions.getColumn(index), new FactionReactionsAdapter ())); mFactions.getNestableColumn(index)->addColumn( new NestedChildColumn (Columns::ColumnId_Faction, ColumnBase::Display_Faction)); mFactions.getNestableColumn(index)->addColumn( new NestedChildColumn (Columns::ColumnId_FactionReaction, ColumnBase::Display_Integer)); // Faction Ranks mFactions.addColumn (new NestedParentColumn (Columns::ColumnId_FactionRanks)); index = mFactions.getColumns()-1; mFactions.addAdapter (std::make_pair(&mFactions.getColumn(index), new FactionRanksAdapter ())); mFactions.getNestableColumn(index)->addColumn( new NestedChildColumn (Columns::ColumnId_RankName, ColumnBase::Display_Rank)); mFactions.getNestableColumn(index)->addColumn( new NestedChildColumn (Columns::ColumnId_FactionAttrib1, ColumnBase::Display_Integer)); mFactions.getNestableColumn(index)->addColumn( new NestedChildColumn (Columns::ColumnId_FactionAttrib2, ColumnBase::Display_Integer)); mFactions.getNestableColumn(index)->addColumn( new NestedChildColumn (Columns::ColumnId_FactionPrimSkill, ColumnBase::Display_Integer)); mFactions.getNestableColumn(index)->addColumn( new NestedChildColumn (Columns::ColumnId_FactionFavSkill, ColumnBase::Display_Integer)); mFactions.getNestableColumn(index)->addColumn( new NestedChildColumn (Columns::ColumnId_FactionRep, ColumnBase::Display_Integer)); mRaces.addColumn (new StringIdColumn); mRaces.addColumn (new RecordStateColumn); mRaces.addColumn (new FixedRecordTypeColumn (UniversalId::Type_Race)); mRaces.addColumn (new NameColumn); mRaces.addColumn (new DescriptionColumn); mRaces.addColumn (new FlagColumn (Columns::ColumnId_Playable, 0x1)); mRaces.addColumn (new FlagColumn (Columns::ColumnId_BeastRace, 0x2)); mRaces.addColumn (new WeightHeightColumn (true, true)); mRaces.addColumn (new WeightHeightColumn (true, false)); mRaces.addColumn (new WeightHeightColumn (false, true)); mRaces.addColumn (new WeightHeightColumn (false, false)); // Race spells mRaces.addColumn (new NestedParentColumn (Columns::ColumnId_PowerList)); index = mRaces.getColumns()-1; mRaces.addAdapter (std::make_pair(&mRaces.getColumn(index), new SpellListAdapter ())); mRaces.getNestableColumn(index)->addColumn( new NestedChildColumn (Columns::ColumnId_SpellId, ColumnBase::Display_Spell)); // Race attributes mRaces.addColumn (new NestedParentColumn (Columns::ColumnId_RaceAttributes, ColumnBase::Flag_Dialogue, true)); // fixed rows table index = mRaces.getColumns()-1; mRaces.addAdapter (std::make_pair(&mRaces.getColumn(index), new RaceAttributeAdapter())); mRaces.getNestableColumn(index)->addColumn( new NestedChildColumn (Columns::ColumnId_Attribute, ColumnBase::Display_Attribute, ColumnBase::Flag_Dialogue, false)); mRaces.getNestableColumn(index)->addColumn( new NestedChildColumn (Columns::ColumnId_Male, ColumnBase::Display_Integer)); mRaces.getNestableColumn(index)->addColumn( new NestedChildColumn (Columns::ColumnId_Female, ColumnBase::Display_Integer)); // Race skill bonus mRaces.addColumn (new NestedParentColumn (Columns::ColumnId_RaceSkillBonus, ColumnBase::Flag_Dialogue, true)); // fixed rows table index = mRaces.getColumns()-1; mRaces.addAdapter (std::make_pair(&mRaces.getColumn(index), new RaceSkillsBonusAdapter())); mRaces.getNestableColumn(index)->addColumn( new NestedChildColumn (Columns::ColumnId_Skill, ColumnBase::Display_SkillId)); mRaces.getNestableColumn(index)->addColumn( new NestedChildColumn (Columns::ColumnId_RaceBonus, ColumnBase::Display_Integer)); mSounds.addColumn (new StringIdColumn); mSounds.addColumn (new RecordStateColumn); mSounds.addColumn (new FixedRecordTypeColumn (UniversalId::Type_Sound)); mSounds.addColumn (new SoundParamColumn (SoundParamColumn::Type_Volume)); mSounds.addColumn (new SoundParamColumn (SoundParamColumn::Type_MinRange)); mSounds.addColumn (new SoundParamColumn (SoundParamColumn::Type_MaxRange)); mSounds.addColumn (new SoundFileColumn); mScripts.addColumn (new StringIdColumn); mScripts.addColumn (new RecordStateColumn); mScripts.addColumn (new FixedRecordTypeColumn (UniversalId::Type_Script)); mScripts.addColumn (new ScriptColumn (ScriptColumn::Type_File)); mRegions.addColumn (new StringIdColumn); mRegions.addColumn (new RecordStateColumn); mRegions.addColumn (new FixedRecordTypeColumn (UniversalId::Type_Region)); mRegions.addColumn (new NameColumn); mRegions.addColumn (new MapColourColumn); mRegions.addColumn (new SleepListColumn); // Region Weather mRegions.addColumn (new NestedParentColumn (Columns::ColumnId_RegionWeather)); index = mRegions.getColumns()-1; mRegions.addAdapter (std::make_pair(&mRegions.getColumn(index), new RegionWeatherAdapter ())); mRegions.getNestableColumn(index)->addColumn( new NestedChildColumn (Columns::ColumnId_WeatherName, ColumnBase::Display_String, false)); mRegions.getNestableColumn(index)->addColumn( new NestedChildColumn (Columns::ColumnId_WeatherChance, ColumnBase::Display_UnsignedInteger8)); // Region Sounds mRegions.addColumn (new NestedParentColumn (Columns::ColumnId_RegionSounds)); index = mRegions.getColumns()-1; mRegions.addAdapter (std::make_pair(&mRegions.getColumn(index), new RegionSoundListAdapter ())); mRegions.getNestableColumn(index)->addColumn( new NestedChildColumn (Columns::ColumnId_SoundName, ColumnBase::Display_Sound)); mRegions.getNestableColumn(index)->addColumn( new NestedChildColumn (Columns::ColumnId_SoundChance, ColumnBase::Display_UnsignedInteger8)); mBirthsigns.addColumn (new StringIdColumn); mBirthsigns.addColumn (new RecordStateColumn); mBirthsigns.addColumn (new FixedRecordTypeColumn (UniversalId::Type_Birthsign)); mBirthsigns.addColumn (new NameColumn); mBirthsigns.addColumn (new TextureColumn); mBirthsigns.addColumn (new DescriptionColumn); // Birthsign spells mBirthsigns.addColumn (new NestedParentColumn (Columns::ColumnId_PowerList)); index = mBirthsigns.getColumns()-1; mBirthsigns.addAdapter (std::make_pair(&mBirthsigns.getColumn(index), new SpellListAdapter ())); mBirthsigns.getNestableColumn(index)->addColumn( new NestedChildColumn (Columns::ColumnId_SpellId, ColumnBase::Display_Spell)); mSpells.addColumn (new StringIdColumn); mSpells.addColumn (new RecordStateColumn); mSpells.addColumn (new FixedRecordTypeColumn (UniversalId::Type_Spell)); mSpells.addColumn (new NameColumn); mSpells.addColumn (new SpellTypeColumn); mSpells.addColumn (new CostColumn); mSpells.addColumn (new FlagColumn (Columns::ColumnId_AutoCalc, 0x1)); mSpells.addColumn (new FlagColumn (Columns::ColumnId_StarterSpell, 0x2)); mSpells.addColumn (new FlagColumn (Columns::ColumnId_AlwaysSucceeds, 0x4)); // Spell effects mSpells.addColumn (new NestedParentColumn (Columns::ColumnId_EffectList)); index = mSpells.getColumns()-1; mSpells.addAdapter (std::make_pair(&mSpells.getColumn(index), new EffectsListAdapter ())); mSpells.getNestableColumn(index)->addColumn( new NestedChildColumn (Columns::ColumnId_EffectId, ColumnBase::Display_EffectId)); mSpells.getNestableColumn(index)->addColumn( new NestedChildColumn (Columns::ColumnId_Skill, ColumnBase::Display_EffectSkill)); mSpells.getNestableColumn(index)->addColumn( new NestedChildColumn (Columns::ColumnId_Attribute, ColumnBase::Display_EffectAttribute)); mSpells.getNestableColumn(index)->addColumn( new NestedChildColumn (Columns::ColumnId_EffectRange, ColumnBase::Display_EffectRange)); mSpells.getNestableColumn(index)->addColumn( new NestedChildColumn (Columns::ColumnId_EffectArea, ColumnBase::Display_String)); mSpells.getNestableColumn(index)->addColumn( new NestedChildColumn (Columns::ColumnId_Duration, ColumnBase::Display_Integer)); // reuse from light mSpells.getNestableColumn(index)->addColumn( new NestedChildColumn (Columns::ColumnId_MinMagnitude, ColumnBase::Display_Integer)); mSpells.getNestableColumn(index)->addColumn( new NestedChildColumn (Columns::ColumnId_MaxMagnitude, ColumnBase::Display_Integer)); mTopics.addColumn (new StringIdColumn); mTopics.addColumn (new RecordStateColumn); mTopics.addColumn (new FixedRecordTypeColumn (UniversalId::Type_Topic)); mTopics.addColumn (new DialogueTypeColumn); mJournals.addColumn (new StringIdColumn); mJournals.addColumn (new RecordStateColumn); mJournals.addColumn (new FixedRecordTypeColumn (UniversalId::Type_Journal)); mJournals.addColumn (new DialogueTypeColumn (true)); mTopicInfos.addColumn (new StringIdColumn (true)); mTopicInfos.addColumn (new RecordStateColumn); mTopicInfos.addColumn (new FixedRecordTypeColumn (UniversalId::Type_TopicInfo)); mTopicInfos.addColumn (new TopicColumn (false)); mTopicInfos.addColumn (new ActorColumn); mTopicInfos.addColumn (new RaceColumn); mTopicInfos.addColumn (new ClassColumn); mTopicInfos.addColumn (new FactionColumn); mTopicInfos.addColumn (new CellColumn); mTopicInfos.addColumn (new DispositionColumn); mTopicInfos.addColumn (new RankColumn); mTopicInfos.addColumn (new GenderColumn); mTopicInfos.addColumn (new PcFactionColumn); mTopicInfos.addColumn (new PcRankColumn); mTopicInfos.addColumn (new SoundFileColumn); mTopicInfos.addColumn (new ResponseColumn); // Result script mTopicInfos.addColumn (new NestedParentColumn (Columns::ColumnId_InfoList, ColumnBase::Flag_Dialogue | ColumnBase::Flag_Dialogue_List)); index = mTopicInfos.getColumns()-1; mTopicInfos.addAdapter (std::make_pair(&mTopicInfos.getColumn(index), new InfoListAdapter ())); mTopicInfos.getNestableColumn(index)->addColumn( new NestedChildColumn (Columns::ColumnId_ScriptText, ColumnBase::Display_ScriptLines)); // Special conditions mTopicInfos.addColumn (new NestedParentColumn (Columns::ColumnId_InfoCondition)); index = mTopicInfos.getColumns()-1; mTopicInfos.addAdapter (std::make_pair(&mTopicInfos.getColumn(index), new InfoConditionAdapter ())); mTopicInfos.getNestableColumn(index)->addColumn( new NestedChildColumn (Columns::ColumnId_InfoCondFunc, ColumnBase::Display_InfoCondFunc)); // FIXME: don't have dynamic value enum delegate, use Display_String for now mTopicInfos.getNestableColumn(index)->addColumn( new NestedChildColumn (Columns::ColumnId_InfoCondVar, ColumnBase::Display_InfoCondVar)); mTopicInfos.getNestableColumn(index)->addColumn( new NestedChildColumn (Columns::ColumnId_InfoCondComp, ColumnBase::Display_InfoCondComp)); mTopicInfos.getNestableColumn(index)->addColumn( new NestedChildColumn (Columns::ColumnId_Value, ColumnBase::Display_Var)); mJournalInfos.addColumn (new StringIdColumn (true)); mJournalInfos.addColumn (new RecordStateColumn); mJournalInfos.addColumn (new FixedRecordTypeColumn (UniversalId::Type_JournalInfo)); mJournalInfos.addColumn (new TopicColumn (true)); mJournalInfos.addColumn (new QuestStatusTypeColumn); mJournalInfos.addColumn (new QuestIndexColumn); mJournalInfos.addColumn (new QuestDescriptionColumn); mCells.addColumn (new StringIdColumn); mCells.addColumn (new RecordStateColumn); mCells.addColumn (new FixedRecordTypeColumn (UniversalId::Type_Cell)); mCells.addColumn (new NameColumn); mCells.addColumn (new FlagColumn (Columns::ColumnId_SleepForbidden, ESM::Cell::NoSleep)); mCells.addColumn (new FlagColumn (Columns::ColumnId_InteriorWater, ESM::Cell::HasWater, ColumnBase::Flag_Table | ColumnBase::Flag_Dialogue | ColumnBase::Flag_Dialogue_Refresh)); mCells.addColumn (new FlagColumn (Columns::ColumnId_InteriorSky, ESM::Cell::QuasiEx, ColumnBase::Flag_Table | ColumnBase::Flag_Dialogue | ColumnBase::Flag_Dialogue_Refresh)); mCells.addColumn (new RegionColumn); mCells.addColumn (new RefNumCounterColumn); // Misc Cell data mCells.addColumn (new NestedParentColumn (Columns::ColumnId_Cell, ColumnBase::Flag_Dialogue | ColumnBase::Flag_Dialogue_List)); index = mCells.getColumns()-1; mCells.addAdapter (std::make_pair(&mCells.getColumn(index), new CellListAdapter ())); mCells.getNestableColumn(index)->addColumn( new NestedChildColumn (Columns::ColumnId_Interior, ColumnBase::Display_Boolean, ColumnBase::Flag_Table | ColumnBase::Flag_Dialogue | ColumnBase::Flag_Dialogue_Refresh)); mCells.getNestableColumn(index)->addColumn( new NestedChildColumn (Columns::ColumnId_Ambient, ColumnBase::Display_Colour)); mCells.getNestableColumn(index)->addColumn( new NestedChildColumn (Columns::ColumnId_Sunlight, ColumnBase::Display_Colour)); mCells.getNestableColumn(index)->addColumn( new NestedChildColumn (Columns::ColumnId_Fog, ColumnBase::Display_Colour)); mCells.getNestableColumn(index)->addColumn( new NestedChildColumn (Columns::ColumnId_FogDensity, ColumnBase::Display_Float)); mCells.getNestableColumn(index)->addColumn( new NestedChildColumn (Columns::ColumnId_WaterLevel, ColumnBase::Display_Float)); mCells.getNestableColumn(index)->addColumn( new NestedChildColumn (Columns::ColumnId_MapColor, ColumnBase::Display_Colour)); mEnchantments.addColumn (new StringIdColumn); mEnchantments.addColumn (new RecordStateColumn); mEnchantments.addColumn (new FixedRecordTypeColumn (UniversalId::Type_Enchantment)); mEnchantments.addColumn (new EnchantmentTypeColumn); mEnchantments.addColumn (new CostColumn); mEnchantments.addColumn (new ChargesColumn2); mEnchantments.addColumn (new FlagColumn (Columns::ColumnId_AutoCalc, ESM::Enchantment::Autocalc)); // Enchantment effects mEnchantments.addColumn (new NestedParentColumn (Columns::ColumnId_EffectList)); index = mEnchantments.getColumns()-1; mEnchantments.addAdapter (std::make_pair(&mEnchantments.getColumn(index), new EffectsListAdapter ())); mEnchantments.getNestableColumn(index)->addColumn( new NestedChildColumn (Columns::ColumnId_EffectId, ColumnBase::Display_EffectId)); mEnchantments.getNestableColumn(index)->addColumn( new NestedChildColumn (Columns::ColumnId_Skill, ColumnBase::Display_EffectSkill)); mEnchantments.getNestableColumn(index)->addColumn( new NestedChildColumn (Columns::ColumnId_Attribute, ColumnBase::Display_EffectAttribute)); mEnchantments.getNestableColumn(index)->addColumn( new NestedChildColumn (Columns::ColumnId_EffectRange, ColumnBase::Display_EffectRange)); mEnchantments.getNestableColumn(index)->addColumn( new NestedChildColumn (Columns::ColumnId_EffectArea, ColumnBase::Display_String)); mEnchantments.getNestableColumn(index)->addColumn( new NestedChildColumn (Columns::ColumnId_Duration, ColumnBase::Display_Integer)); // reuse from light mEnchantments.getNestableColumn(index)->addColumn( new NestedChildColumn (Columns::ColumnId_MinMagnitude, ColumnBase::Display_Integer)); mEnchantments.getNestableColumn(index)->addColumn( new NestedChildColumn (Columns::ColumnId_MaxMagnitude, ColumnBase::Display_Integer)); mBodyParts.addColumn (new StringIdColumn); mBodyParts.addColumn (new RecordStateColumn); mBodyParts.addColumn (new FixedRecordTypeColumn (UniversalId::Type_BodyPart)); mBodyParts.addColumn (new BodyPartTypeColumn); mBodyParts.addColumn (new VampireColumn); mBodyParts.addColumn(new GenderNpcColumn); mBodyParts.addColumn (new FlagColumn (Columns::ColumnId_Playable, ESM::BodyPart::BPF_NotPlayable, ColumnBase::Flag_Table | ColumnBase::Flag_Dialogue, true)); int meshTypeFlags = ColumnBase::Flag_Table | ColumnBase::Flag_Dialogue | ColumnBase::Flag_Dialogue_Refresh; MeshTypeColumn *meshTypeColumn = new MeshTypeColumn(meshTypeFlags); mBodyParts.addColumn (meshTypeColumn); mBodyParts.addColumn (new ModelColumn); mBodyParts.addColumn (new BodyPartRaceColumn(meshTypeColumn)); mSoundGens.addColumn (new StringIdColumn); mSoundGens.addColumn (new RecordStateColumn); mSoundGens.addColumn (new FixedRecordTypeColumn (UniversalId::Type_SoundGen)); mSoundGens.addColumn (new CreatureColumn); mSoundGens.addColumn (new SoundColumn); mSoundGens.addColumn (new SoundGeneratorTypeColumn); mMagicEffects.addColumn (new StringIdColumn); mMagicEffects.addColumn (new RecordStateColumn); mMagicEffects.addColumn (new FixedRecordTypeColumn (UniversalId::Type_MagicEffect)); mMagicEffects.addColumn (new SchoolColumn); mMagicEffects.addColumn (new BaseCostColumn); mMagicEffects.addColumn (new EffectTextureColumn (Columns::ColumnId_Icon)); mMagicEffects.addColumn (new EffectTextureColumn (Columns::ColumnId_Particle)); mMagicEffects.addColumn (new EffectObjectColumn (Columns::ColumnId_CastingObject)); mMagicEffects.addColumn (new EffectObjectColumn (Columns::ColumnId_HitObject)); mMagicEffects.addColumn (new EffectObjectColumn (Columns::ColumnId_AreaObject)); mMagicEffects.addColumn (new EffectObjectColumn (Columns::ColumnId_BoltObject)); mMagicEffects.addColumn (new EffectSoundColumn (Columns::ColumnId_CastingSound)); mMagicEffects.addColumn (new EffectSoundColumn (Columns::ColumnId_HitSound)); mMagicEffects.addColumn (new EffectSoundColumn (Columns::ColumnId_AreaSound)); mMagicEffects.addColumn (new EffectSoundColumn (Columns::ColumnId_BoltSound)); mMagicEffects.addColumn (new FlagColumn ( Columns::ColumnId_AllowSpellmaking, ESM::MagicEffect::AllowSpellmaking)); mMagicEffects.addColumn (new FlagColumn ( Columns::ColumnId_AllowEnchanting, ESM::MagicEffect::AllowEnchanting)); mMagicEffects.addColumn (new FlagColumn ( Columns::ColumnId_NegativeLight, ESM::MagicEffect::NegativeLight)); mMagicEffects.addColumn (new DescriptionColumn); mLand.addColumn (new StringIdColumn); mLand.addColumn (new RecordStateColumn); mLand.addColumn (new FixedRecordTypeColumn(UniversalId::Type_Land)); mLand.addColumn (new LandPluginIndexColumn); mLand.addColumn (new LandNormalsColumn); mLand.addColumn (new LandHeightsColumn); mLand.addColumn (new LandColoursColumn); mLand.addColumn (new LandTexturesColumn); mLandTextures.addColumn (new StringIdColumn(true)); mLandTextures.addColumn (new RecordStateColumn); mLandTextures.addColumn (new FixedRecordTypeColumn(UniversalId::Type_LandTexture)); mLandTextures.addColumn (new LandTextureNicknameColumn); mLandTextures.addColumn (new LandTexturePluginIndexColumn); mLandTextures.addColumn (new LandTextureIndexColumn); mLandTextures.addColumn (new TextureColumn); mPathgrids.addColumn (new StringIdColumn); mPathgrids.addColumn (new RecordStateColumn); mPathgrids.addColumn (new FixedRecordTypeColumn (UniversalId::Type_Pathgrid)); // new object deleted in dtor of Collection mPathgrids.addColumn (new NestedParentColumn (Columns::ColumnId_PathgridPoints)); index = mPathgrids.getColumns()-1; // new object deleted in dtor of NestedCollection mPathgrids.addAdapter (std::make_pair(&mPathgrids.getColumn(index), new PathgridPointListAdapter ())); // new objects deleted in dtor of NestableColumn // WARNING: The order of the columns below are assumed in PathgridPointListAdapter mPathgrids.getNestableColumn(index)->addColumn( new NestedChildColumn (Columns::ColumnId_PathgridIndex, ColumnBase::Display_Integer, ColumnBase::Flag_Dialogue, false)); mPathgrids.getNestableColumn(index)->addColumn( new NestedChildColumn (Columns::ColumnId_PathgridPosX, ColumnBase::Display_Integer)); mPathgrids.getNestableColumn(index)->addColumn( new NestedChildColumn (Columns::ColumnId_PathgridPosY, ColumnBase::Display_Integer)); mPathgrids.getNestableColumn(index)->addColumn( new NestedChildColumn (Columns::ColumnId_PathgridPosZ, ColumnBase::Display_Integer)); mPathgrids.addColumn (new NestedParentColumn (Columns::ColumnId_PathgridEdges)); index = mPathgrids.getColumns()-1; mPathgrids.addAdapter (std::make_pair(&mPathgrids.getColumn(index), new PathgridEdgeListAdapter ())); mPathgrids.getNestableColumn(index)->addColumn( new NestedChildColumn (Columns::ColumnId_PathgridEdgeIndex, ColumnBase::Display_Integer, ColumnBase::Flag_Dialogue, false)); mPathgrids.getNestableColumn(index)->addColumn( new NestedChildColumn (Columns::ColumnId_PathgridEdge0, ColumnBase::Display_Integer)); mPathgrids.getNestableColumn(index)->addColumn( new NestedChildColumn (Columns::ColumnId_PathgridEdge1, ColumnBase::Display_Integer)); mStartScripts.addColumn (new StringIdColumn); mStartScripts.addColumn (new RecordStateColumn); mStartScripts.addColumn (new FixedRecordTypeColumn (UniversalId::Type_StartScript)); mRefs.addColumn (new StringIdColumn (true)); mRefs.addColumn (new RecordStateColumn); mRefs.addColumn (new FixedRecordTypeColumn (UniversalId::Type_Reference)); mRefs.addColumn (new CellColumn (true)); mRefs.addColumn (new OriginalCellColumn); mRefs.addColumn (new IdColumn); mRefs.addColumn (new PosColumn (&CellRef::mPos, 0, false)); mRefs.addColumn (new PosColumn (&CellRef::mPos, 1, false)); mRefs.addColumn (new PosColumn (&CellRef::mPos, 2, false)); mRefs.addColumn (new RotColumn (&CellRef::mPos, 0, false)); mRefs.addColumn (new RotColumn (&CellRef::mPos, 1, false)); mRefs.addColumn (new RotColumn (&CellRef::mPos, 2, false)); mRefs.addColumn (new ScaleColumn); mRefs.addColumn (new OwnerColumn); mRefs.addColumn (new SoulColumn); mRefs.addColumn (new FactionColumn); mRefs.addColumn (new FactionIndexColumn); mRefs.addColumn (new ChargesColumn); mRefs.addColumn (new EnchantmentChargesColumn); mRefs.addColumn (new GoldValueColumn); mRefs.addColumn (new TeleportColumn); mRefs.addColumn (new TeleportCellColumn); mRefs.addColumn (new PosColumn (&CellRef::mDoorDest, 0, true)); mRefs.addColumn (new PosColumn (&CellRef::mDoorDest, 1, true)); mRefs.addColumn (new PosColumn (&CellRef::mDoorDest, 2, true)); mRefs.addColumn (new RotColumn (&CellRef::mDoorDest, 0, true)); mRefs.addColumn (new RotColumn (&CellRef::mDoorDest, 1, true)); mRefs.addColumn (new RotColumn (&CellRef::mDoorDest, 2, true)); mRefs.addColumn (new LockLevelColumn); mRefs.addColumn (new KeyColumn); mRefs.addColumn (new TrapColumn); mRefs.addColumn (new OwnerGlobalColumn); mRefs.addColumn (new RefNumColumn); mFilters.addColumn (new StringIdColumn); mFilters.addColumn (new RecordStateColumn); mFilters.addColumn (new FixedRecordTypeColumn (UniversalId::Type_Filter)); mFilters.addColumn (new FilterColumn); mFilters.addColumn (new DescriptionColumn); mDebugProfiles.addColumn (new StringIdColumn); mDebugProfiles.addColumn (new RecordStateColumn); mDebugProfiles.addColumn (new FixedRecordTypeColumn (UniversalId::Type_DebugProfile)); mDebugProfiles.addColumn (new FlagColumn2 ( Columns::ColumnId_DefaultProfile, ESM::DebugProfile::Flag_Default)); mDebugProfiles.addColumn (new FlagColumn2 ( Columns::ColumnId_BypassNewGame, ESM::DebugProfile::Flag_BypassNewGame)); mDebugProfiles.addColumn (new FlagColumn2 ( Columns::ColumnId_GlobalProfile, ESM::DebugProfile::Flag_Global)); mDebugProfiles.addColumn (new DescriptionColumn); mDebugProfiles.addColumn (new ScriptColumn ( ScriptColumn::Type_Lines)); mMetaData.appendBlankRecord ("sys::meta"); mMetaData.addColumn (new StringIdColumn (true)); mMetaData.addColumn (new RecordStateColumn); mMetaData.addColumn (new FixedRecordTypeColumn (UniversalId::Type_MetaData)); mMetaData.addColumn (new FormatColumn); mMetaData.addColumn (new AuthorColumn); mMetaData.addColumn (new FileDescriptionColumn); addModel (new IdTable (&mGlobals), UniversalId::Type_Global); addModel (new IdTable (&mGmsts), UniversalId::Type_Gmst); addModel (new IdTable (&mSkills), UniversalId::Type_Skill); addModel (new IdTable (&mClasses), UniversalId::Type_Class); addModel (new IdTree (&mFactions, &mFactions), UniversalId::Type_Faction); addModel (new IdTree (&mRaces, &mRaces), UniversalId::Type_Race); addModel (new IdTable (&mSounds), UniversalId::Type_Sound); addModel (new IdTable (&mScripts), UniversalId::Type_Script); addModel (new IdTree (&mRegions, &mRegions), UniversalId::Type_Region); addModel (new IdTree (&mBirthsigns, &mBirthsigns), UniversalId::Type_Birthsign); addModel (new IdTree (&mSpells, &mSpells), UniversalId::Type_Spell); addModel (new IdTable (&mTopics), UniversalId::Type_Topic); addModel (new IdTable (&mJournals), UniversalId::Type_Journal); addModel (new IdTree (&mTopicInfos, &mTopicInfos, IdTable::Feature_ReorderWithinTopic), UniversalId::Type_TopicInfo); addModel (new IdTable (&mJournalInfos, IdTable::Feature_ReorderWithinTopic), UniversalId::Type_JournalInfo); addModel (new IdTree (&mCells, &mCells, IdTable::Feature_ViewId), UniversalId::Type_Cell); addModel (new IdTree (&mEnchantments, &mEnchantments), UniversalId::Type_Enchantment); addModel (new IdTable (&mBodyParts), UniversalId::Type_BodyPart); addModel (new IdTable (&mSoundGens), UniversalId::Type_SoundGen); addModel (new IdTable (&mMagicEffects), UniversalId::Type_MagicEffect); addModel (new IdTable (&mLand, IdTable::Feature_AllowTouch), UniversalId::Type_Land); addModel (new LandTextureIdTable (&mLandTextures), UniversalId::Type_LandTexture); addModel (new IdTree (&mPathgrids, &mPathgrids), UniversalId::Type_Pathgrid); addModel (new IdTable (&mStartScripts), UniversalId::Type_StartScript); addModel (new IdTree (&mReferenceables, &mReferenceables, IdTable::Feature_Preview), UniversalId::Type_Referenceable); addModel (new IdTable (&mRefs, IdTable::Feature_ViewCell | IdTable::Feature_Preview), UniversalId::Type_Reference); addModel (new IdTable (&mFilters), UniversalId::Type_Filter); addModel (new IdTable (&mDebugProfiles), UniversalId::Type_DebugProfile); addModel (new ResourceTable (&mResourcesManager.get (UniversalId::Type_Meshes)), UniversalId::Type_Mesh); addModel (new ResourceTable (&mResourcesManager.get (UniversalId::Type_Icons)), UniversalId::Type_Icon); addModel (new ResourceTable (&mResourcesManager.get (UniversalId::Type_Musics)), UniversalId::Type_Music); addModel (new ResourceTable (&mResourcesManager.get (UniversalId::Type_SoundsRes)), UniversalId::Type_SoundRes); addModel (new ResourceTable (&mResourcesManager.get (UniversalId::Type_Textures)), UniversalId::Type_Texture); addModel (new ResourceTable (&mResourcesManager.get (UniversalId::Type_Videos)), UniversalId::Type_Video); addModel (new IdTable (&mMetaData), UniversalId::Type_MetaData); mActorAdapter.reset(new ActorAdapter(*this)); mRefLoadCache.clear(); // clear here rather than startLoading() and continueLoading() for multiple content files } CSMWorld::Data::~Data() { for (std::vector::iterator iter (mModels.begin()); iter!=mModels.end(); ++iter) delete *iter; delete mReader; } std::shared_ptr CSMWorld::Data::getResourceSystem() { return mResourceSystem; } std::shared_ptr CSMWorld::Data::getResourceSystem() const { return mResourceSystem; } const CSMWorld::IdCollection& CSMWorld::Data::getGlobals() const { return mGlobals; } CSMWorld::IdCollection& CSMWorld::Data::getGlobals() { return mGlobals; } const CSMWorld::IdCollection& CSMWorld::Data::getGmsts() const { return mGmsts; } CSMWorld::IdCollection& CSMWorld::Data::getGmsts() { return mGmsts; } const CSMWorld::IdCollection& CSMWorld::Data::getSkills() const { return mSkills; } CSMWorld::IdCollection& CSMWorld::Data::getSkills() { return mSkills; } const CSMWorld::IdCollection& CSMWorld::Data::getClasses() const { return mClasses; } CSMWorld::IdCollection& CSMWorld::Data::getClasses() { return mClasses; } const CSMWorld::IdCollection& CSMWorld::Data::getFactions() const { return mFactions; } CSMWorld::IdCollection& CSMWorld::Data::getFactions() { return mFactions; } const CSMWorld::IdCollection& CSMWorld::Data::getRaces() const { return mRaces; } CSMWorld::IdCollection& CSMWorld::Data::getRaces() { return mRaces; } const CSMWorld::IdCollection& CSMWorld::Data::getSounds() const { return mSounds; } CSMWorld::IdCollection& CSMWorld::Data::getSounds() { return mSounds; } const CSMWorld::IdCollection& CSMWorld::Data::getScripts() const { return mScripts; } CSMWorld::IdCollection& CSMWorld::Data::getScripts() { return mScripts; } const CSMWorld::IdCollection& CSMWorld::Data::getRegions() const { return mRegions; } CSMWorld::IdCollection& CSMWorld::Data::getRegions() { return mRegions; } const CSMWorld::IdCollection& CSMWorld::Data::getBirthsigns() const { return mBirthsigns; } CSMWorld::IdCollection& CSMWorld::Data::getBirthsigns() { return mBirthsigns; } const CSMWorld::IdCollection& CSMWorld::Data::getSpells() const { return mSpells; } CSMWorld::IdCollection& CSMWorld::Data::getSpells() { return mSpells; } const CSMWorld::IdCollection& CSMWorld::Data::getTopics() const { return mTopics; } CSMWorld::IdCollection& CSMWorld::Data::getTopics() { return mTopics; } const CSMWorld::IdCollection& CSMWorld::Data::getJournals() const { return mJournals; } CSMWorld::IdCollection& CSMWorld::Data::getJournals() { return mJournals; } const CSMWorld::InfoCollection& CSMWorld::Data::getTopicInfos() const { return mTopicInfos; } CSMWorld::InfoCollection& CSMWorld::Data::getTopicInfos() { return mTopicInfos; } const CSMWorld::InfoCollection& CSMWorld::Data::getJournalInfos() const { return mJournalInfos; } CSMWorld::InfoCollection& CSMWorld::Data::getJournalInfos() { return mJournalInfos; } const CSMWorld::IdCollection& CSMWorld::Data::getCells() const { return mCells; } CSMWorld::IdCollection& CSMWorld::Data::getCells() { return mCells; } const CSMWorld::RefIdCollection& CSMWorld::Data::getReferenceables() const { return mReferenceables; } CSMWorld::RefIdCollection& CSMWorld::Data::getReferenceables() { return mReferenceables; } const CSMWorld::RefCollection& CSMWorld::Data::getReferences() const { return mRefs; } CSMWorld::RefCollection& CSMWorld::Data::getReferences() { return mRefs; } const CSMWorld::IdCollection& CSMWorld::Data::getFilters() const { return mFilters; } CSMWorld::IdCollection& CSMWorld::Data::getFilters() { return mFilters; } const CSMWorld::IdCollection& CSMWorld::Data::getEnchantments() const { return mEnchantments; } CSMWorld::IdCollection& CSMWorld::Data::getEnchantments() { return mEnchantments; } const CSMWorld::IdCollection& CSMWorld::Data::getBodyParts() const { return mBodyParts; } CSMWorld::IdCollection& CSMWorld::Data::getBodyParts() { return mBodyParts; } const CSMWorld::IdCollection& CSMWorld::Data::getDebugProfiles() const { return mDebugProfiles; } CSMWorld::IdCollection& CSMWorld::Data::getDebugProfiles() { return mDebugProfiles; } const CSMWorld::IdCollection& CSMWorld::Data::getLand() const { return mLand; } CSMWorld::IdCollection& CSMWorld::Data::getLand() { return mLand; } const CSMWorld::IdCollection& CSMWorld::Data::getLandTextures() const { return mLandTextures; } CSMWorld::IdCollection& CSMWorld::Data::getLandTextures() { return mLandTextures; } const CSMWorld::IdCollection& CSMWorld::Data::getSoundGens() const { return mSoundGens; } CSMWorld::IdCollection& CSMWorld::Data::getSoundGens() { return mSoundGens; } const CSMWorld::IdCollection& CSMWorld::Data::getMagicEffects() const { return mMagicEffects; } CSMWorld::IdCollection& CSMWorld::Data::getMagicEffects() { return mMagicEffects; } const CSMWorld::SubCellCollection& CSMWorld::Data::getPathgrids() const { return mPathgrids; } CSMWorld::SubCellCollection& CSMWorld::Data::getPathgrids() { return mPathgrids; } const CSMWorld::IdCollection& CSMWorld::Data::getStartScripts() const { return mStartScripts; } CSMWorld::IdCollection& CSMWorld::Data::getStartScripts() { return mStartScripts; } const CSMWorld::Resources& CSMWorld::Data::getResources (const UniversalId& id) const { return mResourcesManager.get (id.getType()); } const CSMWorld::MetaData& CSMWorld::Data::getMetaData() const { return mMetaData.getRecord (0).get(); } void CSMWorld::Data::setMetaData (const MetaData& metaData) { Record record (RecordBase::State_ModifiedOnly, nullptr, &metaData); mMetaData.setRecord (0, record); } QAbstractItemModel *CSMWorld::Data::getTableModel (const CSMWorld::UniversalId& id) { std::map::iterator iter = mModelIndex.find (id.getType()); if (iter==mModelIndex.end()) { // try creating missing (secondary) tables on the fly // // Note: We create these tables here so we don't have to deal with them during load/initial // construction of the ESX data where no update signals are available. if (id.getType()==UniversalId::Type_RegionMap) { RegionMap *table = nullptr; addModel (table = new RegionMap (*this), UniversalId::Type_RegionMap, false); return table; } throw std::logic_error ("No table model available for " + id.toString()); } return iter->second; } const CSMWorld::ActorAdapter* CSMWorld::Data::getActorAdapter() const { return mActorAdapter.get(); } CSMWorld::ActorAdapter* CSMWorld::Data::getActorAdapter() { return mActorAdapter.get(); } void CSMWorld::Data::merge() { mGlobals.merge(); } int CSMWorld::Data::startLoading (const boost::filesystem::path& path, bool base, bool project) { // Don't delete the Reader yet. Some record types store a reference to the Reader to handle on-demand loading std::shared_ptr ptr(mReader); mReaders.push_back(ptr); mReader = nullptr; mDialogue = nullptr; mReader = new ESM::ESMReader; mReader->setEncoder (&mEncoder); mReader->setIndex((project || !base) ? 0 : mReaderIndex++); mReader->open (path.string()); mContentFileNames.insert(std::make_pair(path.filename().string(), mReader->getIndex())); mBase = base; mProject = project; if (!mProject && !mBase) { MetaData metaData; metaData.mId = "sys::meta"; metaData.load (*mReader); mMetaData.setRecord (0, Record (RecordBase::State_ModifiedOnly, nullptr, &metaData)); } return mReader->getRecordCount(); } void CSMWorld::Data::loadFallbackEntries() { // Load default marker definitions, if game files do not have them for some reason std::pair staticMarkers[] = { std::make_pair("DivineMarker", "marker_divine.nif"), std::make_pair("DoorMarker", "marker_arrow.nif"), std::make_pair("NorthMarker", "marker_north.nif"), std::make_pair("TempleMarker", "marker_temple.nif"), std::make_pair("TravelMarker", "marker_travel.nif") }; std::pair doorMarkers[] = { std::make_pair("PrisonMarker", "marker_prison.nif") }; for (const auto &marker : staticMarkers) { if (mReferenceables.searchId (marker.first)==-1) { ESM::Static newMarker; newMarker.mId = marker.first; newMarker.mModel = marker.second; CSMWorld::Record record; record.mBase = newMarker; record.mState = CSMWorld::RecordBase::State_BaseOnly; mReferenceables.appendRecord (record, CSMWorld::UniversalId::Type_Static); } } for (const auto &marker : doorMarkers) { if (mReferenceables.searchId (marker.first)==-1) { ESM::Door newMarker; newMarker.mId = marker.first; newMarker.mModel = marker.second; CSMWorld::Record record; record.mBase = newMarker; record.mState = CSMWorld::RecordBase::State_BaseOnly; mReferenceables.appendRecord (record, CSMWorld::UniversalId::Type_Door); } } } bool CSMWorld::Data::continueLoading (CSMDoc::Messages& messages) { if (!mReader) throw std::logic_error ("can't continue loading, because no load has been started"); if (!mReader->hasMoreRecs()) { if (mBase) { // Don't delete the Reader yet. Some record types store a reference to the Reader to handle on-demand loading. // We don't store non-base reader, because everything going into modified will be // fully loaded during the initial loading process. std::shared_ptr ptr(mReader); mReaders.push_back(ptr); } else delete mReader; mReader = nullptr; mDialogue = nullptr; loadFallbackEntries(); return true; } ESM::NAME n = mReader->getRecName(); mReader->getRecHeader(); bool unhandledRecord = false; switch (n.intval) { case ESM::REC_GLOB: mGlobals.load (*mReader, mBase); break; case ESM::REC_GMST: mGmsts.load (*mReader, mBase); break; case ESM::REC_SKIL: mSkills.load (*mReader, mBase); break; case ESM::REC_CLAS: mClasses.load (*mReader, mBase); break; case ESM::REC_FACT: mFactions.load (*mReader, mBase); break; case ESM::REC_RACE: mRaces.load (*mReader, mBase); break; case ESM::REC_SOUN: mSounds.load (*mReader, mBase); break; case ESM::REC_SCPT: mScripts.load (*mReader, mBase); break; case ESM::REC_REGN: mRegions.load (*mReader, mBase); break; case ESM::REC_BSGN: mBirthsigns.load (*mReader, mBase); break; case ESM::REC_SPEL: mSpells.load (*mReader, mBase); break; case ESM::REC_ENCH: mEnchantments.load (*mReader, mBase); break; case ESM::REC_BODY: mBodyParts.load (*mReader, mBase); break; case ESM::REC_SNDG: mSoundGens.load (*mReader, mBase); break; case ESM::REC_MGEF: mMagicEffects.load (*mReader, mBase); break; case ESM::REC_PGRD: mPathgrids.load (*mReader, mBase); break; case ESM::REC_SSCR: mStartScripts.load (*mReader, mBase); break; case ESM::REC_LTEX: mLandTextures.load (*mReader, mBase); break; case ESM::REC_LAND: mLand.load(*mReader, mBase); break; case ESM::REC_CELL: { int index = mCells.load (*mReader, mBase); if (index < 0 || index >= mCells.getSize()) { // log an error and continue loading the refs to the last loaded cell CSMWorld::UniversalId id (CSMWorld::UniversalId::Type_None); messages.add (id, "Logic error: cell index out of bounds", "", CSMDoc::Message::Severity_Error); index = mCells.getSize()-1; } std::string cellId = Misc::StringUtils::lowerCase (mCells.getId (index)); mRefs.load (*mReader, index, mBase, mRefLoadCache[cellId], messages); break; } case ESM::REC_ACTI: mReferenceables.load (*mReader, mBase, UniversalId::Type_Activator); break; case ESM::REC_ALCH: mReferenceables.load (*mReader, mBase, UniversalId::Type_Potion); break; case ESM::REC_APPA: mReferenceables.load (*mReader, mBase, UniversalId::Type_Apparatus); break; case ESM::REC_ARMO: mReferenceables.load (*mReader, mBase, UniversalId::Type_Armor); break; case ESM::REC_BOOK: mReferenceables.load (*mReader, mBase, UniversalId::Type_Book); break; case ESM::REC_CLOT: mReferenceables.load (*mReader, mBase, UniversalId::Type_Clothing); break; case ESM::REC_CONT: mReferenceables.load (*mReader, mBase, UniversalId::Type_Container); break; case ESM::REC_CREA: mReferenceables.load (*mReader, mBase, UniversalId::Type_Creature); break; case ESM::REC_DOOR: mReferenceables.load (*mReader, mBase, UniversalId::Type_Door); break; case ESM::REC_INGR: mReferenceables.load (*mReader, mBase, UniversalId::Type_Ingredient); break; case ESM::REC_LEVC: mReferenceables.load (*mReader, mBase, UniversalId::Type_CreatureLevelledList); break; case ESM::REC_LEVI: mReferenceables.load (*mReader, mBase, UniversalId::Type_ItemLevelledList); break; case ESM::REC_LIGH: mReferenceables.load (*mReader, mBase, UniversalId::Type_Light); break; case ESM::REC_LOCK: mReferenceables.load (*mReader, mBase, UniversalId::Type_Lockpick); break; case ESM::REC_MISC: mReferenceables.load (*mReader, mBase, UniversalId::Type_Miscellaneous); break; case ESM::REC_NPC_: mReferenceables.load (*mReader, mBase, UniversalId::Type_Npc); break; case ESM::REC_PROB: mReferenceables.load (*mReader, mBase, UniversalId::Type_Probe); break; case ESM::REC_REPA: mReferenceables.load (*mReader, mBase, UniversalId::Type_Repair); break; case ESM::REC_STAT: mReferenceables.load (*mReader, mBase, UniversalId::Type_Static); break; case ESM::REC_WEAP: mReferenceables.load (*mReader, mBase, UniversalId::Type_Weapon); break; case ESM::REC_DIAL: { ESM::Dialogue record; bool isDeleted = false; record.load (*mReader, isDeleted); if (isDeleted) { // record vector can be shuffled around which would make pointer to record invalid mDialogue = nullptr; if (mJournals.tryDelete (record.mId)) { mJournalInfos.removeDialogueInfos(record.mId); } else if (mTopics.tryDelete (record.mId)) { mTopicInfos.removeDialogueInfos(record.mId); } else { messages.add (UniversalId::Type_None, "Trying to delete dialogue record " + record.mId + " which does not exist", "", CSMDoc::Message::Severity_Warning); } } else { if (record.mType == ESM::Dialogue::Journal) { mJournals.load (record, mBase); mDialogue = &mJournals.getRecord (record.mId).get(); } else { mTopics.load (record, mBase); mDialogue = &mTopics.getRecord (record.mId).get(); } } break; } case ESM::REC_INFO: { if (!mDialogue) { messages.add (UniversalId::Type_None, "Found info record not following a dialogue record", "", CSMDoc::Message::Severity_Error); mReader->skipRecord(); break; } if (mDialogue->mType==ESM::Dialogue::Journal) mJournalInfos.load (*mReader, mBase, *mDialogue); else mTopicInfos.load (*mReader, mBase, *mDialogue); break; } case ESM::REC_FILT: if (!mProject) { unhandledRecord = true; break; } mFilters.load (*mReader, mBase); break; case ESM::REC_DBGP: if (!mProject) { unhandledRecord = true; break; } mDebugProfiles.load (*mReader, mBase); break; default: unhandledRecord = true; } if (unhandledRecord) { messages.add (UniversalId::Type_None, "Unsupported record type: " + n.toString(), "", CSMDoc::Message::Severity_Error); mReader->skipRecord(); } return false; } bool CSMWorld::Data::hasId (const std::string& id) const { return getGlobals().searchId (id)!=-1 || getGmsts().searchId (id)!=-1 || getSkills().searchId (id)!=-1 || getClasses().searchId (id)!=-1 || getFactions().searchId (id)!=-1 || getRaces().searchId (id)!=-1 || getSounds().searchId (id)!=-1 || getScripts().searchId (id)!=-1 || getRegions().searchId (id)!=-1 || getBirthsigns().searchId (id)!=-1 || getSpells().searchId (id)!=-1 || getTopics().searchId (id)!=-1 || getJournals().searchId (id)!=-1 || getCells().searchId (id)!=-1 || getEnchantments().searchId (id)!=-1 || getBodyParts().searchId (id)!=-1 || getSoundGens().searchId (id)!=-1 || getMagicEffects().searchId (id)!=-1 || getReferenceables().searchId (id)!=-1; } int CSMWorld::Data::count (RecordBase::State state) const { return count (state, mGlobals) + count (state, mGmsts) + count (state, mSkills) + count (state, mClasses) + count (state, mFactions) + count (state, mRaces) + count (state, mSounds) + count (state, mScripts) + count (state, mRegions) + count (state, mBirthsigns) + count (state, mSpells) + count (state, mCells) + count (state, mEnchantments) + count (state, mBodyParts) + count (state, mLand) + count (state, mLandTextures) + count (state, mSoundGens) + count (state, mMagicEffects) + count (state, mReferenceables) + count (state, mPathgrids); } std::vector CSMWorld::Data::getIds (bool listDeleted) const { std::vector ids; appendIds (ids, mGlobals, listDeleted); appendIds (ids, mGmsts, listDeleted); appendIds (ids, mClasses, listDeleted); appendIds (ids, mFactions, listDeleted); appendIds (ids, mRaces, listDeleted); appendIds (ids, mSounds, listDeleted); appendIds (ids, mScripts, listDeleted); appendIds (ids, mRegions, listDeleted); appendIds (ids, mBirthsigns, listDeleted); appendIds (ids, mSpells, listDeleted); appendIds (ids, mTopics, listDeleted); appendIds (ids, mJournals, listDeleted); appendIds (ids, mCells, listDeleted); appendIds (ids, mEnchantments, listDeleted); appendIds (ids, mBodyParts, listDeleted); appendIds (ids, mSoundGens, listDeleted); appendIds (ids, mMagicEffects, listDeleted); appendIds (ids, mReferenceables, listDeleted); std::sort (ids.begin(), ids.end()); return ids; } void CSMWorld::Data::assetsChanged() { mVFS.get()->reset(); VFS::registerArchives(mVFS.get(), Files::Collections(mDataPaths, !mFsStrict), mArchives, true); const UniversalId assetTableIds[] = { UniversalId::Type_Meshes, UniversalId::Type_Icons, UniversalId::Type_Musics, UniversalId::Type_SoundsRes, UniversalId::Type_Textures, UniversalId::Type_Videos }; size_t numAssetTables = sizeof(assetTableIds) / sizeof(UniversalId); for (size_t i = 0; i < numAssetTables; ++i) { ResourceTable* table = static_cast(getTableModel(assetTableIds[i])); table->beginReset(); } // Trigger recreation mResourcesManager.recreateResources(); for (size_t i = 0; i < numAssetTables; ++i) { ResourceTable* table = static_cast(getTableModel(assetTableIds[i])); table->endReset(); } // Get rid of potentially old cached assets mResourceSystem->clearCache(); emit assetTablesChanged(); } void CSMWorld::Data::dataChanged (const QModelIndex& topLeft, const QModelIndex& bottomRight) { if (topLeft.column()<=0) emit idListChanged(); } void CSMWorld::Data::rowsChanged (const QModelIndex& parent, int start, int end) { emit idListChanged(); } const VFS::Manager* CSMWorld::Data::getVFS() const { return mVFS.get(); } ================================================ FILE: apps/opencs/model/world/data.hpp ================================================ #ifndef CSM_WOLRD_DATA_H #define CSM_WOLRD_DATA_H #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "../doc/stage.hpp" #include "actoradapter.hpp" #include "idcollection.hpp" #include "nestedidcollection.hpp" #include "universalid.hpp" #include "cell.hpp" #include "land.hpp" #include "landtexture.hpp" #include "refidcollection.hpp" #include "refcollection.hpp" #include "infocollection.hpp" #include "nestedinfocollection.hpp" #include "pathgrid.hpp" #include "resourcesmanager.hpp" #include "metadata.hpp" #ifndef Q_MOC_RUN #include "subcellcollection.hpp" #endif class QAbstractItemModel; namespace VFS { class Manager; } namespace Fallback { class Map; } namespace ESM { class ESMReader; struct Dialogue; } namespace CSMWorld { class ResourcesManager; class Resources; class Data : public QObject { Q_OBJECT ToUTF8::Utf8Encoder mEncoder; IdCollection mGlobals; IdCollection mGmsts; IdCollection mSkills; IdCollection mClasses; NestedIdCollection mFactions; NestedIdCollection mRaces; IdCollection mSounds; IdCollection mScripts; NestedIdCollection mRegions; NestedIdCollection mBirthsigns; NestedIdCollection mSpells; IdCollection mTopics; IdCollection mJournals; NestedIdCollection mEnchantments; IdCollection mBodyParts; IdCollection mMagicEffects; SubCellCollection mPathgrids; IdCollection mDebugProfiles; IdCollection mSoundGens; IdCollection mStartScripts; NestedInfoCollection mTopicInfos; InfoCollection mJournalInfos; NestedIdCollection mCells; IdCollection mLandTextures; IdCollection mLand; RefIdCollection mReferenceables; RefCollection mRefs; IdCollection mFilters; Collection mMetaData; std::unique_ptr mActorAdapter; std::vector mModels; std::map mModelIndex; ESM::ESMReader *mReader; const ESM::Dialogue *mDialogue; // last loaded dialogue bool mBase; bool mProject; std::map > mRefLoadCache; int mReaderIndex; bool mFsStrict; Files::PathContainer mDataPaths; std::vector mArchives; std::unique_ptr mVFS; ResourcesManager mResourcesManager; std::shared_ptr mResourceSystem; std::vector > mReaders; std::map mContentFileNames; // not implemented Data (const Data&); Data& operator= (const Data&); void addModel (QAbstractItemModel *model, UniversalId::Type type, bool update = true); static void appendIds (std::vector& ids, const CollectionBase& collection, bool listDeleted); ///< Append all IDs from collection to \a ids. static int count (RecordBase::State state, const CollectionBase& collection); void loadFallbackEntries(); public: Data (ToUTF8::FromType encoding, bool fsStrict, const Files::PathContainer& dataPaths, const std::vector& archives, const boost::filesystem::path& resDir); virtual ~Data(); const VFS::Manager* getVFS() const; std::shared_ptr getResourceSystem(); std::shared_ptr getResourceSystem() const; const IdCollection& getGlobals() const; IdCollection& getGlobals(); const IdCollection& getGmsts() const; IdCollection& getGmsts(); const IdCollection& getSkills() const; IdCollection& getSkills(); const IdCollection& getClasses() const; IdCollection& getClasses(); const IdCollection& getFactions() const; IdCollection& getFactions(); const IdCollection& getRaces() const; IdCollection& getRaces(); const IdCollection& getSounds() const; IdCollection& getSounds(); const IdCollection& getScripts() const; IdCollection& getScripts(); const IdCollection& getRegions() const; IdCollection& getRegions(); const IdCollection& getBirthsigns() const; IdCollection& getBirthsigns(); const IdCollection& getSpells() const; IdCollection& getSpells(); const IdCollection& getTopics() const; IdCollection& getTopics(); const IdCollection& getJournals() const; IdCollection& getJournals(); const InfoCollection& getTopicInfos() const; InfoCollection& getTopicInfos(); const InfoCollection& getJournalInfos() const; InfoCollection& getJournalInfos(); const IdCollection& getCells() const; IdCollection& getCells(); const RefIdCollection& getReferenceables() const; RefIdCollection& getReferenceables(); const RefCollection& getReferences() const; RefCollection& getReferences(); const IdCollection& getFilters() const; IdCollection& getFilters(); const IdCollection& getEnchantments() const; IdCollection& getEnchantments(); const IdCollection& getBodyParts() const; IdCollection& getBodyParts(); const IdCollection& getDebugProfiles() const; IdCollection& getDebugProfiles(); const IdCollection& getLand() const; IdCollection& getLand(); const IdCollection& getLandTextures() const; IdCollection& getLandTextures(); const IdCollection& getSoundGens() const; IdCollection& getSoundGens(); const IdCollection& getMagicEffects() const; IdCollection& getMagicEffects(); const SubCellCollection& getPathgrids() const; SubCellCollection& getPathgrids(); const IdCollection& getStartScripts() const; IdCollection& getStartScripts(); /// Throws an exception, if \a id does not match a resources list. const Resources& getResources (const UniversalId& id) const; const MetaData& getMetaData() const; void setMetaData (const MetaData& metaData); QAbstractItemModel *getTableModel (const UniversalId& id); ///< If no table model is available for \a id, an exception is thrown. /// /// \note The returned table may either be the model for the ID itself or the model that /// contains the record specified by the ID. const ActorAdapter* getActorAdapter() const; ActorAdapter* getActorAdapter(); void merge(); ///< Merge modified into base. int startLoading (const boost::filesystem::path& path, bool base, bool project); ///< Begin merging content of a file into base or modified. /// /// \param project load project file instead of content file /// ///< \return estimated number of records bool continueLoading (CSMDoc::Messages& messages); ///< \return Finished? bool hasId (const std::string& id) const; std::vector getIds (bool listDeleted = true) const; ///< Return a sorted collection of all IDs that are not internal to the editor. /// /// \param listDeleted include deleted record in the list int count (RecordBase::State state) const; ///< Return number of top-level records with the given \a state. signals: void idListChanged(); void assetTablesChanged(); private slots: void assetsChanged(); void dataChanged (const QModelIndex& topLeft, const QModelIndex& bottomRight); void rowsChanged (const QModelIndex& parent, int start, int end); }; } #endif ================================================ FILE: apps/opencs/model/world/defaultgmsts.cpp ================================================ #include "defaultgmsts.hpp" #include const float FInf = std::numeric_limits::infinity(); const float FEps = std::numeric_limits::epsilon(); const int IMax = std::numeric_limits::max(); const int IMin = std::numeric_limits::min(); const char* CSMWorld::DefaultGmsts::Floats[CSMWorld::DefaultGmsts::FloatCount] = { "fAIFleeFleeMult", "fAIFleeHealthMult", "fAIMagicSpellMult", "fAIMeleeArmorMult", "fAIMeleeSummWeaponMult", "fAIMeleeWeaponMult", "fAIRangeMagicSpellMult", "fAIRangeMeleeWeaponMult", "fAlarmRadius", "fAthleticsRunBonus", "fAudioDefaultMaxDistance", "fAudioDefaultMinDistance", "fAudioMaxDistanceMult", "fAudioMinDistanceMult", "fAudioVoiceDefaultMaxDistance", "fAudioVoiceDefaultMinDistance", "fAutoPCSpellChance", "fAutoSpellChance", "fBargainOfferBase", "fBargainOfferMulti", "fBarterGoldResetDelay", "fBaseRunMultiplier", "fBlockStillBonus", "fBribe1000Mod", "fBribe100Mod", "fBribe10Mod", "fCombatAngleXY", "fCombatAngleZ", "fCombatArmorMinMult", "fCombatBlockLeftAngle", "fCombatBlockRightAngle", "fCombatCriticalStrikeMult", "fCombatDelayCreature", "fCombatDelayNPC", "fCombatDistance", "fCombatDistanceWerewolfMod", "fCombatForceSideAngle", "fCombatInvisoMult", "fCombatKODamageMult", "fCombatTorsoSideAngle", "fCombatTorsoStartPercent", "fCombatTorsoStopPercent", "fConstantEffectMult", "fCorpseClearDelay", "fCorpseRespawnDelay", "fCrimeGoldDiscountMult", "fCrimeGoldTurnInMult", "fCrimeStealing", "fDamageStrengthBase", "fDamageStrengthMult", "fDifficultyMult", "fDiseaseXferChance", "fDispAttacking", "fDispBargainFailMod", "fDispBargainSuccessMod", "fDispCrimeMod", "fDispDiseaseMod", "fDispFactionMod", "fDispFactionRankBase", "fDispFactionRankMult", "fDispositionMod", "fDispPersonalityBase", "fDispPersonalityMult", "fDispPickPocketMod", "fDispRaceMod", "fDispStealing", "fDispWeaponDrawn", "fEffectCostMult", "fElementalShieldMult", "fEnchantmentChanceMult", "fEnchantmentConstantChanceMult", "fEnchantmentConstantDurationMult", "fEnchantmentMult", "fEnchantmentValueMult", "fEncumberedMoveEffect", "fEncumbranceStrMult", "fEndFatigueMult", "fFallAcroBase", "fFallAcroMult", "fFallDamageDistanceMin", "fFallDistanceBase", "fFallDistanceMult", "fFatigueAttackBase", "fFatigueAttackMult", "fFatigueBase", "fFatigueBlockBase", "fFatigueBlockMult", "fFatigueJumpBase", "fFatigueJumpMult", "fFatigueMult", "fFatigueReturnBase", "fFatigueReturnMult", "fFatigueRunBase", "fFatigueRunMult", "fFatigueSneakBase", "fFatigueSneakMult", "fFatigueSpellBase", "fFatigueSpellCostMult", "fFatigueSpellMult", "fFatigueSwimRunBase", "fFatigueSwimRunMult", "fFatigueSwimWalkBase", "fFatigueSwimWalkMult", "fFightDispMult", "fFightDistanceMultiplier", "fFightStealing", "fFleeDistance", "fGreetDistanceReset", "fHandtoHandHealthPer", "fHandToHandReach", "fHoldBreathEndMult", "fHoldBreathTime", "fIdleChanceMultiplier", "fIngredientMult", "fInteriorHeadTrackMult", "fJumpAcrobaticsBase", "fJumpAcroMultiplier", "fJumpEncumbranceBase", "fJumpEncumbranceMultiplier", "fJumpMoveBase", "fJumpMoveMult", "fJumpRunMultiplier", "fKnockDownMult", "fLevelMod", "fLevelUpHealthEndMult", "fLightMaxMod", "fLuckMod", "fMagesGuildTravel", "fMagicCreatureCastDelay", "fMagicDetectRefreshRate", "fMagicItemConstantMult", "fMagicItemCostMult", "fMagicItemOnceMult", "fMagicItemPriceMult", "fMagicItemRechargePerSecond", "fMagicItemStrikeMult", "fMagicItemUsedMult", "fMagicStartIconBlink", "fMagicSunBlockedMult", "fMajorSkillBonus", "fMaxFlySpeed", "fMaxHandToHandMult", "fMaxHeadTrackDistance", "fMaxWalkSpeed", "fMaxWalkSpeedCreature", "fMedMaxMod", "fMessageTimePerChar", "fMinFlySpeed", "fMinHandToHandMult", "fMinorSkillBonus", "fMinWalkSpeed", "fMinWalkSpeedCreature", "fMiscSkillBonus", "fNPCbaseMagickaMult", "fNPCHealthBarFade", "fNPCHealthBarTime", "fPCbaseMagickaMult", "fPerDieRollMult", "fPersonalityMod", "fPerTempMult", "fPickLockMult", "fPickPocketMod", "fPotionMinUsefulDuration", "fPotionStrengthMult", "fPotionT1DurMult", "fPotionT1MagMult", "fPotionT4BaseStrengthMult", "fPotionT4EquipStrengthMult", "fProjectileMaxSpeed", "fProjectileMinSpeed", "fProjectileThrownStoreChance", "fRepairAmountMult", "fRepairMult", "fReputationMod", "fRestMagicMult", "fSeriousWoundMult", "fSleepRandMod", "fSleepRestMod", "fSneakBootMult", "fSneakDistanceBase", "fSneakDistanceMultiplier", "fSneakNoViewMult", "fSneakSkillMult", "fSneakSpeedMultiplier", "fSneakUseDelay", "fSneakUseDist", "fSneakViewMult", "fSoulGemMult", "fSpecialSkillBonus", "fSpellMakingValueMult", "fSpellPriceMult", "fSpellValueMult", "fStromWalkMult", "fStromWindSpeed", "fSuffocationDamage", "fSwimHeightScale", "fSwimRunAthleticsMult", "fSwimRunBase", "fSwimWalkAthleticsMult", "fSwimWalkBase", "fSwingBlockBase", "fSwingBlockMult", "fTargetSpellMaxSpeed", "fThrownWeaponMaxSpeed", "fThrownWeaponMinSpeed", "fTrapCostMult", "fTravelMult", "fTravelTimeMult", "fUnarmoredBase1", "fUnarmoredBase2", "fVanityDelay", "fVoiceIdleOdds", "fWaterReflectUpdateAlways", "fWaterReflectUpdateSeldom", "fWeaponDamageMult", "fWeaponFatigueBlockMult", "fWeaponFatigueMult", "fWereWolfAcrobatics", "fWereWolfAgility", "fWereWolfAlchemy", "fWereWolfAlteration", "fWereWolfArmorer", "fWereWolfAthletics", "fWereWolfAxe", "fWereWolfBlock", "fWereWolfBluntWeapon", "fWereWolfConjuration", "fWereWolfDestruction", "fWereWolfEnchant", "fWereWolfEndurance", "fWereWolfFatigue", "fWereWolfHandtoHand", "fWereWolfHealth", "fWereWolfHeavyArmor", "fWereWolfIllusion", "fWereWolfIntellegence", "fWereWolfLightArmor", "fWereWolfLongBlade", "fWereWolfLuck", "fWereWolfMagicka", "fWereWolfMarksman", "fWereWolfMediumArmor", "fWereWolfMerchantile", "fWereWolfMysticism", "fWereWolfPersonality", "fWereWolfRestoration", "fWereWolfRunMult", "fWereWolfSecurity", "fWereWolfShortBlade", "fWereWolfSilverWeaponDamageMult", "fWereWolfSneak", "fWereWolfSpear", "fWereWolfSpeechcraft", "fWereWolfSpeed", "fWereWolfStrength", "fWereWolfUnarmored", "fWereWolfWillPower", "fWortChanceValue" }; const char * CSMWorld::DefaultGmsts::Ints[CSMWorld::DefaultGmsts::IntCount] = { "i1stPersonSneakDelta", "iAlarmAttack", "iAlarmKilling", "iAlarmPickPocket", "iAlarmStealing", "iAlarmTresspass", "iAlchemyMod", "iAutoPCSpellMax", "iAutoRepFacMod", "iAutoRepLevMod", "iAutoSpellAlterationMax", "iAutoSpellAttSkillMin", "iAutoSpellConjurationMax", "iAutoSpellDestructionMax", "iAutoSpellIllusionMax", "iAutoSpellMysticismMax", "iAutoSpellRestorationMax", "iAutoSpellTimesCanCast", "iBarterFailDisposition", "iBarterSuccessDisposition", "iBaseArmorSkill", "iBlockMaxChance", "iBlockMinChance", "iBootsWeight", "iCrimeAttack", "iCrimeKilling", "iCrimePickPocket", "iCrimeThreshold", "iCrimeThresholdMultiplier", "iCrimeTresspass", "iCuirassWeight", "iDaysinPrisonMod", "iDispAttackMod", "iDispKilling", "iDispTresspass", "iFightAlarmMult", "iFightAttack", "iFightAttacking", "iFightDistanceBase", "iFightKilling", "iFightPickpocket", "iFightTrespass", "iFlee", "iGauntletWeight", "iGreavesWeight", "iGreetDistanceMultiplier", "iGreetDuration", "iHelmWeight", "iKnockDownOddsBase", "iKnockDownOddsMult", "iLevelUp01Mult", "iLevelUp02Mult", "iLevelUp03Mult", "iLevelUp04Mult", "iLevelUp05Mult", "iLevelUp06Mult", "iLevelUp07Mult", "iLevelUp08Mult", "iLevelUp09Mult", "iLevelUp10Mult", "iLevelupMajorMult", "iLevelupMajorMultAttribute", "iLevelupMinorMult", "iLevelupMinorMultAttribute", "iLevelupMiscMultAttriubte", "iLevelupSpecialization", "iLevelupTotal", "iMagicItemChargeConst", "iMagicItemChargeOnce", "iMagicItemChargeStrike", "iMagicItemChargeUse", "iMaxActivateDist", "iMaxInfoDist", "iMonthsToRespawn", "iNumberCreatures", "iPauldronWeight", "iPerMinChance", "iPerMinChange", "iPickMaxChance", "iPickMinChance", "iShieldWeight", "iSoulAmountForConstantEffect", "iTrainingMod", "iVoiceAttackOdds", "iVoiceHitOdds", "iWereWolfBounty", "iWereWolfFightMod", "iWereWolfFleeMod", "iWereWolfLevelToAttack" }; const char * CSMWorld::DefaultGmsts::Strings[CSMWorld::DefaultGmsts::StringCount] = { "s3dAudio", "s3dHardware", "s3dSoftware", "sAbsorb", "sAcrobat", "sActivate", "sActivateXbox", "sActorInCombat", "sAdmire", "sAdmireFail", "sAdmireSuccess", "sAgent", "sAgiDesc", "sAIDistance", "sAlembic", "sAllTab", "sAlways", "sAlways_Run", "sand", "sApparatus", "sApparelTab", "sArcher", "sArea", "sAreaDes", "sArmor", "sArmorRating", "sAsk", "sAssassin", "sAt", "sAttack", "sAttributeAgility", "sAttributeEndurance", "sAttributeIntelligence", "sAttributeListTitle", "sAttributeLuck", "sAttributePersonality", "sAttributesMenu1", "sAttributeSpeed", "sAttributeStrength", "sAttributeWillpower", "sAudio", "sAuto_Run", "sBack", "sBackspace", "sBackXbox", "sBarbarian", "sBard", "sBarter", "sBarterDialog1", "sBarterDialog10", "sBarterDialog11", "sBarterDialog12", "sBarterDialog2", "sBarterDialog3", "sBarterDialog4", "sBarterDialog5", "sBarterDialog6", "sBarterDialog7", "sBarterDialog8", "sBarterDialog9", "sBattlemage", "sBestAttack", "sBirthSign", "sBirthsignmenu1", "sBirthsignmenu2", "sBlocks", "sBonusSkillTitle", "sBookPageOne", "sBookPageTwo", "sBookSkillMessage", "sBounty", "sBreath", "sBribe 10 Gold", "sBribe 100 Gold", "sBribe 1000 Gold", "sBribeFail", "sBribeSuccess", "sBuy", "sBye", "sCalcinator", "sCancel", "sCantEquipWeapWarning", "sCastCost", "sCaughtStealingMessage", "sCenter", "sChangedMastersMsg", "sCharges", "sChooseClassMenu1", "sChooseClassMenu2", "sChooseClassMenu3", "sChooseClassMenu4", "sChop", "sClass", "sClassChoiceMenu1", "sClassChoiceMenu2", "sClassChoiceMenu3", "sClose", "sCompanionShare", "sCompanionWarningButtonOne", "sCompanionWarningButtonTwo", "sCompanionWarningMessage", "sCondition", "sConsoleTitle", "sContainer", "sContentsMessage1", "sContentsMessage2", "sContentsMessage3", "sControlerVibration", "sControls", "sControlsMenu1", "sControlsMenu2", "sControlsMenu3", "sControlsMenu4", "sControlsMenu5", "sControlsMenu6", "sCostChance", "sCostCharge", "sCreate", "sCreateClassMenu1", "sCreateClassMenu2", "sCreateClassMenu3", "sCreateClassMenuHelp1", "sCreateClassMenuHelp2", "sCreateClassMenuWarning", "sCreatedEffects", "sCrimeHelp", "sCrimeMessage", "sCrouch_Sneak", "sCrouchXbox", "sCrusader", "sCursorOff", "sCustom", "sCustomClassName", "sDamage", "sDark_Gamma", "sDay", "sDefaultCellname", "sDelete", "sDeleteGame", "sDeleteNote", "sDeleteSpell", "sDeleteSpellError", "sDetail_Level", "sDialogMenu1", "sDialogText1Xbox", "sDialogText2Xbox", "sDialogText3Xbox", "sDifficulty", "sDisposeCorpseFail", "sDisposeofCorpse", "sDone", "sDoYouWantTo", "sDrain", "sDrop", "sDuration", "sDurationDes", "sEasy", "sEditNote", "sEffectAbsorbAttribute", "sEffectAbsorbFatigue", "sEffectAbsorbHealth", "sEffectAbsorbSkill", "sEffectAbsorbSpellPoints", "sEffectAlmsiviIntervention", "sEffectBlind", "sEffectBoundBattleAxe", "sEffectBoundBoots", "sEffectBoundCuirass", "sEffectBoundDagger", "sEffectBoundGloves", "sEffectBoundHelm", "sEffectBoundLongbow", "sEffectBoundLongsword", "sEffectBoundMace", "sEffectBoundShield", "sEffectBoundSpear", "sEffectBurden", "sEffectCalmCreature", "sEffectCalmHumanoid", "sEffectChameleon", "sEffectCharm", "sEffectCommandCreatures", "sEffectCommandHumanoids", "sEffectCorpus", "sEffectCureBlightDisease", "sEffectCureCommonDisease", "sEffectCureCorprusDisease", "sEffectCureParalyzation", "sEffectCurePoison", "sEffectDamageAttribute", "sEffectDamageFatigue", "sEffectDamageHealth", "sEffectDamageMagicka", "sEffectDamageSkill", "sEffectDemoralizeCreature", "sEffectDemoralizeHumanoid", "sEffectDetectAnimal", "sEffectDetectEnchantment", "sEffectDetectKey", "sEffectDisintegrateArmor", "sEffectDisintegrateWeapon", "sEffectDispel", "sEffectDivineIntervention", "sEffectDrainAttribute", "sEffectDrainFatigue", "sEffectDrainHealth", "sEffectDrainSkill", "sEffectDrainSpellpoints", "sEffectExtraSpell", "sEffectFeather", "sEffectFireDamage", "sEffectFireShield", "sEffectFortifyAttackBonus", "sEffectFortifyAttribute", "sEffectFortifyFatigue", "sEffectFortifyHealth", "sEffectFortifyMagickaMultiplier", "sEffectFortifySkill", "sEffectFortifySpellpoints", "sEffectFrenzyCreature", "sEffectFrenzyHumanoid", "sEffectFrostDamage", "sEffectFrostShield", "sEffectInvisibility", "sEffectJump", "sEffectLevitate", "sEffectLight", "sEffectLightningShield", "sEffectLock", "sEffectMark", "sEffectNightEye", "sEffectOpen", "sEffectParalyze", "sEffectPoison", "sEffectRallyCreature", "sEffectRallyHumanoid", "sEffectRecall", "sEffectReflect", "sEffectRemoveCurse", "sEffectResistBlightDisease", "sEffectResistCommonDisease", "sEffectResistCorprusDisease", "sEffectResistFire", "sEffectResistFrost", "sEffectResistMagicka", "sEffectResistNormalWeapons", "sEffectResistParalysis", "sEffectResistPoison", "sEffectResistShock", "sEffectRestoreAttribute", "sEffectRestoreFatigue", "sEffectRestoreHealth", "sEffectRestoreSkill", "sEffectRestoreSpellPoints", "sEffects", "sEffectSanctuary", "sEffectShield", "sEffectShockDamage", "sEffectSilence", "sEffectSlowFall", "sEffectSoultrap", "sEffectSound", "sEffectSpellAbsorption", "sEffectStuntedMagicka", "sEffectSummonAncestralGhost", "sEffectSummonBonelord", "sEffectSummonCenturionSphere", "sEffectSummonClannfear", "sEffectSummonCreature01", "sEffectSummonCreature02", "sEffectSummonCreature03", "sEffectSummonCreature04", "sEffectSummonCreature05", "sEffectSummonDaedroth", "sEffectSummonDremora", "sEffectSummonFabricant", "sEffectSummonFlameAtronach", "sEffectSummonFrostAtronach", "sEffectSummonGoldensaint", "sEffectSummonGreaterBonewalker", "sEffectSummonHunger", "sEffectSummonLeastBonewalker", "sEffectSummonScamp", "sEffectSummonSkeletalMinion", "sEffectSummonStormAtronach", "sEffectSummonWingedTwilight", "sEffectSunDamage", "sEffectSwiftSwim", "sEffectTelekinesis", "sEffectTurnUndead", "sEffectVampirism", "sEffectWaterBreathing", "sEffectWaterWalking", "sEffectWeaknessToBlightDisease", "sEffectWeaknessToCommonDisease", "sEffectWeaknessToCorprusDisease", "sEffectWeaknessToFire", "sEffectWeaknessToFrost", "sEffectWeaknessToMagicka", "sEffectWeaknessToNormalWeapons", "sEffectWeaknessToPoison", "sEffectWeaknessToShock", "sEnableJoystick", "sEnchanting", "sEnchantItems", "sEnchantmentHelp1", "sEnchantmentHelp10", "sEnchantmentHelp2", "sEnchantmentHelp3", "sEnchantmentHelp4", "sEnchantmentHelp5", "sEnchantmentHelp6", "sEnchantmentHelp7", "sEnchantmentHelp8", "sEnchantmentHelp9", "sEnchantmentMenu1", "sEnchantmentMenu10", "sEnchantmentMenu11", "sEnchantmentMenu12", "sEnchantmentMenu2", "sEnchantmentMenu3", "sEnchantmentMenu4", "sEnchantmentMenu5", "sEnchantmentMenu6", "sEnchantmentMenu7", "sEnchantmentMenu8", "sEnchantmentMenu9", "sEncumbrance", "sEndDesc", "sEquip", "sExitGame", "sExpelled", "sExpelledMessage", "sFace", "sFaction", "sFar", "sFast", "sFatDesc", "sFatigue", "sFavoriteSkills", "sfeet", "sFileSize", "sfootarea", "sFootsteps", "sfor", "sFortify", "sForward", "sForwardXbox", "sFull", "sGame", "sGameWithoutLauncherXbox", "sGamma_Correction", "sGeneralMastPlugMismatchMsg", "sGold", "sGoodbye", "sGoverningAttribute", "sgp", "sHair", "sHard", "sHeal", "sHealer", "sHealth", "sHealthDesc", "sHealthPerHourOfRest", "sHealthPerLevel", "sHeavy", "sHigh", "sin", "sInfo", "sInfoRefusal", "sIngredients", "sInPrisonTitle", "sInputMenu1", "sIntDesc", "sIntimidate", "sIntimidateFail", "sIntimidateSuccess", "sInvalidSaveGameMsg", "sInvalidSaveGameMsgXBOX", "sInventory", "sInventoryMenu1", "sInventoryMessage1", "sInventoryMessage2", "sInventoryMessage3", "sInventoryMessage4", "sInventoryMessage5", "sInventorySelectNoIngredients", "sInventorySelectNoItems", "sInventorySelectNoSoul", "sItem", "sItemCastConstant", "sItemCastOnce", "sItemCastWhenStrikes", "sItemCastWhenUsed", "sItemName", "sJournal", "sJournalCmd", "sJournalEntry", "sJournalXbox", "sJoystickHatShort", "sJoystickNotFound", "sJoystickShort", "sJump", "sJumpXbox", "sKeyName_00", "sKeyName_01", "sKeyName_02", "sKeyName_03", "sKeyName_04", "sKeyName_05", "sKeyName_06", "sKeyName_07", "sKeyName_08", "sKeyName_09", "sKeyName_0A", "sKeyName_0B", "sKeyName_0C", "sKeyName_0D", "sKeyName_0E", "sKeyName_0F", "sKeyName_10", "sKeyName_11", "sKeyName_12", "sKeyName_13", "sKeyName_14", "sKeyName_15", "sKeyName_16", "sKeyName_17", "sKeyName_18", "sKeyName_19", "sKeyName_1A", "sKeyName_1B", "sKeyName_1C", "sKeyName_1D", "sKeyName_1E", "sKeyName_1F", "sKeyName_20", "sKeyName_21", "sKeyName_22", "sKeyName_23", "sKeyName_24", "sKeyName_25", "sKeyName_26", "sKeyName_27", "sKeyName_28", "sKeyName_29", "sKeyName_2A", "sKeyName_2B", "sKeyName_2C", "sKeyName_2D", "sKeyName_2E", "sKeyName_2F", "sKeyName_30", "sKeyName_31", "sKeyName_32", "sKeyName_33", "sKeyName_34", "sKeyName_35", "sKeyName_36", "sKeyName_37", "sKeyName_38", "sKeyName_39", "sKeyName_3A", "sKeyName_3B", "sKeyName_3C", "sKeyName_3D", "sKeyName_3E", "sKeyName_3F", "sKeyName_40", "sKeyName_41", "sKeyName_42", "sKeyName_43", "sKeyName_44", "sKeyName_45", "sKeyName_46", "sKeyName_47", "sKeyName_48", "sKeyName_49", "sKeyName_4A", "sKeyName_4B", "sKeyName_4C", "sKeyName_4D", "sKeyName_4E", "sKeyName_4F", "sKeyName_50", "sKeyName_51", "sKeyName_52", "sKeyName_53", "sKeyName_54", "sKeyName_55", "sKeyName_56", "sKeyName_57", "sKeyName_58", "sKeyName_59", "sKeyName_5A", "sKeyName_5B", "sKeyName_5C", "sKeyName_5D", "sKeyName_5E", "sKeyName_5F", "sKeyName_60", "sKeyName_61", "sKeyName_62", "sKeyName_63", "sKeyName_64", "sKeyName_65", "sKeyName_66", "sKeyName_67", "sKeyName_68", "sKeyName_69", "sKeyName_6A", "sKeyName_6B", "sKeyName_6C", "sKeyName_6D", "sKeyName_6E", "sKeyName_6F", "sKeyName_70", "sKeyName_71", "sKeyName_72", "sKeyName_73", "sKeyName_74", "sKeyName_75", "sKeyName_76", "sKeyName_77", "sKeyName_78", "sKeyName_79", "sKeyName_7A", "sKeyName_7B", "sKeyName_7C", "sKeyName_7D", "sKeyName_7E", "sKeyName_7F", "sKeyName_80", "sKeyName_81", "sKeyName_82", "sKeyName_83", "sKeyName_84", "sKeyName_85", "sKeyName_86", "sKeyName_87", "sKeyName_88", "sKeyName_89", "sKeyName_8A", "sKeyName_8B", "sKeyName_8C", "sKeyName_8D", "sKeyName_8E", "sKeyName_8F", "sKeyName_90", "sKeyName_91", "sKeyName_92", "sKeyName_93", "sKeyName_94", "sKeyName_95", "sKeyName_96", "sKeyName_97", "sKeyName_98", "sKeyName_99", "sKeyName_9A", "sKeyName_9B", "sKeyName_9C", "sKeyName_9D", "sKeyName_9E", "sKeyName_9F", "sKeyName_A0", "sKeyName_A1", "sKeyName_A2", "sKeyName_A3", "sKeyName_A4", "sKeyName_A5", "sKeyName_A6", "sKeyName_A7", "sKeyName_A8", "sKeyName_A9", "sKeyName_AA", "sKeyName_AB", "sKeyName_AC", "sKeyName_AD", "sKeyName_AE", "sKeyName_AF", "sKeyName_B0", "sKeyName_B1", "sKeyName_B2", "sKeyName_B3", "sKeyName_B4", "sKeyName_B5", "sKeyName_B6", "sKeyName_B7", "sKeyName_B8", "sKeyName_B9", "sKeyName_BA", "sKeyName_BB", "sKeyName_BC", "sKeyName_BD", "sKeyName_BE", "sKeyName_BF", "sKeyName_C0", "sKeyName_C1", "sKeyName_C2", "sKeyName_C3", "sKeyName_C4", "sKeyName_C5", "sKeyName_C6", "sKeyName_C7", "sKeyName_C8", "sKeyName_C9", "sKeyName_CA", "sKeyName_CB", "sKeyName_CC", "sKeyName_CD", "sKeyName_CE", "sKeyName_CF", "sKeyName_D0", "sKeyName_D1", "sKeyName_D2", "sKeyName_D3", "sKeyName_D4", "sKeyName_D5", "sKeyName_D6", "sKeyName_D7", "sKeyName_D8", "sKeyName_D9", "sKeyName_DA", "sKeyName_DB", "sKeyName_DC", "sKeyName_DD", "sKeyName_DE", "sKeyName_DF", "sKeyName_E0", "sKeyName_E1", "sKeyName_E2", "sKeyName_E3", "sKeyName_E4", "sKeyName_E5", "sKeyName_E6", "sKeyName_E7", "sKeyName_E8", "sKeyName_E9", "sKeyName_EA", "sKeyName_EB", "sKeyName_EC", "sKeyName_ED", "sKeyName_EE", "sKeyName_EF", "sKeyName_F0", "sKeyName_F1", "sKeyName_F2", "sKeyName_F3", "sKeyName_F4", "sKeyName_F5", "sKeyName_F6", "sKeyName_F7", "sKeyName_F8", "sKeyName_F9", "sKeyName_FA", "sKeyName_FB", "sKeyName_FC", "sKeyName_FD", "sKeyName_FE", "sKeyName_FF", "sKeyUsed", "sKilledEssential", "sKnight", "sLeft", "sLess", "sLevel", "sLevelProgress", "sLevels", "sLevelUp", "sLevelUpMenu1", "sLevelUpMenu2", "sLevelUpMenu3", "sLevelUpMenu4", "sLevelUpMsg", "sLevitateDisabled", "sLight", "sLight_Gamma", "sLoadFailedMessage", "sLoadGame", "sLoadingErrorsMsg", "sLoadingMessage1", "sLoadingMessage14", "sLoadingMessage15", "sLoadingMessage2", "sLoadingMessage3", "sLoadingMessage4", "sLoadingMessage5", "sLoadingMessage9", "sLoadLastSaveMsg", "sLocal", "sLockFail", "sLockImpossible", "sLockLevel", "sLockSuccess", "sLookDownXbox", "sLookUpXbox", "sLow", "sLucDesc", "sMagDesc", "sMage", "sMagic", "sMagicAncestralGhostID", "sMagicBonelordID", "sMagicBoundBattleAxeID", "sMagicBoundBootsID", "sMagicBoundCuirassID", "sMagicBoundDaggerID", "sMagicBoundHelmID", "sMagicBoundLeftGauntletID", "sMagicBoundLongbowID", "sMagicBoundLongswordID", "sMagicBoundMaceID", "sMagicBoundRightGauntletID", "sMagicBoundShieldID", "sMagicBoundSpearID", "sMagicCannotRecast", "sMagicCenturionSphereID", "sMagicClannfearID", "sMagicContractDisease", "sMagicCorprusWorsens", "sMagicCreature01ID", "sMagicCreature02ID", "sMagicCreature03ID", "sMagicCreature04ID", "sMagicCreature05ID", "sMagicDaedrothID", "sMagicDremoraID", "sMagicEffects", "sMagicFabricantID", "sMagicFlameAtronachID", "sMagicFrostAtronachID", "sMagicGoldenSaintID", "sMagicGreaterBonewalkerID", "sMagicHungerID", "sMagicInsufficientCharge", "sMagicInsufficientSP", "sMagicInvalidEffect", "sMagicInvalidTarget", "sMagicItem", "sMagicLeastBonewalkerID", "sMagicLockSuccess", "sMagicMenu", "sMagicOpenSuccess", "sMagicPCResisted", "sMagicScampID", "sMagicSelectTitle", "sMagicSkeletalMinionID", "sMagicSkillFail", "sMagicStormAtronachID", "sMagicTab", "sMagicTargetResisted", "sMagicTargetResistsWeapons", "sMagicWingedTwilightID", "sMagnitude", "sMagnitudeDes", "sMake Enchantment", "sMap", "sMaster", "sMastPlugMismatchMsg", "sMaximumSaveGameMessage", "sMaxSale", "sMedium", "sMenu_Help_Delay", "sMenu_Mode", "sMenuModeXbox", "sMenuNextXbox", "sMenuPrevXbox", "sMenus", "sMessage1", "sMessage2", "sMessage3", "sMessage4", "sMessage5", "sMessageQuestionAnswer1", "sMessageQuestionAnswer2", "sMessageQuestionAnswer3", "sMiscTab", "sMissingMastersMsg", "sMonk", "sMonthEveningstar", "sMonthFirstseed", "sMonthFrostfall", "sMonthHeartfire", "sMonthLastseed", "sMonthMidyear", "sMonthMorningstar", "sMonthRainshand", "sMonthSecondseed", "sMonthSunsdawn", "sMonthSunsdusk", "sMonthSunsheight", "sMore", "sMortar", "sMouse", "sMouseFlip", "sMouseWheelDownShort", "sMouseWheelUpShort", "sMove", "sMoveDownXbox", "sMoveUpXbox", "sMusic", "sName", "sNameTitle", "sNear", "sNeedOneSkill", "sNeedTwoSkills", "sNewGame", "sNext", "sNextRank", "sNextSpell", "sNextSpellXbox", "sNextWeapon", "sNextWeaponXbox", "sNightblade", "sNo", "sNoName", "sNone", "sNotifyMessage1", "sNotifyMessage10", "sNotifyMessage11", "sNotifyMessage12", "sNotifyMessage13", "sNotifyMessage14", "sNotifyMessage15", "sNotifyMessage16", "sNotifyMessage16_a", "sNotifyMessage17", "sNotifyMessage18", "sNotifyMessage19", "sNotifyMessage2", "sNotifyMessage20", "sNotifyMessage21", "sNotifyMessage22", "sNotifyMessage23", "sNotifyMessage24", "sNotifyMessage25", "sNotifyMessage26", "sNotifyMessage27", "sNotifyMessage28", "sNotifyMessage29", "sNotifyMessage3", "sNotifyMessage30", "sNotifyMessage31", "sNotifyMessage32", "sNotifyMessage33", "sNotifyMessage34", "sNotifyMessage35", "sNotifyMessage36", "sNotifyMessage37", "sNotifyMessage38", "sNotifyMessage39", "sNotifyMessage4", "sNotifyMessage40", "sNotifyMessage41", "sNotifyMessage42", "sNotifyMessage43", "sNotifyMessage44", "sNotifyMessage45", "sNotifyMessage46", "sNotifyMessage47", "sNotifyMessage48", "sNotifyMessage49", "sNotifyMessage4XBOX", "sNotifyMessage5", "sNotifyMessage50", "sNotifyMessage51", "sNotifyMessage52", "sNotifyMessage53", "sNotifyMessage54", "sNotifyMessage55", "sNotifyMessage56", "sNotifyMessage57", "sNotifyMessage58", "sNotifyMessage59", "sNotifyMessage6", "sNotifyMessage60", "sNotifyMessage61", "sNotifyMessage62", "sNotifyMessage63", "sNotifyMessage64", "sNotifyMessage65", "sNotifyMessage66", "sNotifyMessage67", "sNotifyMessage6a", "sNotifyMessage7", "sNotifyMessage8", "sNotifyMessage9", "sOff", "sOffer", "sOfferMenuTitle", "sOK", "sOn", "sOnce", "sOneHanded", "sOnetypeEffectMessage", "sonword", "sOptions", "sOptionsMenuXbox", "spercent", "sPerDesc", "sPersuasion", "sPersuasionMenuTitle", "sPickUp", "sPilgrim", "spoint", "spoints", "sPotionSuccess", "sPowerAlreadyUsed", "sPowers", "sPreferences", "sPrefs", "sPrev", "sPrevSpell", "sPrevSpellXbox", "sPrevWeapon", "sPrevWeaponXbox", "sProfitValue", "sQuality", "sQuanityMenuMessage01", "sQuanityMenuMessage02", "sQuestionDeleteSpell", "sQuestionMark", "sQuick0Xbox", "sQuick10Cmd", "sQuick1Cmd", "sQuick2Cmd", "sQuick3Cmd", "sQuick4Cmd", "sQuick4Xbox", "sQuick5Cmd", "sQuick5Xbox", "sQuick6Cmd", "sQuick6Xbox", "sQuick7Cmd", "sQuick7Xbox", "sQuick8Cmd", "sQuick8Xbox", "sQuick9Cmd", "sQuick9Xbox", "sQuick_Save", "sQuickLoadCmd", "sQuickLoadXbox", "sQuickMenu", "sQuickMenu1", "sQuickMenu2", "sQuickMenu3", "sQuickMenu4", "sQuickMenu5", "sQuickMenu6", "sQuickMenuInstruc", "sQuickMenuTitle", "sQuickSaveCmd", "sQuickSaveXbox", "sRace", "sRaceMenu1", "sRaceMenu2", "sRaceMenu3", "sRaceMenu4", "sRaceMenu5", "sRaceMenu6", "sRaceMenu7", "sRacialTraits", "sRange", "sRangeDes", "sRangeSelf", "sRangeTarget", "sRangeTouch", "sReady_Magic", "sReady_Weapon", "sReadyItemXbox", "sReadyMagicXbox", "sRechargeEnchantment", "sRender_Distance", "sRepair", "sRepairFailed", "sRepairServiceTitle", "sRepairSuccess", "sReputation", "sResChangeWarning", "sRest", "sRestIllegal", "sRestKey", "sRestMenu1", "sRestMenu2", "sRestMenu3", "sRestMenu4", "sRestMenuXbox", "sRestore", "sRetort", "sReturnToGame", "sRight", "sRogue", "sRun", "sRunXbox", "sSave", "sSaveGame", "sSaveGameDenied", "sSaveGameFailed", "sSaveGameNoMemory", "sSaveGameTooBig", "sSaveMenu1", "sSaveMenuHelp01", "sSaveMenuHelp02", "sSaveMenuHelp03", "sSaveMenuHelp04", "sSaveMenuHelp05", "sSaveMenuHelp06", "sSchool", "sSchoolAlteration", "sSchoolConjuration", "sSchoolDestruction", "sSchoolIllusion", "sSchoolMysticism", "sSchoolRestoration", "sScout", "sScrolldown", "sScrollup", "ssecond", "sseconds", "sSeldom", "sSelect", "sSell", "sSellerGold", "sService", "sServiceRefusal", "sServiceRepairTitle", "sServiceSpellsTitle", "sServiceTrainingTitle", "sServiceTrainingWords", "sServiceTravelTitle", "sSetValueMessage01", "sSex", "sShadows", "sShadowText", "sShift", "sSkill", "sSkillAcrobatics", "sSkillAlchemy", "sSkillAlteration", "sSkillArmorer", "sSkillAthletics", "sSkillAxe", "sSkillBlock", "sSkillBluntweapon", "sSkillClassMajor", "sSkillClassMinor", "sSkillClassMisc", "sSkillConjuration", "sSkillDestruction", "sSkillEnchant", "sSkillHandtohand", "sSkillHeavyarmor", "sSkillIllusion", "sSkillLightarmor", "sSkillLongblade", "sSkillMarksman", "sSkillMaxReached", "sSkillMediumarmor", "sSkillMercantile", "sSkillMysticism", "sSkillProgress", "sSkillRestoration", "sSkillSecurity", "sSkillShortblade", "sSkillsMenu1", "sSkillsMenuReputationHelp", "sSkillSneak", "sSkillSpear", "sSkillSpeechcraft", "sSkillUnarmored", "sSlash", "sSleepInterrupt", "sSlideLeftXbox", "sSlideRightXbox", "sSlow", "sSorceror", "sSoulGem", "sSoulGemsWithSouls", "sSoultrapSuccess", "sSpace", "sSpdDesc", "sSpecialization", "sSpecializationCombat", "sSpecializationMagic", "sSpecializationMenu1", "sSpecializationStealth", "sSpellmaking", "sSpellmakingHelp1", "sSpellmakingHelp2", "sSpellmakingHelp3", "sSpellmakingHelp4", "sSpellmakingHelp5", "sSpellmakingHelp6", "sSpellmakingMenu1", "sSpellmakingMenuTitle", "sSpells", "sSpellServiceTitle", "sSpellsword", "sStartCell", "sStartCellError", "sStartError", "sStats", "sStrafe", "sStrDesc", "sStrip", "sSubtitles", "sSystemMenuXbox", "sTake", "sTakeAll", "sTargetCriticalStrike", "sTaunt", "sTauntFail", "sTauntSuccess", "sTeleportDisabled", "sThief", "sThrust", "sTo", "sTogglePOVCmd", "sTogglePOVXbox", "sToggleRunXbox", "sTopics", "sTotalCost", "sTotalSold", "sTraining", "sTrainingServiceTitle", "sTraits", "sTransparency_Menu", "sTrapFail", "sTrapImpossible", "sTrapped", "sTrapSuccess", "sTravel", "sTravelServiceTitle", "sTurn", "sTurnLeftXbox", "sTurnRightXbox", "sTwoHanded", "sType", "sTypeAbility", "sTypeBlightDisease", "sTypeCurse", "sTypeDisease", "sTypePower", "sTypeSpell", "sUnequip", "sUnlocked", "sUntilHealed", "sUse", "sUserDefinedClass", "sUses", "sUseXbox", "sValue", "sVideo", "sVideoWarning", "sVoice", "sWait", "sWarrior", "sWaterReflectUpdate", "sWaterTerrainReflect", "sWeaponTab", "sWeight", "sWerewolfAlarmMessage", "sWerewolfPopup", "sWerewolfRefusal", "sWerewolfRestMessage", "sWilDesc", "sWitchhunter", "sWorld", "sWornTab", "sXStrafe", "sXTimes", "sXTimesINT", "sYes", "sYourGold" }; const char * CSMWorld::DefaultGmsts::OptionalFloats[CSMWorld::DefaultGmsts::OptionalFloatCount] = { "fCombatDistanceWerewolfMod", "fFleeDistance", "fWereWolfAcrobatics", "fWereWolfAgility", "fWereWolfAlchemy", "fWereWolfAlteration", "fWereWolfArmorer", "fWereWolfAthletics", "fWereWolfAxe", "fWereWolfBlock", "fWereWolfBluntWeapon", "fWereWolfConjuration", "fWereWolfDestruction", "fWereWolfEnchant", "fWereWolfEndurance", "fWereWolfFatigue", "fWereWolfHandtoHand", "fWereWolfHealth", "fWereWolfHeavyArmor", "fWereWolfIllusion", "fWereWolfIntellegence", "fWereWolfLightArmor", "fWereWolfLongBlade", "fWereWolfLuck", "fWereWolfMagicka", "fWereWolfMarksman", "fWereWolfMediumArmor", "fWereWolfMerchantile", "fWereWolfMysticism", "fWereWolfPersonality", "fWereWolfRestoration", "fWereWolfRunMult", "fWereWolfSecurity", "fWereWolfShortBlade", "fWereWolfSilverWeaponDamageMult", "fWereWolfSneak", "fWereWolfSpear", "fWereWolfSpeechcraft", "fWereWolfSpeed", "fWereWolfStrength", "fWereWolfUnarmored", "fWereWolfWillPower" }; const char * CSMWorld::DefaultGmsts::OptionalInts[CSMWorld::DefaultGmsts::OptionalIntCount] = { "iWereWolfBounty", "iWereWolfFightMod", "iWereWolfFleeMod", "iWereWolfLevelToAttack" }; const char * CSMWorld::DefaultGmsts::OptionalStrings[CSMWorld::DefaultGmsts::OptionalStringCount] = { "sCompanionShare", "sCompanionWarningButtonOne", "sCompanionWarningButtonTwo", "sCompanionWarningMessage", "sDeleteNote", "sEditNote", "sEffectSummonCreature01", "sEffectSummonCreature02", "sEffectSummonCreature03", "sEffectSummonCreature04", "sEffectSummonCreature05", "sEffectSummonFabricant", "sLevitateDisabled", "sMagicCreature01ID", "sMagicCreature02ID", "sMagicCreature03ID", "sMagicCreature04ID", "sMagicCreature05ID", "sMagicFabricantID", "sMaxSale", "sProfitValue", "sTeleportDisabled", "sWerewolfAlarmMessage", "sWerewolfPopup", "sWerewolfRefusal", "sWerewolfRestMessage" }; const float CSMWorld::DefaultGmsts::FloatsDefaultValues[CSMWorld::DefaultGmsts::FloatCount] = { 0.3f, // fAIFleeFleeMult 7.0f, // fAIFleeHealthMult 3.0f, // fAIMagicSpellMult 1.0f, // fAIMeleeArmorMult 1.0f, // fAIMeleeSummWeaponMult 2.0f, // fAIMeleeWeaponMult 5.0f, // fAIRangeMagicSpellMult 5.0f, // fAIRangeMeleeWeaponMult 2000.0f, // fAlarmRadius 1.0f, // fAthleticsRunBonus 40.0f, // fAudioDefaultMaxDistance 5.0f, // fAudioDefaultMinDistance 50.0f, // fAudioMaxDistanceMult 20.0f, // fAudioMinDistanceMult 60.0f, // fAudioVoiceDefaultMaxDistance 10.0f, // fAudioVoiceDefaultMinDistance 50.0f, // fAutoPCSpellChance 80.0f, // fAutoSpellChance 50.0f, // fBargainOfferBase -4.0f, // fBargainOfferMulti 24.0f, // fBarterGoldResetDelay 1.75f, // fBaseRunMultiplier 1.25f, // fBlockStillBonus 150.0f, // fBribe1000Mod 75.0f, // fBribe100Mod 35.0f, // fBribe10Mod 60.0f, // fCombatAngleXY 60.0f, // fCombatAngleZ 0.25f, // fCombatArmorMinMult -90.0f, // fCombatBlockLeftAngle 30.0f, // fCombatBlockRightAngle 4.0f, // fCombatCriticalStrikeMult 0.1f, // fCombatDelayCreature 0.1f, // fCombatDelayNPC 128.0f, // fCombatDistance 0.3f, // fCombatDistanceWerewolfMod 30.0f, // fCombatForceSideAngle 0.2f, // fCombatInvisoMult 1.5f, // fCombatKODamageMult 45.0f, // fCombatTorsoSideAngle 0.3f, // fCombatTorsoStartPercent 0.8f, // fCombatTorsoStopPercent 15.0f, // fConstantEffectMult 72.0f, // fCorpseClearDelay 72.0f, // fCorpseRespawnDelay 0.5f, // fCrimeGoldDiscountMult 0.9f, // fCrimeGoldTurnInMult 1.0f, // fCrimeStealing 0.5f, // fDamageStrengthBase 0.1f, // fDamageStrengthMult 5.0f, // fDifficultyMult 2.5f, // fDiseaseXferChance -10.0f, // fDispAttacking -1.0f, // fDispBargainFailMod 1.0f, // fDispBargainSuccessMod 0.0f, // fDispCrimeMod -10.0f, // fDispDiseaseMod 3.0f, // fDispFactionMod 1.0f, // fDispFactionRankBase 0.5f, // fDispFactionRankMult 1.0f, // fDispositionMod 50.0f, // fDispPersonalityBase 0.5f, // fDispPersonalityMult -25.0f, // fDispPickPocketMod 5.0f, // fDispRaceMod -0.5f, // fDispStealing -5.0f, // fDispWeaponDrawn 0.5f, // fEffectCostMult 0.1f, // fElementalShieldMult 3.0f, // fEnchantmentChanceMult 0.5f, // fEnchantmentConstantChanceMult 100.0f, // fEnchantmentConstantDurationMult 0.1f, // fEnchantmentMult 1000.0f, // fEnchantmentValueMult 0.3f, // fEncumberedMoveEffect 5.0f, // fEncumbranceStrMult 0.04f, // fEndFatigueMult 0.25f, // fFallAcroBase 0.01f, // fFallAcroMult 400.0f, // fFallDamageDistanceMin 0.0f, // fFallDistanceBase 0.07f, // fFallDistanceMult 2.0f, // fFatigueAttackBase 0.0f, // fFatigueAttackMult 1.25f, // fFatigueBase 4.0f, // fFatigueBlockBase 0.0f, // fFatigueBlockMult 5.0f, // fFatigueJumpBase 0.0f, // fFatigueJumpMult 0.5f, // fFatigueMult 2.5f, // fFatigueReturnBase 0.02f, // fFatigueReturnMult 5.0f, // fFatigueRunBase 2.0f, // fFatigueRunMult 1.5f, // fFatigueSneakBase 1.5f, // fFatigueSneakMult 0.0f, // fFatigueSpellBase 0.0f, // fFatigueSpellCostMult 0.0f, // fFatigueSpellMult 7.0f, // fFatigueSwimRunBase 0.0f, // fFatigueSwimRunMult 2.5f, // fFatigueSwimWalkBase 0.0f, // fFatigueSwimWalkMult 0.2f, // fFightDispMult 0.005f, // fFightDistanceMultiplier 50.0f, // fFightStealing 3000.0f, // fFleeDistance 512.0f, // fGreetDistanceReset 0.1f, // fHandtoHandHealthPer 1.0f, // fHandToHandReach 0.5f, // fHoldBreathEndMult 20.0f, // fHoldBreathTime 0.75f, // fIdleChanceMultiplier 1.0f, // fIngredientMult 0.5f, // fInteriorHeadTrackMult 128.0f, // fJumpAcrobaticsBase 4.0f, // fJumpAcroMultiplier 0.5f, // fJumpEncumbranceBase 1.0f, // fJumpEncumbranceMultiplier 0.5f, // fJumpMoveBase 0.5f, // fJumpMoveMult 1.0f, // fJumpRunMultiplier 0.5f, // fKnockDownMult 5.0f, // fLevelMod 0.1f, // fLevelUpHealthEndMult 0.6f, // fLightMaxMod 10.0f, // fLuckMod 10.0f, // fMagesGuildTravel 1.5f, // fMagicCreatureCastDelay 0.0167f, // fMagicDetectRefreshRate 1.0f, // fMagicItemConstantMult 1.0f, // fMagicItemCostMult 1.0f, // fMagicItemOnceMult 1.0f, // fMagicItemPriceMult 0.05f, // fMagicItemRechargePerSecond 1.0f, // fMagicItemStrikeMult 1.0f, // fMagicItemUsedMult 3.0f, // fMagicStartIconBlink 0.5f, // fMagicSunBlockedMult 0.75f, // fMajorSkillBonus 300.0f, // fMaxFlySpeed 0.5f, // fMaxHandToHandMult 400.0f, // fMaxHeadTrackDistance 200.0f, // fMaxWalkSpeed 300.0f, // fMaxWalkSpeedCreature 0.9f, // fMedMaxMod 0.1f, // fMessageTimePerChar 5.0f, // fMinFlySpeed 0.1f, // fMinHandToHandMult 1.0f, // fMinorSkillBonus 100.0f, // fMinWalkSpeed 5.0f, // fMinWalkSpeedCreature 1.25f, // fMiscSkillBonus 2.0f, // fNPCbaseMagickaMult 0.5f, // fNPCHealthBarFade 3.0f, // fNPCHealthBarTime 1.0f, // fPCbaseMagickaMult 0.3f, // fPerDieRollMult 5.0f, // fPersonalityMod 1.0f, // fPerTempMult -1.0f, // fPickLockMult 0.3f, // fPickPocketMod 20.0f, // fPotionMinUsefulDuration 0.5f, // fPotionStrengthMult 0.5f, // fPotionT1DurMult 1.5f, // fPotionT1MagMult 20.0f, // fPotionT4BaseStrengthMult 12.0f, // fPotionT4EquipStrengthMult 3000.0f, // fProjectileMaxSpeed 400.0f, // fProjectileMinSpeed 25.0f, // fProjectileThrownStoreChance 3.0f, // fRepairAmountMult 1.0f, // fRepairMult 1.0f, // fReputationMod 0.15f, // fRestMagicMult 0.0f, // fSeriousWoundMult 0.25f, // fSleepRandMod 0.3f, // fSleepRestMod -1.0f, // fSneakBootMult 0.5f, // fSneakDistanceBase 0.002f, // fSneakDistanceMultiplier 0.5f, // fSneakNoViewMult 1.0f, // fSneakSkillMult 0.75f, // fSneakSpeedMultiplier 1.0f, // fSneakUseDelay 500.0f, // fSneakUseDist 1.5f, // fSneakViewMult 3.0f, // fSoulGemMult 0.8f, // fSpecialSkillBonus 7.0f, // fSpellMakingValueMult 2.0f, // fSpellPriceMult 10.0f, // fSpellValueMult 0.25f, // fStromWalkMult 0.7f, // fStromWindSpeed 3.0f, // fSuffocationDamage 0.9f, // fSwimHeightScale 0.1f, // fSwimRunAthleticsMult 0.5f, // fSwimRunBase 0.02f, // fSwimWalkAthleticsMult 0.5f, // fSwimWalkBase 1.0f, // fSwingBlockBase 1.0f, // fSwingBlockMult 1000.0f, // fTargetSpellMaxSpeed 1000.0f, // fThrownWeaponMaxSpeed 300.0f, // fThrownWeaponMinSpeed 0.0f, // fTrapCostMult 4000.0f, // fTravelMult 16000.0f,// fTravelTimeMult 0.1f, // fUnarmoredBase1 0.065f, // fUnarmoredBase2 30.0f, // fVanityDelay 10.0f, // fVoiceIdleOdds 0.0f, // fWaterReflectUpdateAlways 10.0f, // fWaterReflectUpdateSeldom 0.1f, // fWeaponDamageMult 1.0f, // fWeaponFatigueBlockMult 0.25f, // fWeaponFatigueMult 150.0f, // fWereWolfAcrobatics 150.0f, // fWereWolfAgility 1.0f, // fWereWolfAlchemy 1.0f, // fWereWolfAlteration 1.0f, // fWereWolfArmorer 150.0f, // fWereWolfAthletics 1.0f, // fWereWolfAxe 1.0f, // fWereWolfBlock 1.0f, // fWereWolfBluntWeapon 1.0f, // fWereWolfConjuration 1.0f, // fWereWolfDestruction 1.0f, // fWereWolfEnchant 150.0f, // fWereWolfEndurance 400.0f, // fWereWolfFatigue 100.0f, // fWereWolfHandtoHand 2.0f, // fWereWolfHealth 1.0f, // fWereWolfHeavyArmor 1.0f, // fWereWolfIllusion 1.0f, // fWereWolfIntellegence 1.0f, // fWereWolfLightArmor 1.0f, // fWereWolfLongBlade 1.0f, // fWereWolfLuck 100.0f, // fWereWolfMagicka 1.0f, // fWereWolfMarksman 1.0f, // fWereWolfMediumArmor 1.0f, // fWereWolfMerchantile 1.0f, // fWereWolfMysticism 1.0f, // fWereWolfPersonality 1.0f, // fWereWolfRestoration 1.5f, // fWereWolfRunMult 1.0f, // fWereWolfSecurity 1.0f, // fWereWolfShortBlade 1.5f, // fWereWolfSilverWeaponDamageMult 1.0f, // fWereWolfSneak 1.0f, // fWereWolfSpear 1.0f, // fWereWolfSpeechcraft 150.0f, // fWereWolfSpeed 150.0f, // fWereWolfStrength 100.0f, // fWereWolfUnarmored 1.0f, // fWereWolfWillPower 15.0f // fWortChanceValue }; const int CSMWorld::DefaultGmsts::IntsDefaultValues[CSMWorld::DefaultGmsts::IntCount] = { 10, // i1stPersonSneakDelta 50, // iAlarmAttack 90, // iAlarmKilling 20, // iAlarmPickPocket 1, // iAlarmStealing 5, // iAlarmTresspass 2, // iAlchemyMod 100, // iAutoPCSpellMax 2, // iAutoRepFacMod 0, // iAutoRepLevMod 5, // iAutoSpellAlterationMax 70, // iAutoSpellAttSkillMin 2, // iAutoSpellConjurationMax 5, // iAutoSpellDestructionMax 5, // iAutoSpellIllusionMax 5, // iAutoSpellMysticismMax 5, // iAutoSpellRestorationMax 3, // iAutoSpellTimesCanCast -1, // iBarterFailDisposition 1, // iBarterSuccessDisposition 30, // iBaseArmorSkill 50, // iBlockMaxChance 10, // iBlockMinChance 20, // iBootsWeight 40, // iCrimeAttack 1000, // iCrimeKilling 25, // iCrimePickPocket 1000, // iCrimeThreshold 10, // iCrimeThresholdMultiplier 5, // iCrimeTresspass 30, // iCuirassWeight 100, // iDaysinPrisonMod -50, // iDispAttackMod -50, // iDispKilling -20, // iDispTresspass 1, // iFightAlarmMult 100, // iFightAttack 50, // iFightAttacking 20, // iFightDistanceBase 50, // iFightKilling 25, // iFightPickpocket 25, // iFightTrespass 0, // iFlee 5, // iGauntletWeight 15, // iGreavesWeight 6, // iGreetDistanceMultiplier 4, // iGreetDuration 5, // iHelmWeight 50, // iKnockDownOddsBase 50, // iKnockDownOddsMult 2, // iLevelUp01Mult 2, // iLevelUp02Mult 2, // iLevelUp03Mult 2, // iLevelUp04Mult 3, // iLevelUp05Mult 3, // iLevelUp06Mult 3, // iLevelUp07Mult 4, // iLevelUp08Mult 4, // iLevelUp09Mult 5, // iLevelUp10Mult 1, // iLevelupMajorMult 1, // iLevelupMajorMultAttribute 1, // iLevelupMinorMult 1, // iLevelupMinorMultAttribute 1, // iLevelupMiscMultAttriubte 1, // iLevelupSpecialization 10, // iLevelupTotal 10, // iMagicItemChargeConst 1, // iMagicItemChargeOnce 10, // iMagicItemChargeStrike 5, // iMagicItemChargeUse 192, // iMaxActivateDist 192, // iMaxInfoDist 4, // iMonthsToRespawn 1, // iNumberCreatures 10, // iPauldronWeight 5, // iPerMinChance 10, // iPerMinChange 75, // iPickMaxChance 5, // iPickMinChance 15, // iShieldWeight 400, // iSoulAmountForConstantEffect 10, // iTrainingMod 10, // iVoiceAttackOdds 30, // iVoiceHitOdds 10000, // iWereWolfBounty 100, // iWereWolfFightMod 100, // iWereWolfFleeMod 20 // iWereWolfLevelToAttack }; const float CSMWorld::DefaultGmsts::FloatLimits[CSMWorld::DefaultGmsts::FloatCount * 2] = { -FInf, FInf, // fAIFleeFleeMult -FInf, FInf, // fAIFleeHealthMult -FInf, FInf, // fAIMagicSpellMult -FInf, FInf, // fAIMeleeArmorMult -FInf, FInf, // fAIMeleeSummWeaponMult -FInf, FInf, // fAIMeleeWeaponMult -FInf, FInf, // fAIRangeMagicSpellMult -FInf, FInf, // fAIRangeMeleeWeaponMult 0, FInf, // fAlarmRadius -FInf, FInf, // fAthleticsRunBonus 0, FInf, // fAudioDefaultMaxDistance 0, FInf, // fAudioDefaultMinDistance 0, FInf, // fAudioMaxDistanceMult 0, FInf, // fAudioMinDistanceMult 0, FInf, // fAudioVoiceDefaultMaxDistance 0, FInf, // fAudioVoiceDefaultMinDistance 0, FInf, // fAutoPCSpellChance 0, FInf, // fAutoSpellChance -FInf, FInf, // fBargainOfferBase -FInf, 0, // fBargainOfferMulti -FInf, FInf, // fBarterGoldResetDelay 0, FInf, // fBaseRunMultiplier -FInf, FInf, // fBlockStillBonus 0, FInf, // fBribe1000Mod 0, FInf, // fBribe100Mod 0, FInf, // fBribe10Mod 0, FInf, // fCombatAngleXY 0, FInf, // fCombatAngleZ 0, 1, // fCombatArmorMinMult -180, 0, // fCombatBlockLeftAngle 0, 180, // fCombatBlockRightAngle 0, FInf, // fCombatCriticalStrikeMult 0, FInf, // fCombatDelayCreature 0, FInf, // fCombatDelayNPC 0, FInf, // fCombatDistance -FInf, FInf, // fCombatDistanceWerewolfMod -FInf, FInf, // fCombatForceSideAngle 0, FInf, // fCombatInvisoMult 0, FInf, // fCombatKODamageMult -FInf, FInf, // fCombatTorsoSideAngle -FInf, FInf, // fCombatTorsoStartPercent -FInf, FInf, // fCombatTorsoStopPercent -FInf, FInf, // fConstantEffectMult -FInf, FInf, // fCorpseClearDelay -FInf, FInf, // fCorpseRespawnDelay 0, 1, // fCrimeGoldDiscountMult 0, FInf, // fCrimeGoldTurnInMult 0, FInf, // fCrimeStealing 0, FInf, // fDamageStrengthBase 0, FInf, // fDamageStrengthMult -FInf, FInf, // fDifficultyMult 0, FInf, // fDiseaseXferChance -FInf, 0, // fDispAttacking -FInf, FInf, // fDispBargainFailMod -FInf, FInf, // fDispBargainSuccessMod -FInf, 0, // fDispCrimeMod -FInf, 0, // fDispDiseaseMod 0, FInf, // fDispFactionMod 0, FInf, // fDispFactionRankBase 0, FInf, // fDispFactionRankMult 0, FInf, // fDispositionMod 0, FInf, // fDispPersonalityBase 0, FInf, // fDispPersonalityMult -FInf, 0, // fDispPickPocketMod 0, FInf, // fDispRaceMod -FInf, 0, // fDispStealing -FInf, 0, // fDispWeaponDrawn 0, FInf, // fEffectCostMult 0, FInf, // fElementalShieldMult FEps, FInf, // fEnchantmentChanceMult 0, FInf, // fEnchantmentConstantChanceMult 0, FInf, // fEnchantmentConstantDurationMult 0, FInf, // fEnchantmentMult 0, FInf, // fEnchantmentValueMult 0, FInf, // fEncumberedMoveEffect 0, FInf, // fEncumbranceStrMult 0, FInf, // fEndFatigueMult -FInf, FInf, // fFallAcroBase 0, FInf, // fFallAcroMult 0, FInf, // fFallDamageDistanceMin -FInf, FInf, // fFallDistanceBase 0, FInf, // fFallDistanceMult -FInf, FInf, // fFatigueAttackBase 0, FInf, // fFatigueAttackMult 0, FInf, // fFatigueBase 0, FInf, // fFatigueBlockBase 0, FInf, // fFatigueBlockMult 0, FInf, // fFatigueJumpBase 0, FInf, // fFatigueJumpMult 0, FInf, // fFatigueMult -FInf, FInf, // fFatigueReturnBase 0, FInf, // fFatigueReturnMult -FInf, FInf, // fFatigueRunBase 0, FInf, // fFatigueRunMult -FInf, FInf, // fFatigueSneakBase 0, FInf, // fFatigueSneakMult -FInf, FInf, // fFatigueSpellBase -FInf, FInf, // fFatigueSpellCostMult 0, FInf, // fFatigueSpellMult -FInf, FInf, // fFatigueSwimRunBase 0, FInf, // fFatigueSwimRunMult -FInf, FInf, // fFatigueSwimWalkBase 0, FInf, // fFatigueSwimWalkMult -FInf, FInf, // fFightDispMult -FInf, FInf, // fFightDistanceMultiplier -FInf, FInf, // fFightStealing -FInf, FInf, // fFleeDistance -FInf, FInf, // fGreetDistanceReset 0, FInf, // fHandtoHandHealthPer 0, FInf, // fHandToHandReach -FInf, FInf, // fHoldBreathEndMult 0, FInf, // fHoldBreathTime 0, FInf, // fIdleChanceMultiplier -FInf, FInf, // fIngredientMult 0, FInf, // fInteriorHeadTrackMult -FInf, FInf, // fJumpAcrobaticsBase 0, FInf, // fJumpAcroMultiplier -FInf, FInf, // fJumpEncumbranceBase 0, FInf, // fJumpEncumbranceMultiplier -FInf, FInf, // fJumpMoveBase 0, FInf, // fJumpMoveMult 0, FInf, // fJumpRunMultiplier -FInf, FInf, // fKnockDownMult 0, FInf, // fLevelMod 0, FInf, // fLevelUpHealthEndMult 0, FInf, // fLightMaxMod 0, FInf, // fLuckMod 0, FInf, // fMagesGuildTravel -FInf, FInf, // fMagicCreatureCastDelay -FInf, FInf, // fMagicDetectRefreshRate -FInf, FInf, // fMagicItemConstantMult -FInf, FInf, // fMagicItemCostMult -FInf, FInf, // fMagicItemOnceMult -FInf, FInf, // fMagicItemPriceMult 0, FInf, // fMagicItemRechargePerSecond -FInf, FInf, // fMagicItemStrikeMult -FInf, FInf, // fMagicItemUsedMult 0, FInf, // fMagicStartIconBlink 0, FInf, // fMagicSunBlockedMult FEps, FInf, // fMajorSkillBonus 0, FInf, // fMaxFlySpeed 0, FInf, // fMaxHandToHandMult 0, FInf, // fMaxHeadTrackDistance 0, FInf, // fMaxWalkSpeed 0, FInf, // fMaxWalkSpeedCreature 0, FInf, // fMedMaxMod 0, FInf, // fMessageTimePerChar 0, FInf, // fMinFlySpeed 0, FInf, // fMinHandToHandMult FEps, FInf, // fMinorSkillBonus 0, FInf, // fMinWalkSpeed 0, FInf, // fMinWalkSpeedCreature FEps, FInf, // fMiscSkillBonus 0, FInf, // fNPCbaseMagickaMult 0, FInf, // fNPCHealthBarFade 0, FInf, // fNPCHealthBarTime 0, FInf, // fPCbaseMagickaMult 0, FInf, // fPerDieRollMult 0, FInf, // fPersonalityMod 0, FInf, // fPerTempMult -FInf, 0, // fPickLockMult 0, FInf, // fPickPocketMod -FInf, FInf, // fPotionMinUsefulDuration 0, FInf, // fPotionStrengthMult FEps, FInf, // fPotionT1DurMult FEps, FInf, // fPotionT1MagMult -FInf, FInf, // fPotionT4BaseStrengthMult -FInf, FInf, // fPotionT4EquipStrengthMult 0, FInf, // fProjectileMaxSpeed 0, FInf, // fProjectileMinSpeed 0, FInf, // fProjectileThrownStoreChance 0, FInf, // fRepairAmountMult 0, FInf, // fRepairMult 0, FInf, // fReputationMod 0, FInf, // fRestMagicMult -FInf, FInf, // fSeriousWoundMult 0, FInf, // fSleepRandMod 0, FInf, // fSleepRestMod -FInf, 0, // fSneakBootMult -FInf, FInf, // fSneakDistanceBase 0, FInf, // fSneakDistanceMultiplier 0, FInf, // fSneakNoViewMult 0, FInf, // fSneakSkillMult 0, FInf, // fSneakSpeedMultiplier 0, FInf, // fSneakUseDelay 0, FInf, // fSneakUseDist 0, FInf, // fSneakViewMult 0, FInf, // fSoulGemMult 0, FInf, // fSpecialSkillBonus 0, FInf, // fSpellMakingValueMult -FInf, FInf, // fSpellPriceMult 0, FInf, // fSpellValueMult 0, FInf, // fStromWalkMult 0, FInf, // fStromWindSpeed 0, FInf, // fSuffocationDamage 0, FInf, // fSwimHeightScale 0, FInf, // fSwimRunAthleticsMult 0, FInf, // fSwimRunBase -FInf, FInf, // fSwimWalkAthleticsMult -FInf, FInf, // fSwimWalkBase 0, FInf, // fSwingBlockBase 0, FInf, // fSwingBlockMult 0, FInf, // fTargetSpellMaxSpeed 0, FInf, // fThrownWeaponMaxSpeed 0, FInf, // fThrownWeaponMinSpeed 0, FInf, // fTrapCostMult 0, FInf, // fTravelMult 0, FInf, // fTravelTimeMult 0, FInf, // fUnarmoredBase1 0, FInf, // fUnarmoredBase2 0, FInf, // fVanityDelay 0, FInf, // fVoiceIdleOdds -FInf, FInf, // fWaterReflectUpdateAlways -FInf, FInf, // fWaterReflectUpdateSeldom 0, FInf, // fWeaponDamageMult 0, FInf, // fWeaponFatigueBlockMult 0, FInf, // fWeaponFatigueMult 0, FInf, // fWereWolfAcrobatics -FInf, FInf, // fWereWolfAgility -FInf, FInf, // fWereWolfAlchemy -FInf, FInf, // fWereWolfAlteration -FInf, FInf, // fWereWolfArmorer -FInf, FInf, // fWereWolfAthletics -FInf, FInf, // fWereWolfAxe -FInf, FInf, // fWereWolfBlock -FInf, FInf, // fWereWolfBluntWeapon -FInf, FInf, // fWereWolfConjuration -FInf, FInf, // fWereWolfDestruction -FInf, FInf, // fWereWolfEnchant -FInf, FInf, // fWereWolfEndurance -FInf, FInf, // fWereWolfFatigue -FInf, FInf, // fWereWolfHandtoHand -FInf, FInf, // fWereWolfHealth -FInf, FInf, // fWereWolfHeavyArmor -FInf, FInf, // fWereWolfIllusion -FInf, FInf, // fWereWolfIntellegence -FInf, FInf, // fWereWolfLightArmor -FInf, FInf, // fWereWolfLongBlade -FInf, FInf, // fWereWolfLuck -FInf, FInf, // fWereWolfMagicka -FInf, FInf, // fWereWolfMarksman -FInf, FInf, // fWereWolfMediumArmor -FInf, FInf, // fWereWolfMerchantile -FInf, FInf, // fWereWolfMysticism -FInf, FInf, // fWereWolfPersonality -FInf, FInf, // fWereWolfRestoration 0, FInf, // fWereWolfRunMult -FInf, FInf, // fWereWolfSecurity -FInf, FInf, // fWereWolfShortBlade -FInf, FInf, // fWereWolfSilverWeaponDamageMult -FInf, FInf, // fWereWolfSneak -FInf, FInf, // fWereWolfSpear -FInf, FInf, // fWereWolfSpeechcraft -FInf, FInf, // fWereWolfSpeed -FInf, FInf, // fWereWolfStrength -FInf, FInf, // fWereWolfUnarmored -FInf, FInf, // fWereWolfWillPower 0, FInf // fWortChanceValue }; const int CSMWorld::DefaultGmsts::IntLimits[CSMWorld::DefaultGmsts::IntCount * 2] = { IMin, IMax, // i1stPersonSneakDelta IMin, IMax, // iAlarmAttack IMin, IMax, // iAlarmKilling IMin, IMax, // iAlarmPickPocket IMin, IMax, // iAlarmStealing IMin, IMax, // iAlarmTresspass IMin, IMax, // iAlchemyMod 0, IMax, // iAutoPCSpellMax IMin, IMax, // iAutoRepFacMod IMin, IMax, // iAutoRepLevMod IMin, IMax, // iAutoSpellAlterationMax 0, IMax, // iAutoSpellAttSkillMin IMin, IMax, // iAutoSpellConjurationMax IMin, IMax, // iAutoSpellDestructionMax IMin, IMax, // iAutoSpellIllusionMax IMin, IMax, // iAutoSpellMysticismMax IMin, IMax, // iAutoSpellRestorationMax 0, IMax, // iAutoSpellTimesCanCast IMin, 0, // iBarterFailDisposition 0, IMax, // iBarterSuccessDisposition 1, IMax, // iBaseArmorSkill 0, IMax, // iBlockMaxChance 0, IMax, // iBlockMinChance 0, IMax, // iBootsWeight IMin, IMax, // iCrimeAttack IMin, IMax, // iCrimeKilling IMin, IMax, // iCrimePickPocket 0, IMax, // iCrimeThreshold 0, IMax, // iCrimeThresholdMultiplier IMin, IMax, // iCrimeTresspass 0, IMax, // iCuirassWeight 1, IMax, // iDaysinPrisonMod IMin, 0, // iDispAttackMod IMin, 0, // iDispKilling IMin, 0, // iDispTresspass IMin, IMax, // iFightAlarmMult IMin, IMax, // iFightAttack IMin, IMax, // iFightAttacking 0, IMax, // iFightDistanceBase IMin, IMax, // iFightKilling IMin, IMax, // iFightPickpocket IMin, IMax, // iFightTrespass IMin, IMax, // iFlee 0, IMax, // iGauntletWeight 0, IMax, // iGreavesWeight 0, IMax, // iGreetDistanceMultiplier 0, IMax, // iGreetDuration 0, IMax, // iHelmWeight IMin, IMax, // iKnockDownOddsBase IMin, IMax, // iKnockDownOddsMult IMin, IMax, // iLevelUp01Mult IMin, IMax, // iLevelUp02Mult IMin, IMax, // iLevelUp03Mult IMin, IMax, // iLevelUp04Mult IMin, IMax, // iLevelUp05Mult IMin, IMax, // iLevelUp06Mult IMin, IMax, // iLevelUp07Mult IMin, IMax, // iLevelUp08Mult IMin, IMax, // iLevelUp09Mult IMin, IMax, // iLevelUp10Mult IMin, IMax, // iLevelupMajorMult IMin, IMax, // iLevelupMajorMultAttribute IMin, IMax, // iLevelupMinorMult IMin, IMax, // iLevelupMinorMultAttribute IMin, IMax, // iLevelupMiscMultAttriubte IMin, IMax, // iLevelupSpecialization IMin, IMax, // iLevelupTotal IMin, IMax, // iMagicItemChargeConst IMin, IMax, // iMagicItemChargeOnce IMin, IMax, // iMagicItemChargeStrike IMin, IMax, // iMagicItemChargeUse IMin, IMax, // iMaxActivateDist IMin, IMax, // iMaxInfoDist 0, IMax, // iMonthsToRespawn 0, IMax, // iNumberCreatures 0, IMax, // iPauldronWeight 0, IMax, // iPerMinChance 0, IMax, // iPerMinChange 0, IMax, // iPickMaxChance 0, IMax, // iPickMinChance 0, IMax, // iShieldWeight 0, IMax, // iSoulAmountForConstantEffect 0, IMax, // iTrainingMod 0, IMax, // iVoiceAttackOdds 0, IMax, // iVoiceHitOdds IMin, IMax, // iWereWolfBounty IMin, IMax, // iWereWolfFightMod IMin, IMax, // iWereWolfFleeMod IMin, IMax // iWereWolfLevelToAttack }; ================================================ FILE: apps/opencs/model/world/defaultgmsts.hpp ================================================ #ifndef CSM_WORLD_DEFAULTGMSTS_H #define CSM_WORLD_DEFAULTGMSTS_H #include namespace CSMWorld { namespace DefaultGmsts { const size_t FloatCount = 258; const size_t IntCount = 89; const size_t StringCount = 1174; const size_t OptionalFloatCount = 42; const size_t OptionalIntCount = 4; const size_t OptionalStringCount = 26; extern const char* Floats[]; extern const char * Ints[]; extern const char * Strings[]; extern const char * OptionalFloats[]; extern const char * OptionalInts[]; extern const char * OptionalStrings[]; extern const float FloatsDefaultValues[]; extern const int IntsDefaultValues[]; extern const float FloatLimits[]; extern const int IntLimits[]; } } #endif ================================================ FILE: apps/opencs/model/world/idcollection.hpp ================================================ #ifndef CSM_WOLRD_IDCOLLECTION_H #define CSM_WOLRD_IDCOLLECTION_H #include #include "collection.hpp" #include "land.hpp" namespace CSMWorld { /// \brief Single type collection of top level records template > class IdCollection : public Collection { virtual void loadRecord (ESXRecordT& record, ESM::ESMReader& reader, bool& isDeleted); public: /// \return Index of loaded record (-1 if no record was loaded) int load (ESM::ESMReader& reader, bool base); /// \param index Index at which the record can be found. /// Special values: -2 index unknown, -1 record does not exist yet and therefore /// does not have an index /// /// \return index int load (const ESXRecordT& record, bool base, int index = -2); bool tryDelete (const std::string& id); ///< Try deleting \a id. If the id does not exist or can't be deleted the call is ignored. /// /// \return Has the ID been deleted? }; template void IdCollection::loadRecord (ESXRecordT& record, ESM::ESMReader& reader, bool& isDeleted) { record.load (reader, isDeleted); } template<> inline void IdCollection >::loadRecord (Land& record, ESM::ESMReader& reader, bool& isDeleted) { record.load (reader, isDeleted); // Load all land data for now. A future optimisation may only load non-base data // if a suitable mechanism for avoiding race conditions can be established. int flags = ESM::Land::DATA_VHGT | ESM::Land::DATA_VNML | ESM::Land::DATA_VCLR | ESM::Land::DATA_VTEX; record.loadData (flags); // Prevent data from being reloaded. record.mContext.filename.clear(); } template int IdCollection::load (ESM::ESMReader& reader, bool base) { ESXRecordT record; bool isDeleted = false; loadRecord (record, reader, isDeleted); std::string id = IdAccessorT().getId (record); int index = this->searchId (id); if (isDeleted) { if (index==-1) { // deleting a record that does not exist // ignore it for now /// \todo report the problem to the user return -1; } if (base) { this->removeRows (index, 1); return -1; } Record baseRecord = this->getRecord (index); baseRecord.mState = RecordBase::State_Deleted; this->setRecord (index, baseRecord); return index; } return load (record, base, index); } template int IdCollection::load (const ESXRecordT& record, bool base, int index) { if (index==-2) index = this->searchId (IdAccessorT().getId (record)); if (index==-1) { // new record Record record2; record2.mState = base ? RecordBase::State_BaseOnly : RecordBase::State_ModifiedOnly; (base ? record2.mBase : record2.mModified) = record; index = this->getSize(); this->appendRecord (record2); } else { // old record Record record2 = Collection::getRecord (index); if (base) record2.mBase = record; else record2.setModified (record); this->setRecord (index, record2); } return index; } template bool IdCollection::tryDelete (const std::string& id) { int index = this->searchId (id); if (index==-1) return false; Record record = Collection::getRecord (index); if (record.isDeleted()) return false; if (record.mState==RecordBase::State_ModifiedOnly) { Collection::removeRows (index, 1); } else { record.mState = RecordBase::State_Deleted; this->setRecord (index, record); } return true; } } #endif ================================================ FILE: apps/opencs/model/world/idcompletionmanager.cpp ================================================ #include "idcompletionmanager.hpp" #include #include "../../view/widget/completerpopup.hpp" #include "data.hpp" #include "idtablebase.hpp" namespace { std::map generateModelTypes() { std::map types; types[CSMWorld::ColumnBase::Display_BodyPart ] = CSMWorld::UniversalId::Type_BodyPart; types[CSMWorld::ColumnBase::Display_Cell ] = CSMWorld::UniversalId::Type_Cell; types[CSMWorld::ColumnBase::Display_Class ] = CSMWorld::UniversalId::Type_Class; types[CSMWorld::ColumnBase::Display_CreatureLevelledList] = CSMWorld::UniversalId::Type_Referenceable; types[CSMWorld::ColumnBase::Display_Creature ] = CSMWorld::UniversalId::Type_Referenceable; types[CSMWorld::ColumnBase::Display_Enchantment ] = CSMWorld::UniversalId::Type_Enchantment; types[CSMWorld::ColumnBase::Display_Faction ] = CSMWorld::UniversalId::Type_Faction; types[CSMWorld::ColumnBase::Display_GlobalVariable ] = CSMWorld::UniversalId::Type_Global; types[CSMWorld::ColumnBase::Display_Icon ] = CSMWorld::UniversalId::Type_Icon; types[CSMWorld::ColumnBase::Display_Journal ] = CSMWorld::UniversalId::Type_Journal; types[CSMWorld::ColumnBase::Display_Mesh ] = CSMWorld::UniversalId::Type_Mesh; types[CSMWorld::ColumnBase::Display_Miscellaneous ] = CSMWorld::UniversalId::Type_Referenceable; types[CSMWorld::ColumnBase::Display_Npc ] = CSMWorld::UniversalId::Type_Referenceable; types[CSMWorld::ColumnBase::Display_Race ] = CSMWorld::UniversalId::Type_Race; types[CSMWorld::ColumnBase::Display_Region ] = CSMWorld::UniversalId::Type_Region; types[CSMWorld::ColumnBase::Display_Referenceable ] = CSMWorld::UniversalId::Type_Referenceable; types[CSMWorld::ColumnBase::Display_Script ] = CSMWorld::UniversalId::Type_Script; types[CSMWorld::ColumnBase::Display_Skill ] = CSMWorld::UniversalId::Type_Skill; types[CSMWorld::ColumnBase::Display_Sound ] = CSMWorld::UniversalId::Type_Sound; types[CSMWorld::ColumnBase::Display_SoundRes ] = CSMWorld::UniversalId::Type_SoundRes; types[CSMWorld::ColumnBase::Display_Spell ] = CSMWorld::UniversalId::Type_Spell; types[CSMWorld::ColumnBase::Display_Static ] = CSMWorld::UniversalId::Type_Referenceable; types[CSMWorld::ColumnBase::Display_Texture ] = CSMWorld::UniversalId::Type_Texture; types[CSMWorld::ColumnBase::Display_Topic ] = CSMWorld::UniversalId::Type_Topic; types[CSMWorld::ColumnBase::Display_Weapon ] = CSMWorld::UniversalId::Type_Referenceable; return types; } typedef std::map::const_iterator ModelTypeConstIterator; } const std::map CSMWorld::IdCompletionManager::sCompleterModelTypes = generateModelTypes(); std::vector CSMWorld::IdCompletionManager::getDisplayTypes() { std::vector types; ModelTypeConstIterator current = sCompleterModelTypes.begin(); ModelTypeConstIterator end = sCompleterModelTypes.end(); for (; current != end; ++current) { types.push_back(current->first); } // Hack for Display_InfoCondVar types.push_back(CSMWorld::ColumnBase::Display_InfoCondVar); return types; } CSMWorld::IdCompletionManager::IdCompletionManager(CSMWorld::Data &data) { generateCompleters(data); } bool CSMWorld::IdCompletionManager::hasCompleterFor(CSMWorld::ColumnBase::Display display) const { return mCompleters.find(display) != mCompleters.end(); } std::shared_ptr CSMWorld::IdCompletionManager::getCompleter(CSMWorld::ColumnBase::Display display) { if (!hasCompleterFor(display)) { throw std::logic_error("This column doesn't have an ID completer"); } return mCompleters[display]; } void CSMWorld::IdCompletionManager::generateCompleters(CSMWorld::Data &data) { ModelTypeConstIterator current = sCompleterModelTypes.begin(); ModelTypeConstIterator end = sCompleterModelTypes.end(); for (; current != end; ++current) { QAbstractItemModel *model = data.getTableModel(current->second); CSMWorld::IdTableBase *table = dynamic_cast(model); if (table != nullptr) { int idColumn = table->searchColumnIndex(CSMWorld::Columns::ColumnId_Id); if (idColumn != -1) { std::shared_ptr completer = std::make_shared(table); completer->setCompletionColumn(idColumn); // The completion role must be Qt::DisplayRole to get the ID values from the model completer->setCompletionRole(Qt::DisplayRole); completer->setCaseSensitivity(Qt::CaseInsensitive); QAbstractItemView *popup = new CSVWidget::CompleterPopup(); completer->setPopup(popup); // The completer takes ownership of the popup completer->setMaxVisibleItems(10); mCompleters[current->first] = completer; } } } } ================================================ FILE: apps/opencs/model/world/idcompletionmanager.hpp ================================================ #ifndef CSM_WORLD_IDCOMPLETIONMANAGER_HPP #define CSM_WORLD_IDCOMPLETIONMANAGER_HPP #include #include #include #include "columnbase.hpp" #include "universalid.hpp" class QCompleter; namespace CSMWorld { class Data; /// \brief Creates and stores all ID completers class IdCompletionManager { static const std::map sCompleterModelTypes; std::map > mCompleters; // Don't allow copying IdCompletionManager(const IdCompletionManager &); IdCompletionManager &operator = (const IdCompletionManager &); void generateCompleters(Data &data); public: static std::vector getDisplayTypes(); IdCompletionManager(Data &data); bool hasCompleterFor(ColumnBase::Display display) const; std::shared_ptr getCompleter(ColumnBase::Display display); }; } #endif ================================================ FILE: apps/opencs/model/world/idtable.cpp ================================================ #include "idtable.hpp" #include #include #include #include #include #include #include #include "collectionbase.hpp" #include "columnbase.hpp" #include "landtexture.hpp" CSMWorld::IdTable::IdTable (CollectionBase *idCollection, unsigned int features) : IdTableBase (features), mIdCollection (idCollection) {} CSMWorld::IdTable::~IdTable() {} int CSMWorld::IdTable::rowCount (const QModelIndex & parent) const { if (parent.isValid()) return 0; return mIdCollection->getSize(); } int CSMWorld::IdTable::columnCount (const QModelIndex & parent) const { if (parent.isValid()) return 0; return mIdCollection->getColumns(); } QVariant CSMWorld::IdTable::data (const QModelIndex & index, int role) const { if (index.row() < 0 || index.column() < 0) return QVariant(); if (role==ColumnBase::Role_Display) return QVariant(mIdCollection->getColumn(index.column()).mDisplayType); if (role==ColumnBase::Role_ColumnId) return QVariant (getColumnId (index.column())); if ((role!=Qt::DisplayRole && role!=Qt::EditRole)) return QVariant(); if (role==Qt::EditRole && !mIdCollection->getColumn (index.column()).isEditable()) return QVariant(); return mIdCollection->getData (index.row(), index.column()); } QVariant CSMWorld::IdTable::headerData (int section, Qt::Orientation orientation, int role) const { if (orientation==Qt::Vertical) return QVariant(); if (orientation != Qt::Horizontal) throw std::logic_error("Unknown header orientation specified"); if (role==Qt::DisplayRole) return tr (mIdCollection->getColumn (section).getTitle().c_str()); if (role==ColumnBase::Role_Flags) return mIdCollection->getColumn (section).mFlags; if (role==ColumnBase::Role_Display) return mIdCollection->getColumn (section).mDisplayType; if (role==ColumnBase::Role_ColumnId) return getColumnId (section); return QVariant(); } bool CSMWorld::IdTable::setData (const QModelIndex &index, const QVariant &value, int role) { if (mIdCollection->getColumn (index.column()).isEditable() && role==Qt::EditRole) { mIdCollection->setData (index.row(), index.column(), value); int stateColumn = searchColumnIndex(Columns::ColumnId_Modification); if (stateColumn != -1) { if (index.column() == stateColumn) { // modifying the state column can modify other values. we need to tell // views that the whole row has changed. emit dataChanged(this->index(index.row(), 0), this->index(index.row(), columnCount(index.parent()) - 1)); } else { emit dataChanged(index, index); // Modifying a value can also change the Modified status of a record. QModelIndex stateIndex = this->index(index.row(), stateColumn); emit dataChanged(stateIndex, stateIndex); } } else emit dataChanged(index, index); return true; } return false; } Qt::ItemFlags CSMWorld::IdTable::flags (const QModelIndex & index) const { if (!index.isValid()) return Qt::ItemFlags(); Qt::ItemFlags flags = Qt::ItemIsSelectable | Qt::ItemIsEnabled; if (mIdCollection->getColumn (index.column()).isUserEditable()) flags |= Qt::ItemIsEditable; return flags; } bool CSMWorld::IdTable::removeRows (int row, int count, const QModelIndex& parent) { if (parent.isValid()) return false; beginRemoveRows (parent, row, row+count-1); mIdCollection->removeRows (row, count); endRemoveRows(); return true; } QModelIndex CSMWorld::IdTable::index (int row, int column, const QModelIndex& parent) const { if (parent.isValid()) return QModelIndex(); if (row<0 || row>=mIdCollection->getSize()) return QModelIndex(); if (column<0 || column>=mIdCollection->getColumns()) return QModelIndex(); return createIndex (row, column); } QModelIndex CSMWorld::IdTable::parent (const QModelIndex& index) const { return QModelIndex(); } void CSMWorld::IdTable::addRecord (const std::string& id, UniversalId::Type type) { int index = mIdCollection->getAppendIndex (id, type); beginInsertRows (QModelIndex(), index, index); mIdCollection->appendBlankRecord (id, type); endInsertRows(); } void CSMWorld::IdTable::addRecordWithData (const std::string& id, const std::map& data, UniversalId::Type type) { int index = mIdCollection->getAppendIndex (id, type); beginInsertRows (QModelIndex(), index, index); mIdCollection->appendBlankRecord (id, type); for (std::map::const_iterator iter (data.begin()); iter!=data.end(); ++iter) { mIdCollection->setData(index, iter->first, iter->second); } endInsertRows(); } void CSMWorld::IdTable::cloneRecord(const std::string& origin, const std::string& destination, CSMWorld::UniversalId::Type type) { int index = mIdCollection->getAppendIndex (destination); beginInsertRows (QModelIndex(), index, index); mIdCollection->cloneRecord(origin, destination, type); endInsertRows(); } bool CSMWorld::IdTable::touchRecord(const std::string& id) { bool changed = mIdCollection->touchRecord(id); int row = mIdCollection->getIndex(id); int column = mIdCollection->searchColumnIndex(Columns::ColumnId_RecordType); if (changed && column != -1) { QModelIndex modelIndex = index(row, column); emit dataChanged(modelIndex, modelIndex); } return changed; } std::string CSMWorld::IdTable::getId(int row) const { return mIdCollection->getId(row); } ///This method can return only indexes to the top level table cells QModelIndex CSMWorld::IdTable::getModelIndex (const std::string& id, int column) const { int row = mIdCollection->searchId (id); if (row != -1) return index(row, column); return QModelIndex(); } void CSMWorld::IdTable::setRecord (const std::string& id, const RecordBase& record, CSMWorld::UniversalId::Type type) { int index = mIdCollection->searchId (id); if (index==-1) { index = mIdCollection->getAppendIndex (id, type); beginInsertRows (QModelIndex(), index, index); mIdCollection->appendRecord (record, type); endInsertRows(); } else { mIdCollection->replace (index, record); emit dataChanged (CSMWorld::IdTable::index (index, 0), CSMWorld::IdTable::index (index, mIdCollection->getColumns()-1)); } } const CSMWorld::RecordBase& CSMWorld::IdTable::getRecord (const std::string& id) const { return mIdCollection->getRecord (id); } int CSMWorld::IdTable::searchColumnIndex (Columns::ColumnId id) const { return mIdCollection->searchColumnIndex (id); } int CSMWorld::IdTable::findColumnIndex (Columns::ColumnId id) const { return mIdCollection->findColumnIndex (id); } void CSMWorld::IdTable::reorderRows (int baseIndex, const std::vector& newOrder) { if (!newOrder.empty()) if (mIdCollection->reorderRows (baseIndex, newOrder)) emit dataChanged (index (baseIndex, 0), index (baseIndex+static_cast(newOrder.size())-1, mIdCollection->getColumns()-1)); } std::pair CSMWorld::IdTable::view (int row) const { std::string id; std::string hint; if (getFeatures() & Feature_ViewCell) { int cellColumn = mIdCollection->searchColumnIndex (Columns::ColumnId_Cell); int idColumn = mIdCollection->searchColumnIndex (Columns::ColumnId_Id); if (cellColumn!=-1 && idColumn!=-1) { id = mIdCollection->getData (row, cellColumn).toString().toUtf8().constData(); hint = "r:" + std::string (mIdCollection->getData (row, idColumn).toString().toUtf8().constData()); } } else if (getFeatures() & Feature_ViewId) { int column = mIdCollection->searchColumnIndex (Columns::ColumnId_Id); if (column!=-1) { id = mIdCollection->getData (row, column).toString().toUtf8().constData(); hint = "c:" + id; } } if (id.empty()) return std::make_pair (UniversalId::Type_None, ""); if (id[0]=='#') id = ESM::CellId::sDefaultWorldspace; return std::make_pair (UniversalId (UniversalId::Type_Scene, id), hint); } ///For top level data/columns bool CSMWorld::IdTable::isDeleted (const std::string& id) const { return getRecord (id).isDeleted(); } int CSMWorld::IdTable::getColumnId(int column) const { return mIdCollection->getColumn(column).getId(); } CSMWorld::CollectionBase *CSMWorld::IdTable::idCollection() const { return mIdCollection; } CSMWorld::LandTextureIdTable::LandTextureIdTable(CollectionBase* idCollection, unsigned int features) : IdTable(idCollection, features) { } CSMWorld::LandTextureIdTable::ImportResults CSMWorld::LandTextureIdTable::importTextures(const std::vector& ids) { ImportResults results; // Map existing textures to ids std::map reverseLookupMap; for (int i = 0; i < idCollection()->getSize(); ++i) { auto& record = static_cast&>(idCollection()->getRecord(i)); std::string texture = record.get().mTexture; std::transform(texture.begin(), texture.end(), texture.begin(), tolower); if (record.isModified()) reverseLookupMap.emplace(texture, idCollection()->getId(i)); } for (const std::string& id : ids) { int plugin, index; LandTexture::parseUniqueRecordId(id, plugin, index); int oldRow = idCollection()->searchId(id); // If it does not exist or it is in the current plugin, it can be skipped. if (oldRow < 0 || plugin == 0) { results.recordMapping.emplace_back(id, id); continue; } // Look for a pre-existing record auto& record = static_cast&>(idCollection()->getRecord(oldRow)); std::string texture = record.get().mTexture; std::transform(texture.begin(), texture.end(), texture.begin(), tolower); auto searchIt = reverseLookupMap.find(texture); if (searchIt != reverseLookupMap.end()) { results.recordMapping.emplace_back(id, searchIt->second); continue; } // Iterate until an unused index or found, or the index has completely wrapped around. int startIndex = index; do { std::string newId = LandTexture::createUniqueRecordId(0, index); int newRow = idCollection()->searchId(newId); if (newRow < 0) { // Id not taken, clone it cloneRecord(id, newId, UniversalId::Type_LandTexture); results.createdRecords.push_back(newId); results.recordMapping.emplace_back(id, newId); reverseLookupMap.emplace(texture, newId); break; } const size_t MaxIndex = std::numeric_limits::max() - 1; index = (index + 1) % MaxIndex; } while (index != startIndex); } return results; } ================================================ FILE: apps/opencs/model/world/idtable.hpp ================================================ #ifndef CSM_WOLRD_IDTABLE_H #define CSM_WOLRD_IDTABLE_H #include #include "idtablebase.hpp" #include "universalid.hpp" #include "columns.hpp" namespace CSMWorld { class CollectionBase; struct RecordBase; class IdTable : public IdTableBase { Q_OBJECT private: CollectionBase *mIdCollection; // not implemented IdTable (const IdTable&); IdTable& operator= (const IdTable&); public: IdTable (CollectionBase *idCollection, unsigned int features = 0); ///< The ownership of \a idCollection is not transferred. virtual ~IdTable(); int rowCount (const QModelIndex & parent = QModelIndex()) const override; int columnCount (const QModelIndex & parent = QModelIndex()) const override; QVariant data (const QModelIndex & index, int role = Qt::DisplayRole) const override; QVariant headerData (int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override; bool setData ( const QModelIndex &index, const QVariant &value, int role = Qt::EditRole) override; Qt::ItemFlags flags (const QModelIndex & index) const override; bool removeRows (int row, int count, const QModelIndex& parent = QModelIndex()) override; QModelIndex index (int row, int column, const QModelIndex& parent = QModelIndex()) const override; QModelIndex parent (const QModelIndex& index) const override; void addRecord (const std::string& id, UniversalId::Type type = UniversalId::Type_None); ///< \param type Will be ignored, unless the collection supports multiple record types void addRecordWithData (const std::string& id, const std::map& data, UniversalId::Type type = UniversalId::Type_None); ///< \param type Will be ignored, unless the collection supports multiple record types void cloneRecord(const std::string& origin, const std::string& destination, UniversalId::Type type = UniversalId::Type_None); bool touchRecord(const std::string& id); ///< Will change the record state to modified, if it is not already. std::string getId(int row) const; QModelIndex getModelIndex (const std::string& id, int column) const override; void setRecord (const std::string& id, const RecordBase& record, UniversalId::Type type = UniversalId::Type_None); ///< Add record or overwrite existing record. const RecordBase& getRecord (const std::string& id) const; int searchColumnIndex (Columns::ColumnId id) const override; ///< Return index of column with the given \a id. If no such column exists, -1 is returned. int findColumnIndex (Columns::ColumnId id) const override; ///< Return index of column with the given \a id. If no such column exists, an exception is /// thrown. void reorderRows (int baseIndex, const std::vector& newOrder); ///< Reorder the rows [baseIndex, baseIndex+newOrder.size()) according to the indices /// given in \a newOrder (baseIndex+newOrder[0] specifies the new index of row baseIndex). std::pair view (int row) const override; ///< Return the UniversalId and the hint for viewing \a row. If viewing is not /// supported by this table, return (UniversalId::Type_None, ""). /// Is \a id flagged as deleted? bool isDeleted (const std::string& id) const override; int getColumnId(int column) const override; protected: virtual CollectionBase *idCollection() const; }; /// An IdTable customized to handle the more unique needs of LandTextureId's which behave /// differently from other records. The major difference is that base records cannot be /// modified. class LandTextureIdTable : public IdTable { public: struct ImportResults { using StringPair = std::pair; /// The newly added records std::vector createdRecords; /// The 1st string is the original id, the 2nd is the mapped id std::vector recordMapping; }; LandTextureIdTable(CollectionBase* idCollection, unsigned int features=0); /// Finds and maps/recreates the specified ids. ImportResults importTextures(const std::vector& ids); }; } #endif ================================================ FILE: apps/opencs/model/world/idtablebase.cpp ================================================ #include "idtablebase.hpp" CSMWorld::IdTableBase::IdTableBase (unsigned int features) : mFeatures (features) {} unsigned int CSMWorld::IdTableBase::getFeatures() const { return mFeatures; } ================================================ FILE: apps/opencs/model/world/idtablebase.hpp ================================================ #ifndef CSM_WOLRD_IDTABLEBASE_H #define CSM_WOLRD_IDTABLEBASE_H #include #include "columns.hpp" namespace CSMWorld { class UniversalId; class IdTableBase : public QAbstractItemModel { Q_OBJECT public: enum Features { Feature_ReorderWithinTopic = 1, /// Use ID column to generate view request (ID is transformed into /// worldspace and original ID is passed as hint with c: prefix). Feature_ViewId = 2, /// Use cell column to generate view request (cell ID is transformed /// into worldspace and record ID is passed as hint with r: prefix). Feature_ViewCell = 4, Feature_View = Feature_ViewId | Feature_ViewCell, Feature_Preview = 8, /// Table can not be modified through ordinary means. Feature_Constant = 16, Feature_AllowTouch = 32 }; private: unsigned int mFeatures; public: IdTableBase (unsigned int features); virtual QModelIndex getModelIndex (const std::string& id, int column) const = 0; /// Return index of column with the given \a id. If no such column exists, -1 is /// returned. virtual int searchColumnIndex (Columns::ColumnId id) const = 0; /// Return index of column with the given \a id. If no such column exists, an /// exception is thrown. virtual int findColumnIndex (Columns::ColumnId id) const = 0; /// Return the UniversalId and the hint for viewing \a row. If viewing is not /// supported by this table, return (UniversalId::Type_None, ""). virtual std::pair view (int row) const = 0; /// Is \a id flagged as deleted? virtual bool isDeleted (const std::string& id) const = 0; virtual int getColumnId (int column) const = 0; unsigned int getFeatures() const; }; } #endif ================================================ FILE: apps/opencs/model/world/idtableproxymodel.cpp ================================================ #include "idtableproxymodel.hpp" #include #include "idtablebase.hpp" namespace { std::string getEnumValue(const std::vector> &values, int index) { if (index < 0 || index >= static_cast(values.size())) { return ""; } return values[index].second; } } void CSMWorld::IdTableProxyModel::updateColumnMap() { Q_ASSERT(mSourceModel != nullptr); mColumnMap.clear(); if (mFilter) { std::vector columns = mFilter->getReferencedColumns(); for (std::vector::const_iterator iter (columns.begin()); iter!=columns.end(); ++iter) mColumnMap.insert (std::make_pair (*iter, mSourceModel->searchColumnIndex (static_cast (*iter)))); } } bool CSMWorld::IdTableProxyModel::filterAcceptsRow (int sourceRow, const QModelIndex& sourceParent) const { Q_ASSERT(mSourceModel != nullptr); // It is not possible to use filterAcceptsColumn() and check for // sourceModel()->headerData (sourceColumn, Qt::Horizontal, CSMWorld::ColumnBase::Role_Flags) // because the sourceColumn parameter excludes the hidden columns, i.e. wrong columns can // be rejected. Workaround by disallowing tree branches (nested columns), which are not meant // to be visible, from the filter. if (sourceParent.isValid()) return false; if (!mFilter) return true; return mFilter->test (*mSourceModel, sourceRow, mColumnMap); } CSMWorld::IdTableProxyModel::IdTableProxyModel (QObject *parent) : QSortFilterProxyModel (parent), mSourceModel(nullptr) { setSortCaseSensitivity (Qt::CaseInsensitive); } QModelIndex CSMWorld::IdTableProxyModel::getModelIndex (const std::string& id, int column) const { Q_ASSERT(mSourceModel != nullptr); return mapFromSource(mSourceModel->getModelIndex (id, column)); } void CSMWorld::IdTableProxyModel::setSourceModel(QAbstractItemModel *model) { QSortFilterProxyModel::setSourceModel(model); mSourceModel = dynamic_cast(sourceModel()); connect(mSourceModel, SIGNAL(rowsInserted(const QModelIndex &, int, int)), this, SLOT(sourceRowsInserted(const QModelIndex &, int, int))); connect(mSourceModel, SIGNAL(rowsRemoved(const QModelIndex &, int, int)), this, SLOT(sourceRowsRemoved(const QModelIndex &, int, int))); connect(mSourceModel, SIGNAL(dataChanged(const QModelIndex &, const QModelIndex &)), this, SLOT(sourceDataChanged(const QModelIndex &, const QModelIndex &))); } void CSMWorld::IdTableProxyModel::setFilter (const std::shared_ptr& filter) { beginResetModel(); mFilter = filter; updateColumnMap(); endResetModel(); } bool CSMWorld::IdTableProxyModel::lessThan(const QModelIndex &left, const QModelIndex &right) const { Columns::ColumnId id = static_cast(left.data(ColumnBase::Role_ColumnId).toInt()); EnumColumnCache::const_iterator valuesIt = mEnumColumnCache.find(id); if (valuesIt == mEnumColumnCache.end()) { if (Columns::hasEnums(id)) { valuesIt = mEnumColumnCache.insert(std::make_pair(id, Columns::getEnums(id))).first; } } if (valuesIt != mEnumColumnCache.end()) { std::string first = getEnumValue(valuesIt->second, left.data().toInt()); std::string second = getEnumValue(valuesIt->second, right.data().toInt()); return first < second; } return QSortFilterProxyModel::lessThan(left, right); } QString CSMWorld::IdTableProxyModel::getRecordId(int sourceRow) const { Q_ASSERT(mSourceModel != nullptr); int idColumn = mSourceModel->findColumnIndex(Columns::ColumnId_Id); return mSourceModel->data(mSourceModel->index(sourceRow, idColumn)).toString(); } void CSMWorld::IdTableProxyModel::refreshFilter() { updateColumnMap(); invalidateFilter(); } void CSMWorld::IdTableProxyModel::sourceRowsInserted(const QModelIndex &parent, int /*start*/, int end) { refreshFilter(); if (!parent.isValid()) { emit rowAdded(getRecordId(end).toUtf8().constData()); } } void CSMWorld::IdTableProxyModel::sourceRowsRemoved(const QModelIndex &/*parent*/, int /*start*/, int /*end*/) { refreshFilter(); } void CSMWorld::IdTableProxyModel::sourceDataChanged(const QModelIndex &/*topLeft*/, const QModelIndex &/*bottomRight*/) { refreshFilter(); } ================================================ FILE: apps/opencs/model/world/idtableproxymodel.hpp ================================================ #ifndef CSM_WOLRD_IDTABLEPROXYMODEL_H #define CSM_WOLRD_IDTABLEPROXYMODEL_H #include #include #include #include "../filter/node.hpp" #include "columns.hpp" namespace CSMWorld { class IdTableProxyModel : public QSortFilterProxyModel { Q_OBJECT std::shared_ptr mFilter; std::map mColumnMap; // column ID, column index in this model (or -1) // Cache of enum values for enum columns (e.g. Modified, Record Type). // Used to speed up comparisons during the sort by such columns. typedef std::map> > EnumColumnCache; mutable EnumColumnCache mEnumColumnCache; protected: IdTableBase *mSourceModel; private: void updateColumnMap(); public: IdTableProxyModel (QObject *parent = nullptr); virtual QModelIndex getModelIndex (const std::string& id, int column) const; void setSourceModel(QAbstractItemModel *model) override; void setFilter (const std::shared_ptr& filter); void refreshFilter(); protected: bool lessThan(const QModelIndex &left, const QModelIndex &right) const override; bool filterAcceptsRow (int sourceRow, const QModelIndex& sourceParent) const override; QString getRecordId(int sourceRow) const; protected slots: virtual void sourceRowsInserted(const QModelIndex &parent, int start, int end); virtual void sourceRowsRemoved(const QModelIndex &parent, int start, int end); virtual void sourceDataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight); signals: void rowAdded(const std::string &id); }; } #endif ================================================ FILE: apps/opencs/model/world/idtree.cpp ================================================ #include "idtree.hpp" #include "nestedtablewrapper.hpp" #include "collectionbase.hpp" #include "nestedcollection.hpp" #include "columnbase.hpp" // NOTE: parent class still needs idCollection CSMWorld::IdTree::IdTree (NestedCollection *nestedCollection, CollectionBase *idCollection, unsigned int features) : IdTable (idCollection, features), mNestedCollection (nestedCollection) {} CSMWorld::IdTree::~IdTree() {} int CSMWorld::IdTree::rowCount (const QModelIndex & parent) const { if (hasChildren(parent)) return mNestedCollection->getNestedRowsCount(parent.row(), parent.column()); return IdTable::rowCount(parent); } int CSMWorld::IdTree::columnCount (const QModelIndex & parent) const { if (hasChildren(parent)) return mNestedCollection->getNestedColumnsCount(parent.row(), parent.column()); return IdTable::columnCount(parent); } QVariant CSMWorld::IdTree::data (const QModelIndex & index, int role) const { if (!index.isValid()) return QVariant(); if (index.internalId() != 0) { std::pair parentAddress(unfoldIndexAddress(index.internalId())); const NestableColumn *parentColumn = mNestedCollection->getNestableColumn(parentAddress.second); if (role == ColumnBase::Role_Display) return parentColumn->nestedColumn(index.column()).mDisplayType; if (role == ColumnBase::Role_ColumnId) return parentColumn->nestedColumn(index.column()).mColumnId; if (role == Qt::EditRole && !parentColumn->nestedColumn(index.column()).isEditable()) return QVariant(); if (role != Qt::DisplayRole && role != Qt::EditRole) return QVariant(); return mNestedCollection->getNestedData(parentAddress.first, parentAddress.second, index.row(), index.column()); } else { return IdTable::data(index, role); } } QVariant CSMWorld::IdTree::nestedHeaderData(int section, int subSection, Qt::Orientation orientation, int role) const { if (section < 0 || section >= idCollection()->getColumns()) return QVariant(); const NestableColumn *parentColumn = mNestedCollection->getNestableColumn(section); if (orientation==Qt::Vertical) return QVariant(); if (role==Qt::DisplayRole) return tr(parentColumn->nestedColumn(subSection).getTitle().c_str()); if (role==ColumnBase::Role_Flags) return parentColumn->nestedColumn(subSection).mFlags; if (role==ColumnBase::Role_Display) return parentColumn->nestedColumn(subSection).mDisplayType; if (role==ColumnBase::Role_ColumnId) return parentColumn->nestedColumn(subSection).mColumnId; return QVariant(); } bool CSMWorld::IdTree::setData (const QModelIndex &index, const QVariant &value, int role) { if (index.internalId() != 0) { if (idCollection()->getColumn(parent(index).column()).isEditable() && role==Qt::EditRole) { const std::pair& parentAddress(unfoldIndexAddress(index.internalId())); mNestedCollection->setNestedData(parentAddress.first, parentAddress.second, value, index.row(), index.column()); emit dataChanged(index, index); // Modifying a value can also change the Modified status of a record. int stateColumn = searchColumnIndex(Columns::ColumnId_Modification); if (stateColumn != -1) { QModelIndex stateIndex = this->index(index.parent().row(), stateColumn); emit dataChanged(stateIndex, stateIndex); } return true; } else return false; } return IdTable::setData(index, value, role); } Qt::ItemFlags CSMWorld::IdTree::flags (const QModelIndex & index) const { if (!index.isValid()) return Qt::ItemFlags(); if (index.internalId() != 0) { std::pair parentAddress(unfoldIndexAddress(index.internalId())); Qt::ItemFlags flags = Qt::ItemIsSelectable | Qt::ItemIsEnabled; if (mNestedCollection->getNestableColumn(parentAddress.second)->nestedColumn(index.column()).isEditable()) flags |= Qt::ItemIsEditable; return flags; } else return IdTable::flags(index); } bool CSMWorld::IdTree::removeRows (int row, int count, const QModelIndex& parent) { if (parent.isValid()) { beginRemoveRows (parent, row, row+count-1); for (int i = 0; i < count; ++i) { mNestedCollection->removeNestedRows(parent.row(), parent.column(), row+i); } endRemoveRows(); emit dataChanged (CSMWorld::IdTree::index (parent.row(), 0), CSMWorld::IdTree::index (parent.row(), idCollection()->getColumns()-1)); return true; } else return IdTable::removeRows(row, count, parent); } void CSMWorld::IdTree::addNestedRow(const QModelIndex& parent, int position) { if (!hasChildren(parent)) throw std::logic_error("Tried to set nested table, but index has no children"); int row = parent.row(); beginInsertRows(parent, position, position); mNestedCollection->addNestedRow(row, parent.column(), position); endInsertRows(); emit dataChanged (CSMWorld::IdTree::index (row, 0), CSMWorld::IdTree::index (row, idCollection()->getColumns()-1)); } QModelIndex CSMWorld::IdTree::index (int row, int column, const QModelIndex& parent) const { unsigned int encodedId = 0; if (parent.isValid()) { encodedId = this->foldIndexAddress(parent); } if (row < 0 || row >= rowCount(parent)) return QModelIndex(); if (column < 0 || column >= columnCount(parent)) return QModelIndex(); return createIndex(row, column, encodedId); // store internal id } QModelIndex CSMWorld::IdTree::getNestedModelIndex (const std::string& id, int column) const { return CSMWorld::IdTable::index(idCollection()->getIndex (id), column); } QModelIndex CSMWorld::IdTree::parent (const QModelIndex& index) const { if (index.internalId() == 0) // 0 is used for indexs with invalid parent (top level data) return QModelIndex(); unsigned int id = index.internalId(); const std::pair& address(unfoldIndexAddress(id)); if (address.first >= this->rowCount() || address.second >= this->columnCount()) throw std::logic_error("Parent index is not present in the model"); return createIndex(address.first, address.second); } unsigned int CSMWorld::IdTree::foldIndexAddress (const QModelIndex& index) const { unsigned int out = index.row() * this->columnCount(); out += index.column(); return ++out; } std::pair< int, int > CSMWorld::IdTree::unfoldIndexAddress (unsigned int id) const { if (id == 0) throw std::runtime_error("Attempt to unfold index id of the top level data cell"); --id; int row = id / this->columnCount(); int column = id - row * this->columnCount(); return std::make_pair (row, column); } // FIXME: Not sure why this check is also needed? // // index.data().isValid() requires RefIdAdapter::getData() to return a valid QVariant for // nested columns (refidadapterimp.hpp) // // Also see comments in refidadapter.hpp and refidadapterimp.hpp. bool CSMWorld::IdTree::hasChildren(const QModelIndex& index) const { return (index.isValid() && index.internalId() == 0 && mNestedCollection->getNestableColumn(index.column())->hasChildren() && index.data().isValid()); } void CSMWorld::IdTree::setNestedTable(const QModelIndex& index, const CSMWorld::NestedTableWrapperBase& nestedTable) { if (!hasChildren(index)) throw std::logic_error("Tried to set nested table, but index has no children"); bool removeRowsMode = false; if (nestedTable.size() != this->nestedTable(index)->size()) { emit resetStart(this->index(index.row(), 0).data().toString()); removeRowsMode = true; } mNestedCollection->setNestedTable(index.row(), index.column(), nestedTable); emit dataChanged (CSMWorld::IdTree::index (index.row(), 0), CSMWorld::IdTree::index (index.row(), idCollection()->getColumns()-1)); if (removeRowsMode) { emit resetEnd(this->index(index.row(), 0).data().toString()); } } CSMWorld::NestedTableWrapperBase* CSMWorld::IdTree::nestedTable(const QModelIndex& index) const { if (!hasChildren(index)) throw std::logic_error("Tried to retrieve nested table, but index has no children"); return mNestedCollection->nestedTable(index.row(), index.column()); } int CSMWorld::IdTree::searchNestedColumnIndex(int parentColumn, Columns::ColumnId id) { return mNestedCollection->searchNestedColumnIndex(parentColumn, id); } int CSMWorld::IdTree::findNestedColumnIndex(int parentColumn, Columns::ColumnId id) { return mNestedCollection->findNestedColumnIndex(parentColumn, id); } ================================================ FILE: apps/opencs/model/world/idtree.hpp ================================================ #ifndef CSM_WOLRD_IDTREE_H #define CSM_WOLRD_IDTREE_H #include "idtable.hpp" #include "universalid.hpp" #include "columns.hpp" /*! \brief * Class for holding the model. Uses typical qt table abstraction/interface for granting access * to the individiual fields of the records, Some records are holding nested data (for instance * inventory list of the npc). In cases like this, table model offers interface to access * nested data in the qt way - that is specify parent. Since some of those nested data require * multiple columns to represent information, single int (default way to index model in the * qmodelindex) is not sufficiant. Therefore tablemodelindex class can hold two ints for the * sake of indexing two dimensions of the table. This model does not support multiple levels of * the nested data. Vast majority of methods makes sense only for the top level data. */ namespace CSMWorld { class NestedCollection; struct RecordBase; struct NestedTableWrapperBase; class IdTree : public IdTable { Q_OBJECT private: NestedCollection *mNestedCollection; // not implemented IdTree (const IdTree&); IdTree& operator= (const IdTree&); unsigned int foldIndexAddress(const QModelIndex& index) const; std::pair unfoldIndexAddress(unsigned int id) const; public: IdTree (NestedCollection *nestedCollection, CollectionBase *idCollection, unsigned int features = 0); ///< The ownerships of \a nestedCollecton and \a idCollection are not transferred. virtual ~IdTree(); int rowCount (const QModelIndex & parent = QModelIndex()) const override; int columnCount (const QModelIndex & parent = QModelIndex()) const override; QVariant data (const QModelIndex & index, int role = Qt::DisplayRole) const override; bool setData ( const QModelIndex &index, const QVariant &value, int role = Qt::EditRole) override; Qt::ItemFlags flags (const QModelIndex & index) const override; bool removeRows (int row, int count, const QModelIndex& parent = QModelIndex()) override; QModelIndex index (int row, int column, const QModelIndex& parent = QModelIndex()) const override; QModelIndex parent (const QModelIndex& index) const override; QModelIndex getNestedModelIndex (const std::string& id, int column) const; QVariant nestedHeaderData(int section, int subSection, Qt::Orientation orientation, int role = Qt::DisplayRole) const; NestedTableWrapperBase* nestedTable(const QModelIndex &index) const; void setNestedTable(const QModelIndex &index, const NestedTableWrapperBase& nestedTable); void addNestedRow (const QModelIndex& parent, int position); bool hasChildren (const QModelIndex& index) const override; virtual int searchNestedColumnIndex(int parentColumn, Columns::ColumnId id); ///< \return the column index or -1 if the requested column wasn't found. virtual int findNestedColumnIndex(int parentColumn, Columns::ColumnId id); ///< \return the column index or throws an exception if the requested column wasn't found. signals: void resetStart(const QString& id); void resetEnd(const QString& id); }; } #endif ================================================ FILE: apps/opencs/model/world/info.hpp ================================================ #ifndef CSM_WOLRD_INFO_H #define CSM_WOLRD_INFO_H #include namespace CSMWorld { struct Info : public ESM::DialInfo { std::string mTopicId; }; } #endif ================================================ FILE: apps/opencs/model/world/infocollection.cpp ================================================ #include "infocollection.hpp" #include #include #include #include #include void CSMWorld::InfoCollection::load (const Info& record, bool base) { int index = searchId (record.mId); if (index==-1) { // new record Record record2; record2.mState = base ? RecordBase::State_BaseOnly : RecordBase::State_ModifiedOnly; (base ? record2.mBase : record2.mModified) = record; std::string topic = Misc::StringUtils::lowerCase (record2.get().mTopicId); if (!record2.get().mPrev.empty()) { index = getInfoIndex (record2.get().mPrev, topic); if (index!=-1) ++index; } if (index==-1 && !record2.get().mNext.empty()) { index = getInfoIndex (record2.get().mNext, topic); } if (index==-1) { Range range = getTopicRange (topic); index = std::distance (getRecords().begin(), range.second); } insertRecord (record2, index); } else { // old record Record record2 = getRecord (index); if (base) record2.mBase = record; else record2.setModified (record); setRecord (index, record2); } } int CSMWorld::InfoCollection::getInfoIndex (const std::string& id, const std::string& topic) const { std::string fullId = Misc::StringUtils::lowerCase (topic) + "#" + id; std::pair range = getTopicRange (topic); for (; range.first!=range.second; ++range.first) if (Misc::StringUtils::ciEqual(range.first->get().mId, fullId)) return std::distance (getRecords().begin(), range.first); return -1; } int CSMWorld::InfoCollection::getAppendIndex (const std::string& id, UniversalId::Type type) const { std::string::size_type separator = id.find_last_of ('#'); if (separator==std::string::npos) throw std::runtime_error ("invalid info ID: " + id); std::pair range = getTopicRange (id.substr (0, separator)); if (range.first==range.second) return Collection >::getAppendIndex (id, type); return std::distance (getRecords().begin(), range.second); } bool CSMWorld::InfoCollection::reorderRows (int baseIndex, const std::vector& newOrder) { // check if the range is valid int lastIndex = baseIndex + newOrder.size() -1; if (lastIndex>=getSize()) return false; // Check that topics match if (!Misc::StringUtils::ciEqual(getRecord(baseIndex).get().mTopicId, getRecord(lastIndex).get().mTopicId)) return false; // reorder return reorderRowsImp (baseIndex, newOrder); } void CSMWorld::InfoCollection::load (ESM::ESMReader& reader, bool base, const ESM::Dialogue& dialogue) { Info info; bool isDeleted = false; info.load (reader, isDeleted); std::string id = Misc::StringUtils::lowerCase (dialogue.mId) + "#" + info.mId; if (isDeleted) { int index = searchId (id); if (index==-1) { // deleting a record that does not exist // ignore it for now /// \todo report the problem to the user } else if (base) { removeRows (index, 1); } else { Record record = getRecord (index); record.mState = RecordBase::State_Deleted; setRecord (index, record); } } else { info.mTopicId = dialogue.mId; info.mId = id; load (info, base); } } CSMWorld::InfoCollection::Range CSMWorld::InfoCollection::getTopicRange (const std::string& topic) const { std::string topic2 = Misc::StringUtils::lowerCase (topic); std::map::const_iterator iter = getIdMap().lower_bound (topic2); // Skip invalid records: The beginning of a topic string could be identical to another topic // string. for (; iter!=getIdMap().end(); ++iter) { std::string testTopicId = Misc::StringUtils::lowerCase (getRecord (iter->second).get().mTopicId); if (testTopicId==topic2) break; std::size_t size = topic2.size(); if (testTopicId.size()second; while (begin != getRecords().begin()) { if (!Misc::StringUtils::ciEqual(begin->get().mTopicId, topic2)) { // we've gone one too far, go back ++begin; break; } --begin; } // Find end RecordConstIterator end = begin; for (; end!=getRecords().end(); ++end) if (!Misc::StringUtils::ciEqual(end->get().mTopicId, topic2)) break; return Range (begin, end); } void CSMWorld::InfoCollection::removeDialogueInfos(const std::string& dialogueId) { std::string id = Misc::StringUtils::lowerCase(dialogueId); std::vector erasedRecords; std::map::const_iterator current = getIdMap().lower_bound(id); std::map::const_iterator end = getIdMap().end(); for (; current != end; ++current) { Record record = getRecord(current->second); if (Misc::StringUtils::ciEqual(dialogueId, record.get().mTopicId)) { if (record.mState == RecordBase::State_ModifiedOnly) { erasedRecords.push_back(current->second); } else { record.mState = RecordBase::State_Deleted; setRecord(current->second, record); } } else { break; } } while (!erasedRecords.empty()) { removeRows(erasedRecords.back(), 1); erasedRecords.pop_back(); } } ================================================ FILE: apps/opencs/model/world/infocollection.hpp ================================================ #ifndef CSM_WOLRD_INFOCOLLECTION_H #define CSM_WOLRD_INFOCOLLECTION_H #include "collection.hpp" #include "info.hpp" namespace ESM { struct Dialogue; } namespace CSMWorld { class InfoCollection : public Collection > { public: typedef std::vector >::const_iterator RecordConstIterator; typedef std::pair Range; private: void load (const Info& record, bool base); int getInfoIndex (const std::string& id, const std::string& topic) const; ///< Return index for record \a id or -1 (if not present; deleted records are considered) /// /// \param id info ID without topic prefix public: int getAppendIndex (const std::string& id, UniversalId::Type type = UniversalId::Type_None) const override; ///< \param type Will be ignored, unless the collection supports multiple record types bool reorderRows (int baseIndex, const std::vector& newOrder) override; ///< Reorder the rows [baseIndex, baseIndex+newOrder.size()) according to the indices /// given in \a newOrder (baseIndex+newOrder[0] specifies the new index of row baseIndex). /// /// \return Success? void load (ESM::ESMReader& reader, bool base, const ESM::Dialogue& dialogue); Range getTopicRange (const std::string& topic) const; ///< Return iterators that point to the beginning and past the end of the range for /// the given topic. void removeDialogueInfos(const std::string& dialogueId); }; } #endif ================================================ FILE: apps/opencs/model/world/infoselectwrapper.cpp ================================================ #include "infoselectwrapper.hpp" #include #include #include const size_t CSMWorld::ConstInfoSelectWrapper::RuleMinSize = 5; const size_t CSMWorld::ConstInfoSelectWrapper::FunctionPrefixOffset = 1; const size_t CSMWorld::ConstInfoSelectWrapper::FunctionIndexOffset = 2; const size_t CSMWorld::ConstInfoSelectWrapper::RelationIndexOffset = 4; const size_t CSMWorld::ConstInfoSelectWrapper::VarNameOffset = 5; const char* CSMWorld::ConstInfoSelectWrapper::FunctionEnumStrings[] = { "Rank Low", "Rank High", "Rank Requirement", "Reputation", "Health Percent", "PC Reputation", "PC Level", "PC Health Percent", "PC Magicka", "PC Fatigue", "PC Strength", "PC Block", "PC Armorer", "PC Medium Armor", "PC Heavy Armor", "PC Blunt Weapon", "PC Long Blade", "PC Axe", "PC Spear", "PC Athletics", "PC Enchant", "PC Detruction", "PC Alteration", "PC Illusion", "PC Conjuration", "PC Mysticism", "PC Restoration", "PC Alchemy", "PC Unarmored", "PC Security", "PC Sneak", "PC Acrobatics", "PC Light Armor", "PC Short Blade", "PC Marksman", "PC Merchantile", "PC Speechcraft", "PC Hand to Hand", "PC Sex", "PC Expelled", "PC Common Disease", "PC Blight Disease", "PC Clothing Modifier", "PC Crime Level", "Same Sex", "Same Race", "Same Faction", "Faction Rank Difference", "Detected", "Alarmed", "Choice", "PC Intelligence", "PC Willpower", "PC Agility", "PC Speed", "PC Endurance", "PC Personality", "PC Luck", "PC Corpus", "Weather", "PC Vampire", "Level", "Attacked", "Talked to PC", "PC Health", "Creature Target", "Friend Hit", "Fight", "Hello", "Alarm", "Flee", "Should Attack", "Werewolf", "PC Werewolf Kills", "Global", "Local", "Journal", "Item", "Dead", "Not Id", "Not Faction", "Not Class", "Not Race", "Not Cell", "Not Local", 0 }; const char* CSMWorld::ConstInfoSelectWrapper::RelationEnumStrings[] = { "=", "!=", ">", ">=", "<", "<=", 0 }; const char* CSMWorld::ConstInfoSelectWrapper::ComparisonEnumStrings[] = { "Boolean", "Integer", "Numeric", 0 }; // static functions std::string CSMWorld::ConstInfoSelectWrapper::convertToString(FunctionName name) { if (name < Function_None) return FunctionEnumStrings[name]; else return "(Invalid Data: Function)"; } std::string CSMWorld::ConstInfoSelectWrapper::convertToString(RelationType type) { if (type < Relation_None) return RelationEnumStrings[type]; else return "(Invalid Data: Relation)"; } std::string CSMWorld::ConstInfoSelectWrapper::convertToString(ComparisonType type) { if (type < Comparison_None) return ComparisonEnumStrings[type]; else return "(Invalid Data: Comparison)"; } // ConstInfoSelectWrapper CSMWorld::ConstInfoSelectWrapper::ConstInfoSelectWrapper(const ESM::DialInfo::SelectStruct& select) : mConstSelect(select) { readRule(); } CSMWorld::ConstInfoSelectWrapper::FunctionName CSMWorld::ConstInfoSelectWrapper::getFunctionName() const { return mFunctionName; } CSMWorld::ConstInfoSelectWrapper::RelationType CSMWorld::ConstInfoSelectWrapper::getRelationType() const { return mRelationType; } CSMWorld::ConstInfoSelectWrapper::ComparisonType CSMWorld::ConstInfoSelectWrapper::getComparisonType() const { return mComparisonType; } bool CSMWorld::ConstInfoSelectWrapper::hasVariable() const { return mHasVariable; } const std::string& CSMWorld::ConstInfoSelectWrapper::getVariableName() const { return mVariableName; } bool CSMWorld::ConstInfoSelectWrapper::conditionIsAlwaysTrue() const { if (!variantTypeIsValid()) return false; if (mComparisonType == Comparison_Boolean || mComparisonType == Comparison_Integer) { if (mConstSelect.mValue.getType() == ESM::VT_Float) return conditionIsAlwaysTrue(getConditionFloatRange(), getValidIntRange()); else return conditionIsAlwaysTrue(getConditionIntRange(), getValidIntRange()); } else if (mComparisonType == Comparison_Numeric) { if (mConstSelect.mValue.getType() == ESM::VT_Float) return conditionIsAlwaysTrue(getConditionFloatRange(), getValidFloatRange()); else return conditionIsAlwaysTrue(getConditionIntRange(), getValidFloatRange()); } return false; } bool CSMWorld::ConstInfoSelectWrapper::conditionIsNeverTrue() const { if (!variantTypeIsValid()) return false; if (mComparisonType == Comparison_Boolean || mComparisonType == Comparison_Integer) { if (mConstSelect.mValue.getType() == ESM::VT_Float) return conditionIsNeverTrue(getConditionFloatRange(), getValidIntRange()); else return conditionIsNeverTrue(getConditionIntRange(), getValidIntRange()); } else if (mComparisonType == Comparison_Numeric) { if (mConstSelect.mValue.getType() == ESM::VT_Float) return conditionIsNeverTrue(getConditionFloatRange(), getValidFloatRange()); else return conditionIsNeverTrue(getConditionIntRange(), getValidFloatRange()); } return false; } bool CSMWorld::ConstInfoSelectWrapper::variantTypeIsValid() const { return (mConstSelect.mValue.getType() == ESM::VT_Int || mConstSelect.mValue.getType() == ESM::VT_Float); } const ESM::Variant& CSMWorld::ConstInfoSelectWrapper::getVariant() const { return mConstSelect.mValue; } std::string CSMWorld::ConstInfoSelectWrapper::toString() const { std::ostringstream stream; stream << convertToString(mFunctionName) << " "; if (mHasVariable) stream << mVariableName << " "; stream << convertToString(mRelationType) << " "; switch (mConstSelect.mValue.getType()) { case ESM::VT_Int: stream << mConstSelect.mValue.getInteger(); break; case ESM::VT_Float: stream << mConstSelect.mValue.getFloat(); break; default: stream << "(Invalid value type)"; break; } return stream.str(); } void CSMWorld::ConstInfoSelectWrapper::readRule() { if (mConstSelect.mSelectRule.size() < RuleMinSize) throw std::runtime_error("InfoSelectWrapper: rule is to small"); readFunctionName(); readRelationType(); readVariableName(); updateHasVariable(); updateComparisonType(); } void CSMWorld::ConstInfoSelectWrapper::readFunctionName() { char functionPrefix = mConstSelect.mSelectRule[FunctionPrefixOffset]; std::string functionIndex = mConstSelect.mSelectRule.substr(FunctionIndexOffset, 2); int convertedIndex = -1; // Read in function index, form ## from 00 .. 73, skip leading zero if (functionIndex[0] == '0') functionIndex = functionIndex[1]; std::stringstream stream; stream << functionIndex; stream >> convertedIndex; switch (functionPrefix) { case '1': if (convertedIndex >= 0 && convertedIndex <= 73) mFunctionName = static_cast(convertedIndex); else mFunctionName = Function_None; break; case '2': mFunctionName = Function_Global; break; case '3': mFunctionName = Function_Local; break; case '4': mFunctionName = Function_Journal; break; case '5': mFunctionName = Function_Item; break; case '6': mFunctionName = Function_Dead; break; case '7': mFunctionName = Function_NotId; break; case '8': mFunctionName = Function_NotFaction; break; case '9': mFunctionName = Function_NotClass; break; case 'A': mFunctionName = Function_NotRace; break; case 'B': mFunctionName = Function_NotCell; break; case 'C': mFunctionName = Function_NotLocal; break; default: mFunctionName = Function_None; break; } } void CSMWorld::ConstInfoSelectWrapper::readRelationType() { char relationIndex = mConstSelect.mSelectRule[RelationIndexOffset]; switch (relationIndex) { case '0': mRelationType = Relation_Equal; break; case '1': mRelationType = Relation_NotEqual; break; case '2': mRelationType = Relation_Greater; break; case '3': mRelationType = Relation_GreaterOrEqual; break; case '4': mRelationType = Relation_Less; break; case '5': mRelationType = Relation_LessOrEqual; break; default: mRelationType = Relation_None; } } void CSMWorld::ConstInfoSelectWrapper::readVariableName() { if (mConstSelect.mSelectRule.size() >= VarNameOffset) mVariableName = mConstSelect.mSelectRule.substr(VarNameOffset); else mVariableName.clear(); } void CSMWorld::ConstInfoSelectWrapper::updateHasVariable() { switch (mFunctionName) { case Function_Global: case Function_Local: case Function_Journal: case Function_Item: case Function_Dead: case Function_NotId: case Function_NotFaction: case Function_NotClass: case Function_NotRace: case Function_NotCell: case Function_NotLocal: mHasVariable = true; break; default: mHasVariable = false; break; } } void CSMWorld::ConstInfoSelectWrapper::updateComparisonType() { switch (mFunctionName) { // Boolean case Function_NotId: case Function_NotFaction: case Function_NotClass: case Function_NotRace: case Function_NotCell: case Function_PcExpelled: case Function_PcCommonDisease: case Function_PcBlightDisease: case Function_SameSex: case Function_SameRace: case Function_SameFaction: case Function_Detected: case Function_Alarmed: case Function_PcCorpus: case Function_PcVampire: case Function_Attacked: case Function_TalkedToPc: case Function_ShouldAttack: case Function_Werewolf: mComparisonType = Comparison_Boolean; break; // Integer case Function_Journal: case Function_Item: case Function_Dead: case Function_RankLow: case Function_RankHigh: case Function_RankRequirement: case Function_Reputation: case Function_PcReputation: case Function_PcLevel: case Function_PcStrength: case Function_PcBlock: case Function_PcArmorer: case Function_PcMediumArmor: case Function_PcHeavyArmor: case Function_PcBluntWeapon: case Function_PcLongBlade: case Function_PcAxe: case Function_PcSpear: case Function_PcAthletics: case Function_PcEnchant: case Function_PcDestruction: case Function_PcAlteration: case Function_PcIllusion: case Function_PcConjuration: case Function_PcMysticism: case Function_PcRestoration: case Function_PcAlchemy: case Function_PcUnarmored: case Function_PcSecurity: case Function_PcSneak: case Function_PcAcrobatics: case Function_PcLightArmor: case Function_PcShortBlade: case Function_PcMarksman: case Function_PcMerchantile: case Function_PcSpeechcraft: case Function_PcHandToHand: case Function_PcGender: case Function_PcClothingModifier: case Function_PcCrimeLevel: case Function_FactionRankDifference: case Function_Choice: case Function_PcIntelligence: case Function_PcWillpower: case Function_PcAgility: case Function_PcSpeed: case Function_PcEndurance: case Function_PcPersonality: case Function_PcLuck: case Function_Weather: case Function_Level: case Function_CreatureTarget: case Function_FriendHit: case Function_Fight: case Function_Hello: case Function_Alarm: case Function_Flee: case Function_PcWerewolfKills: mComparisonType = Comparison_Integer; break; // Numeric case Function_Global: case Function_Local: case Function_NotLocal: case Function_Health_Percent: case Function_PcHealthPercent: case Function_PcMagicka: case Function_PcFatigue: case Function_PcHealth: mComparisonType = Comparison_Numeric; break; default: mComparisonType = Comparison_None; break; } } std::pair CSMWorld::ConstInfoSelectWrapper::getConditionIntRange() const { const int IntMax = std::numeric_limits::max(); const int IntMin = std::numeric_limits::min(); const std::pair InvalidRange(IntMax, IntMin); int value = mConstSelect.mValue.getInteger(); switch (mRelationType) { case Relation_Equal: case Relation_NotEqual: return std::pair(value, value); case Relation_Greater: if (value == IntMax) { return InvalidRange; } else { return std::pair(value + 1, IntMax); } break; case Relation_GreaterOrEqual: return std::pair(value, IntMax); case Relation_Less: if (value == IntMin) { return InvalidRange; } else { return std::pair(IntMin, value - 1); } case Relation_LessOrEqual: return std::pair(IntMin, value); default: throw std::logic_error("InfoSelectWrapper: relation does not have a range"); } } std::pair CSMWorld::ConstInfoSelectWrapper::getConditionFloatRange() const { const float FloatMax = std::numeric_limits::infinity(); const float FloatMin = -std::numeric_limits::infinity(); const float Epsilon = std::numeric_limits::epsilon(); float value = mConstSelect.mValue.getFloat(); switch (mRelationType) { case Relation_Equal: case Relation_NotEqual: return std::pair(value, value); case Relation_Greater: return std::pair(value + Epsilon, FloatMax); case Relation_GreaterOrEqual: return std::pair(value, FloatMax); case Relation_Less: return std::pair(FloatMin, value - Epsilon); case Relation_LessOrEqual: return std::pair(FloatMin, value); default: throw std::logic_error("InfoSelectWrapper: given relation does not have a range"); } } std::pair CSMWorld::ConstInfoSelectWrapper::getValidIntRange() const { const int IntMax = std::numeric_limits::max(); const int IntMin = std::numeric_limits::min(); switch (mFunctionName) { // Boolean case Function_NotId: case Function_NotFaction: case Function_NotClass: case Function_NotRace: case Function_NotCell: case Function_PcExpelled: case Function_PcCommonDisease: case Function_PcBlightDisease: case Function_SameSex: case Function_SameRace: case Function_SameFaction: case Function_Detected: case Function_Alarmed: case Function_PcCorpus: case Function_PcVampire: case Function_Attacked: case Function_TalkedToPc: case Function_ShouldAttack: case Function_Werewolf: return std::pair(0, 1); // Integer case Function_RankLow: case Function_RankHigh: case Function_Reputation: case Function_PcReputation: case Function_Journal: return std::pair(IntMin, IntMax); case Function_Item: case Function_Dead: case Function_PcLevel: case Function_PcStrength: case Function_PcBlock: case Function_PcArmorer: case Function_PcMediumArmor: case Function_PcHeavyArmor: case Function_PcBluntWeapon: case Function_PcLongBlade: case Function_PcAxe: case Function_PcSpear: case Function_PcAthletics: case Function_PcEnchant: case Function_PcDestruction: case Function_PcAlteration: case Function_PcIllusion: case Function_PcConjuration: case Function_PcMysticism: case Function_PcRestoration: case Function_PcAlchemy: case Function_PcUnarmored: case Function_PcSecurity: case Function_PcSneak: case Function_PcAcrobatics: case Function_PcLightArmor: case Function_PcShortBlade: case Function_PcMarksman: case Function_PcMerchantile: case Function_PcSpeechcraft: case Function_PcHandToHand: case Function_PcClothingModifier: case Function_PcCrimeLevel: case Function_Choice: case Function_PcIntelligence: case Function_PcWillpower: case Function_PcAgility: case Function_PcSpeed: case Function_PcEndurance: case Function_PcPersonality: case Function_PcLuck: case Function_Level: case Function_PcWerewolfKills: return std::pair(0, IntMax); case Function_Fight: case Function_Hello: case Function_Alarm: case Function_Flee: return std::pair(0, 100); case Function_Weather: return std::pair(0, 9); case Function_FriendHit: return std::pair(0, 4); case Function_RankRequirement: return std::pair(0, 3); case Function_CreatureTarget: return std::pair(0, 2); case Function_PcGender: return std::pair(0, 1); case Function_FactionRankDifference: return std::pair(-9, 9); // Numeric case Function_Global: case Function_Local: case Function_NotLocal: return std::pair(IntMin, IntMax); case Function_PcMagicka: case Function_PcFatigue: case Function_PcHealth: return std::pair(0, IntMax); case Function_Health_Percent: case Function_PcHealthPercent: return std::pair(0, 100); default: throw std::runtime_error("InfoSelectWrapper: function does not exist"); } } std::pair CSMWorld::ConstInfoSelectWrapper::getValidFloatRange() const { const float FloatMax = std::numeric_limits::infinity(); const float FloatMin = -std::numeric_limits::infinity(); switch (mFunctionName) { // Numeric case Function_Global: case Function_Local: case Function_NotLocal: return std::pair(FloatMin, FloatMax); case Function_PcMagicka: case Function_PcFatigue: case Function_PcHealth: return std::pair(0, FloatMax); case Function_Health_Percent: case Function_PcHealthPercent: return std::pair(0, 100); default: throw std::runtime_error("InfoSelectWrapper: function does not exist or is not numeric"); } } template bool CSMWorld::ConstInfoSelectWrapper::rangeContains(T1 value, std::pair range) const { return (value >= range.first && value <= range.second); } template bool CSMWorld::ConstInfoSelectWrapper::rangeFullyContains(std::pair containingRange, std::pair testRange) const { return (containingRange.first <= testRange.first) && (testRange.second <= containingRange.second); } template bool CSMWorld::ConstInfoSelectWrapper::rangesOverlap(std::pair range1, std::pair range2) const { // One of the bounds of either range should fall within the other range return (range1.first <= range2.first && range2.first <= range1.second) || (range1.first <= range2.second && range2.second <= range1.second) || (range2.first <= range1.first && range1.first <= range2.second) || (range2.first <= range1.second && range1.second <= range2.second); } template bool CSMWorld::ConstInfoSelectWrapper::rangesMatch(std::pair range1, std::pair range2) const { return (range1.first == range2.first && range1.second == range2.second); } template bool CSMWorld::ConstInfoSelectWrapper::conditionIsAlwaysTrue(std::pair conditionRange, std::pair validRange) const { switch (mRelationType) { case Relation_Equal: return false; case Relation_NotEqual: // If value is not within range, it will always be true return !rangeContains(conditionRange.first, validRange); case Relation_Greater: case Relation_GreaterOrEqual: case Relation_Less: case Relation_LessOrEqual: // If the valid range is completely within the condition range, it will always be true return rangeFullyContains(conditionRange, validRange); default: throw std::logic_error("InfoCondition: operator can not be used to compare"); } return false; } template bool CSMWorld::ConstInfoSelectWrapper::conditionIsNeverTrue(std::pair conditionRange, std::pair validRange) const { switch (mRelationType) { case Relation_Equal: return !rangeContains(conditionRange.first, validRange); case Relation_NotEqual: return false; case Relation_Greater: case Relation_GreaterOrEqual: case Relation_Less: case Relation_LessOrEqual: // If ranges do not overlap, it will never be true return !rangesOverlap(conditionRange, validRange); default: throw std::logic_error("InfoCondition: operator can not be used to compare"); } return false; } // InfoSelectWrapper CSMWorld::InfoSelectWrapper::InfoSelectWrapper(ESM::DialInfo::SelectStruct& select) : CSMWorld::ConstInfoSelectWrapper(select), mSelect(select) { } void CSMWorld::InfoSelectWrapper::setFunctionName(FunctionName name) { mFunctionName = name; updateHasVariable(); updateComparisonType(); } void CSMWorld::InfoSelectWrapper::setRelationType(RelationType type) { mRelationType = type; } void CSMWorld::InfoSelectWrapper::setVariableName(const std::string& name) { mVariableName = name; } void CSMWorld::InfoSelectWrapper::setDefaults() { if (!variantTypeIsValid()) mSelect.mValue.setType(ESM::VT_Int); switch (mComparisonType) { case Comparison_Boolean: setRelationType(Relation_Equal); mSelect.mValue.setInteger(1); break; case Comparison_Integer: case Comparison_Numeric: setRelationType(Relation_Greater); mSelect.mValue.setInteger(0); break; default: // Do nothing break; } update(); } void CSMWorld::InfoSelectWrapper::update() { std::ostringstream stream; // Leading 0 stream << '0'; // Write Function bool writeIndex = false; size_t functionIndex = static_cast(mFunctionName); switch (mFunctionName) { case Function_None: stream << '0'; break; case Function_Global: stream << '2'; break; case Function_Local: stream << '3'; break; case Function_Journal: stream << '4'; break; case Function_Item: stream << '5'; break; case Function_Dead: stream << '6'; break; case Function_NotId: stream << '7'; break; case Function_NotFaction: stream << '8'; break; case Function_NotClass: stream << '9'; break; case Function_NotRace: stream << 'A'; break; case Function_NotCell: stream << 'B'; break; case Function_NotLocal: stream << 'C'; break; default: stream << '1'; writeIndex = true; break; } if (writeIndex && functionIndex < 10) // leading 0 stream << '0' << functionIndex; else if (writeIndex) stream << functionIndex; else stream << "00"; // Write Relation switch (mRelationType) { case Relation_Equal: stream << '0'; break; case Relation_NotEqual: stream << '1'; break; case Relation_Greater: stream << '2'; break; case Relation_GreaterOrEqual: stream << '3'; break; case Relation_Less: stream << '4'; break; case Relation_LessOrEqual: stream << '5'; break; default: stream << '0'; break; } if (mHasVariable) stream << mVariableName; mSelect.mSelectRule = stream.str(); } ESM::Variant& CSMWorld::InfoSelectWrapper::getVariant() { return mSelect.mValue; } ================================================ FILE: apps/opencs/model/world/infoselectwrapper.hpp ================================================ #ifndef CSM_WORLD_INFOSELECTWRAPPER_H #define CSM_WORLD_INFOSELECTWRAPPER_H #include namespace CSMWorld { // ESM::DialInfo::SelectStruct.mSelectRule // 012345... // ^^^ ^^ // ||| || // ||| |+------------- condition variable string // ||| +-------------- comparison type, ['0'..'5']; e.g. !=, <, >=, etc // ||+---------------- function index (encoded, where function == '1') // |+----------------- function, ['1'..'C']; e.g. Global, Local, Not ID, etc // +------------------ unknown // // Wrapper for DialInfo::SelectStruct class ConstInfoSelectWrapper { public: // Order matters enum FunctionName { Function_RankLow=0, Function_RankHigh, Function_RankRequirement, Function_Reputation, Function_Health_Percent, Function_PcReputation, Function_PcLevel, Function_PcHealthPercent, Function_PcMagicka, Function_PcFatigue, Function_PcStrength, Function_PcBlock, Function_PcArmorer, Function_PcMediumArmor, Function_PcHeavyArmor, Function_PcBluntWeapon, Function_PcLongBlade, Function_PcAxe, Function_PcSpear, Function_PcAthletics, Function_PcEnchant, Function_PcDestruction, Function_PcAlteration, Function_PcIllusion, Function_PcConjuration, Function_PcMysticism, Function_PcRestoration, Function_PcAlchemy, Function_PcUnarmored, Function_PcSecurity, Function_PcSneak, Function_PcAcrobatics, Function_PcLightArmor, Function_PcShortBlade, Function_PcMarksman, Function_PcMerchantile, Function_PcSpeechcraft, Function_PcHandToHand, Function_PcGender, Function_PcExpelled, Function_PcCommonDisease, Function_PcBlightDisease, Function_PcClothingModifier, Function_PcCrimeLevel, Function_SameSex, Function_SameRace, Function_SameFaction, Function_FactionRankDifference, Function_Detected, Function_Alarmed, Function_Choice, Function_PcIntelligence, Function_PcWillpower, Function_PcAgility, Function_PcSpeed, Function_PcEndurance, Function_PcPersonality, Function_PcLuck, Function_PcCorpus, Function_Weather, Function_PcVampire, Function_Level, Function_Attacked, Function_TalkedToPc, Function_PcHealth, Function_CreatureTarget, Function_FriendHit, Function_Fight, Function_Hello, Function_Alarm, Function_Flee, Function_ShouldAttack, Function_Werewolf, Function_PcWerewolfKills=73, Function_Global, Function_Local, Function_Journal, Function_Item, Function_Dead, Function_NotId, Function_NotFaction, Function_NotClass, Function_NotRace, Function_NotCell, Function_NotLocal, Function_None }; enum RelationType { Relation_Equal, Relation_NotEqual, Relation_Greater, Relation_GreaterOrEqual, Relation_Less, Relation_LessOrEqual, Relation_None }; enum ComparisonType { Comparison_Boolean, Comparison_Integer, Comparison_Numeric, Comparison_None }; static const size_t RuleMinSize; static const size_t FunctionPrefixOffset; static const size_t FunctionIndexOffset; static const size_t RelationIndexOffset; static const size_t VarNameOffset; static const char* FunctionEnumStrings[]; static const char* RelationEnumStrings[]; static const char* ComparisonEnumStrings[]; static std::string convertToString(FunctionName name); static std::string convertToString(RelationType type); static std::string convertToString(ComparisonType type); ConstInfoSelectWrapper(const ESM::DialInfo::SelectStruct& select); FunctionName getFunctionName() const; RelationType getRelationType() const; ComparisonType getComparisonType() const; bool hasVariable() const; const std::string& getVariableName() const; bool conditionIsAlwaysTrue() const; bool conditionIsNeverTrue() const; bool variantTypeIsValid() const; const ESM::Variant& getVariant() const; std::string toString() const; protected: void readRule(); void readFunctionName(); void readRelationType(); void readVariableName(); void updateHasVariable(); void updateComparisonType(); std::pair getConditionIntRange() const; std::pair getConditionFloatRange() const; std::pair getValidIntRange() const; std::pair getValidFloatRange() const; template bool rangeContains(Type1 value, std::pair range) const; template bool rangesOverlap(std::pair range1, std::pair range2) const; template bool rangeFullyContains(std::pair containing, std::pair test) const; template bool rangesMatch(std::pair range1, std::pair range2) const; template bool conditionIsAlwaysTrue(std::pair conditionRange, std::pair validRange) const; template bool conditionIsNeverTrue(std::pair conditionRange, std::pair validRange) const; FunctionName mFunctionName; RelationType mRelationType; ComparisonType mComparisonType; bool mHasVariable; std::string mVariableName; private: const ESM::DialInfo::SelectStruct& mConstSelect; }; // Wrapper for DialInfo::SelectStruct that can modify the wrapped select struct class InfoSelectWrapper : public ConstInfoSelectWrapper { public: InfoSelectWrapper(ESM::DialInfo::SelectStruct& select); // Wrapped SelectStruct will not be modified until update() is called void setFunctionName(FunctionName name); void setRelationType(RelationType type); void setVariableName(const std::string& name); // Modified wrapped SelectStruct void update(); // This sets properties based on the function name to its defaults and updates the wrapped object void setDefaults(); ESM::Variant& getVariant(); private: ESM::DialInfo::SelectStruct& mSelect; void writeRule(); }; } #endif ================================================ FILE: apps/opencs/model/world/infotableproxymodel.cpp ================================================ #include "infotableproxymodel.hpp" #include #include "idtablebase.hpp" #include "columns.hpp" namespace { QString toLower(const QString &str) { return QString::fromUtf8(Misc::StringUtils::lowerCase(str.toUtf8().constData()).c_str()); } } CSMWorld::InfoTableProxyModel::InfoTableProxyModel(CSMWorld::UniversalId::Type type, QObject *parent) : IdTableProxyModel(parent), mType(type), mInfoColumnId(type == UniversalId::Type_TopicInfos ? Columns::ColumnId_Topic : Columns::ColumnId_Journal), mInfoColumnIndex(-1), mLastAddedSourceRow(-1) { Q_ASSERT(type == UniversalId::Type_TopicInfos || type == UniversalId::Type_JournalInfos); } void CSMWorld::InfoTableProxyModel::setSourceModel(QAbstractItemModel *sourceModel) { IdTableProxyModel::setSourceModel(sourceModel); if (mSourceModel != nullptr) { mInfoColumnIndex = mSourceModel->findColumnIndex(mInfoColumnId); mFirstRowCache.clear(); } } bool CSMWorld::InfoTableProxyModel::lessThan(const QModelIndex &left, const QModelIndex &right) const { Q_ASSERT(mSourceModel != nullptr); QModelIndex first = mSourceModel->index(getFirstInfoRow(left.row()), left.column()); QModelIndex second = mSourceModel->index(getFirstInfoRow(right.row()), right.column()); // If both indexes are belonged to the same Topic/Journal, compare their original rows only if (first.row() == second.row()) { return sortOrder() == Qt::AscendingOrder ? left.row() < right.row() : right.row() < left.row(); } return IdTableProxyModel::lessThan(first, second); } int CSMWorld::InfoTableProxyModel::getFirstInfoRow(int currentRow) const { Q_ASSERT(mSourceModel != nullptr); int row = currentRow; int column = mInfoColumnIndex; QString info = toLower(mSourceModel->data(mSourceModel->index(row, column)).toString()); if (mFirstRowCache.contains(info)) { return mFirstRowCache[info]; } while (--row >= 0 && toLower(mSourceModel->data(mSourceModel->index(row, column)).toString()) == info); ++row; mFirstRowCache[info] = row; return row; } void CSMWorld::InfoTableProxyModel::sourceRowsRemoved(const QModelIndex &/*parent*/, int /*start*/, int /*end*/) { refreshFilter(); mFirstRowCache.clear(); } void CSMWorld::InfoTableProxyModel::sourceRowsInserted(const QModelIndex &parent, int /*start*/, int end) { refreshFilter(); if (!parent.isValid()) { mFirstRowCache.clear(); // We can't re-sort the model here, because the topic of the added row isn't set yet. // Store the row index for using in the first dataChanged() after this row insertion. mLastAddedSourceRow = end; } } void CSMWorld::InfoTableProxyModel::sourceDataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight) { refreshFilter(); if (mLastAddedSourceRow != -1 && topLeft.row() <= mLastAddedSourceRow && bottomRight.row() >= mLastAddedSourceRow) { // Now the topic of the last added row is set, // so we can re-sort the model to ensure the corrent position of this row int column = sortColumn(); Qt::SortOrder order = sortOrder(); sort(mInfoColumnIndex); // Restore the correct position of an added row sort(column, order); // Restore the original sort order emit rowAdded(getRecordId(mLastAddedSourceRow).toUtf8().constData()); // Make sure that we perform a re-sorting only in the first dataChanged() after a row insertion mLastAddedSourceRow = -1; } } ================================================ FILE: apps/opencs/model/world/infotableproxymodel.hpp ================================================ #ifndef CSM_WORLD_INFOTABLEPROXYMODEL_HPP #define CSM_WORLD_INFOTABLEPROXYMODEL_HPP #include #include "idtableproxymodel.hpp" #include "columns.hpp" #include "universalid.hpp" namespace CSMWorld { class IdTableBase; class InfoTableProxyModel : public IdTableProxyModel { Q_OBJECT UniversalId::Type mType; Columns::ColumnId mInfoColumnId; ///< Contains ID for Topic or Journal ID int mInfoColumnIndex; int mLastAddedSourceRow; mutable QHash mFirstRowCache; int getFirstInfoRow(int currentRow) const; ///< Finds the first row with the same topic (journal entry) as in \a currentRow ///< \a currentRow is a row of the source model. public: InfoTableProxyModel(UniversalId::Type type, QObject *parent = nullptr); void setSourceModel(QAbstractItemModel *sourceModel) override; protected: bool lessThan(const QModelIndex &left, const QModelIndex &right) const override; protected slots: void sourceRowsInserted(const QModelIndex &parent, int start, int end) override; void sourceRowsRemoved(const QModelIndex &parent, int start, int end) override; void sourceDataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight) override; }; } #endif ================================================ FILE: apps/opencs/model/world/land.cpp ================================================ #include "land.hpp" #include #include namespace CSMWorld { void Land::load(ESM::ESMReader &esm, bool &isDeleted) { ESM::Land::load(esm, isDeleted); } std::string Land::createUniqueRecordId(int x, int y) { std::ostringstream stream; stream << "#" << x << " " << y; return stream.str(); } void Land::parseUniqueRecordId(const std::string& id, int& x, int& y) { size_t mid = id.find(' '); if (mid == std::string::npos || id[0] != '#') throw std::runtime_error("Invalid Land ID"); x = std::stoi(id.substr(1, mid - 1)); y = std::stoi(id.substr(mid + 1)); } } ================================================ FILE: apps/opencs/model/world/land.hpp ================================================ #ifndef CSM_WORLD_LAND_H #define CSM_WORLD_LAND_H #include #include namespace CSMWorld { /// \brief Wrapper for Land record. Encodes X and Y cell index in the ID. /// /// \todo Add worldspace support to the Land record. struct Land : public ESM::Land { /// Loads the metadata and ID void load (ESM::ESMReader &esm, bool &isDeleted); static std::string createUniqueRecordId(int x, int y); static void parseUniqueRecordId(const std::string& id, int& x, int& y); }; } #endif ================================================ FILE: apps/opencs/model/world/landtexture.cpp ================================================ #include "landtexture.hpp" #include #include #include namespace CSMWorld { void LandTexture::load(ESM::ESMReader &esm, bool &isDeleted) { ESM::LandTexture::load(esm, isDeleted); mPluginIndex = esm.getIndex(); } std::string LandTexture::createUniqueRecordId(int plugin, int index) { std::stringstream ss; ss << 'L' << plugin << '#' << index; return ss.str(); } void LandTexture::parseUniqueRecordId(const std::string& id, int& plugin, int& index) { size_t middle = id.find('#'); if (middle == std::string::npos || id[0] != 'L') throw std::runtime_error("Invalid LandTexture ID"); plugin = std::stoi(id.substr(1,middle-1)); index = std::stoi(id.substr(middle+1)); } } ================================================ FILE: apps/opencs/model/world/landtexture.hpp ================================================ #ifndef CSM_WORLD_LANDTEXTURE_H #define CSM_WORLD_LANDTEXTURE_H #include #include namespace CSMWorld { /// \brief Wrapper for LandTexture record, providing info which plugin the LandTexture was loaded from. struct LandTexture : public ESM::LandTexture { int mPluginIndex; void load (ESM::ESMReader &esm, bool &isDeleted); /// Returns a string identifier that will be unique to any LandTexture. static std::string createUniqueRecordId(int plugin, int index); /// Deconstructs a unique string identifier into plugin and index. static void parseUniqueRecordId(const std::string& id, int& plugin, int& index); }; } #endif ================================================ FILE: apps/opencs/model/world/landtexturetableproxymodel.cpp ================================================ #include "landtexturetableproxymodel.hpp" #include "idtable.hpp" namespace CSMWorld { LandTextureTableProxyModel::LandTextureTableProxyModel(QObject* parent) : IdTableProxyModel(parent) { } bool LandTextureTableProxyModel::filterAcceptsRow (int sourceRow, const QModelIndex& sourceParent) const { return IdTableProxyModel::filterAcceptsRow(sourceRow, sourceParent); } } ================================================ FILE: apps/opencs/model/world/landtexturetableproxymodel.hpp ================================================ #ifndef CSM_WORLD_LANDTEXTURETABLEPROXYMODEL_H #define CSM_WORLD_LANDTEXTURETABLEPROXYMODEL_H #include "idtableproxymodel.hpp" namespace CSMWorld { /// \brief Removes base records from filtered results. class LandTextureTableProxyModel : public IdTableProxyModel { Q_OBJECT public: LandTextureTableProxyModel(QObject* parent = nullptr); protected: bool filterAcceptsRow (int sourceRow, const QModelIndex& sourceParent) const override; }; } #endif ================================================ FILE: apps/opencs/model/world/metadata.cpp ================================================ #include "metadata.hpp" #include #include #include void CSMWorld::MetaData::blank() { mFormat = ESM::Header::CurrentFormat; mAuthor.clear(); mDescription.clear(); } void CSMWorld::MetaData::load (ESM::ESMReader& esm) { mFormat = esm.getHeader().mFormat; mAuthor = esm.getHeader().mData.author; mDescription = esm.getHeader().mData.desc; } void CSMWorld::MetaData::save (ESM::ESMWriter& esm) const { esm.setFormat (mFormat); esm.setAuthor (mAuthor); esm.setDescription (mDescription); } ================================================ FILE: apps/opencs/model/world/metadata.hpp ================================================ #ifndef CSM_WOLRD_METADATA_H #define CSM_WOLRD_METADATA_H #include namespace ESM { class ESMReader; class ESMWriter; } namespace CSMWorld { struct MetaData { std::string mId; int mFormat; std::string mAuthor; std::string mDescription; void blank(); void load (ESM::ESMReader& esm); void save (ESM::ESMWriter& esm) const; }; } #endif ================================================ FILE: apps/opencs/model/world/nestedcoladapterimp.cpp ================================================ #include "nestedcoladapterimp.hpp" #include #include #include "idcollection.hpp" #include "pathgrid.hpp" #include "info.hpp" #include "infoselectwrapper.hpp" namespace CSMWorld { PathgridPointListAdapter::PathgridPointListAdapter () {} void PathgridPointListAdapter::addRow(Record& record, int position) const { Pathgrid pathgrid = record.get(); ESM::Pathgrid::PointList& points = pathgrid.mPoints; // blank row ESM::Pathgrid::Point point; point.mX = 0; point.mY = 0; point.mZ = 0; point.mAutogenerated = 0; point.mConnectionNum = 0; point.mUnknown = 0; points.insert(points.begin()+position, point); pathgrid.mData.mS2 = pathgrid.mPoints.size(); record.setModified (pathgrid); } void PathgridPointListAdapter::removeRow(Record& record, int rowToRemove) const { Pathgrid pathgrid = record.get(); ESM::Pathgrid::PointList& points = pathgrid.mPoints; if (rowToRemove < 0 || rowToRemove >= static_cast (points.size())) throw std::runtime_error ("index out of range"); // Do not remove dangling edges, does not work with current undo mechanism // Do not automatically adjust indices, what would be done with dangling edges? points.erase(points.begin()+rowToRemove); pathgrid.mData.mS2 = pathgrid.mPoints.size(); record.setModified (pathgrid); } void PathgridPointListAdapter::setTable(Record& record, const NestedTableWrapperBase& nestedTable) const { Pathgrid pathgrid = record.get(); pathgrid.mPoints = static_cast &>(nestedTable).mNestedTable; pathgrid.mData.mS2 = pathgrid.mPoints.size(); record.setModified (pathgrid); } NestedTableWrapperBase* PathgridPointListAdapter::table(const Record& record) const { // deleted by dtor of NestedTableStoring return new NestedTableWrapper(record.get().mPoints); } QVariant PathgridPointListAdapter::getData(const Record& record, int subRowIndex, int subColIndex) const { ESM::Pathgrid::Point point = record.get().mPoints[subRowIndex]; switch (subColIndex) { case 0: return subRowIndex; case 1: return point.mX; case 2: return point.mY; case 3: return point.mZ; default: throw std::runtime_error("Pathgrid point subcolumn index out of range"); } } void PathgridPointListAdapter::setData(Record& record, const QVariant& value, int subRowIndex, int subColIndex) const { Pathgrid pathgrid = record.get(); ESM::Pathgrid::Point point = pathgrid.mPoints[subRowIndex]; switch (subColIndex) { case 0: return; // return without saving case 1: point.mX = value.toInt(); break; case 2: point.mY = value.toInt(); break; case 3: point.mZ = value.toInt(); break; default: throw std::runtime_error("Pathgrid point subcolumn index out of range"); } pathgrid.mPoints[subRowIndex] = point; record.setModified (pathgrid); } int PathgridPointListAdapter::getColumnsCount(const Record& record) const { return 4; } int PathgridPointListAdapter::getRowsCount(const Record& record) const { return static_cast(record.get().mPoints.size()); } PathgridEdgeListAdapter::PathgridEdgeListAdapter () {} void PathgridEdgeListAdapter::addRow(Record& record, int position) const { Pathgrid pathgrid = record.get(); ESM::Pathgrid::EdgeList& edges = pathgrid.mEdges; // blank row ESM::Pathgrid::Edge edge; edge.mV0 = 0; edge.mV1 = 0; // NOTE: inserting a blank edge does not really make sense, perhaps this should be a // logic_error exception // // Currently the code assumes that the end user to know what he/she is doing. // e.g. Edges come in pairs, from points a->b and b->a edges.insert(edges.begin()+position, edge); record.setModified (pathgrid); } void PathgridEdgeListAdapter::removeRow(Record& record, int rowToRemove) const { Pathgrid pathgrid = record.get(); ESM::Pathgrid::EdgeList& edges = pathgrid.mEdges; if (rowToRemove < 0 || rowToRemove >= static_cast (edges.size())) throw std::runtime_error ("index out of range"); edges.erase(edges.begin()+rowToRemove); record.setModified (pathgrid); } void PathgridEdgeListAdapter::setTable(Record& record, const NestedTableWrapperBase& nestedTable) const { Pathgrid pathgrid = record.get(); pathgrid.mEdges = static_cast &>(nestedTable).mNestedTable; record.setModified (pathgrid); } NestedTableWrapperBase* PathgridEdgeListAdapter::table(const Record& record) const { // deleted by dtor of NestedTableStoring return new NestedTableWrapper(record.get().mEdges); } QVariant PathgridEdgeListAdapter::getData(const Record& record, int subRowIndex, int subColIndex) const { Pathgrid pathgrid = record.get(); if (subRowIndex < 0 || subRowIndex >= static_cast (pathgrid.mEdges.size())) throw std::runtime_error ("index out of range"); ESM::Pathgrid::Edge edge = pathgrid.mEdges[subRowIndex]; switch (subColIndex) { case 0: return subRowIndex; case 1: return edge.mV0; case 2: return edge.mV1; default: throw std::runtime_error("Pathgrid edge subcolumn index out of range"); } } void PathgridEdgeListAdapter::setData(Record& record, const QVariant& value, int subRowIndex, int subColIndex) const { Pathgrid pathgrid = record.get(); if (subRowIndex < 0 || subRowIndex >= static_cast (pathgrid.mEdges.size())) throw std::runtime_error ("index out of range"); ESM::Pathgrid::Edge edge = pathgrid.mEdges[subRowIndex]; switch (subColIndex) { case 0: return; // return without saving case 1: edge.mV0 = value.toInt(); break; case 2: edge.mV1 = value.toInt(); break; default: throw std::runtime_error("Pathgrid edge subcolumn index out of range"); } pathgrid.mEdges[subRowIndex] = edge; record.setModified (pathgrid); } int PathgridEdgeListAdapter::getColumnsCount(const Record& record) const { return 3; } int PathgridEdgeListAdapter::getRowsCount(const Record& record) const { return static_cast(record.get().mEdges.size()); } FactionReactionsAdapter::FactionReactionsAdapter () {} void FactionReactionsAdapter::addRow(Record& record, int position) const { ESM::Faction faction = record.get(); std::map& reactions = faction.mReactions; // blank row reactions.insert(std::make_pair("", 0)); record.setModified (faction); } void FactionReactionsAdapter::removeRow(Record& record, int rowToRemove) const { ESM::Faction faction = record.get(); std::map& reactions = faction.mReactions; if (rowToRemove < 0 || rowToRemove >= static_cast (reactions.size())) throw std::runtime_error ("index out of range"); // FIXME: how to ensure that the map entries correspond to table indicies? // WARNING: Assumed that the table view has the same order as std::map std::map::iterator iter = reactions.begin(); for(int i = 0; i < rowToRemove; ++i) ++iter; reactions.erase(iter); record.setModified (faction); } void FactionReactionsAdapter::setTable(Record& record, const NestedTableWrapperBase& nestedTable) const { ESM::Faction faction = record.get(); faction.mReactions = static_cast >&>(nestedTable).mNestedTable; record.setModified (faction); } NestedTableWrapperBase* FactionReactionsAdapter::table(const Record& record) const { // deleted by dtor of NestedTableStoring return new NestedTableWrapper >(record.get().mReactions); } QVariant FactionReactionsAdapter::getData(const Record& record, int subRowIndex, int subColIndex) const { ESM::Faction faction = record.get(); std::map& reactions = faction.mReactions; if (subRowIndex < 0 || subRowIndex >= static_cast (reactions.size())) throw std::runtime_error ("index out of range"); // FIXME: how to ensure that the map entries correspond to table indicies? // WARNING: Assumed that the table view has the same order as std::map std::map::const_iterator iter = reactions.begin(); for(int i = 0; i < subRowIndex; ++i) ++iter; switch (subColIndex) { case 0: return QString((*iter).first.c_str()); case 1: return (*iter).second; default: throw std::runtime_error("Faction reactions subcolumn index out of range"); } } void FactionReactionsAdapter::setData(Record& record, const QVariant& value, int subRowIndex, int subColIndex) const { ESM::Faction faction = record.get(); std::map& reactions = faction.mReactions; if (subRowIndex < 0 || subRowIndex >= static_cast (reactions.size())) throw std::runtime_error ("index out of range"); // FIXME: how to ensure that the map entries correspond to table indicies? // WARNING: Assumed that the table view has the same order as std::map std::map::iterator iter = reactions.begin(); for(int i = 0; i < subRowIndex; ++i) ++iter; std::string factionId = (*iter).first; int reaction = (*iter).second; switch (subColIndex) { case 0: { reactions.erase(iter); reactions.insert(std::make_pair(value.toString().toUtf8().constData(), reaction)); break; } case 1: { reactions[factionId] = value.toInt(); break; } default: throw std::runtime_error("Faction reactions subcolumn index out of range"); } record.setModified (faction); } int FactionReactionsAdapter::getColumnsCount(const Record& record) const { return 2; } int FactionReactionsAdapter::getRowsCount(const Record& record) const { return static_cast(record.get().mReactions.size()); } RegionSoundListAdapter::RegionSoundListAdapter () {} void RegionSoundListAdapter::addRow(Record& record, int position) const { ESM::Region region = record.get(); std::vector& soundList = region.mSoundList; // blank row ESM::Region::SoundRef soundRef; soundRef.mSound.assign(""); soundRef.mChance = 0; soundList.insert(soundList.begin()+position, soundRef); record.setModified (region); } void RegionSoundListAdapter::removeRow(Record& record, int rowToRemove) const { ESM::Region region = record.get(); std::vector& soundList = region.mSoundList; if (rowToRemove < 0 || rowToRemove >= static_cast (soundList.size())) throw std::runtime_error ("index out of range"); soundList.erase(soundList.begin()+rowToRemove); record.setModified (region); } void RegionSoundListAdapter::setTable(Record& record, const NestedTableWrapperBase& nestedTable) const { ESM::Region region = record.get(); region.mSoundList = static_cast >&>(nestedTable).mNestedTable; record.setModified (region); } NestedTableWrapperBase* RegionSoundListAdapter::table(const Record& record) const { // deleted by dtor of NestedTableStoring return new NestedTableWrapper >(record.get().mSoundList); } QVariant RegionSoundListAdapter::getData(const Record& record, int subRowIndex, int subColIndex) const { ESM::Region region = record.get(); std::vector& soundList = region.mSoundList; if (subRowIndex < 0 || subRowIndex >= static_cast (soundList.size())) throw std::runtime_error ("index out of range"); ESM::Region::SoundRef soundRef = soundList[subRowIndex]; switch (subColIndex) { case 0: return QString(soundRef.mSound.c_str()); case 1: return soundRef.mChance; default: throw std::runtime_error("Region sounds subcolumn index out of range"); } } void RegionSoundListAdapter::setData(Record& record, const QVariant& value, int subRowIndex, int subColIndex) const { ESM::Region region = record.get(); std::vector& soundList = region.mSoundList; if (subRowIndex < 0 || subRowIndex >= static_cast (soundList.size())) throw std::runtime_error ("index out of range"); ESM::Region::SoundRef soundRef = soundList[subRowIndex]; switch (subColIndex) { case 0: soundRef.mSound.assign(value.toString().toUtf8().constData()); break; case 1: soundRef.mChance = static_cast(value.toInt()); break; default: throw std::runtime_error("Region sounds subcolumn index out of range"); } region.mSoundList[subRowIndex] = soundRef; record.setModified (region); } int RegionSoundListAdapter::getColumnsCount(const Record& record) const { return 2; } int RegionSoundListAdapter::getRowsCount(const Record& record) const { return static_cast(record.get().mSoundList.size()); } InfoListAdapter::InfoListAdapter () {} void InfoListAdapter::addRow(Record& record, int position) const { throw std::logic_error ("cannot add a row to a fixed table"); } void InfoListAdapter::removeRow(Record& record, int rowToRemove) const { throw std::logic_error ("cannot remove a row to a fixed table"); } void InfoListAdapter::setTable(Record& record, const NestedTableWrapperBase& nestedTable) const { throw std::logic_error ("table operation not supported"); } NestedTableWrapperBase* InfoListAdapter::table(const Record& record) const { throw std::logic_error ("table operation not supported"); } QVariant InfoListAdapter::getData(const Record& record, int subRowIndex, int subColIndex) const { Info info = record.get(); if (subColIndex == 0) return QString(info.mResultScript.c_str()); else throw std::runtime_error("Trying to access non-existing column in the nested table!"); } void InfoListAdapter::setData(Record& record, const QVariant& value, int subRowIndex, int subColIndex) const { Info info = record.get(); if (subColIndex == 0) info.mResultScript = value.toString().toStdString(); else throw std::runtime_error("Trying to access non-existing column in the nested table!"); record.setModified (info); } int InfoListAdapter::getColumnsCount(const Record& record) const { return 1; } int InfoListAdapter::getRowsCount(const Record& record) const { return 1; // fixed at size 1 } InfoConditionAdapter::InfoConditionAdapter () {} void InfoConditionAdapter::addRow(Record& record, int position) const { Info info = record.get(); std::vector& conditions = info.mSelects; // default row ESM::DialInfo::SelectStruct condStruct; condStruct.mSelectRule = "01000"; condStruct.mValue = ESM::Variant(); condStruct.mValue.setType(ESM::VT_Int); conditions.insert(conditions.begin()+position, condStruct); record.setModified (info); } void InfoConditionAdapter::removeRow(Record& record, int rowToRemove) const { Info info = record.get(); std::vector& conditions = info.mSelects; if (rowToRemove < 0 || rowToRemove >= static_cast (conditions.size())) throw std::runtime_error ("index out of range"); conditions.erase(conditions.begin()+rowToRemove); record.setModified (info); } void InfoConditionAdapter::setTable(Record& record, const NestedTableWrapperBase& nestedTable) const { Info info = record.get(); info.mSelects = static_cast >&>(nestedTable).mNestedTable; record.setModified (info); } NestedTableWrapperBase* InfoConditionAdapter::table(const Record& record) const { // deleted by dtor of NestedTableStoring return new NestedTableWrapper >(record.get().mSelects); } QVariant InfoConditionAdapter::getData(const Record& record, int subRowIndex, int subColIndex) const { Info info = record.get(); std::vector& conditions = info.mSelects; if (subRowIndex < 0 || subRowIndex >= static_cast (conditions.size())) throw std::runtime_error ("index out of range"); ConstInfoSelectWrapper infoSelectWrapper(conditions[subRowIndex]); switch (subColIndex) { case 0: { return infoSelectWrapper.getFunctionName(); } case 1: { if (infoSelectWrapper.hasVariable()) return QString(infoSelectWrapper.getVariableName().c_str()); else return ""; } case 2: { return infoSelectWrapper.getRelationType(); } case 3: { switch (infoSelectWrapper.getVariant().getType()) { case ESM::VT_Int: { return infoSelectWrapper.getVariant().getInteger(); } case ESM::VT_Float: { return infoSelectWrapper.getVariant().getFloat(); } default: return QVariant(); } } default: throw std::runtime_error("Info condition subcolumn index out of range"); } } void InfoConditionAdapter::setData(Record& record, const QVariant& value, int subRowIndex, int subColIndex) const { Info info = record.get(); std::vector& conditions = info.mSelects; if (subRowIndex < 0 || subRowIndex >= static_cast (conditions.size())) throw std::runtime_error ("index out of range"); InfoSelectWrapper infoSelectWrapper(conditions[subRowIndex]); bool conversionResult = false; switch (subColIndex) { case 0: // Function { infoSelectWrapper.setFunctionName(static_cast(value.toInt())); if (infoSelectWrapper.getComparisonType() != ConstInfoSelectWrapper::Comparison_Numeric && infoSelectWrapper.getVariant().getType() != ESM::VT_Int) { infoSelectWrapper.getVariant().setType(ESM::VT_Int); } infoSelectWrapper.update(); break; } case 1: // Variable { infoSelectWrapper.setVariableName(value.toString().toUtf8().constData()); infoSelectWrapper.update(); break; } case 2: // Relation { infoSelectWrapper.setRelationType(static_cast(value.toInt())); infoSelectWrapper.update(); break; } case 3: // Value { switch (infoSelectWrapper.getComparisonType()) { case ConstInfoSelectWrapper::Comparison_Numeric: { // QVariant seems to have issues converting 0 if ((value.toInt(&conversionResult) && conversionResult) || value.toString().compare("0") == 0) { infoSelectWrapper.getVariant().setType(ESM::VT_Int); infoSelectWrapper.getVariant().setInteger(value.toInt()); } else if (value.toFloat(&conversionResult) && conversionResult) { infoSelectWrapper.getVariant().setType(ESM::VT_Float); infoSelectWrapper.getVariant().setFloat(value.toFloat()); } break; } case ConstInfoSelectWrapper::Comparison_Boolean: case ConstInfoSelectWrapper::Comparison_Integer: { if ((value.toInt(&conversionResult) && conversionResult) || value.toString().compare("0") == 0) { infoSelectWrapper.getVariant().setType(ESM::VT_Int); infoSelectWrapper.getVariant().setInteger(value.toInt()); } break; } default: break; } break; } default: throw std::runtime_error("Info condition subcolumn index out of range"); } record.setModified (info); } int InfoConditionAdapter::getColumnsCount(const Record& record) const { return 4; } int InfoConditionAdapter::getRowsCount(const Record& record) const { return static_cast(record.get().mSelects.size()); } RaceAttributeAdapter::RaceAttributeAdapter () {} void RaceAttributeAdapter::addRow(Record& record, int position) const { // Do nothing, this table cannot be changed by the user } void RaceAttributeAdapter::removeRow(Record& record, int rowToRemove) const { // Do nothing, this table cannot be changed by the user } void RaceAttributeAdapter::setTable(Record& record, const NestedTableWrapperBase& nestedTable) const { ESM::Race race = record.get(); race.mData = static_cast >&>(nestedTable).mNestedTable.at(0); record.setModified (race); } NestedTableWrapperBase* RaceAttributeAdapter::table(const Record& record) const { std::vector wrap; wrap.push_back(record.get().mData); // deleted by dtor of NestedTableStoring return new NestedTableWrapper >(wrap); } QVariant RaceAttributeAdapter::getData(const Record& record, int subRowIndex, int subColIndex) const { ESM::Race race = record.get(); if (subRowIndex < 0 || subRowIndex >= ESM::Attribute::Length) throw std::runtime_error ("index out of range"); switch (subColIndex) { case 0: return subRowIndex; case 1: return race.mData.mAttributeValues[subRowIndex].mMale; case 2: return race.mData.mAttributeValues[subRowIndex].mFemale; default: throw std::runtime_error("Race Attribute subcolumn index out of range"); } } void RaceAttributeAdapter::setData(Record& record, const QVariant& value, int subRowIndex, int subColIndex) const { ESM::Race race = record.get(); if (subRowIndex < 0 || subRowIndex >= ESM::Attribute::Length) throw std::runtime_error ("index out of range"); switch (subColIndex) { case 0: return; // throw an exception here? case 1: race.mData.mAttributeValues[subRowIndex].mMale = value.toInt(); break; case 2: race.mData.mAttributeValues[subRowIndex].mFemale = value.toInt(); break; default: throw std::runtime_error("Race Attribute subcolumn index out of range"); } record.setModified (race); } int RaceAttributeAdapter::getColumnsCount(const Record& record) const { return 3; // attrib, male, female } int RaceAttributeAdapter::getRowsCount(const Record& record) const { return ESM::Attribute::Length; // there are 8 attributes } RaceSkillsBonusAdapter::RaceSkillsBonusAdapter () {} void RaceSkillsBonusAdapter::addRow(Record& record, int position) const { // Do nothing, this table cannot be changed by the user } void RaceSkillsBonusAdapter::removeRow(Record& record, int rowToRemove) const { // Do nothing, this table cannot be changed by the user } void RaceSkillsBonusAdapter::setTable(Record& record, const NestedTableWrapperBase& nestedTable) const { ESM::Race race = record.get(); race.mData = static_cast >&>(nestedTable).mNestedTable.at(0); record.setModified (race); } NestedTableWrapperBase* RaceSkillsBonusAdapter::table(const Record& record) const { std::vector wrap; wrap.push_back(record.get().mData); // deleted by dtor of NestedTableStoring return new NestedTableWrapper >(wrap); } QVariant RaceSkillsBonusAdapter::getData(const Record& record, int subRowIndex, int subColIndex) const { ESM::Race race = record.get(); if (subRowIndex < 0 || subRowIndex >= static_cast(sizeof(race.mData.mBonus)/sizeof(race.mData.mBonus[0]))) throw std::runtime_error ("index out of range"); switch (subColIndex) { case 0: return race.mData.mBonus[subRowIndex].mSkill; // can be -1 case 1: return race.mData.mBonus[subRowIndex].mBonus; default: throw std::runtime_error("Race skill bonus subcolumn index out of range"); } } void RaceSkillsBonusAdapter::setData(Record& record, const QVariant& value, int subRowIndex, int subColIndex) const { ESM::Race race = record.get(); if (subRowIndex < 0 || subRowIndex >= static_cast(sizeof(race.mData.mBonus)/sizeof(race.mData.mBonus[0]))) throw std::runtime_error ("index out of range"); switch (subColIndex) { case 0: race.mData.mBonus[subRowIndex].mSkill = value.toInt(); break; // can be -1 case 1: race.mData.mBonus[subRowIndex].mBonus = value.toInt(); break; default: throw std::runtime_error("Race skill bonus subcolumn index out of range"); } record.setModified (race); } int RaceSkillsBonusAdapter::getColumnsCount(const Record& record) const { return 2; // skill, bonus } int RaceSkillsBonusAdapter::getRowsCount(const Record& record) const { // there are 7 skill bonuses return static_cast(sizeof(record.get().mData.mBonus)/sizeof(record.get().mData.mBonus[0])); } CellListAdapter::CellListAdapter () {} void CellListAdapter::addRow(Record& record, int position) const { throw std::logic_error ("cannot add a row to a fixed table"); } void CellListAdapter::removeRow(Record& record, int rowToRemove) const { throw std::logic_error ("cannot remove a row to a fixed table"); } void CellListAdapter::setTable(Record& record, const NestedTableWrapperBase& nestedTable) const { throw std::logic_error ("table operation not supported"); } NestedTableWrapperBase* CellListAdapter::table(const Record& record) const { throw std::logic_error ("table operation not supported"); } QVariant CellListAdapter::getData(const Record& record, int subRowIndex, int subColIndex) const { CSMWorld::Cell cell = record.get(); bool isInterior = (cell.mData.mFlags & ESM::Cell::Interior) != 0; bool behaveLikeExterior = (cell.mData.mFlags & ESM::Cell::QuasiEx) != 0; bool interiorWater = (cell.mData.mFlags & ESM::Cell::HasWater) != 0; switch (subColIndex) { case 0: return isInterior; // While the ambient information is not necessarily valid if the subrecord wasn't loaded, // the user should still be allowed to edit it case 1: return (isInterior && !behaveLikeExterior) ? cell.mAmbi.mAmbient : QVariant(QVariant::UserType); case 2: return (isInterior && !behaveLikeExterior) ? cell.mAmbi.mSunlight : QVariant(QVariant::UserType); case 3: return (isInterior && !behaveLikeExterior) ? cell.mAmbi.mFog : QVariant(QVariant::UserType); case 4: return (isInterior && !behaveLikeExterior) ? cell.mAmbi.mFogDensity : QVariant(QVariant::UserType); case 5: { if (isInterior && interiorWater) return cell.mWater; else return QVariant(QVariant::UserType); } case 6: return isInterior ? QVariant(QVariant::UserType) : cell.mMapColor; // TODO: how to select? //case 7: return isInterior ? //behaveLikeExterior : QVariant(QVariant::UserType); default: throw std::runtime_error("Cell subcolumn index out of range"); } } void CellListAdapter::setData(Record& record, const QVariant& value, int subRowIndex, int subColIndex) const { CSMWorld::Cell cell = record.get(); bool isInterior = (cell.mData.mFlags & ESM::Cell::Interior) != 0; bool behaveLikeExterior = (cell.mData.mFlags & ESM::Cell::QuasiEx) != 0; bool interiorWater = (cell.mData.mFlags & ESM::Cell::HasWater) != 0; switch (subColIndex) { case 0: { if (value.toBool()) cell.mData.mFlags |= ESM::Cell::Interior; else cell.mData.mFlags &= ~ESM::Cell::Interior; break; } case 1: { if (isInterior && !behaveLikeExterior) { cell.mAmbi.mAmbient = static_cast(value.toInt()); cell.setHasAmbient(true); } else return; // return without saving break; } case 2: { if (isInterior && !behaveLikeExterior) { cell.mAmbi.mSunlight = static_cast(value.toInt()); cell.setHasAmbient(true); } else return; // return without saving break; } case 3: { if (isInterior && !behaveLikeExterior) { cell.mAmbi.mFog = static_cast(value.toInt()); cell.setHasAmbient(true); } else return; // return without saving break; } case 4: { if (isInterior && !behaveLikeExterior) { cell.mAmbi.mFogDensity = value.toFloat(); cell.setHasAmbient(true); } else return; // return without saving break; } case 5: { if (isInterior && interiorWater) cell.mWater = value.toFloat(); else return; // return without saving break; } case 6: { if (!isInterior) cell.mMapColor = value.toInt(); else return; // return without saving break; } #if 0 // redundant since this flag is shown in the main table as "Interior Sky" // keep here for documenting the logic based on vanilla case 7: { if (isInterior) { if (value.toBool()) cell.mData.mFlags |= ESM::Cell::QuasiEx; else cell.mData.mFlags &= ~ESM::Cell::QuasiEx; } else return; // return without saving break; } #endif default: throw std::runtime_error("Cell subcolumn index out of range"); } record.setModified (cell); } int CellListAdapter::getColumnsCount(const Record& record) const { return 7; } int CellListAdapter::getRowsCount(const Record& record) const { return 1; // fixed at size 1 } RegionWeatherAdapter::RegionWeatherAdapter () {} void RegionWeatherAdapter::addRow(Record& record, int position) const { throw std::logic_error ("cannot add a row to a fixed table"); } void RegionWeatherAdapter::removeRow(Record& record, int rowToRemove) const { throw std::logic_error ("cannot remove a row from a fixed table"); } void RegionWeatherAdapter::setTable(Record& record, const NestedTableWrapperBase& nestedTable) const { throw std::logic_error ("table operation not supported"); } NestedTableWrapperBase* RegionWeatherAdapter::table(const Record& record) const { throw std::logic_error ("table operation not supported"); } QVariant RegionWeatherAdapter::getData(const Record& record, int subRowIndex, int subColIndex) const { const char* WeatherNames[] = { "Clear", "Cloudy", "Fog", "Overcast", "Rain", "Thunder", "Ash", "Blight", "Snow", "Blizzard" }; const ESM::Region& region = record.get(); if (subColIndex == 0 && subRowIndex >= 0 && subRowIndex < 10) { return WeatherNames[subRowIndex]; } else if (subColIndex == 1) { switch (subRowIndex) { case 0: return region.mData.mClear; case 1: return region.mData.mCloudy; case 2: return region.mData.mFoggy; case 3: return region.mData.mOvercast; case 4: return region.mData.mRain; case 5: return region.mData.mThunder; case 6: return region.mData.mAsh; case 7: return region.mData.mBlight; case 8: return region.mData.mA; // Snow case 9: return region.mData.mB; // Blizzard default: break; } } throw std::runtime_error("index out of range"); } void RegionWeatherAdapter::setData(Record& record, const QVariant& value, int subRowIndex, int subColIndex) const { ESM::Region region = record.get(); unsigned char chance = static_cast(value.toInt()); if (subColIndex == 1) { switch (subRowIndex) { case 0: region.mData.mClear = chance; break; case 1: region.mData.mCloudy = chance; break; case 2: region.mData.mFoggy = chance; break; case 3: region.mData.mOvercast = chance; break; case 4: region.mData.mRain = chance; break; case 5: region.mData.mThunder = chance; break; case 6: region.mData.mAsh = chance; break; case 7: region.mData.mBlight = chance; break; case 8: region.mData.mA = chance; break; case 9: region.mData.mB = chance; break; default: throw std::runtime_error("index out of range"); } record.setModified (region); } } int RegionWeatherAdapter::getColumnsCount(const Record& record) const { return 2; } int RegionWeatherAdapter::getRowsCount(const Record& record) const { return 10; } FactionRanksAdapter::FactionRanksAdapter () {} void FactionRanksAdapter::addRow(Record& record, int position) const { throw std::logic_error ("cannot add a row to a fixed table"); } void FactionRanksAdapter::removeRow(Record& record, int rowToRemove) const { throw std::logic_error ("cannot remove a row from a fixed table"); } void FactionRanksAdapter::setTable(Record& record, const NestedTableWrapperBase& nestedTable) const { throw std::logic_error ("table operation not supported"); } NestedTableWrapperBase* FactionRanksAdapter::table(const Record& record) const { throw std::logic_error ("table operation not supported"); } QVariant FactionRanksAdapter::getData(const Record& record, int subRowIndex, int subColIndex) const { ESM::Faction faction = record.get(); if (subRowIndex < 0 || subRowIndex >= static_cast(sizeof(faction.mData.mRankData)/sizeof(faction.mData.mRankData[0]))) throw std::runtime_error ("index out of range"); auto& rankData = faction.mData.mRankData[subRowIndex]; switch (subColIndex) { case 0: return QString(faction.mRanks[subRowIndex].c_str()); case 1: return rankData.mAttribute1; case 2: return rankData.mAttribute2; case 3: return rankData.mPrimarySkill; case 4: return rankData.mFavouredSkill; case 5: return rankData.mFactReaction; default: throw std::runtime_error("Rank subcolumn index out of range"); } } void FactionRanksAdapter::setData(Record& record, const QVariant& value, int subRowIndex, int subColIndex) const { ESM::Faction faction = record.get(); if (subRowIndex < 0 || subRowIndex >= static_cast(sizeof(faction.mData.mRankData)/sizeof(faction.mData.mRankData[0]))) throw std::runtime_error ("index out of range"); auto& rankData = faction.mData.mRankData[subRowIndex]; switch (subColIndex) { case 0: faction.mRanks[subRowIndex] = value.toString().toUtf8().constData(); break; case 1: rankData.mAttribute1 = value.toInt(); break; case 2: rankData.mAttribute2 = value.toInt(); break; case 3: rankData.mPrimarySkill = value.toInt(); break; case 4: rankData.mFavouredSkill = value.toInt(); break; case 5: rankData.mFactReaction = value.toInt(); break; default: throw std::runtime_error("Rank index out of range"); } record.setModified (faction); } int FactionRanksAdapter::getColumnsCount(const Record& record) const { return 6; } int FactionRanksAdapter::getRowsCount(const Record& record) const { return 10; } } ================================================ FILE: apps/opencs/model/world/nestedcoladapterimp.hpp ================================================ #ifndef CSM_WOLRD_NESTEDCOLADAPTERIMP_H #define CSM_WOLRD_NESTEDCOLADAPTERIMP_H #include #include #include #include // for converting magic effect id to string & back #include // for converting skill names #include // for converting attributes #include #include "nestedcolumnadapter.hpp" #include "nestedtablewrapper.hpp" #include "cell.hpp" namespace ESM { struct Faction; struct Region; } namespace CSMWorld { struct Pathgrid; struct Info; class PathgridPointListAdapter : public NestedColumnAdapter { public: PathgridPointListAdapter (); void addRow(Record& record, int position) const override; void removeRow(Record& record, int rowToRemove) const override; void setTable(Record& record, const NestedTableWrapperBase& nestedTable) const override; NestedTableWrapperBase* table(const Record& record) const override; QVariant getData(const Record& record, int subRowIndex, int subColIndex) const override; void setData(Record& record, const QVariant& value, int subRowIndex, int subColIndex) const override; int getColumnsCount(const Record& record) const override; int getRowsCount(const Record& record) const override; }; class PathgridEdgeListAdapter : public NestedColumnAdapter { public: PathgridEdgeListAdapter (); void addRow(Record& record, int position) const override; void removeRow(Record& record, int rowToRemove) const override; void setTable(Record& record, const NestedTableWrapperBase& nestedTable) const override; NestedTableWrapperBase* table(const Record& record) const override; QVariant getData(const Record& record, int subRowIndex, int subColIndex) const override; void setData(Record& record, const QVariant& value, int subRowIndex, int subColIndex) const override; int getColumnsCount(const Record& record) const override; int getRowsCount(const Record& record) const override; }; class FactionReactionsAdapter : public NestedColumnAdapter { public: FactionReactionsAdapter (); void addRow(Record& record, int position) const override; void removeRow(Record& record, int rowToRemove) const override; void setTable(Record& record, const NestedTableWrapperBase& nestedTable) const override; NestedTableWrapperBase* table(const Record& record) const override; QVariant getData(const Record& record, int subRowIndex, int subColIndex) const override; void setData(Record& record, const QVariant& value, int subRowIndex, int subColIndex) const override; int getColumnsCount(const Record& record) const override; int getRowsCount(const Record& record) const override; }; class FactionRanksAdapter : public NestedColumnAdapter { public: FactionRanksAdapter (); void addRow(Record& record, int position) const override; void removeRow(Record& record, int rowToRemove) const override; void setTable(Record& record, const NestedTableWrapperBase& nestedTable) const override; NestedTableWrapperBase* table(const Record& record) const override; QVariant getData(const Record& record, int subRowIndex, int subColIndex) const override; void setData(Record& record, const QVariant& value, int subRowIndex, int subColIndex) const override; int getColumnsCount(const Record& record) const override; int getRowsCount(const Record& record) const override; }; class RegionSoundListAdapter : public NestedColumnAdapter { public: RegionSoundListAdapter (); void addRow(Record& record, int position) const override; void removeRow(Record& record, int rowToRemove) const override; void setTable(Record& record, const NestedTableWrapperBase& nestedTable) const override; NestedTableWrapperBase* table(const Record& record) const override; QVariant getData(const Record& record, int subRowIndex, int subColIndex) const override; void setData(Record& record, const QVariant& value, int subRowIndex, int subColIndex) const override; int getColumnsCount(const Record& record) const override; int getRowsCount(const Record& record) const override; }; template class SpellListAdapter : public NestedColumnAdapter { public: SpellListAdapter () {} void addRow(Record& record, int position) const override { ESXRecordT raceOrBthSgn = record.get(); std::vector& spells = raceOrBthSgn.mPowers.mList; // blank row std::string spell = ""; spells.insert(spells.begin()+position, spell); record.setModified (raceOrBthSgn); } void removeRow(Record& record, int rowToRemove) const override { ESXRecordT raceOrBthSgn = record.get(); std::vector& spells = raceOrBthSgn.mPowers.mList; if (rowToRemove < 0 || rowToRemove >= static_cast (spells.size())) throw std::runtime_error ("index out of range"); spells.erase(spells.begin()+rowToRemove); record.setModified (raceOrBthSgn); } void setTable(Record& record, const NestedTableWrapperBase& nestedTable) const override { ESXRecordT raceOrBthSgn = record.get(); raceOrBthSgn.mPowers.mList = static_cast >&>(nestedTable).mNestedTable; record.setModified (raceOrBthSgn); } NestedTableWrapperBase* table(const Record& record) const override { // deleted by dtor of NestedTableStoring return new NestedTableWrapper >(record.get().mPowers.mList); } QVariant getData(const Record& record, int subRowIndex, int subColIndex) const override { ESXRecordT raceOrBthSgn = record.get(); std::vector& spells = raceOrBthSgn.mPowers.mList; if (subRowIndex < 0 || subRowIndex >= static_cast (spells.size())) throw std::runtime_error ("index out of range"); std::string spell = spells[subRowIndex]; switch (subColIndex) { case 0: return QString(spell.c_str()); default: throw std::runtime_error("Spells subcolumn index out of range"); } } void setData(Record& record, const QVariant& value, int subRowIndex, int subColIndex) const override { ESXRecordT raceOrBthSgn = record.get(); std::vector& spells = raceOrBthSgn.mPowers.mList; if (subRowIndex < 0 || subRowIndex >= static_cast (spells.size())) throw std::runtime_error ("index out of range"); std::string spell = spells[subRowIndex]; switch (subColIndex) { case 0: spell = value.toString().toUtf8().constData(); break; default: throw std::runtime_error("Spells subcolumn index out of range"); } raceOrBthSgn.mPowers.mList[subRowIndex] = spell; record.setModified (raceOrBthSgn); } int getColumnsCount(const Record& record) const override { return 1; } int getRowsCount(const Record& record) const override { return static_cast(record.get().mPowers.mList.size()); } }; template class EffectsListAdapter : public NestedColumnAdapter { public: EffectsListAdapter () {} void addRow(Record& record, int position) const override { ESXRecordT magic = record.get(); std::vector& effectsList = magic.mEffects.mList; // blank row ESM::ENAMstruct effect; effect.mEffectID = 0; effect.mSkill = -1; effect.mAttribute = -1; effect.mRange = 0; effect.mArea = 0; effect.mDuration = 0; effect.mMagnMin = 0; effect.mMagnMax = 0; effectsList.insert(effectsList.begin()+position, effect); record.setModified (magic); } void removeRow(Record& record, int rowToRemove) const override { ESXRecordT magic = record.get(); std::vector& effectsList = magic.mEffects.mList; if (rowToRemove < 0 || rowToRemove >= static_cast (effectsList.size())) throw std::runtime_error ("index out of range"); effectsList.erase(effectsList.begin()+rowToRemove); record.setModified (magic); } void setTable(Record& record, const NestedTableWrapperBase& nestedTable) const override { ESXRecordT magic = record.get(); magic.mEffects.mList = static_cast >&>(nestedTable).mNestedTable; record.setModified (magic); } NestedTableWrapperBase* table(const Record& record) const override { // deleted by dtor of NestedTableStoring return new NestedTableWrapper >(record.get().mEffects.mList); } QVariant getData(const Record& record, int subRowIndex, int subColIndex) const override { ESXRecordT magic = record.get(); std::vector& effectsList = magic.mEffects.mList; if (subRowIndex < 0 || subRowIndex >= static_cast (effectsList.size())) throw std::runtime_error ("index out of range"); ESM::ENAMstruct effect = effectsList[subRowIndex]; switch (subColIndex) { case 0: { if (effect.mEffectID >=0 && effect.mEffectID < ESM::MagicEffect::Length) return effect.mEffectID; else throw std::runtime_error("Magic effects ID unexpected value"); } case 1: { switch (effect.mEffectID) { case ESM::MagicEffect::DrainSkill: case ESM::MagicEffect::DamageSkill: case ESM::MagicEffect::RestoreSkill: case ESM::MagicEffect::FortifySkill: case ESM::MagicEffect::AbsorbSkill: return effect.mSkill; default: return QVariant(); } } case 2: { switch (effect.mEffectID) { case ESM::MagicEffect::DrainAttribute: case ESM::MagicEffect::DamageAttribute: case ESM::MagicEffect::RestoreAttribute: case ESM::MagicEffect::FortifyAttribute: case ESM::MagicEffect::AbsorbAttribute: return effect.mAttribute; default: return QVariant(); } } case 3: { if (effect.mRange >=0 && effect.mRange <=2) return effect.mRange; else throw std::runtime_error("Magic effects range unexpected value"); } case 4: return effect.mArea; case 5: return effect.mDuration; case 6: return effect.mMagnMin; case 7: return effect.mMagnMax; default: throw std::runtime_error("Magic Effects subcolumn index out of range"); } } void setData(Record& record, const QVariant& value, int subRowIndex, int subColIndex) const override { ESXRecordT magic = record.get(); std::vector& effectsList = magic.mEffects.mList; if (subRowIndex < 0 || subRowIndex >= static_cast (effectsList.size())) throw std::runtime_error ("index out of range"); ESM::ENAMstruct effect = effectsList[subRowIndex]; switch (subColIndex) { case 0: { effect.mEffectID = static_cast(value.toInt()); break; } case 1: { effect.mSkill = static_cast(value.toInt()); break; } case 2: { effect.mAttribute = static_cast(value.toInt()); break; } case 3: { effect.mRange = value.toInt(); break; } case 4: effect.mArea = value.toInt(); break; case 5: effect.mDuration = value.toInt(); break; case 6: effect.mMagnMin = value.toInt(); break; case 7: effect.mMagnMax = value.toInt(); break; default: throw std::runtime_error("Magic Effects subcolumn index out of range"); } magic.mEffects.mList[subRowIndex] = effect; record.setModified (magic); } int getColumnsCount(const Record& record) const override { return 8; } int getRowsCount(const Record& record) const override { return static_cast(record.get().mEffects.mList.size()); } }; class InfoListAdapter : public NestedColumnAdapter { public: InfoListAdapter (); void addRow(Record& record, int position) const override; void removeRow(Record& record, int rowToRemove) const override; void setTable(Record& record, const NestedTableWrapperBase& nestedTable) const override; NestedTableWrapperBase* table(const Record& record) const override; QVariant getData(const Record& record, int subRowIndex, int subColIndex) const override; void setData(Record& record, const QVariant& value, int subRowIndex, int subColIndex) const override; int getColumnsCount(const Record& record) const override; int getRowsCount(const Record& record) const override; }; class InfoConditionAdapter : public NestedColumnAdapter { public: InfoConditionAdapter (); void addRow(Record& record, int position) const override; void removeRow(Record& record, int rowToRemove) const override; void setTable(Record& record, const NestedTableWrapperBase& nestedTable) const override; NestedTableWrapperBase* table(const Record& record) const override; QVariant getData(const Record& record, int subRowIndex, int subColIndex) const override; void setData(Record& record, const QVariant& value, int subRowIndex, int subColIndex) const override; int getColumnsCount(const Record& record) const override; int getRowsCount(const Record& record) const override; }; class RaceAttributeAdapter : public NestedColumnAdapter { public: RaceAttributeAdapter (); void addRow(Record& record, int position) const override; void removeRow(Record& record, int rowToRemove) const override; void setTable(Record& record, const NestedTableWrapperBase& nestedTable) const override; NestedTableWrapperBase* table(const Record& record) const override; QVariant getData(const Record& record, int subRowIndex, int subColIndex) const override; void setData(Record& record, const QVariant& value, int subRowIndex, int subColIndex) const override; int getColumnsCount(const Record& record) const override; int getRowsCount(const Record& record) const override; }; class RaceSkillsBonusAdapter : public NestedColumnAdapter { public: RaceSkillsBonusAdapter (); void addRow(Record& record, int position) const override; void removeRow(Record& record, int rowToRemove) const override; void setTable(Record& record, const NestedTableWrapperBase& nestedTable) const override; NestedTableWrapperBase* table(const Record& record) const override; QVariant getData(const Record& record, int subRowIndex, int subColIndex) const override; void setData(Record& record, const QVariant& value, int subRowIndex, int subColIndex) const override; int getColumnsCount(const Record& record) const override; int getRowsCount(const Record& record) const override; }; class CellListAdapter : public NestedColumnAdapter { public: CellListAdapter (); void addRow(Record& record, int position) const override; void removeRow(Record& record, int rowToRemove) const override; void setTable(Record& record, const NestedTableWrapperBase& nestedTable) const override; NestedTableWrapperBase* table(const Record& record) const override; QVariant getData(const Record& record, int subRowIndex, int subColIndex) const override; void setData(Record& record, const QVariant& value, int subRowIndex, int subColIndex) const override; int getColumnsCount(const Record& record) const override; int getRowsCount(const Record& record) const override; }; class RegionWeatherAdapter : public NestedColumnAdapter { public: RegionWeatherAdapter (); void addRow(Record& record, int position) const override; void removeRow(Record& record, int rowToRemove) const override; void setTable(Record& record, const NestedTableWrapperBase& nestedTable) const override; NestedTableWrapperBase* table(const Record& record) const override; QVariant getData(const Record& record, int subRowIndex, int subColIndex) const override; void setData(Record& record, const QVariant& value, int subRowIndex, int subColIndex) const override; int getColumnsCount(const Record& record) const override; int getRowsCount(const Record& record) const override; }; } #endif // CSM_WOLRD_NESTEDCOLADAPTERIMP_H ================================================ FILE: apps/opencs/model/world/nestedcollection.cpp ================================================ #include "nestedcollection.hpp" CSMWorld::NestedCollection::NestedCollection() {} CSMWorld::NestedCollection::~NestedCollection() {} int CSMWorld::NestedCollection::getNestedRowsCount(int row, int column) const { return 0; } int CSMWorld::NestedCollection::getNestedColumnsCount(int row, int column) const { return 0; } int CSMWorld::NestedCollection::searchNestedColumnIndex(int parentColumn, Columns::ColumnId id) { // Assumed that the parentColumn is always a valid index const NestableColumn *parent = getNestableColumn(parentColumn); int nestedColumnCount = getNestedColumnsCount(0, parentColumn); for (int i = 0; i < nestedColumnCount; ++i) { if (parent->nestedColumn(i).mColumnId == id) { return i; } } return -1; } int CSMWorld::NestedCollection::findNestedColumnIndex(int parentColumn, Columns::ColumnId id) { int index = searchNestedColumnIndex(parentColumn, id); if (index == -1) { throw std::logic_error("CSMWorld::NestedCollection: No such nested column"); } return index; } ================================================ FILE: apps/opencs/model/world/nestedcollection.hpp ================================================ #ifndef CSM_WOLRD_NESTEDCOLLECTION_H #define CSM_WOLRD_NESTEDCOLLECTION_H #include "columns.hpp" class QVariant; namespace CSMWorld { class NestableColumn; struct NestedTableWrapperBase; class NestedCollection { public: NestedCollection(); virtual ~NestedCollection(); virtual void addNestedRow(int row, int col, int position) = 0; virtual void removeNestedRows(int row, int column, int subRow) = 0; virtual QVariant getNestedData(int row, int column, int subRow, int subColumn) const = 0; virtual void setNestedData(int row, int column, const QVariant& data, int subRow, int subColumn) = 0; virtual NestedTableWrapperBase* nestedTable(int row, int column) const = 0; virtual void setNestedTable(int row, int column, const NestedTableWrapperBase& nestedTable) = 0; virtual int getNestedRowsCount(int row, int column) const; virtual int getNestedColumnsCount(int row, int column) const; virtual NestableColumn *getNestableColumn(int column) = 0; virtual int searchNestedColumnIndex(int parentColumn, Columns::ColumnId id); ///< \return the column index or -1 if the requested column wasn't found. virtual int findNestedColumnIndex(int parentColumn, Columns::ColumnId id); ///< \return the column index or throws an exception if the requested column wasn't found. }; } #endif // CSM_WOLRD_NESTEDCOLLECTION_H ================================================ FILE: apps/opencs/model/world/nestedcolumnadapter.hpp ================================================ #ifndef CSM_WOLRD_NESTEDCOLUMNADAPTER_H #define CSM_WOLRD_NESTEDCOLUMNADAPTER_H class QVariant; namespace CSMWorld { struct NestedTableWrapperBase; template struct Record; template class NestedColumnAdapter { public: NestedColumnAdapter() {} virtual ~NestedColumnAdapter() {} virtual void addRow(Record& record, int position) const = 0; virtual void removeRow(Record& record, int rowToRemove) const = 0; virtual void setTable(Record& record, const NestedTableWrapperBase& nestedTable) const = 0; virtual NestedTableWrapperBase* table(const Record& record) const = 0; virtual QVariant getData(const Record& record, int subRowIndex, int subColIndex) const = 0; virtual void setData(Record& record, const QVariant& value, int subRowIndex, int subColIndex) const = 0; virtual int getColumnsCount(const Record& record) const = 0; virtual int getRowsCount(const Record& record) const = 0; }; } #endif // CSM_WOLRD_NESTEDCOLUMNADAPTER_H ================================================ FILE: apps/opencs/model/world/nestedidcollection.hpp ================================================ #ifndef CSM_WOLRD_NESTEDIDCOLLECTION_H #define CSM_WOLRD_NESTEDIDCOLLECTION_H #include #include #include "nestedcollection.hpp" #include "nestedcoladapterimp.hpp" namespace ESM { class ESMReader; } namespace CSMWorld { struct NestedTableWrapperBase; struct Cell; template class IdCollection; template > class NestedIdCollection : public IdCollection, public NestedCollection { std::map* > mAdapters; const NestedColumnAdapter& getAdapter(const ColumnBase &column) const; public: NestedIdCollection (); ~NestedIdCollection(); void addNestedRow(int row, int column, int position) override; void removeNestedRows(int row, int column, int subRow) override; QVariant getNestedData(int row, int column, int subRow, int subColumn) const override; void setNestedData(int row, int column, const QVariant& data, int subRow, int subColumn) override; NestedTableWrapperBase* nestedTable(int row, int column) const override; void setNestedTable(int row, int column, const NestedTableWrapperBase& nestedTable) override; int getNestedRowsCount(int row, int column) const override; int getNestedColumnsCount(int row, int column) const override; // this method is inherited from NestedCollection, not from Collection NestableColumn *getNestableColumn(int column) override; void addAdapter(std::pair* > adapter); }; template NestedIdCollection::NestedIdCollection () {} template NestedIdCollection::~NestedIdCollection() { for (typename std::map* >::iterator iter (mAdapters.begin()); iter!=mAdapters.end(); ++iter) { delete (*iter).second; } } template void NestedIdCollection::addAdapter(std::pair* > adapter) { mAdapters.insert(adapter); } template const NestedColumnAdapter& NestedIdCollection::getAdapter(const ColumnBase &column) const { typename std::map* >::const_iterator iter = mAdapters.find (&column); if (iter==mAdapters.end()) throw std::logic_error("No such column in the nestedidadapter"); return *iter->second; } template void NestedIdCollection::addNestedRow(int row, int column, int position) { Record record; record.assign(Collection::getRecord(row)); getAdapter(Collection::getColumn(column)).addRow(record, position); Collection::setRecord(row, record); } template void NestedIdCollection::removeNestedRows(int row, int column, int subRow) { Record record; record.assign(Collection::getRecord(row)); getAdapter(Collection::getColumn(column)).removeRow(record, subRow); Collection::setRecord(row, record); } template QVariant NestedIdCollection::getNestedData (int row, int column, int subRow, int subColumn) const { return getAdapter(Collection::getColumn(column)).getData( Collection::getRecord(row), subRow, subColumn); } template void NestedIdCollection::setNestedData(int row, int column, const QVariant& data, int subRow, int subColumn) { Record record; record.assign(Collection::getRecord(row)); getAdapter(Collection::getColumn(column)).setData( record, data, subRow, subColumn); Collection::setRecord(row, record); } template CSMWorld::NestedTableWrapperBase* NestedIdCollection::nestedTable(int row, int column) const { return getAdapter(Collection::getColumn(column)).table( Collection::getRecord(row)); } template void NestedIdCollection::setNestedTable(int row, int column, const CSMWorld::NestedTableWrapperBase& nestedTable) { Record record; record.assign(Collection::getRecord(row)); getAdapter(Collection::getColumn(column)).setTable( record, nestedTable); Collection::setRecord(row, record); } template int NestedIdCollection::getNestedRowsCount(int row, int column) const { return getAdapter(Collection::getColumn(column)).getRowsCount( Collection::getRecord(row)); } template int NestedIdCollection::getNestedColumnsCount(int row, int column) const { const ColumnBase &nestedColumn = Collection::getColumn(column); int numRecords = Collection::getSize(); if (row >= 0 && row < numRecords) { const Record& record = Collection::getRecord(row); return getAdapter(nestedColumn).getColumnsCount(record); } else { // If the row is invalid (or there no records), retrieve the column count using a blank record const Record record; return getAdapter(nestedColumn).getColumnsCount(record); } } template CSMWorld::NestableColumn *NestedIdCollection::getNestableColumn(int column) { return Collection::getNestableColumn(column); } } #endif // CSM_WOLRD_NESTEDIDCOLLECTION_H ================================================ FILE: apps/opencs/model/world/nestedinfocollection.cpp ================================================ #include "nestedinfocollection.hpp" #include "nestedcoladapterimp.hpp" namespace CSMWorld { NestedInfoCollection::NestedInfoCollection () {} NestedInfoCollection::~NestedInfoCollection() { for (std::map* >::iterator iter (mAdapters.begin()); iter!=mAdapters.end(); ++iter) { delete (*iter).second; } } void NestedInfoCollection::addAdapter(std::pair* > adapter) { mAdapters.insert(adapter); } const NestedColumnAdapter& NestedInfoCollection::getAdapter(const ColumnBase &column) const { std::map* >::const_iterator iter = mAdapters.find (&column); if (iter==mAdapters.end()) throw std::logic_error("No such column in the nestedidadapter"); return *iter->second; } void NestedInfoCollection::addNestedRow(int row, int column, int position) { Record record; record.assign(Collection >::getRecord(row)); getAdapter(Collection >::getColumn(column)).addRow(record, position); Collection >::setRecord(row, record); } void NestedInfoCollection::removeNestedRows(int row, int column, int subRow) { Record record; record.assign(Collection >::getRecord(row)); getAdapter(Collection >::getColumn(column)).removeRow(record, subRow); Collection >::setRecord(row, record); } QVariant NestedInfoCollection::getNestedData (int row, int column, int subRow, int subColumn) const { return getAdapter(Collection >::getColumn(column)).getData( Collection >::getRecord(row), subRow, subColumn); } void NestedInfoCollection::setNestedData(int row, int column, const QVariant& data, int subRow, int subColumn) { Record record; record.assign(Collection >::getRecord(row)); getAdapter(Collection >::getColumn(column)).setData( record, data, subRow, subColumn); Collection >::setRecord(row, record); } CSMWorld::NestedTableWrapperBase* NestedInfoCollection::nestedTable(int row, int column) const { return getAdapter(Collection >::getColumn(column)).table( Collection >::getRecord(row)); } void NestedInfoCollection::setNestedTable(int row, int column, const CSMWorld::NestedTableWrapperBase& nestedTable) { Record record; record.assign(Collection >::getRecord(row)); getAdapter(Collection >::getColumn(column)).setTable( record, nestedTable); Collection >::setRecord(row, record); } int NestedInfoCollection::getNestedRowsCount(int row, int column) const { return getAdapter(Collection >::getColumn(column)).getRowsCount( Collection >::getRecord(row)); } int NestedInfoCollection::getNestedColumnsCount(int row, int column) const { return getAdapter(Collection >::getColumn(column)).getColumnsCount( Collection >::getRecord(row)); } CSMWorld::NestableColumn *NestedInfoCollection::getNestableColumn(int column) { return Collection >::getNestableColumn(column); } } ================================================ FILE: apps/opencs/model/world/nestedinfocollection.hpp ================================================ #ifndef CSM_WOLRD_NESTEDINFOCOLLECTION_H #define CSM_WOLRD_NESTEDINFOCOLLECTION_H #include #include "infocollection.hpp" #include "nestedcollection.hpp" namespace CSMWorld { struct NestedTableWrapperBase; template class NestedColumnAdapter; class NestedInfoCollection : public InfoCollection, public NestedCollection { std::map* > mAdapters; const NestedColumnAdapter& getAdapter(const ColumnBase &column) const; public: NestedInfoCollection (); ~NestedInfoCollection(); void addNestedRow(int row, int column, int position) override; void removeNestedRows(int row, int column, int subRow) override; QVariant getNestedData(int row, int column, int subRow, int subColumn) const override; void setNestedData(int row, int column, const QVariant& data, int subRow, int subColumn) override; NestedTableWrapperBase* nestedTable(int row, int column) const override; void setNestedTable(int row, int column, const NestedTableWrapperBase& nestedTable) override; int getNestedRowsCount(int row, int column) const override; int getNestedColumnsCount(int row, int column) const override; // this method is inherited from NestedCollection, not from Collection > NestableColumn *getNestableColumn(int column) override; void addAdapter(std::pair* > adapter); }; } #endif // CSM_WOLRD_NESTEDINFOCOLLECTION_H ================================================ FILE: apps/opencs/model/world/nestedtableproxymodel.cpp ================================================ #include "nestedtableproxymodel.hpp" #include #include "idtree.hpp" CSMWorld::NestedTableProxyModel::NestedTableProxyModel(const QModelIndex& parent, ColumnBase::Display columnId, CSMWorld::IdTree* parentModel) : mParentColumn(parent.column()), mMainModel(parentModel) { const int parentRow = parent.row(); mId = std::string(parentModel->index(parentRow, 0).data().toString().toUtf8()); QAbstractProxyModel::setSourceModel(parentModel); connect(mMainModel, SIGNAL(rowsAboutToBeInserted(const QModelIndex &, int, int)), this, SLOT(forwardRowsAboutToInserted(const QModelIndex &, int, int))); connect(mMainModel, SIGNAL(rowsInserted(const QModelIndex &, int, int)), this, SLOT(forwardRowsInserted(const QModelIndex &, int, int))); connect(mMainModel, SIGNAL(rowsAboutToBeRemoved(const QModelIndex &, int, int)), this, SLOT(forwardRowsAboutToRemoved(const QModelIndex &, int, int))); connect(mMainModel, SIGNAL(rowsRemoved(const QModelIndex &, int, int)), this, SLOT(forwardRowsRemoved(const QModelIndex &, int, int))); connect(mMainModel, SIGNAL(resetStart(const QString&)), this, SLOT(forwardResetStart(const QString&))); connect(mMainModel, SIGNAL(resetEnd(const QString&)), this, SLOT(forwardResetEnd(const QString&))); connect(mMainModel, SIGNAL(dataChanged(const QModelIndex &, const QModelIndex &)), this, SLOT(forwardDataChanged(const QModelIndex &, const QModelIndex &))); } QModelIndex CSMWorld::NestedTableProxyModel::mapFromSource(const QModelIndex& sourceIndex) const { const QModelIndex& testedParent = mMainModel->parent(sourceIndex); const QModelIndex& parent = mMainModel->getNestedModelIndex (mId, mParentColumn); if (testedParent == parent) { return createIndex(sourceIndex.row(), sourceIndex.column()); } else { return QModelIndex(); } } QModelIndex CSMWorld::NestedTableProxyModel::mapToSource(const QModelIndex& proxyIndex) const { const QModelIndex& parent = mMainModel->getNestedModelIndex (mId, mParentColumn); return mMainModel->index(proxyIndex.row(), proxyIndex.column(), parent); } int CSMWorld::NestedTableProxyModel::rowCount(const QModelIndex& index) const { assert (!index.isValid()); return mMainModel->rowCount(mMainModel->getModelIndex(mId, mParentColumn)); } int CSMWorld::NestedTableProxyModel::columnCount(const QModelIndex& parent) const { assert (!parent.isValid()); return mMainModel->columnCount(mMainModel->getModelIndex(mId, mParentColumn)); } QModelIndex CSMWorld::NestedTableProxyModel::index(int row, int column, const QModelIndex& parent) const { assert (!parent.isValid()); int numRows = rowCount(parent); int numColumns = columnCount(parent); if (row < 0 || row >= numRows || column < 0 || column >= numColumns) return QModelIndex(); return createIndex(row, column); } QModelIndex CSMWorld::NestedTableProxyModel::parent(const QModelIndex& index) const { return QModelIndex(); } QVariant CSMWorld::NestedTableProxyModel::headerData(int section, Qt::Orientation orientation, int role) const { return mMainModel->nestedHeaderData(mParentColumn, section, orientation, role); } QVariant CSMWorld::NestedTableProxyModel::data(const QModelIndex& index, int role) const { return mMainModel->data(mapToSource(index), role); } // NOTE: Due to mapToSouce(index) the dataChanged() signal resulting from setData() will have the // source model's index values. The indicies need to be converted to the proxy space values. // See forwardDataChanged() bool CSMWorld::NestedTableProxyModel::setData (const QModelIndex & index, const QVariant & value, int role) { return mMainModel->setData(mapToSource(index), value, role); } Qt::ItemFlags CSMWorld::NestedTableProxyModel::flags(const QModelIndex& index) const { return mMainModel->flags(mapToSource(index)); } std::string CSMWorld::NestedTableProxyModel::getParentId() const { return mId; } int CSMWorld::NestedTableProxyModel::getParentColumn() const { return mParentColumn; } CSMWorld::IdTree* CSMWorld::NestedTableProxyModel::model() const { return mMainModel; } void CSMWorld::NestedTableProxyModel::forwardRowsAboutToInserted(const QModelIndex& parent, int first, int last) { if (indexIsParent(parent)) { beginInsertRows(QModelIndex(), first, last); } } void CSMWorld::NestedTableProxyModel::forwardRowsInserted(const QModelIndex& parent, int first, int last) { if (indexIsParent(parent)) { endInsertRows(); } } bool CSMWorld::NestedTableProxyModel::indexIsParent(const QModelIndex& index) { return (index.isValid() && index.column() == mParentColumn && mMainModel->data(mMainModel->index(index.row(), 0)).toString().toUtf8().constData() == mId); } void CSMWorld::NestedTableProxyModel::forwardRowsAboutToRemoved(const QModelIndex& parent, int first, int last) { if (indexIsParent(parent)) { beginRemoveRows(QModelIndex(), first, last); } } void CSMWorld::NestedTableProxyModel::forwardRowsRemoved(const QModelIndex& parent, int first, int last) { if (indexIsParent(parent)) { endRemoveRows(); } } void CSMWorld::NestedTableProxyModel::forwardResetStart(const QString& id) { if (id.toUtf8() == mId.c_str()) beginResetModel(); } void CSMWorld::NestedTableProxyModel::forwardResetEnd(const QString& id) { if (id.toUtf8() == mId.c_str()) endResetModel(); } void CSMWorld::NestedTableProxyModel::forwardDataChanged (const QModelIndex& topLeft, const QModelIndex& bottomRight) { const QModelIndex& parent = mMainModel->getNestedModelIndex (mId, mParentColumn); if (topLeft.column() <= parent.column() && bottomRight.column() >= parent.column()) { emit dataChanged(index(0,0), index(mMainModel->rowCount(parent)-1, mMainModel->columnCount(parent)-1)); } else if (topLeft.parent() == parent && bottomRight.parent() == parent) { emit dataChanged(index(topLeft.row(), topLeft.column()), index(bottomRight.row(), bottomRight.column())); } } ================================================ FILE: apps/opencs/model/world/nestedtableproxymodel.hpp ================================================ #ifndef CSM_WOLRD_NESTEDTABLEPROXYMODEL_H #define CSM_WOLRD_NESTEDTABLEPROXYMODEL_H #include #include #include "universalid.hpp" #include "columns.hpp" #include "columnbase.hpp" /*! \brief * Proxy model used to connect view in the dialogue into the nested columns of the main model. */ namespace CSMWorld { class CollectionBase; struct RecordBase; class IdTree; class NestedTableProxyModel : public QAbstractProxyModel { Q_OBJECT const int mParentColumn; IdTree* mMainModel; std::string mId; public: NestedTableProxyModel(const QModelIndex& parent, ColumnBase::Display displayType, IdTree* parentModel); //parent is the parent of columns to work with. Columnid provides information about the column std::string getParentId() const; int getParentColumn() const; CSMWorld::IdTree* model() const; QModelIndex mapFromSource(const QModelIndex& sourceIndex) const override; QModelIndex mapToSource(const QModelIndex& proxyIndex) const override; int rowCount(const QModelIndex& parent) const override; int columnCount(const QModelIndex& parent) const override; QModelIndex index(int row, int column, const QModelIndex& parent = QModelIndex()) const override; QModelIndex parent(const QModelIndex& index) const override; QVariant headerData (int section, Qt::Orientation orientation, int role) const override; QVariant data(const QModelIndex & index, int role = Qt::DisplayRole) const override; bool setData (const QModelIndex & index, const QVariant & value, int role = Qt::EditRole) override; Qt::ItemFlags flags(const QModelIndex& index) const override; private: void setupHeaderVectors(ColumnBase::Display columnId); bool indexIsParent(const QModelIndex& index); private slots: void forwardRowsAboutToInserted(const QModelIndex & parent, int first, int last); void forwardRowsInserted(const QModelIndex & parent, int first, int last); void forwardRowsAboutToRemoved(const QModelIndex & parent, int first, int last); void forwardRowsRemoved(const QModelIndex & parent, int first, int last); void forwardResetStart(const QString& id); void forwardResetEnd(const QString& id); void forwardDataChanged (const QModelIndex& topLeft, const QModelIndex& bottomRight); }; } #endif ================================================ FILE: apps/opencs/model/world/nestedtablewrapper.cpp ================================================ #include "nestedtablewrapper.hpp" CSMWorld::NestedTableWrapperBase::NestedTableWrapperBase() {} CSMWorld::NestedTableWrapperBase::~NestedTableWrapperBase() {} int CSMWorld::NestedTableWrapperBase::size() const { return -5; } ================================================ FILE: apps/opencs/model/world/nestedtablewrapper.hpp ================================================ #ifndef CSM_WOLRD_NESTEDTABLEWRAPPER_H #define CSM_WOLRD_NESTEDTABLEWRAPPER_H namespace CSMWorld { struct NestedTableWrapperBase { virtual ~NestedTableWrapperBase(); virtual int size() const; NestedTableWrapperBase(); }; template struct NestedTableWrapper : public NestedTableWrapperBase { NestedTable mNestedTable; NestedTableWrapper(const NestedTable& nestedTable) : mNestedTable(nestedTable) {} virtual ~NestedTableWrapper() {} int size() const override { return mNestedTable.size(); //i hope that this will be enough } }; } #endif ================================================ FILE: apps/opencs/model/world/pathgrid.cpp ================================================ #include "cell.hpp" #include "idcollection.hpp" #include "pathgrid.hpp" #include void CSMWorld::Pathgrid::load (ESM::ESMReader &esm, bool &isDeleted, const IdCollection& cells) { load (esm, isDeleted); // correct ID if (!mId.empty() && mId[0]!='#' && cells.searchId (mId)==-1) { std::ostringstream stream; stream << "#" << mData.mX << " " << mData.mY; mId = stream.str(); } } void CSMWorld::Pathgrid::load (ESM::ESMReader &esm, bool &isDeleted) { ESM::Pathgrid::load (esm, isDeleted); mId = mCell; if (mCell.empty()) { std::ostringstream stream; stream << "#" << mData.mX << " " << mData.mY; mId = stream.str(); } } ================================================ FILE: apps/opencs/model/world/pathgrid.hpp ================================================ #ifndef CSM_WOLRD_PATHGRID_H #define CSM_WOLRD_PATHGRID_H #include #include #include namespace CSMWorld { struct Cell; template class IdCollection; /// \brief Wrapper for Pathgrid record /// /// \attention The mData.mX and mData.mY fields of the ESM::Pathgrid struct are not used. /// Exterior cell coordinates are encoded in the pathgrid ID. struct Pathgrid : public ESM::Pathgrid { std::string mId; void load (ESM::ESMReader &esm, bool &isDeleted, const IdCollection& cells); void load (ESM::ESMReader &esm, bool &isDeleted); }; } #endif ================================================ FILE: apps/opencs/model/world/record.cpp ================================================ #include "record.hpp" CSMWorld::RecordBase::~RecordBase() {} bool CSMWorld::RecordBase::isDeleted() const { return mState==State_Deleted || mState==State_Erased; } bool CSMWorld::RecordBase::isErased() const { return mState==State_Erased; } bool CSMWorld::RecordBase::isModified() const { return mState==State_Modified || mState==State_ModifiedOnly; } ================================================ FILE: apps/opencs/model/world/record.hpp ================================================ #ifndef CSM_WOLRD_RECORD_H #define CSM_WOLRD_RECORD_H #include namespace CSMWorld { struct RecordBase { enum State { State_BaseOnly = 0, // defined in base only State_Modified = 1, // exists in base, but has been modified State_ModifiedOnly = 2, // newly created in modified State_Deleted = 3, // exists in base, but has been deleted State_Erased = 4 // does not exist at all (we mostly treat that the same way as deleted) }; State mState; virtual ~RecordBase(); virtual RecordBase *clone() const = 0; virtual RecordBase *modifiedCopy() const = 0; virtual void assign (const RecordBase& record) = 0; ///< Will throw an exception if the types don't match. bool isDeleted() const; bool isErased() const; bool isModified() const; }; template struct Record : public RecordBase { ESXRecordT mBase; ESXRecordT mModified; Record(); Record(State state, const ESXRecordT *base = 0, const ESXRecordT *modified = 0); RecordBase *clone() const override; RecordBase *modifiedCopy() const override; void assign (const RecordBase& record) override; const ESXRecordT& get() const; ///< Throws an exception, if the record is deleted. ESXRecordT& get(); ///< Throws an exception, if the record is deleted. const ESXRecordT& getBase() const; ///< Throws an exception, if the record is deleted. Returns modified, if there is no base. void setModified (const ESXRecordT& modified); ///< Throws an exception, if the record is deleted. void merge(); ///< Merge modified into base. }; template Record::Record() : mBase(), mModified() { } template Record::Record(State state, const ESXRecordT *base, const ESXRecordT *modified) { if(base) mBase = *base; if(modified) mModified = *modified; this->mState = state; } template RecordBase *Record::modifiedCopy() const { return new Record (State_ModifiedOnly, nullptr, &(this->get())); } template RecordBase *Record::clone() const { return new Record (*this); } template void Record::assign (const RecordBase& record) { *this = dynamic_cast& > (record); } template const ESXRecordT& Record::get() const { if (mState==State_Erased) throw std::logic_error ("attempt to access a deleted record"); return mState==State_BaseOnly || mState==State_Deleted ? mBase : mModified; } template ESXRecordT& Record::get() { if (mState==State_Erased) throw std::logic_error ("attempt to access a deleted record"); return mState==State_BaseOnly || mState==State_Deleted ? mBase : mModified; } template const ESXRecordT& Record::getBase() const { if (mState==State_Erased) throw std::logic_error ("attempt to access a deleted record"); return mState==State_ModifiedOnly ? mModified : mBase; } template void Record::setModified (const ESXRecordT& modified) { if (mState==State_Erased) throw std::logic_error ("attempt to modify a deleted record"); mModified = modified; if (mState!=State_ModifiedOnly) mState = State_Modified; } template void Record::merge() { if (isModified()) { mBase = mModified; mState = State_BaseOnly; } else if (mState==State_Deleted) { mState = State_Erased; } } } #endif ================================================ FILE: apps/opencs/model/world/ref.cpp ================================================ #include "ref.hpp" #include "cellcoordinates.hpp" CSMWorld::CellRef::CellRef() : mNew (true) { mRefNum.mIndex = 0; mRefNum.mContentFile = 0; } std::pair CSMWorld::CellRef::getCellIndex() const { return CellCoordinates::coordinatesToCellIndex (mPos.pos[0], mPos.pos[1]); } ================================================ FILE: apps/opencs/model/world/ref.hpp ================================================ #ifndef CSM_WOLRD_REF_H #define CSM_WOLRD_REF_H #include #include namespace CSMWorld { /// \brief Wrapper for CellRef sub record struct CellRef : public ESM::CellRef { std::string mId; std::string mCell; std::string mOriginalCell; bool mNew; // new reference, not counted yet, ref num not assigned yet CellRef(); /// Calculate cell index based on coordinates (x and y) std::pair getCellIndex() const; }; } #endif ================================================ FILE: apps/opencs/model/world/refcollection.cpp ================================================ #include "refcollection.hpp" #include #include "ref.hpp" #include "cell.hpp" #include "universalid.hpp" #include "record.hpp" void CSMWorld::RefCollection::load (ESM::ESMReader& reader, int cellIndex, bool base, std::map& cache, CSMDoc::Messages& messages) { Record cell = mCells.getRecord (cellIndex); Cell& cell2 = base ? cell.mBase : cell.mModified; CellRef ref; ref.mNew = false; ESM::MovedCellRef mref; mref.mRefNum.mIndex = 0; bool isDeleted = false; while (ESM::Cell::getNextRef(reader, ref, isDeleted, true, &mref)) { // Keep mOriginalCell empty when in modified (as an indicator that the // original cell will always be equal the current cell). ref.mOriginalCell = base ? cell2.mId : ""; if (cell.get().isExterior()) { // Autocalculate the cell index from coordinates first std::pair index = ref.getCellIndex(); ref.mCell = "#" + std::to_string(index.first) + " " + std::to_string(index.second); // Handle non-base moved references if (!base && mref.mRefNum.mIndex != 0) { // Moved references must have a link back to their original cell // See discussion: https://forum.openmw.org/viewtopic.php?f=6&t=577&start=30 ref.mOriginalCell = cell2.mId; // Some mods may move references outside of the bounds, which often happens they are deleted. // This results in nonsensical autocalculated cell IDs, so we must use the record target cell. // Log a warning if the record target cell is different if (index.first != mref.mTarget[0] || index.second != mref.mTarget[1]) { std::string indexCell = ref.mCell; ref.mCell = "#" + std::to_string(mref.mTarget[0]) + " " + std::to_string(mref.mTarget[1]); CSMWorld::UniversalId id(CSMWorld::UniversalId::Type_Cell, mCells.getId (cellIndex)); messages.add(id, "The position of the moved reference " + ref.mRefID + " (cell " + indexCell + ")" " does not match the target cell (" + ref.mCell + ")", std::string(), CSMDoc::Message::Severity_Warning); } } } else ref.mCell = cell2.mId; mref.mRefNum.mIndex = 0; // ignore content file number std::map::iterator iter = cache.begin(); unsigned int thisIndex = ref.mRefNum.mIndex & 0x00ffffff; if (ref.mRefNum.mContentFile != -1 && !base) ref.mRefNum.mContentFile = ref.mRefNum.mIndex >> 24; for (; iter != cache.end(); ++iter) { if (thisIndex == iter->first.mIndex) break; } if (isDeleted) { if (iter==cache.end()) { CSMWorld::UniversalId id (CSMWorld::UniversalId::Type_Cell, mCells.getId (cellIndex)); messages.add (id, "Attempt to delete a non-existent reference"); continue; } int index = getIndex (iter->second); Record record = getRecord (index); if (base) { removeRows (index, 1); cache.erase (iter); } else { record.mState = RecordBase::State_Deleted; setRecord (index, record); } continue; } if (iter==cache.end()) { // new reference ref.mId = getNewId(); Record record; record.mState = base ? RecordBase::State_BaseOnly : RecordBase::State_ModifiedOnly; const ESM::RefNum refNum = ref.mRefNum; std::string refId = ref.mId; (base ? record.mBase : record.mModified) = std::move(ref); appendRecord (record); cache.emplace(refNum, std::move(refId)); } else { // old reference -> merge ref.mId = iter->second; int index = getIndex (ref.mId); Record record = getRecord (index); record.mState = base ? RecordBase::State_BaseOnly : RecordBase::State_Modified; (base ? record.mBase : record.mModified) = std::move(ref); setRecord (index, record); } } } std::string CSMWorld::RefCollection::getNewId() { return "ref#" + std::to_string(mNextId++); } ================================================ FILE: apps/opencs/model/world/refcollection.hpp ================================================ #ifndef CSM_WOLRD_REFCOLLECTION_H #define CSM_WOLRD_REFCOLLECTION_H #include #include "../doc/stage.hpp" #include "collection.hpp" #include "ref.hpp" #include "record.hpp" namespace CSMWorld { struct Cell; class UniversalId; /// \brief References in cells class RefCollection : public Collection { Collection& mCells; int mNextId; public: // MSVC needs the constructor for a class inheriting a template to be defined in header RefCollection (Collection& cells) : mCells (cells), mNextId (0) {} void load (ESM::ESMReader& reader, int cellIndex, bool base, std::map& cache, CSMDoc::Messages& messages); ///< Load a sequence of references. std::string getNewId(); }; } #endif ================================================ FILE: apps/opencs/model/world/refidadapter.cpp ================================================ #include "refidadapter.hpp" CSMWorld::RefIdAdapter::RefIdAdapter() {} CSMWorld::RefIdAdapter::~RefIdAdapter() {} CSMWorld::NestedRefIdAdapterBase::NestedRefIdAdapterBase() {} CSMWorld::NestedRefIdAdapterBase::~NestedRefIdAdapterBase() {} ================================================ FILE: apps/opencs/model/world/refidadapter.hpp ================================================ #ifndef CSM_WOLRD_REFIDADAPTER_H #define CSM_WOLRD_REFIDADAPTER_H #include #include /*! \brief * Adapters acts as indirection layer, abstracting details of the record types (in the wrappers) from the higher levels of model. * Please notice that nested adaptor uses helper classes for actually performing any actions. Different record types require different helpers (needs to be created in the subclass and then fetched via member function). * * Important point: don't forget to make sure that getData on the nestedColumn returns true (otherwise code will not treat the index pointing to the column as having children! */ class QVariant; namespace CSMWorld { class RefIdColumn; class RefIdData; struct RecordBase; struct NestedTableWrapperBase; class HelperBase; class RefIdAdapter { // not implemented RefIdAdapter (const RefIdAdapter&); RefIdAdapter& operator= (const RefIdAdapter&); public: RefIdAdapter(); virtual ~RefIdAdapter(); virtual QVariant getData (const RefIdColumn *column, const RefIdData& data, int idnex) const = 0; ///< If called on the nest column, should return QVariant(true). virtual void setData (const RefIdColumn *column, RefIdData& data, int index, const QVariant& value) const = 0; ///< If the data type does not match an exception is thrown. virtual std::string getId (const RecordBase& record) const = 0; virtual void setId(RecordBase& record, const std::string& id) = 0; // used by RefIdCollection::cloneRecord() }; class NestedRefIdAdapterBase { public: NestedRefIdAdapterBase(); virtual ~NestedRefIdAdapterBase(); virtual void setNestedData (const RefIdColumn *column, RefIdData& data, int row, const QVariant& value, int subRowIndex, int subColIndex) const = 0; virtual QVariant getNestedData (const RefIdColumn *column, const RefIdData& data, int index, int subRowIndex, int subColIndex) const = 0; virtual int getNestedColumnsCount(const RefIdColumn *column, const RefIdData& data) const = 0; virtual int getNestedRowsCount(const RefIdColumn *column, const RefIdData& data, int index) const = 0; virtual void removeNestedRow (const RefIdColumn *column, RefIdData& data, int index, int rowToRemove) const = 0; virtual void addNestedRow (const RefIdColumn *column, RefIdData& data, int index, int position) const = 0; virtual void setNestedTable (const RefIdColumn* column, RefIdData& data, int index, const NestedTableWrapperBase& nestedTable) const = 0; virtual NestedTableWrapperBase* nestedTable (const RefIdColumn* column, const RefIdData& data, int index) const = 0; }; } #endif ================================================ FILE: apps/opencs/model/world/refidadapterimp.cpp ================================================ #include "refidadapterimp.hpp" #include #include #include #include #include #include "nestedtablewrapper.hpp" CSMWorld::PotionColumns::PotionColumns (const InventoryColumns& columns) : InventoryColumns (columns), mEffects(nullptr) {} CSMWorld::PotionRefIdAdapter::PotionRefIdAdapter (const PotionColumns& columns, const RefIdColumn *autoCalc) : InventoryRefIdAdapter (UniversalId::Type_Potion, columns), mColumns(columns), mAutoCalc (autoCalc) {} QVariant CSMWorld::PotionRefIdAdapter::getData (const RefIdColumn *column, const RefIdData& data, int index) const { const Record& record = static_cast&> ( data.getRecord (RefIdData::LocalIndex (index, UniversalId::Type_Potion))); if (column==mAutoCalc) return record.get().mData.mAutoCalc!=0; // to show nested tables in dialogue subview, see IdTree::hasChildren() if (column==mColumns.mEffects) return QVariant::fromValue(ColumnBase::TableEdit_Full); return InventoryRefIdAdapter::getData (column, data, index); } void CSMWorld::PotionRefIdAdapter::setData (const RefIdColumn *column, RefIdData& data, int index, const QVariant& value) const { Record& record = static_cast&> ( data.getRecord (RefIdData::LocalIndex (index, UniversalId::Type_Potion))); ESM::Potion potion = record.get(); if (column==mAutoCalc) potion.mData.mAutoCalc = value.toInt(); else { InventoryRefIdAdapter::setData (column, data, index, value); return; } record.setModified(potion); } CSMWorld::IngredientColumns::IngredientColumns (const InventoryColumns& columns) : InventoryColumns (columns) , mEffects(nullptr) {} CSMWorld::IngredientRefIdAdapter::IngredientRefIdAdapter (const IngredientColumns& columns) : InventoryRefIdAdapter (UniversalId::Type_Ingredient, columns), mColumns(columns) {} QVariant CSMWorld::IngredientRefIdAdapter::getData (const RefIdColumn *column, const RefIdData& data, int index) const { if (column==mColumns.mEffects) return QVariant::fromValue(ColumnBase::TableEdit_FixedRows); return InventoryRefIdAdapter::getData (column, data, index); } void CSMWorld::IngredientRefIdAdapter::setData (const RefIdColumn *column, RefIdData& data, int index, const QVariant& value) const { InventoryRefIdAdapter::setData (column, data, index, value); return; } CSMWorld::IngredEffectRefIdAdapter::IngredEffectRefIdAdapter() : mType(UniversalId::Type_Ingredient) {} CSMWorld::IngredEffectRefIdAdapter::~IngredEffectRefIdAdapter() {} void CSMWorld::IngredEffectRefIdAdapter::addNestedRow (const RefIdColumn *column, RefIdData& data, int index, int position) const { // Do nothing, this table cannot be changed by the user } void CSMWorld::IngredEffectRefIdAdapter::removeNestedRow (const RefIdColumn *column, RefIdData& data, int index, int rowToRemove) const { // Do nothing, this table cannot be changed by the user } void CSMWorld::IngredEffectRefIdAdapter::setNestedTable (const RefIdColumn* column, RefIdData& data, int index, const NestedTableWrapperBase& nestedTable) const { Record& record = static_cast&> (data.getRecord (RefIdData::LocalIndex (index, mType))); ESM::Ingredient ingredient = record.get(); ingredient.mData = static_cast >&>(nestedTable).mNestedTable.at(0); record.setModified (ingredient); } CSMWorld::NestedTableWrapperBase* CSMWorld::IngredEffectRefIdAdapter::nestedTable (const RefIdColumn* column, const RefIdData& data, int index) const { const Record& record = static_cast&> (data.getRecord (RefIdData::LocalIndex (index, mType))); // return the whole struct std::vector wrap; wrap.push_back(record.get().mData); // deleted by dtor of NestedTableStoring return new NestedTableWrapper >(wrap); } QVariant CSMWorld::IngredEffectRefIdAdapter::getNestedData (const RefIdColumn *column, const RefIdData& data, int index, int subRowIndex, int subColIndex) const { const Record& record = static_cast&> (data.getRecord (RefIdData::LocalIndex (index, mType))); if (subRowIndex < 0 || subRowIndex >= 4) throw std::runtime_error ("index out of range"); switch (subColIndex) { case 0: return record.get().mData.mEffectID[subRowIndex]; case 1: { switch (record.get().mData.mEffectID[subRowIndex]) { case ESM::MagicEffect::DrainSkill: case ESM::MagicEffect::DamageSkill: case ESM::MagicEffect::RestoreSkill: case ESM::MagicEffect::FortifySkill: case ESM::MagicEffect::AbsorbSkill: return record.get().mData.mSkills[subRowIndex]; default: return QVariant(); } } case 2: { switch (record.get().mData.mEffectID[subRowIndex]) { case ESM::MagicEffect::DrainAttribute: case ESM::MagicEffect::DamageAttribute: case ESM::MagicEffect::RestoreAttribute: case ESM::MagicEffect::FortifyAttribute: case ESM::MagicEffect::AbsorbAttribute: return record.get().mData.mAttributes[subRowIndex]; default: return QVariant(); } } default: throw std::runtime_error("Trying to access non-existing column in the nested table!"); } } void CSMWorld::IngredEffectRefIdAdapter::setNestedData (const RefIdColumn *column, RefIdData& data, int row, const QVariant& value, int subRowIndex, int subColIndex) const { Record& record = static_cast&> (data.getRecord (RefIdData::LocalIndex (row, mType))); ESM::Ingredient ingredient = record.get(); if (subRowIndex < 0 || subRowIndex >= 4) throw std::runtime_error ("index out of range"); switch(subColIndex) { case 0: ingredient.mData.mEffectID[subRowIndex] = value.toInt(); break; case 1: ingredient.mData.mSkills[subRowIndex] = value.toInt(); break; case 2: ingredient.mData.mAttributes[subRowIndex] = value.toInt(); break; default: throw std::runtime_error("Trying to access non-existing column in the nested table!"); } record.setModified (ingredient); } int CSMWorld::IngredEffectRefIdAdapter::getNestedColumnsCount(const RefIdColumn *column, const RefIdData& data) const { return 3; // effect, skill, attribute } int CSMWorld::IngredEffectRefIdAdapter::getNestedRowsCount(const RefIdColumn *column, const RefIdData& data, int index) const { return 4; // up to 4 effects } CSMWorld::ApparatusRefIdAdapter::ApparatusRefIdAdapter (const InventoryColumns& columns, const RefIdColumn *type, const RefIdColumn *quality) : InventoryRefIdAdapter (UniversalId::Type_Apparatus, columns), mType (type), mQuality (quality) {} QVariant CSMWorld::ApparatusRefIdAdapter::getData (const RefIdColumn *column, const RefIdData& data, int index) const { const Record& record = static_cast&> ( data.getRecord (RefIdData::LocalIndex (index, UniversalId::Type_Apparatus))); if (column==mType) return record.get().mData.mType; if (column==mQuality) return record.get().mData.mQuality; return InventoryRefIdAdapter::getData (column, data, index); } void CSMWorld::ApparatusRefIdAdapter::setData (const RefIdColumn *column, RefIdData& data, int index, const QVariant& value) const { Record& record = static_cast&> ( data.getRecord (RefIdData::LocalIndex (index, UniversalId::Type_Apparatus))); ESM::Apparatus apparatus = record.get(); if (column==mType) apparatus.mData.mType = value.toInt(); else if (column==mQuality) apparatus.mData.mQuality = value.toFloat(); else { InventoryRefIdAdapter::setData (column, data, index, value); return; } record.setModified(apparatus); } CSMWorld::ArmorRefIdAdapter::ArmorRefIdAdapter (const EnchantableColumns& columns, const RefIdColumn *type, const RefIdColumn *health, const RefIdColumn *armor, const RefIdColumn *partRef) : EnchantableRefIdAdapter (UniversalId::Type_Armor, columns), mType (type), mHealth (health), mArmor (armor), mPartRef(partRef) {} QVariant CSMWorld::ArmorRefIdAdapter::getData (const RefIdColumn *column, const RefIdData& data, int index) const { const Record& record = static_cast&> ( data.getRecord (RefIdData::LocalIndex (index, UniversalId::Type_Armor))); if (column==mType) return record.get().mData.mType; if (column==mHealth) return record.get().mData.mHealth; if (column==mArmor) return record.get().mData.mArmor; if (column==mPartRef) return QVariant::fromValue(ColumnBase::TableEdit_Full); return EnchantableRefIdAdapter::getData (column, data, index); } void CSMWorld::ArmorRefIdAdapter::setData (const RefIdColumn *column, RefIdData& data, int index, const QVariant& value) const { Record& record = static_cast&> ( data.getRecord (RefIdData::LocalIndex (index, UniversalId::Type_Armor))); ESM::Armor armor = record.get(); if (column==mType) armor.mData.mType = value.toInt(); else if (column==mHealth) armor.mData.mHealth = value.toInt(); else if (column==mArmor) armor.mData.mArmor = value.toInt(); else { EnchantableRefIdAdapter::setData (column, data, index, value); return; } record.setModified(armor); } CSMWorld::BookRefIdAdapter::BookRefIdAdapter (const EnchantableColumns& columns, const RefIdColumn *bookType, const RefIdColumn *skill, const RefIdColumn *text) : EnchantableRefIdAdapter (UniversalId::Type_Book, columns), mBookType (bookType), mSkill (skill), mText (text) {} QVariant CSMWorld::BookRefIdAdapter::getData (const RefIdColumn *column, const RefIdData& data, int index) const { const Record& record = static_cast&> ( data.getRecord (RefIdData::LocalIndex (index, UniversalId::Type_Book))); if (column==mBookType) return record.get().mData.mIsScroll; if (column==mSkill) return record.get().mData.mSkillId; if (column==mText) return QString::fromUtf8 (record.get().mText.c_str()); return EnchantableRefIdAdapter::getData (column, data, index); } void CSMWorld::BookRefIdAdapter::setData (const RefIdColumn *column, RefIdData& data, int index, const QVariant& value) const { Record& record = static_cast&> ( data.getRecord (RefIdData::LocalIndex (index, UniversalId::Type_Book))); ESM::Book book = record.get(); if (column==mBookType) book.mData.mIsScroll = value.toInt(); else if (column==mSkill) book.mData.mSkillId = value.toInt(); else if (column==mText) book.mText = value.toString().toUtf8().data(); else { EnchantableRefIdAdapter::setData (column, data, index, value); return; } record.setModified(book); } CSMWorld::ClothingRefIdAdapter::ClothingRefIdAdapter (const EnchantableColumns& columns, const RefIdColumn *type, const RefIdColumn *partRef) : EnchantableRefIdAdapter (UniversalId::Type_Clothing, columns), mType (type), mPartRef(partRef) {} QVariant CSMWorld::ClothingRefIdAdapter::getData (const RefIdColumn *column, const RefIdData& data, int index) const { const Record& record = static_cast&> ( data.getRecord (RefIdData::LocalIndex (index, UniversalId::Type_Clothing))); if (column==mType) return record.get().mData.mType; if (column==mPartRef) return QVariant::fromValue(ColumnBase::TableEdit_Full); return EnchantableRefIdAdapter::getData (column, data, index); } void CSMWorld::ClothingRefIdAdapter::setData (const RefIdColumn *column, RefIdData& data, int index, const QVariant& value) const { Record& record = static_cast&> ( data.getRecord (RefIdData::LocalIndex (index, UniversalId::Type_Clothing))); ESM::Clothing clothing = record.get(); if (column==mType) clothing.mData.mType = value.toInt(); else { EnchantableRefIdAdapter::setData (column, data, index, value); return; } record.setModified(clothing); } CSMWorld::ContainerRefIdAdapter::ContainerRefIdAdapter (const NameColumns& columns, const RefIdColumn *weight, const RefIdColumn *organic, const RefIdColumn *respawn, const RefIdColumn *content) : NameRefIdAdapter (UniversalId::Type_Container, columns), mWeight (weight), mOrganic (organic), mRespawn (respawn), mContent(content) {} QVariant CSMWorld::ContainerRefIdAdapter::getData (const RefIdColumn *column, const RefIdData& data, int index) const { const Record& record = static_cast&> ( data.getRecord (RefIdData::LocalIndex (index, UniversalId::Type_Container))); if (column==mWeight) return record.get().mWeight; if (column==mOrganic) return (record.get().mFlags & ESM::Container::Organic)!=0; if (column==mRespawn) return (record.get().mFlags & ESM::Container::Respawn)!=0; if (column==mContent) return QVariant::fromValue(ColumnBase::TableEdit_Full); return NameRefIdAdapter::getData (column, data, index); } void CSMWorld::ContainerRefIdAdapter::setData (const RefIdColumn *column, RefIdData& data, int index, const QVariant& value) const { Record& record = static_cast&> ( data.getRecord (RefIdData::LocalIndex (index, UniversalId::Type_Container))); ESM::Container container = record.get(); if (column==mWeight) container.mWeight = value.toFloat(); else if (column==mOrganic) { if (value.toInt()) container.mFlags |= ESM::Container::Organic; else container.mFlags &= ~ESM::Container::Organic; } else if (column==mRespawn) { if (value.toInt()) container.mFlags |= ESM::Container::Respawn; else container.mFlags &= ~ESM::Container::Respawn; } else { NameRefIdAdapter::setData (column, data, index, value); return; } record.setModified(container); } CSMWorld::CreatureColumns::CreatureColumns (const ActorColumns& actorColumns) : ActorColumns (actorColumns), mType(nullptr), mScale(nullptr), mOriginal(nullptr), mAttributes(nullptr), mAttacks(nullptr), mMisc(nullptr), mBloodType(nullptr) {} CSMWorld::CreatureRefIdAdapter::CreatureRefIdAdapter (const CreatureColumns& columns) : ActorRefIdAdapter (UniversalId::Type_Creature, columns), mColumns (columns) {} QVariant CSMWorld::CreatureRefIdAdapter::getData (const RefIdColumn *column, const RefIdData& data, int index) const { const Record& record = static_cast&> ( data.getRecord (RefIdData::LocalIndex (index, UniversalId::Type_Creature))); if (column==mColumns.mType) return record.get().mData.mType; if (column==mColumns.mScale) return record.get().mScale; if (column==mColumns.mOriginal) return QString::fromUtf8 (record.get().mOriginal.c_str()); if (column==mColumns.mAttributes) return QVariant::fromValue(ColumnBase::TableEdit_FixedRows); if (column==mColumns.mAttacks) return QVariant::fromValue(ColumnBase::TableEdit_FixedRows); if (column==mColumns.mMisc) return QVariant::fromValue(ColumnBase::TableEdit_Full); if (column == mColumns.mBloodType) return record.get().mBloodType; std::map::const_iterator iter = mColumns.mFlags.find (column); if (iter!=mColumns.mFlags.end()) return (record.get().mFlags & iter->second)!=0; return ActorRefIdAdapter::getData (column, data, index); } void CSMWorld::CreatureRefIdAdapter::setData (const RefIdColumn *column, RefIdData& data, int index, const QVariant& value) const { Record& record = static_cast&> ( data.getRecord (RefIdData::LocalIndex (index, UniversalId::Type_Creature))); ESM::Creature creature = record.get(); if (column==mColumns.mType) creature.mData.mType = value.toInt(); else if (column==mColumns.mScale) creature.mScale = value.toFloat(); else if (column==mColumns.mOriginal) creature.mOriginal = value.toString().toUtf8().constData(); else if (column == mColumns.mBloodType) creature.mBloodType = value.toInt(); else { std::map::const_iterator iter = mColumns.mFlags.find (column); if (iter!=mColumns.mFlags.end()) { if (value.toInt()!=0) creature.mFlags |= iter->second; else creature.mFlags &= ~iter->second; } else { ActorRefIdAdapter::setData (column, data, index, value); return; } } record.setModified(creature); } CSMWorld::DoorRefIdAdapter::DoorRefIdAdapter (const NameColumns& columns, const RefIdColumn *openSound, const RefIdColumn *closeSound) : NameRefIdAdapter (UniversalId::Type_Door, columns), mOpenSound (openSound), mCloseSound (closeSound) {} QVariant CSMWorld::DoorRefIdAdapter::getData (const RefIdColumn *column, const RefIdData& data, int index) const { const Record& record = static_cast&> ( data.getRecord (RefIdData::LocalIndex (index, UniversalId::Type_Door))); if (column==mOpenSound) return QString::fromUtf8 (record.get().mOpenSound.c_str()); if (column==mCloseSound) return QString::fromUtf8 (record.get().mCloseSound.c_str()); return NameRefIdAdapter::getData (column, data, index); } void CSMWorld::DoorRefIdAdapter::setData (const RefIdColumn *column, RefIdData& data, int index, const QVariant& value) const { Record& record = static_cast&> ( data.getRecord (RefIdData::LocalIndex (index, UniversalId::Type_Door))); ESM::Door door = record.get(); if (column==mOpenSound) door.mOpenSound = value.toString().toUtf8().constData(); else if (column==mCloseSound) door.mCloseSound = value.toString().toUtf8().constData(); else { NameRefIdAdapter::setData (column, data, index, value); return; } record.setModified(door); } CSMWorld::LightColumns::LightColumns (const InventoryColumns& columns) : InventoryColumns (columns) , mTime(nullptr) , mRadius(nullptr) , mColor(nullptr) , mSound(nullptr) , mEmitterType(nullptr) {} CSMWorld::LightRefIdAdapter::LightRefIdAdapter (const LightColumns& columns) : InventoryRefIdAdapter (UniversalId::Type_Light, columns), mColumns (columns) {} QVariant CSMWorld::LightRefIdAdapter::getData (const RefIdColumn *column, const RefIdData& data, int index) const { const Record& record = static_cast&> ( data.getRecord (RefIdData::LocalIndex (index, UniversalId::Type_Light))); if (column==mColumns.mTime) return record.get().mData.mTime; if (column==mColumns.mRadius) return record.get().mData.mRadius; if (column==mColumns.mColor) return record.get().mData.mColor; if (column==mColumns.mSound) return QString::fromUtf8 (record.get().mSound.c_str()); if (column == mColumns.mEmitterType) { int mask = ESM::Light::Flicker | ESM::Light::FlickerSlow | ESM::Light::Pulse | ESM::Light::PulseSlow; if ((record.get().mData.mFlags & mask) == ESM::Light::Flicker) return 1; if ((record.get().mData.mFlags & mask) == ESM::Light::FlickerSlow) return 2; if ((record.get().mData.mFlags & mask) == ESM::Light::Pulse) return 3; if ((record.get().mData.mFlags & mask) == ESM::Light::PulseSlow) return 4; return 0; } std::map::const_iterator iter = mColumns.mFlags.find (column); if (iter!=mColumns.mFlags.end()) return (record.get().mData.mFlags & iter->second)!=0; return InventoryRefIdAdapter::getData (column, data, index); } void CSMWorld::LightRefIdAdapter::setData (const RefIdColumn *column, RefIdData& data, int index, const QVariant& value) const { Record& record = static_cast&> ( data.getRecord (RefIdData::LocalIndex (index, UniversalId::Type_Light))); ESM::Light light = record.get(); if (column==mColumns.mTime) light.mData.mTime = value.toInt(); else if (column==mColumns.mRadius) light.mData.mRadius = value.toInt(); else if (column==mColumns.mColor) light.mData.mColor = value.toInt(); else if (column==mColumns.mSound) light.mSound = value.toString().toUtf8().constData(); else if (column == mColumns.mEmitterType) { int mask = ~(ESM::Light::Flicker | ESM::Light::FlickerSlow | ESM::Light::Pulse | ESM::Light::PulseSlow); if (value.toInt() == 0) light.mData.mFlags = light.mData.mFlags & mask; else if (value.toInt() == 1) light.mData.mFlags = (light.mData.mFlags & mask) | ESM::Light::Flicker; else if (value.toInt() == 2) light.mData.mFlags = (light.mData.mFlags & mask) | ESM::Light::FlickerSlow; else if (value.toInt() == 3) light.mData.mFlags = (light.mData.mFlags & mask) | ESM::Light::Pulse; else light.mData.mFlags = (light.mData.mFlags & mask) | ESM::Light::PulseSlow; } else { std::map::const_iterator iter = mColumns.mFlags.find (column); if (iter!=mColumns.mFlags.end()) { if (value.toInt()!=0) light.mData.mFlags |= iter->second; else light.mData.mFlags &= ~iter->second; } else { InventoryRefIdAdapter::setData (column, data, index, value); return; } } record.setModified (light); } CSMWorld::MiscRefIdAdapter::MiscRefIdAdapter (const InventoryColumns& columns, const RefIdColumn *key) : InventoryRefIdAdapter (UniversalId::Type_Miscellaneous, columns), mKey (key) {} QVariant CSMWorld::MiscRefIdAdapter::getData (const RefIdColumn *column, const RefIdData& data, int index) const { const Record& record = static_cast&> ( data.getRecord (RefIdData::LocalIndex (index, UniversalId::Type_Miscellaneous))); if (column==mKey) return record.get().mData.mIsKey!=0; return InventoryRefIdAdapter::getData (column, data, index); } void CSMWorld::MiscRefIdAdapter::setData (const RefIdColumn *column, RefIdData& data, int index, const QVariant& value) const { Record& record = static_cast&> ( data.getRecord (RefIdData::LocalIndex (index, UniversalId::Type_Miscellaneous))); ESM::Miscellaneous misc = record.get(); if (column==mKey) misc.mData.mIsKey = value.toInt(); else { InventoryRefIdAdapter::setData (column, data, index, value); return; } record.setModified(misc); } CSMWorld::NpcColumns::NpcColumns (const ActorColumns& actorColumns) : ActorColumns (actorColumns), mRace(nullptr), mClass(nullptr), mFaction(nullptr), mHair(nullptr), mHead(nullptr), mAttributes(nullptr), mSkills(nullptr), mMisc(nullptr), mBloodType(nullptr), mGender(nullptr) {} CSMWorld::NpcRefIdAdapter::NpcRefIdAdapter (const NpcColumns& columns) : ActorRefIdAdapter (UniversalId::Type_Npc, columns), mColumns (columns) {} QVariant CSMWorld::NpcRefIdAdapter::getData (const RefIdColumn *column, const RefIdData& data, int index) const { const Record& record = static_cast&> ( data.getRecord (RefIdData::LocalIndex (index, UniversalId::Type_Npc))); if (column==mColumns.mRace) return QString::fromUtf8 (record.get().mRace.c_str()); if (column==mColumns.mClass) return QString::fromUtf8 (record.get().mClass.c_str()); if (column==mColumns.mFaction) return QString::fromUtf8 (record.get().mFaction.c_str()); if (column==mColumns.mHair) return QString::fromUtf8 (record.get().mHair.c_str()); if (column==mColumns.mHead) return QString::fromUtf8 (record.get().mHead.c_str()); if (column==mColumns.mAttributes || column==mColumns.mSkills) { if ((record.get().mFlags & ESM::NPC::Autocalc) != 0) return QVariant::fromValue(ColumnBase::TableEdit_None); else return QVariant::fromValue(ColumnBase::TableEdit_FixedRows); } if (column==mColumns.mMisc) return QVariant::fromValue(ColumnBase::TableEdit_Full); if (column == mColumns.mBloodType) return record.get().mBloodType; if (column == mColumns.mGender) { // Implemented this way to allow additional gender types in the future. if ((record.get().mFlags & ESM::NPC::Female) == ESM::NPC::Female) return 1; return 0; } std::map::const_iterator iter = mColumns.mFlags.find (column); if (iter!=mColumns.mFlags.end()) return (record.get().mFlags & iter->second)!=0; return ActorRefIdAdapter::getData (column, data, index); } void CSMWorld::NpcRefIdAdapter::setData (const RefIdColumn *column, RefIdData& data, int index, const QVariant& value) const { Record& record = static_cast&> ( data.getRecord (RefIdData::LocalIndex (index, UniversalId::Type_Npc))); ESM::NPC npc = record.get(); if (column==mColumns.mRace) npc.mRace = value.toString().toUtf8().constData(); else if (column==mColumns.mClass) npc.mClass = value.toString().toUtf8().constData(); else if (column==mColumns.mFaction) npc.mFaction = value.toString().toUtf8().constData(); else if (column==mColumns.mHair) npc.mHair = value.toString().toUtf8().constData(); else if (column==mColumns.mHead) npc.mHead = value.toString().toUtf8().constData(); else if (column == mColumns.mBloodType) npc.mBloodType = value.toInt(); else if (column == mColumns.mGender) { // Implemented this way to allow additional gender types in the future. if (value.toInt() == 1) npc.mFlags = (npc.mFlags & ~ESM::NPC::Female) | ESM::NPC::Female; else npc.mFlags = npc.mFlags & ~ESM::NPC::Female; } else { std::map::const_iterator iter = mColumns.mFlags.find (column); if (iter!=mColumns.mFlags.end()) { if (value.toInt()!=0) npc.mFlags |= iter->second; else npc.mFlags &= ~iter->second; if (iter->second == ESM::NPC::Autocalc) npc.mNpdtType = (value.toInt() != 0) ? ESM::NPC::NPC_WITH_AUTOCALCULATED_STATS : ESM::NPC::NPC_DEFAULT; } else { ActorRefIdAdapter::setData (column, data, index, value); return; } } record.setModified (npc); } CSMWorld::NpcAttributesRefIdAdapter::NpcAttributesRefIdAdapter () {} void CSMWorld::NpcAttributesRefIdAdapter::addNestedRow (const RefIdColumn *column, RefIdData& data, int index, int position) const { // Do nothing, this table cannot be changed by the user } void CSMWorld::NpcAttributesRefIdAdapter::removeNestedRow (const RefIdColumn *column, RefIdData& data, int index, int rowToRemove) const { // Do nothing, this table cannot be changed by the user } void CSMWorld::NpcAttributesRefIdAdapter::setNestedTable (const RefIdColumn* column, RefIdData& data, int index, const NestedTableWrapperBase& nestedTable) const { Record& record = static_cast&> (data.getRecord (RefIdData::LocalIndex (index, UniversalId::Type_Npc))); ESM::NPC npc = record.get(); // store the whole struct npc.mNpdt = static_cast > &>(nestedTable).mNestedTable.at(0); record.setModified (npc); } CSMWorld::NestedTableWrapperBase* CSMWorld::NpcAttributesRefIdAdapter::nestedTable (const RefIdColumn* column, const RefIdData& data, int index) const { const Record& record = static_cast&> (data.getRecord (RefIdData::LocalIndex (index, UniversalId::Type_Npc))); // return the whole struct std::vector wrap; wrap.push_back(record.get().mNpdt); // deleted by dtor of NestedTableStoring return new NestedTableWrapper >(wrap); } QVariant CSMWorld::NpcAttributesRefIdAdapter::getNestedData (const RefIdColumn *column, const RefIdData& data, int index, int subRowIndex, int subColIndex) const { const Record& record = static_cast&> (data.getRecord (RefIdData::LocalIndex (index, UniversalId::Type_Npc))); const ESM::NPC::NPDTstruct52& npcStruct = record.get().mNpdt; if (subColIndex == 0) return subRowIndex; else if (subColIndex == 1) switch (subRowIndex) { case 0: return static_cast(npcStruct.mStrength); case 1: return static_cast(npcStruct.mIntelligence); case 2: return static_cast(npcStruct.mWillpower); case 3: return static_cast(npcStruct.mAgility); case 4: return static_cast(npcStruct.mSpeed); case 5: return static_cast(npcStruct.mEndurance); case 6: return static_cast(npcStruct.mPersonality); case 7: return static_cast(npcStruct.mLuck); default: return QVariant(); // throw an exception here? } else return QVariant(); // throw an exception here? } void CSMWorld::NpcAttributesRefIdAdapter::setNestedData (const RefIdColumn *column, RefIdData& data, int row, const QVariant& value, int subRowIndex, int subColIndex) const { Record& record = static_cast&> (data.getRecord (RefIdData::LocalIndex (row, UniversalId::Type_Npc))); ESM::NPC npc = record.get(); ESM::NPC::NPDTstruct52& npcStruct = npc.mNpdt; if (subColIndex == 1) switch(subRowIndex) { case 0: npcStruct.mStrength = static_cast(value.toInt()); break; case 1: npcStruct.mIntelligence = static_cast(value.toInt()); break; case 2: npcStruct.mWillpower = static_cast(value.toInt()); break; case 3: npcStruct.mAgility = static_cast(value.toInt()); break; case 4: npcStruct.mSpeed = static_cast(value.toInt()); break; case 5: npcStruct.mEndurance = static_cast(value.toInt()); break; case 6: npcStruct.mPersonality = static_cast(value.toInt()); break; case 7: npcStruct.mLuck = static_cast(value.toInt()); break; default: return; // throw an exception here? } else return; // throw an exception here? record.setModified (npc); } int CSMWorld::NpcAttributesRefIdAdapter::getNestedColumnsCount(const RefIdColumn *column, const RefIdData& data) const { return 2; } int CSMWorld::NpcAttributesRefIdAdapter::getNestedRowsCount(const RefIdColumn *column, const RefIdData& data, int index) const { // There are 8 attributes return 8; } CSMWorld::NpcSkillsRefIdAdapter::NpcSkillsRefIdAdapter () {} void CSMWorld::NpcSkillsRefIdAdapter::addNestedRow (const RefIdColumn *column, RefIdData& data, int index, int position) const { // Do nothing, this table cannot be changed by the user } void CSMWorld::NpcSkillsRefIdAdapter::removeNestedRow (const RefIdColumn *column, RefIdData& data, int index, int rowToRemove) const { // Do nothing, this table cannot be changed by the user } void CSMWorld::NpcSkillsRefIdAdapter::setNestedTable (const RefIdColumn* column, RefIdData& data, int index, const NestedTableWrapperBase& nestedTable) const { Record& record = static_cast&> (data.getRecord (RefIdData::LocalIndex (index, UniversalId::Type_Npc))); ESM::NPC npc = record.get(); // store the whole struct npc.mNpdt = static_cast > &>(nestedTable).mNestedTable.at(0); record.setModified (npc); } CSMWorld::NestedTableWrapperBase* CSMWorld::NpcSkillsRefIdAdapter::nestedTable (const RefIdColumn* column, const RefIdData& data, int index) const { const Record& record = static_cast&> (data.getRecord (RefIdData::LocalIndex (index, UniversalId::Type_Npc))); // return the whole struct std::vector wrap; wrap.push_back(record.get().mNpdt); // deleted by dtor of NestedTableStoring return new NestedTableWrapper >(wrap); } QVariant CSMWorld::NpcSkillsRefIdAdapter::getNestedData (const RefIdColumn *column, const RefIdData& data, int index, int subRowIndex, int subColIndex) const { const Record& record = static_cast&> (data.getRecord (RefIdData::LocalIndex (index, UniversalId::Type_Npc))); const ESM::NPC::NPDTstruct52& npcStruct = record.get().mNpdt; if (subRowIndex < 0 || subRowIndex >= ESM::Skill::Length) throw std::runtime_error ("index out of range"); if (subColIndex == 0) return subRowIndex; else if (subColIndex == 1) return static_cast(npcStruct.mSkills[subRowIndex]); else return QVariant(); // throw an exception here? } void CSMWorld::NpcSkillsRefIdAdapter::setNestedData (const RefIdColumn *column, RefIdData& data, int row, const QVariant& value, int subRowIndex, int subColIndex) const { Record& record = static_cast&> (data.getRecord (RefIdData::LocalIndex (row, UniversalId::Type_Npc))); ESM::NPC npc = record.get(); ESM::NPC::NPDTstruct52& npcStruct = npc.mNpdt; if (subRowIndex < 0 || subRowIndex >= ESM::Skill::Length) throw std::runtime_error ("index out of range"); if (subColIndex == 1) npcStruct.mSkills[subRowIndex] = static_cast(value.toInt()); else return; // throw an exception here? record.setModified (npc); } int CSMWorld::NpcSkillsRefIdAdapter::getNestedColumnsCount(const RefIdColumn *column, const RefIdData& data) const { return 2; } int CSMWorld::NpcSkillsRefIdAdapter::getNestedRowsCount(const RefIdColumn *column, const RefIdData& data, int index) const { // There are 27 skills return ESM::Skill::Length; } CSMWorld::NpcMiscRefIdAdapter::NpcMiscRefIdAdapter () {} CSMWorld::NpcMiscRefIdAdapter::~NpcMiscRefIdAdapter() {} void CSMWorld::NpcMiscRefIdAdapter::addNestedRow (const RefIdColumn *column, RefIdData& data, int index, int position) const { throw std::logic_error ("cannot add a row to a fixed table"); } void CSMWorld::NpcMiscRefIdAdapter::removeNestedRow (const RefIdColumn *column, RefIdData& data, int index, int rowToRemove) const { throw std::logic_error ("cannot remove a row to a fixed table"); } void CSMWorld::NpcMiscRefIdAdapter::setNestedTable (const RefIdColumn* column, RefIdData& data, int index, const NestedTableWrapperBase& nestedTable) const { throw std::logic_error ("table operation not supported"); } CSMWorld::NestedTableWrapperBase* CSMWorld::NpcMiscRefIdAdapter::nestedTable (const RefIdColumn* column, const RefIdData& data, int index) const { throw std::logic_error ("table operation not supported"); } QVariant CSMWorld::NpcMiscRefIdAdapter::getNestedData (const RefIdColumn *column, const RefIdData& data, int index, int subRowIndex, int subColIndex) const { const Record& record = static_cast&> (data.getRecord (RefIdData::LocalIndex (index, UniversalId::Type_Npc))); bool autoCalc = (record.get().mFlags & ESM::NPC::Autocalc) != 0; if (autoCalc) switch (subColIndex) { case 0: return static_cast(record.get().mNpdt.mLevel); case 1: return QVariant(QVariant::UserType); case 2: return QVariant(QVariant::UserType); case 3: return QVariant(QVariant::UserType); case 4: return static_cast(record.get().mNpdt.mDisposition); case 5: return static_cast(record.get().mNpdt.mReputation); case 6: return static_cast(record.get().mNpdt.mRank); case 7: return record.get().mNpdt.mGold; case 8: return record.get().mPersistent == true; default: return QVariant(); // throw an exception here? } else switch (subColIndex) { case 0: return static_cast(record.get().mNpdt.mLevel); case 1: return static_cast(record.get().mNpdt.mHealth); case 2: return static_cast(record.get().mNpdt.mMana); case 3: return static_cast(record.get().mNpdt.mFatigue); case 4: return static_cast(record.get().mNpdt.mDisposition); case 5: return static_cast(record.get().mNpdt.mReputation); case 6: return static_cast(record.get().mNpdt.mRank); case 7: return record.get().mNpdt.mGold; case 8: return record.get().mPersistent == true; default: return QVariant(); // throw an exception here? } } void CSMWorld::NpcMiscRefIdAdapter::setNestedData (const RefIdColumn *column, RefIdData& data, int row, const QVariant& value, int subRowIndex, int subColIndex) const { Record& record = static_cast&> (data.getRecord (RefIdData::LocalIndex (row, UniversalId::Type_Npc))); ESM::NPC npc = record.get(); bool autoCalc = (record.get().mFlags & ESM::NPC::Autocalc) != 0; if (autoCalc) switch(subColIndex) { case 0: npc.mNpdt.mLevel = static_cast(value.toInt()); break; case 1: return; case 2: return; case 3: return; case 4: npc.mNpdt.mDisposition = static_cast(value.toInt()); break; case 5: npc.mNpdt.mReputation = static_cast(value.toInt()); break; case 6: npc.mNpdt.mRank = static_cast(value.toInt()); break; case 7: npc.mNpdt.mGold = value.toInt(); break; case 8: npc.mPersistent = value.toBool(); break; default: return; // throw an exception here? } else switch(subColIndex) { case 0: npc.mNpdt.mLevel = static_cast(value.toInt()); break; case 1: npc.mNpdt.mHealth = static_cast(value.toInt()); break; case 2: npc.mNpdt.mMana = static_cast(value.toInt()); break; case 3: npc.mNpdt.mFatigue = static_cast(value.toInt()); break; case 4: npc.mNpdt.mDisposition = static_cast(value.toInt()); break; case 5: npc.mNpdt.mReputation = static_cast(value.toInt()); break; case 6: npc.mNpdt.mRank = static_cast(value.toInt()); break; case 7: npc.mNpdt.mGold = value.toInt(); break; case 8: npc.mPersistent = value.toBool(); break; default: return; // throw an exception here? } record.setModified (npc); } int CSMWorld::NpcMiscRefIdAdapter::getNestedColumnsCount(const RefIdColumn *column, const RefIdData& data) const { return 9; // Level, Health, Mana, Fatigue, Disposition, Reputation, Rank, Gold, Persist } int CSMWorld::NpcMiscRefIdAdapter::getNestedRowsCount(const RefIdColumn *column, const RefIdData& data, int index) const { return 1; // fixed at size 1 } CSMWorld::CreatureAttributesRefIdAdapter::CreatureAttributesRefIdAdapter() {} void CSMWorld::CreatureAttributesRefIdAdapter::addNestedRow (const RefIdColumn *column, RefIdData& data, int index, int position) const { // Do nothing, this table cannot be changed by the user } void CSMWorld::CreatureAttributesRefIdAdapter::removeNestedRow (const RefIdColumn *column, RefIdData& data, int index, int rowToRemove) const { // Do nothing, this table cannot be changed by the user } void CSMWorld::CreatureAttributesRefIdAdapter::setNestedTable (const RefIdColumn* column, RefIdData& data, int index, const NestedTableWrapperBase& nestedTable) const { Record& record = static_cast&> (data.getRecord (RefIdData::LocalIndex (index, UniversalId::Type_Creature))); ESM::Creature creature = record.get(); // store the whole struct creature.mData = static_cast > &>(nestedTable).mNestedTable.at(0); record.setModified (creature); } CSMWorld::NestedTableWrapperBase* CSMWorld::CreatureAttributesRefIdAdapter::nestedTable (const RefIdColumn* column, const RefIdData& data, int index) const { const Record& record = static_cast&> (data.getRecord (RefIdData::LocalIndex (index, UniversalId::Type_Creature))); // return the whole struct std::vector wrap; wrap.push_back(record.get().mData); // deleted by dtor of NestedTableStoring return new NestedTableWrapper >(wrap); } QVariant CSMWorld::CreatureAttributesRefIdAdapter::getNestedData (const RefIdColumn *column, const RefIdData& data, int index, int subRowIndex, int subColIndex) const { const Record& record = static_cast&> (data.getRecord (RefIdData::LocalIndex (index, UniversalId::Type_Creature))); const ESM::Creature& creature = record.get(); if (subColIndex == 0) return subRowIndex; else if (subColIndex == 1) switch (subRowIndex) { case 0: return creature.mData.mStrength; case 1: return creature.mData.mIntelligence; case 2: return creature.mData.mWillpower; case 3: return creature.mData.mAgility; case 4: return creature.mData.mSpeed; case 5: return creature.mData.mEndurance; case 6: return creature.mData.mPersonality; case 7: return creature.mData.mLuck; default: return QVariant(); // throw an exception here? } else return QVariant(); // throw an exception here? } void CSMWorld::CreatureAttributesRefIdAdapter::setNestedData (const RefIdColumn *column, RefIdData& data, int row, const QVariant& value, int subRowIndex, int subColIndex) const { Record& record = static_cast&> (data.getRecord (RefIdData::LocalIndex (row, UniversalId::Type_Creature))); ESM::Creature creature = record.get(); if (subColIndex == 1) switch(subRowIndex) { case 0: creature.mData.mStrength = value.toInt(); break; case 1: creature.mData.mIntelligence = value.toInt(); break; case 2: creature.mData.mWillpower = value.toInt(); break; case 3: creature.mData.mAgility = value.toInt(); break; case 4: creature.mData.mSpeed = value.toInt(); break; case 5: creature.mData.mEndurance = value.toInt(); break; case 6: creature.mData.mPersonality = value.toInt(); break; case 7: creature.mData.mLuck = value.toInt(); break; default: return; // throw an exception here? } else return; // throw an exception here? record.setModified (creature); } int CSMWorld::CreatureAttributesRefIdAdapter::getNestedColumnsCount(const RefIdColumn *column, const RefIdData& data) const { return 2; } int CSMWorld::CreatureAttributesRefIdAdapter::getNestedRowsCount(const RefIdColumn *column, const RefIdData& data, int index) const { // There are 8 attributes return 8; } CSMWorld::CreatureAttackRefIdAdapter::CreatureAttackRefIdAdapter() {} void CSMWorld::CreatureAttackRefIdAdapter::addNestedRow (const RefIdColumn *column, RefIdData& data, int index, int position) const { // Do nothing, this table cannot be changed by the user } void CSMWorld::CreatureAttackRefIdAdapter::removeNestedRow (const RefIdColumn *column, RefIdData& data, int index, int rowToRemove) const { // Do nothing, this table cannot be changed by the user } void CSMWorld::CreatureAttackRefIdAdapter::setNestedTable (const RefIdColumn* column, RefIdData& data, int index, const NestedTableWrapperBase& nestedTable) const { Record& record = static_cast&> (data.getRecord (RefIdData::LocalIndex (index, UniversalId::Type_Creature))); ESM::Creature creature = record.get(); // store the whole struct creature.mData = static_cast > &>(nestedTable).mNestedTable.at(0); record.setModified (creature); } CSMWorld::NestedTableWrapperBase* CSMWorld::CreatureAttackRefIdAdapter::nestedTable (const RefIdColumn* column, const RefIdData& data, int index) const { const Record& record = static_cast&> (data.getRecord (RefIdData::LocalIndex (index, UniversalId::Type_Creature))); // return the whole struct std::vector wrap; wrap.push_back(record.get().mData); // deleted by dtor of NestedTableStoring return new NestedTableWrapper >(wrap); } QVariant CSMWorld::CreatureAttackRefIdAdapter::getNestedData (const RefIdColumn *column, const RefIdData& data, int index, int subRowIndex, int subColIndex) const { const Record& record = static_cast&> (data.getRecord (RefIdData::LocalIndex (index, UniversalId::Type_Creature))); const ESM::Creature& creature = record.get(); if (subRowIndex < 0 || subRowIndex > 2) throw std::runtime_error ("index out of range"); if (subColIndex == 0) return subRowIndex + 1; else if (subColIndex == 1 || subColIndex == 2) return creature.mData.mAttack[(subRowIndex * 2) + (subColIndex - 1)]; else throw std::runtime_error ("index out of range"); } void CSMWorld::CreatureAttackRefIdAdapter::setNestedData (const RefIdColumn *column, RefIdData& data, int row, const QVariant& value, int subRowIndex, int subColIndex) const { Record& record = static_cast&> (data.getRecord (RefIdData::LocalIndex (row, UniversalId::Type_Creature))); ESM::Creature creature = record.get(); if (subRowIndex < 0 || subRowIndex > 2) throw std::runtime_error ("index out of range"); if (subColIndex == 1 || subColIndex == 2) creature.mData.mAttack[(subRowIndex * 2) + (subColIndex - 1)] = value.toInt(); else return; // throw an exception here? record.setModified (creature); } int CSMWorld::CreatureAttackRefIdAdapter::getNestedColumnsCount(const RefIdColumn *column, const RefIdData& data) const { return 3; } int CSMWorld::CreatureAttackRefIdAdapter::getNestedRowsCount(const RefIdColumn *column, const RefIdData& data, int index) const { // There are 3 attacks return 3; } CSMWorld::CreatureMiscRefIdAdapter::CreatureMiscRefIdAdapter() {} CSMWorld::CreatureMiscRefIdAdapter::~CreatureMiscRefIdAdapter() {} void CSMWorld::CreatureMiscRefIdAdapter::addNestedRow (const RefIdColumn *column, RefIdData& data, int index, int position) const { throw std::logic_error ("cannot add a row to a fixed table"); } void CSMWorld::CreatureMiscRefIdAdapter::removeNestedRow (const RefIdColumn *column, RefIdData& data, int index, int rowToRemove) const { throw std::logic_error ("cannot remove a row to a fixed table"); } void CSMWorld::CreatureMiscRefIdAdapter::setNestedTable (const RefIdColumn* column, RefIdData& data, int index, const NestedTableWrapperBase& nestedTable) const { throw std::logic_error ("table operation not supported"); } CSMWorld::NestedTableWrapperBase* CSMWorld::CreatureMiscRefIdAdapter::nestedTable (const RefIdColumn* column, const RefIdData& data, int index) const { throw std::logic_error ("table operation not supported"); } QVariant CSMWorld::CreatureMiscRefIdAdapter::getNestedData (const RefIdColumn *column, const RefIdData& data, int index, int subRowIndex, int subColIndex) const { const Record& record = static_cast&> (data.getRecord (RefIdData::LocalIndex (index, UniversalId::Type_Creature))); const ESM::Creature& creature = record.get(); switch (subColIndex) { case 0: return creature.mData.mLevel; case 1: return creature.mData.mHealth; case 2: return creature.mData.mMana; case 3: return creature.mData.mFatigue; case 4: return creature.mData.mSoul; case 5: return creature.mData.mCombat; case 6: return creature.mData.mMagic; case 7: return creature.mData.mStealth; case 8: return creature.mData.mGold; default: return QVariant(); // throw an exception here? } } void CSMWorld::CreatureMiscRefIdAdapter::setNestedData (const RefIdColumn *column, RefIdData& data, int row, const QVariant& value, int subRowIndex, int subColIndex) const { Record& record = static_cast&> (data.getRecord (RefIdData::LocalIndex (row, UniversalId::Type_Creature))); ESM::Creature creature = record.get(); switch(subColIndex) { case 0: creature.mData.mLevel = value.toInt(); break; case 1: creature.mData.mHealth = value.toInt(); break; case 2: creature.mData.mMana = value.toInt(); break; case 3: creature.mData.mFatigue = value.toInt(); break; case 4: creature.mData.mSoul = value.toInt(); break; case 5: creature.mData.mCombat = value.toInt(); break; case 6: creature.mData.mMagic = value.toInt(); break; case 7: creature.mData.mStealth = value.toInt(); break; case 8: creature.mData.mGold = value.toInt(); break; default: return; // throw an exception here? } record.setModified (creature); } int CSMWorld::CreatureMiscRefIdAdapter::getNestedColumnsCount(const RefIdColumn *column, const RefIdData& data) const { return 9; // Level, Health, Mana, Fatigue, Soul, Combat, Magic, Steath, Gold } int CSMWorld::CreatureMiscRefIdAdapter::getNestedRowsCount(const RefIdColumn *column, const RefIdData& data, int index) const { return 1; // fixed at size 1 } CSMWorld::WeaponColumns::WeaponColumns (const EnchantableColumns& columns) : EnchantableColumns (columns) , mType(nullptr) , mHealth(nullptr) , mSpeed(nullptr) , mReach(nullptr) , mChop{nullptr} , mSlash{nullptr} , mThrust{nullptr} {} CSMWorld::WeaponRefIdAdapter::WeaponRefIdAdapter (const WeaponColumns& columns) : EnchantableRefIdAdapter (UniversalId::Type_Weapon, columns), mColumns (columns) {} QVariant CSMWorld::WeaponRefIdAdapter::getData (const RefIdColumn *column, const RefIdData& data, int index) const { const Record& record = static_cast&> ( data.getRecord (RefIdData::LocalIndex (index, UniversalId::Type_Weapon))); if (column==mColumns.mType) return record.get().mData.mType; if (column==mColumns.mHealth) return record.get().mData.mHealth; if (column==mColumns.mSpeed) return record.get().mData.mSpeed; if (column==mColumns.mReach) return record.get().mData.mReach; for (int i=0; i<2; ++i) { if (column==mColumns.mChop[i]) return record.get().mData.mChop[i]; if (column==mColumns.mSlash[i]) return record.get().mData.mSlash[i]; if (column==mColumns.mThrust[i]) return record.get().mData.mThrust[i]; } std::map::const_iterator iter = mColumns.mFlags.find (column); if (iter!=mColumns.mFlags.end()) return (record.get().mData.mFlags & iter->second)!=0; return EnchantableRefIdAdapter::getData (column, data, index); } void CSMWorld::WeaponRefIdAdapter::setData (const RefIdColumn *column, RefIdData& data, int index, const QVariant& value) const { Record& record = static_cast&> ( data.getRecord (RefIdData::LocalIndex (index, UniversalId::Type_Weapon))); ESM::Weapon weapon = record.get(); if (column==mColumns.mType) weapon.mData.mType = value.toInt(); else if (column==mColumns.mHealth) weapon.mData.mHealth = value.toInt(); else if (column==mColumns.mSpeed) weapon.mData.mSpeed = value.toFloat(); else if (column==mColumns.mReach) weapon.mData.mReach = value.toFloat(); else if (column==mColumns.mChop[0]) weapon.mData.mChop[0] = value.toInt(); else if (column==mColumns.mChop[1]) weapon.mData.mChop[1] = value.toInt(); else if (column==mColumns.mSlash[0]) weapon.mData.mSlash[0] = value.toInt(); else if (column==mColumns.mSlash[1]) weapon.mData.mSlash[1] = value.toInt(); else if (column==mColumns.mThrust[0]) weapon.mData.mThrust[0] = value.toInt(); else if (column==mColumns.mThrust[1]) weapon.mData.mThrust[1] = value.toInt(); else { std::map::const_iterator iter = mColumns.mFlags.find (column); if (iter!=mColumns.mFlags.end()) { if (value.toInt()!=0) weapon.mData.mFlags |= iter->second; else weapon.mData.mFlags &= ~iter->second; } else { EnchantableRefIdAdapter::setData (column, data, index, value); return; // Don't overwrite changes made by base class } } record.setModified(weapon); } ================================================ FILE: apps/opencs/model/world/refidadapterimp.hpp ================================================ #ifndef CSM_WOLRD_REFIDADAPTERIMP_H #define CSM_WOLRD_REFIDADAPTERIMP_H #include #include #include #include #include #include #include #include "columnbase.hpp" #include "record.hpp" #include "refiddata.hpp" #include "universalid.hpp" #include "refidadapter.hpp" #include "nestedtablewrapper.hpp" namespace CSMWorld { struct BaseColumns { const RefIdColumn *mId; const RefIdColumn *mModified; const RefIdColumn *mType; }; /// \brief Base adapter for all refereceable record types /// Adapters that can handle nested tables, needs to return valid qvariant for parent columns template class BaseRefIdAdapter : public RefIdAdapter { UniversalId::Type mType; BaseColumns mBase; public: BaseRefIdAdapter (UniversalId::Type type, const BaseColumns& base); std::string getId (const RecordBase& record) const override; void setId (RecordBase& record, const std::string& id) override; QVariant getData (const RefIdColumn *column, const RefIdData& data, int index) const override; void setData (const RefIdColumn *column, RefIdData& data, int index, const QVariant& value) const override; ///< If the data type does not match an exception is thrown. UniversalId::Type getType() const; }; template BaseRefIdAdapter::BaseRefIdAdapter (UniversalId::Type type, const BaseColumns& base) : mType (type), mBase (base) {} template void BaseRefIdAdapter::setId (RecordBase& record, const std::string& id) { (dynamic_cast&> (record).get().mId) = id; } template std::string BaseRefIdAdapter::getId (const RecordBase& record) const { return dynamic_cast&> (record).get().mId; } template QVariant BaseRefIdAdapter::getData (const RefIdColumn *column, const RefIdData& data, int index) const { const Record& record = static_cast&> ( data.getRecord (RefIdData::LocalIndex (index, mType))); if (column==mBase.mId) return QString::fromUtf8 (record.get().mId.c_str()); if (column==mBase.mModified) { if (record.mState==Record::State_Erased) return static_cast (Record::State_Deleted); return static_cast (record.mState); } if (column==mBase.mType) return static_cast (mType); return QVariant(); } template void BaseRefIdAdapter::setData (const RefIdColumn *column, RefIdData& data, int index, const QVariant& value) const { Record& record = static_cast&> ( data.getRecord (RefIdData::LocalIndex (index, mType))); if (column==mBase.mModified) record.mState = static_cast (value.toInt()); } template UniversalId::Type BaseRefIdAdapter::getType() const { return mType; } struct ModelColumns : public BaseColumns { const RefIdColumn *mModel; ModelColumns (const BaseColumns& base) : BaseColumns (base), mModel(nullptr) {} }; /// \brief Adapter for IDs with models (all but levelled lists) template class ModelRefIdAdapter : public BaseRefIdAdapter { ModelColumns mModel; public: ModelRefIdAdapter (UniversalId::Type type, const ModelColumns& columns); QVariant getData (const RefIdColumn *column, const RefIdData& data, int index) const override; void setData (const RefIdColumn *column, RefIdData& data, int index, const QVariant& value) const override; ///< If the data type does not match an exception is thrown. }; template ModelRefIdAdapter::ModelRefIdAdapter (UniversalId::Type type, const ModelColumns& columns) : BaseRefIdAdapter (type, columns), mModel (columns) {} template QVariant ModelRefIdAdapter::getData (const RefIdColumn *column, const RefIdData& data, int index) const { const Record& record = static_cast&> ( data.getRecord (RefIdData::LocalIndex (index, BaseRefIdAdapter::getType()))); if (column==mModel.mModel) return QString::fromUtf8 (record.get().mModel.c_str()); return BaseRefIdAdapter::getData (column, data, index); } template void ModelRefIdAdapter::setData (const RefIdColumn *column, RefIdData& data, int index, const QVariant& value) const { Record& record = static_cast&> ( data.getRecord (RefIdData::LocalIndex (index, BaseRefIdAdapter::getType()))); RecordT record2 = record.get(); if (column==mModel.mModel) record2.mModel = value.toString().toUtf8().constData(); else { BaseRefIdAdapter::setData (column, data, index, value); return; } record.setModified(record2); } struct NameColumns : public ModelColumns { const RefIdColumn *mName; const RefIdColumn *mScript; NameColumns (const ModelColumns& base) : ModelColumns (base) , mName(nullptr) , mScript(nullptr) {} }; /// \brief Adapter for IDs with names (all but levelled lists and statics) template class NameRefIdAdapter : public ModelRefIdAdapter { NameColumns mName; public: NameRefIdAdapter (UniversalId::Type type, const NameColumns& columns); QVariant getData (const RefIdColumn *column, const RefIdData& data, int index) const override; void setData (const RefIdColumn *column, RefIdData& data, int index, const QVariant& value) const override; ///< If the data type does not match an exception is thrown. }; template NameRefIdAdapter::NameRefIdAdapter (UniversalId::Type type, const NameColumns& columns) : ModelRefIdAdapter (type, columns), mName (columns) {} template QVariant NameRefIdAdapter::getData (const RefIdColumn *column, const RefIdData& data, int index) const { const Record& record = static_cast&> ( data.getRecord (RefIdData::LocalIndex (index, BaseRefIdAdapter::getType()))); if (column==mName.mName) return QString::fromUtf8 (record.get().mName.c_str()); if (column==mName.mScript) return QString::fromUtf8 (record.get().mScript.c_str()); return ModelRefIdAdapter::getData (column, data, index); } template void NameRefIdAdapter::setData (const RefIdColumn *column, RefIdData& data, int index, const QVariant& value) const { Record& record = static_cast&> ( data.getRecord (RefIdData::LocalIndex (index, BaseRefIdAdapter::getType()))); RecordT record2 = record.get(); if (column==mName.mName) record2.mName = value.toString().toUtf8().constData(); else if (column==mName.mScript) record2.mScript = value.toString().toUtf8().constData(); else { ModelRefIdAdapter::setData (column, data, index, value); return; } record.setModified(record2); } struct InventoryColumns : public NameColumns { const RefIdColumn *mIcon; const RefIdColumn *mWeight; const RefIdColumn *mValue; InventoryColumns (const NameColumns& base) : NameColumns (base) , mIcon(nullptr) , mWeight(nullptr) , mValue(nullptr) {} }; /// \brief Adapter for IDs that can go into an inventory template class InventoryRefIdAdapter : public NameRefIdAdapter { InventoryColumns mInventory; public: InventoryRefIdAdapter (UniversalId::Type type, const InventoryColumns& columns); QVariant getData (const RefIdColumn *column, const RefIdData& data, int index) const override; void setData (const RefIdColumn *column, RefIdData& data, int index, const QVariant& value) const override; ///< If the data type does not match an exception is thrown. }; template InventoryRefIdAdapter::InventoryRefIdAdapter (UniversalId::Type type, const InventoryColumns& columns) : NameRefIdAdapter (type, columns), mInventory (columns) {} template QVariant InventoryRefIdAdapter::getData (const RefIdColumn *column, const RefIdData& data, int index) const { const Record& record = static_cast&> ( data.getRecord (RefIdData::LocalIndex (index, BaseRefIdAdapter::getType()))); if (column==mInventory.mIcon) return QString::fromUtf8 (record.get().mIcon.c_str()); if (column==mInventory.mWeight) return record.get().mData.mWeight; if (column==mInventory.mValue) return record.get().mData.mValue; return NameRefIdAdapter::getData (column, data, index); } template void InventoryRefIdAdapter::setData (const RefIdColumn *column, RefIdData& data, int index, const QVariant& value) const { Record& record = static_cast&> ( data.getRecord (RefIdData::LocalIndex (index, BaseRefIdAdapter::getType()))); RecordT record2 = record.get(); if (column==mInventory.mIcon) record2.mIcon = value.toString().toUtf8().constData(); else if (column==mInventory.mWeight) record2.mData.mWeight = value.toFloat(); else if (column==mInventory.mValue) record2.mData.mValue = value.toInt(); else { NameRefIdAdapter::setData (column, data, index, value); return; } record.setModified(record2); } struct PotionColumns : public InventoryColumns { const RefIdColumn *mEffects; PotionColumns (const InventoryColumns& columns); }; class PotionRefIdAdapter : public InventoryRefIdAdapter { PotionColumns mColumns; const RefIdColumn *mAutoCalc; public: PotionRefIdAdapter (const PotionColumns& columns, const RefIdColumn *autoCalc); QVariant getData (const RefIdColumn *column, const RefIdData& data, int index) const override; void setData (const RefIdColumn *column, RefIdData& data, int index, const QVariant& value) const override; ///< If the data type does not match an exception is thrown. }; struct IngredientColumns : public InventoryColumns { const RefIdColumn *mEffects; IngredientColumns (const InventoryColumns& columns); }; class IngredientRefIdAdapter : public InventoryRefIdAdapter { IngredientColumns mColumns; public: IngredientRefIdAdapter (const IngredientColumns& columns); QVariant getData (const RefIdColumn *column, const RefIdData& data, int index) const override; void setData (const RefIdColumn *column, RefIdData& data, int index, const QVariant& value) const override; ///< If the data type does not match an exception is thrown. }; class IngredEffectRefIdAdapter : public NestedRefIdAdapterBase { UniversalId::Type mType; // not implemented IngredEffectRefIdAdapter (const IngredEffectRefIdAdapter&); IngredEffectRefIdAdapter& operator= (const IngredEffectRefIdAdapter&); public: IngredEffectRefIdAdapter(); virtual ~IngredEffectRefIdAdapter(); void addNestedRow (const RefIdColumn *column, RefIdData& data, int index, int position) const override; void removeNestedRow (const RefIdColumn *column, RefIdData& data, int index, int rowToRemove) const override; void setNestedTable (const RefIdColumn* column, RefIdData& data, int index, const NestedTableWrapperBase& nestedTable) const override; NestedTableWrapperBase* nestedTable (const RefIdColumn* column, const RefIdData& data, int index) const override; QVariant getNestedData (const RefIdColumn *column, const RefIdData& data, int index, int subRowIndex, int subColIndex) const override; void setNestedData (const RefIdColumn *column, RefIdData& data, int row, const QVariant& value, int subRowIndex, int subColIndex) const override; int getNestedColumnsCount(const RefIdColumn *column, const RefIdData& data) const override; int getNestedRowsCount(const RefIdColumn *column, const RefIdData& data, int index) const override; }; struct EnchantableColumns : public InventoryColumns { const RefIdColumn *mEnchantment; const RefIdColumn *mEnchantmentPoints; EnchantableColumns (const InventoryColumns& base) : InventoryColumns (base) , mEnchantment(nullptr) , mEnchantmentPoints(nullptr) {} }; /// \brief Adapter for enchantable IDs template class EnchantableRefIdAdapter : public InventoryRefIdAdapter { EnchantableColumns mEnchantable; public: EnchantableRefIdAdapter (UniversalId::Type type, const EnchantableColumns& columns); QVariant getData (const RefIdColumn *column, const RefIdData& data, int index) const override; void setData (const RefIdColumn *column, RefIdData& data, int index, const QVariant& value) const override; ///< If the data type does not match an exception is thrown. }; template EnchantableRefIdAdapter::EnchantableRefIdAdapter (UniversalId::Type type, const EnchantableColumns& columns) : InventoryRefIdAdapter (type, columns), mEnchantable (columns) {} template QVariant EnchantableRefIdAdapter::getData (const RefIdColumn *column, const RefIdData& data, int index) const { const Record& record = static_cast&> ( data.getRecord (RefIdData::LocalIndex (index, BaseRefIdAdapter::getType()))); if (column==mEnchantable.mEnchantment) return QString::fromUtf8 (record.get().mEnchant.c_str()); if (column==mEnchantable.mEnchantmentPoints) return static_cast (record.get().mData.mEnchant); return InventoryRefIdAdapter::getData (column, data, index); } template void EnchantableRefIdAdapter::setData (const RefIdColumn *column, RefIdData& data, int index, const QVariant& value) const { Record& record = static_cast&> ( data.getRecord (RefIdData::LocalIndex (index, BaseRefIdAdapter::getType()))); RecordT record2 = record.get(); if (column==mEnchantable.mEnchantment) record2.mEnchant = value.toString().toUtf8().constData(); else if (column==mEnchantable.mEnchantmentPoints) record2.mData.mEnchant = value.toInt(); else { InventoryRefIdAdapter::setData (column, data, index, value); return; } record.setModified(record2); } struct ToolColumns : public InventoryColumns { const RefIdColumn *mQuality; const RefIdColumn *mUses; ToolColumns (const InventoryColumns& base) : InventoryColumns (base) , mQuality(nullptr) , mUses(nullptr) {} }; /// \brief Adapter for tools with limited uses IDs (lockpick, repair, probes) template class ToolRefIdAdapter : public InventoryRefIdAdapter { ToolColumns mTools; public: ToolRefIdAdapter (UniversalId::Type type, const ToolColumns& columns); QVariant getData (const RefIdColumn *column, const RefIdData& data, int index) const override; void setData (const RefIdColumn *column, RefIdData& data, int index, const QVariant& value) const override; ///< If the data type does not match an exception is thrown. }; template ToolRefIdAdapter::ToolRefIdAdapter (UniversalId::Type type, const ToolColumns& columns) : InventoryRefIdAdapter (type, columns), mTools (columns) {} template QVariant ToolRefIdAdapter::getData (const RefIdColumn *column, const RefIdData& data, int index) const { const Record& record = static_cast&> ( data.getRecord (RefIdData::LocalIndex (index, BaseRefIdAdapter::getType()))); if (column==mTools.mQuality) return record.get().mData.mQuality; if (column==mTools.mUses) return record.get().mData.mUses; return InventoryRefIdAdapter::getData (column, data, index); } template void ToolRefIdAdapter::setData (const RefIdColumn *column, RefIdData& data, int index, const QVariant& value) const { Record& record = static_cast&> ( data.getRecord (RefIdData::LocalIndex (index, BaseRefIdAdapter::getType()))); RecordT record2 = record.get(); if (column==mTools.mQuality) record2.mData.mQuality = value.toFloat(); else if (column==mTools.mUses) record2.mData.mUses = value.toInt(); else { InventoryRefIdAdapter::setData (column, data, index, value); return; } record.setModified(record2); } struct ActorColumns : public NameColumns { const RefIdColumn *mHello; const RefIdColumn *mFlee; const RefIdColumn *mFight; const RefIdColumn *mAlarm; const RefIdColumn *mInventory; const RefIdColumn *mSpells; const RefIdColumn *mDestinations; const RefIdColumn *mAiPackages; std::map mServices; ActorColumns (const NameColumns& base) : NameColumns (base) , mHello(nullptr) , mFlee(nullptr) , mFight(nullptr) , mAlarm(nullptr) , mInventory(nullptr) , mSpells(nullptr) , mDestinations(nullptr) , mAiPackages(nullptr) {} }; /// \brief Adapter for actor IDs (handles common AI functionality) template class ActorRefIdAdapter : public NameRefIdAdapter { ActorColumns mActors; public: ActorRefIdAdapter (UniversalId::Type type, const ActorColumns& columns); QVariant getData (const RefIdColumn *column, const RefIdData& data, int index) const override; void setData (const RefIdColumn *column, RefIdData& data, int index, const QVariant& value) const override; ///< If the data type does not match an exception is thrown. }; template ActorRefIdAdapter::ActorRefIdAdapter (UniversalId::Type type, const ActorColumns& columns) : NameRefIdAdapter (type, columns), mActors (columns) {} template QVariant ActorRefIdAdapter::getData (const RefIdColumn *column, const RefIdData& data, int index) const { const Record& record = static_cast&> ( data.getRecord (RefIdData::LocalIndex (index, BaseRefIdAdapter::getType()))); if (column==mActors.mHello) return record.get().mAiData.mHello; if (column==mActors.mFlee) return record.get().mAiData.mFlee; if (column==mActors.mFight) return record.get().mAiData.mFight; if (column==mActors.mAlarm) return record.get().mAiData.mAlarm; if (column==mActors.mInventory) return QVariant::fromValue(ColumnBase::TableEdit_Full); if (column==mActors.mSpells) return QVariant::fromValue(ColumnBase::TableEdit_Full); if (column==mActors.mDestinations) return QVariant::fromValue(ColumnBase::TableEdit_Full); if (column==mActors.mAiPackages) return QVariant::fromValue(ColumnBase::TableEdit_Full); std::map::const_iterator iter = mActors.mServices.find (column); if (iter!=mActors.mServices.end()) return (record.get().mAiData.mServices & iter->second)!=0; return NameRefIdAdapter::getData (column, data, index); } template void ActorRefIdAdapter::setData (const RefIdColumn *column, RefIdData& data, int index, const QVariant& value) const { Record& record = static_cast&> ( data.getRecord (RefIdData::LocalIndex (index, BaseRefIdAdapter::getType()))); RecordT record2 = record.get(); if (column==mActors.mHello) record2.mAiData.mHello = value.toInt(); else if (column==mActors.mFlee) // Flee, Fight and Alarm ratings are probabilities. record2.mAiData.mFlee = std::min(100, value.toInt()); else if (column==mActors.mFight) record2.mAiData.mFight = std::min(100, value.toInt()); else if (column==mActors.mAlarm) record2.mAiData.mAlarm = std::min(100, value.toInt()); else { typename std::map::const_iterator iter = mActors.mServices.find (column); if (iter!=mActors.mServices.end()) { if (value.toInt()!=0) record2.mAiData.mServices |= iter->second; else record2.mAiData.mServices &= ~iter->second; } else { NameRefIdAdapter::setData (column, data, index, value); return; } } record.setModified(record2); } class ApparatusRefIdAdapter : public InventoryRefIdAdapter { const RefIdColumn *mType; const RefIdColumn *mQuality; public: ApparatusRefIdAdapter (const InventoryColumns& columns, const RefIdColumn *type, const RefIdColumn *quality); QVariant getData (const RefIdColumn *column, const RefIdData& data, int index) const override; void setData (const RefIdColumn *column, RefIdData& data, int index, const QVariant& value) const override; ///< If the data type does not match an exception is thrown. }; class ArmorRefIdAdapter : public EnchantableRefIdAdapter { const RefIdColumn *mType; const RefIdColumn *mHealth; const RefIdColumn *mArmor; const RefIdColumn *mPartRef; public: ArmorRefIdAdapter (const EnchantableColumns& columns, const RefIdColumn *type, const RefIdColumn *health, const RefIdColumn *armor, const RefIdColumn *partRef); QVariant getData (const RefIdColumn *column, const RefIdData& data, int index) const override; void setData (const RefIdColumn *column, RefIdData& data, int index, const QVariant& value) const override; ///< If the data type does not match an exception is thrown. }; class BookRefIdAdapter : public EnchantableRefIdAdapter { const RefIdColumn *mBookType; const RefIdColumn *mSkill; const RefIdColumn *mText; public: BookRefIdAdapter (const EnchantableColumns& columns, const RefIdColumn *bookType, const RefIdColumn *skill, const RefIdColumn *text); QVariant getData (const RefIdColumn *column, const RefIdData& data, int index) const override; void setData (const RefIdColumn *column, RefIdData& data, int index, const QVariant& value) const override; ///< If the data type does not match an exception is thrown. }; class ClothingRefIdAdapter : public EnchantableRefIdAdapter { const RefIdColumn *mType; const RefIdColumn *mPartRef; public: ClothingRefIdAdapter (const EnchantableColumns& columns, const RefIdColumn *type, const RefIdColumn *partRef); QVariant getData (const RefIdColumn *column, const RefIdData& data, int index) const override; void setData (const RefIdColumn *column, RefIdData& data, int index, const QVariant& value) const override; ///< If the data type does not match an exception is thrown. }; class ContainerRefIdAdapter : public NameRefIdAdapter { const RefIdColumn *mWeight; const RefIdColumn *mOrganic; const RefIdColumn *mRespawn; const RefIdColumn *mContent; public: ContainerRefIdAdapter (const NameColumns& columns, const RefIdColumn *weight, const RefIdColumn *organic, const RefIdColumn *respawn, const RefIdColumn *content); QVariant getData (const RefIdColumn *column, const RefIdData& data, int index) const override; void setData (const RefIdColumn *column, RefIdData& data, int index, const QVariant& value) const override; ///< If the data type does not match an exception is thrown. }; struct CreatureColumns : public ActorColumns { std::map mFlags; const RefIdColumn *mType; const RefIdColumn *mScale; const RefIdColumn *mOriginal; const RefIdColumn *mAttributes; const RefIdColumn *mAttacks; const RefIdColumn *mMisc; const RefIdColumn *mBloodType; CreatureColumns (const ActorColumns& actorColumns); }; class CreatureRefIdAdapter : public ActorRefIdAdapter { CreatureColumns mColumns; public: CreatureRefIdAdapter (const CreatureColumns& columns); QVariant getData (const RefIdColumn *column, const RefIdData& data, int index) const override; void setData (const RefIdColumn *column, RefIdData& data, int index, const QVariant& value) const override; ///< If the data type does not match an exception is thrown. }; class DoorRefIdAdapter : public NameRefIdAdapter { const RefIdColumn *mOpenSound; const RefIdColumn *mCloseSound; public: DoorRefIdAdapter (const NameColumns& columns, const RefIdColumn *openSound, const RefIdColumn *closeSound); QVariant getData (const RefIdColumn *column, const RefIdData& data, int index) const override; void setData (const RefIdColumn *column, RefIdData& data, int index, const QVariant& value) const override; ///< If the data type does not match an exception is thrown. }; struct LightColumns : public InventoryColumns { const RefIdColumn *mTime; const RefIdColumn *mRadius; const RefIdColumn *mColor; const RefIdColumn *mSound; const RefIdColumn *mEmitterType; std::map mFlags; LightColumns (const InventoryColumns& columns); }; class LightRefIdAdapter : public InventoryRefIdAdapter { LightColumns mColumns; public: LightRefIdAdapter (const LightColumns& columns); QVariant getData (const RefIdColumn *column, const RefIdData& data, int index) const override; void setData (const RefIdColumn *column, RefIdData& data, int index, const QVariant& value) const override; ///< If the data type does not match an exception is thrown. }; class MiscRefIdAdapter : public InventoryRefIdAdapter { const RefIdColumn *mKey; public: MiscRefIdAdapter (const InventoryColumns& columns, const RefIdColumn *key); QVariant getData (const RefIdColumn *column, const RefIdData& data, int index) const override; void setData (const RefIdColumn *column, RefIdData& data, int index, const QVariant& value) const override; ///< If the data type does not match an exception is thrown. }; struct NpcColumns : public ActorColumns { std::map mFlags; const RefIdColumn *mRace; const RefIdColumn *mClass; const RefIdColumn *mFaction; const RefIdColumn *mHair; const RefIdColumn *mHead; const RefIdColumn *mAttributes; // depends on npc type const RefIdColumn *mSkills; // depends on npc type const RefIdColumn *mMisc; // may depend on npc type, e.g. FactionID const RefIdColumn *mBloodType; const RefIdColumn *mGender; NpcColumns (const ActorColumns& actorColumns); }; class NpcRefIdAdapter : public ActorRefIdAdapter { NpcColumns mColumns; public: NpcRefIdAdapter (const NpcColumns& columns); QVariant getData (const RefIdColumn *column, const RefIdData& data, int index) const override; void setData (const RefIdColumn *column, RefIdData& data, int index, const QVariant& value) const override; ///< If the data type does not match an exception is thrown. }; struct WeaponColumns : public EnchantableColumns { const RefIdColumn *mType; const RefIdColumn *mHealth; const RefIdColumn *mSpeed; const RefIdColumn *mReach; const RefIdColumn *mChop[2]; const RefIdColumn *mSlash[2]; const RefIdColumn *mThrust[2]; std::map mFlags; WeaponColumns (const EnchantableColumns& columns); }; class WeaponRefIdAdapter : public EnchantableRefIdAdapter { WeaponColumns mColumns; public: WeaponRefIdAdapter (const WeaponColumns& columns); QVariant getData (const RefIdColumn *column, const RefIdData& data, int index) const override; void setData (const RefIdColumn *column, RefIdData& data, int index, const QVariant& value) const override; ///< If the data type does not match an exception is thrown. }; class NestedRefIdAdapterBase; class NpcAttributesRefIdAdapter : public NestedRefIdAdapterBase { public: NpcAttributesRefIdAdapter (); void addNestedRow (const RefIdColumn *column, RefIdData& data, int index, int position) const override; void removeNestedRow (const RefIdColumn *column, RefIdData& data, int index, int rowToRemove) const override; void setNestedTable (const RefIdColumn* column, RefIdData& data, int index, const NestedTableWrapperBase& nestedTable) const override; NestedTableWrapperBase* nestedTable (const RefIdColumn* column, const RefIdData& data, int index) const override; QVariant getNestedData (const RefIdColumn *column, const RefIdData& data, int index, int subRowIndex, int subColIndex) const override; void setNestedData (const RefIdColumn *column, RefIdData& data, int row, const QVariant& value, int subRowIndex, int subColIndex) const override; int getNestedColumnsCount(const RefIdColumn *column, const RefIdData& data) const override; int getNestedRowsCount(const RefIdColumn *column, const RefIdData& data, int index) const override; }; class NpcSkillsRefIdAdapter : public NestedRefIdAdapterBase { public: NpcSkillsRefIdAdapter (); void addNestedRow (const RefIdColumn *column, RefIdData& data, int index, int position) const override; void removeNestedRow (const RefIdColumn *column, RefIdData& data, int index, int rowToRemove) const override; void setNestedTable (const RefIdColumn* column, RefIdData& data, int index, const NestedTableWrapperBase& nestedTable) const override; NestedTableWrapperBase* nestedTable (const RefIdColumn* column, const RefIdData& data, int index) const override; QVariant getNestedData (const RefIdColumn *column, const RefIdData& data, int index, int subRowIndex, int subColIndex) const override; void setNestedData (const RefIdColumn *column, RefIdData& data, int row, const QVariant& value, int subRowIndex, int subColIndex) const override; int getNestedColumnsCount(const RefIdColumn *column, const RefIdData& data) const override; int getNestedRowsCount(const RefIdColumn *column, const RefIdData& data, int index) const override; }; class NpcMiscRefIdAdapter : public NestedRefIdAdapterBase { NpcMiscRefIdAdapter (const NpcMiscRefIdAdapter&); NpcMiscRefIdAdapter& operator= (const NpcMiscRefIdAdapter&); public: NpcMiscRefIdAdapter (); virtual ~NpcMiscRefIdAdapter(); void addNestedRow (const RefIdColumn *column, RefIdData& data, int index, int position) const override; void removeNestedRow (const RefIdColumn *column, RefIdData& data, int index, int rowToRemove) const override; void setNestedTable (const RefIdColumn* column, RefIdData& data, int index, const NestedTableWrapperBase& nestedTable) const override; NestedTableWrapperBase* nestedTable (const RefIdColumn* column, const RefIdData& data, int index) const override; QVariant getNestedData (const RefIdColumn *column, const RefIdData& data, int index, int subRowIndex, int subColIndex) const override; void setNestedData (const RefIdColumn *column, RefIdData& data, int row, const QVariant& value, int subRowIndex, int subColIndex) const override; int getNestedColumnsCount(const RefIdColumn *column, const RefIdData& data) const override; int getNestedRowsCount(const RefIdColumn *column, const RefIdData& data, int index) const override; }; class CreatureAttributesRefIdAdapter : public NestedRefIdAdapterBase { public: CreatureAttributesRefIdAdapter (); void addNestedRow (const RefIdColumn *column, RefIdData& data, int index, int position) const override; void removeNestedRow (const RefIdColumn *column, RefIdData& data, int index, int rowToRemove) const override; void setNestedTable (const RefIdColumn* column, RefIdData& data, int index, const NestedTableWrapperBase& nestedTable) const override; NestedTableWrapperBase* nestedTable (const RefIdColumn* column, const RefIdData& data, int index) const override; QVariant getNestedData (const RefIdColumn *column, const RefIdData& data, int index, int subRowIndex, int subColIndex) const override; void setNestedData (const RefIdColumn *column, RefIdData& data, int row, const QVariant& value, int subRowIndex, int subColIndex) const override; int getNestedColumnsCount(const RefIdColumn *column, const RefIdData& data) const override; int getNestedRowsCount(const RefIdColumn *column, const RefIdData& data, int index) const override; }; class CreatureAttackRefIdAdapter : public NestedRefIdAdapterBase { public: CreatureAttackRefIdAdapter (); void addNestedRow (const RefIdColumn *column, RefIdData& data, int index, int position) const override; void removeNestedRow (const RefIdColumn *column, RefIdData& data, int index, int rowToRemove) const override; void setNestedTable (const RefIdColumn* column, RefIdData& data, int index, const NestedTableWrapperBase& nestedTable) const override; NestedTableWrapperBase* nestedTable (const RefIdColumn* column, const RefIdData& data, int index) const override; QVariant getNestedData (const RefIdColumn *column, const RefIdData& data, int index, int subRowIndex, int subColIndex) const override; void setNestedData (const RefIdColumn *column, RefIdData& data, int row, const QVariant& value, int subRowIndex, int subColIndex) const override; int getNestedColumnsCount(const RefIdColumn *column, const RefIdData& data) const override; int getNestedRowsCount(const RefIdColumn *column, const RefIdData& data, int index) const override; }; class CreatureMiscRefIdAdapter : public NestedRefIdAdapterBase { CreatureMiscRefIdAdapter (const CreatureMiscRefIdAdapter&); CreatureMiscRefIdAdapter& operator= (const CreatureMiscRefIdAdapter&); public: CreatureMiscRefIdAdapter (); virtual ~CreatureMiscRefIdAdapter(); void addNestedRow (const RefIdColumn *column, RefIdData& data, int index, int position) const override; void removeNestedRow (const RefIdColumn *column, RefIdData& data, int index, int rowToRemove) const override; void setNestedTable (const RefIdColumn* column, RefIdData& data, int index, const NestedTableWrapperBase& nestedTable) const override; NestedTableWrapperBase* nestedTable (const RefIdColumn* column, const RefIdData& data, int index) const override; QVariant getNestedData (const RefIdColumn *column, const RefIdData& data, int index, int subRowIndex, int subColIndex) const override; void setNestedData (const RefIdColumn *column, RefIdData& data, int row, const QVariant& value, int subRowIndex, int subColIndex) const override; int getNestedColumnsCount(const RefIdColumn *column, const RefIdData& data) const override; int getNestedRowsCount(const RefIdColumn *column, const RefIdData& data, int index) const override; }; template class EffectsListAdapter; template class EffectsRefIdAdapter : public EffectsListAdapter, public NestedRefIdAdapterBase { UniversalId::Type mType; // not implemented EffectsRefIdAdapter (const EffectsRefIdAdapter&); EffectsRefIdAdapter& operator= (const EffectsRefIdAdapter&); public: EffectsRefIdAdapter(UniversalId::Type type) :mType(type) {} virtual ~EffectsRefIdAdapter() {} void addNestedRow (const RefIdColumn *column, RefIdData& data, int index, int position) const override { Record& record = static_cast&> (data.getRecord (RefIdData::LocalIndex (index, mType))); EffectsListAdapter::addRow(record, position); } void removeNestedRow (const RefIdColumn *column, RefIdData& data, int index, int rowToRemove) const override { Record& record = static_cast&> (data.getRecord (RefIdData::LocalIndex (index, mType))); EffectsListAdapter::removeRow(record, rowToRemove); } void setNestedTable (const RefIdColumn* column, RefIdData& data, int index, const NestedTableWrapperBase& nestedTable) const override { Record& record = static_cast&> (data.getRecord (RefIdData::LocalIndex (index, mType))); EffectsListAdapter::setTable(record, nestedTable); } NestedTableWrapperBase* nestedTable (const RefIdColumn* column, const RefIdData& data, int index) const override { const Record& record = static_cast&> (data.getRecord (RefIdData::LocalIndex (index, mType))); return EffectsListAdapter::table(record); } QVariant getNestedData (const RefIdColumn *column, const RefIdData& data, int index, int subRowIndex, int subColIndex) const override { const Record& record = static_cast&> (data.getRecord (RefIdData::LocalIndex (index, mType))); return EffectsListAdapter::getData(record, subRowIndex, subColIndex); } void setNestedData (const RefIdColumn *column, RefIdData& data, int row, const QVariant& value, int subRowIndex, int subColIndex) const override { Record& record = static_cast&> (data.getRecord (RefIdData::LocalIndex (row, mType))); EffectsListAdapter::setData(record, value, subRowIndex, subColIndex); } int getNestedColumnsCount(const RefIdColumn *column, const RefIdData& data) const override { const Record record; // not used, just a dummy return EffectsListAdapter::getColumnsCount(record); } int getNestedRowsCount(const RefIdColumn *column, const RefIdData& data, int index) const override { const Record& record = static_cast&> (data.getRecord (RefIdData::LocalIndex (index, mType))); return EffectsListAdapter::getRowsCount(record); } }; template class NestedInventoryRefIdAdapter : public NestedRefIdAdapterBase { UniversalId::Type mType; // not implemented NestedInventoryRefIdAdapter (const NestedInventoryRefIdAdapter&); NestedInventoryRefIdAdapter& operator= (const NestedInventoryRefIdAdapter&); public: NestedInventoryRefIdAdapter(UniversalId::Type type) :mType(type) {} virtual ~NestedInventoryRefIdAdapter() {} void addNestedRow (const RefIdColumn *column, RefIdData& data, int index, int position) const override { Record& record = static_cast&> (data.getRecord (RefIdData::LocalIndex (index, mType))); ESXRecordT container = record.get(); std::vector& list = container.mInventory.mList; ESM::ContItem newRow = ESM::ContItem(); if (position >= (int)list.size()) list.push_back(newRow); else list.insert(list.begin()+position, newRow); record.setModified (container); } void removeNestedRow (const RefIdColumn *column, RefIdData& data, int index, int rowToRemove) const override { Record& record = static_cast&> (data.getRecord (RefIdData::LocalIndex (index, mType))); ESXRecordT container = record.get(); std::vector& list = container.mInventory.mList; if (rowToRemove < 0 || rowToRemove >= static_cast (list.size())) throw std::runtime_error ("index out of range"); list.erase (list.begin () + rowToRemove); record.setModified (container); } void setNestedTable (const RefIdColumn* column, RefIdData& data, int index, const NestedTableWrapperBase& nestedTable) const override { Record& record = static_cast&> (data.getRecord (RefIdData::LocalIndex (index, mType))); ESXRecordT container = record.get(); container.mInventory.mList = static_cast >&>(nestedTable).mNestedTable; record.setModified (container); } NestedTableWrapperBase* nestedTable (const RefIdColumn* column, const RefIdData& data, int index) const override { const Record& record = static_cast&> (data.getRecord (RefIdData::LocalIndex (index, mType))); // deleted by dtor of NestedTableStoring return new NestedTableWrapper >(record.get().mInventory.mList); } QVariant getNestedData (const RefIdColumn *column, const RefIdData& data, int index, int subRowIndex, int subColIndex) const override { const Record& record = static_cast&> (data.getRecord (RefIdData::LocalIndex (index, mType))); const std::vector& list = record.get().mInventory.mList; if (subRowIndex < 0 || subRowIndex >= static_cast (list.size())) throw std::runtime_error ("index out of range"); const ESM::ContItem& content = list.at(subRowIndex); switch (subColIndex) { case 0: return QString::fromUtf8(content.mItem.c_str()); case 1: return content.mCount; default: throw std::runtime_error("Trying to access non-existing column in the nested table!"); } } void setNestedData (const RefIdColumn *column, RefIdData& data, int row, const QVariant& value, int subRowIndex, int subColIndex) const override { Record& record = static_cast&> (data.getRecord (RefIdData::LocalIndex (row, mType))); ESXRecordT container = record.get(); std::vector& list = container.mInventory.mList; if (subRowIndex < 0 || subRowIndex >= static_cast (list.size())) throw std::runtime_error ("index out of range"); switch(subColIndex) { case 0: list.at(subRowIndex).mItem.assign(std::string(value.toString().toUtf8().constData())); break; case 1: list.at(subRowIndex).mCount = value.toInt(); break; default: throw std::runtime_error("Trying to access non-existing column in the nested table!"); } record.setModified (container); } int getNestedColumnsCount(const RefIdColumn *column, const RefIdData& data) const override { return 2; } int getNestedRowsCount(const RefIdColumn *column, const RefIdData& data, int index) const override { const Record& record = static_cast&> (data.getRecord (RefIdData::LocalIndex (index, mType))); return static_cast(record.get().mInventory.mList.size()); } }; template class NestedSpellRefIdAdapter : public NestedRefIdAdapterBase { UniversalId::Type mType; // not implemented NestedSpellRefIdAdapter (const NestedSpellRefIdAdapter&); NestedSpellRefIdAdapter& operator= (const NestedSpellRefIdAdapter&); public: NestedSpellRefIdAdapter(UniversalId::Type type) :mType(type) {} virtual ~NestedSpellRefIdAdapter() {} void addNestedRow (const RefIdColumn *column, RefIdData& data, int index, int position) const override { Record& record = static_cast&> (data.getRecord (RefIdData::LocalIndex (index, mType))); ESXRecordT caster = record.get(); std::vector& list = caster.mSpells.mList; std::string newString; if (position >= (int)list.size()) list.push_back(newString); else list.insert(list.begin()+position, newString); record.setModified (caster); } void removeNestedRow (const RefIdColumn *column, RefIdData& data, int index, int rowToRemove) const override { Record& record = static_cast&> (data.getRecord (RefIdData::LocalIndex (index, mType))); ESXRecordT caster = record.get(); std::vector& list = caster.mSpells.mList; if (rowToRemove < 0 || rowToRemove >= static_cast (list.size())) throw std::runtime_error ("index out of range"); list.erase (list.begin () + rowToRemove); record.setModified (caster); } void setNestedTable (const RefIdColumn* column, RefIdData& data, int index, const NestedTableWrapperBase& nestedTable) const override { Record& record = static_cast&> (data.getRecord (RefIdData::LocalIndex (index, mType))); ESXRecordT caster = record.get(); caster.mSpells.mList = static_cast >&>(nestedTable).mNestedTable; record.setModified (caster); } NestedTableWrapperBase* nestedTable (const RefIdColumn* column, const RefIdData& data, int index) const override { const Record& record = static_cast&> (data.getRecord (RefIdData::LocalIndex (index, mType))); // deleted by dtor of NestedTableStoring return new NestedTableWrapper >(record.get().mSpells.mList); } QVariant getNestedData (const RefIdColumn *column, const RefIdData& data, int index, int subRowIndex, int subColIndex) const override { const Record& record = static_cast&> (data.getRecord (RefIdData::LocalIndex (index, mType))); const std::vector& list = record.get().mSpells.mList; if (subRowIndex < 0 || subRowIndex >= static_cast (list.size())) throw std::runtime_error ("index out of range"); const std::string& content = list.at(subRowIndex); if (subColIndex == 0) return QString::fromUtf8(content.c_str()); else throw std::runtime_error("Trying to access non-existing column in the nested table!"); } void setNestedData (const RefIdColumn *column, RefIdData& data, int row, const QVariant& value, int subRowIndex, int subColIndex) const override { Record& record = static_cast&> (data.getRecord (RefIdData::LocalIndex (row, mType))); ESXRecordT caster = record.get(); std::vector& list = caster.mSpells.mList; if (subRowIndex < 0 || subRowIndex >= static_cast (list.size())) throw std::runtime_error ("index out of range"); if (subColIndex == 0) list.at(subRowIndex) = std::string(value.toString().toUtf8()); else throw std::runtime_error("Trying to access non-existing column in the nested table!"); record.setModified (caster); } int getNestedColumnsCount(const RefIdColumn *column, const RefIdData& data) const override { return 1; } int getNestedRowsCount(const RefIdColumn *column, const RefIdData& data, int index) const override { const Record& record = static_cast&> (data.getRecord (RefIdData::LocalIndex (index, mType))); return static_cast(record.get().mSpells.mList.size()); } }; template class NestedTravelRefIdAdapter : public NestedRefIdAdapterBase { UniversalId::Type mType; // not implemented NestedTravelRefIdAdapter (const NestedTravelRefIdAdapter&); NestedTravelRefIdAdapter& operator= (const NestedTravelRefIdAdapter&); public: NestedTravelRefIdAdapter(UniversalId::Type type) :mType(type) {} virtual ~NestedTravelRefIdAdapter() {} void addNestedRow (const RefIdColumn *column, RefIdData& data, int index, int position) const override { Record& record = static_cast&> (data.getRecord (RefIdData::LocalIndex (index, mType))); ESXRecordT traveller = record.get(); std::vector& list = traveller.mTransport.mList; ESM::Position newPos; for (unsigned i = 0; i < 3; ++i) { newPos.pos[i] = 0; newPos.rot[i] = 0; } ESM::Transport::Dest newRow; newRow.mPos = newPos; newRow.mCellName = ""; if (position >= (int)list.size()) list.push_back(newRow); else list.insert(list.begin()+position, newRow); record.setModified (traveller); } void removeNestedRow (const RefIdColumn *column, RefIdData& data, int index, int rowToRemove) const override { Record& record = static_cast&> (data.getRecord (RefIdData::LocalIndex (index, mType))); ESXRecordT traveller = record.get(); std::vector& list = traveller.mTransport.mList; if (rowToRemove < 0 || rowToRemove >= static_cast (list.size())) throw std::runtime_error ("index out of range"); list.erase (list.begin () + rowToRemove); record.setModified (traveller); } void setNestedTable (const RefIdColumn* column, RefIdData& data, int index, const NestedTableWrapperBase& nestedTable) const override { Record& record = static_cast&> (data.getRecord (RefIdData::LocalIndex (index, mType))); ESXRecordT traveller = record.get(); traveller.mTransport.mList = static_cast >&>(nestedTable).mNestedTable; record.setModified (traveller); } NestedTableWrapperBase* nestedTable (const RefIdColumn* column, const RefIdData& data, int index) const override { const Record& record = static_cast&> (data.getRecord (RefIdData::LocalIndex (index, mType))); // deleted by dtor of NestedTableStoring return new NestedTableWrapper >(record.get().mTransport.mList); } QVariant getNestedData (const RefIdColumn *column, const RefIdData& data, int index, int subRowIndex, int subColIndex) const override { const Record& record = static_cast&> (data.getRecord (RefIdData::LocalIndex (index, mType))); const std::vector& list = record.get().mTransport.mList; if (subRowIndex < 0 || subRowIndex >= static_cast (list.size())) throw std::runtime_error ("index out of range"); const ESM::Transport::Dest& content = list.at(subRowIndex); switch (subColIndex) { case 0: return QString::fromUtf8(content.mCellName.c_str()); case 1: return content.mPos.pos[0]; case 2: return content.mPos.pos[1]; case 3: return content.mPos.pos[2]; case 4: return content.mPos.rot[0]; case 5: return content.mPos.rot[1]; case 6: return content.mPos.rot[2]; default: throw std::runtime_error("Trying to access non-existing column in the nested table!"); } } void setNestedData (const RefIdColumn *column, RefIdData& data, int row, const QVariant& value, int subRowIndex, int subColIndex) const override { Record& record = static_cast&> (data.getRecord (RefIdData::LocalIndex (row, mType))); ESXRecordT traveller = record.get(); std::vector& list = traveller.mTransport.mList; if (subRowIndex < 0 || subRowIndex >= static_cast (list.size())) throw std::runtime_error ("index out of range"); switch(subColIndex) { case 0: list.at(subRowIndex).mCellName = std::string(value.toString().toUtf8().constData()); break; case 1: list.at(subRowIndex).mPos.pos[0] = value.toFloat(); break; case 2: list.at(subRowIndex).mPos.pos[1] = value.toFloat(); break; case 3: list.at(subRowIndex).mPos.pos[2] = value.toFloat(); break; case 4: list.at(subRowIndex).mPos.rot[0] = value.toFloat(); break; case 5: list.at(subRowIndex).mPos.rot[1] = value.toFloat(); break; case 6: list.at(subRowIndex).mPos.rot[2] = value.toFloat(); break; default: throw std::runtime_error("Trying to access non-existing column in the nested table!"); } record.setModified (traveller); } int getNestedColumnsCount(const RefIdColumn *column, const RefIdData& data) const override { return 7; } int getNestedRowsCount(const RefIdColumn *column, const RefIdData& data, int index) const override { const Record& record = static_cast&> (data.getRecord (RefIdData::LocalIndex (index, mType))); return static_cast(record.get().mTransport.mList.size()); } }; template class ActorAiRefIdAdapter : public NestedRefIdAdapterBase { UniversalId::Type mType; // not implemented ActorAiRefIdAdapter (const ActorAiRefIdAdapter&); ActorAiRefIdAdapter& operator= (const ActorAiRefIdAdapter&); public: ActorAiRefIdAdapter(UniversalId::Type type) :mType(type) {} virtual ~ActorAiRefIdAdapter() {} // FIXME: should check if the AI package type is already in the list and use a default // that wasn't used already (in extreme case do not add anything at all? void addNestedRow (const RefIdColumn *column, RefIdData& data, int index, int position) const override { Record& record = static_cast&> (data.getRecord (RefIdData::LocalIndex (index, mType))); ESXRecordT actor = record.get(); std::vector& list = actor.mAiPackage.mList; ESM::AIPackage newRow; newRow.mType = ESM::AI_Wander; newRow.mWander.mDistance = 0; newRow.mWander.mDuration = 0; newRow.mWander.mTimeOfDay = 0; for (int i = 0; i < 8; ++i) newRow.mWander.mIdle[i] = 0; newRow.mWander.mShouldRepeat = 0; newRow.mCellName = ""; if (position >= (int)list.size()) list.push_back(newRow); else list.insert(list.begin()+position, newRow); record.setModified (actor); } void removeNestedRow (const RefIdColumn *column, RefIdData& data, int index, int rowToRemove) const override { Record& record = static_cast&> (data.getRecord (RefIdData::LocalIndex (index, mType))); ESXRecordT actor = record.get(); std::vector& list = actor.mAiPackage.mList; if (rowToRemove < 0 || rowToRemove >= static_cast (list.size())) throw std::runtime_error ("index out of range"); list.erase (list.begin () + rowToRemove); record.setModified (actor); } void setNestedTable (const RefIdColumn* column, RefIdData& data, int index, const NestedTableWrapperBase& nestedTable) const override { Record& record = static_cast&> (data.getRecord (RefIdData::LocalIndex (index, mType))); ESXRecordT actor = record.get(); actor.mAiPackage.mList = static_cast >&>(nestedTable).mNestedTable; record.setModified (actor); } NestedTableWrapperBase* nestedTable (const RefIdColumn* column, const RefIdData& data, int index) const override { const Record& record = static_cast&> (data.getRecord (RefIdData::LocalIndex (index, mType))); // deleted by dtor of NestedTableStoring return new NestedTableWrapper >(record.get().mAiPackage.mList); } QVariant getNestedData (const RefIdColumn *column, const RefIdData& data, int index, int subRowIndex, int subColIndex) const override { const Record& record = static_cast&> (data.getRecord (RefIdData::LocalIndex (index, mType))); const std::vector& list = record.get().mAiPackage.mList; if (subRowIndex < 0 || subRowIndex >= static_cast (list.size())) throw std::runtime_error ("index out of range"); const ESM::AIPackage& content = list.at(subRowIndex); switch (subColIndex) { case 0: // FIXME: should more than one AI package type be allowed? Check vanilla switch (content.mType) { case ESM::AI_Wander: return 0; case ESM::AI_Travel: return 1; case ESM::AI_Follow: return 2; case ESM::AI_Escort: return 3; case ESM::AI_Activate: return 4; case ESM::AI_CNDT: default: return QVariant(); } case 1: // wander dist if (content.mType == ESM::AI_Wander) return content.mWander.mDistance; else return QVariant(); case 2: // wander dur if (content.mType == ESM::AI_Wander || content.mType == ESM::AI_Follow || content.mType == ESM::AI_Escort) return content.mWander.mDuration; else return QVariant(); case 3: // wander ToD if (content.mType == ESM::AI_Wander) return content.mWander.mTimeOfDay; // FIXME: not sure of the format else return QVariant(); case 4: // wander idle case 5: case 6: case 7: case 8: case 9: case 10: case 11: if (content.mType == ESM::AI_Wander) return static_cast(content.mWander.mIdle[subColIndex-4]); else return QVariant(); case 12: // wander repeat if (content.mType == ESM::AI_Wander) return content.mWander.mShouldRepeat != 0; else return QVariant(); case 13: // activate name if (content.mType == ESM::AI_Activate) return QString(content.mActivate.mName.toString().c_str()); else return QVariant(); case 14: // target id if (content.mType == ESM::AI_Follow || content.mType == ESM::AI_Escort) return QString(content.mTarget.mId.toString().c_str()); else return QVariant(); case 15: // target cell if (content.mType == ESM::AI_Follow || content.mType == ESM::AI_Escort) return QString::fromUtf8(content.mCellName.c_str()); else return QVariant(); case 16: if (content.mType == ESM::AI_Travel) return content.mTravel.mX; else if (content.mType == ESM::AI_Follow || content.mType == ESM::AI_Escort) return content.mTarget.mX; else return QVariant(); case 17: if (content.mType == ESM::AI_Travel) return content.mTravel.mY; else if (content.mType == ESM::AI_Follow || content.mType == ESM::AI_Escort) return content.mTarget.mY; else return QVariant(); case 18: if (content.mType == ESM::AI_Travel) return content.mTravel.mZ; else if (content.mType == ESM::AI_Follow || content.mType == ESM::AI_Escort) return content.mTarget.mZ; else return QVariant(); default: throw std::runtime_error("Trying to access non-existing column in the nested table!"); } } void setNestedData (const RefIdColumn *column, RefIdData& data, int row, const QVariant& value, int subRowIndex, int subColIndex) const override { Record& record = static_cast&> (data.getRecord (RefIdData::LocalIndex (row, mType))); ESXRecordT actor = record.get(); std::vector& list = actor.mAiPackage.mList; if (subRowIndex < 0 || subRowIndex >= static_cast (list.size())) throw std::runtime_error ("index out of range"); ESM::AIPackage& content = list.at(subRowIndex); switch(subColIndex) { case 0: // ai package type switch (value.toInt()) { case 0: content.mType = ESM::AI_Wander; break; case 1: content.mType = ESM::AI_Travel; break; case 2: content.mType = ESM::AI_Follow; break; case 3: content.mType = ESM::AI_Escort; break; case 4: content.mType = ESM::AI_Activate; break; default: return; // return without saving } break; // always save case 1: if (content.mType == ESM::AI_Wander) content.mWander.mDistance = static_cast(value.toInt()); else return; // return without saving break; // always save case 2: if (content.mType == ESM::AI_Wander || content.mType == ESM::AI_Follow || content.mType == ESM::AI_Escort) content.mWander.mDuration = static_cast(value.toInt()); else return; // return without saving case 3: if (content.mType == ESM::AI_Wander) content.mWander.mTimeOfDay = static_cast(value.toInt()); else return; // return without saving break; // always save case 4: case 5: case 6: case 7: case 8: case 9: case 10: case 11: if (content.mType == ESM::AI_Wander) content.mWander.mIdle[subColIndex-4] = static_cast(value.toInt()); else return; // return without saving break; // always save case 12: if (content.mType == ESM::AI_Wander) content.mWander.mShouldRepeat = static_cast(value.toInt()); else return; // return without saving break; // always save case 13: // NAME32 if (content.mType == ESM::AI_Activate) content.mActivate.mName.assign(value.toString().toUtf8().constData()); else return; // return without saving break; // always save case 14: // NAME32 if (content.mType == ESM::AI_Follow || content.mType == ESM::AI_Escort) content.mTarget.mId.assign(value.toString().toUtf8().constData()); else return; // return without saving break; // always save case 15: if (content.mType == ESM::AI_Follow || content.mType == ESM::AI_Escort) content.mCellName = std::string(value.toString().toUtf8().constData()); else return; // return without saving break; // always save case 16: if (content.mType == ESM::AI_Travel) content.mTravel.mX = value.toFloat(); else if (content.mType == ESM::AI_Follow || content.mType == ESM::AI_Escort) content.mTarget.mX = value.toFloat(); else return; // return without saving break; // always save case 17: if (content.mType == ESM::AI_Travel) content.mTravel.mY = value.toFloat(); else if (content.mType == ESM::AI_Follow || content.mType == ESM::AI_Escort) content.mTarget.mY = value.toFloat(); else return; // return without saving break; // always save case 18: if (content.mType == ESM::AI_Travel) content.mTravel.mZ = value.toFloat(); else if (content.mType == ESM::AI_Follow || content.mType == ESM::AI_Escort) content.mTarget.mZ = value.toFloat(); else return; // return without saving break; // always save default: throw std::runtime_error("Trying to access non-existing column in the nested table!"); } record.setModified (actor); } int getNestedColumnsCount(const RefIdColumn *column, const RefIdData& data) const override { return 19; } int getNestedRowsCount(const RefIdColumn *column, const RefIdData& data, int index) const override { const Record& record = static_cast&> (data.getRecord (RefIdData::LocalIndex (index, mType))); return static_cast(record.get().mAiPackage.mList.size()); } }; template class BodyPartRefIdAdapter : public NestedRefIdAdapterBase { UniversalId::Type mType; // not implemented BodyPartRefIdAdapter (const BodyPartRefIdAdapter&); BodyPartRefIdAdapter& operator= (const BodyPartRefIdAdapter&); public: BodyPartRefIdAdapter(UniversalId::Type type) :mType(type) {} virtual ~BodyPartRefIdAdapter() {} void addNestedRow (const RefIdColumn *column, RefIdData& data, int index, int position) const override { Record& record = static_cast&> (data.getRecord (RefIdData::LocalIndex (index, mType))); ESXRecordT apparel = record.get(); std::vector& list = apparel.mParts.mParts; ESM::PartReference newPart; newPart.mPart = 0; // 0 == head newPart.mMale = ""; newPart.mFemale = ""; if (position >= (int)list.size()) list.push_back(newPart); else list.insert(list.begin()+position, newPart); record.setModified (apparel); } void removeNestedRow (const RefIdColumn *column, RefIdData& data, int index, int rowToRemove) const override { Record& record = static_cast&> (data.getRecord (RefIdData::LocalIndex (index, mType))); ESXRecordT apparel = record.get(); std::vector& list = apparel.mParts.mParts; if (rowToRemove < 0 || rowToRemove >= static_cast (list.size())) throw std::runtime_error ("index out of range"); list.erase (list.begin () + rowToRemove); record.setModified (apparel); } void setNestedTable (const RefIdColumn* column, RefIdData& data, int index, const NestedTableWrapperBase& nestedTable) const override { Record& record = static_cast&> (data.getRecord (RefIdData::LocalIndex (index, mType))); ESXRecordT apparel = record.get(); apparel.mParts.mParts = static_cast >&>(nestedTable).mNestedTable; record.setModified (apparel); } NestedTableWrapperBase* nestedTable (const RefIdColumn* column, const RefIdData& data, int index) const override { const Record& record = static_cast&> (data.getRecord (RefIdData::LocalIndex (index, mType))); // deleted by dtor of NestedTableStoring return new NestedTableWrapper >(record.get().mParts.mParts); } QVariant getNestedData (const RefIdColumn *column, const RefIdData& data, int index, int subRowIndex, int subColIndex) const override { const Record& record = static_cast&> (data.getRecord (RefIdData::LocalIndex (index, mType))); const std::vector& list = record.get().mParts.mParts; if (subRowIndex < 0 || subRowIndex >= static_cast (list.size())) throw std::runtime_error ("index out of range"); const ESM::PartReference& content = list.at(subRowIndex); switch (subColIndex) { case 0: { if (content.mPart < ESM::PRT_Count) return content.mPart; else throw std::runtime_error("Part Reference Type unexpected value"); } case 1: return QString(content.mMale.c_str()); case 2: return QString(content.mFemale.c_str()); default: throw std::runtime_error("Trying to access non-existing column in the nested table!"); } } void setNestedData (const RefIdColumn *column, RefIdData& data, int row, const QVariant& value, int subRowIndex, int subColIndex) const override { Record& record = static_cast&> (data.getRecord (RefIdData::LocalIndex (row, mType))); ESXRecordT apparel = record.get(); std::vector& list = apparel.mParts.mParts; if (subRowIndex < 0 || subRowIndex >= static_cast (list.size())) throw std::runtime_error ("index out of range"); switch(subColIndex) { case 0: list.at(subRowIndex).mPart = static_cast(value.toInt()); break; case 1: list.at(subRowIndex).mMale = value.toString().toStdString(); break; case 2: list.at(subRowIndex).mFemale = value.toString().toStdString(); break; default: throw std::runtime_error("Trying to access non-existing column in the nested table!"); } record.setModified (apparel); } int getNestedColumnsCount(const RefIdColumn *column, const RefIdData& data) const override { return 3; } int getNestedRowsCount(const RefIdColumn *column, const RefIdData& data, int index) const override { const Record& record = static_cast&> (data.getRecord (RefIdData::LocalIndex (index, mType))); return static_cast(record.get().mParts.mParts.size()); } }; struct LevListColumns : public BaseColumns { const RefIdColumn *mLevList; const RefIdColumn *mNestedListLevList; LevListColumns (const BaseColumns& base) : BaseColumns (base) , mLevList(nullptr) , mNestedListLevList(nullptr) {} }; template class LevelledListRefIdAdapter : public BaseRefIdAdapter { LevListColumns mLevList; public: LevelledListRefIdAdapter (UniversalId::Type type, const LevListColumns &columns); QVariant getData (const RefIdColumn *column, const RefIdData& data, int index) const override; void setData (const RefIdColumn *column, RefIdData& data, int index, const QVariant& value) const override; ///< If the data type does not match an exception is thrown. }; template LevelledListRefIdAdapter::LevelledListRefIdAdapter (UniversalId::Type type, const LevListColumns &columns) : BaseRefIdAdapter (type, columns), mLevList (columns) {} template QVariant LevelledListRefIdAdapter::getData (const RefIdColumn *column, const RefIdData& data, int index) const { if (column==mLevList.mLevList || column == mLevList.mNestedListLevList) return QVariant::fromValue(ColumnBase::TableEdit_Full); return BaseRefIdAdapter::getData (column, data, index); } template void LevelledListRefIdAdapter::setData (const RefIdColumn *column, RefIdData& data, int index, const QVariant& value) const { BaseRefIdAdapter::setData (column, data, index, value); return; } // for non-tables template class NestedListLevListRefIdAdapter : public NestedRefIdAdapterBase { UniversalId::Type mType; // not implemented NestedListLevListRefIdAdapter (const NestedListLevListRefIdAdapter&); NestedListLevListRefIdAdapter& operator= (const NestedListLevListRefIdAdapter&); public: NestedListLevListRefIdAdapter(UniversalId::Type type) :mType(type) {} virtual ~NestedListLevListRefIdAdapter() {} void addNestedRow (const RefIdColumn *column, RefIdData& data, int index, int position) const override { throw std::logic_error ("cannot add a row to a fixed table"); } void removeNestedRow (const RefIdColumn *column, RefIdData& data, int index, int rowToRemove) const override { throw std::logic_error ("cannot remove a row to a fixed table"); } void setNestedTable (const RefIdColumn* column, RefIdData& data, int index, const NestedTableWrapperBase& nestedTable) const override { throw std::logic_error ("table operation not supported"); } NestedTableWrapperBase* nestedTable (const RefIdColumn* column, const RefIdData& data, int index) const override { throw std::logic_error ("table operation not supported"); } QVariant getNestedData (const RefIdColumn *column, const RefIdData& data, int index, int subRowIndex, int subColIndex) const override { const Record& record = static_cast&> (data.getRecord (RefIdData::LocalIndex (index, mType))); if (mType == UniversalId::Type_CreatureLevelledList) { switch (subColIndex) { case 0: return QVariant(); // disable the checkbox editor case 1: return record.get().mFlags & ESM::CreatureLevList::AllLevels; case 2: return static_cast (record.get().mChanceNone); default: throw std::runtime_error("Trying to access non-existing column in levelled creatues!"); } } else { switch (subColIndex) { case 0: return record.get().mFlags & ESM::ItemLevList::Each; case 1: return record.get().mFlags & ESM::ItemLevList::AllLevels; case 2: return static_cast (record.get().mChanceNone); default: throw std::runtime_error("Trying to access non-existing column in levelled items!"); } } } void setNestedData (const RefIdColumn *column, RefIdData& data, int row, const QVariant& value, int subRowIndex, int subColIndex) const override { Record& record = static_cast&> (data.getRecord (RefIdData::LocalIndex (row, mType))); ESXRecordT leveled = record.get(); if (mType == UniversalId::Type_CreatureLevelledList) { switch(subColIndex) { case 0: return; // return without saving case 1: { if(value.toBool()) { leveled.mFlags |= ESM::CreatureLevList::AllLevels; break; } else { leveled.mFlags &= ~ESM::CreatureLevList::AllLevels; break; } } case 2: leveled.mChanceNone = static_cast(value.toInt()); break; default: throw std::runtime_error("Trying to set non-existing column in levelled creatures!"); } } else { switch(subColIndex) { case 0: { if(value.toBool()) { leveled.mFlags |= ESM::ItemLevList::Each; break; } else { leveled.mFlags &= ~ESM::ItemLevList::Each; break; } } case 1: { if(value.toBool()) { leveled.mFlags |= ESM::ItemLevList::AllLevels; break; } else { leveled.mFlags &= ~ESM::ItemLevList::AllLevels; break; } } case 2: leveled.mChanceNone = static_cast(value.toInt()); break; default: throw std::runtime_error("Trying to set non-existing column in levelled items!"); } } record.setModified (leveled); } int getNestedColumnsCount(const RefIdColumn *column, const RefIdData& data) const override { return 3; } int getNestedRowsCount(const RefIdColumn *column, const RefIdData& data, int index) const override { return 1; // fixed at size 1 } }; // for tables template class NestedLevListRefIdAdapter : public NestedRefIdAdapterBase { UniversalId::Type mType; // not implemented NestedLevListRefIdAdapter (const NestedLevListRefIdAdapter&); NestedLevListRefIdAdapter& operator= (const NestedLevListRefIdAdapter&); public: NestedLevListRefIdAdapter(UniversalId::Type type) :mType(type) {} virtual ~NestedLevListRefIdAdapter() {} void addNestedRow (const RefIdColumn *column, RefIdData& data, int index, int position) const override { Record& record = static_cast&> (data.getRecord (RefIdData::LocalIndex (index, mType))); ESXRecordT leveled = record.get(); std::vector& list = leveled.mList; ESM::LevelledListBase::LevelItem newItem; newItem.mId = ""; newItem.mLevel = 0; if (position >= (int)list.size()) list.push_back(newItem); else list.insert(list.begin()+position, newItem); record.setModified (leveled); } void removeNestedRow (const RefIdColumn *column, RefIdData& data, int index, int rowToRemove) const override { Record& record = static_cast&> (data.getRecord (RefIdData::LocalIndex (index, mType))); ESXRecordT leveled = record.get(); std::vector& list = leveled.mList; if (rowToRemove < 0 || rowToRemove >= static_cast (list.size())) throw std::runtime_error ("index out of range"); list.erase (list.begin () + rowToRemove); record.setModified (leveled); } void setNestedTable (const RefIdColumn* column, RefIdData& data, int index, const NestedTableWrapperBase& nestedTable) const override { Record& record = static_cast&> (data.getRecord (RefIdData::LocalIndex (index, mType))); ESXRecordT leveled = record.get(); leveled.mList = static_cast >&>(nestedTable).mNestedTable; record.setModified (leveled); } NestedTableWrapperBase* nestedTable (const RefIdColumn* column, const RefIdData& data, int index) const override { const Record& record = static_cast&> (data.getRecord (RefIdData::LocalIndex (index, mType))); // deleted by dtor of NestedTableStoring return new NestedTableWrapper >(record.get().mList); } QVariant getNestedData (const RefIdColumn *column, const RefIdData& data, int index, int subRowIndex, int subColIndex) const override { const Record& record = static_cast&> (data.getRecord (RefIdData::LocalIndex (index, mType))); const std::vector& list = record.get().mList; if (subRowIndex < 0 || subRowIndex >= static_cast (list.size())) throw std::runtime_error ("index out of range"); const ESM::LevelledListBase::LevelItem& content = list.at(subRowIndex); switch (subColIndex) { case 0: return QString(content.mId.c_str()); case 1: return content.mLevel; default: throw std::runtime_error("Trying to access non-existing column in the nested table!"); } } void setNestedData (const RefIdColumn *column, RefIdData& data, int row, const QVariant& value, int subRowIndex, int subColIndex) const override { Record& record = static_cast&> (data.getRecord (RefIdData::LocalIndex (row, mType))); ESXRecordT leveled = record.get(); std::vector& list = leveled.mList; if (subRowIndex < 0 || subRowIndex >= static_cast (list.size())) throw std::runtime_error ("index out of range"); switch(subColIndex) { case 0: list.at(subRowIndex).mId = value.toString().toStdString(); break; case 1: list.at(subRowIndex).mLevel = static_cast(value.toInt()); break; default: throw std::runtime_error("Trying to access non-existing column in the nested table!"); } record.setModified (leveled); } int getNestedColumnsCount(const RefIdColumn *column, const RefIdData& data) const override { return 2; } int getNestedRowsCount(const RefIdColumn *column, const RefIdData& data, int index) const override { const Record& record = static_cast&> (data.getRecord (RefIdData::LocalIndex (index, mType))); return static_cast(record.get().mList.size()); } }; } #endif ================================================ FILE: apps/opencs/model/world/refidcollection.cpp ================================================ #include "refidcollection.hpp" #include #include #include #include "refidadapter.hpp" #include "refidadapterimp.hpp" #include "columns.hpp" #include "nestedtablewrapper.hpp" #include "nestedcoladapterimp.hpp" CSMWorld::RefIdColumn::RefIdColumn (int columnId, Display displayType, int flag, bool editable, bool userEditable) : NestableColumn (columnId, displayType, flag), mEditable (editable), mUserEditable (userEditable) {} bool CSMWorld::RefIdColumn::isEditable() const { return mEditable; } bool CSMWorld::RefIdColumn::isUserEditable() const { return mUserEditable; } const CSMWorld::RefIdAdapter& CSMWorld::RefIdCollection::findAdapter (UniversalId::Type type) const { std::map::const_iterator iter = mAdapters.find (type); if (iter==mAdapters.end()) throw std::logic_error ("unsupported type in RefIdCollection"); return *iter->second; } CSMWorld::RefIdCollection::RefIdCollection() { BaseColumns baseColumns; mColumns.emplace_back(Columns::ColumnId_Id, ColumnBase::Display_Id, ColumnBase::Flag_Table | ColumnBase::Flag_Dialogue, false, false); baseColumns.mId = &mColumns.back(); mColumns.emplace_back(Columns::ColumnId_Modification, ColumnBase::Display_RecordState, ColumnBase::Flag_Table | ColumnBase::Flag_Dialogue, true, false); baseColumns.mModified = &mColumns.back(); mColumns.emplace_back(Columns::ColumnId_RecordType, ColumnBase::Display_RefRecordType, ColumnBase::Flag_Table | ColumnBase::Flag_Dialogue, false, false); baseColumns.mType = &mColumns.back(); ModelColumns modelColumns (baseColumns); mColumns.emplace_back(Columns::ColumnId_Model, ColumnBase::Display_Mesh); modelColumns.mModel = &mColumns.back(); NameColumns nameColumns (modelColumns); mColumns.emplace_back(Columns::ColumnId_Name, ColumnBase::Display_String); nameColumns.mName = &mColumns.back(); mColumns.emplace_back(Columns::ColumnId_Script, ColumnBase::Display_Script); nameColumns.mScript = &mColumns.back(); InventoryColumns inventoryColumns (nameColumns); mColumns.emplace_back(Columns::ColumnId_Icon, ColumnBase::Display_Icon); inventoryColumns.mIcon = &mColumns.back(); mColumns.emplace_back(Columns::ColumnId_Weight, ColumnBase::Display_Float); inventoryColumns.mWeight = &mColumns.back(); mColumns.emplace_back(Columns::ColumnId_CoinValue, ColumnBase::Display_Integer); inventoryColumns.mValue = &mColumns.back(); IngredientColumns ingredientColumns (inventoryColumns); mColumns.emplace_back(Columns::ColumnId_EffectList, ColumnBase::Display_NestedHeader, ColumnBase::Flag_Dialogue); ingredientColumns.mEffects = &mColumns.back(); std::map ingredientEffectsMap; ingredientEffectsMap.insert(std::make_pair(UniversalId::Type_Ingredient, new IngredEffectRefIdAdapter ())); mNestedAdapters.emplace_back(&mColumns.back(), ingredientEffectsMap); mColumns.back().addColumn( new NestedChildColumn (Columns::ColumnId_EffectId, ColumnBase::Display_IngredEffectId)); mColumns.back().addColumn( new NestedChildColumn (Columns::ColumnId_Skill, ColumnBase::Display_EffectSkill)); mColumns.back().addColumn( new NestedChildColumn (Columns::ColumnId_Attribute, ColumnBase::Display_EffectAttribute)); // nested table PotionColumns potionColumns (inventoryColumns); mColumns.emplace_back(Columns::ColumnId_EffectList, ColumnBase::Display_NestedHeader, ColumnBase::Flag_Dialogue); potionColumns.mEffects = &mColumns.back(); // see refidadapterimp.hpp std::map effectsMap; effectsMap.insert(std::make_pair(UniversalId::Type_Potion, new EffectsRefIdAdapter (UniversalId::Type_Potion))); mNestedAdapters.emplace_back(&mColumns.back(), effectsMap); mColumns.back().addColumn( new NestedChildColumn (Columns::ColumnId_EffectId, ColumnBase::Display_EffectId)); mColumns.back().addColumn( new NestedChildColumn (Columns::ColumnId_Skill, ColumnBase::Display_EffectSkill)); mColumns.back().addColumn( new NestedChildColumn (Columns::ColumnId_Attribute, ColumnBase::Display_EffectAttribute)); mColumns.back().addColumn( new NestedChildColumn (Columns::ColumnId_EffectRange, ColumnBase::Display_EffectRange)); mColumns.back().addColumn( new NestedChildColumn (Columns::ColumnId_EffectArea, ColumnBase::Display_String)); mColumns.back().addColumn( new NestedChildColumn (Columns::ColumnId_Duration, ColumnBase::Display_Integer)); // reuse from light mColumns.back().addColumn( new NestedChildColumn (Columns::ColumnId_MinMagnitude, ColumnBase::Display_Integer)); mColumns.back().addColumn( new NestedChildColumn (Columns::ColumnId_MaxMagnitude, ColumnBase::Display_Integer)); EnchantableColumns enchantableColumns (inventoryColumns); mColumns.emplace_back(Columns::ColumnId_Enchantment, ColumnBase::Display_Enchantment); enchantableColumns.mEnchantment = &mColumns.back(); mColumns.emplace_back(Columns::ColumnId_EnchantmentPoints, ColumnBase::Display_Integer); enchantableColumns.mEnchantmentPoints = &mColumns.back(); ToolColumns toolsColumns (inventoryColumns); mColumns.emplace_back(Columns::ColumnId_Quality, ColumnBase::Display_Float); toolsColumns.mQuality = &mColumns.back(); mColumns.emplace_back(Columns::ColumnId_Charges, ColumnBase::Display_Integer); toolsColumns.mUses = &mColumns.back(); ActorColumns actorsColumns (nameColumns); mColumns.emplace_back(Columns::ColumnId_AiHello, ColumnBase::Display_UnsignedInteger16); actorsColumns.mHello = &mColumns.back(); mColumns.emplace_back(Columns::ColumnId_AiFlee, ColumnBase::Display_UnsignedInteger8); actorsColumns.mFlee = &mColumns.back(); mColumns.emplace_back(Columns::ColumnId_AiFight, ColumnBase::Display_UnsignedInteger8); actorsColumns.mFight = &mColumns.back(); mColumns.emplace_back(Columns::ColumnId_AiAlarm, ColumnBase::Display_UnsignedInteger8); actorsColumns.mAlarm = &mColumns.back(); // Nested table mColumns.emplace_back(Columns::ColumnId_ActorInventory, ColumnBase::Display_NestedHeader, ColumnBase::Flag_Dialogue); actorsColumns.mInventory = &mColumns.back(); std::map inventoryMap; inventoryMap.insert(std::make_pair(UniversalId::Type_Npc, new NestedInventoryRefIdAdapter (UniversalId::Type_Npc))); inventoryMap.insert(std::make_pair(UniversalId::Type_Creature, new NestedInventoryRefIdAdapter (UniversalId::Type_Creature))); mNestedAdapters.emplace_back(&mColumns.back(), inventoryMap); mColumns.back().addColumn( new RefIdColumn (Columns::ColumnId_InventoryItemId, CSMWorld::ColumnBase::Display_Referenceable)); mColumns.back().addColumn( new RefIdColumn (Columns::ColumnId_ItemCount, CSMWorld::ColumnBase::Display_Integer)); // Nested table mColumns.emplace_back(Columns::ColumnId_SpellList, ColumnBase::Display_NestedHeader, ColumnBase::Flag_Dialogue); actorsColumns.mSpells = &mColumns.back(); std::map spellsMap; spellsMap.insert(std::make_pair(UniversalId::Type_Npc, new NestedSpellRefIdAdapter (UniversalId::Type_Npc))); spellsMap.insert(std::make_pair(UniversalId::Type_Creature, new NestedSpellRefIdAdapter (UniversalId::Type_Creature))); mNestedAdapters.emplace_back(&mColumns.back(), spellsMap); mColumns.back().addColumn( new RefIdColumn (Columns::ColumnId_SpellId, CSMWorld::ColumnBase::Display_Spell)); // Nested table mColumns.emplace_back(Columns::ColumnId_NpcDestinations, ColumnBase::Display_NestedHeader, ColumnBase::Flag_Dialogue); actorsColumns.mDestinations = &mColumns.back(); std::map destMap; destMap.insert(std::make_pair(UniversalId::Type_Npc, new NestedTravelRefIdAdapter (UniversalId::Type_Npc))); destMap.insert(std::make_pair(UniversalId::Type_Creature, new NestedTravelRefIdAdapter (UniversalId::Type_Creature))); mNestedAdapters.emplace_back(&mColumns.back(), destMap); mColumns.back().addColumn( new RefIdColumn (Columns::ColumnId_DestinationCell, CSMWorld::ColumnBase::Display_Cell)); mColumns.back().addColumn( new RefIdColumn (Columns::ColumnId_PosX, CSMWorld::ColumnBase::Display_Float)); mColumns.back().addColumn( new RefIdColumn (Columns::ColumnId_PosY, CSMWorld::ColumnBase::Display_Float)); mColumns.back().addColumn( new RefIdColumn (Columns::ColumnId_PosZ, CSMWorld::ColumnBase::Display_Float)); mColumns.back().addColumn( new RefIdColumn (Columns::ColumnId_RotX, CSMWorld::ColumnBase::Display_Double)); mColumns.back().addColumn( new RefIdColumn (Columns::ColumnId_RotY, CSMWorld::ColumnBase::Display_Double)); mColumns.back().addColumn( new RefIdColumn (Columns::ColumnId_RotZ, CSMWorld::ColumnBase::Display_Double)); // Nested table mColumns.emplace_back(Columns::ColumnId_AiPackageList, ColumnBase::Display_NestedHeader, ColumnBase::Flag_Dialogue); actorsColumns.mAiPackages = &mColumns.back(); std::map aiMap; aiMap.insert(std::make_pair(UniversalId::Type_Npc, new ActorAiRefIdAdapter (UniversalId::Type_Npc))); aiMap.insert(std::make_pair(UniversalId::Type_Creature, new ActorAiRefIdAdapter (UniversalId::Type_Creature))); mNestedAdapters.emplace_back(&mColumns.back(), aiMap); mColumns.back().addColumn( new RefIdColumn (Columns::ColumnId_AiPackageType, CSMWorld::ColumnBase::Display_AiPackageType)); mColumns.back().addColumn( new RefIdColumn (Columns::ColumnId_AiWanderDist, CSMWorld::ColumnBase::Display_Integer)); mColumns.back().addColumn( new RefIdColumn (Columns::ColumnId_AiDuration, CSMWorld::ColumnBase::Display_Integer)); mColumns.back().addColumn( new RefIdColumn (Columns::ColumnId_AiWanderToD, CSMWorld::ColumnBase::Display_Integer)); mColumns.back().addColumn( new RefIdColumn (Columns::ColumnId_Idle1, CSMWorld::ColumnBase::Display_Integer)); mColumns.back().addColumn( new RefIdColumn (Columns::ColumnId_Idle2, CSMWorld::ColumnBase::Display_Integer)); mColumns.back().addColumn( new RefIdColumn (Columns::ColumnId_Idle3, CSMWorld::ColumnBase::Display_Integer)); mColumns.back().addColumn( new RefIdColumn (Columns::ColumnId_Idle4, CSMWorld::ColumnBase::Display_Integer)); mColumns.back().addColumn( new RefIdColumn (Columns::ColumnId_Idle5, CSMWorld::ColumnBase::Display_Integer)); mColumns.back().addColumn( new RefIdColumn (Columns::ColumnId_Idle6, CSMWorld::ColumnBase::Display_Integer)); mColumns.back().addColumn( new RefIdColumn (Columns::ColumnId_Idle7, CSMWorld::ColumnBase::Display_Integer)); mColumns.back().addColumn( new RefIdColumn (Columns::ColumnId_Idle8, CSMWorld::ColumnBase::Display_Integer)); mColumns.back().addColumn( new RefIdColumn (Columns::ColumnId_AiWanderRepeat, CSMWorld::ColumnBase::Display_Boolean)); mColumns.back().addColumn( new RefIdColumn (Columns::ColumnId_AiActivateName, CSMWorld::ColumnBase::Display_String)); mColumns.back().addColumn( new RefIdColumn (Columns::ColumnId_AiTargetId, CSMWorld::ColumnBase::Display_String)); mColumns.back().addColumn( new RefIdColumn (Columns::ColumnId_AiTargetCell, CSMWorld::ColumnBase::Display_String)); mColumns.back().addColumn( new RefIdColumn (Columns::ColumnId_PosX, CSMWorld::ColumnBase::Display_Float)); mColumns.back().addColumn( new RefIdColumn (Columns::ColumnId_PosY, CSMWorld::ColumnBase::Display_Float)); mColumns.back().addColumn( new RefIdColumn (Columns::ColumnId_PosZ, CSMWorld::ColumnBase::Display_Float)); static const struct { int mName; unsigned int mFlag; } sServiceTable[] = { { Columns::ColumnId_BuysWeapons, ESM::NPC::Weapon}, { Columns::ColumnId_BuysArmor, ESM::NPC::Armor}, { Columns::ColumnId_BuysClothing, ESM::NPC::Clothing}, { Columns::ColumnId_BuysBooks, ESM::NPC::Books}, { Columns::ColumnId_BuysIngredients, ESM::NPC::Ingredients}, { Columns::ColumnId_BuysLockpicks, ESM::NPC::Picks}, { Columns::ColumnId_BuysProbes, ESM::NPC::Probes}, { Columns::ColumnId_BuysLights, ESM::NPC::Lights}, { Columns::ColumnId_BuysApparati, ESM::NPC::Apparatus}, { Columns::ColumnId_BuysRepairItems, ESM::NPC::RepairItem}, { Columns::ColumnId_BuysMiscItems, ESM::NPC::Misc}, { Columns::ColumnId_BuysPotions, ESM::NPC::Potions}, { Columns::ColumnId_BuysMagicItems, ESM::NPC::MagicItems}, { Columns::ColumnId_SellsSpells, ESM::NPC::Spells}, { Columns::ColumnId_Trainer, ESM::NPC::Training}, { Columns::ColumnId_Spellmaking, ESM::NPC::Spellmaking}, { Columns::ColumnId_EnchantingService, ESM::NPC::Enchanting}, { Columns::ColumnId_RepairService, ESM::NPC::Repair}, { -1, 0 } }; for (int i=0; sServiceTable[i].mName!=-1; ++i) { mColumns.emplace_back(sServiceTable[i].mName, ColumnBase::Display_Boolean); actorsColumns.mServices.insert (std::make_pair (&mColumns.back(), sServiceTable[i].mFlag)); } mColumns.emplace_back(Columns::ColumnId_AutoCalc, ColumnBase::Display_Boolean, ColumnBase::Flag_Table | ColumnBase::Flag_Dialogue | ColumnBase::Flag_Dialogue_Refresh); const RefIdColumn *autoCalc = &mColumns.back(); mColumns.emplace_back(Columns::ColumnId_ApparatusType, ColumnBase::Display_ApparatusType); const RefIdColumn *apparatusType = &mColumns.back(); mColumns.emplace_back(Columns::ColumnId_ArmorType, ColumnBase::Display_ArmorType); const RefIdColumn *armorType = &mColumns.back(); mColumns.emplace_back(Columns::ColumnId_Health, ColumnBase::Display_Integer); const RefIdColumn *health = &mColumns.back(); mColumns.emplace_back(Columns::ColumnId_ArmorValue, ColumnBase::Display_Integer); const RefIdColumn *armor = &mColumns.back(); mColumns.emplace_back(Columns::ColumnId_BookType, ColumnBase::Display_BookType); const RefIdColumn *bookType = &mColumns.back(); mColumns.emplace_back(Columns::ColumnId_Skill, ColumnBase::Display_SkillId); const RefIdColumn *skill = &mColumns.back(); mColumns.emplace_back(Columns::ColumnId_Text, ColumnBase::Display_LongString); const RefIdColumn *text = &mColumns.back(); mColumns.emplace_back(Columns::ColumnId_ClothingType, ColumnBase::Display_ClothingType); const RefIdColumn *clothingType = &mColumns.back(); mColumns.emplace_back(Columns::ColumnId_WeightCapacity, ColumnBase::Display_Float); const RefIdColumn *weightCapacity = &mColumns.back(); mColumns.emplace_back(Columns::ColumnId_OrganicContainer, ColumnBase::Display_Boolean); const RefIdColumn *organic = &mColumns.back(); mColumns.emplace_back(Columns::ColumnId_Respawn, ColumnBase::Display_Boolean); const RefIdColumn *respawn = &mColumns.back(); // Nested table mColumns.emplace_back(Columns::ColumnId_ContainerContent, ColumnBase::Display_NestedHeader, ColumnBase::Flag_Dialogue); const RefIdColumn *content = &mColumns.back(); std::map contMap; contMap.insert(std::make_pair(UniversalId::Type_Container, new NestedInventoryRefIdAdapter (UniversalId::Type_Container))); mNestedAdapters.emplace_back(&mColumns.back(), contMap); mColumns.back().addColumn( new RefIdColumn (Columns::ColumnId_InventoryItemId, CSMWorld::ColumnBase::Display_Referenceable)); mColumns.back().addColumn( new RefIdColumn (Columns::ColumnId_ItemCount, CSMWorld::ColumnBase::Display_Integer)); CreatureColumns creatureColumns (actorsColumns); mColumns.emplace_back(Columns::ColumnId_CreatureType, ColumnBase::Display_CreatureType); creatureColumns.mType = &mColumns.back(); mColumns.emplace_back(Columns::ColumnId_Scale, ColumnBase::Display_Float); creatureColumns.mScale = &mColumns.back(); mColumns.emplace_back(Columns::ColumnId_ParentCreature, ColumnBase::Display_Creature); creatureColumns.mOriginal = &mColumns.back(); static const struct { int mName; unsigned int mFlag; } sCreatureFlagTable[] = { { Columns::ColumnId_Biped, ESM::Creature::Bipedal }, { Columns::ColumnId_HasWeapon, ESM::Creature::Weapon }, { Columns::ColumnId_Swims, ESM::Creature::Swims }, { Columns::ColumnId_Flies, ESM::Creature::Flies }, { Columns::ColumnId_Walks, ESM::Creature::Walks }, { Columns::ColumnId_Essential, ESM::Creature::Essential }, { -1, 0 } }; // for re-use in NPC records const RefIdColumn *essential = nullptr; for (int i=0; sCreatureFlagTable[i].mName!=-1; ++i) { mColumns.emplace_back(sCreatureFlagTable[i].mName, ColumnBase::Display_Boolean); creatureColumns.mFlags.insert (std::make_pair (&mColumns.back(), sCreatureFlagTable[i].mFlag)); switch (sCreatureFlagTable[i].mFlag) { case ESM::Creature::Essential: essential = &mColumns.back(); break; } } mColumns.emplace_back(Columns::ColumnId_BloodType, ColumnBase::Display_BloodType); // For re-use in NPC records. const RefIdColumn *bloodType = &mColumns.back(); creatureColumns.mBloodType = bloodType; creatureColumns.mFlags.insert (std::make_pair (respawn, ESM::Creature::Respawn)); // Nested table mColumns.emplace_back(Columns::ColumnId_CreatureAttributes, ColumnBase::Display_NestedHeader, ColumnBase::Flag_Dialogue); creatureColumns.mAttributes = &mColumns.back(); std::map creaAttrMap; creaAttrMap.insert(std::make_pair(UniversalId::Type_Creature, new CreatureAttributesRefIdAdapter())); mNestedAdapters.emplace_back(&mColumns.back(), creaAttrMap); mColumns.back().addColumn( new RefIdColumn (Columns::ColumnId_Attribute, CSMWorld::ColumnBase::Display_Attribute, false, false)); mColumns.back().addColumn( new RefIdColumn (Columns::ColumnId_AttributeValue, CSMWorld::ColumnBase::Display_Integer)); // Nested table mColumns.emplace_back(Columns::ColumnId_CreatureAttack, ColumnBase::Display_NestedHeader, ColumnBase::Flag_Dialogue); creatureColumns.mAttacks = &mColumns.back(); std::map attackMap; attackMap.insert(std::make_pair(UniversalId::Type_Creature, new CreatureAttackRefIdAdapter())); mNestedAdapters.emplace_back(&mColumns.back(), attackMap); mColumns.back().addColumn( new RefIdColumn (Columns::ColumnId_CreatureAttack, CSMWorld::ColumnBase::Display_Integer, false, false)); mColumns.back().addColumn( new RefIdColumn (Columns::ColumnId_MinAttack, CSMWorld::ColumnBase::Display_Integer)); mColumns.back().addColumn( new RefIdColumn (Columns::ColumnId_MaxAttack, CSMWorld::ColumnBase::Display_Integer)); // Nested list mColumns.emplace_back(Columns::ColumnId_CreatureMisc, ColumnBase::Display_NestedHeader, ColumnBase::Flag_Dialogue | ColumnBase::Flag_Dialogue_List); creatureColumns.mMisc = &mColumns.back(); std::map creaMiscMap; creaMiscMap.insert(std::make_pair(UniversalId::Type_Creature, new CreatureMiscRefIdAdapter())); mNestedAdapters.emplace_back(&mColumns.back(), creaMiscMap); mColumns.back().addColumn( new RefIdColumn (Columns::ColumnId_Level, CSMWorld::ColumnBase::Display_Integer, ColumnBase::Flag_Dialogue | ColumnBase::Flag_Dialogue_Refresh)); mColumns.back().addColumn( new RefIdColumn (Columns::ColumnId_Health, CSMWorld::ColumnBase::Display_Integer)); mColumns.back().addColumn( new RefIdColumn (Columns::ColumnId_Mana, CSMWorld::ColumnBase::Display_Integer)); mColumns.back().addColumn( new RefIdColumn (Columns::ColumnId_Fatigue, CSMWorld::ColumnBase::Display_Integer)); mColumns.back().addColumn( new RefIdColumn (Columns::ColumnId_SoulPoints, CSMWorld::ColumnBase::Display_Integer)); mColumns.back().addColumn( new RefIdColumn (Columns::ColumnId_CombatState, CSMWorld::ColumnBase::Display_Integer)); mColumns.back().addColumn( new RefIdColumn (Columns::ColumnId_MagicState, CSMWorld::ColumnBase::Display_Integer)); mColumns.back().addColumn( new RefIdColumn (Columns::ColumnId_StealthState, CSMWorld::ColumnBase::Display_Integer)); mColumns.back().addColumn( new RefIdColumn (Columns::ColumnId_Gold, CSMWorld::ColumnBase::Display_Integer)); mColumns.emplace_back(Columns::ColumnId_OpenSound, ColumnBase::Display_Sound); const RefIdColumn *openSound = &mColumns.back(); mColumns.emplace_back(Columns::ColumnId_CloseSound, ColumnBase::Display_Sound); const RefIdColumn *closeSound = &mColumns.back(); LightColumns lightColumns (inventoryColumns); mColumns.emplace_back(Columns::ColumnId_Duration, ColumnBase::Display_Integer); lightColumns.mTime = &mColumns.back(); mColumns.emplace_back(Columns::ColumnId_Radius, ColumnBase::Display_Integer); lightColumns.mRadius = &mColumns.back(); mColumns.emplace_back(Columns::ColumnId_Colour, ColumnBase::Display_Colour); lightColumns.mColor = &mColumns.back(); mColumns.emplace_back(Columns::ColumnId_Sound, ColumnBase::Display_Sound); lightColumns.mSound = &mColumns.back(); mColumns.emplace_back(Columns::ColumnId_EmitterType, ColumnBase::Display_EmitterType); lightColumns.mEmitterType = &mColumns.back(); static const struct { int mName; unsigned int mFlag; } sLightFlagTable[] = { { Columns::ColumnId_Dynamic, ESM::Light::Dynamic }, { Columns::ColumnId_Portable, ESM::Light::Carry }, { Columns::ColumnId_NegativeLight, ESM::Light::Negative }, { Columns::ColumnId_Fire, ESM::Light::Fire }, { Columns::ColumnId_OffByDefault, ESM::Light::OffDefault }, { -1, 0 } }; for (int i=0; sLightFlagTable[i].mName!=-1; ++i) { mColumns.emplace_back(sLightFlagTable[i].mName, ColumnBase::Display_Boolean); lightColumns.mFlags.insert (std::make_pair (&mColumns.back(), sLightFlagTable[i].mFlag)); } mColumns.emplace_back(Columns::ColumnId_IsKey, ColumnBase::Display_Boolean); const RefIdColumn *key = &mColumns.back(); NpcColumns npcColumns (actorsColumns); mColumns.emplace_back(Columns::ColumnId_Race, ColumnBase::Display_Race); npcColumns.mRace = &mColumns.back(); mColumns.emplace_back(Columns::ColumnId_Class, ColumnBase::Display_Class); npcColumns.mClass = &mColumns.back(); mColumns.emplace_back(Columns::ColumnId_Faction, ColumnBase::Display_Faction); npcColumns.mFaction = &mColumns.back(); mColumns.emplace_back(Columns::Columnid_Hair, ColumnBase::Display_BodyPart); npcColumns.mHair = &mColumns.back(); mColumns.emplace_back(Columns::ColumnId_Head, ColumnBase::Display_BodyPart); npcColumns.mHead = &mColumns.back(); mColumns.emplace_back(Columns::ColumnId_Gender, ColumnBase::Display_GenderNpc); npcColumns.mGender = &mColumns.back(); npcColumns.mFlags.insert (std::make_pair (essential, ESM::NPC::Essential)); npcColumns.mFlags.insert (std::make_pair (respawn, ESM::NPC::Respawn)); npcColumns.mFlags.insert (std::make_pair (autoCalc, ESM::NPC::Autocalc)); // Re-used from Creature records. npcColumns.mBloodType = bloodType; // Need a way to add a table of stats and values (rather than adding a long list of // entries in the dialogue subview) E.g. attributes+stats(health, mana, fatigue), skills // These needs to be driven from the autocalculated setting. // Nested table mColumns.emplace_back(Columns::ColumnId_NpcAttributes, ColumnBase::Display_NestedHeader, ColumnBase::Flag_Dialogue); npcColumns.mAttributes = &mColumns.back(); std::map attrMap; attrMap.insert(std::make_pair(UniversalId::Type_Npc, new NpcAttributesRefIdAdapter())); mNestedAdapters.emplace_back(&mColumns.back(), attrMap); mColumns.back().addColumn( new RefIdColumn (Columns::ColumnId_Attribute, CSMWorld::ColumnBase::Display_Attribute, false, false)); mColumns.back().addColumn( new RefIdColumn (Columns::ColumnId_UChar, CSMWorld::ColumnBase::Display_UnsignedInteger8)); // Nested table mColumns.emplace_back(Columns::ColumnId_NpcSkills, ColumnBase::Display_NestedHeader, ColumnBase::Flag_Dialogue); npcColumns.mSkills = &mColumns.back(); std::map skillsMap; skillsMap.insert(std::make_pair(UniversalId::Type_Npc, new NpcSkillsRefIdAdapter())); mNestedAdapters.emplace_back(&mColumns.back(), skillsMap); mColumns.back().addColumn( new RefIdColumn (Columns::ColumnId_Skill, CSMWorld::ColumnBase::Display_SkillId, false, false)); mColumns.back().addColumn( new RefIdColumn (Columns::ColumnId_UChar, CSMWorld::ColumnBase::Display_UnsignedInteger8)); // Nested list mColumns.emplace_back(Columns::ColumnId_NpcMisc, ColumnBase::Display_NestedHeader, ColumnBase::Flag_Dialogue | ColumnBase::Flag_Dialogue_List); npcColumns.mMisc = &mColumns.back(); std::map miscMap; miscMap.insert(std::make_pair(UniversalId::Type_Npc, new NpcMiscRefIdAdapter())); mNestedAdapters.emplace_back(&mColumns.back(), miscMap); mColumns.back().addColumn( new RefIdColumn (Columns::ColumnId_Level, CSMWorld::ColumnBase::Display_SignedInteger16)); mColumns.back().addColumn( new RefIdColumn (Columns::ColumnId_Health, CSMWorld::ColumnBase::Display_UnsignedInteger16)); mColumns.back().addColumn( new RefIdColumn (Columns::ColumnId_Mana, CSMWorld::ColumnBase::Display_UnsignedInteger16)); mColumns.back().addColumn( new RefIdColumn (Columns::ColumnId_Fatigue, CSMWorld::ColumnBase::Display_UnsignedInteger16)); mColumns.back().addColumn( new RefIdColumn (Columns::ColumnId_NpcDisposition, CSMWorld::ColumnBase::Display_UnsignedInteger8)); mColumns.back().addColumn( new RefIdColumn (Columns::ColumnId_NpcReputation, CSMWorld::ColumnBase::Display_UnsignedInteger8)); mColumns.back().addColumn( new RefIdColumn (Columns::ColumnId_NpcRank, CSMWorld::ColumnBase::Display_UnsignedInteger8)); mColumns.back().addColumn( new RefIdColumn (Columns::ColumnId_Gold, CSMWorld::ColumnBase::Display_Integer)); mColumns.back().addColumn( new RefIdColumn (Columns::ColumnId_NpcPersistence, CSMWorld::ColumnBase::Display_Boolean)); WeaponColumns weaponColumns (enchantableColumns); mColumns.emplace_back(Columns::ColumnId_WeaponType, ColumnBase::Display_WeaponType); weaponColumns.mType = &mColumns.back(); weaponColumns.mHealth = health; mColumns.emplace_back(Columns::ColumnId_WeaponSpeed, ColumnBase::Display_Float); weaponColumns.mSpeed = &mColumns.back(); mColumns.emplace_back(Columns::ColumnId_WeaponReach, ColumnBase::Display_Float); weaponColumns.mReach = &mColumns.back(); for (int i=0; i<3; ++i) { const RefIdColumn **column = i==0 ? weaponColumns.mChop : (i==1 ? weaponColumns.mSlash : weaponColumns.mThrust); for (int j=0; j<2; ++j) { mColumns.emplace_back(Columns::ColumnId_MinChop+i*2+j, ColumnBase::Display_Integer); column[j] = &mColumns.back(); } } static const struct { int mName; unsigned int mFlag; } sWeaponFlagTable[] = { { Columns::ColumnId_Magical, ESM::Weapon::Magical }, { Columns::ColumnId_Silver, ESM::Weapon::Silver }, { -1, 0 } }; for (int i=0; sWeaponFlagTable[i].mName!=-1; ++i) { mColumns.emplace_back(sWeaponFlagTable[i].mName, ColumnBase::Display_Boolean); weaponColumns.mFlags.insert (std::make_pair (&mColumns.back(), sWeaponFlagTable[i].mFlag)); } // Nested table mColumns.emplace_back(Columns::ColumnId_PartRefList, ColumnBase::Display_NestedHeader, ColumnBase::Flag_Dialogue); const RefIdColumn *partRef = &mColumns.back(); std::map partMap; partMap.insert(std::make_pair(UniversalId::Type_Armor, new BodyPartRefIdAdapter (UniversalId::Type_Armor))); partMap.insert(std::make_pair(UniversalId::Type_Clothing, new BodyPartRefIdAdapter (UniversalId::Type_Clothing))); mNestedAdapters.emplace_back(&mColumns.back(), partMap); mColumns.back().addColumn( new RefIdColumn (Columns::ColumnId_PartRefType, CSMWorld::ColumnBase::Display_PartRefType)); mColumns.back().addColumn( new RefIdColumn (Columns::ColumnId_PartRefMale, CSMWorld::ColumnBase::Display_BodyPart)); mColumns.back().addColumn( new RefIdColumn (Columns::ColumnId_PartRefFemale, CSMWorld::ColumnBase::Display_BodyPart)); LevListColumns levListColumns (baseColumns); // Nested table mColumns.emplace_back(Columns::ColumnId_LevelledList, ColumnBase::Display_NestedHeader, ColumnBase::Flag_Dialogue); levListColumns.mLevList = &mColumns.back(); std::map levListMap; levListMap.insert(std::make_pair(UniversalId::Type_CreatureLevelledList, new NestedLevListRefIdAdapter (UniversalId::Type_CreatureLevelledList))); levListMap.insert(std::make_pair(UniversalId::Type_ItemLevelledList, new NestedLevListRefIdAdapter (UniversalId::Type_ItemLevelledList))); mNestedAdapters.emplace_back(&mColumns.back(), levListMap); mColumns.back().addColumn( new RefIdColumn (Columns::ColumnId_LevelledItemId, CSMWorld::ColumnBase::Display_Referenceable)); mColumns.back().addColumn( new RefIdColumn (Columns::ColumnId_LevelledItemLevel, CSMWorld::ColumnBase::Display_Integer)); // Nested list mColumns.emplace_back(Columns::ColumnId_LevelledList, ColumnBase::Display_NestedHeader, ColumnBase::Flag_Dialogue | ColumnBase::Flag_Dialogue_List); levListColumns.mNestedListLevList = &mColumns.back(); std::map nestedListLevListMap; nestedListLevListMap.insert(std::make_pair(UniversalId::Type_CreatureLevelledList, new NestedListLevListRefIdAdapter (UniversalId::Type_CreatureLevelledList))); nestedListLevListMap.insert(std::make_pair(UniversalId::Type_ItemLevelledList, new NestedListLevListRefIdAdapter (UniversalId::Type_ItemLevelledList))); mNestedAdapters.emplace_back(&mColumns.back(), nestedListLevListMap); mColumns.back().addColumn( new RefIdColumn (Columns::ColumnId_LevelledItemTypeEach, CSMWorld::ColumnBase::Display_Boolean)); mColumns.back().addColumn( new RefIdColumn (Columns::ColumnId_LevelledItemType, CSMWorld::ColumnBase::Display_Boolean)); mColumns.back().addColumn( new RefIdColumn (Columns::ColumnId_LevelledItemChanceNone, CSMWorld::ColumnBase::Display_UnsignedInteger8)); mAdapters.insert (std::make_pair (UniversalId::Type_Activator, new NameRefIdAdapter (UniversalId::Type_Activator, nameColumns))); mAdapters.insert (std::make_pair (UniversalId::Type_Potion, new PotionRefIdAdapter (potionColumns, autoCalc))); mAdapters.insert (std::make_pair (UniversalId::Type_Apparatus, new ApparatusRefIdAdapter (inventoryColumns, apparatusType, toolsColumns.mQuality))); mAdapters.insert (std::make_pair (UniversalId::Type_Armor, new ArmorRefIdAdapter (enchantableColumns, armorType, health, armor, partRef))); mAdapters.insert (std::make_pair (UniversalId::Type_Book, new BookRefIdAdapter (enchantableColumns, bookType, skill, text))); mAdapters.insert (std::make_pair (UniversalId::Type_Clothing, new ClothingRefIdAdapter (enchantableColumns, clothingType, partRef))); mAdapters.insert (std::make_pair (UniversalId::Type_Container, new ContainerRefIdAdapter (nameColumns, weightCapacity, organic, respawn, content))); mAdapters.insert (std::make_pair (UniversalId::Type_Creature, new CreatureRefIdAdapter (creatureColumns))); mAdapters.insert (std::make_pair (UniversalId::Type_Door, new DoorRefIdAdapter (nameColumns, openSound, closeSound))); mAdapters.insert (std::make_pair (UniversalId::Type_Ingredient, new IngredientRefIdAdapter (ingredientColumns))); mAdapters.insert (std::make_pair (UniversalId::Type_CreatureLevelledList, new LevelledListRefIdAdapter ( UniversalId::Type_CreatureLevelledList, levListColumns))); mAdapters.insert (std::make_pair (UniversalId::Type_ItemLevelledList, new LevelledListRefIdAdapter (UniversalId::Type_ItemLevelledList, levListColumns))); mAdapters.insert (std::make_pair (UniversalId::Type_Light, new LightRefIdAdapter (lightColumns))); mAdapters.insert (std::make_pair (UniversalId::Type_Lockpick, new ToolRefIdAdapter (UniversalId::Type_Lockpick, toolsColumns))); mAdapters.insert (std::make_pair (UniversalId::Type_Miscellaneous, new MiscRefIdAdapter (inventoryColumns, key))); mAdapters.insert (std::make_pair (UniversalId::Type_Npc, new NpcRefIdAdapter (npcColumns))); mAdapters.insert (std::make_pair (UniversalId::Type_Probe, new ToolRefIdAdapter (UniversalId::Type_Probe, toolsColumns))); mAdapters.insert (std::make_pair (UniversalId::Type_Repair, new ToolRefIdAdapter (UniversalId::Type_Repair, toolsColumns))); mAdapters.insert (std::make_pair (UniversalId::Type_Static, new ModelRefIdAdapter (UniversalId::Type_Static, modelColumns))); mAdapters.insert (std::make_pair (UniversalId::Type_Weapon, new WeaponRefIdAdapter (weaponColumns))); } CSMWorld::RefIdCollection::~RefIdCollection() { for (std::map::iterator iter (mAdapters.begin()); iter!=mAdapters.end(); ++iter) delete iter->second; for (std::vector > >::iterator iter (mNestedAdapters.begin()); iter!=mNestedAdapters.end(); ++iter) { for (std::map::iterator it ((iter->second).begin()); it!=(iter->second).end(); ++it) delete it->second; } } int CSMWorld::RefIdCollection::getSize() const { return mData.getSize(); } std::string CSMWorld::RefIdCollection::getId (int index) const { return getData (index, 0).toString().toUtf8().constData(); } int CSMWorld::RefIdCollection::getIndex (const std::string& id) const { int index = searchId (id); if (index==-1) throw std::runtime_error ("invalid ID: " + id); return index; } int CSMWorld::RefIdCollection::getColumns() const { return mColumns.size(); } const CSMWorld::ColumnBase& CSMWorld::RefIdCollection::getColumn (int column) const { return mColumns.at (column); } QVariant CSMWorld::RefIdCollection::getData (int index, int column) const { RefIdData::LocalIndex localIndex = mData.globalToLocalIndex (index); const RefIdAdapter& adaptor = findAdapter (localIndex.second); return adaptor.getData (&mColumns.at (column), mData, localIndex.first); } QVariant CSMWorld::RefIdCollection::getNestedData (int row, int column, int subRow, int subColumn) const { RefIdData::LocalIndex localIndex = mData.globalToLocalIndex(row); const CSMWorld::NestedRefIdAdapterBase& nestedAdapter = getNestedAdapter(mColumns.at(column), localIndex.second); return nestedAdapter.getNestedData(&mColumns.at (column), mData, localIndex.first, subRow, subColumn); } void CSMWorld::RefIdCollection::setData (int index, int column, const QVariant& data) { RefIdData::LocalIndex localIndex = mData.globalToLocalIndex (index); const RefIdAdapter& adaptor = findAdapter (localIndex.second); adaptor.setData (&mColumns.at (column), mData, localIndex.first, data); } void CSMWorld::RefIdCollection::setNestedData(int row, int column, const QVariant& data, int subRow, int subColumn) { RefIdData::LocalIndex localIndex = mData.globalToLocalIndex (row); const CSMWorld::NestedRefIdAdapterBase& nestedAdapter = getNestedAdapter(mColumns.at(column), localIndex.second); nestedAdapter.setNestedData(&mColumns.at (column), mData, localIndex.first, data, subRow, subColumn); return; } void CSMWorld::RefIdCollection::removeRows (int index, int count) { mData.erase (index, count); } void CSMWorld::RefIdCollection::removeNestedRows(int row, int column, int subRow) { RefIdData::LocalIndex localIndex = mData.globalToLocalIndex (row); const CSMWorld::NestedRefIdAdapterBase& nestedAdapter = getNestedAdapter(mColumns.at(column), localIndex.second); nestedAdapter.removeNestedRow(&mColumns.at (column), mData, localIndex.first, subRow); return; } void CSMWorld::RefIdCollection::appendBlankRecord (const std::string& id, UniversalId::Type type) { mData.appendRecord (type, id, false); } int CSMWorld::RefIdCollection::searchId (const std::string& id) const { RefIdData::LocalIndex localIndex = mData.searchId (id); if (localIndex.first==-1) return -1; return mData.localToGlobalIndex (localIndex); } void CSMWorld::RefIdCollection::replace (int index, const RecordBase& record) { mData.getRecord (mData.globalToLocalIndex (index)).assign (record); } void CSMWorld::RefIdCollection::cloneRecord(const std::string& origin, const std::string& destination, const CSMWorld::UniversalId::Type type) { std::unique_ptr newRecord(mData.getRecord(mData.searchId(origin)).modifiedCopy()); mAdapters.find(type)->second->setId(*newRecord, destination); mData.insertRecord(*newRecord, type, destination); } bool CSMWorld::RefIdCollection::touchRecord(const std::string& id) { throw std::runtime_error("RefIdCollection::touchRecord is unimplemented"); return false; } void CSMWorld::RefIdCollection::appendRecord (const RecordBase& record, UniversalId::Type type) { std::string id = findAdapter (type).getId (record); int index = mData.getAppendIndex (type); mData.appendRecord (type, id, false); mData.getRecord (mData.globalToLocalIndex (index)).assign (record); } const CSMWorld::RecordBase& CSMWorld::RefIdCollection::getRecord (const std::string& id) const { return mData.getRecord (mData.searchId (id)); } const CSMWorld::RecordBase& CSMWorld::RefIdCollection::getRecord (int index) const { return mData.getRecord (mData.globalToLocalIndex (index)); } void CSMWorld::RefIdCollection::load (ESM::ESMReader& reader, bool base, UniversalId::Type type) { mData.load(reader, base, type); } int CSMWorld::RefIdCollection::getAppendIndex (const std::string& id, UniversalId::Type type) const { return mData.getAppendIndex (type); } std::vector CSMWorld::RefIdCollection::getIds (bool listDeleted) const { return mData.getIds (listDeleted); } bool CSMWorld::RefIdCollection::reorderRows (int baseIndex, const std::vector& newOrder) { return false; } void CSMWorld::RefIdCollection::save (int index, ESM::ESMWriter& writer) const { mData.save (index, writer); } const CSMWorld::RefIdData& CSMWorld::RefIdCollection::getDataSet() const { return mData; } int CSMWorld::RefIdCollection::getNestedRowsCount(int row, int column) const { RefIdData::LocalIndex localIndex = mData.globalToLocalIndex (row); const CSMWorld::NestedRefIdAdapterBase& nestedAdapter = getNestedAdapter(mColumns.at(column), localIndex.second); return nestedAdapter.getNestedRowsCount(&mColumns.at(column), mData, localIndex.first); } int CSMWorld::RefIdCollection::getNestedColumnsCount(int row, int column) const { RefIdData::LocalIndex localIndex = mData.globalToLocalIndex (row); const CSMWorld::NestedRefIdAdapterBase& nestedAdapter = getNestedAdapter(mColumns.at(column), localIndex.second); return nestedAdapter.getNestedColumnsCount(&mColumns.at(column), mData); } CSMWorld::NestableColumn *CSMWorld::RefIdCollection::getNestableColumn(int column) { return &mColumns.at(column); } void CSMWorld::RefIdCollection::addNestedRow(int row, int col, int position) { RefIdData::LocalIndex localIndex = mData.globalToLocalIndex (row); const CSMWorld::NestedRefIdAdapterBase& nestedAdapter = getNestedAdapter(mColumns.at(col), localIndex.second); nestedAdapter.addNestedRow(&mColumns.at(col), mData, localIndex.first, position); return; } void CSMWorld::RefIdCollection::setNestedTable(int row, int column, const CSMWorld::NestedTableWrapperBase& nestedTable) { RefIdData::LocalIndex localIndex = mData.globalToLocalIndex (row); const CSMWorld::NestedRefIdAdapterBase& nestedAdapter = getNestedAdapter(mColumns.at(column), localIndex.second); nestedAdapter.setNestedTable(&mColumns.at(column), mData, localIndex.first, nestedTable); return; } CSMWorld::NestedTableWrapperBase* CSMWorld::RefIdCollection::nestedTable(int row, int column) const { RefIdData::LocalIndex localIndex = mData.globalToLocalIndex (row); const CSMWorld::NestedRefIdAdapterBase& nestedAdapter = getNestedAdapter(mColumns.at(column), localIndex.second); return nestedAdapter.nestedTable(&mColumns.at(column), mData, localIndex.first); } const CSMWorld::NestedRefIdAdapterBase& CSMWorld::RefIdCollection::getNestedAdapter(const CSMWorld::ColumnBase &column, UniversalId::Type type) const { for (std::vector > >::const_iterator iter (mNestedAdapters.begin()); iter!=mNestedAdapters.end(); ++iter) { if ((iter->first) == &column) { std::map::const_iterator it = (iter->second).find(type); if (it == (iter->second).end()) throw std::runtime_error("No such type in the nestedadapters"); return *it->second; } } throw std::runtime_error("No such column in the nestedadapters"); } void CSMWorld::RefIdCollection::copyTo (int index, RefIdCollection& target) const { mData.copyTo (index, target.mData); } ================================================ FILE: apps/opencs/model/world/refidcollection.hpp ================================================ #ifndef CSM_WOLRD_REFIDCOLLECTION_H #define CSM_WOLRD_REFIDCOLLECTION_H #include #include #include #include "columnbase.hpp" #include "collectionbase.hpp" #include "nestedcollection.hpp" #include "refiddata.hpp" namespace ESM { class ESMWriter; } namespace CSMWorld { class RefIdAdapter; struct NestedTableWrapperBase; class NestedRefIdAdapterBase; class RefIdColumn : public NestableColumn { bool mEditable; bool mUserEditable; public: RefIdColumn (int columnId, Display displayType, int flag = Flag_Table | Flag_Dialogue, bool editable = true, bool userEditable = true); bool isEditable() const override; bool isUserEditable() const override; }; class RefIdCollection : public CollectionBase, public NestedCollection { private: RefIdData mData; std::deque mColumns; std::map mAdapters; std::vector > > mNestedAdapters; private: const RefIdAdapter& findAdapter (UniversalId::Type) const; ///< Throws an exception if no adaptor for \a Type can be found. const NestedRefIdAdapterBase& getNestedAdapter(const ColumnBase &column, UniversalId::Type type) const; public: RefIdCollection(); virtual ~RefIdCollection(); int getSize() const override; std::string getId (int index) const override; int getIndex (const std::string& id) const override; int getColumns() const override; const ColumnBase& getColumn (int column) const override; QVariant getData (int index, int column) const override; void setData (int index, int column, const QVariant& data) override; void removeRows (int index, int count) override; void cloneRecord(const std::string& origin, const std::string& destination, const UniversalId::Type type) override; bool touchRecord(const std::string& id) override; void appendBlankRecord (const std::string& id, UniversalId::Type type) override; ///< \param type Will be ignored, unless the collection supports multiple record types int searchId (const std::string& id) const override; ////< Search record with \a id. /// \return index of record (if found) or -1 (not found) void replace (int index, const RecordBase& record) override; ///< If the record type does not match, an exception is thrown. /// /// \attention \a record must not change the ID. void appendRecord (const RecordBase& record, UniversalId::Type type) override; ///< If the record type does not match, an exception is thrown. /// ///< \param type Will be ignored, unless the collection supports multiple record types const RecordBase& getRecord (const std::string& id) const override; const RecordBase& getRecord (int index) const override; void load (ESM::ESMReader& reader, bool base, UniversalId::Type type); int getAppendIndex (const std::string& id, UniversalId::Type type) const override; ///< \param type Will be ignored, unless the collection supports multiple record types std::vector getIds (bool listDeleted) const override; ///< Return a sorted collection of all IDs /// /// \param listDeleted include deleted record in the list bool reorderRows (int baseIndex, const std::vector& newOrder) override; ///< Reorder the rows [baseIndex, baseIndex+newOrder.size()) according to the indices /// given in \a newOrder (baseIndex+newOrder[0] specifies the new index of row baseIndex). /// /// \return Success? QVariant getNestedData(int row, int column, int subRow, int subColumn) const override; NestedTableWrapperBase* nestedTable(int row, int column) const override; void setNestedTable(int row, int column, const NestedTableWrapperBase& nestedTable) override; int getNestedRowsCount(int row, int column) const override; int getNestedColumnsCount(int row, int column) const override; NestableColumn *getNestableColumn(int column) override; void setNestedData(int row, int column, const QVariant& data, int subRow, int subColumn) override; void removeNestedRows(int row, int column, int subRow) override; void addNestedRow(int row, int col, int position) override; void save (int index, ESM::ESMWriter& writer) const; const RefIdData& getDataSet() const; //I can't figure out a better name for this one :( void copyTo (int index, RefIdCollection& target) const; }; } #endif ================================================ FILE: apps/opencs/model/world/refiddata.cpp ================================================ #include "refiddata.hpp" #include #include CSMWorld::RefIdDataContainerBase::~RefIdDataContainerBase() {} std::string CSMWorld::RefIdData::getRecordId(const CSMWorld::RefIdData::LocalIndex &index) const { std::map::const_iterator found = mRecordContainers.find (index.second); if (found == mRecordContainers.end()) throw std::logic_error ("invalid local index type"); return found->second->getId(index.first); } CSMWorld::RefIdData::RefIdData() { mRecordContainers.insert (std::make_pair (UniversalId::Type_Activator, &mActivators)); mRecordContainers.insert (std::make_pair (UniversalId::Type_Potion, &mPotions)); mRecordContainers.insert (std::make_pair (UniversalId::Type_Apparatus, &mApparati)); mRecordContainers.insert (std::make_pair (UniversalId::Type_Armor, &mArmors)); mRecordContainers.insert (std::make_pair (UniversalId::Type_Book, &mBooks)); mRecordContainers.insert (std::make_pair (UniversalId::Type_Clothing, &mClothing)); mRecordContainers.insert (std::make_pair (UniversalId::Type_Container, &mContainers)); mRecordContainers.insert (std::make_pair (UniversalId::Type_Creature, &mCreatures)); mRecordContainers.insert (std::make_pair (UniversalId::Type_Door, &mDoors)); mRecordContainers.insert (std::make_pair (UniversalId::Type_Ingredient, &mIngredients)); mRecordContainers.insert (std::make_pair (UniversalId::Type_CreatureLevelledList, &mCreatureLevelledLists)); mRecordContainers.insert (std::make_pair (UniversalId::Type_ItemLevelledList, &mItemLevelledLists)); mRecordContainers.insert (std::make_pair (UniversalId::Type_Light, &mLights)); mRecordContainers.insert (std::make_pair (UniversalId::Type_Lockpick, &mLockpicks)); mRecordContainers.insert (std::make_pair (UniversalId::Type_Miscellaneous, &mMiscellaneous)); mRecordContainers.insert (std::make_pair (UniversalId::Type_Npc, &mNpcs)); mRecordContainers.insert (std::make_pair (UniversalId::Type_Probe, &mProbes)); mRecordContainers.insert (std::make_pair (UniversalId::Type_Repair, &mRepairs)); mRecordContainers.insert (std::make_pair (UniversalId::Type_Static, &mStatics)); mRecordContainers.insert (std::make_pair (UniversalId::Type_Weapon, &mWeapons)); } CSMWorld::RefIdData::LocalIndex CSMWorld::RefIdData::globalToLocalIndex (int index) const { for (std::map::const_iterator iter ( mRecordContainers.begin()); iter!=mRecordContainers.end(); ++iter) { if (indexsecond->getSize()) return LocalIndex (index, iter->first); index -= iter->second->getSize(); } throw std::runtime_error ("RefIdData index out of range"); } int CSMWorld::RefIdData::localToGlobalIndex (const LocalIndex& index) const { std::map::const_iterator end = mRecordContainers.find (index.second); if (end==mRecordContainers.end()) throw std::logic_error ("invalid local index type"); int globalIndex = index.first; for (std::map::const_iterator iter ( mRecordContainers.begin()); iter!=end; ++iter) globalIndex += iter->second->getSize(); return globalIndex; } CSMWorld::RefIdData::LocalIndex CSMWorld::RefIdData::searchId ( const std::string& id) const { std::string id2 = Misc::StringUtils::lowerCase (id); std::map >::const_iterator iter = mIndex.find (id2); if (iter==mIndex.end()) return std::make_pair (-1, CSMWorld::UniversalId::Type_None); return iter->second; } void CSMWorld::RefIdData::erase (int index, int count) { LocalIndex localIndex = globalToLocalIndex (index); std::map::const_iterator iter = mRecordContainers.find (localIndex.second); while (count>0 && iter!=mRecordContainers.end()) { int size = iter->second->getSize(); if (localIndex.first+count>size) { erase (localIndex, size-localIndex.first); count -= size-localIndex.first; ++iter; if (iter==mRecordContainers.end()) throw std::runtime_error ("invalid count value for erase operation"); localIndex.first = 0; localIndex.second = iter->first; } else { erase (localIndex, count); count = 0; } } } const CSMWorld::RecordBase& CSMWorld::RefIdData::getRecord (const LocalIndex& index) const { std::map::const_iterator iter = mRecordContainers.find (index.second); if (iter==mRecordContainers.end()) throw std::logic_error ("invalid local index type"); return iter->second->getRecord (index.first); } CSMWorld::RecordBase& CSMWorld::RefIdData::getRecord (const LocalIndex& index) { std::map::iterator iter = mRecordContainers.find (index.second); if (iter==mRecordContainers.end()) throw std::logic_error ("invalid local index type"); return iter->second->getRecord (index.first); } void CSMWorld::RefIdData::appendRecord (UniversalId::Type type, const std::string& id, bool base) { std::map::iterator iter = mRecordContainers.find (type); if (iter==mRecordContainers.end()) throw std::logic_error ("invalid local index type"); iter->second->appendRecord (id, base); mIndex.insert (std::make_pair (Misc::StringUtils::lowerCase (id), LocalIndex (iter->second->getSize()-1, type))); } int CSMWorld::RefIdData::getAppendIndex (UniversalId::Type type) const { int index = 0; for (std::map::const_iterator iter ( mRecordContainers.begin()); iter!=mRecordContainers.end(); ++iter) { index += iter->second->getSize(); if (type==iter->first) break; } return index; } void CSMWorld::RefIdData::load (ESM::ESMReader& reader, bool base, CSMWorld::UniversalId::Type type) { std::map::iterator found = mRecordContainers.find (type); if (found == mRecordContainers.end()) throw std::logic_error ("Invalid Referenceable ID type"); int index = found->second->load(reader, base); if (index != -1) { LocalIndex localIndex = LocalIndex(index, type); if (base && getRecord(localIndex).mState == RecordBase::State_Deleted) { erase(localIndex, 1); } else { mIndex[Misc::StringUtils::lowerCase(getRecordId(localIndex))] = localIndex; } } } void CSMWorld::RefIdData::erase (const LocalIndex& index, int count) { std::map::iterator iter = mRecordContainers.find (index.second); if (iter==mRecordContainers.end()) throw std::logic_error ("invalid local index type"); for (int i=index.first; i::iterator result = mIndex.find (Misc::StringUtils::lowerCase (iter->second->getId (i))); if (result!=mIndex.end()) mIndex.erase (result); } // Adjust the local indexes to avoid gaps between them after removal of records int recordIndex = index.first + count; int recordCount = iter->second->getSize(); while (recordIndex < recordCount) { std::map::iterator recordIndexFound = mIndex.find(Misc::StringUtils::lowerCase(iter->second->getId(recordIndex))); if (recordIndexFound != mIndex.end()) { recordIndexFound->second.first -= count; } ++recordIndex; } iter->second->erase (index.first, count); } int CSMWorld::RefIdData::getSize() const { return mIndex.size(); } std::vector CSMWorld::RefIdData::getIds (bool listDeleted) const { std::vector ids; for (std::map::const_iterator iter (mIndex.begin()); iter!=mIndex.end(); ++iter) { if (listDeleted || !getRecord (iter->second).isDeleted()) { std::map::const_iterator container = mRecordContainers.find (iter->second.second); if (container==mRecordContainers.end()) throw std::logic_error ("Invalid referenceable ID type"); ids.push_back (container->second->getId (iter->second.first)); } } return ids; } void CSMWorld::RefIdData::save (int index, ESM::ESMWriter& writer) const { LocalIndex localIndex = globalToLocalIndex (index); std::map::const_iterator iter = mRecordContainers.find (localIndex.second); if (iter==mRecordContainers.end()) throw std::logic_error ("invalid local index type"); iter->second->save (localIndex.first, writer); } const CSMWorld::RefIdDataContainer< ESM::Book >& CSMWorld::RefIdData::getBooks() const { return mBooks; } const CSMWorld::RefIdDataContainer< ESM::Activator >& CSMWorld::RefIdData::getActivators() const { return mActivators; } const CSMWorld::RefIdDataContainer< ESM::Potion >& CSMWorld::RefIdData::getPotions() const { return mPotions; } const CSMWorld::RefIdDataContainer< ESM::Apparatus >& CSMWorld::RefIdData::getApparati() const { return mApparati; } const CSMWorld::RefIdDataContainer< ESM::Armor >& CSMWorld::RefIdData::getArmors() const { return mArmors; } const CSMWorld::RefIdDataContainer< ESM::Clothing >& CSMWorld::RefIdData::getClothing() const { return mClothing; } const CSMWorld::RefIdDataContainer< ESM::Container >& CSMWorld::RefIdData::getContainers() const { return mContainers; } const CSMWorld::RefIdDataContainer< ESM::Creature >& CSMWorld::RefIdData::getCreatures() const { return mCreatures; } const CSMWorld::RefIdDataContainer< ESM::Door >& CSMWorld::RefIdData::getDoors() const { return mDoors; } const CSMWorld::RefIdDataContainer< ESM::Ingredient >& CSMWorld::RefIdData::getIngredients() const { return mIngredients; } const CSMWorld::RefIdDataContainer< ESM::CreatureLevList >& CSMWorld::RefIdData::getCreatureLevelledLists() const { return mCreatureLevelledLists; } const CSMWorld::RefIdDataContainer< ESM::ItemLevList >& CSMWorld::RefIdData::getItemLevelledList() const { return mItemLevelledLists; } const CSMWorld::RefIdDataContainer< ESM::Light >& CSMWorld::RefIdData::getLights() const { return mLights; } const CSMWorld::RefIdDataContainer< ESM::Lockpick >& CSMWorld::RefIdData::getLocpicks() const { return mLockpicks; } const CSMWorld::RefIdDataContainer< ESM::Miscellaneous >& CSMWorld::RefIdData::getMiscellaneous() const { return mMiscellaneous; } const CSMWorld::RefIdDataContainer< ESM::NPC >& CSMWorld::RefIdData::getNPCs() const { return mNpcs; } const CSMWorld::RefIdDataContainer< ESM::Weapon >& CSMWorld::RefIdData::getWeapons() const { return mWeapons; } const CSMWorld::RefIdDataContainer< ESM::Probe >& CSMWorld::RefIdData::getProbes() const { return mProbes; } const CSMWorld::RefIdDataContainer< ESM::Repair >& CSMWorld::RefIdData::getRepairs() const { return mRepairs; } const CSMWorld::RefIdDataContainer< ESM::Static >& CSMWorld::RefIdData::getStatics() const { return mStatics; } void CSMWorld::RefIdData::insertRecord (CSMWorld::RecordBase& record, CSMWorld::UniversalId::Type type, const std::string& id) { std::map::iterator iter = mRecordContainers.find (type); if (iter==mRecordContainers.end()) throw std::logic_error ("invalid local index type"); iter->second->insertRecord(record); mIndex.insert (std::make_pair (Misc::StringUtils::lowerCase (id), LocalIndex (iter->second->getSize()-1, type))); } void CSMWorld::RefIdData::copyTo (int index, RefIdData& target) const { LocalIndex localIndex = globalToLocalIndex (index); RefIdDataContainerBase *source = mRecordContainers.find (localIndex.second)->second; std::string id = source->getId (localIndex.first); std::unique_ptr newRecord (source->getRecord (localIndex.first).modifiedCopy()); target.insertRecord (*newRecord, localIndex.second, id); } ================================================ FILE: apps/opencs/model/world/refiddata.hpp ================================================ #ifndef CSM_WOLRD_REFIDDATA_H #define CSM_WOLRD_REFIDDATA_H #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "record.hpp" #include "universalid.hpp" namespace ESM { class ESMReader; } namespace CSMWorld { struct RefIdDataContainerBase { virtual ~RefIdDataContainerBase(); virtual int getSize() const = 0; virtual const RecordBase& getRecord (int index) const = 0; virtual RecordBase& getRecord (int index)= 0; virtual void appendRecord (const std::string& id, bool base) = 0; virtual void insertRecord (RecordBase& record) = 0; virtual int load (ESM::ESMReader& reader, bool base) = 0; ///< \return index of a loaded record or -1 if no record was loaded virtual void erase (int index, int count) = 0; virtual std::string getId (int index) const = 0; virtual void save (int index, ESM::ESMWriter& writer) const = 0; }; template struct RefIdDataContainer : public RefIdDataContainerBase { std::vector > mContainer; int getSize() const override; const RecordBase& getRecord (int index) const override; RecordBase& getRecord (int index) override; void appendRecord (const std::string& id, bool base) override; void insertRecord (RecordBase& record) override; int load (ESM::ESMReader& reader, bool base) override; ///< \return index of a loaded record or -1 if no record was loaded void erase (int index, int count) override; std::string getId (int index) const override; void save (int index, ESM::ESMWriter& writer) const override; }; template void RefIdDataContainer::insertRecord(RecordBase& record) { Record& newRecord = dynamic_cast& >(record); mContainer.push_back(newRecord); } template int RefIdDataContainer::getSize() const { return static_cast (mContainer.size()); } template const RecordBase& RefIdDataContainer::getRecord (int index) const { return mContainer.at (index); } template RecordBase& RefIdDataContainer::getRecord (int index) { return mContainer.at (index); } template void RefIdDataContainer::appendRecord (const std::string& id, bool base) { Record record; record.mState = base ? RecordBase::State_BaseOnly : RecordBase::State_ModifiedOnly; record.mBase.mId = id; record.mModified.mId = id; (base ? record.mBase : record.mModified).blank(); mContainer.push_back (record); } template int RefIdDataContainer::load (ESM::ESMReader& reader, bool base) { RecordT record; bool isDeleted = false; record.load(reader, isDeleted); int index = 0; int numRecords = static_cast(mContainer.size()); for (; index < numRecords; ++index) { if (Misc::StringUtils::ciEqual(mContainer[index].get().mId, record.mId)) { break; } } if (isDeleted) { if (index == numRecords) { // deleting a record that does not exist // ignore it for now /// \todo report the problem to the user return -1; } // Flag the record as Deleted even for a base content file. // RefIdData is responsible for its erasure. mContainer[index].mState = RecordBase::State_Deleted; } else { if (index == numRecords) { appendRecord(record.mId, base); if (base) { mContainer.back().mBase = record; } else { mContainer.back().mModified = record; } } else if (!base) { mContainer[index].setModified(record); } else { // Overwrite mContainer[index].setModified(record); mContainer[index].merge(); } } return index; } template void RefIdDataContainer::erase (int index, int count) { if (index<0 || index+count>getSize()) throw std::runtime_error ("invalid RefIdDataContainer index"); mContainer.erase (mContainer.begin()+index, mContainer.begin()+index+count); } template std::string RefIdDataContainer::getId (int index) const { return mContainer.at (index).get().mId; } template void RefIdDataContainer::save (int index, ESM::ESMWriter& writer) const { Record record = mContainer.at(index); if (record.isModified() || record.mState == RecordBase::State_Deleted) { RecordT esmRecord = record.get(); writer.startRecord(esmRecord.sRecordId); esmRecord.save(writer, record.mState == RecordBase::State_Deleted); writer.endRecord(esmRecord.sRecordId); } } class RefIdData { public: typedef std::pair LocalIndex; private: RefIdDataContainer mActivators; RefIdDataContainer mPotions; RefIdDataContainer mApparati; RefIdDataContainer mArmors; RefIdDataContainer mBooks; RefIdDataContainer mClothing; RefIdDataContainer mContainers; RefIdDataContainer mCreatures; RefIdDataContainer mDoors; RefIdDataContainer mIngredients; RefIdDataContainer mCreatureLevelledLists; RefIdDataContainer mItemLevelledLists; RefIdDataContainer mLights; RefIdDataContainer mLockpicks; RefIdDataContainer mMiscellaneous; RefIdDataContainer mNpcs; RefIdDataContainer mProbes; RefIdDataContainer mRepairs; RefIdDataContainer mStatics; RefIdDataContainer mWeapons; std::map mIndex; std::map mRecordContainers; void erase (const LocalIndex& index, int count); ///< Must not spill over into another type. std::string getRecordId(const LocalIndex &index) const; public: RefIdData(); LocalIndex globalToLocalIndex (int index) const; int localToGlobalIndex (const LocalIndex& index) const; LocalIndex searchId (const std::string& id) const; void erase (int index, int count); void insertRecord (CSMWorld::RecordBase& record, CSMWorld::UniversalId::Type type, const std::string& id); const RecordBase& getRecord (const LocalIndex& index) const; RecordBase& getRecord (const LocalIndex& index); void appendRecord (UniversalId::Type type, const std::string& id, bool base); int getAppendIndex (UniversalId::Type type) const; void load (ESM::ESMReader& reader, bool base, UniversalId::Type type); int getSize() const; std::vector getIds (bool listDeleted = true) const; ///< Return a sorted collection of all IDs /// /// \param listDeleted include deleted record in the list void save (int index, ESM::ESMWriter& writer) const; //RECORD CONTAINERS ACCESS METHODS const RefIdDataContainer& getBooks() const; const RefIdDataContainer& getActivators() const; const RefIdDataContainer& getPotions() const; const RefIdDataContainer& getApparati() const; const RefIdDataContainer& getArmors() const; const RefIdDataContainer& getClothing() const; const RefIdDataContainer& getContainers() const; const RefIdDataContainer& getCreatures() const; const RefIdDataContainer& getDoors() const; const RefIdDataContainer& getIngredients() const; const RefIdDataContainer& getCreatureLevelledLists() const; const RefIdDataContainer& getItemLevelledList() const; const RefIdDataContainer& getLights() const; const RefIdDataContainer& getLocpicks() const; const RefIdDataContainer& getMiscellaneous() const; const RefIdDataContainer& getNPCs() const; const RefIdDataContainer& getWeapons() const; const RefIdDataContainer& getProbes() const; const RefIdDataContainer& getRepairs() const; const RefIdDataContainer& getStatics() const; void copyTo (int index, RefIdData& target) const; }; } #endif ================================================ FILE: apps/opencs/model/world/regionmap.cpp ================================================ #include "regionmap.hpp" #include #include #include #include #include "data.hpp" #include "universalid.hpp" CSMWorld::RegionMap::CellDescription::CellDescription() : mDeleted (false) {} CSMWorld::RegionMap::CellDescription::CellDescription (const Record& cell) { const Cell& cell2 = cell.get(); if (!cell2.isExterior()) throw std::logic_error ("Interior cell in region map"); mDeleted = cell.isDeleted(); mRegion = cell2.mRegion; mName = cell2.mName; } CSMWorld::CellCoordinates CSMWorld::RegionMap::getIndex (const QModelIndex& index) const { return mMin.move (index.column(), mMax.getY()-mMin.getY() - index.row()-1); } QModelIndex CSMWorld::RegionMap::getIndex (const CellCoordinates& index) const { // I hate you, Qt API naming scheme! return QAbstractTableModel::index (mMax.getY()-mMin.getY() - (index.getY()-mMin.getY())-1, index.getX()-mMin.getX()); } CSMWorld::CellCoordinates CSMWorld::RegionMap::getIndex (const Cell& cell) const { std::istringstream stream (cell.mId); char ignore; int x = 0; int y = 0; stream >> ignore >> x >> y; return CellCoordinates (x, y); } void CSMWorld::RegionMap::buildRegions() { const IdCollection& regions = mData.getRegions(); int size = regions.getSize(); for (int i=0; i& region = regions.getRecord (i); if (!region.isDeleted()) mColours.insert (std::make_pair (Misc::StringUtils::lowerCase (region.get().mId), region.get().mMapColor)); } } void CSMWorld::RegionMap::buildMap() { const IdCollection& cells = mData.getCells(); int size = cells.getSize(); for (int i=0; i& cell = cells.getRecord (i); const Cell& cell2 = cell.get(); if (cell2.isExterior()) { CellDescription description (cell); CellCoordinates index = getIndex (cell2); mMap.insert (std::make_pair (index, description)); } } std::pair mapSize = getSize(); mMin = mapSize.first; mMax = mapSize.second; } void CSMWorld::RegionMap::addCell (const CellCoordinates& index, const CellDescription& description) { std::map::iterator cell = mMap.find (index); if (cell!=mMap.end()) { cell->second = description; } else { updateSize(); mMap.insert (std::make_pair (index, description)); } QModelIndex index2 = getIndex (index); dataChanged (index2, index2); } void CSMWorld::RegionMap::addCells (int start, int end) { const IdCollection& cells = mData.getCells(); for (int i=start; i<=end; ++i) { const Record& cell = cells.getRecord (i); const Cell& cell2 = cell.get(); if (cell2.isExterior()) { CellCoordinates index = getIndex (cell2); CellDescription description (cell); addCell (index, description); } } } void CSMWorld::RegionMap::removeCell (const CellCoordinates& index) { std::map::iterator cell = mMap.find (index); if (cell!=mMap.end()) { mMap.erase (cell); QModelIndex index2 = getIndex (index); dataChanged (index2, index2); updateSize(); } } void CSMWorld::RegionMap::addRegion (const std::string& region, unsigned int colour) { mColours[Misc::StringUtils::lowerCase (region)] = colour; } void CSMWorld::RegionMap::removeRegion (const std::string& region) { std::map::iterator iter ( mColours.find (Misc::StringUtils::lowerCase (region))); if (iter!=mColours.end()) mColours.erase (iter); } void CSMWorld::RegionMap::updateRegions (const std::vector& regions) { std::vector regions2 (regions); std::for_each (regions2.begin(), regions2.end(), Misc::StringUtils::lowerCaseInPlace); std::sort (regions2.begin(), regions2.end()); for (std::map::const_iterator iter (mMap.begin()); iter!=mMap.end(); ++iter) { if (!iter->second.mRegion.empty() && std::find (regions2.begin(), regions2.end(), Misc::StringUtils::lowerCase (iter->second.mRegion))!=regions2.end()) { QModelIndex index = getIndex (iter->first); dataChanged (index, index); } } } void CSMWorld::RegionMap::updateSize() { std::pair size = getSize(); if (int diff = size.first.getX() - mMin.getX()) { beginInsertColumns (QModelIndex(), 0, std::abs (diff)-1); mMin = CellCoordinates (size.first.getX(), mMin.getY()); endInsertColumns(); } if (int diff = size.first.getY() - mMin.getY()) { beginInsertRows (QModelIndex(), 0, std::abs (diff)-1); mMin = CellCoordinates (mMin.getX(), size.first.getY()); endInsertRows(); } if (int diff = size.second.getX() - mMax.getX()) { int columns = columnCount(); if (diff>0) beginInsertColumns (QModelIndex(), columns, columns+diff-1); else beginRemoveColumns (QModelIndex(), columns+diff, columns-1); mMax = CellCoordinates (size.second.getX(), mMax.getY()); endInsertColumns(); } if (int diff = size.second.getY() - mMax.getY()) { int rows = rowCount(); if (diff>0) beginInsertRows (QModelIndex(), rows, rows+diff-1); else beginRemoveRows (QModelIndex(), rows+diff, rows-1); mMax = CellCoordinates (mMax.getX(), size.second.getY()); endInsertRows(); } } std::pair CSMWorld::RegionMap::getSize() const { const IdCollection& cells = mData.getCells(); int size = cells.getSize(); CellCoordinates min (0, 0); CellCoordinates max (0, 0); for (int i=0; i& cell = cells.getRecord (i); const Cell& cell2 = cell.get(); if (cell2.isExterior()) { CellCoordinates index = getIndex (cell2); if (min==max) { min = index; max = min.move (1, 1); } else { if (index.getX()=max.getX()) max = CellCoordinates (index.getX()+1, max.getY()); if (index.getY()=max.getY()) max = CellCoordinates (max.getX(), index.getY() + 1); } } } return std::make_pair (min, max); } CSMWorld::RegionMap::RegionMap (Data& data) : mData (data) { buildRegions(); buildMap(); QAbstractItemModel *regions = data.getTableModel (UniversalId (UniversalId::Type_Regions)); connect (regions, SIGNAL (rowsAboutToBeRemoved (const QModelIndex&, int, int)), this, SLOT (regionsAboutToBeRemoved (const QModelIndex&, int, int))); connect (regions, SIGNAL (rowsInserted (const QModelIndex&, int, int)), this, SLOT (regionsInserted (const QModelIndex&, int, int))); connect (regions, SIGNAL (dataChanged (const QModelIndex&, const QModelIndex&)), this, SLOT (regionsChanged (const QModelIndex&, const QModelIndex&))); QAbstractItemModel *cells = data.getTableModel (UniversalId (UniversalId::Type_Cells)); connect (cells, SIGNAL (rowsAboutToBeRemoved (const QModelIndex&, int, int)), this, SLOT (cellsAboutToBeRemoved (const QModelIndex&, int, int))); connect (cells, SIGNAL (rowsInserted (const QModelIndex&, int, int)), this, SLOT (cellsInserted (const QModelIndex&, int, int))); connect (cells, SIGNAL (dataChanged (const QModelIndex&, const QModelIndex&)), this, SLOT (cellsChanged (const QModelIndex&, const QModelIndex&))); } int CSMWorld::RegionMap::rowCount (const QModelIndex& parent) const { if (parent.isValid()) return 0; return mMax.getY()-mMin.getY(); } int CSMWorld::RegionMap::columnCount (const QModelIndex& parent) const { if (parent.isValid()) return 0; return mMax.getX()-mMin.getX(); } QVariant CSMWorld::RegionMap::data (const QModelIndex& index, int role) const { if (role==Qt::SizeHintRole) return QSize (16, 16); if (role==Qt::BackgroundRole) { /// \todo GUI class in non-GUI code. Needs to be addressed eventually. std::map::const_iterator cell = mMap.find (getIndex (index)); if (cell!=mMap.end()) { if (cell->second.mDeleted) return QBrush (Qt::red, Qt::DiagCrossPattern); std::map::const_iterator iter = mColours.find (Misc::StringUtils::lowerCase (cell->second.mRegion)); if (iter!=mColours.end()) return QBrush (QColor (iter->second & 0xff, (iter->second >> 8) & 0xff, (iter->second >> 16) & 0xff)); if (cell->second.mRegion.empty()) return QBrush (Qt::Dense6Pattern); // no region return QBrush (Qt::red, Qt::Dense6Pattern); // invalid region } return QBrush (Qt::DiagCrossPattern); } if (role==Qt::ToolTipRole) { CellCoordinates cellIndex = getIndex (index); std::ostringstream stream; stream << cellIndex; std::map::const_iterator cell = mMap.find (cellIndex); if (cell!=mMap.end()) { if (!cell->second.mName.empty()) stream << " " << cell->second.mName; if (cell->second.mDeleted) stream << " (deleted)"; if (!cell->second.mRegion.empty()) { stream << "
"; std::map::const_iterator iter = mColours.find (Misc::StringUtils::lowerCase (cell->second.mRegion)); if (iter!=mColours.end()) stream << cell->second.mRegion; else stream << "" << cell->second.mRegion << ""; } } else stream << " (no cell)"; return QString::fromUtf8 (stream.str().c_str()); } if (role==Role_Region) { CellCoordinates cellIndex = getIndex (index); std::map::const_iterator cell = mMap.find (cellIndex); if (cell!=mMap.end() && !cell->second.mRegion.empty()) return QString::fromUtf8 (Misc::StringUtils::lowerCase (cell->second.mRegion).c_str()); } if (role==Role_CellId) { CellCoordinates cellIndex = getIndex (index); std::ostringstream stream; stream << "#" << cellIndex.getX() << " " << cellIndex.getY(); return QString::fromUtf8 (stream.str().c_str()); } return QVariant(); } Qt::ItemFlags CSMWorld::RegionMap::flags (const QModelIndex& index) const { return Qt::ItemIsSelectable | Qt::ItemIsEnabled; } void CSMWorld::RegionMap::regionsAboutToBeRemoved (const QModelIndex& parent, int start, int end) { std::vector update; const IdCollection& regions = mData.getRegions(); for (int i=start; i<=end; ++i) { const Record& region = regions.getRecord (i); update.push_back (region.get().mId); removeRegion (region.get().mId); } updateRegions (update); } void CSMWorld::RegionMap::regionsInserted (const QModelIndex& parent, int start, int end) { std::vector update; const IdCollection& regions = mData.getRegions(); for (int i=start; i<=end; ++i) { const Record& region = regions.getRecord (i); if (!region.isDeleted()) { update.push_back (region.get().mId); addRegion (region.get().mId, region.get().mMapColor); } } updateRegions (update); } void CSMWorld::RegionMap::regionsChanged (const QModelIndex& topLeft, const QModelIndex& bottomRight) { // Note: At this point an additional check could be inserted to see if there is any change to the // columns we are interested in. If not we can exit the function here and avoid all updating. std::vector update; const IdCollection& regions = mData.getRegions(); for (int i=topLeft.row(); i<=bottomRight.column(); ++i) { const Record& region = regions.getRecord (i); update.push_back (region.get().mId); if (!region.isDeleted()) addRegion (region.get().mId, region.get().mMapColor); else removeRegion (region.get().mId); } updateRegions (update); } void CSMWorld::RegionMap::cellsAboutToBeRemoved (const QModelIndex& parent, int start, int end) { const IdCollection& cells = mData.getCells(); for (int i=start; i<=end; ++i) { const Record& cell = cells.getRecord (i); const Cell& cell2 = cell.get(); if (cell2.isExterior()) removeCell (getIndex (cell2)); } } void CSMWorld::RegionMap::cellsInserted (const QModelIndex& parent, int start, int end) { addCells (start, end); } void CSMWorld::RegionMap::cellsChanged (const QModelIndex& topLeft, const QModelIndex& bottomRight) { // Note: At this point an additional check could be inserted to see if there is any change to the // columns we are interested in. If not we can exit the function here and avoid all updating. addCells (topLeft.row(), bottomRight.row()); } ================================================ FILE: apps/opencs/model/world/regionmap.hpp ================================================ #ifndef CSM_WOLRD_REGIONMAP_H #define CSM_WOLRD_REGIONMAP_H #include #include #include #include #include "record.hpp" #include "cell.hpp" #include "cellcoordinates.hpp" namespace CSMWorld { class Data; /// \brief Model for the region map /// /// This class does not holds any record data (other than for the purpose of buffering). class RegionMap : public QAbstractTableModel { Q_OBJECT public: enum Role { Role_Region = Qt::UserRole, Role_CellId = Qt::UserRole+1 }; private: struct CellDescription { bool mDeleted; std::string mRegion; std::string mName; CellDescription(); CellDescription (const Record& cell); }; Data& mData; std::map mMap; CellCoordinates mMin; ///< inclusive CellCoordinates mMax; ///< exclusive std::map mColours; ///< region ID, colour (RGBA) CellCoordinates getIndex (const QModelIndex& index) const; ///< Translates a Qt model index into a cell index (which can contain negative components) QModelIndex getIndex (const CellCoordinates& index) const; CellCoordinates getIndex (const Cell& cell) const; void buildRegions(); void buildMap(); void addCell (const CellCoordinates& index, const CellDescription& description); ///< May be called on a cell that is already in the map (in which case an update is // performed) void addCells (int start, int end); void removeCell (const CellCoordinates& index); ///< May be called on a cell that is not in the map (in which case the call is ignored) void addRegion (const std::string& region, unsigned int colour); ///< May be called on a region that is already listed (in which case an update is /// performed) /// /// \note This function does not update the region map. void removeRegion (const std::string& region); ///< May be called on a region that is not listed (in which case the call is ignored) /// /// \note This function does not update the region map. void updateRegions (const std::vector& regions); ///< Update cells affected by the listed regions void updateSize(); std::pair getSize() const; public: RegionMap (Data& data); int rowCount (const QModelIndex& parent = QModelIndex()) const override; int columnCount (const QModelIndex& parent = QModelIndex()) const override; QVariant data (const QModelIndex& index, int role = Qt::DisplayRole) const override; ///< \note Calling this function with role==Role_CellId may return the ID of a cell /// that does not exist. Qt::ItemFlags flags (const QModelIndex& index) const override; private slots: void regionsAboutToBeRemoved (const QModelIndex& parent, int start, int end); void regionsInserted (const QModelIndex& parent, int start, int end); void regionsChanged (const QModelIndex& topLeft, const QModelIndex& bottomRight); void cellsAboutToBeRemoved (const QModelIndex& parent, int start, int end); void cellsInserted (const QModelIndex& parent, int start, int end); void cellsChanged (const QModelIndex& topLeft, const QModelIndex& bottomRight); }; } #endif ================================================ FILE: apps/opencs/model/world/resources.cpp ================================================ #include "resources.hpp" #include #include #include #include #include CSMWorld::Resources::Resources (const VFS::Manager* vfs, const std::string& baseDirectory, UniversalId::Type type, const char * const *extensions) : mBaseDirectory (baseDirectory), mType (type) { recreate(vfs, extensions); } void CSMWorld::Resources::recreate(const VFS::Manager* vfs, const char * const *extensions) { mFiles.clear(); mIndex.clear(); size_t baseSize = mBaseDirectory.size(); const std::map& index = vfs->getIndex(); for (std::map::const_iterator it = index.begin(); it != index.end(); ++it) { std::string filepath = it->first; if (filepath.size() (mFiles.size())-1)); } } int CSMWorld::Resources::getSize() const { return static_cast(mFiles.size()); } std::string CSMWorld::Resources::getId (int index) const { return mFiles.at (index); } int CSMWorld::Resources::getIndex (const std::string& id) const { int index = searchId (id); if (index==-1) { std::ostringstream stream; stream << "Invalid resource: " << mBaseDirectory << '/' << id; throw std::runtime_error (stream.str()); } return index; } int CSMWorld::Resources::searchId (const std::string& id) const { std::string id2 = Misc::StringUtils::lowerCase (id); std::replace (id2.begin(), id2.end(), '\\', '/'); std::map::const_iterator iter = mIndex.find (id2); if (iter==mIndex.end()) return -1; return iter->second; } CSMWorld::UniversalId::Type CSMWorld::Resources::getType() const { return mType; } ================================================ FILE: apps/opencs/model/world/resources.hpp ================================================ #ifndef CSM_WOLRD_RESOURCES_H #define CSM_WOLRD_RESOURCES_H #include #include #include #include "universalid.hpp" namespace VFS { class Manager; } namespace CSMWorld { class Resources { std::map mIndex; std::vector mFiles; std::string mBaseDirectory; UniversalId::Type mType; public: /// \param type Type of resources in this table. Resources (const VFS::Manager* vfs, const std::string& baseDirectory, UniversalId::Type type, const char * const *extensions = nullptr); void recreate(const VFS::Manager* vfs, const char * const *extensions = nullptr); int getSize() const; std::string getId (int index) const; int getIndex (const std::string& id) const; int searchId (const std::string& id) const; UniversalId::Type getType() const; }; } #endif ================================================ FILE: apps/opencs/model/world/resourcesmanager.cpp ================================================ #include "resourcesmanager.hpp" #include CSMWorld::ResourcesManager::ResourcesManager() : mVFS(nullptr) { } void CSMWorld::ResourcesManager::addResources (const Resources& resources) { mResources.insert (std::make_pair (resources.getType(), resources)); mResources.insert (std::make_pair (UniversalId::getParentType (resources.getType()), resources)); } const char * const * CSMWorld::ResourcesManager::getMeshExtensions() { // maybe we could go over the osgDB::Registry to list all supported node formats static const char * const sMeshTypes[] = { "nif", "osg", "osgt", "osgb", "osgx", "osg2", "dae", 0 }; return sMeshTypes; } void CSMWorld::ResourcesManager::setVFS(const VFS::Manager *vfs) { mVFS = vfs; mResources.clear(); addResources (Resources (vfs, "meshes", UniversalId::Type_Mesh, getMeshExtensions())); addResources (Resources (vfs, "icons", UniversalId::Type_Icon)); addResources (Resources (vfs, "music", UniversalId::Type_Music)); addResources (Resources (vfs, "sound", UniversalId::Type_SoundRes)); addResources (Resources (vfs, "textures", UniversalId::Type_Texture)); addResources (Resources (vfs, "video", UniversalId::Type_Video)); } const VFS::Manager* CSMWorld::ResourcesManager::getVFS() const { return mVFS; } void CSMWorld::ResourcesManager::recreateResources() { std::map::iterator it = mResources.begin(); for ( ; it != mResources.end(); ++it) { if (it->first == UniversalId::Type_Mesh) it->second.recreate(mVFS, getMeshExtensions()); else it->second.recreate(mVFS); } } const CSMWorld::Resources& CSMWorld::ResourcesManager::get (UniversalId::Type type) const { std::map::const_iterator iter = mResources.find (type); if (iter==mResources.end()) throw std::logic_error ("Unknown resource type"); return iter->second; } ================================================ FILE: apps/opencs/model/world/resourcesmanager.hpp ================================================ #ifndef CSM_WOLRD_RESOURCESMANAGER_H #define CSM_WOLRD_RESOURCESMANAGER_H #include #include "universalid.hpp" #include "resources.hpp" namespace VFS { class Manager; } namespace CSMWorld { class ResourcesManager { std::map mResources; const VFS::Manager* mVFS; private: void addResources (const Resources& resources); const char * const * getMeshExtensions(); public: ResourcesManager(); const VFS::Manager* getVFS() const; void setVFS(const VFS::Manager* vfs); void recreateResources(); const Resources& get (UniversalId::Type type) const; }; } #endif ================================================ FILE: apps/opencs/model/world/resourcetable.cpp ================================================ #include "resourcetable.hpp" #include #include "resources.hpp" #include "columnbase.hpp" #include "universalid.hpp" CSMWorld::ResourceTable::ResourceTable (const Resources *resources, unsigned int features) : IdTableBase (features | Feature_Constant), mResources (resources) {} CSMWorld::ResourceTable::~ResourceTable() {} int CSMWorld::ResourceTable::rowCount (const QModelIndex & parent) const { if (parent.isValid()) return 0; return mResources->getSize(); } int CSMWorld::ResourceTable::columnCount (const QModelIndex & parent) const { if (parent.isValid()) return 0; return 2; // ID, type } QVariant CSMWorld::ResourceTable::data (const QModelIndex & index, int role) const { if (role!=Qt::DisplayRole) return QVariant(); if (index.column()==0) return QString::fromUtf8 (mResources->getId (index.row()).c_str()); if (index.column()==1) return static_cast (mResources->getType()); throw std::logic_error ("Invalid column in resource table"); } QVariant CSMWorld::ResourceTable::headerData (int section, Qt::Orientation orientation, int role ) const { if (orientation==Qt::Vertical) return QVariant(); if (role==ColumnBase::Role_Flags) return section==0 ? ColumnBase::Flag_Table : 0; switch (section) { case 0: if (role==Qt::DisplayRole) return Columns::getName (Columns::ColumnId_Id).c_str(); if (role==ColumnBase::Role_Display) return ColumnBase::Display_Id; break; case 1: if (role==Qt::DisplayRole) return Columns::getName (Columns::ColumnId_RecordType).c_str(); if (role==ColumnBase::Role_Display) return ColumnBase::Display_Integer; break; } return QVariant(); } bool CSMWorld::ResourceTable::setData ( const QModelIndex &index, const QVariant &value, int role) { return false; } Qt::ItemFlags CSMWorld::ResourceTable::flags (const QModelIndex & index) const { return Qt::ItemIsSelectable | Qt::ItemIsEnabled; } QModelIndex CSMWorld::ResourceTable::index (int row, int column, const QModelIndex& parent) const { if (parent.isValid()) return QModelIndex(); if (row<0 || row>=mResources->getSize()) return QModelIndex(); if (column<0 || column>1) return QModelIndex(); return createIndex (row, column); } QModelIndex CSMWorld::ResourceTable::parent (const QModelIndex& index) const { return QModelIndex(); } QModelIndex CSMWorld::ResourceTable::getModelIndex (const std::string& id, int column) const { int row = mResources->searchId(id); if (row != -1) return index (row, column); return QModelIndex(); } int CSMWorld::ResourceTable::searchColumnIndex (Columns::ColumnId id) const { if (id==Columns::ColumnId_Id) return 0; if (id==Columns::ColumnId_RecordType) return 1; return -1; } int CSMWorld::ResourceTable::findColumnIndex (Columns::ColumnId id) const { int index = searchColumnIndex (id); if (index==-1) throw std::logic_error ("invalid column index"); return index; } std::pair CSMWorld::ResourceTable::view (int row) const { return std::make_pair (UniversalId::Type_None, ""); } bool CSMWorld::ResourceTable::isDeleted (const std::string& id) const { return false; } int CSMWorld::ResourceTable::getColumnId (int column) const { switch (column) { case 0: return Columns::ColumnId_Id; case 1: return Columns::ColumnId_RecordType; } return -1; } void CSMWorld::ResourceTable::beginReset() { beginResetModel(); } void CSMWorld::ResourceTable::endReset() { endResetModel(); } ================================================ FILE: apps/opencs/model/world/resourcetable.hpp ================================================ #ifndef CSM_WOLRD_RESOURCETABLE_H #define CSM_WOLRD_RESOURCETABLE_H #include "idtablebase.hpp" namespace CSMWorld { class Resources; class ResourceTable : public IdTableBase { const Resources *mResources; public: /// \note The feature Feature_Constant will be added implicitly. ResourceTable (const Resources *resources, unsigned int features = 0); virtual ~ResourceTable(); int rowCount (const QModelIndex & parent = QModelIndex()) const override; int columnCount (const QModelIndex & parent = QModelIndex()) const override; QVariant data (const QModelIndex & index, int role = Qt::DisplayRole) const override; QVariant headerData (int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override; bool setData ( const QModelIndex &index, const QVariant &value, int role = Qt::EditRole) override; Qt::ItemFlags flags (const QModelIndex & index) const override; QModelIndex index (int row, int column, const QModelIndex& parent = QModelIndex()) const override; QModelIndex parent (const QModelIndex& index) const override; QModelIndex getModelIndex (const std::string& id, int column) const override; /// Return index of column with the given \a id. If no such column exists, -1 is /// returned. int searchColumnIndex (Columns::ColumnId id) const override; /// Return index of column with the given \a id. If no such column exists, an /// exception is thrown. int findColumnIndex (Columns::ColumnId id) const override; /// Return the UniversalId and the hint for viewing \a row. If viewing is not /// supported by this table, return (UniversalId::Type_None, ""). std::pair view (int row) const override; /// Is \a id flagged as deleted? bool isDeleted (const std::string& id) const override; int getColumnId (int column) const override; /// Signal Qt that the data is about to change. void beginReset(); /// Signal Qt that the data has been changed. void endReset(); }; } #endif ================================================ FILE: apps/opencs/model/world/scope.cpp ================================================ #include "scope.hpp" #include #include CSMWorld::Scope CSMWorld::getScopeFromId (const std::string& id) { // get root namespace std::string namespace_; std::string::size_type i = id.find ("::"); if (i!=std::string::npos) namespace_ = Misc::StringUtils::lowerCase (id.substr (0, i)); if (namespace_=="project") return Scope_Project; if (namespace_=="session") return Scope_Session; return Scope_Content; } ================================================ FILE: apps/opencs/model/world/scope.hpp ================================================ #ifndef CSM_WOLRD_SCOPE_H #define CSM_WOLRD_SCOPE_H #include namespace CSMWorld { enum Scope { // record stored in content file Scope_Content = 1, // record stored in project file Scope_Project = 2, // record that exists only for the duration of one editing session Scope_Session = 4 }; Scope getScopeFromId (const std::string& id); } #endif ================================================ FILE: apps/opencs/model/world/scriptcontext.cpp ================================================ #include "scriptcontext.hpp" #include #include #include #include #include #include "data.hpp" CSMWorld::ScriptContext::ScriptContext (const Data& data) : mData (data), mIdsUpdated (false) {} bool CSMWorld::ScriptContext::canDeclareLocals() const { return true; } char CSMWorld::ScriptContext::getGlobalType (const std::string& name) const { int index = mData.getGlobals().searchId (name); if (index!=-1) { switch (mData.getGlobals().getRecord (index).get().mValue.getType()) { case ESM::VT_Short: return 's'; case ESM::VT_Long: return 'l'; case ESM::VT_Float: return 'f'; default: return ' '; } } return ' '; } std::pair CSMWorld::ScriptContext::getMemberType (const std::string& name, const std::string& id) const { std::string id2 = Misc::StringUtils::lowerCase (id); int index = mData.getScripts().searchId (id2); bool reference = false; if (index==-1) { // ID is not a script ID. Search for a matching referenceable instead. index = mData.getReferenceables().searchId (id2); if (index!=-1) { // Referenceable found. int columnIndex = mData.getReferenceables().findColumnIndex (Columns::ColumnId_Script); id2 = Misc::StringUtils::lowerCase (mData.getReferenceables(). getData (index, columnIndex).toString().toUtf8().constData()); if (!id2.empty()) { // Referenceable has a script -> use it. index = mData.getScripts().searchId (id2); reference = true; } } } if (index==-1) return std::make_pair (' ', false); std::map::iterator iter = mLocals.find (id2); if (iter==mLocals.end()) { Compiler::Locals locals; Compiler::NullErrorHandler errorHandler; std::istringstream stream (mData.getScripts().getRecord (index).get().mScriptText); Compiler::QuickFileParser parser (errorHandler, *this, locals); Compiler::Scanner scanner (errorHandler, stream, getExtensions()); scanner.scan (parser); iter = mLocals.insert (std::make_pair (id2, locals)).first; } return std::make_pair (iter->second.getType (Misc::StringUtils::lowerCase (name)), reference); } bool CSMWorld::ScriptContext::isId (const std::string& name) const { if (!mIdsUpdated) { mIds = mData.getIds(); std::for_each (mIds.begin(), mIds.end(), &Misc::StringUtils::lowerCaseInPlace); std::sort (mIds.begin(), mIds.end()); mIdsUpdated = true; } return std::binary_search (mIds.begin(), mIds.end(), Misc::StringUtils::lowerCase (name)); } bool CSMWorld::ScriptContext::isJournalId (const std::string& name) const { return mData.getJournals().searchId (name)!=-1; } void CSMWorld::ScriptContext::invalidateIds() { mIdsUpdated = false; } void CSMWorld::ScriptContext::clear() { mIds.clear(); mIdsUpdated = false; mLocals.clear(); } bool CSMWorld::ScriptContext::clearLocals (const std::string& script) { std::map::iterator iter = mLocals.find (Misc::StringUtils::lowerCase (script)); if (iter!=mLocals.end()) { mLocals.erase (iter); mIdsUpdated = false; return true; } return false; } ================================================ FILE: apps/opencs/model/world/scriptcontext.hpp ================================================ #ifndef CSM_WORLD_SCRIPTCONTEXT_H #define CSM_WORLD_SCRIPTCONTEXT_H #include #include #include #include #include namespace CSMWorld { class Data; class ScriptContext : public Compiler::Context { const Data& mData; mutable std::vector mIds; mutable bool mIdsUpdated; mutable std::map mLocals; public: ScriptContext (const Data& data); bool canDeclareLocals() const override; ///< Is the compiler allowed to declare local variables? char getGlobalType (const std::string& name) const override; ///< 'l: long, 's': short, 'f': float, ' ': does not exist. std::pair getMemberType (const std::string& name, const std::string& id) const override; ///< Return type of member variable \a name in script \a id or in script of reference of /// \a id /// \return first: 'l: long, 's': short, 'f': float, ' ': does not exist. /// second: true: script of reference bool isId (const std::string& name) const override; ///< Does \a name match an ID, that can be referenced? bool isJournalId (const std::string& name) const override; ///< Does \a name match a journal ID? void invalidateIds(); void clear(); ///< Remove all cached data. /// \return Were there any locals that needed clearing? bool clearLocals (const std::string& script); }; } #endif ================================================ FILE: apps/opencs/model/world/subcellcollection.hpp ================================================ #ifndef CSM_WOLRD_SUBCOLLECTION_H #define CSM_WOLRD_SUBCOLLECTION_H #include "nestedidcollection.hpp" namespace ESM { class ESMReader; } namespace CSMWorld { struct Cell; template class IdCollection; /// \brief Single type collection of top level records that are associated with cells template > class SubCellCollection : public NestedIdCollection { const IdCollection& mCells; void loadRecord (ESXRecordT& record, ESM::ESMReader& reader, bool& isDeleted) override; public: SubCellCollection (const IdCollection& cells); }; template void SubCellCollection::loadRecord (ESXRecordT& record, ESM::ESMReader& reader, bool& isDeleted) { record.load (reader, isDeleted, mCells); } template SubCellCollection::SubCellCollection ( const IdCollection& cells) : mCells (cells) {} } #endif ================================================ FILE: apps/opencs/model/world/tablemimedata.cpp ================================================ #include "tablemimedata.hpp" #include #include #include "universalid.hpp" #include "columnbase.hpp" CSMWorld::TableMimeData::TableMimeData (UniversalId id, const CSMDoc::Document& document) : mDocument(document) { mUniversalId.push_back (id); mObjectsFormats << QString::fromUtf8 (("tabledata/" + id.getTypeName()).c_str()); } CSMWorld::TableMimeData::TableMimeData (const std::vector< CSMWorld::UniversalId >& id, const CSMDoc::Document& document) : mUniversalId (id), mDocument(document) { for (std::vector::iterator it (mUniversalId.begin()); it != mUniversalId.end(); ++it) { mObjectsFormats << QString::fromUtf8 (("tabledata/" + it->getTypeName()).c_str()); } } QStringList CSMWorld::TableMimeData::formats() const { return mObjectsFormats; } CSMWorld::TableMimeData::~TableMimeData() { } std::string CSMWorld::TableMimeData::getIcon() const { if (mUniversalId.empty()) { qDebug()<<"TableMimeData object does not hold any records!"; //because throwing in the event loop tends to be problematic throw std::runtime_error ("TableMimeData object does not hold any records!"); } std::string tmpIcon; bool firstIteration = true; for (unsigned i = 0; i < mUniversalId.size(); ++i) { if (firstIteration) { firstIteration = false; tmpIcon = mUniversalId[i].getIcon(); continue; } if (tmpIcon != mUniversalId[i].getIcon()) { return ":/multitype.png"; //icon stolen from gnome TODO: get new icon } tmpIcon = mUniversalId[i].getIcon(); } return mUniversalId.begin()->getIcon(); //All objects are of the same type; } std::vector< CSMWorld::UniversalId > CSMWorld::TableMimeData::getData() const { return mUniversalId; } bool CSMWorld::TableMimeData::isReferencable(CSMWorld::ColumnBase::Display type) const { return ( type == CSMWorld::ColumnBase::Display_Activator || type == CSMWorld::ColumnBase::Display_Potion || type == CSMWorld::ColumnBase::Display_Apparatus || type == CSMWorld::ColumnBase::Display_Armor || type == CSMWorld::ColumnBase::Display_Book || type == CSMWorld::ColumnBase::Display_Clothing || type == CSMWorld::ColumnBase::Display_Container || type == CSMWorld::ColumnBase::Display_Creature || type == CSMWorld::ColumnBase::Display_Door || type == CSMWorld::ColumnBase::Display_Ingredient || type == CSMWorld::ColumnBase::Display_CreatureLevelledList || type == CSMWorld::ColumnBase::Display_ItemLevelledList || type == CSMWorld::ColumnBase::Display_Light || type == CSMWorld::ColumnBase::Display_Lockpick || type == CSMWorld::ColumnBase::Display_Miscellaneous || type == CSMWorld::ColumnBase::Display_Npc || type == CSMWorld::ColumnBase::Display_Probe || type == CSMWorld::ColumnBase::Display_Repair || type == CSMWorld::ColumnBase::Display_Static || type == CSMWorld::ColumnBase::Display_Weapon); } bool CSMWorld::TableMimeData::isReferencable(CSMWorld::UniversalId::Type type) { return ( type == CSMWorld::UniversalId::Type_Activator || type == CSMWorld::UniversalId::Type_Potion || type == CSMWorld::UniversalId::Type_Apparatus || type == CSMWorld::UniversalId::Type_Armor || type == CSMWorld::UniversalId::Type_Book || type == CSMWorld::UniversalId::Type_Clothing || type == CSMWorld::UniversalId::Type_Container || type == CSMWorld::UniversalId::Type_Creature || type == CSMWorld::UniversalId::Type_Door || type == CSMWorld::UniversalId::Type_Ingredient || type == CSMWorld::UniversalId::Type_CreatureLevelledList || type == CSMWorld::UniversalId::Type_ItemLevelledList || type == CSMWorld::UniversalId::Type_Light || type == CSMWorld::UniversalId::Type_Lockpick || type == CSMWorld::UniversalId::Type_Miscellaneous || type == CSMWorld::UniversalId::Type_Npc || type == CSMWorld::UniversalId::Type_Probe || type == CSMWorld::UniversalId::Type_Repair || type == CSMWorld::UniversalId::Type_Static || type == CSMWorld::UniversalId::Type_Weapon); } bool CSMWorld::TableMimeData::holdsType (CSMWorld::UniversalId::Type type) const { bool referencable = (type == CSMWorld::UniversalId::Type_Referenceable); for (std::vector::const_iterator it = mUniversalId.begin(); it != mUniversalId.end(); ++it) { if (referencable) { if (isReferencable(it->getType())) { return true; } } else { if (it->getType() == type) { return true; } } } return false; } bool CSMWorld::TableMimeData::holdsType (CSMWorld::ColumnBase::Display type) const { bool referencable = (type == CSMWorld::ColumnBase::Display_Referenceable); for (std::vector::const_iterator it = mUniversalId.begin(); it != mUniversalId.end(); ++it) { if (referencable) { if (isReferencable(it->getType())) { return true; } } else { if (it->getType() == convertEnums (type)) { return true; } } } return false; } CSMWorld::UniversalId CSMWorld::TableMimeData::returnMatching (CSMWorld::UniversalId::Type type) const { bool referencable = (type == CSMWorld::UniversalId::Type_Referenceable); for (std::vector::const_iterator it = mUniversalId.begin(); it != mUniversalId.end(); ++it) { if (referencable) { if (isReferencable(it->getType())) { return *it; } } else { if (it->getType() == type) { return *it; } } } throw std::runtime_error ("TableMimeData object does not hold object of the sought type"); } CSMWorld::UniversalId CSMWorld::TableMimeData::returnMatching (CSMWorld::ColumnBase::Display type) const { bool referencable = (type == CSMWorld::ColumnBase::Display_Referenceable); for (std::vector::const_iterator it = mUniversalId.begin(); it != mUniversalId.end(); ++it) { if (referencable) { if (isReferencable(it->getType())) { return *it; } } else { if (it->getType() == convertEnums (type)) { return *it; } } } throw std::runtime_error ("TableMimeData object does not hold object of the sought type"); } bool CSMWorld::TableMimeData::fromDocument (const CSMDoc::Document& document) const { return &document == &mDocument; } namespace { struct Mapping { CSMWorld::UniversalId::Type mUniversalIdType; CSMWorld::ColumnBase::Display mDisplayType; }; const Mapping mapping[] = { { CSMWorld::UniversalId::Type_Race, CSMWorld::ColumnBase::Display_Race }, { CSMWorld::UniversalId::Type_Skill, CSMWorld::ColumnBase::Display_Skill }, { CSMWorld::UniversalId::Type_Class, CSMWorld::ColumnBase::Display_Class }, { CSMWorld::UniversalId::Type_Faction, CSMWorld::ColumnBase::Display_Faction }, { CSMWorld::UniversalId::Type_Sound, CSMWorld::ColumnBase::Display_Sound }, { CSMWorld::UniversalId::Type_Region, CSMWorld::ColumnBase::Display_Region }, { CSMWorld::UniversalId::Type_Birthsign, CSMWorld::ColumnBase::Display_Birthsign }, { CSMWorld::UniversalId::Type_Spell, CSMWorld::ColumnBase::Display_Spell }, { CSMWorld::UniversalId::Type_Cell, CSMWorld::ColumnBase::Display_Cell }, { CSMWorld::UniversalId::Type_Referenceable, CSMWorld::ColumnBase::Display_Referenceable }, { CSMWorld::UniversalId::Type_Activator, CSMWorld::ColumnBase::Display_Activator }, { CSMWorld::UniversalId::Type_Potion, CSMWorld::ColumnBase::Display_Potion }, { CSMWorld::UniversalId::Type_Apparatus, CSMWorld::ColumnBase::Display_Apparatus }, { CSMWorld::UniversalId::Type_Armor, CSMWorld::ColumnBase::Display_Armor }, { CSMWorld::UniversalId::Type_Book, CSMWorld::ColumnBase::Display_Book }, { CSMWorld::UniversalId::Type_Clothing, CSMWorld::ColumnBase::Display_Clothing }, { CSMWorld::UniversalId::Type_Container, CSMWorld::ColumnBase::Display_Container }, { CSMWorld::UniversalId::Type_Creature, CSMWorld::ColumnBase::Display_Creature }, { CSMWorld::UniversalId::Type_Door, CSMWorld::ColumnBase::Display_Door }, { CSMWorld::UniversalId::Type_Ingredient, CSMWorld::ColumnBase::Display_Ingredient }, { CSMWorld::UniversalId::Type_CreatureLevelledList, CSMWorld::ColumnBase::Display_CreatureLevelledList }, { CSMWorld::UniversalId::Type_ItemLevelledList, CSMWorld::ColumnBase::Display_ItemLevelledList }, { CSMWorld::UniversalId::Type_Light, CSMWorld::ColumnBase::Display_Light }, { CSMWorld::UniversalId::Type_Lockpick, CSMWorld::ColumnBase::Display_Lockpick }, { CSMWorld::UniversalId::Type_Miscellaneous, CSMWorld::ColumnBase::Display_Miscellaneous }, { CSMWorld::UniversalId::Type_Npc, CSMWorld::ColumnBase::Display_Npc }, { CSMWorld::UniversalId::Type_Probe, CSMWorld::ColumnBase::Display_Probe }, { CSMWorld::UniversalId::Type_Repair, CSMWorld::ColumnBase::Display_Repair }, { CSMWorld::UniversalId::Type_Static, CSMWorld::ColumnBase::Display_Static }, { CSMWorld::UniversalId::Type_Weapon, CSMWorld::ColumnBase::Display_Weapon }, { CSMWorld::UniversalId::Type_Reference, CSMWorld::ColumnBase::Display_Reference }, { CSMWorld::UniversalId::Type_Filter, CSMWorld::ColumnBase::Display_Filter }, { CSMWorld::UniversalId::Type_Topic, CSMWorld::ColumnBase::Display_Topic }, { CSMWorld::UniversalId::Type_Journal, CSMWorld::ColumnBase::Display_Journal }, { CSMWorld::UniversalId::Type_TopicInfo, CSMWorld::ColumnBase::Display_TopicInfo }, { CSMWorld::UniversalId::Type_JournalInfo, CSMWorld::ColumnBase::Display_JournalInfo }, { CSMWorld::UniversalId::Type_Scene, CSMWorld::ColumnBase::Display_Scene }, { CSMWorld::UniversalId::Type_Script, CSMWorld::ColumnBase::Display_Script }, { CSMWorld::UniversalId::Type_Mesh, CSMWorld::ColumnBase::Display_Mesh }, { CSMWorld::UniversalId::Type_Icon, CSMWorld::ColumnBase::Display_Icon }, { CSMWorld::UniversalId::Type_Music, CSMWorld::ColumnBase::Display_Music }, { CSMWorld::UniversalId::Type_SoundRes, CSMWorld::ColumnBase::Display_SoundRes }, { CSMWorld::UniversalId::Type_Texture, CSMWorld::ColumnBase::Display_Texture }, { CSMWorld::UniversalId::Type_Video, CSMWorld::ColumnBase::Display_Video }, { CSMWorld::UniversalId::Type_Global, CSMWorld::ColumnBase::Display_GlobalVariable }, { CSMWorld::UniversalId::Type_BodyPart, CSMWorld::ColumnBase::Display_BodyPart }, { CSMWorld::UniversalId::Type_Enchantment, CSMWorld::ColumnBase::Display_Enchantment }, { CSMWorld::UniversalId::Type_None, CSMWorld::ColumnBase::Display_None } // end marker }; } CSMWorld::UniversalId::Type CSMWorld::TableMimeData::convertEnums (ColumnBase::Display type) { for (int i=0; mapping[i].mUniversalIdType!=CSMWorld::UniversalId::Type_None; ++i) if (mapping[i].mDisplayType==type) return mapping[i].mUniversalIdType; return CSMWorld::UniversalId::Type_None; } CSMWorld::ColumnBase::Display CSMWorld::TableMimeData::convertEnums (UniversalId::Type type) { for (int i=0; mapping[i].mUniversalIdType!=CSMWorld::UniversalId::Type_None; ++i) if (mapping[i].mUniversalIdType==type) return mapping[i].mDisplayType; return CSMWorld::ColumnBase::Display_None; } const CSMDoc::Document* CSMWorld::TableMimeData::getDocumentPtr() const { return &mDocument; } ================================================ FILE: apps/opencs/model/world/tablemimedata.hpp ================================================ #ifndef TABLEMIMEDATA_H #define TABLEMIMEDATA_H #include #include #include #include "universalid.hpp" #include "columnbase.hpp" namespace CSMDoc { class Document; } namespace CSMWorld { /// \brief Subclass of QmimeData, augmented to contain and transport UniversalIds. /// /// This class provides way to construct mimedata object holding the universalid copy /// Universalid is used in the majority of the tables to store type, id, argument types. /// This way universalid grants a way to retrieve record from the concrete table. /// Please note, that tablemimedata object can hold multiple universalIds in the vector. class TableMimeData : public QMimeData { std::vector mUniversalId; QStringList mObjectsFormats; const CSMDoc::Document& mDocument; public: TableMimeData(UniversalId id, const CSMDoc::Document& document); TableMimeData(const std::vector& id, const CSMDoc::Document& document); ~TableMimeData(); QStringList formats() const override; std::string getIcon() const; std::vector getData() const; bool holdsType(UniversalId::Type type) const; bool holdsType(CSMWorld::ColumnBase::Display type) const; bool fromDocument(const CSMDoc::Document& document) const; UniversalId returnMatching(UniversalId::Type type) const; const CSMDoc::Document* getDocumentPtr() const; UniversalId returnMatching(CSMWorld::ColumnBase::Display type) const; static CSMWorld::UniversalId::Type convertEnums(CSMWorld::ColumnBase::Display type); static CSMWorld::ColumnBase::Display convertEnums(CSMWorld::UniversalId::Type type); static bool isReferencable(CSMWorld::UniversalId::Type type); private: bool isReferencable(CSMWorld::ColumnBase::Display type) const; }; } #endif // TABLEMIMEDATA_H ================================================ FILE: apps/opencs/model/world/universalid.cpp ================================================ #include "universalid.hpp" #include #include #include #include namespace { struct TypeData { CSMWorld::UniversalId::Class mClass; CSMWorld::UniversalId::Type mType; const char *mName; const char *mIcon; }; static const TypeData sNoArg[] = { { CSMWorld::UniversalId::Class_None, CSMWorld::UniversalId::Type_None, "-", 0 }, { CSMWorld::UniversalId::Class_RecordList, CSMWorld::UniversalId::Type_Globals, "Global Variables", ":./global-variable.png" }, { CSMWorld::UniversalId::Class_RecordList, CSMWorld::UniversalId::Type_Gmsts, "Game Settings", ":./gmst.png" }, { CSMWorld::UniversalId::Class_RecordList, CSMWorld::UniversalId::Type_Skills, "Skills", ":./skill.png" }, { CSMWorld::UniversalId::Class_RecordList, CSMWorld::UniversalId::Type_Classes, "Classes", ":./class.png" }, { CSMWorld::UniversalId::Class_RecordList, CSMWorld::UniversalId::Type_Factions, "Factions", ":./faction.png" }, { CSMWorld::UniversalId::Class_RecordList, CSMWorld::UniversalId::Type_Races, "Races", ":./race.png" }, { CSMWorld::UniversalId::Class_RecordList, CSMWorld::UniversalId::Type_Sounds, "Sounds", ":./sound.png" }, { CSMWorld::UniversalId::Class_RecordList, CSMWorld::UniversalId::Type_Scripts, "Scripts", ":./script.png" }, { CSMWorld::UniversalId::Class_RecordList, CSMWorld::UniversalId::Type_Regions, "Regions", ":./region.png" }, { CSMWorld::UniversalId::Class_RecordList, CSMWorld::UniversalId::Type_Birthsigns, "Birthsigns", ":./birthsign.png" }, { CSMWorld::UniversalId::Class_RecordList, CSMWorld::UniversalId::Type_Spells, "Spells", ":./spell.png" }, { CSMWorld::UniversalId::Class_RecordList, CSMWorld::UniversalId::Type_Topics, "Topics", ":./dialogue-topics.png" }, { CSMWorld::UniversalId::Class_RecordList, CSMWorld::UniversalId::Type_Journals, "Journals", ":./journal-topics.png" }, { CSMWorld::UniversalId::Class_RecordList, CSMWorld::UniversalId::Type_TopicInfos, "Topic Infos", ":./dialogue-topic-infos.png" }, { CSMWorld::UniversalId::Class_RecordList, CSMWorld::UniversalId::Type_JournalInfos, "Journal Infos", ":./journal-topic-infos.png" }, { CSMWorld::UniversalId::Class_RecordList, CSMWorld::UniversalId::Type_Cells, "Cells", ":./cell.png" }, { CSMWorld::UniversalId::Class_RecordList, CSMWorld::UniversalId::Type_Enchantments, "Enchantments", ":./enchantment.png" }, { CSMWorld::UniversalId::Class_RecordList, CSMWorld::UniversalId::Type_BodyParts, "Body Parts", ":./body-part.png" }, { CSMWorld::UniversalId::Class_RecordList, CSMWorld::UniversalId::Type_Referenceables, "Objects", ":./object.png" }, { CSMWorld::UniversalId::Class_RecordList, CSMWorld::UniversalId::Type_References, "Instances", ":./instance.png" }, { CSMWorld::UniversalId::Class_NonRecord, CSMWorld::UniversalId::Type_RegionMap, "Region Map", ":./region-map.png" }, { CSMWorld::UniversalId::Class_RecordList, CSMWorld::UniversalId::Type_Filters, "Filters", ":./filter.png" }, { CSMWorld::UniversalId::Class_ResourceList, CSMWorld::UniversalId::Type_Meshes, "Meshes", ":./resources-mesh" }, { CSMWorld::UniversalId::Class_ResourceList, CSMWorld::UniversalId::Type_Icons, "Icons", ":./resources-icon" }, { CSMWorld::UniversalId::Class_ResourceList, CSMWorld::UniversalId::Type_Musics, "Music Files", ":./resources-music" }, { CSMWorld::UniversalId::Class_ResourceList, CSMWorld::UniversalId::Type_SoundsRes, "Sound Files", ":resources-sound" }, { CSMWorld::UniversalId::Class_ResourceList, CSMWorld::UniversalId::Type_Textures, "Textures", ":./resources-texture" }, { CSMWorld::UniversalId::Class_ResourceList, CSMWorld::UniversalId::Type_Videos, "Videos", ":./resources-video" }, { CSMWorld::UniversalId::Class_RecordList, CSMWorld::UniversalId::Type_DebugProfiles, "Debug Profiles", ":./debug-profile.png" }, { CSMWorld::UniversalId::Class_Transient, CSMWorld::UniversalId::Type_RunLog, "Run Log", ":./run-log.png" }, { CSMWorld::UniversalId::Class_RecordList, CSMWorld::UniversalId::Type_SoundGens, "Sound Generators", ":./sound-generator.png" }, { CSMWorld::UniversalId::Class_RecordList, CSMWorld::UniversalId::Type_MagicEffects, "Magic Effects", ":./magic-effect.png" }, { CSMWorld::UniversalId::Class_RecordList, CSMWorld::UniversalId::Type_Lands, "Lands", ":./land-heightmap.png" }, { CSMWorld::UniversalId::Class_RecordList, CSMWorld::UniversalId::Type_LandTextures, "Land Textures", ":./land-texture.png" }, { CSMWorld::UniversalId::Class_RecordList, CSMWorld::UniversalId::Type_Pathgrids, "Pathgrids", ":./pathgrid.png" }, { CSMWorld::UniversalId::Class_RecordList, CSMWorld::UniversalId::Type_StartScripts, "Start Scripts", ":./start-script.png" }, { CSMWorld::UniversalId::Class_RecordList, CSMWorld::UniversalId::Type_MetaDatas, "Metadata", ":./metadata.png" }, { CSMWorld::UniversalId::Class_None, CSMWorld::UniversalId::Type_None, 0, 0 } // end marker }; static const TypeData sIdArg[] = { { CSMWorld::UniversalId::Class_Record, CSMWorld::UniversalId::Type_Global, "Global Variable", ":./global-variable.png" }, { CSMWorld::UniversalId::Class_Record, CSMWorld::UniversalId::Type_Gmst, "Game Setting", ":./gmst.png" }, { CSMWorld::UniversalId::Class_Record, CSMWorld::UniversalId::Type_Skill, "Skill", ":./skill.png" }, { CSMWorld::UniversalId::Class_Record, CSMWorld::UniversalId::Type_Class, "Class", ":./class.png" }, { CSMWorld::UniversalId::Class_Record, CSMWorld::UniversalId::Type_Faction, "Faction", ":./faction.png" }, { CSMWorld::UniversalId::Class_Record, CSMWorld::UniversalId::Type_Race, "Race", ":./race.png" }, { CSMWorld::UniversalId::Class_Record, CSMWorld::UniversalId::Type_Sound, "Sound", ":./sound.png" }, { CSMWorld::UniversalId::Class_Record, CSMWorld::UniversalId::Type_Script, "Script", ":./script.png" }, { CSMWorld::UniversalId::Class_Record, CSMWorld::UniversalId::Type_Region, "Region", ":./region.png" }, { CSMWorld::UniversalId::Class_Record, CSMWorld::UniversalId::Type_Birthsign, "Birthsign", ":./birthsign.png" }, { CSMWorld::UniversalId::Class_Record, CSMWorld::UniversalId::Type_Spell, "Spell", ":./spell.png" }, { CSMWorld::UniversalId::Class_Record, CSMWorld::UniversalId::Type_Topic, "Topic", ":./dialogue-topics.png" }, { CSMWorld::UniversalId::Class_Record, CSMWorld::UniversalId::Type_Journal, "Journal", ":./journal-topics.png" }, { CSMWorld::UniversalId::Class_SubRecord, CSMWorld::UniversalId::Type_TopicInfo, "TopicInfo", ":./dialogue-topic-infos.png" }, { CSMWorld::UniversalId::Class_SubRecord, CSMWorld::UniversalId::Type_JournalInfo, "JournalInfo", ":./journal-topic-infos.png" }, { CSMWorld::UniversalId::Class_Record, CSMWorld::UniversalId::Type_Cell, "Cell", ":./cell.png" }, { CSMWorld::UniversalId::Class_Record, CSMWorld::UniversalId::Type_Cell_Missing, "Cell", ":./cell.png" }, { CSMWorld::UniversalId::Class_Record, CSMWorld::UniversalId::Type_Referenceable, "Object", ":./object.png" }, { CSMWorld::UniversalId::Class_RefRecord, CSMWorld::UniversalId::Type_Activator, "Activator", ":./activator.png" }, { CSMWorld::UniversalId::Class_RefRecord, CSMWorld::UniversalId::Type_Potion, "Potion", ":./potion.png" }, { CSMWorld::UniversalId::Class_RefRecord, CSMWorld::UniversalId::Type_Apparatus, "Apparatus", ":./apparatus.png" }, { CSMWorld::UniversalId::Class_RefRecord, CSMWorld::UniversalId::Type_Armor, "Armor", ":./armor.png" }, { CSMWorld::UniversalId::Class_RefRecord, CSMWorld::UniversalId::Type_Book, "Book", ":./book.png" }, { CSMWorld::UniversalId::Class_RefRecord, CSMWorld::UniversalId::Type_Clothing, "Clothing", ":./clothing.png" }, { CSMWorld::UniversalId::Class_RefRecord, CSMWorld::UniversalId::Type_Container, "Container", ":./container.png" }, { CSMWorld::UniversalId::Class_RefRecord, CSMWorld::UniversalId::Type_Creature, "Creature", ":./creature.png" }, { CSMWorld::UniversalId::Class_RefRecord, CSMWorld::UniversalId::Type_Door, "Door", ":./door.png" }, { CSMWorld::UniversalId::Class_RefRecord, CSMWorld::UniversalId::Type_Ingredient, "Ingredient", ":./ingredient.png" }, { CSMWorld::UniversalId::Class_RefRecord, CSMWorld::UniversalId::Type_CreatureLevelledList, "Creature Levelled List", ":./levelled-creature.png" }, { CSMWorld::UniversalId::Class_RefRecord, CSMWorld::UniversalId::Type_ItemLevelledList, "Item Levelled List", ":./levelled-item.png" }, { CSMWorld::UniversalId::Class_RefRecord, CSMWorld::UniversalId::Type_Light, "Light", ":./light.png" }, { CSMWorld::UniversalId::Class_RefRecord, CSMWorld::UniversalId::Type_Lockpick, "Lockpick", ":./lockpick.png" }, { CSMWorld::UniversalId::Class_RefRecord, CSMWorld::UniversalId::Type_Miscellaneous, "Miscellaneous", ":./miscellaneous.png" }, { CSMWorld::UniversalId::Class_RefRecord, CSMWorld::UniversalId::Type_Npc, "NPC", ":./npc.png" }, { CSMWorld::UniversalId::Class_RefRecord, CSMWorld::UniversalId::Type_Probe, "Probe", ":./probe.png" }, { CSMWorld::UniversalId::Class_RefRecord, CSMWorld::UniversalId::Type_Repair, "Repair", ":./repair.png" }, { CSMWorld::UniversalId::Class_RefRecord, CSMWorld::UniversalId::Type_Static, "Static", ":./static.png" }, { CSMWorld::UniversalId::Class_RefRecord, CSMWorld::UniversalId::Type_Weapon, "Weapon", ":./weapon.png" }, { CSMWorld::UniversalId::Class_SubRecord, CSMWorld::UniversalId::Type_Reference, "Instance", ":./instance.png" }, { CSMWorld::UniversalId::Class_Record, CSMWorld::UniversalId::Type_Filter, "Filter", ":./filter.png" }, { CSMWorld::UniversalId::Class_Collection, CSMWorld::UniversalId::Type_Scene, "Scene", ":./scene.png" }, { CSMWorld::UniversalId::Class_Collection, CSMWorld::UniversalId::Type_Preview, "Preview", ":./record-preview.png" }, { CSMWorld::UniversalId::Class_Record, CSMWorld::UniversalId::Type_Enchantment, "Enchantment", ":./enchantment.png" }, { CSMWorld::UniversalId::Class_Record, CSMWorld::UniversalId::Type_BodyPart, "Body Part", ":./body-part.png" }, { CSMWorld::UniversalId::Class_Resource, CSMWorld::UniversalId::Type_Mesh, "Mesh", ":./resources-mesh"}, { CSMWorld::UniversalId::Class_Resource, CSMWorld::UniversalId::Type_Icon, "Icon", ":./resources-icon"}, { CSMWorld::UniversalId::Class_Resource, CSMWorld::UniversalId::Type_Music, "Music", ":./resources-music" }, { CSMWorld::UniversalId::Class_Resource, CSMWorld::UniversalId::Type_SoundRes, "Sound File", ":./resources-sound" }, { CSMWorld::UniversalId::Class_Resource, CSMWorld::UniversalId::Type_Texture, "Texture", ":./resources-texture" }, { CSMWorld::UniversalId::Class_Resource, CSMWorld::UniversalId::Type_Video, "Video", ":./resources-video" }, { CSMWorld::UniversalId::Class_Record, CSMWorld::UniversalId::Type_DebugProfile, "Debug Profile", ":./debug-profile.png" }, { CSMWorld::UniversalId::Class_Record, CSMWorld::UniversalId::Type_SoundGen, "Sound Generator", ":./sound-generator.png" }, { CSMWorld::UniversalId::Class_Record, CSMWorld::UniversalId::Type_MagicEffect, "Magic Effect", ":./magic-effect.png" }, { CSMWorld::UniversalId::Class_Record, CSMWorld::UniversalId::Type_Land, "Land", ":./land-heightmap.png" }, { CSMWorld::UniversalId::Class_Record, CSMWorld::UniversalId::Type_LandTexture, "Land Texture", ":./land-texture.png" }, { CSMWorld::UniversalId::Class_Record, CSMWorld::UniversalId::Type_Pathgrid, "Pathgrid", ":./pathgrid.png" }, { CSMWorld::UniversalId::Class_Record, CSMWorld::UniversalId::Type_StartScript, "Start Script", ":./start-script.png" }, { CSMWorld::UniversalId::Class_Record, CSMWorld::UniversalId::Type_MetaData, "Metadata", ":./metadata.png" }, { CSMWorld::UniversalId::Class_None, CSMWorld::UniversalId::Type_None, 0, 0 } // end marker }; static const TypeData sIndexArg[] = { { CSMWorld::UniversalId::Class_Transient, CSMWorld::UniversalId::Type_VerificationResults, "Verification Results", ":./menu-verify.png" }, { CSMWorld::UniversalId::Class_Transient, CSMWorld::UniversalId::Type_LoadErrorLog, "Load Error Log", ":./error-log.png" }, { CSMWorld::UniversalId::Class_Transient, CSMWorld::UniversalId::Type_Search, "Global Search", ":./menu-search.png" }, { CSMWorld::UniversalId::Class_None, CSMWorld::UniversalId::Type_None, 0, 0 } // end marker }; } CSMWorld::UniversalId::UniversalId (const std::string& universalId) : mIndex(0) { std::string::size_type index = universalId.find (':'); if (index!=std::string::npos) { std::string type = universalId.substr (0, index); for (int i=0; sIdArg[i].mName; ++i) if (type==sIdArg[i].mName) { mArgumentType = ArgumentType_Id; mType = sIdArg[i].mType; mClass = sIdArg[i].mClass; mId = universalId.substr (index+2); return; } for (int i=0; sIndexArg[i].mName; ++i) if (type==sIndexArg[i].mName) { mArgumentType = ArgumentType_Index; mType = sIndexArg[i].mType; mClass = sIndexArg[i].mClass; std::istringstream stream (universalId.substr (index+2)); if (stream >> mIndex) return; break; } } else { for (int i=0; sNoArg[i].mName; ++i) if (universalId==sNoArg[i].mName) { mArgumentType = ArgumentType_None; mType = sNoArg[i].mType; mClass = sNoArg[i].mClass; return; } } throw std::runtime_error ("invalid UniversalId: " + universalId); } CSMWorld::UniversalId::UniversalId (Type type) : mArgumentType (ArgumentType_None), mType (type), mIndex (0) { for (int i=0; sNoArg[i].mName; ++i) if (type==sNoArg[i].mType) { mClass = sNoArg[i].mClass; return; } for (int i=0; sIdArg[i].mName; ++i) if (type==sIdArg[i].mType) { mArgumentType = ArgumentType_Id; mClass = sIdArg[i].mClass; return; } for (int i=0; sIndexArg[i].mName; ++i) if (type==sIndexArg[i].mType) { mArgumentType = ArgumentType_Index; mClass = sIndexArg[i].mClass; return; } throw std::logic_error ("invalid argument-less UniversalId type"); } CSMWorld::UniversalId::UniversalId (Type type, const std::string& id) : mArgumentType (ArgumentType_Id), mType (type), mId (id), mIndex (0) { for (int i=0; sIdArg[i].mName; ++i) if (type==sIdArg[i].mType) { mClass = sIdArg[i].mClass; return; } throw std::logic_error ("invalid ID argument UniversalId type"); } CSMWorld::UniversalId::UniversalId (Type type, int index) : mArgumentType (ArgumentType_Index), mType (type), mIndex (index) { for (int i=0; sIndexArg[i].mName; ++i) if (type==sIndexArg[i].mType) { mClass = sIndexArg[i].mClass; return; } throw std::logic_error ("invalid index argument UniversalId type"); } CSMWorld::UniversalId::Class CSMWorld::UniversalId::getClass() const { return mClass; } CSMWorld::UniversalId::ArgumentType CSMWorld::UniversalId::getArgumentType() const { return mArgumentType; } CSMWorld::UniversalId::Type CSMWorld::UniversalId::getType() const { return mType; } const std::string& CSMWorld::UniversalId::getId() const { if (mArgumentType!=ArgumentType_Id) throw std::logic_error ("invalid access to ID of non-ID UniversalId"); return mId; } int CSMWorld::UniversalId::getIndex() const { if (mArgumentType!=ArgumentType_Index) throw std::logic_error ("invalid access to index of non-index UniversalId"); return mIndex; } bool CSMWorld::UniversalId::isEqual (const UniversalId& universalId) const { if (mClass!=universalId.mClass || mArgumentType!=universalId.mArgumentType || mType!=universalId.mType) return false; switch (mArgumentType) { case ArgumentType_Id: return mId==universalId.mId; case ArgumentType_Index: return mIndex==universalId.mIndex; default: return true; } } bool CSMWorld::UniversalId::isLess (const UniversalId& universalId) const { if (mTypeuniversalId.mType) return false; switch (mArgumentType) { case ArgumentType_Id: return mId CSMWorld::UniversalId::listReferenceableTypes() { std::vector list; for (int i=0; sIdArg[i].mName; ++i) if (sIdArg[i].mClass==Class_RefRecord) list.push_back (sIdArg[i].mType); return list; } std::vector CSMWorld::UniversalId::listTypes (int classes) { std::vector list; for (int i=0; sNoArg[i].mName; ++i) if (sNoArg[i].mClass & classes) list.push_back (sNoArg[i].mType); for (int i=0; sIdArg[i].mName; ++i) if (sIdArg[i].mClass & classes) list.push_back (sIdArg[i].mType); for (int i=0; sIndexArg[i].mName; ++i) if (sIndexArg[i].mClass & classes) list.push_back (sIndexArg[i].mType); return list; } CSMWorld::UniversalId::Type CSMWorld::UniversalId::getParentType (Type type) { for (int i=0; sIdArg[i].mType; ++i) if (type==sIdArg[i].mType) { if (sIdArg[i].mClass==Class_RefRecord) return Type_Referenceables; if (sIdArg[i].mClass==Class_SubRecord || sIdArg[i].mClass==Class_Record || sIdArg[i].mClass==Class_Resource) { if (type==Type_Cell_Missing) return Type_Cells; return static_cast (type-1); } break; } return Type_None; } bool CSMWorld::operator== (const CSMWorld::UniversalId& left, const CSMWorld::UniversalId& right) { return left.isEqual (right); } bool CSMWorld::operator!= (const CSMWorld::UniversalId& left, const CSMWorld::UniversalId& right) { return !left.isEqual (right); } bool CSMWorld::operator< (const UniversalId& left, const UniversalId& right) { return left.isLess (right); } std::ostream& CSMWorld::operator< (std::ostream& stream, const CSMWorld::UniversalId& universalId) { return stream << universalId.toString(); } ================================================ FILE: apps/opencs/model/world/universalid.hpp ================================================ #ifndef CSM_WOLRD_UNIVERSALID_H #define CSM_WOLRD_UNIVERSALID_H #include #include #include #include namespace CSMWorld { class UniversalId { public: enum Class { Class_None = 0, Class_Record = 1, Class_RefRecord = 2, // referenceable record Class_SubRecord = 4, Class_RecordList = 8, Class_Collection = 16, // multiple types of records combined Class_Transient = 32, // not part of the world data or the project data Class_NonRecord = 64, // record like data that is not part of the world Class_Resource = 128, ///< \attention Resource IDs are unique only within the /// respective collection Class_ResourceList = 256 }; enum ArgumentType { ArgumentType_None, ArgumentType_Id, ArgumentType_Index }; /// \note A record list type must always be immediately followed by the matching /// record type, if this type is of class SubRecord or Record. enum Type { Type_None = 0, Type_Globals, Type_Global, Type_VerificationResults, Type_Gmsts, Type_Gmst, Type_Skills, Type_Skill, Type_Classes, Type_Class, Type_Factions, Type_Faction, Type_Races, Type_Race, Type_Sounds, Type_Sound, Type_Scripts, Type_Script, Type_Regions, Type_Region, Type_Birthsigns, Type_Birthsign, Type_Spells, Type_Spell, Type_Cells, Type_Cell, Type_Cell_Missing, //For cells that does not exist yet. Type_Referenceables, Type_Referenceable, Type_Activator, Type_Potion, Type_Apparatus, Type_Armor, Type_Book, Type_Clothing, Type_Container, Type_Creature, Type_Door, Type_Ingredient, Type_CreatureLevelledList, Type_ItemLevelledList, Type_Light, Type_Lockpick, Type_Miscellaneous, Type_Npc, Type_Probe, Type_Repair, Type_Static, Type_Weapon, Type_References, Type_Reference, Type_RegionMap, Type_Filters, Type_Filter, Type_Topics, Type_Topic, Type_Journals, Type_Journal, Type_TopicInfos, Type_TopicInfo, Type_JournalInfos, Type_JournalInfo, Type_Scene, Type_Preview, Type_LoadErrorLog, Type_Enchantments, Type_Enchantment, Type_BodyParts, Type_BodyPart, Type_Meshes, Type_Mesh, Type_Icons, Type_Icon, Type_Musics, Type_Music, Type_SoundsRes, Type_SoundRes, Type_Textures, Type_Texture, Type_Videos, Type_Video, Type_DebugProfiles, Type_DebugProfile, Type_SoundGens, Type_SoundGen, Type_MagicEffects, Type_MagicEffect, Type_Lands, Type_Land, Type_LandTextures, Type_LandTexture, Type_Pathgrids, Type_Pathgrid, Type_StartScripts, Type_StartScript, Type_Search, Type_MetaDatas, Type_MetaData, Type_RunLog }; enum { NumberOfTypes = Type_RunLog+1 }; private: Class mClass; ArgumentType mArgumentType; Type mType; std::string mId; int mIndex; public: UniversalId (const std::string& universalId); UniversalId (Type type = Type_None); UniversalId (Type type, const std::string& id); ///< Using a type for a non-ID-argument UniversalId will throw an exception. UniversalId (Type type, int index); ///< Using a type for a non-index-argument UniversalId will throw an exception. Class getClass() const; ArgumentType getArgumentType() const; Type getType() const; const std::string& getId() const; ///< Calling this function for a non-ID type will throw an exception. int getIndex() const; ///< Calling this function for a non-index type will throw an exception. bool isEqual (const UniversalId& universalId) const; bool isLess (const UniversalId& universalId) const; std::string getTypeName() const; std::string toString() const; std::string getIcon() const; ///< Will return an empty string, if no icon is available. static std::vector listReferenceableTypes(); static std::vector listTypes (int classes); /// If \a type is a SubRecord, RefRecord or Record type return the type of the table /// that contains records of type \a type. /// Otherwise return Type_None. static Type getParentType (Type type); }; bool operator== (const UniversalId& left, const UniversalId& right); bool operator!= (const UniversalId& left, const UniversalId& right); bool operator< (const UniversalId& left, const UniversalId& right); std::ostream& operator< (std::ostream& stream, const UniversalId& universalId); } Q_DECLARE_METATYPE (CSMWorld::UniversalId) #endif ================================================ FILE: apps/opencs/view/doc/adjusterwidget.cpp ================================================ #include "adjusterwidget.hpp" #include #include #include #include #include CSVDoc::AdjusterWidget::AdjusterWidget (QWidget *parent) : QWidget (parent), mValid (false), mAction (ContentAction_Undefined) { QHBoxLayout *layout = new QHBoxLayout (this); mIcon = new QLabel (this); layout->addWidget (mIcon, 0); mMessage = new QLabel (this); mMessage->setWordWrap (true); mMessage->setSizePolicy (QSizePolicy (QSizePolicy::Minimum, QSizePolicy::Minimum)); layout->addWidget (mMessage, 1); setName ("", false); setLayout (layout); } void CSVDoc::AdjusterWidget::setAction (ContentAction action) { mAction = action; } void CSVDoc::AdjusterWidget::setLocalData (const boost::filesystem::path& localData) { mLocalData = localData; } boost::filesystem::path CSVDoc::AdjusterWidget::getPath() const { if (!mValid) throw std::logic_error ("invalid content file path"); return mResultPath; } bool CSVDoc::AdjusterWidget::isValid() const { return mValid; } void CSVDoc::AdjusterWidget::setFilenameCheck (bool doCheck) { mDoFilenameCheck = doCheck; } void CSVDoc::AdjusterWidget::setName (const QString& name, bool addon) { QString message; mValid = (!name.isEmpty()); bool warning = false; if (!mValid) { message = "No name."; } else { boost::filesystem::path path (name.toUtf8().data()); std::string extension = Misc::StringUtils::lowerCase(path.extension().string()); bool isLegacyPath = (extension == ".esm" || extension == ".esp"); bool isFilePathChanged = (path.parent_path().string() != mLocalData.string()); if (isLegacyPath) path.replace_extension (addon ? ".omwaddon" : ".omwgame"); //if the file came from data-local and is not a legacy file to be converted, //don't worry about doing a file check. if (!isFilePathChanged && !isLegacyPath) { // path already points to the local data directory message = QString::fromUtf8 (("Will be saved as: " + path.string()).c_str()); mResultPath = path; } //in all other cases, ensure the path points to data-local and do an existing file check else { // path points somewhere else or is a leaf name. if (isFilePathChanged) path = mLocalData / path.filename(); message = QString::fromUtf8 (("Will be saved as: " + path.string()).c_str()); mResultPath = path; if (boost::filesystem::exists (path)) { /// \todo add an user setting to make this an error. message += "

A file with the same name already exists. If you continue, it will be overwritten."; warning = true; } } } mMessage->setText (message); mIcon->setPixmap (style()->standardIcon ( mValid ? (warning ? QStyle::SP_MessageBoxWarning : QStyle::SP_MessageBoxInformation) : QStyle::SP_MessageBoxCritical). pixmap (QSize (16, 16))); emit stateChanged (mValid); } ================================================ FILE: apps/opencs/view/doc/adjusterwidget.hpp ================================================ #ifndef CSV_DOC_ADJUSTERWIDGET_H #define CSV_DOC_ADJUSTERWIDGET_H #include #include class QLabel; namespace CSVDoc { enum ContentAction { ContentAction_New, ContentAction_Edit, ContentAction_Undefined }; class AdjusterWidget : public QWidget { Q_OBJECT public: boost::filesystem::path mLocalData; QLabel *mMessage; QLabel *mIcon; bool mValid; boost::filesystem::path mResultPath; ContentAction mAction; bool mDoFilenameCheck; public: AdjusterWidget (QWidget *parent = nullptr); void setLocalData (const boost::filesystem::path& localData); void setAction (ContentAction action); void setFilenameCheck (bool doCheck); bool isValid() const; boost::filesystem::path getPath() const; ///< This function must not be called if there is no valid path. public slots: void setName (const QString& name, bool addon); signals: void stateChanged (bool valid); }; } #endif ================================================ FILE: apps/opencs/view/doc/filedialog.cpp ================================================ #include "filedialog.hpp" #include #include #include #include #include #include #include #include #include #include #include "components/contentselector/model/esmfile.hpp" #include "components/contentselector/view/contentselector.hpp" #include "filewidget.hpp" #include "adjusterwidget.hpp" CSVDoc::FileDialog::FileDialog(QWidget *parent) : QDialog(parent), mSelector (nullptr), mAction(ContentAction_Undefined), mFileWidget (nullptr), mAdjusterWidget (nullptr), mDialogBuilt(false) { ui.setupUi (this); resize(400, 400); setObjectName ("FileDialog"); mSelector = new ContentSelectorView::ContentSelector (ui.contentSelectorWidget); mAdjusterWidget = new AdjusterWidget (this); } void CSVDoc::FileDialog::addFiles(const QString &path) { mSelector->addFiles(path); } void CSVDoc::FileDialog::setEncoding(const QString &encoding) { mSelector->setEncoding(encoding); } void CSVDoc::FileDialog::clearFiles() { mSelector->clearFiles(); } QStringList CSVDoc::FileDialog::selectedFilePaths() { QStringList filePaths; for (ContentSelectorModel::EsmFile *file : mSelector->selectedFiles() ) filePaths.append(file->filePath()); return filePaths; } void CSVDoc::FileDialog::setLocalData (const boost::filesystem::path& localData) { mAdjusterWidget->setLocalData (localData); } void CSVDoc::FileDialog::showDialog (ContentAction action) { mAction = action; ui.projectGroupBoxLayout->insertWidget (0, mAdjusterWidget); switch (mAction) { case ContentAction_New: buildNewFileView(); break; case ContentAction_Edit: buildOpenFileView(); break; default: break; } mAdjusterWidget->setFilenameCheck (mAction == ContentAction_New); if(!mDialogBuilt) { //connections common to both dialog view flavors connect (mSelector, SIGNAL (signalCurrentGamefileIndexChanged (int)), this, SLOT (slotUpdateAcceptButton (int))); connect (ui.projectButtonBox, SIGNAL (rejected()), this, SLOT (slotRejected())); mDialogBuilt = true; } show(); raise(); activateWindow(); } void CSVDoc::FileDialog::buildNewFileView() { setWindowTitle(tr("Create a new addon")); QPushButton* createButton = ui.projectButtonBox->button (QDialogButtonBox::Ok); createButton->setText ("Create"); createButton->setEnabled (false); if(!mFileWidget) { mFileWidget = new FileWidget (this); mFileWidget->setType (true); mFileWidget->extensionLabelIsVisible(true); connect (mFileWidget, SIGNAL (nameChanged (const QString&, bool)), mAdjusterWidget, SLOT (setName (const QString&, bool))); connect (mFileWidget, SIGNAL (nameChanged(const QString &, bool)), this, SLOT (slotUpdateAcceptButton(const QString &, bool))); } ui.projectGroupBoxLayout->insertWidget (0, mFileWidget); connect (ui.projectButtonBox, SIGNAL (accepted()), this, SLOT (slotNewFile())); } void CSVDoc::FileDialog::buildOpenFileView() { setWindowTitle(tr("Open")); ui.projectGroupBox->setTitle (QString("")); ui.projectButtonBox->button(QDialogButtonBox::Ok)->setText ("Open"); if(mSelector->isGamefileSelected()) ui.projectButtonBox->button(QDialogButtonBox::Ok)->setEnabled (true); else ui.projectButtonBox->button(QDialogButtonBox::Ok)->setEnabled (false); if(!mDialogBuilt) { connect (mSelector, SIGNAL (signalAddonDataChanged (const QModelIndex&, const QModelIndex&)), this, SLOT (slotAddonDataChanged(const QModelIndex&, const QModelIndex&))); } connect (ui.projectButtonBox, SIGNAL (accepted()), this, SLOT (slotOpenFile())); } void CSVDoc::FileDialog::slotAddonDataChanged(const QModelIndex &topleft, const QModelIndex &bottomright) { slotUpdateAcceptButton(0); } void CSVDoc::FileDialog::slotUpdateAcceptButton(int) { QString name = ""; if (mFileWidget && mAction == ContentAction_New) name = mFileWidget->getName(); slotUpdateAcceptButton (name, true); } void CSVDoc::FileDialog::slotUpdateAcceptButton(const QString &name, bool) { bool success = !mSelector->selectedFiles().empty(); bool isNew = (mAction == ContentAction_New); if (isNew) success = success && !(name.isEmpty()); else if (success) { ContentSelectorModel::EsmFile *file = mSelector->selectedFiles().back(); mAdjusterWidget->setName (file->filePath(), !file->isGameFile()); } else mAdjusterWidget->setName ("", true); ui.projectButtonBox->button (QDialogButtonBox::Ok)->setEnabled (success); } QString CSVDoc::FileDialog::filename() const { if (mAction == ContentAction_New) return ""; return mSelector->currentFile(); } void CSVDoc::FileDialog::slotRejected() { emit rejected(); disconnect (ui.projectButtonBox, SIGNAL (accepted()), this, SLOT (slotNewFile())); disconnect (ui.projectButtonBox, SIGNAL (accepted()), this, SLOT (slotOpenFile())); if(mFileWidget) { delete mFileWidget; mFileWidget = nullptr; } close(); } void CSVDoc::FileDialog::slotNewFile() { emit signalCreateNewFile (mAdjusterWidget->getPath()); if(mFileWidget) { delete mFileWidget; mFileWidget = nullptr; } disconnect (ui.projectButtonBox, SIGNAL (accepted()), this, SLOT (slotNewFile())); close(); } void CSVDoc::FileDialog::slotOpenFile() { ContentSelectorModel::EsmFile *file = mSelector->selectedFiles().back(); mAdjusterWidget->setName (file->filePath(), !file->isGameFile()); emit signalOpenFiles (mAdjusterWidget->getPath()); disconnect (ui.projectButtonBox, SIGNAL (accepted()), this, SLOT (slotOpenFile())); close(); } ================================================ FILE: apps/opencs/view/doc/filedialog.hpp ================================================ #ifndef FILEDIALOG_HPP #define FILEDIALOG_HPP #include #include #ifndef Q_MOC_RUN #include #include "adjusterwidget.hpp" #ifndef CS_QT_BOOST_FILESYSTEM_PATH_DECLARED #define CS_QT_BOOST_FILESYSTEM_PATH_DECLARED Q_DECLARE_METATYPE (boost::filesystem::path) #endif #endif #include "ui_filedialog.h" namespace ContentSelectorView { class ContentSelector; } namespace CSVDoc { class FileWidget; class FileDialog : public QDialog { Q_OBJECT private: ContentSelectorView::ContentSelector *mSelector; Ui::FileDialog ui; ContentAction mAction; FileWidget *mFileWidget; AdjusterWidget *mAdjusterWidget; bool mDialogBuilt; public: explicit FileDialog(QWidget *parent = nullptr); void showDialog (ContentAction action); void addFiles (const QString &path); void setEncoding (const QString &encoding); void clearFiles (); QString filename() const; QStringList selectedFilePaths(); void setLocalData (const boost::filesystem::path& localData); private: void buildNewFileView(); void buildOpenFileView(); signals: void signalOpenFiles (const boost::filesystem::path &path); void signalCreateNewFile (const boost::filesystem::path &path); void signalUpdateAcceptButton (bool, int); private slots: void slotNewFile(); void slotOpenFile(); void slotUpdateAcceptButton (int); void slotUpdateAcceptButton (const QString &, bool); void slotRejected(); void slotAddonDataChanged(const QModelIndex& topleft, const QModelIndex& bottomright); }; } #endif // FILEDIALOG_HPP ================================================ FILE: apps/opencs/view/doc/filewidget.cpp ================================================ #include "filewidget.hpp" #include #include #include #include #include QString CSVDoc::FileWidget::getExtension() const { return mAddon ? ".omwaddon" : ".omwgame"; } CSVDoc::FileWidget::FileWidget (QWidget *parent) : QWidget (parent), mAddon (false) { QHBoxLayout *layout = new QHBoxLayout (this); mInput = new QLineEdit (this); layout->addWidget (mInput, 1); mType = new QLabel (this); layout ->addWidget (mType); connect (mInput, SIGNAL (textChanged (const QString&)), this, SLOT (textChanged (const QString&))); setLayout (layout); } void CSVDoc::FileWidget::setType (bool addon) { mAddon = addon; mType->setText (getExtension()); } QString CSVDoc::FileWidget::getName() const { QString text = mInput->text(); if (text.isEmpty()) return ""; return text + getExtension(); } void CSVDoc::FileWidget::textChanged (const QString& text) { emit nameChanged (getName(), mAddon); } void CSVDoc::FileWidget::extensionLabelIsVisible(bool visible) { mType->setVisible(visible); } void CSVDoc::FileWidget::setName (const std::string& text) { QString text2 = QString::fromUtf8 (text.c_str()); mInput->setText (text2); textChanged (text2); } ================================================ FILE: apps/opencs/view/doc/filewidget.hpp ================================================ #ifndef CSV_DOC_FILEWIDGET_H #define CSV_DOC_FILEWIDGET_H #include #include class QLabel; class QString; class QLineEdit; namespace CSVDoc { class FileWidget : public QWidget { Q_OBJECT bool mAddon; QLineEdit *mInput; QLabel *mType; QString getExtension() const; public: FileWidget (QWidget *parent = nullptr); void setType (bool addon); QString getName() const; void extensionLabelIsVisible(bool visible); void setName (const std::string& text); private slots: void textChanged (const QString& text); signals: void nameChanged (const QString& file, bool addon); }; } #endif ================================================ FILE: apps/opencs/view/doc/globaldebugprofilemenu.cpp ================================================ #include "globaldebugprofilemenu.hpp" #include #include #include #include "../../model/world/idtable.hpp" #include "../../model/world/record.hpp" void CSVDoc::GlobalDebugProfileMenu::rebuild() { clear(); delete mActions; mActions = nullptr; int idColumn = mDebugProfiles->findColumnIndex (CSMWorld::Columns::ColumnId_Id); int stateColumn = mDebugProfiles->findColumnIndex (CSMWorld::Columns::ColumnId_Modification); int globalColumn = mDebugProfiles->findColumnIndex ( CSMWorld::Columns::ColumnId_GlobalProfile); int size = mDebugProfiles->rowCount(); std::vector ids; for (int i=0; idata (mDebugProfiles->index (i, stateColumn)).toInt(); bool global = mDebugProfiles->data (mDebugProfiles->index (i, globalColumn)).toInt(); if (state!=CSMWorld::RecordBase::State_Deleted && global) ids.push_back ( mDebugProfiles->data (mDebugProfiles->index (i, idColumn)).toString()); } mActions = new QActionGroup (this); connect (mActions, SIGNAL (triggered (QAction *)), this, SLOT (actionTriggered (QAction *))); std::sort (ids.begin(), ids.end()); for (std::vector::const_iterator iter (ids.begin()); iter!=ids.end(); ++iter) { mActions->addAction (addAction (*iter)); } } CSVDoc::GlobalDebugProfileMenu::GlobalDebugProfileMenu (CSMWorld::IdTable *debugProfiles, QWidget *parent) : QMenu (parent), mDebugProfiles (debugProfiles), mActions (nullptr) { rebuild(); connect (mDebugProfiles, SIGNAL (rowsAboutToBeRemoved (const QModelIndex&, int, int)), this, SLOT (profileAboutToBeRemoved (const QModelIndex&, int, int))); connect (mDebugProfiles, SIGNAL (rowsInserted (const QModelIndex&, int, int)), this, SLOT (profileInserted (const QModelIndex&, int, int))); connect (mDebugProfiles, SIGNAL (dataChanged (const QModelIndex&, const QModelIndex&)), this, SLOT (profileChanged (const QModelIndex&, const QModelIndex&))); } void CSVDoc::GlobalDebugProfileMenu::updateActions (bool running) { if (mActions) mActions->setEnabled (!running); } void CSVDoc::GlobalDebugProfileMenu::profileAboutToBeRemoved (const QModelIndex& parent, int start, int end) { rebuild(); } void CSVDoc::GlobalDebugProfileMenu::profileInserted (const QModelIndex& parent, int start, int end) { rebuild(); } void CSVDoc::GlobalDebugProfileMenu::profileChanged (const QModelIndex& topLeft, const QModelIndex& bottomRight) { rebuild(); } void CSVDoc::GlobalDebugProfileMenu::actionTriggered (QAction *action) { emit triggered (std::string (action->text().toUtf8().constData())); } ================================================ FILE: apps/opencs/view/doc/globaldebugprofilemenu.hpp ================================================ #ifndef CSV_DOC_GLOBALDEBUGPROFILEMENU_H #define CSV_DOC_GLOBALDEBUGPROFILEMENU_H #include class QModelIndex; class QActionGroup; namespace CSMWorld { class IdTable; } namespace CSVDoc { class GlobalDebugProfileMenu : public QMenu { Q_OBJECT CSMWorld::IdTable *mDebugProfiles; QActionGroup *mActions; private: void rebuild(); public: GlobalDebugProfileMenu (CSMWorld::IdTable *debugProfiles, QWidget *parent = nullptr); void updateActions (bool running); private slots: void profileAboutToBeRemoved (const QModelIndex& parent, int start, int end); void profileInserted (const QModelIndex& parent, int start, int end); void profileChanged (const QModelIndex& topLeft, const QModelIndex& bottomRight); void actionTriggered (QAction *action); signals: void triggered (const std::string& profile); }; } #endif ================================================ FILE: apps/opencs/view/doc/loader.cpp ================================================ #include "loader.hpp" #include #include #include #include #include #include #include #include "../../model/doc/document.hpp" void CSVDoc::LoadingDocument::closeEvent (QCloseEvent *event) { event->ignore(); cancel(); } CSVDoc::LoadingDocument::LoadingDocument (CSMDoc::Document *document) : mDocument (document), mAborted (false), mMessages (nullptr), mTotalRecords (0) { setWindowTitle (QString::fromUtf8((std::string("Opening ") + document->getSavePath().filename().string()).c_str())); setMinimumWidth (400); mLayout = new QVBoxLayout (this); // file progress mFile = new QLabel (this); mLayout->addWidget (mFile); mFileProgress = new QProgressBar (this); mLayout->addWidget (mFileProgress); int size = static_cast (document->getContentFiles().size())+1; if (document->isNew()) --size; mFileProgress->setMinimum (0); mFileProgress->setMaximum (size); mFileProgress->setTextVisible (true); mFileProgress->setValue (0); // record progress mLayout->addWidget (mRecords = new QLabel ("Records", this)); mRecordProgress = new QProgressBar (this); mLayout->addWidget (mRecordProgress); mRecordProgress->setMinimum (0); mRecordProgress->setTextVisible (true); mRecordProgress->setValue (0); // error message mError = new QLabel (this); mError->setWordWrap (true); mLayout->addWidget (mError); // buttons mButtons = new QDialogButtonBox (QDialogButtonBox::Cancel, Qt::Horizontal, this); mLayout->addWidget (mButtons); setLayout (mLayout); move (QCursor::pos()); show(); connect (mButtons, SIGNAL (rejected()), this, SLOT (cancel())); } void CSVDoc::LoadingDocument::nextStage (const std::string& name, int totalRecords) { mFile->setText (QString::fromUtf8 (("Loading: " + name).c_str())); mFileProgress->setValue (mFileProgress->value()+1); mRecordProgress->setValue (0); mRecordProgress->setMaximum (totalRecords>0 ? totalRecords : 1); mTotalRecords = totalRecords; } void CSVDoc::LoadingDocument::nextRecord (int records) { if (records<=mTotalRecords) { mRecordProgress->setValue (records); std::ostringstream stream; stream << "Records: " << records << " of " << mTotalRecords; mRecords->setText (QString::fromUtf8 (stream.str().c_str())); } } void CSVDoc::LoadingDocument::abort (const std::string& error) { mAborted = true; mError->setText (QString::fromUtf8 (("Loading failed: " + error + "").c_str())); mButtons->setStandardButtons (QDialogButtonBox::Close); } void CSVDoc::LoadingDocument::addMessage (const std::string& message) { if (!mMessages) { mMessages = new QListWidget (this); mLayout->insertWidget (4, mMessages); } new QListWidgetItem (QString::fromUtf8 (message.c_str()), mMessages); } void CSVDoc::LoadingDocument::cancel() { if (!mAborted) emit cancel (mDocument); else { emit close (mDocument); deleteLater(); } } CSVDoc::Loader::Loader() {} CSVDoc::Loader::~Loader() { for (std::map::iterator iter (mDocuments.begin()); iter!=mDocuments.end(); ++iter) delete iter->second; } void CSVDoc::Loader::add (CSMDoc::Document *document) { LoadingDocument *loading = new LoadingDocument (document); mDocuments.insert (std::make_pair (document, loading)); connect (loading, SIGNAL (cancel (CSMDoc::Document *)), this, SIGNAL (cancel (CSMDoc::Document *))); connect (loading, SIGNAL (close (CSMDoc::Document *)), this, SIGNAL (close (CSMDoc::Document *))); } void CSVDoc::Loader::loadingStopped (CSMDoc::Document *document, bool completed, const std::string& error) { std::map::iterator iter = mDocuments.begin(); for (; iter!=mDocuments.end(); ++iter) if (iter->first==document) break; if (iter==mDocuments.end()) return; if (completed || error.empty()) { delete iter->second; mDocuments.erase (iter); } else { iter->second->abort (error); // Leave the window open for now (wait for the user to close it) mDocuments.erase (iter); } } void CSVDoc::Loader::nextStage (CSMDoc::Document *document, const std::string& name, int totalRecords) { std::map::iterator iter = mDocuments.find (document); if (iter!=mDocuments.end()) iter->second->nextStage (name, totalRecords); } void CSVDoc::Loader::nextRecord (CSMDoc::Document *document, int records) { std::map::iterator iter = mDocuments.find (document); if (iter!=mDocuments.end()) iter->second->nextRecord (records); } void CSVDoc::Loader::loadMessage (CSMDoc::Document *document, const std::string& message) { std::map::iterator iter = mDocuments.find (document); if (iter!=mDocuments.end()) iter->second->addMessage (message); } ================================================ FILE: apps/opencs/view/doc/loader.hpp ================================================ #ifndef CSV_DOC_LOADER_H #define CSV_DOC_LOADER_H #include #include #include #include class QLabel; class QProgressBar; class QDialogButtonBox; class QListWidget; class QVBoxLayout; namespace CSMDoc { class Document; } namespace CSVDoc { class LoadingDocument : public QWidget { Q_OBJECT CSMDoc::Document *mDocument; QLabel *mFile; QLabel *mRecords; QProgressBar *mFileProgress; QProgressBar *mRecordProgress; bool mAborted; QDialogButtonBox *mButtons; QLabel *mError; QListWidget *mMessages; QVBoxLayout *mLayout; int mTotalRecords; private: void closeEvent (QCloseEvent *event) override; public: LoadingDocument (CSMDoc::Document *document); void nextStage (const std::string& name, int totalRecords); void nextRecord (int records); void abort (const std::string& error); void addMessage (const std::string& message); private slots: void cancel(); signals: void cancel (CSMDoc::Document *document); ///< Stop loading process. void close (CSMDoc::Document *document); ///< Close stopped loading process. }; class Loader : public QObject { Q_OBJECT std::map mDocuments; public: Loader(); virtual ~Loader(); signals: void cancel (CSMDoc::Document *document); void close (CSMDoc::Document *document); public slots: void add (CSMDoc::Document *document); void loadingStopped (CSMDoc::Document *document, bool completed, const std::string& error); void nextStage (CSMDoc::Document *document, const std::string& name, int totalRecords); void nextRecord (CSMDoc::Document *document, int records); void loadMessage (CSMDoc::Document *document, const std::string& message); }; } #endif ================================================ FILE: apps/opencs/view/doc/newgame.cpp ================================================ #include "newgame.hpp" #include #include #include #include #include #include #include "filewidget.hpp" #include "adjusterwidget.hpp" CSVDoc::NewGameDialogue::NewGameDialogue() { setWindowTitle ("Create New Game"); QVBoxLayout *layout = new QVBoxLayout (this); mFileWidget = new FileWidget (this); mFileWidget->setType (false); layout->addWidget (mFileWidget, 1); mAdjusterWidget = new AdjusterWidget (this); layout->addWidget (mAdjusterWidget, 1); QDialogButtonBox *buttons = new QDialogButtonBox (this); mCreate = new QPushButton ("Create", this); mCreate->setDefault (true); mCreate->setEnabled (false); buttons->addButton (mCreate, QDialogButtonBox::AcceptRole); QPushButton *cancel = new QPushButton ("Cancel", this); buttons->addButton (cancel, QDialogButtonBox::RejectRole); layout->addWidget (buttons); setLayout (layout); connect (mAdjusterWidget, SIGNAL (stateChanged (bool)), this, SLOT (stateChanged (bool))); connect (mCreate, SIGNAL (clicked()), this, SLOT (create())); connect (cancel, SIGNAL (clicked()), this, SLOT (reject())); connect (mFileWidget, SIGNAL (nameChanged (const QString&, bool)), mAdjusterWidget, SLOT (setName (const QString&, bool))); QRect scr = QGuiApplication::primaryScreen()->geometry(); QRect rect = geometry(); move (scr.center().x() - rect.center().x(), scr.center().y() - rect.center().y()); } void CSVDoc::NewGameDialogue::setLocalData (const boost::filesystem::path& localData) { mAdjusterWidget->setLocalData (localData); } void CSVDoc::NewGameDialogue::stateChanged (bool valid) { mCreate->setEnabled (valid); } void CSVDoc::NewGameDialogue::create() { emit createRequest (mAdjusterWidget->getPath()); } void CSVDoc::NewGameDialogue::reject() { emit cancelCreateGame (); QDialog::reject(); } ================================================ FILE: apps/opencs/view/doc/newgame.hpp ================================================ #ifndef CSV_DOC_NEWGAME_H #define CSV_DOC_NEWGAME_H #include #include #include #ifndef CS_QT_BOOST_FILESYSTEM_PATH_DECLARED #define CS_QT_BOOST_FILESYSTEM_PATH_DECLARED Q_DECLARE_METATYPE (boost::filesystem::path) #endif class QPushButton; namespace CSVDoc { class FileWidget; class AdjusterWidget; class NewGameDialogue : public QDialog { Q_OBJECT QPushButton *mCreate; FileWidget *mFileWidget; AdjusterWidget *mAdjusterWidget; public: NewGameDialogue(); void setLocalData (const boost::filesystem::path& localData); signals: void createRequest (const boost::filesystem::path& file); void cancelCreateGame (); private slots: void stateChanged (bool valid); void create(); void reject() override; }; } #endif ================================================ FILE: apps/opencs/view/doc/operation.cpp ================================================ #include "operation.hpp" #include #include #include #include #include "../../model/doc/document.hpp" void CSVDoc::Operation::updateLabel (int threads) { if (threads==-1 || ((threads==0)!=mStalling)) { std::string name ("unknown operation"); switch (mType) { case CSMDoc::State_Saving: name = "saving"; break; case CSMDoc::State_Verifying: name = "verifying"; break; case CSMDoc::State_Searching: name = "searching"; break; case CSMDoc::State_Merging: name = "merging"; break; } std::ostringstream stream; if ((mStalling = (threads<=0))) { stream << name << " (waiting for a free worker thread)"; } else { stream << name << " (%p%)"; } mProgressBar->setFormat (stream.str().c_str()); } } CSVDoc::Operation::Operation (int type, QWidget* parent) : mType (type), mStalling (false) { /// \todo Add a cancel button or a pop up menu with a cancel item initWidgets(); setBarColor( type); updateLabel(); /// \todo assign different progress bar colours to allow the user to distinguish easily between operation types } CSVDoc::Operation::~Operation() { delete mLayout; delete mProgressBar; delete mAbortButton; } void CSVDoc::Operation::initWidgets() { mProgressBar = new QProgressBar (); mAbortButton = new QPushButton("Abort"); mLayout = new QHBoxLayout(); mLayout->addWidget (mProgressBar); mLayout->addWidget (mAbortButton); connect (mAbortButton, SIGNAL (clicked()), this, SLOT (abortOperation())); } void CSVDoc::Operation::setProgress (int current, int max, int threads) { updateLabel (threads); mProgressBar->setRange (0, max); mProgressBar->setValue (current); } int CSVDoc::Operation::getType() const { return mType; } void CSVDoc::Operation::setBarColor (int type) { QString style ="QProgressBar {" "text-align: center;" "}" "QProgressBar::chunk {" "background: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 %1, stop:.50 %2 stop: .51 %3 stop:1 %4);" "text-align: center;" "margin: 2px 1px 1p 2px;" "}"; QString topColor = "#F2F6F8"; QString bottomColor = "#E0EFF9"; QString midTopColor = "#D8E1E7"; QString midBottomColor = "#B5C6D0"; // default gray gloss // colors inspired by samples from: // http://www.colorzilla.com/gradient-editor/ switch (type) { case CSMDoc::State_Saving: topColor = "#FECCB1"; midTopColor = "#F17432"; midBottomColor = "#EA5507"; bottomColor = "#FB955E"; // red gloss #2 break; case CSMDoc::State_Searching: topColor = "#EBF1F6"; midTopColor = "#ABD3EE"; midBottomColor = "#89C3EB"; bottomColor = "#D5EBFB"; //blue gloss #4 break; case CSMDoc::State_Verifying: topColor = "#BFD255"; midTopColor = "#8EB92A"; midBottomColor = "#72AA00"; bottomColor = "#9ECB2D"; //green gloss break; case CSMDoc::State_Merging: topColor = "#F3E2C7"; midTopColor = "#C19E67"; midBottomColor = "#B68D4C"; bottomColor = "#E9D4B3"; //l Brown 3D break; default: topColor = "#F2F6F8"; bottomColor = "#E0EFF9"; midTopColor = "#D8E1E7"; midBottomColor = "#B5C6D0"; // gray gloss for undefined ops } mProgressBar->setStyleSheet(style.arg(topColor).arg(midTopColor).arg(midBottomColor).arg(bottomColor)); } QHBoxLayout *CSVDoc::Operation::getLayout() const { return mLayout; } void CSVDoc::Operation::abortOperation() { emit abortOperation (mType); } ================================================ FILE: apps/opencs/view/doc/operation.hpp ================================================ #ifndef CSV_DOC_OPERATION_H #define CSV_DOC_OPERATION_H #include class QHBoxLayout; class QPushButton; class QProgressBar; namespace CSVDoc { class Operation : public QObject { Q_OBJECT int mType; bool mStalling; QProgressBar *mProgressBar; QPushButton *mAbortButton; QHBoxLayout *mLayout; // not implemented Operation (const Operation&); Operation& operator= (const Operation&); void updateLabel (int threads = -1); public: Operation (int type, QWidget *parent); ~Operation(); void setProgress (int current, int max, int threads); int getType() const; QHBoxLayout *getLayout() const; private: void setBarColor (int type); void initWidgets(); signals: void abortOperation (int type); private slots: void abortOperation(); }; } #endif ================================================ FILE: apps/opencs/view/doc/operations.cpp ================================================ #include "operations.hpp" #include #include #include "operation.hpp" CSVDoc::Operations::Operations() { /// \todo make widget height fixed (exactly the height required to display all operations) setFeatures (QDockWidget::NoDockWidgetFeatures); QWidget *widgetContainer = new QWidget (this); mLayout = new QVBoxLayout; widgetContainer->setLayout (mLayout); setWidget (widgetContainer); setVisible (false); setFixedHeight (widgetContainer->height()); setTitleBarWidget (new QWidget (this)); } void CSVDoc::Operations::setProgress (int current, int max, int type, int threads) { for (std::vector::iterator iter (mOperations.begin()); iter!=mOperations.end(); ++iter) if ((*iter)->getType()==type) { (*iter)->setProgress (current, max, threads); return; } int oldCount = static_cast(mOperations.size()); int newCount = oldCount + 1; Operation *operation = new Operation (type, this); connect (operation, SIGNAL (abortOperation (int)), this, SIGNAL (abortOperation (int))); mLayout->addLayout (operation->getLayout()); mOperations.push_back (operation); operation->setProgress (current, max, threads); if ( oldCount > 0) setFixedHeight (height()/oldCount * newCount); setVisible (true); } void CSVDoc::Operations::quitOperation (int type) { for (std::vector::iterator iter (mOperations.begin()); iter!=mOperations.end(); ++iter) if ((*iter)->getType()==type) { int oldCount = static_cast(mOperations.size()); int newCount = oldCount - 1; mLayout->removeItem ((*iter)->getLayout()); (*iter)->deleteLater(); mOperations.erase (iter); if (oldCount > 1) setFixedHeight (height() / oldCount * newCount); else setVisible (false); break; } } ================================================ FILE: apps/opencs/view/doc/operations.hpp ================================================ #ifndef CSV_DOC_OPERATIONS_H #define CSV_DOC_OPERATIONS_H #include #include class QVBoxLayout; namespace CSVDoc { class Operation; class Operations : public QDockWidget { Q_OBJECT QVBoxLayout *mLayout; std::vector mOperations; // not implemented Operations (const Operations&); Operations& operator= (const Operations&); public: Operations(); void setProgress (int current, int max, int type, int threads); ///< Implicitly starts the operation, if it is not running already. void quitOperation (int type); ///< Calling this function for an operation that is not running is a no-op. signals: void abortOperation (int type); }; } #endif ================================================ FILE: apps/opencs/view/doc/runlogsubview.cpp ================================================ #include "runlogsubview.hpp" #include CSVDoc::RunLogSubView::RunLogSubView (const CSMWorld::UniversalId& id, CSMDoc::Document& document) : SubView (id) { QTextEdit *edit = new QTextEdit (this); edit->setDocument (document.getRunLog()); edit->setReadOnly (true); setWidget (edit); } void CSVDoc::RunLogSubView::setEditLock (bool locked) { // ignored since this SubView does not have editing } ================================================ FILE: apps/opencs/view/doc/runlogsubview.hpp ================================================ #ifndef CSV_DOC_RUNLOGSUBVIEW_H #define CSV_DOC_RUNLOGSUBVIEW_H #include "subview.hpp" namespace CSVDoc { class RunLogSubView : public SubView { Q_OBJECT public: RunLogSubView (const CSMWorld::UniversalId& id, CSMDoc::Document& document); void setEditLock (bool locked) override; }; } #endif ================================================ FILE: apps/opencs/view/doc/sizehint.cpp ================================================ #include "sizehint.hpp" CSVDoc::SizeHintWidget::SizeHintWidget(QWidget *parent) : QWidget(parent) {} CSVDoc::SizeHintWidget::~SizeHintWidget() {} QSize CSVDoc::SizeHintWidget::sizeHint() const { return mSize; } void CSVDoc::SizeHintWidget::setSizeHint(const QSize &size) { mSize = size; } ================================================ FILE: apps/opencs/view/doc/sizehint.hpp ================================================ #ifndef CSV_DOC_SIZEHINT_H #define CSV_DOC_SIZEHINT_H #include #include namespace CSVDoc { class SizeHintWidget : public QWidget { QSize mSize; public: SizeHintWidget(QWidget *parent = nullptr); ~SizeHintWidget(); QSize sizeHint() const override; void setSizeHint(const QSize &size); }; } #endif // CSV_DOC_SIZEHINT_H ================================================ FILE: apps/opencs/view/doc/startup.cpp ================================================ #include "startup.hpp" #include #include #include #include #include #include #include #include #include #include QPushButton *CSVDoc::StartupDialogue::addButton (const QString& label, const QIcon& icon) { int column = mColumn--; QPushButton *button = new QPushButton (this); button->setIcon (QIcon (icon)); button->setSizePolicy (QSizePolicy (QSizePolicy::Preferred, QSizePolicy::Preferred)); mLayout->addWidget (button, 0, column); mLayout->addWidget (new QLabel (label, this), 1, column, Qt::AlignCenter); int width = mLayout->itemAtPosition (1, column)->widget()->sizeHint().width(); if (width>mWidth) mWidth = width; return button; } QWidget *CSVDoc::StartupDialogue::createButtons() { QWidget *widget = new QWidget (this); mLayout = new QGridLayout (widget); /// \todo add icons QPushButton *loadDocument = addButton ("Edit A Content File", QIcon (":startup/edit-content")); connect (loadDocument, SIGNAL (clicked()), this, SIGNAL (loadDocument())); QPushButton *createAddon = addButton ("Create A New Addon", QIcon (":startup/create-addon")); connect (createAddon, SIGNAL (clicked()), this, SIGNAL (createAddon())); QPushButton *createGame = addButton ("Create A New Game", QIcon (":startup/create-game")); connect (createGame, SIGNAL (clicked()), this, SIGNAL (createGame())); for (int i=0; i<3; ++i) mLayout->setColumnMinimumWidth (i, mWidth); mLayout->setRowMinimumHeight (0, mWidth); mLayout->setSizeConstraint (QLayout::SetMinimumSize); mLayout->setHorizontalSpacing (32); mLayout->setContentsMargins (16, 16, 16, 8); loadDocument->setIconSize (QSize (mWidth, mWidth)); createGame->setIconSize (QSize (mWidth, mWidth)); createAddon->setIconSize (QSize (mWidth, mWidth)); widget->setLayout (mLayout); return widget; } QWidget *CSVDoc::StartupDialogue::createTools() { QWidget *widget = new QWidget (this); QHBoxLayout *layout = new QHBoxLayout (widget); layout->setDirection (QBoxLayout::RightToLeft); layout->setContentsMargins (4, 4, 4, 4); QPushButton *config = new QPushButton (widget); config->setSizePolicy (QSizePolicy (QSizePolicy::Fixed, QSizePolicy::Fixed)); config->setIcon (QIcon (":startup/configure")); config->setToolTip ("Open user settings"); layout->addWidget (config); layout->addWidget (new QWidget, 1); // dummy widget; stops buttons from taking all the space widget->setLayout (layout); connect (config, SIGNAL (clicked()), this, SIGNAL (editConfig())); return widget; } CSVDoc::StartupDialogue::StartupDialogue() : mWidth (0), mColumn (2) { setWindowTitle ("OpenMW-CS"); QVBoxLayout *layout = new QVBoxLayout (this); layout->setContentsMargins (0, 0, 0, 0); layout->addWidget (createButtons()); layout->addWidget (createTools()); /// \todo remove this label once we are feature complete and convinced that this thing is /// working properly. QLabel *warning = new QLabel ("WARNING: OpenMW-CS is in alpha stage.

The editor is not feature complete and not sufficiently tested.
In theory your data should be safe. But we strongly advise to make backups regularly if you are working with live data.
"); QFont font; font.setPointSize (12); font.setBold (true); warning->setFont (font); warning->setWordWrap (true); layout->addWidget (warning, 1); setLayout (layout); QRect scr = QGuiApplication::primaryScreen()->geometry(); QRect rect = geometry(); move (scr.center().x() - rect.center().x(), scr.center().y() - rect.center().y()); } ================================================ FILE: apps/opencs/view/doc/startup.hpp ================================================ #ifndef CSV_DOC_STARTUP_H #define CSV_DOC_STARTUP_H #include class QGridLayout; class QString; class QPushButton; class QWidget; class QIcon; namespace CSVDoc { class StartupDialogue : public QWidget { Q_OBJECT private: int mWidth; int mColumn; QGridLayout *mLayout; QPushButton *addButton (const QString& label, const QIcon& icon); QWidget *createButtons(); QWidget *createTools(); public: StartupDialogue(); signals: void createGame(); void createAddon(); void loadDocument(); void editConfig(); }; } #endif ================================================ FILE: apps/opencs/view/doc/subview.cpp ================================================ #include "subview.hpp" #include "view.hpp" #include #include #include bool CSVDoc::SubView::event (QEvent *event) { if (event->type()==QEvent::ShortcutOverride) { QKeyEvent *keyEvent = static_cast (event); if (keyEvent->key()==Qt::Key_W && keyEvent->modifiers()==(Qt::ShiftModifier | Qt::ControlModifier)) emit closeRequest(); return true; } return QDockWidget::event (event); } CSVDoc::SubView::SubView (const CSMWorld::UniversalId& id) : mUniversalId (id) { /// \todo add a button to the title bar that clones this sub view setWindowTitle (QString::fromUtf8 (mUniversalId.toString().c_str())); setAttribute(Qt::WA_DeleteOnClose); } CSMWorld::UniversalId CSVDoc::SubView::getUniversalId() const { return mUniversalId; } void CSVDoc::SubView::setStatusBar (bool show) {} void CSVDoc::SubView::useHint (const std::string& hint) {} void CSVDoc::SubView::setUniversalId (const CSMWorld::UniversalId& id) { mUniversalId = id; setWindowTitle (QString::fromUtf8(mUniversalId.toString().c_str())); emit universalIdChanged (mUniversalId); } void CSVDoc::SubView::closeEvent (QCloseEvent *event) { emit updateSubViewIndices (this); } std::string CSVDoc::SubView::getTitle() const { return mUniversalId.toString(); } void CSVDoc::SubView::closeRequest() { emit closeRequest (this); } ================================================ FILE: apps/opencs/view/doc/subview.hpp ================================================ #ifndef CSV_DOC_SUBVIEW_H #define CSV_DOC_SUBVIEW_H #include "../../model/doc/document.hpp" #include "../../model/world/universalid.hpp" #include "subviewfactory.hpp" #include class QUndoStack; namespace CSMWorld { class Data; } namespace CSVDoc { class View; class SubView : public QDockWidget { Q_OBJECT CSMWorld::UniversalId mUniversalId; // not implemented SubView (const SubView&); SubView& operator= (SubView&); protected: void setUniversalId(const CSMWorld::UniversalId& id); bool event (QEvent *event) override; public: SubView (const CSMWorld::UniversalId& id); CSMWorld::UniversalId getUniversalId() const; virtual void setEditLock (bool locked) = 0; virtual void setStatusBar (bool show); ///< Default implementation: ignored virtual void useHint (const std::string& hint); ///< Default implementation: ignored virtual std::string getTitle() const; private: void closeEvent (QCloseEvent *event) override; signals: void focusId (const CSMWorld::UniversalId& universalId, const std::string& hint); void closeRequest (SubView *subView); void updateTitle(); void updateSubViewIndices (SubView *view = nullptr); void universalIdChanged (const CSMWorld::UniversalId& universalId); protected slots: void closeRequest(); }; } #endif ================================================ FILE: apps/opencs/view/doc/subviewfactory.cpp ================================================ #include "subviewfactory.hpp" #include #include CSVDoc::SubViewFactoryBase::SubViewFactoryBase() {} CSVDoc::SubViewFactoryBase::~SubViewFactoryBase() {} CSVDoc::SubViewFactoryManager::SubViewFactoryManager() {} CSVDoc::SubViewFactoryManager::~SubViewFactoryManager() { for (std::map::iterator iter (mSubViewFactories.begin()); iter!=mSubViewFactories.end(); ++iter) delete iter->second; } void CSVDoc::SubViewFactoryManager::add (const CSMWorld::UniversalId::Type& id, SubViewFactoryBase *factory) { assert (mSubViewFactories.find (id)==mSubViewFactories.end()); mSubViewFactories.insert (std::make_pair (id, factory)); } CSVDoc::SubView *CSVDoc::SubViewFactoryManager::makeSubView (const CSMWorld::UniversalId& id, CSMDoc::Document& document) { std::map::iterator iter = mSubViewFactories.find (id.getType()); if (iter==mSubViewFactories.end()) throw std::runtime_error ("Failed to create a sub view for: " + id.toString()); return iter->second->makeSubView (id, document); } ================================================ FILE: apps/opencs/view/doc/subviewfactory.hpp ================================================ #ifndef CSV_DOC_SUBVIEWFACTORY_H #define CSV_DOC_SUBVIEWFACTORY_H #include #include "../../model/world/universalid.hpp" namespace CSMDoc { class Document; } namespace CSVDoc { class SubView; class SubViewFactoryBase { // not implemented SubViewFactoryBase (const SubViewFactoryBase&); SubViewFactoryBase& operator= (const SubViewFactoryBase&); public: SubViewFactoryBase(); virtual ~SubViewFactoryBase(); virtual SubView *makeSubView (const CSMWorld::UniversalId& id, CSMDoc::Document& document) = 0; ///< The ownership of the returned sub view is not transferred. }; class SubViewFactoryManager { std::map mSubViewFactories; // not implemented SubViewFactoryManager (const SubViewFactoryManager&); SubViewFactoryManager& operator= (const SubViewFactoryManager&); public: SubViewFactoryManager(); ~SubViewFactoryManager(); void add (const CSMWorld::UniversalId::Type& id, SubViewFactoryBase *factory); ///< The ownership of \a factory is transferred to this. SubView *makeSubView (const CSMWorld::UniversalId& id, CSMDoc::Document& document); ///< The ownership of the returned sub view is not transferred. }; } #endif ================================================ FILE: apps/opencs/view/doc/subviewfactoryimp.hpp ================================================ #ifndef CSV_DOC_SUBVIEWFACTORYIMP_H #define CSV_DOC_SUBVIEWFACTORYIMP_H #include "../../model/doc/document.hpp" #include "subviewfactory.hpp" namespace CSVDoc { template class SubViewFactory : public SubViewFactoryBase { public: CSVDoc::SubView *makeSubView (const CSMWorld::UniversalId& id, CSMDoc::Document& document) override; }; template CSVDoc::SubView *SubViewFactory::makeSubView (const CSMWorld::UniversalId& id, CSMDoc::Document& document) { return new SubViewT (id, document); } template class SubViewFactoryWithCreator : public SubViewFactoryBase { bool mSorting; public: SubViewFactoryWithCreator (bool sorting = true); CSVDoc::SubView *makeSubView (const CSMWorld::UniversalId& id, CSMDoc::Document& document) override; }; template SubViewFactoryWithCreator::SubViewFactoryWithCreator (bool sorting) : mSorting (sorting) {} template CSVDoc::SubView *SubViewFactoryWithCreator::makeSubView ( const CSMWorld::UniversalId& id, CSMDoc::Document& document) { return new SubViewT (id, document, CreatorFactoryT(), mSorting); } } #endif ================================================ FILE: apps/opencs/view/doc/view.cpp ================================================ #include "view.hpp" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "../../model/doc/document.hpp" #include "../../model/prefs/state.hpp" #include "../../model/prefs/shortcut.hpp" #include "../../model/world/idtable.hpp" #include "../world/subviews.hpp" #include "../world/scenesubview.hpp" #include "../world/tablesubview.hpp" #include "../tools/subviews.hpp" #include #include #include "viewmanager.hpp" #include "operations.hpp" #include "subview.hpp" #include "globaldebugprofilemenu.hpp" #include "runlogsubview.hpp" #include "subviewfactoryimp.hpp" void CSVDoc::View::closeEvent (QCloseEvent *event) { if (!mViewManager.closeRequest (this)) event->ignore(); else { // closeRequest() returns true if last document mViewManager.removeDocAndView(mDocument); } } void CSVDoc::View::setupFileMenu() { QMenu *file = menuBar()->addMenu (tr ("File")); QAction* newGame = createMenuEntry("New Game", ":./menu-new-game.png", file, "document-file-newgame"); connect (newGame, SIGNAL (triggered()), this, SIGNAL (newGameRequest())); QAction* newAddon = createMenuEntry("New Addon", ":./menu-new-addon.png", file, "document-file-newaddon"); connect (newAddon, SIGNAL (triggered()), this, SIGNAL (newAddonRequest())); QAction* open = createMenuEntry("Open", ":./menu-open.png", file, "document-file-open"); connect (open, SIGNAL (triggered()), this, SIGNAL (loadDocumentRequest())); QAction* save = createMenuEntry("Save", ":./menu-save.png", file, "document-file-save"); connect (save, SIGNAL (triggered()), this, SLOT (save())); mSave = save; QAction* verify = createMenuEntry("Verify", ":./menu-verify.png", file, "document-file-verify"); connect (verify, SIGNAL (triggered()), this, SLOT (verify())); mVerify = verify; QAction* merge = createMenuEntry("Merge", ":./menu-merge.png", file, "document-file-merge"); connect (merge, SIGNAL (triggered()), this, SLOT (merge())); mMerge = merge; QAction* loadErrors = createMenuEntry("Error Log", ":./error-log.png", file, "document-file-errorlog"); connect (loadErrors, SIGNAL (triggered()), this, SLOT (loadErrorLog())); QAction* meta = createMenuEntry(CSMWorld::UniversalId::Type_MetaDatas, file, "document-file-metadata"); connect (meta, SIGNAL (triggered()), this, SLOT (addMetaDataSubView())); QAction* close = createMenuEntry("Close", ":./menu-close.png", file, "document-file-close"); connect (close, SIGNAL (triggered()), this, SLOT (close())); QAction* exit = createMenuEntry("Exit", ":./menu-exit.png", file, "document-file-exit"); connect (exit, SIGNAL (triggered()), this, SLOT (exit())); connect (this, SIGNAL(exitApplicationRequest(CSVDoc::View *)), &mViewManager, SLOT(exitApplication(CSVDoc::View *))); } namespace { void updateUndoRedoAction(QAction *action, const std::string &settingsKey) { QKeySequence seq; CSMPrefs::State::get().getShortcutManager().getSequence(settingsKey, seq); action->setShortcut(seq); } } void CSVDoc::View::undoActionChanged() { updateUndoRedoAction(mUndo, "document-edit-undo"); } void CSVDoc::View::redoActionChanged() { updateUndoRedoAction(mRedo, "document-edit-redo"); } void CSVDoc::View::setupEditMenu() { QMenu *edit = menuBar()->addMenu (tr ("Edit")); mUndo = mDocument->getUndoStack().createUndoAction (this, tr("Undo")); setupShortcut("document-edit-undo", mUndo); connect(mUndo, SIGNAL (changed ()), this, SLOT (undoActionChanged ())); mUndo->setIcon(QIcon(QString::fromStdString(":./menu-undo.png"))); edit->addAction (mUndo); mRedo = mDocument->getUndoStack().createRedoAction (this, tr("Redo")); connect(mRedo, SIGNAL (changed ()), this, SLOT (redoActionChanged ())); setupShortcut("document-edit-redo", mRedo); mRedo->setIcon(QIcon(QString::fromStdString(":./menu-redo.png"))); edit->addAction (mRedo); QAction* userSettings = createMenuEntry("Preferences", ":./menu-preferences.png", edit, "document-edit-preferences"); connect (userSettings, SIGNAL (triggered()), this, SIGNAL (editSettingsRequest())); QAction* search = createMenuEntry(CSMWorld::UniversalId::Type_Search, edit, "document-edit-search"); connect (search, SIGNAL (triggered()), this, SLOT (addSearchSubView())); } void CSVDoc::View::setupViewMenu() { QMenu *view = menuBar()->addMenu (tr ("View")); QAction *newWindow = createMenuEntry("New View", ":./menu-new-window.png", view, "document-view-newview"); connect (newWindow, SIGNAL (triggered()), this, SLOT (newView())); mShowStatusBar = createMenuEntry("Toggle Status Bar", ":./menu-status-bar.png", view, "document-view-statusbar"); connect (mShowStatusBar, SIGNAL (toggled (bool)), this, SLOT (toggleShowStatusBar (bool))); mShowStatusBar->setCheckable (true); mShowStatusBar->setChecked (CSMPrefs::get()["Windows"]["show-statusbar"].isTrue()); view->addAction (mShowStatusBar); QAction *filters = createMenuEntry(CSMWorld::UniversalId::Type_Filters, view, "document-mechanics-filters"); connect (filters, SIGNAL (triggered()), this, SLOT (addFiltersSubView())); } void CSVDoc::View::setupWorldMenu() { QMenu *world = menuBar()->addMenu (tr ("World")); QAction* regions = createMenuEntry(CSMWorld::UniversalId::Type_Regions, world, "document-world-regions"); connect (regions, SIGNAL (triggered()), this, SLOT (addRegionsSubView())); QAction* cells = createMenuEntry(CSMWorld::UniversalId::Type_Cells, world, "document-world-cells"); connect (cells, SIGNAL (triggered()), this, SLOT (addCellsSubView())); QAction* referenceables = createMenuEntry(CSMWorld::UniversalId::Type_Referenceables, world, "document-world-referencables"); connect (referenceables, SIGNAL (triggered()), this, SLOT (addReferenceablesSubView())); QAction* references = createMenuEntry(CSMWorld::UniversalId::Type_References, world, "document-world-references"); connect (references, SIGNAL (triggered()), this, SLOT (addReferencesSubView())); QAction *lands = createMenuEntry(CSMWorld::UniversalId::Type_Lands, world, "document-world-lands"); connect (lands, SIGNAL (triggered()), this, SLOT (addLandsSubView())); QAction *landTextures = createMenuEntry(CSMWorld::UniversalId::Type_LandTextures, world, "document-world-landtextures"); connect (landTextures, SIGNAL (triggered()), this, SLOT (addLandTexturesSubView())); QAction *grid = createMenuEntry(CSMWorld::UniversalId::Type_Pathgrids, world, "document-world-pathgrid"); connect (grid, SIGNAL (triggered()), this, SLOT (addPathgridSubView())); world->addSeparator(); // items that don't represent single record lists follow here QAction *regionMap = createMenuEntry(CSMWorld::UniversalId::Type_RegionMap, world, "document-world-regionmap"); connect (regionMap, SIGNAL (triggered()), this, SLOT (addRegionMapSubView())); } void CSVDoc::View::setupMechanicsMenu() { QMenu *mechanics = menuBar()->addMenu (tr ("Mechanics")); QAction* globals = createMenuEntry(CSMWorld::UniversalId::Type_Globals, mechanics, "document-mechanics-globals"); connect (globals, SIGNAL (triggered()), this, SLOT (addGlobalsSubView())); QAction* gmsts = createMenuEntry(CSMWorld::UniversalId::Type_Gmsts, mechanics, "document-mechanics-gamesettings"); connect (gmsts, SIGNAL (triggered()), this, SLOT (addGmstsSubView())); QAction* scripts = createMenuEntry(CSMWorld::UniversalId::Type_Scripts, mechanics, "document-mechanics-scripts"); connect (scripts, SIGNAL (triggered()), this, SLOT (addScriptsSubView())); QAction* spells = createMenuEntry(CSMWorld::UniversalId::Type_Spells, mechanics, "document-mechanics-spells"); connect (spells, SIGNAL (triggered()), this, SLOT (addSpellsSubView())); QAction* enchantments = createMenuEntry(CSMWorld::UniversalId::Type_Enchantments, mechanics, "document-mechanics-enchantments"); connect (enchantments, SIGNAL (triggered()), this, SLOT (addEnchantmentsSubView())); QAction* magicEffects = createMenuEntry(CSMWorld::UniversalId::Type_MagicEffects, mechanics, "document-mechanics-magiceffects"); connect (magicEffects, SIGNAL (triggered()), this, SLOT (addMagicEffectsSubView())); QAction* startScripts = createMenuEntry(CSMWorld::UniversalId::Type_StartScripts, mechanics, "document-mechanics-startscripts"); connect (startScripts, SIGNAL (triggered()), this, SLOT (addStartScriptsSubView())); } void CSVDoc::View::setupCharacterMenu() { QMenu *characters = menuBar()->addMenu (tr ("Characters")); QAction* skills = createMenuEntry(CSMWorld::UniversalId::Type_Skills, characters, "document-character-skills"); connect (skills, SIGNAL (triggered()), this, SLOT (addSkillsSubView())); QAction* classes = createMenuEntry(CSMWorld::UniversalId::Type_Classes, characters, "document-character-classes"); connect (classes, SIGNAL (triggered()), this, SLOT (addClassesSubView())); QAction* factions = createMenuEntry(CSMWorld::UniversalId::Type_Faction, characters, "document-character-factions"); connect (factions, SIGNAL (triggered()), this, SLOT (addFactionsSubView())); QAction* races = createMenuEntry(CSMWorld::UniversalId::Type_Races, characters, "document-character-races"); connect (races, SIGNAL (triggered()), this, SLOT (addRacesSubView())); QAction* birthsigns = createMenuEntry(CSMWorld::UniversalId::Type_Birthsigns, characters, "document-character-birthsigns"); connect (birthsigns, SIGNAL (triggered()), this, SLOT (addBirthsignsSubView())); QAction* topics = createMenuEntry(CSMWorld::UniversalId::Type_Topics, characters, "document-character-topics"); connect (topics, SIGNAL (triggered()), this, SLOT (addTopicsSubView())); QAction* journals = createMenuEntry(CSMWorld::UniversalId::Type_Journals, characters, "document-character-journals"); connect (journals, SIGNAL (triggered()), this, SLOT (addJournalsSubView())); QAction* topicInfos = createMenuEntry(CSMWorld::UniversalId::Type_TopicInfos, characters, "document-character-topicinfos"); connect (topicInfos, SIGNAL (triggered()), this, SLOT (addTopicInfosSubView())); QAction* journalInfos = createMenuEntry(CSMWorld::UniversalId::Type_JournalInfos, characters, "document-character-journalinfos"); connect (journalInfos, SIGNAL (triggered()), this, SLOT (addJournalInfosSubView())); QAction* bodyParts = createMenuEntry(CSMWorld::UniversalId::Type_BodyParts, characters, "document-character-bodyparts"); connect (bodyParts, SIGNAL (triggered()), this, SLOT (addBodyPartsSubView())); } void CSVDoc::View::setupAssetsMenu() { QMenu *assets = menuBar()->addMenu (tr ("Assets")); QAction* reload = createMenuEntry("Reload", ":./menu-reload.png", assets, "document-assets-reload"); connect (reload, SIGNAL (triggered()), &mDocument->getData(), SLOT (assetsChanged())); assets->addSeparator(); QAction* sounds = createMenuEntry(CSMWorld::UniversalId::Type_Sounds, assets, "document-assets-sounds"); connect (sounds, SIGNAL (triggered()), this, SLOT (addSoundsSubView())); QAction* soundGens = createMenuEntry(CSMWorld::UniversalId::Type_SoundGens, assets, "document-assets-soundgens"); connect (soundGens, SIGNAL (triggered()), this, SLOT (addSoundGensSubView())); assets->addSeparator(); // resources follow here QAction* meshes = createMenuEntry(CSMWorld::UniversalId::Type_Meshes, assets, "document-assets-meshes"); connect (meshes, SIGNAL (triggered()), this, SLOT (addMeshesSubView())); QAction* icons = createMenuEntry(CSMWorld::UniversalId::Type_Icons, assets, "document-assets-icons"); connect (icons, SIGNAL (triggered()), this, SLOT (addIconsSubView())); QAction* musics = createMenuEntry(CSMWorld::UniversalId::Type_Musics, assets, "document-assets-musics"); connect (musics, SIGNAL (triggered()), this, SLOT (addMusicsSubView())); QAction* soundFiles = createMenuEntry(CSMWorld::UniversalId::Type_SoundsRes, assets, "document-assets-soundres"); connect (soundFiles, SIGNAL (triggered()), this, SLOT (addSoundsResSubView())); QAction* textures = createMenuEntry(CSMWorld::UniversalId::Type_Textures, assets, "document-assets-textures"); connect (textures, SIGNAL (triggered()), this, SLOT (addTexturesSubView())); QAction* videos = createMenuEntry(CSMWorld::UniversalId::Type_Videos, assets, "document-assets-videos"); connect (videos, SIGNAL (triggered()), this, SLOT (addVideosSubView())); } void CSVDoc::View::setupDebugMenu() { QMenu *debug = menuBar()->addMenu (tr ("Debug")); QAction* profiles = createMenuEntry(CSMWorld::UniversalId::Type_DebugProfiles, debug, "document-debug-profiles"); connect (profiles, SIGNAL (triggered()), this, SLOT (addDebugProfilesSubView())); debug->addSeparator(); mGlobalDebugProfileMenu = new GlobalDebugProfileMenu ( &dynamic_cast (*mDocument->getData().getTableModel ( CSMWorld::UniversalId::Type_DebugProfiles)), this); connect (mGlobalDebugProfileMenu, SIGNAL (triggered (const std::string&)), this, SLOT (run (const std::string&))); QAction *runDebug = debug->addMenu (mGlobalDebugProfileMenu); runDebug->setText (tr ("Run OpenMW")); setupShortcut("document-debug-run", runDebug); runDebug->setIcon(QIcon(QString::fromStdString(":./run-openmw.png"))); QAction* stopDebug = createMenuEntry("Stop OpenMW", ":./stop-openmw.png", debug, "document-debug-shutdown"); connect (stopDebug, SIGNAL (triggered()), this, SLOT (stop())); mStopDebug = stopDebug; QAction* runLog = createMenuEntry(CSMWorld::UniversalId::Type_RunLog, debug, "document-debug-runlog"); connect (runLog, SIGNAL (triggered()), this, SLOT (addRunLogSubView())); } void CSVDoc::View::setupHelpMenu() { QMenu *help = menuBar()->addMenu (tr ("Help")); QAction* helpInfo = createMenuEntry("Help", ":/info.png", help, "document-help-help"); connect (helpInfo, SIGNAL (triggered()), this, SLOT (openHelp())); QAction* tutorial = createMenuEntry("Tutorial", ":/info.png", help, "document-help-tutorial"); connect (tutorial, SIGNAL (triggered()), this, SLOT (tutorial())); QAction* about = createMenuEntry("About OpenMW-CS", ":./info.png", help, "document-help-about"); connect (about, SIGNAL (triggered()), this, SLOT (infoAbout())); QAction* aboutQt = createMenuEntry("About Qt", ":./qt.png", help, "document-help-qt"); connect (aboutQt, SIGNAL (triggered()), this, SLOT (infoAboutQt())); } QAction* CSVDoc::View::createMenuEntry(CSMWorld::UniversalId::Type type, QMenu* menu, const char* shortcutName) { const std::string title = CSMWorld::UniversalId (type).getTypeName(); QAction *entry = new QAction(QString::fromStdString(title), this); setupShortcut(shortcutName, entry); const std::string iconName = CSMWorld::UniversalId (type).getIcon(); if (!iconName.empty() && iconName != ":placeholder") entry->setIcon(QIcon(QString::fromStdString(iconName))); menu->addAction (entry); return entry; } QAction* CSVDoc::View::createMenuEntry(const std::string& title, const std::string& iconName, QMenu* menu, const char* shortcutName) { QAction *entry = new QAction(QString::fromStdString(title), this); setupShortcut(shortcutName, entry); if (!iconName.empty() && iconName != ":placeholder") entry->setIcon(QIcon(QString::fromStdString(iconName))); menu->addAction (entry); return entry; } void CSVDoc::View::setupUi() { setupFileMenu(); setupEditMenu(); setupViewMenu(); setupWorldMenu(); setupMechanicsMenu(); setupCharacterMenu(); setupAssetsMenu(); setupDebugMenu(); setupHelpMenu(); } void CSVDoc::View::setupShortcut(const char* name, QAction* action) { CSMPrefs::Shortcut* shortcut = new CSMPrefs::Shortcut(name, this); shortcut->associateAction(action); } void CSVDoc::View::updateTitle() { std::ostringstream stream; stream << mDocument->getSavePath().filename().string(); if (mDocument->getState() & CSMDoc::State_Modified) stream << " *"; if (mViewTotal>1) stream << " [" << (mViewIndex+1) << "/" << mViewTotal << "]"; CSMPrefs::Category& windows = CSMPrefs::State::get()["Windows"]; bool hideTitle = windows["hide-subview"].isTrue() && mSubViews.size()==1 && !mSubViews.at (0)->isFloating(); if (hideTitle) stream << " - " << mSubViews.at (0)->getTitle(); setWindowTitle (QString::fromUtf8(stream.str().c_str())); } void CSVDoc::View::updateSubViewIndices(SubView *view) { CSMPrefs::Category& windows = CSMPrefs::State::get()["Windows"]; if(view && mSubViews.contains(view)) { mSubViews.removeOne(view); // adjust (reduce) the scroll area (even floating), except when it is "Scrollbar Only" if (windows["mainwindow-scrollbar"].toString() == "Grow then Scroll") updateScrollbar(); } bool hideTitle = windows["hide-subview"].isTrue() && mSubViews.size()==1 && !mSubViews.at (0)->isFloating(); updateTitle(); for (SubView *subView : mSubViews) { if (!subView->isFloating()) { if (hideTitle) { subView->setTitleBarWidget (new QWidget (this)); subView->setWindowTitle (QString::fromUtf8 (subView->getTitle().c_str())); } else { delete subView->titleBarWidget(); subView->setTitleBarWidget (nullptr); } } } } void CSVDoc::View::updateActions() { bool editing = !(mDocument->getState() & CSMDoc::State_Locked); bool running = mDocument->getState() & CSMDoc::State_Running; for (std::vector::iterator iter (mEditingActions.begin()); iter!=mEditingActions.end(); ++iter) (*iter)->setEnabled (editing); mUndo->setEnabled (editing && mDocument->getUndoStack().canUndo()); mRedo->setEnabled (editing && mDocument->getUndoStack().canRedo()); mSave->setEnabled (!(mDocument->getState() & CSMDoc::State_Saving) && !running); mVerify->setEnabled (!(mDocument->getState() & CSMDoc::State_Verifying)); mGlobalDebugProfileMenu->updateActions (running); mStopDebug->setEnabled (running); mMerge->setEnabled (mDocument->getContentFiles().size()>1 && !(mDocument->getState() & CSMDoc::State_Merging)); } CSVDoc::View::View (ViewManager& viewManager, CSMDoc::Document *document, int totalViews) : mViewManager (viewManager), mDocument (document), mViewIndex (totalViews-1), mViewTotal (totalViews), mScroll(nullptr), mScrollbarOnly(false) { CSMPrefs::Category& windows = CSMPrefs::State::get()["Windows"]; int width = std::max (windows["default-width"].toInt(), 300); int height = std::max (windows["default-height"].toInt(), 300); resize (width, height); mSubViewWindow.setDockOptions (QMainWindow::AllowNestedDocks); if (windows["mainwindow-scrollbar"].toString() == "Grow Only") { setCentralWidget (&mSubViewWindow); } else { createScrollArea(); } mOperations = new Operations; addDockWidget (Qt::BottomDockWidgetArea, mOperations); setContextMenuPolicy(Qt::NoContextMenu); updateTitle(); setupUi(); updateActions(); CSVWorld::addSubViewFactories (mSubViewFactory); CSVTools::addSubViewFactories (mSubViewFactory); mSubViewFactory.add (CSMWorld::UniversalId::Type_RunLog, new SubViewFactory); connect (mOperations, SIGNAL (abortOperation (int)), this, SLOT (abortOperation (int))); connect (&CSMPrefs::State::get(), SIGNAL (settingChanged (const CSMPrefs::Setting *)), this, SLOT (settingChanged (const CSMPrefs::Setting *))); } CSVDoc::View::~View() { } const CSMDoc::Document *CSVDoc::View::getDocument() const { return mDocument; } CSMDoc::Document *CSVDoc::View::getDocument() { return mDocument; } void CSVDoc::View::setIndex (int viewIndex, int totalViews) { mViewIndex = viewIndex; mViewTotal = totalViews; updateTitle(); } void CSVDoc::View::updateDocumentState() { updateTitle(); updateActions(); static const int operations[] = { CSMDoc::State_Saving, CSMDoc::State_Verifying, CSMDoc::State_Searching, CSMDoc::State_Merging, -1 // end marker }; int state = mDocument->getState() ; for (int i=0; operations[i]!=-1; ++i) if (!(state & operations[i])) mOperations->quitOperation (operations[i]); QList subViews = findChildren(); for (QList::iterator iter (subViews.begin()); iter!=subViews.end(); ++iter) (*iter)->setEditLock (state & CSMDoc::State_Locked); } void CSVDoc::View::updateProgress (int current, int max, int type, int threads) { mOperations->setProgress (current, max, type, threads); } void CSVDoc::View::addSubView (const CSMWorld::UniversalId& id, const std::string& hint) { CSMPrefs::Category& windows = CSMPrefs::State::get()["Windows"]; bool isReferenceable = id.getClass() == CSMWorld::UniversalId::Class_RefRecord; // User setting to reuse sub views (on a per top level view basis) if (windows["reuse"].isTrue()) { for (SubView *sb : mSubViews) { bool isSubViewReferenceable = sb->getUniversalId().getType() == CSMWorld::UniversalId::Type_Referenceable; if((isReferenceable && isSubViewReferenceable && id.getId() == sb->getUniversalId().getId()) || (!isReferenceable && id == sb->getUniversalId())) { sb->setFocus(); if (!hint.empty()) sb->useHint (hint); return; } } } if (mScroll) QObject::connect(mScroll->horizontalScrollBar(), SIGNAL(rangeChanged(int,int)), this, SLOT(moveScrollBarToEnd(int,int))); // User setting for limiting the number of sub views per top level view. // Automatically open a new top level view if this number is exceeded // // If the sub view limit setting is one, the sub view title bar is hidden and the // text in the main title bar is adjusted accordingly if(mSubViews.size() >= windows["max-subviews"].toInt()) // create a new top level view { mViewManager.addView(mDocument, id, hint); return; } SubView *view = nullptr; if(isReferenceable) { view = mSubViewFactory.makeSubView (CSMWorld::UniversalId(CSMWorld::UniversalId::Type_Referenceable, id.getId()), *mDocument); } else { view = mSubViewFactory.makeSubView (id, *mDocument); } assert(view); view->setParent(this); view->setEditLock (mDocument->getState() & CSMDoc::State_Locked); mSubViews.append(view); // only after assert int minWidth = windows["minimum-width"].toInt(); view->setMinimumWidth (minWidth); view->setStatusBar (mShowStatusBar->isChecked()); // Work out how to deal with additional subviews // // Policy for "Grow then Scroll": // // - Increase the horizontal width of the mainwindow until it becomes greater than or equal // to the screen (monitor) width. // - Move the mainwindow position sideways if necessary to fit within the screen. // - Any more additions increases the size of the mSubViewWindow (horizontal scrollbar // should become visible) // - Move the scroll bar to the newly added subview // mScrollbarOnly = windows["mainwindow-scrollbar"].toString() == "Scrollbar Only"; updateWidth(windows["grow-limit"].isTrue(), minWidth); mSubViewWindow.addDockWidget (Qt::TopDockWidgetArea, view); updateSubViewIndices(); connect (view, SIGNAL (focusId (const CSMWorld::UniversalId&, const std::string&)), this, SLOT (addSubView (const CSMWorld::UniversalId&, const std::string&))); connect (view, SIGNAL (closeRequest (SubView *)), this, SLOT (closeRequest (SubView *))); connect (view, SIGNAL (updateTitle()), this, SLOT (updateTitle())); connect (view, SIGNAL (updateSubViewIndices (SubView *)), this, SLOT (updateSubViewIndices (SubView *))); CSVWorld::TableSubView* tableView = dynamic_cast(view); if (tableView) { connect (this, SIGNAL (requestFocus (const std::string&)), tableView, SLOT (requestFocus (const std::string&))); } CSVWorld::SceneSubView* sceneView = dynamic_cast(view); if (sceneView) { connect(sceneView, SIGNAL(requestFocus(const std::string&)), this, SLOT(onRequestFocus(const std::string&))); } view->show(); if (!hint.empty()) view->useHint (hint); } void CSVDoc::View::moveScrollBarToEnd(int min, int max) { if (mScroll) { mScroll->horizontalScrollBar()->setValue(max); QObject::disconnect(mScroll->horizontalScrollBar(), SIGNAL(rangeChanged(int,int)), this, SLOT(moveScrollBarToEnd(int,int))); } } void CSVDoc::View::settingChanged (const CSMPrefs::Setting *setting) { if (*setting=="Windows/hide-subview") updateSubViewIndices (nullptr); else if (*setting=="Windows/mainwindow-scrollbar") { if (setting->toString()!="Grow Only") { if (mScroll) { if (setting->toString()=="Scrollbar Only") { mScrollbarOnly = true; mSubViewWindow.setMinimumWidth(0); } else if (mScrollbarOnly) { mScrollbarOnly = false; updateScrollbar(); } } else { createScrollArea(); } } else if (mScroll) { mScroll->takeWidget(); setCentralWidget (&mSubViewWindow); mScroll->deleteLater(); mScroll = nullptr; } } } void CSVDoc::View::newView() { mViewManager.addView (mDocument); } void CSVDoc::View::save() { mDocument->save(); } void CSVDoc::View::openHelp() { Misc::HelpViewer::openHelp("manuals/openmw-cs/index.html"); } void CSVDoc::View::tutorial() { Misc::HelpViewer::openHelp("manuals/openmw-cs/tour.html"); } void CSVDoc::View::infoAbout() { // Get current OpenMW version QString versionInfo = (Version::getOpenmwVersionDescription(mDocument->getResourceDir().string())+ #if defined(__x86_64__) || defined(_M_X64) " (64-bit)").c_str(); #else " (32-bit)").c_str(); #endif // Get current year time_t now = time(nullptr); struct tm tstruct; char copyrightInfo[40]; tstruct = *localtime(&now); strftime(copyrightInfo, sizeof(copyrightInfo), "Copyright © 2008-%Y OpenMW Team", &tstruct); QString aboutText = QString( "

" "

OpenMW Construction Set

" "%1\n\n" "%2\n\n" "%3\n\n" "" "" "" "" "" "
%4https://openmw.org
%5https://forum.openmw.org
%6https://gitlab.com/OpenMW/openmw/issues
%7ircs://irc.libera.chat/#openmw
" "

") .arg(versionInfo , tr("OpenMW-CS is a content file editor for OpenMW, a modern, free and open source game engine.") , tr(copyrightInfo) , tr("Home Page:") , tr("Forum:") , tr("Bug Tracker:") , tr("IRC:")); QMessageBox::about(this, "About OpenMW-CS", aboutText); } void CSVDoc::View::infoAboutQt() { QMessageBox::aboutQt(this); } void CSVDoc::View::verify() { addSubView (mDocument->verify()); } void CSVDoc::View::addGlobalsSubView() { addSubView (CSMWorld::UniversalId::Type_Globals); } void CSVDoc::View::addGmstsSubView() { addSubView (CSMWorld::UniversalId::Type_Gmsts); } void CSVDoc::View::addSkillsSubView() { addSubView (CSMWorld::UniversalId::Type_Skills); } void CSVDoc::View::addClassesSubView() { addSubView (CSMWorld::UniversalId::Type_Classes); } void CSVDoc::View::addFactionsSubView() { addSubView (CSMWorld::UniversalId::Type_Factions); } void CSVDoc::View::addRacesSubView() { addSubView (CSMWorld::UniversalId::Type_Races); } void CSVDoc::View::addSoundsSubView() { addSubView (CSMWorld::UniversalId::Type_Sounds); } void CSVDoc::View::addScriptsSubView() { addSubView (CSMWorld::UniversalId::Type_Scripts); } void CSVDoc::View::addRegionsSubView() { addSubView (CSMWorld::UniversalId::Type_Regions); } void CSVDoc::View::addBirthsignsSubView() { addSubView (CSMWorld::UniversalId::Type_Birthsigns); } void CSVDoc::View::addSpellsSubView() { addSubView (CSMWorld::UniversalId::Type_Spells); } void CSVDoc::View::addCellsSubView() { addSubView (CSMWorld::UniversalId::Type_Cells); } void CSVDoc::View::addReferenceablesSubView() { addSubView (CSMWorld::UniversalId::Type_Referenceables); } void CSVDoc::View::addReferencesSubView() { addSubView (CSMWorld::UniversalId::Type_References); } void CSVDoc::View::addRegionMapSubView() { addSubView (CSMWorld::UniversalId::Type_RegionMap); } void CSVDoc::View::addFiltersSubView() { addSubView (CSMWorld::UniversalId::Type_Filters); } void CSVDoc::View::addTopicsSubView() { addSubView (CSMWorld::UniversalId::Type_Topics); } void CSVDoc::View::addJournalsSubView() { addSubView (CSMWorld::UniversalId::Type_Journals); } void CSVDoc::View::addTopicInfosSubView() { addSubView (CSMWorld::UniversalId::Type_TopicInfos); } void CSVDoc::View::addJournalInfosSubView() { addSubView (CSMWorld::UniversalId::Type_JournalInfos); } void CSVDoc::View::addEnchantmentsSubView() { addSubView (CSMWorld::UniversalId::Type_Enchantments); } void CSVDoc::View::addBodyPartsSubView() { addSubView (CSMWorld::UniversalId::Type_BodyParts); } void CSVDoc::View::addSoundGensSubView() { addSubView (CSMWorld::UniversalId::Type_SoundGens); } void CSVDoc::View::addMeshesSubView() { addSubView (CSMWorld::UniversalId::Type_Meshes); } void CSVDoc::View::addIconsSubView() { addSubView (CSMWorld::UniversalId::Type_Icons); } void CSVDoc::View::addMusicsSubView() { addSubView (CSMWorld::UniversalId::Type_Musics); } void CSVDoc::View::addSoundsResSubView() { addSubView (CSMWorld::UniversalId::Type_SoundsRes); } void CSVDoc::View::addMagicEffectsSubView() { addSubView (CSMWorld::UniversalId::Type_MagicEffects); } void CSVDoc::View::addTexturesSubView() { addSubView (CSMWorld::UniversalId::Type_Textures); } void CSVDoc::View::addVideosSubView() { addSubView (CSMWorld::UniversalId::Type_Videos); } void CSVDoc::View::addDebugProfilesSubView() { addSubView (CSMWorld::UniversalId::Type_DebugProfiles); } void CSVDoc::View::addRunLogSubView() { addSubView (CSMWorld::UniversalId::Type_RunLog); } void CSVDoc::View::addLandsSubView() { addSubView (CSMWorld::UniversalId::Type_Lands); } void CSVDoc::View::addLandTexturesSubView() { addSubView (CSMWorld::UniversalId::Type_LandTextures); } void CSVDoc::View::addPathgridSubView() { addSubView (CSMWorld::UniversalId::Type_Pathgrids); } void CSVDoc::View::addStartScriptsSubView() { addSubView (CSMWorld::UniversalId::Type_StartScripts); } void CSVDoc::View::addSearchSubView() { addSubView (mDocument->newSearch()); } void CSVDoc::View::addMetaDataSubView() { addSubView (CSMWorld::UniversalId (CSMWorld::UniversalId::Type_MetaData, "sys::meta")); } void CSVDoc::View::abortOperation (int type) { mDocument->abortOperation (type); updateActions(); } CSVDoc::Operations *CSVDoc::View::getOperations() const { return mOperations; } void CSVDoc::View::exit() { emit exitApplicationRequest (this); } void CSVDoc::View::resizeViewWidth (int width) { if (width >= 0) resize (width, geometry().height()); } void CSVDoc::View::resizeViewHeight (int height) { if (height >= 0) resize (geometry().width(), height); } void CSVDoc::View::toggleShowStatusBar (bool show) { for (QObject *view : mSubViewWindow.children()) { if (CSVDoc::SubView *subView = dynamic_cast (view)) subView->setStatusBar (show); } } void CSVDoc::View::toggleStatusBar(bool checked) { mShowStatusBar->setChecked(checked); } void CSVDoc::View::loadErrorLog() { addSubView (CSMWorld::UniversalId (CSMWorld::UniversalId::Type_LoadErrorLog, 0)); } void CSVDoc::View::run (const std::string& profile, const std::string& startupInstruction) { mDocument->startRunning (profile, startupInstruction); } void CSVDoc::View::stop() { mDocument->stopRunning(); } void CSVDoc::View::closeRequest (SubView *subView) { CSMPrefs::Category& windows = CSMPrefs::State::get()["Windows"]; if (mSubViews.size()>1 || mViewTotal<=1 || !windows["hide-subview"].isTrue()) { subView->deleteLater(); mSubViews.removeOne (subView); } else if (mViewManager.closeRequest (this)) mViewManager.removeDocAndView (mDocument); } void CSVDoc::View::updateScrollbar() { QRect rect; QWidget *topLevel = QApplication::topLevelAt(pos()); if (topLevel) rect = topLevel->rect(); else rect = this->rect(); int newWidth = 0; for (int i = 0; i < mSubViews.size(); ++i) { newWidth += mSubViews[i]->width(); } int frameWidth = frameGeometry().width() - width(); if ((newWidth+frameWidth) >= rect.width()) mSubViewWindow.setMinimumWidth(newWidth); else mSubViewWindow.setMinimumWidth(0); } void CSVDoc::View::merge() { emit mergeDocument (mDocument); } void CSVDoc::View::updateWidth(bool isGrowLimit, int minSubViewWidth) { QDesktopWidget *dw = QApplication::desktop(); QRect rect; if (isGrowLimit) rect = dw->screenGeometry(this); else rect = QGuiApplication::screens().at(dw->screenNumber(this))->geometry(); if (!mScrollbarOnly && mScroll && mSubViews.size() > 1) { int newWidth = width()+minSubViewWidth; int frameWidth = frameGeometry().width() - width(); if (newWidth+frameWidth <= rect.width()) { resize(newWidth, height()); // WARNING: below code assumes that new subviews are added to the right if (x() > rect.width()-(newWidth+frameWidth)) move(rect.width()-(newWidth+frameWidth), y()); // shift left to stay within the screen } else { // full width resize(rect.width()-frameWidth, height()); mSubViewWindow.setMinimumWidth(mSubViewWindow.width()+minSubViewWidth); move(0, y()); } } } void CSVDoc::View::createScrollArea() { mScroll = new QScrollArea(this); mScroll->setWidgetResizable(true); mScroll->setWidget(&mSubViewWindow); setCentralWidget(mScroll); } void CSVDoc::View::onRequestFocus (const std::string& id) { if(CSMPrefs::get()["3D Scene Editing"]["open-list-view"].isTrue()) { addReferencesSubView(); emit requestFocus(id); } else { addSubView(CSMWorld::UniversalId (CSMWorld::UniversalId::Type_Reference, id)); } } ================================================ FILE: apps/opencs/view/doc/view.hpp ================================================ #ifndef CSV_DOC_VIEW_H #define CSV_DOC_VIEW_H #include #include #include #include "subviewfactory.hpp" class QAction; class QDockWidget; class QScrollArea; namespace CSMDoc { class Document; } namespace CSMWorld { class UniversalId; } namespace CSMPrefs { class Setting; } namespace CSVDoc { class ViewManager; class Operations; class GlobalDebugProfileMenu; class View : public QMainWindow { Q_OBJECT ViewManager& mViewManager; CSMDoc::Document *mDocument; int mViewIndex; int mViewTotal; QList mSubViews; QAction *mUndo; QAction *mRedo; QAction *mSave; QAction *mVerify; QAction *mShowStatusBar; QAction *mStopDebug; QAction *mMerge; std::vector mEditingActions; Operations *mOperations; SubViewFactoryManager mSubViewFactory; QMainWindow mSubViewWindow; GlobalDebugProfileMenu *mGlobalDebugProfileMenu; QScrollArea *mScroll; bool mScrollbarOnly; // not implemented View (const View&); View& operator= (const View&); private: void closeEvent (QCloseEvent *event) override; QAction* createMenuEntry(CSMWorld::UniversalId::Type type, QMenu* menu, const char* shortcutName); QAction* createMenuEntry(const std::string& title, const std::string& iconName, QMenu* menu, const char* shortcutName); void setupFileMenu(); void setupEditMenu(); void setupViewMenu(); void setupWorldMenu(); void setupMechanicsMenu(); void setupCharacterMenu(); void setupAssetsMenu(); void setupDebugMenu(); void setupHelpMenu(); void setupUi(); void setupShortcut(const char* name, QAction* action); void updateActions(); void exitApplication(); /// User preference function void resizeViewWidth (int width); /// User preference function void resizeViewHeight (int height); void updateScrollbar(); void updateWidth(bool isGrowLimit, int minSubViewWidth); void createScrollArea(); public: View (ViewManager& viewManager, CSMDoc::Document *document, int totalViews); ///< The ownership of \a document is not transferred to *this. virtual ~View(); const CSMDoc::Document *getDocument() const; CSMDoc::Document *getDocument(); void setIndex (int viewIndex, int totalViews); void updateDocumentState(); void updateProgress (int current, int max, int type, int threads); void toggleStatusBar(bool checked); Operations *getOperations() const; signals: void newGameRequest(); void newAddonRequest(); void loadDocumentRequest(); void exitApplicationRequest (CSVDoc::View *view); void editSettingsRequest(); void mergeDocument (CSMDoc::Document *document); void requestFocus (const std::string& id); public slots: void addSubView (const CSMWorld::UniversalId& id, const std::string& hint = ""); ///< \param hint Suggested view point (e.g. coordinates in a 3D scene or a line number /// in a script). void abortOperation (int type); void updateTitle(); // called when subviews are added or removed void updateSubViewIndices (SubView *view = nullptr); private slots: void settingChanged (const CSMPrefs::Setting *setting); void undoActionChanged(); void redoActionChanged(); void newView(); void save(); void exit(); static void openHelp(); static void tutorial(); void infoAbout(); void infoAboutQt(); void verify(); void addGlobalsSubView(); void addGmstsSubView(); void addSkillsSubView(); void addClassesSubView(); void addFactionsSubView(); void addRacesSubView(); void addSoundsSubView(); void addScriptsSubView(); void addRegionsSubView(); void addBirthsignsSubView(); void addSpellsSubView(); void addCellsSubView(); void addReferenceablesSubView(); void addReferencesSubView(); void addRegionMapSubView(); void addFiltersSubView(); void addTopicsSubView(); void addJournalsSubView(); void addTopicInfosSubView(); void addJournalInfosSubView(); void addEnchantmentsSubView(); void addBodyPartsSubView(); void addSoundGensSubView(); void addMagicEffectsSubView(); void addMeshesSubView(); void addIconsSubView(); void addMusicsSubView(); void addSoundsResSubView(); void addTexturesSubView(); void addVideosSubView(); void addDebugProfilesSubView(); void addRunLogSubView(); void addLandsSubView(); void addLandTexturesSubView(); void addPathgridSubView(); void addStartScriptsSubView(); void addSearchSubView(); void addMetaDataSubView(); void toggleShowStatusBar (bool show); void loadErrorLog(); void run (const std::string& profile, const std::string& startupInstruction = ""); void stop(); void closeRequest (SubView *subView); void moveScrollBarToEnd(int min, int max); void merge(); void onRequestFocus (const std::string& id); }; } #endif ================================================ FILE: apps/opencs/view/doc/viewmanager.cpp ================================================ #include "viewmanager.hpp" #include #include #include #include #include #include #include "../../model/doc/documentmanager.hpp" #include "../../model/doc/document.hpp" #include "../../model/world/columns.hpp" #include "../../model/world/idcompletionmanager.hpp" #include "../../model/prefs/state.hpp" #include "../world/util.hpp" #include "../world/enumdelegate.hpp" #include "../world/vartypedelegate.hpp" #include "../world/recordstatusdelegate.hpp" #include "../world/idtypedelegate.hpp" #include "../world/idcompletiondelegate.hpp" #include "../world/colordelegate.hpp" #include "view.hpp" void CSVDoc::ViewManager::updateIndices() { std::map > documents; for (std::vector::const_iterator iter (mViews.begin()); iter!=mViews.end(); ++iter) { std::map >::iterator document = documents.find ((*iter)->getDocument()); if (document==documents.end()) document = documents.insert ( std::make_pair ((*iter)->getDocument(), std::make_pair (0, countViews ((*iter)->getDocument())))). first; (*iter)->setIndex (document->second.first++, document->second.second); } } CSVDoc::ViewManager::ViewManager (CSMDoc::DocumentManager& documentManager) : mDocumentManager (documentManager), mExitOnSaveStateChange(false), mUserWarned(false) { mDelegateFactories = new CSVWorld::CommandDelegateFactoryCollection; mDelegateFactories->add (CSMWorld::ColumnBase::Display_GmstVarType, new CSVWorld::VarTypeDelegateFactory (ESM::VT_None, ESM::VT_String, ESM::VT_Int, ESM::VT_Float)); mDelegateFactories->add (CSMWorld::ColumnBase::Display_GlobalVarType, new CSVWorld::VarTypeDelegateFactory (ESM::VT_Short, ESM::VT_Long, ESM::VT_Float)); mDelegateFactories->add (CSMWorld::ColumnBase::Display_RecordState, new CSVWorld::RecordStatusDelegateFactory()); mDelegateFactories->add (CSMWorld::ColumnBase::Display_RefRecordType, new CSVWorld::IdTypeDelegateFactory()); mDelegateFactories->add (CSMWorld::ColumnBase::Display_Colour, new CSVWorld::ColorDelegateFactory()); std::vector idCompletionColumns = CSMWorld::IdCompletionManager::getDisplayTypes(); for (std::vector::const_iterator current = idCompletionColumns.begin(); current != idCompletionColumns.end(); ++current) { mDelegateFactories->add(*current, new CSVWorld::IdCompletionDelegateFactory()); } struct Mapping { CSMWorld::ColumnBase::Display mDisplay; CSMWorld::Columns::ColumnId mColumnId; bool mAllowNone; }; static const Mapping sMapping[] = { { CSMWorld::ColumnBase::Display_Specialisation, CSMWorld::Columns::ColumnId_Specialisation, false }, { CSMWorld::ColumnBase::Display_Attribute, CSMWorld::Columns::ColumnId_Attribute, true }, { CSMWorld::ColumnBase::Display_SpellType, CSMWorld::Columns::ColumnId_SpellType, false }, { CSMWorld::ColumnBase::Display_ApparatusType, CSMWorld::Columns::ColumnId_ApparatusType, false }, { CSMWorld::ColumnBase::Display_ArmorType, CSMWorld::Columns::ColumnId_ArmorType, false }, { CSMWorld::ColumnBase::Display_ClothingType, CSMWorld::Columns::ColumnId_ClothingType, false }, { CSMWorld::ColumnBase::Display_CreatureType, CSMWorld::Columns::ColumnId_CreatureType, false }, { CSMWorld::ColumnBase::Display_WeaponType, CSMWorld::Columns::ColumnId_WeaponType, false }, { CSMWorld::ColumnBase::Display_DialogueType, CSMWorld::Columns::ColumnId_DialogueType, false }, { CSMWorld::ColumnBase::Display_QuestStatusType, CSMWorld::Columns::ColumnId_QuestStatusType, false }, { CSMWorld::ColumnBase::Display_EnchantmentType, CSMWorld::Columns::ColumnId_EnchantmentType, false }, { CSMWorld::ColumnBase::Display_BodyPartType, CSMWorld::Columns::ColumnId_BodyPartType, false }, { CSMWorld::ColumnBase::Display_MeshType, CSMWorld::Columns::ColumnId_MeshType, false }, { CSMWorld::ColumnBase::Display_Gender, CSMWorld::Columns::ColumnId_Gender, true }, { CSMWorld::ColumnBase::Display_SoundGeneratorType, CSMWorld::Columns::ColumnId_SoundGeneratorType, false }, { CSMWorld::ColumnBase::Display_School, CSMWorld::Columns::ColumnId_School, false }, { CSMWorld::ColumnBase::Display_SkillId, CSMWorld::Columns::ColumnId_Skill, true }, { CSMWorld::ColumnBase::Display_EffectRange, CSMWorld::Columns::ColumnId_EffectRange, false }, { CSMWorld::ColumnBase::Display_EffectId, CSMWorld::Columns::ColumnId_EffectId, false }, { CSMWorld::ColumnBase::Display_PartRefType, CSMWorld::Columns::ColumnId_PartRefType, false }, { CSMWorld::ColumnBase::Display_AiPackageType, CSMWorld::Columns::ColumnId_AiPackageType, false }, { CSMWorld::ColumnBase::Display_InfoCondFunc, CSMWorld::Columns::ColumnId_InfoCondFunc, false }, { CSMWorld::ColumnBase::Display_InfoCondComp, CSMWorld::Columns::ColumnId_InfoCondComp, false }, { CSMWorld::ColumnBase::Display_IngredEffectId, CSMWorld::Columns::ColumnId_EffectId, true }, { CSMWorld::ColumnBase::Display_EffectSkill, CSMWorld::Columns::ColumnId_Skill, false }, { CSMWorld::ColumnBase::Display_EffectAttribute, CSMWorld::Columns::ColumnId_Attribute, false }, { CSMWorld::ColumnBase::Display_BookType, CSMWorld::Columns::ColumnId_BookType, false }, { CSMWorld::ColumnBase::Display_BloodType, CSMWorld::Columns::ColumnId_BloodType, false }, { CSMWorld::ColumnBase::Display_EmitterType, CSMWorld::Columns::ColumnId_EmitterType, false }, { CSMWorld::ColumnBase::Display_GenderNpc, CSMWorld::Columns::ColumnId_Gender, false } }; for (std::size_t i=0; iadd (sMapping[i].mDisplay, new CSVWorld::EnumDelegateFactory ( CSMWorld::Columns::getEnums (sMapping[i].mColumnId), sMapping[i].mAllowNone)); connect (&mDocumentManager, SIGNAL (loadRequest (CSMDoc::Document *)), &mLoader, SLOT (add (CSMDoc::Document *))); connect ( &mDocumentManager, SIGNAL (loadingStopped (CSMDoc::Document *, bool, const std::string&)), &mLoader, SLOT (loadingStopped (CSMDoc::Document *, bool, const std::string&))); connect ( &mDocumentManager, SIGNAL (nextStage (CSMDoc::Document *, const std::string&, int)), &mLoader, SLOT (nextStage (CSMDoc::Document *, const std::string&, int))); connect ( &mDocumentManager, SIGNAL (nextRecord (CSMDoc::Document *, int)), &mLoader, SLOT (nextRecord (CSMDoc::Document *, int))); connect ( &mDocumentManager, SIGNAL (loadMessage (CSMDoc::Document *, const std::string&)), &mLoader, SLOT (loadMessage (CSMDoc::Document *, const std::string&))); connect ( &mLoader, SIGNAL (cancel (CSMDoc::Document *)), &mDocumentManager, SIGNAL (cancelLoading (CSMDoc::Document *))); connect ( &mLoader, SIGNAL (close (CSMDoc::Document *)), &mDocumentManager, SLOT (removeDocument (CSMDoc::Document *))); } CSVDoc::ViewManager::~ViewManager() { delete mDelegateFactories; for (std::vector::iterator iter (mViews.begin()); iter!=mViews.end(); ++iter) delete *iter; } CSVDoc::View *CSVDoc::ViewManager::addView (CSMDoc::Document *document) { if (countViews (document)==0) { // new document connect (document, SIGNAL (stateChanged (int, CSMDoc::Document *)), this, SLOT (documentStateChanged (int, CSMDoc::Document *))); connect (document, SIGNAL (progress (int, int, int, int, CSMDoc::Document *)), this, SLOT (progress (int, int, int, int, CSMDoc::Document *))); } View *view = new View (*this, document, countViews (document)+1); mViews.push_back (view); view->toggleStatusBar (CSMPrefs::get()["Windows"]["show-statusbar"].isTrue()); view->show(); connect (view, SIGNAL (newGameRequest ()), this, SIGNAL (newGameRequest())); connect (view, SIGNAL (newAddonRequest ()), this, SIGNAL (newAddonRequest())); connect (view, SIGNAL (loadDocumentRequest ()), this, SIGNAL (loadDocumentRequest())); connect (view, SIGNAL (editSettingsRequest()), this, SIGNAL (editSettingsRequest())); connect (view, SIGNAL (mergeDocument (CSMDoc::Document *)), this, SIGNAL (mergeDocument (CSMDoc::Document *))); updateIndices(); return view; } CSVDoc::View *CSVDoc::ViewManager::addView (CSMDoc::Document *document, const CSMWorld::UniversalId& id, const std::string& hint) { View* view = addView(document); view->addSubView(id, hint); return view; } int CSVDoc::ViewManager::countViews (const CSMDoc::Document *document) const { int count = 0; for (std::vector::const_iterator iter (mViews.begin()); iter!=mViews.end(); ++iter) if ((*iter)->getDocument()==document) ++count; return count; } bool CSVDoc::ViewManager::closeRequest (View *view) { std::vector::iterator iter = std::find (mViews.begin(), mViews.end(), view); bool continueWithClose = false; if (iter!=mViews.end()) { bool last = countViews (view->getDocument())<=1; if (last) continueWithClose = notifySaveOnClose (view); else { (*iter)->deleteLater(); mViews.erase (iter); updateIndices(); } } return continueWithClose; } // NOTE: This method assumes that it is called only if the last document void CSVDoc::ViewManager::removeDocAndView (CSMDoc::Document *document) { for (std::vector::iterator iter (mViews.begin()); iter!=mViews.end(); ++iter) { // the first match should also be the only match if((*iter)->getDocument() == document) { mDocumentManager.removeDocument(document); (*iter)->deleteLater(); mViews.erase (iter); updateIndices(); return; } } } bool CSVDoc::ViewManager::notifySaveOnClose (CSVDoc::View *view) { bool result = true; CSMDoc::Document *document = view->getDocument(); //notify user of saving in progress if ( (document->getState() & CSMDoc::State_Saving) ) result = showSaveInProgressMessageBox (view); //notify user of unsaved changes and process response else if ( document->getState() & CSMDoc::State_Modified) result = showModifiedDocumentMessageBox (view); return result; } bool CSVDoc::ViewManager::showModifiedDocumentMessageBox (CSVDoc::View *view) { emit closeMessageBox(); QMessageBox messageBox(view); CSMDoc::Document *document = view->getDocument(); messageBox.setWindowTitle (QString::fromUtf8(document->getSavePath().filename().string().c_str())); messageBox.setText ("The document has been modified."); messageBox.setInformativeText ("Do you want to save your changes?"); messageBox.setStandardButtons (QMessageBox::Save | QMessageBox::Discard | QMessageBox::Cancel); messageBox.setDefaultButton (QMessageBox::Save); messageBox.setWindowModality (Qt::NonModal); messageBox.hide(); messageBox.show(); bool retVal = true; connect (this, SIGNAL (closeMessageBox()), &messageBox, SLOT (close())); connect (document, SIGNAL (stateChanged (int, CSMDoc::Document *)), this, SLOT (onExitWarningHandler(int, CSMDoc::Document *))); mUserWarned = true; int response = messageBox.exec(); mUserWarned = false; switch (response) { case QMessageBox::Save: document->save(); mExitOnSaveStateChange = true; retVal = false; break; case QMessageBox::Discard: disconnect (document, SIGNAL (stateChanged (int, CSMDoc::Document *)), this, SLOT (onExitWarningHandler(int, CSMDoc::Document *))); break; case QMessageBox::Cancel: //disconnect to prevent unintended view closures disconnect (document, SIGNAL (stateChanged (int, CSMDoc::Document *)), this, SLOT (onExitWarningHandler(int, CSMDoc::Document *))); retVal = false; break; default: break; } return retVal; } bool CSVDoc::ViewManager::showSaveInProgressMessageBox (CSVDoc::View *view) { QMessageBox messageBox; CSMDoc::Document *document = view->getDocument(); messageBox.setText ("The document is currently being saved."); messageBox.setInformativeText("Do you want to close now and abort saving, or wait until saving has completed?"); QPushButton* waitButton = messageBox.addButton (tr("Wait"), QMessageBox::YesRole); QPushButton* closeButton = messageBox.addButton (tr("Close Now"), QMessageBox::RejectRole); QPushButton* cancelButton = messageBox.addButton (tr("Cancel"), QMessageBox::NoRole); messageBox.setDefaultButton (waitButton); bool retVal = true; //Connections shut down message box if operation ends before user makes a decision. connect (document, SIGNAL (stateChanged (int, CSMDoc::Document *)), this, SLOT (onExitWarningHandler(int, CSMDoc::Document *))); connect (this, SIGNAL (closeMessageBox()), &messageBox, SLOT (close())); //set / clear the user warned flag to indicate whether or not the message box is currently active. mUserWarned = true; messageBox.exec(); mUserWarned = false; //if closed by the warning handler, defaults to the RejectRole button (closeButton) if (messageBox.clickedButton() == waitButton) { //save the View iterator for shutdown after the save operation ends mExitOnSaveStateChange = true; retVal = false; } else if (messageBox.clickedButton() == closeButton) { //disconnect to avoid segmentation fault disconnect (document, SIGNAL (stateChanged (int, CSMDoc::Document *)), this, SLOT (onExitWarningHandler(int, CSMDoc::Document *))); view->abortOperation(CSMDoc::State_Saving); mExitOnSaveStateChange = true; } else if (messageBox.clickedButton() == cancelButton) { //abort shutdown, allow save to complete //disconnection to prevent unintended view closures mExitOnSaveStateChange = false; disconnect (document, SIGNAL (stateChanged (int, CSMDoc::Document *)), this, SLOT (onExitWarningHandler(int, CSMDoc::Document *))); retVal = false; } return retVal; } void CSVDoc::ViewManager::documentStateChanged (int state, CSMDoc::Document *document) { for (std::vector::const_iterator iter (mViews.begin()); iter!=mViews.end(); ++iter) if ((*iter)->getDocument()==document) (*iter)->updateDocumentState(); } void CSVDoc::ViewManager::progress (int current, int max, int type, int threads, CSMDoc::Document *document) { for (std::vector::const_iterator iter (mViews.begin()); iter!=mViews.end(); ++iter) if ((*iter)->getDocument()==document) (*iter)->updateProgress (current, max, type, threads); } void CSVDoc::ViewManager::onExitWarningHandler (int state, CSMDoc::Document *document) { if ( !(state & CSMDoc::State_Saving) ) { //if the user is being warned (message box is active), shut down the message box, //as there is no save operation currently running if ( mUserWarned ) emit closeMessageBox(); //otherwise, the user has closed the message box before the save operation ended. //exit the application else if (mExitOnSaveStateChange) QApplication::instance()->exit(); } } bool CSVDoc::ViewManager::removeDocument (CSVDoc::View *view) { if(!notifySaveOnClose(view)) return false; else { // don't bother closing views or updating indicies, but remove from mViews CSMDoc::Document * document = view->getDocument(); std::vector remainingViews; std::vector::const_iterator iter = mViews.begin(); for (; iter!=mViews.end(); ++iter) { if(document == (*iter)->getDocument()) (*iter)->setVisible(false); else remainingViews.push_back(*iter); } mDocumentManager.removeDocument(document); mViews = remainingViews; } return true; } void CSVDoc::ViewManager::exitApplication (CSVDoc::View *view) { if(!removeDocument(view)) // close the current document first return; while(!mViews.empty()) // attempt to close all other documents { mViews.back()->activateWindow(); mViews.back()->raise(); // raise the window to alert the user if(!removeDocument(mViews.back())) return; } // Editor exits (via a signal) when the last document is deleted } ================================================ FILE: apps/opencs/view/doc/viewmanager.hpp ================================================ #ifndef CSV_DOC_VIEWMANAGER_H #define CSV_DOC_VIEWMANAGER_H #include #include #include "loader.hpp" namespace CSMDoc { class Document; class DocumentManager; } namespace CSVWorld { class CommandDelegateFactoryCollection; } namespace CSMWorld { class UniversalId; } namespace CSVDoc { class View; class ViewManager : public QObject { Q_OBJECT CSMDoc::DocumentManager& mDocumentManager; std::vector mViews; CSVWorld::CommandDelegateFactoryCollection *mDelegateFactories; bool mExitOnSaveStateChange; bool mUserWarned; Loader mLoader; // not implemented ViewManager (const ViewManager&); ViewManager& operator= (const ViewManager&); void updateIndices(); bool notifySaveOnClose (View *view = nullptr); bool showModifiedDocumentMessageBox (View *view); bool showSaveInProgressMessageBox (View *view); bool removeDocument(View *view); public: ViewManager (CSMDoc::DocumentManager& documentManager); virtual ~ViewManager(); View *addView (CSMDoc::Document *document); ///< The ownership of the returned view is not transferred. View *addView (CSMDoc::Document *document, const CSMWorld::UniversalId& id, const std::string& hint); int countViews (const CSMDoc::Document *document) const; ///< Return number of views for \a document. bool closeRequest (View *view); void removeDocAndView (CSMDoc::Document *document); signals: void newGameRequest(); void newAddonRequest(); void loadDocumentRequest(); void closeMessageBox(); void editSettingsRequest(); void mergeDocument (CSMDoc::Document *document); public slots: void exitApplication (CSVDoc::View *view); private slots: void documentStateChanged (int state, CSMDoc::Document *document); void progress (int current, int max, int type, int threads, CSMDoc::Document *document); void onExitWarningHandler(int state, CSMDoc::Document* document); }; } #endif ================================================ FILE: apps/opencs/view/filter/editwidget.cpp ================================================ #include "editwidget.hpp" #include #include #include #include #include #include #include #include "../../model/world/data.hpp" #include "../../model/world/idtablebase.hpp" #include "../../model/world/columns.hpp" #include "../../model/prefs/shortcut.hpp" CSVFilter::EditWidget::EditWidget (CSMWorld::Data& data, QWidget *parent) : QLineEdit (parent), mParser (data), mIsEmpty(true) { mPalette = palette(); connect (this, SIGNAL (textChanged (const QString&)), this, SLOT (textChanged (const QString&))); const CSMWorld::IdTableBase *model = static_cast (data.getTableModel (CSMWorld::UniversalId::Type_Filters)); connect (model, SIGNAL (dataChanged (const QModelIndex &, const QModelIndex&)), this, SLOT (filterDataChanged (const QModelIndex &, const QModelIndex&)), Qt::QueuedConnection); connect (model, SIGNAL (rowsRemoved (const QModelIndex&, int, int)), this, SLOT (filterRowsRemoved (const QModelIndex&, int, int)), Qt::QueuedConnection); connect (model, SIGNAL (rowsInserted (const QModelIndex&, int, int)), this, SLOT (filterRowsInserted (const QModelIndex&, int, int)), Qt::QueuedConnection); mStateColumnIndex = model->findColumnIndex(CSMWorld::Columns::ColumnId_Modification); mDescColumnIndex = model->findColumnIndex(CSMWorld::Columns::ColumnId_Description); mHelpAction = new QAction (tr ("Help"), this); connect (mHelpAction, SIGNAL (triggered()), this, SLOT (openHelp())); mHelpAction->setIcon(QIcon(":/info.png")); addAction (mHelpAction); auto* openHelpShortcut = new CSMPrefs::Shortcut("help", this); openHelpShortcut->associateAction(mHelpAction); } void CSVFilter::EditWidget::textChanged (const QString& text) { //no need to parse and apply filter if it was empty and now is empty too. //e.g. - we modifiing content of filter with already opened some other (big) tables. if (text.length() == 0){ if (mIsEmpty) return; else mIsEmpty = true; }else mIsEmpty = false; if (mParser.parse (text.toUtf8().constData())) { setPalette (mPalette); emit filterChanged (mParser.getFilter()); } else { QPalette palette (mPalette); palette.setColor (QPalette::Text, Qt::red); setPalette (palette); /// \todo improve error reporting; mark only the faulty part } } void CSVFilter::EditWidget::filterDataChanged (const QModelIndex& topLeft, const QModelIndex& bottomRight) { for (int i = topLeft.column(); i <= bottomRight.column(); ++i) if (i != mStateColumnIndex && i != mDescColumnIndex) textChanged (text()); } void CSVFilter::EditWidget::filterRowsRemoved (const QModelIndex& parent, int start, int end) { textChanged (text()); } void CSVFilter::EditWidget::filterRowsInserted (const QModelIndex& parent, int start, int end) { textChanged (text()); } void CSVFilter::EditWidget::createFilterRequest (std::vector< std::pair< std::string, std::vector< std::string > > >& filterSource, Qt::DropAction action) { const unsigned count = filterSource.size(); bool multipleElements = false; switch (count) //setting multipleElements; { case 0: //empty return; //nothing to do here case 1: //only single multipleElements = false; break; default: multipleElements = true; break; } Qt::KeyboardModifiers key = QApplication::keyboardModifiers(); QString oldContent (text()); bool replaceMode = false; std::string orAnd; switch (key) //setting replaceMode and string used to glue expressions { case Qt::ShiftModifier: orAnd = "!or("; replaceMode = false; break; case Qt::ControlModifier: orAnd = "!and("; replaceMode = false; break; default: replaceMode = true; break; } if (oldContent.isEmpty() || !oldContent.contains (QRegExp ("^!.*$", Qt::CaseInsensitive))) //if line edit is empty or it does not contain one shot filter go into replace mode { replaceMode = true; } if (!replaceMode) { oldContent.remove ('!'); } std::stringstream ss; if (multipleElements) { if (replaceMode) { ss<<"!or("; } else { ss << orAnd << oldContent.toUtf8().constData() << ','; } for (unsigned i = 0; i < count; ++i) { ss<4) { clear(); insert (QString::fromUtf8(ss.str().c_str())); } } std::string CSVFilter::EditWidget::generateFilter (std::pair< std::string, std::vector< std::string > >& seekedString) const { const unsigned columns = seekedString.second.size(); bool multipleColumns = false; switch (columns) { case 0: //empty return ""; //no column to filter case 1: //one column to look for multipleColumns = false; break; default: multipleColumns = true; break; } std::stringstream ss; if (multipleColumns) { ss<<"or("; for (unsigned i = 0; i < columns; ++i) { ss<<"string("<<'"'<addAction(mHelpAction); menu->exec(event->globalPos()); delete menu; } void CSVFilter::EditWidget::openHelp() { Misc::HelpViewer::openHelp("manuals/openmw-cs/record-filters.html"); } ================================================ FILE: apps/opencs/view/filter/editwidget.hpp ================================================ #ifndef CSV_FILTER_EDITWIDGET_H #define CSV_FILTER_EDITWIDGET_H #include #include #include #include "../../model/filter/parser.hpp" #include "../../model/filter/node.hpp" class QModelIndex; namespace CSMWorld { class Data; } namespace CSVFilter { class EditWidget : public QLineEdit { Q_OBJECT CSMFilter::Parser mParser; QPalette mPalette; bool mIsEmpty; int mStateColumnIndex; int mDescColumnIndex; QAction *mHelpAction; public: EditWidget (CSMWorld::Data& data, QWidget *parent = nullptr); void createFilterRequest(std::vector > >& filterSource, Qt::DropAction action); signals: void filterChanged (std::shared_ptr filter); private: std::string generateFilter(std::pair >& seekedString) const; void contextMenuEvent (QContextMenuEvent *event) override; private slots: void textChanged (const QString& text); void filterDataChanged (const QModelIndex& topLeft, const QModelIndex& bottomRight); void filterRowsRemoved (const QModelIndex& parent, int start, int end); void filterRowsInserted (const QModelIndex& parent, int start, int end); static void openHelp(); }; } #endif ================================================ FILE: apps/opencs/view/filter/filterbox.cpp ================================================ #include "filterbox.hpp" #include #include #include "recordfilterbox.hpp" #include CSVFilter::FilterBox::FilterBox (CSMWorld::Data& data, QWidget *parent) : QWidget (parent) { QHBoxLayout *layout = new QHBoxLayout (this); layout->setContentsMargins (0, 0, 0, 0); mRecordFilterBox = new RecordFilterBox (data, this); layout->addWidget (mRecordFilterBox); setLayout (layout); connect (mRecordFilterBox, SIGNAL (filterChanged (std::shared_ptr)), this, SIGNAL (recordFilterChanged (std::shared_ptr))); setAcceptDrops(true); } void CSVFilter::FilterBox::setRecordFilter (const std::string& filter) { mRecordFilterBox->setFilter (filter); } void CSVFilter::FilterBox::dropEvent (QDropEvent* event) { const CSMWorld::TableMimeData* mime = dynamic_cast (event->mimeData()); if (!mime) // May happen when non-records (e.g. plain text) are dragged and dropped return; std::vector universalIdData = mime->getData(); emit recordDropped(universalIdData, event->proposedAction()); } void CSVFilter::FilterBox::dragEnterEvent (QDragEnterEvent* event) { event->acceptProposedAction(); } void CSVFilter::FilterBox::dragMoveEvent (QDragMoveEvent* event) { event->accept(); } void CSVFilter::FilterBox::createFilterRequest (std::vector< std::pair< std::string, std::vector< std::string > > >& filterSource, Qt::DropAction action) { mRecordFilterBox->createFilterRequest(filterSource, action); } ================================================ FILE: apps/opencs/view/filter/filterbox.hpp ================================================ #ifndef CSV_FILTER_FILTERBOX_H #define CSV_FILTER_FILTERBOX_H #include #include #include #include "../../model/filter/node.hpp" #include "../../model/world/universalid.hpp" namespace CSMWorld { class Data; } namespace CSVFilter { class RecordFilterBox; class FilterBox : public QWidget { Q_OBJECT RecordFilterBox *mRecordFilterBox; public: FilterBox (CSMWorld::Data& data, QWidget *parent = nullptr); void setRecordFilter (const std::string& filter); void createFilterRequest(std::vector > >& filterSource, Qt::DropAction action); private: void dragEnterEvent (QDragEnterEvent* event) override; void dropEvent (QDropEvent* event) override; void dragMoveEvent(QDragMoveEvent *event) override; signals: void recordFilterChanged (std::shared_ptr filter); void recordDropped (std::vector& types, Qt::DropAction action); }; } #endif ================================================ FILE: apps/opencs/view/filter/recordfilterbox.cpp ================================================ #include "recordfilterbox.hpp" #include #include #include "editwidget.hpp" CSVFilter::RecordFilterBox::RecordFilterBox (CSMWorld::Data& data, QWidget *parent) : QWidget (parent) { QHBoxLayout *layout = new QHBoxLayout (this); layout->setContentsMargins (0, 6, 5, 0); QLabel *label = new QLabel("Record Filter", this); label->setIndent(2); layout->addWidget (label); mEdit = new EditWidget (data, this); layout->addWidget (mEdit); setLayout (layout); connect ( mEdit, SIGNAL (filterChanged (std::shared_ptr)), this, SIGNAL (filterChanged (std::shared_ptr))); } void CSVFilter::RecordFilterBox::setFilter (const std::string& filter) { mEdit->clear(); mEdit->setText (QString::fromUtf8 (filter.c_str())); } void CSVFilter::RecordFilterBox::createFilterRequest (std::vector< std::pair< std::string, std::vector< std::string > > >& filterSource, Qt::DropAction action) { mEdit->createFilterRequest(filterSource, action); } ================================================ FILE: apps/opencs/view/filter/recordfilterbox.hpp ================================================ #ifndef CSV_FILTER_RECORDFILTERBOX_H #define CSV_FILTER_RECORDFILTERBOX_H #include #include #include #include "../../model/filter/node.hpp" namespace CSMWorld { class Data; } namespace CSVFilter { class EditWidget; class RecordFilterBox : public QWidget { Q_OBJECT EditWidget *mEdit; public: RecordFilterBox (CSMWorld::Data& data, QWidget *parent = nullptr); void setFilter (const std::string& filter); void useFilterRequest(const std::string& idOfFilter); void createFilterRequest(std::vector > >& filterSource, Qt::DropAction action); signals: void filterChanged (std::shared_ptr filter); }; } #endif ================================================ FILE: apps/opencs/view/prefs/contextmenulist.cpp ================================================ #include "contextmenulist.hpp" #include #include #include #include "../../model/prefs/state.hpp" CSVPrefs::ContextMenuList::ContextMenuList(QWidget* parent) :QListWidget(parent) { } void CSVPrefs::ContextMenuList::contextMenuEvent(QContextMenuEvent* e) { QMenu* menu = new QMenu(); menu->addAction("Reset category to default", this, SLOT(resetCategory())); menu->addAction("Reset all to default", this, SLOT(resetAll())); menu->exec(e->globalPos()); delete menu; } void CSVPrefs::ContextMenuList::mousePressEvent(QMouseEvent* e) { // enable all buttons except right click // This means that when right-clicking to enable the // context menu, the page doesn't switch at the same time. if (!(e->buttons() & Qt::RightButton)) { QListWidget::mousePressEvent(e); } } void CSVPrefs::ContextMenuList::resetCategory() { CSMPrefs::State::get().resetCategory(currentItem()->text().toStdString()); } void CSVPrefs::ContextMenuList::resetAll() { CSMPrefs::State::get().resetAll(); } ================================================ FILE: apps/opencs/view/prefs/contextmenulist.hpp ================================================ #ifndef CSV_PREFS_CONTEXTMENULIST_H #define CSV_PREFS_CONTEXTMENULIST_H #include class QContextMenuEvent; class QMouseEvent; namespace CSVPrefs { class ContextMenuList : public QListWidget { Q_OBJECT public: ContextMenuList(QWidget* parent = nullptr); protected: void contextMenuEvent(QContextMenuEvent* e) override; void mousePressEvent(QMouseEvent* e) override; private slots: void resetCategory(); void resetAll(); }; } #endif ================================================ FILE: apps/opencs/view/prefs/dialogue.cpp ================================================ #include "dialogue.hpp" #include #include #include #include #include #include #include #include #include "../../model/prefs/state.hpp" #include "page.hpp" #include "keybindingpage.hpp" #include "contextmenulist.hpp" void CSVPrefs::Dialogue::buildCategorySelector (QSplitter *main) { CSVPrefs::ContextMenuList* list = new CSVPrefs::ContextMenuList (main); list->setMinimumWidth (50); list->setSizePolicy (QSizePolicy::Expanding, QSizePolicy::Expanding); list->setSelectionBehavior (QAbstractItemView::SelectItems); main->addWidget (list); QFontMetrics metrics (QApplication::font(list)); int maxWidth = 1; for (CSMPrefs::State::Iterator iter = CSMPrefs::get().begin(); iter!=CSMPrefs::get().end(); ++iter) { QString label = QString::fromUtf8 (iter->second.getKey().c_str()); maxWidth = std::max (maxWidth, metrics.horizontalAdvance (label)); list->addItem (label); } list->setMaximumWidth (maxWidth + 10); connect (list, SIGNAL (currentItemChanged (QListWidgetItem *, QListWidgetItem *)), this, SLOT (selectionChanged (QListWidgetItem *, QListWidgetItem *))); } void CSVPrefs::Dialogue::buildContentArea (QSplitter *main) { mContent = new QStackedWidget (main); mContent->setSizePolicy (QSizePolicy::Preferred, QSizePolicy::Expanding); main->addWidget (mContent); } CSVPrefs::PageBase *CSVPrefs::Dialogue::makePage (const std::string& key) { // special case page code goes here if (key == "Key Bindings") return new KeyBindingPage(CSMPrefs::get()[key], mContent); else return new Page (CSMPrefs::get()[key], mContent); } CSVPrefs::Dialogue::Dialogue() { setWindowTitle ("User Settings"); setSizePolicy (QSizePolicy::MinimumExpanding, QSizePolicy::MinimumExpanding); setMinimumSize (600, 400); QSplitter *main = new QSplitter (this); setCentralWidget (main); buildCategorySelector (main); buildContentArea (main); } CSVPrefs::Dialogue::~Dialogue() { try { if (isVisible()) CSMPrefs::State::get().save(); } catch(const std::exception& e) { Log(Debug::Error) << "Error in the destructor: " << e.what(); } } void CSVPrefs::Dialogue::closeEvent (QCloseEvent *event) { QMainWindow::closeEvent (event); CSMPrefs::State::get().save(); } void CSVPrefs::Dialogue::show() { if (QWidget *active = QApplication::activeWindow()) { // place at the centre of the window with focus QSize size = active->size(); move (active->geometry().x()+(size.width() - frameGeometry().width())/2, active->geometry().y()+(size.height() - frameGeometry().height())/2); } else { QRect scr = QGuiApplication::primaryScreen()->geometry(); // otherwise place at the centre of the screen QPoint screenCenter = scr.center(); move (screenCenter - QPoint(frameGeometry().width()/2, frameGeometry().height()/2)); } QWidget::show(); } void CSVPrefs::Dialogue::selectionChanged (QListWidgetItem *current, QListWidgetItem *previous) { if (current) { std::string key = current->text().toUtf8().data(); for (int i=0; icount(); ++i) { PageBase& page = dynamic_cast (*mContent->widget (i)); if (page.getCategory().getKey()==key) { mContent->setCurrentIndex (i); return; } } PageBase *page = makePage (key); mContent->setCurrentIndex (mContent->addWidget (page)); } } ================================================ FILE: apps/opencs/view/prefs/dialogue.hpp ================================================ #ifndef CSV_PREFS_DIALOGUE_H #define CSV_PREFS_DIALOGUE_H #include class QSplitter; class QListWidget; class QStackedWidget; class QListWidgetItem; namespace CSVPrefs { class PageBase; class Dialogue : public QMainWindow { Q_OBJECT QStackedWidget *mContent; private: void buildCategorySelector (QSplitter *main); void buildContentArea (QSplitter *main); PageBase *makePage (const std::string& key); public: Dialogue(); virtual ~Dialogue(); protected: void closeEvent (QCloseEvent *event) override; public slots: void show(); private slots: void selectionChanged (QListWidgetItem *current, QListWidgetItem *previous); }; } #endif ================================================ FILE: apps/opencs/view/prefs/keybindingpage.cpp ================================================ #include "keybindingpage.hpp" #include #include #include #include #include #include #include "../../model/prefs/setting.hpp" #include "../../model/prefs/category.hpp" #include "../../model/prefs/state.hpp" namespace CSVPrefs { KeyBindingPage::KeyBindingPage(CSMPrefs::Category& category, QWidget* parent) : PageBase(category, parent) , mStackedLayout(nullptr) , mPageLayout(nullptr) , mPageSelector(nullptr) { // Need one widget for scroll area QWidget* topWidget = new QWidget(); QVBoxLayout* topLayout = new QVBoxLayout(topWidget); // Allows switching between "pages" QWidget* stackedWidget = new QWidget(); mStackedLayout = new QStackedLayout(stackedWidget); mPageSelector = new QComboBox(); connect(mPageSelector, SIGNAL(currentIndexChanged(int)), mStackedLayout, SLOT(setCurrentIndex(int))); QFrame* lineSeparator = new QFrame(topWidget); lineSeparator->setFrameShape(QFrame::HLine); lineSeparator->setFrameShadow(QFrame::Sunken); // Reset key bindings button QPushButton* resetButton = new QPushButton ("Reset to Defaults", topWidget); connect(resetButton, SIGNAL(clicked()), this, SLOT(resetKeyBindings())); topLayout->addWidget(mPageSelector); topLayout->addWidget(stackedWidget); topLayout->addWidget(lineSeparator); topLayout->addWidget(resetButton); topLayout->setSizeConstraint(QLayout::SetMinAndMaxSize); // Add each option for (CSMPrefs::Category::Iterator iter = category.begin(); iter!=category.end(); ++iter) addSetting (*iter); setWidgetResizable(true); setWidget(topWidget); } void KeyBindingPage::addSetting(CSMPrefs::Setting *setting) { std::pair widgets = setting->makeWidgets (this); if (widgets.first) { // Label, Option widgets assert(mPageLayout); int next = mPageLayout->rowCount(); mPageLayout->addWidget(widgets.first, next, 0); mPageLayout->addWidget(widgets.second, next, 1); } else if (widgets.second) { // Wide single widget assert(mPageLayout); int next = mPageLayout->rowCount(); mPageLayout->addWidget(widgets.second, next, 0, 1, 2); } else { if (setting->getLabel().empty()) { // Insert empty space assert(mPageLayout); int next = mPageLayout->rowCount(); mPageLayout->addWidget(new QWidget(), next, 0); } else { // Create new page QWidget* pageWidget = new QWidget(); mPageLayout = new QGridLayout(pageWidget); mPageLayout->setSizeConstraint(QLayout::SetMinAndMaxSize); mStackedLayout->addWidget(pageWidget); mPageSelector->addItem(QString::fromUtf8(setting->getLabel().c_str())); } } } void KeyBindingPage::resetKeyBindings() { CSMPrefs::State::get().resetCategory("Key Bindings"); } } ================================================ FILE: apps/opencs/view/prefs/keybindingpage.hpp ================================================ #ifndef CSV_PREFS_KEYBINDINGPAGE_H #define CSV_PREFS_KEYBINDINGPAGE_H #include "pagebase.hpp" class QComboBox; class QGridLayout; class QStackedLayout; namespace CSMPrefs { class Setting; } namespace CSVPrefs { class KeyBindingPage : public PageBase { Q_OBJECT public: KeyBindingPage(CSMPrefs::Category& category, QWidget* parent); void addSetting(CSMPrefs::Setting* setting); private: QStackedLayout* mStackedLayout; QGridLayout* mPageLayout; QComboBox* mPageSelector; private slots: void resetKeyBindings(); }; } #endif ================================================ FILE: apps/opencs/view/prefs/page.cpp ================================================ #include "page.hpp" #include #include "../../model/prefs/setting.hpp" #include "../../model/prefs/category.hpp" CSVPrefs::Page::Page (CSMPrefs::Category& category, QWidget *parent) : PageBase (category, parent) { QWidget *widget = new QWidget (parent); mGrid = new QGridLayout (widget); for (CSMPrefs::Category::Iterator iter = category.begin(); iter!=category.end(); ++iter) addSetting (*iter); setWidget (widget); } void CSVPrefs::Page::addSetting (CSMPrefs::Setting *setting) { std::pair widgets = setting->makeWidgets (this); int next = mGrid->rowCount(); if (widgets.first) { mGrid->addWidget (widgets.first, next, 0); mGrid->addWidget (widgets.second, next, 1); } else if (widgets.second) { mGrid->addWidget (widgets.second, next, 0, 1, 2); } else { mGrid->addWidget (new QWidget (this), next, 0); } } ================================================ FILE: apps/opencs/view/prefs/page.hpp ================================================ #ifndef CSV_PREFS_PAGE_H #define CSV_PREFS_PAGE_H #include "pagebase.hpp" class QGridLayout; namespace CSMPrefs { class Setting; } namespace CSVPrefs { class Page : public PageBase { Q_OBJECT QGridLayout *mGrid; public: Page (CSMPrefs::Category& category, QWidget *parent); void addSetting (CSMPrefs::Setting *setting); }; } #endif ================================================ FILE: apps/opencs/view/prefs/pagebase.cpp ================================================ #include "pagebase.hpp" #include #include #include "../../model/prefs/category.hpp" #include "../../model/prefs/state.hpp" CSVPrefs::PageBase::PageBase (CSMPrefs::Category& category, QWidget *parent) : QScrollArea (parent), mCategory (category) {} CSMPrefs::Category& CSVPrefs::PageBase::getCategory() { return mCategory; } void CSVPrefs::PageBase::contextMenuEvent(QContextMenuEvent* e) { QMenu* menu = new QMenu(); menu->addAction("Reset category to default", this, SLOT(resetCategory())); menu->addAction("Reset all to default", this, SLOT(resetAll())); menu->exec(e->globalPos()); delete menu; } void CSVPrefs::PageBase::resetCategory() { CSMPrefs::State::get().resetCategory(getCategory().getKey()); } void CSVPrefs::PageBase::resetAll() { CSMPrefs::State::get().resetAll(); } ================================================ FILE: apps/opencs/view/prefs/pagebase.hpp ================================================ #ifndef CSV_PREFS_PAGEBASE_H #define CSV_PREFS_PAGEBASE_H #include class QContextMenuEvent; namespace CSMPrefs { class Category; } namespace CSVPrefs { class PageBase : public QScrollArea { Q_OBJECT CSMPrefs::Category& mCategory; public: PageBase (CSMPrefs::Category& category, QWidget *parent); CSMPrefs::Category& getCategory(); protected: void contextMenuEvent(QContextMenuEvent*) override; private slots: void resetCategory(); void resetAll(); }; } #endif ================================================ FILE: apps/opencs/view/render/actor.cpp ================================================ #include "actor.hpp" #include #include #include #include #include #include #include #include #include "../../model/world/data.hpp" namespace CSVRender { const std::string Actor::MeshPrefix = "meshes\\"; Actor::Actor(const std::string& id, CSMWorld::Data& data) : mId(id) , mData(data) , mBaseNode(new osg::Group()) , mSkeleton(nullptr) { mActorData = mData.getActorAdapter()->getActorData(mId); connect(mData.getActorAdapter(), SIGNAL(actorChanged(const std::string&)), this, SLOT(handleActorChanged(const std::string&))); } osg::Group* Actor::getBaseNode() { return mBaseNode; } void Actor::update() { mBaseNode->removeChildren(0, mBaseNode->getNumChildren()); // Load skeleton std::string skeletonModel = mActorData->getSkeleton(); skeletonModel = Misc::ResourceHelpers::correctActorModelPath(skeletonModel, mData.getResourceSystem()->getVFS()); loadSkeleton(skeletonModel); if (!mActorData->isCreature()) { // Get rid of the extra attachments SceneUtil::CleanObjectRootVisitor cleanVisitor; mSkeleton->accept(cleanVisitor); cleanVisitor.remove(); // Attach parts to skeleton loadBodyParts(); } else { SceneUtil::RemoveTriBipVisitor removeTriBipVisitor; mSkeleton->accept(removeTriBipVisitor); removeTriBipVisitor.remove(); } // Post setup mSkeleton->markDirty(); mSkeleton->setActive(SceneUtil::Skeleton::Active); } void Actor::handleActorChanged(const std::string& refId) { if (mId == refId) { update(); } } void Actor::loadSkeleton(const std::string& model) { auto sceneMgr = mData.getResourceSystem()->getSceneManager(); osg::ref_ptr temp = sceneMgr->getInstance(model); mSkeleton = dynamic_cast(temp.get()); if (!mSkeleton) { mSkeleton = new SceneUtil::Skeleton(); mSkeleton->addChild(temp); } mBaseNode->addChild(mSkeleton); // Map bone names to bones mNodeMap.clear(); SceneUtil::NodeMapVisitor nmVisitor(mNodeMap); mSkeleton->accept(nmVisitor); } void Actor::loadBodyParts() { for (int i = 0; i < ESM::PRT_Count; ++i) { auto type = (ESM::PartReferenceType) i; std::string partId = mActorData->getPart(type); attachBodyPart(type, getBodyPartMesh(partId)); } } void Actor::attachBodyPart(ESM::PartReferenceType type, const std::string& mesh) { auto sceneMgr = mData.getResourceSystem()->getSceneManager(); // Attach to skeleton std::string boneName = ESM::getBoneName(type); auto node = mNodeMap.find(boneName); if (!mesh.empty() && node != mNodeMap.end()) { auto instance = sceneMgr->getInstance(mesh); SceneUtil::attach(instance, mSkeleton, boneName, node->second); } } std::string Actor::getBodyPartMesh(const std::string& bodyPartId) { const auto& bodyParts = mData.getBodyParts(); int index = bodyParts.searchId(bodyPartId); if (index != -1 && !bodyParts.getRecord(index).isDeleted()) return MeshPrefix + bodyParts.getRecord(index).get().mModel; else return ""; } } ================================================ FILE: apps/opencs/view/render/actor.hpp ================================================ #ifndef OPENCS_VIEW_RENDER_ACTOR_H #define OPENCS_VIEW_RENDER_ACTOR_H #include #include #include #include #include #include "../../model/world/actoradapter.hpp" namespace osg { class Group; } namespace CSMWorld { class Data; } namespace SceneUtil { class Skeleton; } namespace CSVRender { /// Handles loading an npc or creature class Actor : public QObject { Q_OBJECT public: /// Creates an actor. /// \param id The referenceable id /// \param type The record type /// \param data The data store Actor(const std::string& id, CSMWorld::Data& data); /// Retrieves the base node that meshes are attached to osg::Group* getBaseNode(); /// (Re)creates the npc or creature renderable void update(); private slots: void handleActorChanged(const std::string& refId); private: void loadSkeleton(const std::string& model); void loadBodyParts(); void attachBodyPart(ESM::PartReferenceType, const std::string& mesh); std::string getBodyPartMesh(const std::string& bodyPartId); static const std::string MeshPrefix; std::string mId; CSMWorld::Data& mData; CSMWorld::ActorAdapter::ActorDataPtr mActorData; osg::ref_ptr mBaseNode; SceneUtil::Skeleton* mSkeleton; SceneUtil::NodeMapVisitor::NodeMap mNodeMap; }; } #endif ================================================ FILE: apps/opencs/view/render/brushdraw.cpp ================================================ #include "brushdraw.hpp" #include #include #include #include #include #include "../../model/world/cellcoordinates.hpp" #include "../widget/brushshapes.hpp" #include "mask.hpp" CSVRender::BrushDraw::BrushDraw(osg::ref_ptr parentNode, bool textureMode) : mParentNode(parentNode), mTextureMode(textureMode) { mBrushDrawNode = new osg::Group(); mGeometry = new osg::Geometry(); mBrushDrawNode->addChild(mGeometry); mParentNode->addChild(mBrushDrawNode); if (mTextureMode) mLandSizeFactor = static_cast(ESM::Land::REAL_SIZE) / static_cast(ESM::Land::LAND_TEXTURE_SIZE); else mLandSizeFactor = static_cast(ESM::Land::REAL_SIZE) / static_cast(ESM::Land::LAND_SIZE - 1); } CSVRender::BrushDraw::~BrushDraw() { mBrushDrawNode->removeChild(mGeometry); mParentNode->removeChild(mBrushDrawNode); } float CSVRender::BrushDraw::getIntersectionHeight (const osg::Vec3d& point) { osg::Vec3d start = point; osg::Vec3d end = point; start.z() = std::numeric_limits::max(); end.z() = std::numeric_limits::lowest(); osg::Vec3d direction = end - start; // Get intersection osg::ref_ptr intersector (new osgUtil::LineSegmentIntersector( osgUtil::Intersector::MODEL, start, end) ); intersector->setIntersectionLimit(osgUtil::LineSegmentIntersector::NO_LIMIT); osgUtil::IntersectionVisitor visitor(intersector); visitor.setTraversalMask(Mask_Terrain); mParentNode->accept(visitor); for (osgUtil::LineSegmentIntersector::Intersections::iterator it = intersector->getIntersections().begin(); it != intersector->getIntersections().end(); ++it) { osgUtil::LineSegmentIntersector::Intersection intersection = *it; // reject back-facing polygons if (direction * intersection.getWorldIntersectNormal() > 0) { continue; } return intersection.getWorldIntersectPoint().z(); } return 0.0f; } void CSVRender::BrushDraw::buildPointGeometry(const osg::Vec3d& point) { osg::ref_ptr geom (new osg::Geometry()); osg::ref_ptr vertices (new osg::Vec3Array()); osg::ref_ptr colors (new osg::Vec4Array()); const float brushOutlineHeight (1.0f); const float crossHeadSize (8.0f); osg::Vec4f lineColor(1.0f, 1.0f, 1.0f, 0.6f); vertices->push_back(osg::Vec3d( point.x() - crossHeadSize, point.y() - crossHeadSize, getIntersectionHeight(osg::Vec3d( point.x() - crossHeadSize, point.y() - crossHeadSize, point.z()) ) + brushOutlineHeight)); colors->push_back(lineColor); vertices->push_back(osg::Vec3d( point.x() + crossHeadSize, point.y() + crossHeadSize, getIntersectionHeight(osg::Vec3d( point.x() + crossHeadSize, point.y() + crossHeadSize, point.z()) ) + brushOutlineHeight)); colors->push_back(lineColor); vertices->push_back(osg::Vec3d( point.x() + crossHeadSize, point.y() - crossHeadSize, getIntersectionHeight(osg::Vec3d( point.x() + crossHeadSize, point.y() - crossHeadSize, point.z()) ) + brushOutlineHeight)); colors->push_back(lineColor); vertices->push_back(osg::Vec3d( point.x() - crossHeadSize, point.y() + crossHeadSize, getIntersectionHeight(osg::Vec3d( point.x() - crossHeadSize, point.y() + crossHeadSize, point.z()) ) + brushOutlineHeight)); colors->push_back(lineColor); geom->setVertexArray(vertices); geom->setColorArray(colors, osg::Array::BIND_PER_VERTEX); geom->addPrimitiveSet(new osg::DrawArrays(osg::PrimitiveSet::LINES, 0, 4)); mGeometry = geom; } void CSVRender::BrushDraw::buildSquareGeometry(const float& radius, const osg::Vec3d& point) { osg::ref_ptr geom (new osg::Geometry()); osg::ref_ptr vertices (new osg::Vec3Array()); osg::ref_ptr colors (new osg::Vec4Array()); const float brushOutlineHeight (1.0f); float diameter = radius * 2; int resolution = static_cast(2.f * diameter / mLandSizeFactor); //half a vertex resolution float resAdjustedLandSizeFactor = mLandSizeFactor / 2; osg::Vec4f lineColor(1.0f, 1.0f, 1.0f, 0.6f); for (int i = 0; i < resolution; i++) { int step = i * resAdjustedLandSizeFactor; int step2 = (i + 1) * resAdjustedLandSizeFactor; osg::Vec3d upHorizontalLinePoint1( point.x() - radius + step, point.y() - radius, getIntersectionHeight(osg::Vec3d( point.x() - radius + step, point.y() - radius, point.z())) + brushOutlineHeight); osg::Vec3d upHorizontalLinePoint2( point.x() - radius + step2, point.y() - radius, getIntersectionHeight(osg::Vec3d( point.x() - radius + step2, point.y() - radius, point.z())) + brushOutlineHeight); osg::Vec3d upVerticalLinePoint1( point.x() - radius, point.y() - radius + step, getIntersectionHeight(osg::Vec3d( point.x() - radius, point.y() - radius + step, point.z())) + brushOutlineHeight); osg::Vec3d upVerticalLinePoint2( point.x() - radius, point.y() - radius + step2, getIntersectionHeight(osg::Vec3d( point.x() - radius, point.y() - radius + step2, point.z())) + brushOutlineHeight); osg::Vec3d downHorizontalLinePoint1( point.x() + radius - step, point.y() + radius, getIntersectionHeight(osg::Vec3d( point.x() + radius - step, point.y() + radius, point.z())) + brushOutlineHeight); osg::Vec3d downHorizontalLinePoint2( point.x() + radius - step2, point.y() + radius, getIntersectionHeight(osg::Vec3d( point.x() + radius - step2, point.y() + radius, point.z())) + brushOutlineHeight); osg::Vec3d downVerticalLinePoint1( point.x() + radius, point.y() + radius - step, getIntersectionHeight(osg::Vec3d( point.x() + radius, point.y() + radius - step, point.z())) + brushOutlineHeight); osg::Vec3d downVerticalLinePoint2( point.x() + radius, point.y() + radius - step2, getIntersectionHeight(osg::Vec3d( point.x() + radius, point.y() + radius - step2, point.z())) + brushOutlineHeight); vertices->push_back(upHorizontalLinePoint1); colors->push_back(lineColor); vertices->push_back(upHorizontalLinePoint2); colors->push_back(lineColor); vertices->push_back(upVerticalLinePoint1); colors->push_back(lineColor); vertices->push_back(upVerticalLinePoint2); colors->push_back(lineColor); vertices->push_back(downHorizontalLinePoint1); colors->push_back(lineColor); vertices->push_back(downHorizontalLinePoint2); colors->push_back(lineColor); vertices->push_back(downVerticalLinePoint1); colors->push_back(lineColor); vertices->push_back(downVerticalLinePoint2); colors->push_back(lineColor); } geom->setVertexArray(vertices); geom->setColorArray(colors, osg::Array::BIND_PER_VERTEX); geom->addPrimitiveSet(new osg::DrawArrays(osg::PrimitiveSet::LINES, 0, resolution * 8)); mGeometry = geom; } void CSVRender::BrushDraw::buildCircleGeometry(const float& radius, const osg::Vec3d& point) { osg::ref_ptr geom (new osg::Geometry()); osg::ref_ptr vertices (new osg::Vec3Array()); osg::ref_ptr colors (new osg::Vec4Array()); const int amountOfPoints = (osg::PI * 2.0f) * radius / 20; const float step ((osg::PI * 2.0f) / static_cast(amountOfPoints)); const float brushOutlineHeight (1.0f); osg::Vec4f lineColor(1.0f, 1.0f, 1.0f, 0.6f); for (int i = 0; i < amountOfPoints + 2; i++) { float angle (i * step); vertices->push_back(osg::Vec3d( point.x() + radius * cosf(angle), point.y() + radius * sinf(angle), getIntersectionHeight(osg::Vec3d( point.x() + radius * cosf(angle), point.y() + radius * sinf(angle), point.z()) ) + brushOutlineHeight)); colors->push_back(lineColor); angle = static_cast(i + 1) * step; vertices->push_back(osg::Vec3d( point.x() + radius * cosf(angle), point.y() + radius * sinf(angle), getIntersectionHeight(osg::Vec3d( point.x() + radius * cosf(angle), point.y() + radius * sinf(angle), point.z()) ) + brushOutlineHeight)); colors->push_back(lineColor); } geom->setVertexArray(vertices); geom->setColorArray(colors, osg::Array::BIND_PER_VERTEX); geom->addPrimitiveSet(new osg::DrawArrays(osg::PrimitiveSet::LINE_STRIP, 0, amountOfPoints * 2)); mGeometry = geom; } void CSVRender::BrushDraw::buildCustomGeometry(const float& radius, const osg::Vec3d& point) { // Not implemented } void CSVRender::BrushDraw::update(osg::Vec3d point, int brushSize, CSVWidget::BrushShape toolShape) { if (mBrushDrawNode->containsNode(mGeometry)) mBrushDrawNode->removeChild(mGeometry); float radius = (mLandSizeFactor * brushSize) / 2; osg::Vec3d snapToGridPoint = point; if (mTextureMode) { std::pair snapToGridXY = CSMWorld::CellCoordinates::toTextureCoords(point); float offsetToMiddle = mLandSizeFactor * 0.5f; snapToGridPoint = osg::Vec3d( CSMWorld::CellCoordinates::textureGlobalXToWorldCoords(snapToGridXY.first) + offsetToMiddle, CSMWorld::CellCoordinates::textureGlobalYToWorldCoords(snapToGridXY.second) + offsetToMiddle, point.z()); } else { std::pair snapToGridXY = CSMWorld::CellCoordinates::toVertexCoords(point); snapToGridPoint = osg::Vec3d( CSMWorld::CellCoordinates::vertexGlobalToWorldCoords(snapToGridXY.first), CSMWorld::CellCoordinates::vertexGlobalToWorldCoords(snapToGridXY.second), point.z()); } switch (toolShape) { case (CSVWidget::BrushShape_Point) : buildPointGeometry(snapToGridPoint); break; case (CSVWidget::BrushShape_Square) : buildSquareGeometry(radius, snapToGridPoint); break; case (CSVWidget::BrushShape_Circle) : buildCircleGeometry(radius, snapToGridPoint); break; case (CSVWidget::BrushShape_Custom) : buildSquareGeometry(1, snapToGridPoint); //buildCustomGeometry break; } mGeometry->getOrCreateStateSet()->setMode(GL_LIGHTING, osg::StateAttribute::OFF); mBrushDrawNode->addChild(mGeometry); } void CSVRender::BrushDraw::hide() { if (mBrushDrawNode->containsNode(mGeometry)) mBrushDrawNode->removeChild(mGeometry); } ================================================ FILE: apps/opencs/view/render/brushdraw.hpp ================================================ #ifndef CSV_RENDER_BRUSHDRAW_H #define CSV_RENDER_BRUSHDRAW_H #include #include #include #include "../widget/brushshapes.hpp" namespace CSVRender { class BrushDraw { public: BrushDraw(osg::ref_ptr parentNode, bool textureMode = false); ~BrushDraw(); void update(osg::Vec3d point, int brushSize, CSVWidget::BrushShape toolShape); void hide(); private: void buildPointGeometry(const osg::Vec3d& point); void buildSquareGeometry(const float& radius, const osg::Vec3d& point); void buildCircleGeometry(const float& radius, const osg::Vec3d& point); void buildCustomGeometry(const float& radius, const osg::Vec3d& point); float getIntersectionHeight (const osg::Vec3d& point); osg::ref_ptr mParentNode; osg::ref_ptr mBrushDrawNode; osg::ref_ptr mGeometry; bool mTextureMode; float mLandSizeFactor; }; } #endif ================================================ FILE: apps/opencs/view/render/cameracontroller.cpp ================================================ #include "cameracontroller.hpp" #include #include #include #include #include #include #include #include #include #include #include "../../model/prefs/shortcut.hpp" #include "scenewidget.hpp" namespace CSVRender { /* Camera Controller */ const osg::Vec3d CameraController::WorldUp = osg::Vec3d(0, 0, 1); const osg::Vec3d CameraController::LocalUp = osg::Vec3d(0, 1, 0); const osg::Vec3d CameraController::LocalLeft = osg::Vec3d(1, 0, 0); const osg::Vec3d CameraController::LocalForward = osg::Vec3d(0, 0, 1); CameraController::CameraController(QObject* parent) : QObject(parent) , mActive(false) , mInverted(false) , mCameraSensitivity(1/650.f) , mSecondaryMoveMult(50) , mWheelMoveMult(8) , mCamera(nullptr) { } CameraController::~CameraController() { } bool CameraController::isActive() const { return mActive; } osg::Camera* CameraController::getCamera() const { return mCamera; } double CameraController::getCameraSensitivity() const { return mCameraSensitivity; } bool CameraController::getInverted() const { return mInverted; } double CameraController::getSecondaryMovementMultiplier() const { return mSecondaryMoveMult; } double CameraController::getWheelMovementMultiplier() const { return mWheelMoveMult; } void CameraController::setCamera(osg::Camera* camera) { bool wasActive = mActive; mCamera = camera; mActive = (mCamera != nullptr); if (mActive != wasActive) { for (std::vector::iterator it = mShortcuts.begin(); it != mShortcuts.end(); ++it) { CSMPrefs::Shortcut* shortcut = *it; shortcut->enable(mActive); } } } void CameraController::setCameraSensitivity(double value) { mCameraSensitivity = value; } void CameraController::setInverted(bool value) { mInverted = value; } void CameraController::setSecondaryMovementMultiplier(double value) { mSecondaryMoveMult = value; } void CameraController::setWheelMovementMultiplier(double value) { mWheelMoveMult = value; } void CameraController::setup(osg::Group* root, unsigned int mask, const osg::Vec3d& up) { // Find World bounds osg::ComputeBoundsVisitor boundsVisitor; osg::BoundingBox& boundingBox = boundsVisitor.getBoundingBox(); boundsVisitor.setTraversalMask(mask); root->accept(boundsVisitor); if (!boundingBox.valid()) { // Try again without any mask boundsVisitor.reset(); boundsVisitor.setTraversalMask(~0u); root->accept(boundsVisitor); // Last resort, set a default if (!boundingBox.valid()) { boundingBox.set(-1, -1, -1, 1, 1, 1); } } // Calculate a good starting position osg::Vec3d minBounds = boundingBox.corner(0) - boundingBox.center(); osg::Vec3d maxBounds = boundingBox.corner(7) - boundingBox.center(); osg::Vec3d camOffset = up * maxBounds > 0 ? maxBounds : minBounds; camOffset *= 2; osg::Vec3d eye = camOffset + boundingBox.center(); osg::Vec3d center = boundingBox.center(); getCamera()->setViewMatrixAsLookAt(eye, center, up); } void CameraController::addShortcut(CSMPrefs::Shortcut* shortcut) { mShortcuts.push_back(shortcut); } /* Free Camera Controller */ FreeCameraController::FreeCameraController(QWidget* widget) : CameraController(widget) , mLockUpright(false) , mModified(false) , mNaviPrimary(false) , mNaviSecondary(false) , mFast(false) , mFastAlternate(false) , mLeft(false) , mRight(false) , mForward(false) , mBackward(false) , mRollLeft(false) , mRollRight(false) , mUp(LocalUp) , mLinSpeed(1000) , mRotSpeed(osg::PI / 2) , mSpeedMult(8) { CSMPrefs::Shortcut* naviPrimaryShortcut = new CSMPrefs::Shortcut("scene-navi-primary", widget); naviPrimaryShortcut->enable(false); connect(naviPrimaryShortcut, SIGNAL(activated(bool)), this, SLOT(naviPrimary(bool))); addShortcut(naviPrimaryShortcut); CSMPrefs::Shortcut* naviSecondaryShortcut = new CSMPrefs::Shortcut("scene-navi-secondary", widget); naviSecondaryShortcut->enable(false); connect(naviSecondaryShortcut, SIGNAL(activated(bool)), this, SLOT(naviSecondary(bool))); addShortcut(naviSecondaryShortcut); CSMPrefs::Shortcut* forwardShortcut = new CSMPrefs::Shortcut("free-forward", "scene-speed-modifier", CSMPrefs::Shortcut::SM_Detach, widget); forwardShortcut->enable(false); connect(forwardShortcut, SIGNAL(activated(bool)), this, SLOT(forward(bool))); connect(forwardShortcut, SIGNAL(secondary(bool)), this, SLOT(alternateFast(bool))); addShortcut(forwardShortcut); CSMPrefs::Shortcut* leftShortcut = new CSMPrefs::Shortcut("free-left", widget); leftShortcut->enable(false); connect(leftShortcut, SIGNAL(activated(bool)), this, SLOT(left(bool))); addShortcut(leftShortcut); CSMPrefs::Shortcut* backShortcut = new CSMPrefs::Shortcut("free-backward", widget); backShortcut->enable(false); connect(backShortcut, SIGNAL(activated(bool)), this, SLOT(backward(bool))); addShortcut(backShortcut); CSMPrefs::Shortcut* rightShortcut = new CSMPrefs::Shortcut("free-right", widget); rightShortcut->enable(false); connect(rightShortcut, SIGNAL(activated(bool)), this, SLOT(right(bool))); addShortcut(rightShortcut); CSMPrefs::Shortcut* rollLeftShortcut = new CSMPrefs::Shortcut("free-roll-left", widget); rollLeftShortcut->enable(false); connect(rollLeftShortcut, SIGNAL(activated(bool)), this, SLOT(rollLeft(bool))); addShortcut(rollLeftShortcut); CSMPrefs::Shortcut* rollRightShortcut = new CSMPrefs::Shortcut("free-roll-right", widget); rollRightShortcut->enable(false); connect(rollRightShortcut, SIGNAL(activated(bool)), this, SLOT(rollRight(bool))); addShortcut(rollRightShortcut); CSMPrefs::Shortcut* speedModeShortcut = new CSMPrefs::Shortcut("free-speed-mode", widget); speedModeShortcut->enable(false); connect(speedModeShortcut, SIGNAL(activated()), this, SLOT(swapSpeedMode())); addShortcut(speedModeShortcut); } double FreeCameraController::getLinearSpeed() const { return mLinSpeed; } double FreeCameraController::getRotationalSpeed() const { return mRotSpeed; } double FreeCameraController::getSpeedMultiplier() const { return mSpeedMult; } void FreeCameraController::setLinearSpeed(double value) { mLinSpeed = value; } void FreeCameraController::setRotationalSpeed(double value) { mRotSpeed = value; } void FreeCameraController::setSpeedMultiplier(double value) { mSpeedMult = value; } void FreeCameraController::fixUpAxis(const osg::Vec3d& up) { mLockUpright = true; mUp = up; mModified = true; } void FreeCameraController::unfixUpAxis() { mLockUpright = false; } void FreeCameraController::handleMouseMoveEvent(int x, int y) { if (!isActive()) return; if (mNaviPrimary) { double scalar = getCameraSensitivity() * (getInverted() ? -1.0 : 1.0); yaw(x * scalar); pitch(y * scalar); } else if (mNaviSecondary) { osg::Vec3d movement; movement += LocalLeft * -x * getSecondaryMovementMultiplier(); movement += LocalUp * y * getSecondaryMovementMultiplier(); translate(movement); } } void FreeCameraController::handleMouseScrollEvent(int x) { if (!isActive()) return; translate(LocalForward * x * ((mFast ^ mFastAlternate) ? getWheelMovementMultiplier() : 1)); } void FreeCameraController::update(double dt) { if (!isActive()) return; double linDist = mLinSpeed * dt; double rotDist = mRotSpeed * dt; if (mFast ^ mFastAlternate) linDist *= mSpeedMult; if (mLeft) translate(LocalLeft * linDist); if (mRight) translate(LocalLeft * -linDist); if (mForward) translate(LocalForward * linDist); if (mBackward) translate(LocalForward * -linDist); if (!mLockUpright) { if (mRollLeft) roll(-rotDist); if (mRollRight) roll(rotDist); } else if(mModified) { stabilize(); mModified = false; } // Normalize the matrix to counter drift getCamera()->getViewMatrix().orthoNormal(getCamera()->getViewMatrix()); } void FreeCameraController::yaw(double value) { getCamera()->getViewMatrix() *= osg::Matrixd::rotate(value, LocalUp); mModified = true; } void FreeCameraController::pitch(double value) { const double Constraint = osg::PI / 2 - 0.1; if (mLockUpright) { osg::Vec3d eye, center, up; getCamera()->getViewMatrixAsLookAt(eye, center, up); osg::Vec3d forward = center - eye; osg::Vec3d left = up ^ forward; double pitchAngle = std::acos(up * mUp); if ((mUp ^ up) * left < 0) pitchAngle *= -1; if (std::abs(pitchAngle + value) > Constraint) value = (pitchAngle > 0 ? 1 : -1) * Constraint - pitchAngle; } getCamera()->getViewMatrix() *= osg::Matrixd::rotate(value, LocalLeft); mModified = true; } void FreeCameraController::roll(double value) { getCamera()->getViewMatrix() *= osg::Matrixd::rotate(value, LocalForward); mModified = true; } void FreeCameraController::translate(const osg::Vec3d& offset) { getCamera()->getViewMatrix() *= osg::Matrixd::translate(offset); mModified = true; } void FreeCameraController::stabilize() { osg::Vec3d eye, center, up; getCamera()->getViewMatrixAsLookAt(eye, center, up); getCamera()->setViewMatrixAsLookAt(eye, center, mUp); } void FreeCameraController::naviPrimary(bool active) { mNaviPrimary = active; } void FreeCameraController::naviSecondary(bool active) { mNaviSecondary = active; } void FreeCameraController::forward(bool active) { mForward = active; } void FreeCameraController::left(bool active) { mLeft = active; } void FreeCameraController::backward(bool active) { mBackward = active; } void FreeCameraController::right(bool active) { mRight = active; } void FreeCameraController::rollLeft(bool active) { mRollLeft = active; } void FreeCameraController::rollRight(bool active) { mRollRight = active; } void FreeCameraController::alternateFast(bool active) { mFastAlternate = active; } void FreeCameraController::swapSpeedMode() { mFast = !mFast; } /* Orbit Camera Controller */ OrbitCameraController::OrbitCameraController(QWidget* widget) : CameraController(widget) , mInitialized(false) , mNaviPrimary(false) , mNaviSecondary(false) , mFast(false) , mFastAlternate(false) , mLeft(false) , mRight(false) , mUp(false) , mDown(false) , mRollLeft(false) , mRollRight(false) , mPickingMask(~0u) , mCenter(0,0,0) , mDistance(0) , mOrbitSpeed(osg::PI / 4) , mOrbitSpeedMult(4) , mConstRoll(false) { CSMPrefs::Shortcut* naviPrimaryShortcut = new CSMPrefs::Shortcut("scene-navi-primary", widget); naviPrimaryShortcut->enable(false); connect(naviPrimaryShortcut, SIGNAL(activated(bool)), this, SLOT(naviPrimary(bool))); addShortcut(naviPrimaryShortcut); CSMPrefs::Shortcut* naviSecondaryShortcut = new CSMPrefs::Shortcut("scene-navi-secondary", widget); naviSecondaryShortcut->enable(false); connect(naviSecondaryShortcut, SIGNAL(activated(bool)), this, SLOT(naviSecondary(bool))); addShortcut(naviSecondaryShortcut); CSMPrefs::Shortcut* upShortcut = new CSMPrefs::Shortcut("orbit-up", "scene-speed-modifier", CSMPrefs::Shortcut::SM_Detach, widget); upShortcut->enable(false); connect(upShortcut, SIGNAL(activated(bool)), this, SLOT(up(bool))); connect(upShortcut, SIGNAL(secondary(bool)), this, SLOT(alternateFast(bool))); addShortcut(upShortcut); CSMPrefs::Shortcut* leftShortcut = new CSMPrefs::Shortcut("orbit-left", widget); leftShortcut->enable(false); connect(leftShortcut, SIGNAL(activated(bool)), this, SLOT(left(bool))); addShortcut(leftShortcut); CSMPrefs::Shortcut* downShortcut = new CSMPrefs::Shortcut("orbit-down", widget); downShortcut->enable(false); connect(downShortcut, SIGNAL(activated(bool)), this, SLOT(down(bool))); addShortcut(downShortcut); CSMPrefs::Shortcut* rightShortcut = new CSMPrefs::Shortcut("orbit-right", widget); rightShortcut->enable(false); connect(rightShortcut, SIGNAL(activated(bool)), this, SLOT(right(bool))); addShortcut(rightShortcut); CSMPrefs::Shortcut* rollLeftShortcut = new CSMPrefs::Shortcut("orbit-roll-left", widget); rollLeftShortcut->enable(false); connect(rollLeftShortcut, SIGNAL(activated(bool)), this, SLOT(rollLeft(bool))); addShortcut(rollLeftShortcut); CSMPrefs::Shortcut* rollRightShortcut = new CSMPrefs::Shortcut("orbit-roll-right", widget); rollRightShortcut->enable(false); connect(rollRightShortcut, SIGNAL(activated(bool)), this, SLOT(rollRight(bool))); addShortcut(rollRightShortcut); CSMPrefs::Shortcut* speedModeShortcut = new CSMPrefs::Shortcut("orbit-speed-mode", widget); speedModeShortcut->enable(false); connect(speedModeShortcut, SIGNAL(activated()), this, SLOT(swapSpeedMode())); addShortcut(speedModeShortcut); } osg::Vec3d OrbitCameraController::getCenter() const { return mCenter; } double OrbitCameraController::getOrbitSpeed() const { return mOrbitSpeed; } double OrbitCameraController::getOrbitSpeedMultiplier() const { return mOrbitSpeedMult; } unsigned int OrbitCameraController::getPickingMask() const { return mPickingMask; } void OrbitCameraController::setCenter(const osg::Vec3d& value) { osg::Vec3d eye, center, up; getCamera()->getViewMatrixAsLookAt(eye, center, up); mCenter = value; mDistance = (eye - mCenter).length(); getCamera()->setViewMatrixAsLookAt(eye, mCenter, up); mInitialized = true; } void OrbitCameraController::setOrbitSpeed(double value) { mOrbitSpeed = value; } void OrbitCameraController::setOrbitSpeedMultiplier(double value) { mOrbitSpeedMult = value; } void OrbitCameraController::setPickingMask(unsigned int value) { mPickingMask = value; } void OrbitCameraController::handleMouseMoveEvent(int x, int y) { if (!isActive()) return; if (!mInitialized) initialize(); if (mNaviPrimary) { double scalar = getCameraSensitivity() * (getInverted() ? -1.0 : 1.0); rotateHorizontal(x * scalar); rotateVertical(-y * scalar); } else if (mNaviSecondary) { osg::Vec3d movement; movement += LocalLeft * x * getSecondaryMovementMultiplier(); movement += LocalUp * -y * getSecondaryMovementMultiplier(); translate(movement); } } void OrbitCameraController::handleMouseScrollEvent(int x) { if (!isActive()) return; zoom(-x * ((mFast ^ mFastAlternate) ? getWheelMovementMultiplier() : 1)); } void OrbitCameraController::update(double dt) { if (!isActive()) return; if (!mInitialized) initialize(); double rotDist = mOrbitSpeed * dt; if (mFast ^ mFastAlternate) rotDist *= mOrbitSpeedMult; if (mLeft) rotateHorizontal(-rotDist); if (mRight) rotateHorizontal(rotDist); if (mUp) rotateVertical(rotDist); if (mDown) rotateVertical(-rotDist); if (mRollLeft) roll(-rotDist); if (mRollRight) roll(rotDist); // Normalize the matrix to counter drift getCamera()->getViewMatrix().orthoNormal(getCamera()->getViewMatrix()); } void OrbitCameraController::reset() { mInitialized = false; } void OrbitCameraController::initialize() { static const int DefaultStartDistance = 10000.f; // Try to intelligently pick focus object osg::ref_ptr intersector (new osgUtil::LineSegmentIntersector( osgUtil::Intersector::PROJECTION, osg::Vec3d(0, 0, 0), LocalForward)); intersector->setIntersectionLimit(osgUtil::LineSegmentIntersector::LIMIT_NEAREST); osgUtil::IntersectionVisitor visitor(intersector); visitor.setTraversalMask(mPickingMask); getCamera()->accept(visitor); osg::Vec3d eye, center, up; getCamera()->getViewMatrixAsLookAt(eye, center, up, DefaultStartDistance); if (intersector->getIntersections().begin() != intersector->getIntersections().end()) { mCenter = intersector->getIntersections().begin()->getWorldIntersectPoint(); mDistance = (eye - mCenter).length(); } else { mCenter = center; mDistance = DefaultStartDistance; } mInitialized = true; } void OrbitCameraController::setConstRoll(bool enabled) { mConstRoll = enabled; } void OrbitCameraController::rotateHorizontal(double value) { osg::Vec3d eye, center, up; getCamera()->getViewMatrixAsLookAt(eye, center, up); osg::Vec3d absoluteUp = osg::Vec3(0,0,1); osg::Quat rotation = osg::Quat(value, mConstRoll ? absoluteUp : up); osg::Vec3d oldOffset = eye - mCenter; osg::Vec3d newOffset = rotation * oldOffset; if (mConstRoll) up = rotation * up; getCamera()->setViewMatrixAsLookAt(mCenter + newOffset, mCenter, up); } void OrbitCameraController::rotateVertical(double value) { osg::Vec3d eye, center, up; getCamera()->getViewMatrixAsLookAt(eye, center, up); osg::Vec3d forward = center - eye; osg::Vec3d axis = up ^ forward; osg::Quat rotation = osg::Quat(value,axis); osg::Vec3d oldOffset = eye - mCenter; osg::Vec3d newOffset = rotation * oldOffset; if (mConstRoll) up = rotation * up; getCamera()->setViewMatrixAsLookAt(mCenter + newOffset, mCenter, up); } void OrbitCameraController::roll(double value) { getCamera()->getViewMatrix() *= osg::Matrixd::rotate(value, LocalForward); } void OrbitCameraController::translate(const osg::Vec3d& offset) { osg::Vec3d eye, center, up; getCamera()->getViewMatrixAsLookAt(eye, center, up); osg::Vec3d newOffset = getCamera()->getViewMatrix().getRotate().inverse() * offset; mCenter += newOffset; eye += newOffset; getCamera()->setViewMatrixAsLookAt(eye, mCenter, up); } void OrbitCameraController::zoom(double value) { mDistance = std::max(10., mDistance + value); osg::Vec3d eye, center, up; getCamera()->getViewMatrixAsLookAt(eye, center, up, 1.f); osg::Vec3d offset = (eye - center) * mDistance; getCamera()->setViewMatrixAsLookAt(mCenter + offset, mCenter, up); } void OrbitCameraController::naviPrimary(bool active) { mNaviPrimary = active; } void OrbitCameraController::naviSecondary(bool active) { mNaviSecondary = active; } void OrbitCameraController::up(bool active) { mUp = active; } void OrbitCameraController::left(bool active) { mLeft = active; } void OrbitCameraController::down(bool active) { mDown = active; } void OrbitCameraController::right(bool active) { mRight = active; } void OrbitCameraController::rollLeft(bool active) { if (isActive()) mRollLeft = active; } void OrbitCameraController::rollRight(bool active) { mRollRight = active; } void OrbitCameraController::alternateFast(bool active) { mFastAlternate = active; } void OrbitCameraController::swapSpeedMode() { mFast = !mFast; } } ================================================ FILE: apps/opencs/view/render/cameracontroller.hpp ================================================ #ifndef OPENCS_VIEW_CAMERACONTROLLER_H #define OPENCS_VIEW_CAMERACONTROLLER_H #include #include #include #include #include namespace osg { class Camera; class Group; } namespace CSMPrefs { class Shortcut; } namespace CSVRender { class SceneWidget; class CameraController : public QObject { Q_OBJECT public: static const osg::Vec3d WorldUp; static const osg::Vec3d LocalUp; static const osg::Vec3d LocalLeft; static const osg::Vec3d LocalForward; CameraController(QObject* parent); virtual ~CameraController(); bool isActive() const; osg::Camera* getCamera() const; double getCameraSensitivity() const; bool getInverted() const; double getSecondaryMovementMultiplier() const; double getWheelMovementMultiplier() const; void setCamera(osg::Camera*); void setCameraSensitivity(double value); void setInverted(bool value); void setSecondaryMovementMultiplier(double value); void setWheelMovementMultiplier(double value); // moves the camera to an intelligent position void setup(osg::Group* root, unsigned int mask, const osg::Vec3d& up); virtual void handleMouseMoveEvent(int x, int y) = 0; virtual void handleMouseScrollEvent(int x) = 0; virtual void update(double dt) = 0; protected: void addShortcut(CSMPrefs::Shortcut* shortcut); private: bool mActive, mInverted; double mCameraSensitivity; double mSecondaryMoveMult; double mWheelMoveMult; osg::Camera* mCamera; std::vector mShortcuts; }; class FreeCameraController : public CameraController { Q_OBJECT public: FreeCameraController(QWidget* parent); double getLinearSpeed() const; double getRotationalSpeed() const; double getSpeedMultiplier() const; void setLinearSpeed(double value); void setRotationalSpeed(double value); void setSpeedMultiplier(double value); void fixUpAxis(const osg::Vec3d& up); void unfixUpAxis(); void handleMouseMoveEvent(int x, int y) override; void handleMouseScrollEvent(int x) override; void update(double dt) override; private: void yaw(double value); void pitch(double value); void roll(double value); void translate(const osg::Vec3d& offset); void stabilize(); bool mLockUpright, mModified; bool mNaviPrimary, mNaviSecondary; bool mFast, mFastAlternate; bool mLeft, mRight, mForward, mBackward, mRollLeft, mRollRight; osg::Vec3d mUp; double mLinSpeed; double mRotSpeed; double mSpeedMult; private slots: void naviPrimary(bool active); void naviSecondary(bool active); void forward(bool active); void left(bool active); void backward(bool active); void right(bool active); void rollLeft(bool active); void rollRight(bool active); void alternateFast(bool active); void swapSpeedMode(); }; class OrbitCameraController : public CameraController { Q_OBJECT public: OrbitCameraController(QWidget* parent); osg::Vec3d getCenter() const; double getOrbitSpeed() const; double getOrbitSpeedMultiplier() const; unsigned int getPickingMask() const; void setCenter(const osg::Vec3d& center); void setOrbitSpeed(double value); void setOrbitSpeedMultiplier(double value); void setPickingMask(unsigned int value); void handleMouseMoveEvent(int x, int y) override; void handleMouseScrollEvent(int x) override; void update(double dt) override; /// \brief Flag controller to be re-initialized. void reset(); void setConstRoll(bool enable); private: void initialize(); void rotateHorizontal(double value); void rotateVertical(double value); void roll(double value); void translate(const osg::Vec3d& offset); void zoom(double value); bool mInitialized; bool mNaviPrimary, mNaviSecondary; bool mFast, mFastAlternate; bool mLeft, mRight, mUp, mDown, mRollLeft, mRollRight; unsigned int mPickingMask; osg::Vec3d mCenter; double mDistance; double mOrbitSpeed; double mOrbitSpeedMult; bool mConstRoll; private slots: void naviPrimary(bool active); void naviSecondary(bool active); void up(bool active); void left(bool active); void down(bool active); void right(bool active); void rollLeft(bool active); void rollRight(bool active); void alternateFast(bool active); void swapSpeedMode(); }; } #endif ================================================ FILE: apps/opencs/view/render/cell.cpp ================================================ #include "cell.hpp" #include #include #include #include #include #include #include #include #include #include #include "../../model/world/idtable.hpp" #include "../../model/world/columns.hpp" #include "../../model/world/data.hpp" #include "../../model/world/refcollection.hpp" #include "../../model/world/cellcoordinates.hpp" #include "cellwater.hpp" #include "cellborder.hpp" #include "cellarrow.hpp" #include "cellmarker.hpp" #include "mask.hpp" #include "pathgrid.hpp" #include "terrainstorage.hpp" #include "object.hpp" #include "instancedragmodes.hpp" namespace CSVRender { class CellNodeContainer : public osg::Referenced { public: CellNodeContainer(Cell* cell) : mCell(cell) {} Cell* getCell(){ return mCell; } private: Cell* mCell; }; class CellNodeCallback : public osg::NodeCallback { public: void operator()(osg::Node* node, osg::NodeVisitor* nv) override { traverse(node, nv); CellNodeContainer* container = static_cast(node->getUserData()); container->getCell()->updateLand(); } }; } bool CSVRender::Cell::removeObject (const std::string& id) { std::map::iterator iter = mObjects.find (Misc::StringUtils::lowerCase (id)); if (iter==mObjects.end()) return false; removeObject (iter); return true; } std::map::iterator CSVRender::Cell::removeObject ( std::map::iterator iter) { delete iter->second; mObjects.erase (iter++); return iter; } bool CSVRender::Cell::addObjects (int start, int end) { bool modified = false; const CSMWorld::RefCollection& collection = mData.getReferences(); for (int i=start; i<=end; ++i) { std::string cell = Misc::StringUtils::lowerCase (collection.getRecord (i).get().mCell); CSMWorld::RecordBase::State state = collection.getRecord (i).mState; if (cell==mId && state!=CSMWorld::RecordBase::State_Deleted) { std::string id = Misc::StringUtils::lowerCase (collection.getRecord (i).get().mId); std::unique_ptr object (new Object (mData, mCellNode, id, false)); if (mSubModeElementMask & Mask_Reference) object->setSubMode (mSubMode); mObjects.insert (std::make_pair (id, object.release())); modified = true; } } return modified; } void CSVRender::Cell::updateLand() { if (!mUpdateLand || mLandDeleted) return; mUpdateLand = false; // Cell is deleted if (mDeleted) { unloadLand(); return; } // Setup land if available const CSMWorld::IdCollection& land = mData.getLand(); int landIndex = land.searchId(mId); if (landIndex != -1 && !land.getRecord(mId).isDeleted()) { const ESM::Land& esmLand = land.getRecord(mId).get(); if (esmLand.getLandData (ESM::Land::DATA_VHGT)) { if (mTerrain) { mTerrain->unloadCell(mCoordinates.getX(), mCoordinates.getY()); mTerrain->clearAssociatedCaches(); } else { mTerrain.reset(new Terrain::TerrainGrid(mCellNode, mCellNode, mData.getResourceSystem().get(), mTerrainStorage, Mask_Terrain)); } mTerrain->loadCell(esmLand.mX, esmLand.mY); if (!mCellBorder) mCellBorder.reset(new CellBorder(mCellNode, mCoordinates)); mCellBorder->buildShape(esmLand); return; } } // No land data unloadLand(); } void CSVRender::Cell::unloadLand() { if (mTerrain) mTerrain->unloadCell(mCoordinates.getX(), mCoordinates.getY()); if (mCellBorder) mCellBorder.reset(); } CSVRender::Cell::Cell (CSMWorld::Data& data, osg::Group* rootNode, const std::string& id, bool deleted) : mData (data), mId (Misc::StringUtils::lowerCase (id)), mDeleted (deleted), mSubMode (0), mSubModeElementMask (0), mUpdateLand(true), mLandDeleted(false) { std::pair result = CSMWorld::CellCoordinates::fromId (id); mTerrainStorage = new TerrainStorage(mData); if (result.second) mCoordinates = result.first; mCellNode = new osg::Group; mCellNode->setUserData(new CellNodeContainer(this)); mCellNode->setUpdateCallback(new CellNodeCallback); rootNode->addChild(mCellNode); setCellMarker(); if (!mDeleted) { CSMWorld::IdTable& references = dynamic_cast ( *mData.getTableModel (CSMWorld::UniversalId::Type_References)); int rows = references.rowCount(); addObjects (0, rows-1); updateLand(); mPathgrid.reset(new Pathgrid(mData, mCellNode, mId, mCoordinates)); mCellWater.reset(new CellWater(mData, mCellNode, mId, mCoordinates)); } } CSVRender::Cell::~Cell() { for (std::map::iterator iter (mObjects.begin()); iter!=mObjects.end(); ++iter) delete iter->second; mCellNode->getParent(0)->removeChild(mCellNode); } CSVRender::Pathgrid* CSVRender::Cell::getPathgrid() const { return mPathgrid.get(); } bool CSVRender::Cell::referenceableDataChanged (const QModelIndex& topLeft, const QModelIndex& bottomRight) { bool modified = false; for (std::map::iterator iter (mObjects.begin()); iter!=mObjects.end(); ++iter) if (iter->second->referenceableDataChanged (topLeft, bottomRight)) modified = true; return modified; } bool CSVRender::Cell::referenceableAboutToBeRemoved (const QModelIndex& parent, int start, int end) { if (parent.isValid()) return false; bool modified = false; for (std::map::iterator iter (mObjects.begin()); iter!=mObjects.end(); ++iter) if (iter->second->referenceableAboutToBeRemoved (parent, start, end)) modified = true; return modified; } bool CSVRender::Cell::referenceDataChanged (const QModelIndex& topLeft, const QModelIndex& bottomRight) { if (mDeleted) return false; CSMWorld::IdTable& references = dynamic_cast ( *mData.getTableModel (CSMWorld::UniversalId::Type_References)); int idColumn = references.findColumnIndex (CSMWorld::Columns::ColumnId_Id); int cellColumn = references.findColumnIndex (CSMWorld::Columns::ColumnId_Cell); int stateColumn = references.findColumnIndex (CSMWorld::Columns::ColumnId_Modification); // list IDs in cell std::map ids; // id, deleted state for (int i=topLeft.row(); i<=bottomRight.row(); ++i) { std::string cell = Misc::StringUtils::lowerCase (references.data ( references.index (i, cellColumn)).toString().toUtf8().constData()); if (cell==mId) { std::string id = Misc::StringUtils::lowerCase (references.data ( references.index (i, idColumn)).toString().toUtf8().constData()); int state = references.data (references.index (i, stateColumn)).toInt(); ids.insert (std::make_pair (id, state==CSMWorld::RecordBase::State_Deleted)); } } // perform update and remove where needed bool modified = false; std::map::iterator iter = mObjects.begin(); while (iter!=mObjects.end()) { if (iter->second->referenceDataChanged (topLeft, bottomRight)) modified = true; std::map::iterator iter2 = ids.find (iter->first); if (iter2!=ids.end()) { bool deleted = iter2->second; ids.erase (iter2); if (deleted) { iter = removeObject (iter); modified = true; continue; } } ++iter; } // add new objects for (std::map::iterator mapIter (ids.begin()); mapIter!=ids.end(); ++mapIter) { if (!mapIter->second) { mObjects.insert (std::make_pair ( mapIter->first, new Object (mData, mCellNode, mapIter->first, false))); modified = true; } } return modified; } bool CSVRender::Cell::referenceAboutToBeRemoved (const QModelIndex& parent, int start, int end) { if (parent.isValid()) return false; if (mDeleted) return false; CSMWorld::IdTable& references = dynamic_cast ( *mData.getTableModel (CSMWorld::UniversalId::Type_References)); int idColumn = references.findColumnIndex (CSMWorld::Columns::ColumnId_Id); bool modified = false; for (int row = start; row<=end; ++row) if (removeObject (references.data ( references.index (row, idColumn)).toString().toUtf8().constData())) modified = true; return modified; } bool CSVRender::Cell::referenceAdded (const QModelIndex& parent, int start, int end) { if (parent.isValid()) return false; if (mDeleted) return false; return addObjects (start, end); } void CSVRender::Cell::setAlteredHeight(int inCellX, int inCellY, float height) { mTerrainStorage->setAlteredHeight(inCellX, inCellY, height); mUpdateLand = true; } float CSVRender::Cell::getSumOfAlteredAndTrueHeight(int cellX, int cellY, int inCellX, int inCellY) { return mTerrainStorage->getSumOfAlteredAndTrueHeight(cellX, cellY, inCellX, inCellY); } float* CSVRender::Cell::getAlteredHeight(int inCellX, int inCellY) { return mTerrainStorage->getAlteredHeight(inCellX, inCellY); } void CSVRender::Cell::resetAlteredHeights() { mTerrainStorage->resetHeights(); mUpdateLand = true; } void CSVRender::Cell::pathgridModified() { if (mPathgrid) mPathgrid->recreateGeometry(); } void CSVRender::Cell::pathgridRemoved() { if (mPathgrid) mPathgrid->removeGeometry(); } void CSVRender::Cell::landDataChanged (const QModelIndex& topLeft, const QModelIndex& bottomRight) { mUpdateLand = true; } void CSVRender::Cell::landAboutToBeRemoved (const QModelIndex& parent, int start, int end) { mLandDeleted = true; unloadLand(); } void CSVRender::Cell::landAdded (const QModelIndex& parent, int start, int end) { mUpdateLand = true; mLandDeleted = false; } void CSVRender::Cell::landTextureChanged (const QModelIndex& topLeft, const QModelIndex& bottomRight) { mUpdateLand = true; } void CSVRender::Cell::landTextureAboutToBeRemoved (const QModelIndex& parent, int start, int end) { mUpdateLand = true; } void CSVRender::Cell::landTextureAdded (const QModelIndex& parent, int start, int end) { mUpdateLand = true; } void CSVRender::Cell::reloadAssets() { for (std::map::const_iterator iter (mObjects.begin()); iter != mObjects.end(); ++iter) { iter->second->reloadAssets(); } if (mTerrain) { mTerrain->unloadCell(mCoordinates.getX(), mCoordinates.getY()); mTerrain->loadCell(mCoordinates.getX(), mCoordinates.getY()); } if (mCellWater) mCellWater->reloadAssets(); } void CSVRender::Cell::setSelection (int elementMask, Selection mode) { if (elementMask & Mask_Reference) { for (std::map::const_iterator iter (mObjects.begin()); iter!=mObjects.end(); ++iter) { bool selected = false; switch (mode) { case Selection_Clear: selected = false; break; case Selection_All: selected = true; break; case Selection_Invert: selected = !iter->second->getSelected(); break; } iter->second->setSelected (selected); } } if (mPathgrid && elementMask & Mask_Pathgrid) { // Only one pathgrid may be selected, so some operations will only have an effect // if the pathgrid is already focused switch (mode) { case Selection_Clear: mPathgrid->clearSelected(); break; case Selection_All: if (mPathgrid->isSelected()) mPathgrid->selectAll(); break; case Selection_Invert: if (mPathgrid->isSelected()) mPathgrid->invertSelected(); break; } } } void CSVRender::Cell::selectAllWithSameParentId (int elementMask) { std::set ids; for (std::map::const_iterator iter (mObjects.begin()); iter!=mObjects.end(); ++iter) { if (iter->second->getSelected()) ids.insert (iter->second->getReferenceableId()); } for (std::map::const_iterator iter (mObjects.begin()); iter!=mObjects.end(); ++iter) { if (!iter->second->getSelected() && ids.find (iter->second->getReferenceableId())!=ids.end()) { iter->second->setSelected (true); } } } void CSVRender::Cell::handleSelectDrag(Object* object, DragMode dragMode) { if (dragMode == DragMode_Select_Only || dragMode == DragMode_Select_Add) object->setSelected(true); else if (dragMode == DragMode_Select_Remove) object->setSelected(false); else if (dragMode == DragMode_Select_Invert) object->setSelected (!object->getSelected()); } void CSVRender::Cell::selectInsideCube(const osg::Vec3d& pointA, const osg::Vec3d& pointB, DragMode dragMode) { for (auto& object : mObjects) { if (dragMode == DragMode_Select_Only) object.second->setSelected (false); if ( ( object.second->getPosition().pos[0] > pointA[0] && object.second->getPosition().pos[0] < pointB[0] ) || ( object.second->getPosition().pos[0] > pointB[0] && object.second->getPosition().pos[0] < pointA[0] )) { if ( ( object.second->getPosition().pos[1] > pointA[1] && object.second->getPosition().pos[1] < pointB[1] ) || ( object.second->getPosition().pos[1] > pointB[1] && object.second->getPosition().pos[1] < pointA[1] )) { if ( ( object.second->getPosition().pos[2] > pointA[2] && object.second->getPosition().pos[2] < pointB[2] ) || ( object.second->getPosition().pos[2] > pointB[2] && object.second->getPosition().pos[2] < pointA[2] )) handleSelectDrag(object.second, dragMode); } } } } void CSVRender::Cell::selectWithinDistance(const osg::Vec3d& point, float distance, DragMode dragMode) { for (auto& object : mObjects) { if (dragMode == DragMode_Select_Only) object.second->setSelected (false); float distanceFromObject = (point - object.second->getPosition().asVec3()).length(); if (distanceFromObject < distance) handleSelectDrag(object.second, dragMode); } } void CSVRender::Cell::setCellArrows (int mask) { for (int i=0; i<4; ++i) { CellArrow::Direction direction = static_cast (1< -1) { const CSMWorld::Record& cellRecord = mData.getCells().getRecord(cellIndex); cellExists = !cellRecord.isDeleted(); isInteriorCell = cellRecord.get().mData.mFlags & ESM::Cell::Interior; } if (!isInteriorCell) { mCellMarker.reset(new CellMarker(mCellNode, mCoordinates, cellExists)); } } CSMWorld::CellCoordinates CSVRender::Cell::getCoordinates() const { return mCoordinates; } bool CSVRender::Cell::isDeleted() const { return mDeleted; } std::vector > CSVRender::Cell::getSelection (unsigned int elementMask) const { std::vector > result; if (elementMask & Mask_Reference) for (std::map::const_iterator iter (mObjects.begin()); iter!=mObjects.end(); ++iter) if (iter->second->getSelected()) result.push_back (iter->second->getTag()); if (mPathgrid && elementMask & Mask_Pathgrid) if (mPathgrid->isSelected()) result.emplace_back(mPathgrid->getTag()); return result; } std::vector > CSVRender::Cell::getEdited (unsigned int elementMask) const { std::vector > result; if (elementMask & Mask_Reference) for (std::map::const_iterator iter (mObjects.begin()); iter!=mObjects.end(); ++iter) if (iter->second->isEdited()) result.push_back (iter->second->getTag()); return result; } void CSVRender::Cell::setSubMode (int subMode, unsigned int elementMask) { mSubMode = subMode; mSubModeElementMask = elementMask; if (elementMask & Mask_Reference) for (std::map::const_iterator iter (mObjects.begin()); iter!=mObjects.end(); ++iter) iter->second->setSubMode (subMode); } void CSVRender::Cell::reset (unsigned int elementMask) { if (elementMask & Mask_Reference) for (std::map::const_iterator iter (mObjects.begin()); iter!=mObjects.end(); ++iter) iter->second->reset(); if (mPathgrid && elementMask & Mask_Pathgrid) mPathgrid->resetIndicators(); } ================================================ FILE: apps/opencs/view/render/cell.hpp ================================================ #ifndef OPENCS_VIEW_CELL_H #define OPENCS_VIEW_CELL_H #include #include #include #include #include #include "../../model/world/cellcoordinates.hpp" #include "terrainstorage.hpp" #include "instancedragmodes.hpp" class QModelIndex; namespace osg { class Group; class Geometry; class Geode; } namespace CSMWorld { class Data; } namespace Terrain { class TerrainGrid; } namespace CSVRender { class CellWater; class Pathgrid; class TagBase; class Object; class CellArrow; class CellBorder; class CellMarker; class CellWater; class Cell { CSMWorld::Data& mData; std::string mId; osg::ref_ptr mCellNode; std::map mObjects; std::unique_ptr mTerrain; CSMWorld::CellCoordinates mCoordinates; std::unique_ptr mCellArrows[4]; std::unique_ptr mCellMarker; std::unique_ptr mCellBorder; std::unique_ptr mCellWater; std::unique_ptr mPathgrid; bool mDeleted; int mSubMode; unsigned int mSubModeElementMask; bool mUpdateLand, mLandDeleted; TerrainStorage *mTerrainStorage; /// Ignored if cell does not have an object with the given ID. /// /// \return Was the object deleted? bool removeObject (const std::string& id); // Remove object and return iterator to next object. std::map::iterator removeObject ( std::map::iterator iter); /// Add objects from reference table that are within this cell. /// /// \return Have any objects been added? bool addObjects (int start, int end); void updateLand(); void unloadLand(); public: enum Selection { Selection_Clear, Selection_All, Selection_Invert }; public: /// \note Deleted covers both cells that are deleted and cells that don't exist in /// the first place. Cell (CSMWorld::Data& data, osg::Group* rootNode, const std::string& id, bool deleted = false); ~Cell(); /// \note Returns the pathgrid representation which will exist as long as the cell exists Pathgrid* getPathgrid() const; /// \return Did this call result in a modification of the visual representation of /// this cell? bool referenceableDataChanged (const QModelIndex& topLeft, const QModelIndex& bottomRight); /// \return Did this call result in a modification of the visual representation of /// this cell? bool referenceableAboutToBeRemoved (const QModelIndex& parent, int start, int end); /// \return Did this call result in a modification of the visual representation of /// this cell? bool referenceDataChanged (const QModelIndex& topLeft, const QModelIndex& bottomRight); /// \return Did this call result in a modification of the visual representation of /// this cell? bool referenceAboutToBeRemoved (const QModelIndex& parent, int start, int end); /// \return Did this call result in a modification of the visual representation of /// this cell? bool referenceAdded (const QModelIndex& parent, int start, int end); void setAlteredHeight(int inCellX, int inCellY, float height); float getSumOfAlteredAndTrueHeight(int cellX, int cellY, int inCellX, int inCellY); float* getAlteredHeight(int inCellX, int inCellY); void resetAlteredHeights(); void pathgridModified(); void pathgridRemoved(); void landDataChanged (const QModelIndex& topLeft, const QModelIndex& bottomRight); void landAboutToBeRemoved (const QModelIndex& parent, int start, int end); void landAdded (const QModelIndex& parent, int start, int end); void landTextureChanged (const QModelIndex& topLeft, const QModelIndex& bottomRight); void landTextureAboutToBeRemoved (const QModelIndex& parent, int start, int end); void landTextureAdded (const QModelIndex& parent, int start, int end); void reloadAssets(); void setSelection (int elementMask, Selection mode); // Select everything that references the same ID as at least one of the elements // already selected void selectAllWithSameParentId (int elementMask); void handleSelectDrag(Object* object, DragMode dragMode); void selectInsideCube(const osg::Vec3d& pointA, const osg::Vec3d& pointB, DragMode dragMode); void selectWithinDistance(const osg::Vec3d& pointA, float distance, DragMode dragMode); void setCellArrows (int mask); /// \brief Set marker for this cell. void setCellMarker(); /// Returns 0, 0 in case of an unpaged cell. CSMWorld::CellCoordinates getCoordinates() const; bool isDeleted() const; std::vector > getSelection (unsigned int elementMask) const; std::vector > getEdited (unsigned int elementMask) const; void setSubMode (int subMode, unsigned int elementMask); /// Erase all overrides and restore the visual representation of the cell to its /// true state. void reset (unsigned int elementMask); friend class CellNodeCallback; }; } #endif ================================================ FILE: apps/opencs/view/render/cellarrow.cpp ================================================ #include "cellarrow.hpp" #include #include #include #include #include #include "../../model/prefs/state.hpp" #include "../../model/prefs/shortcutmanager.hpp" #include #include "mask.hpp" CSVRender::CellArrowTag::CellArrowTag (CellArrow *arrow) : TagBase (Mask_CellArrow), mArrow (arrow) {} CSVRender::CellArrow *CSVRender::CellArrowTag::getCellArrow() const { return mArrow; } QString CSVRender::CellArrowTag::getToolTip (bool hideBasics) const { QString text ("Direction: "); switch (mArrow->getDirection()) { case CellArrow::Direction_North: text += "North"; break; case CellArrow::Direction_West: text += "West"; break; case CellArrow::Direction_South: text += "South"; break; case CellArrow::Direction_East: text += "East"; break; } if (!hideBasics) { text += "

" "Modify which cells are shown" "

  • {scene-edit-primary}: Add cell in given direction
  • " "
  • {scene-edit-secondary}: Add cell and remove old cell
  • " "
  • {scene-select-primary}: Add cells in given direction
  • " "
  • {scene-select-secondary}: Add cells and remove old cells
  • " "
  • {scene-load-cam-cell}: Load cell where camera is located
  • " "
  • {scene-load-cam-eastcell}: Load cell to east
  • " "
  • {scene-load-cam-northcell}: Load cell to north
  • " "
  • {scene-load-cam-westcell}: Load cell to west
  • " "
  • {scene-load-cam-southcell}: Load cell to south
  • " "
"; } return CSMPrefs::State::get().getShortcutManager().processToolTip(text); } void CSVRender::CellArrow::adjustTransform() { // position const int cellSize = Constants::CellSizeInUnits; const int offset = cellSize / 2 + 800; int x = mCoordinates.getX()*cellSize + cellSize/2; int y = mCoordinates.getY()*cellSize + cellSize/2; float xr = 0; float yr = 0; float zr = 0; float angle = osg::DegreesToRadians (90.0f); switch (mDirection) { case Direction_North: y += offset; xr = -angle; zr = angle; break; case Direction_West: x -= offset; yr = -angle; break; case Direction_South: y -= offset; xr = angle; zr = angle; break; case Direction_East: x += offset; yr = angle; break; }; mBaseNode->setPosition (osg::Vec3f (x, y, 0)); // orientation osg::Quat xr2 (xr, osg::Vec3f (1,0,0)); osg::Quat yr2 (yr, osg::Vec3f (0,1,0)); osg::Quat zr2 (zr, osg::Vec3f (0,0,1)); mBaseNode->setAttitude (zr2*yr2*xr2); } void CSVRender::CellArrow::buildShape() { osg::ref_ptr geometry (new osg::Geometry); const int arrowWidth = 4000; const int arrowLength = 1500; const int arrowHeight = 500; osg::Vec3Array *vertices = new osg::Vec3Array; for (int i2=0; i2<2; ++i2) for (int i=0; i<2; ++i) { float height = i ? -arrowHeight/2 : arrowHeight/2; vertices->push_back (osg::Vec3f (height, -arrowWidth/2, 0)); vertices->push_back (osg::Vec3f (height, arrowWidth/2, 0)); vertices->push_back (osg::Vec3f (height, 0, arrowLength)); } geometry->setVertexArray (vertices); osg::DrawElementsUShort *primitives = new osg::DrawElementsUShort (osg::PrimitiveSet::TRIANGLES, 0); // top primitives->push_back (0); primitives->push_back (1); primitives->push_back (2); // bottom primitives->push_back (5); primitives->push_back (4); primitives->push_back (3); // back primitives->push_back (3+6); primitives->push_back (4+6); primitives->push_back (1+6); primitives->push_back (3+6); primitives->push_back (1+6); primitives->push_back (0+6); // sides primitives->push_back (0+6); primitives->push_back (2+6); primitives->push_back (5+6); primitives->push_back (0+6); primitives->push_back (5+6); primitives->push_back (3+6); primitives->push_back (4+6); primitives->push_back (5+6); primitives->push_back (2+6); primitives->push_back (4+6); primitives->push_back (2+6); primitives->push_back (1+6); geometry->addPrimitiveSet (primitives); osg::Vec4Array *colours = new osg::Vec4Array; for (int i=0; i<6; ++i) colours->push_back (osg::Vec4f (0.11f, 0.6f, 0.95f, 1.0f)); for (int i=0; i<6; ++i) colours->push_back (osg::Vec4f (0.08f, 0.44f, 0.7f, 1.0f)); geometry->setColorArray (colours, osg::Array::BIND_PER_VERTEX); geometry->getOrCreateStateSet()->setMode (GL_LIGHTING, osg::StateAttribute::OFF); osg::ref_ptr geode (new osg::Geode); geode->addDrawable (geometry); mBaseNode->addChild (geode); } CSVRender::CellArrow::CellArrow (osg::Group *cellNode, Direction direction, const CSMWorld::CellCoordinates& coordinates) : mDirection (direction), mParentNode (cellNode), mCoordinates (coordinates) { mBaseNode = new osg::PositionAttitudeTransform; mBaseNode->setUserData (new CellArrowTag (this)); mParentNode->addChild (mBaseNode); mBaseNode->setNodeMask (Mask_CellArrow); adjustTransform(); buildShape(); } CSVRender::CellArrow::~CellArrow() { mParentNode->removeChild (mBaseNode); } CSMWorld::CellCoordinates CSVRender::CellArrow::getCoordinates() const { return mCoordinates; } CSVRender::CellArrow::Direction CSVRender::CellArrow::getDirection() const { return mDirection; } ================================================ FILE: apps/opencs/view/render/cellarrow.hpp ================================================ #ifndef OPENCS_VIEW_CELLARROW_H #define OPENCS_VIEW_CELLARROW_H #include "tagbase.hpp" #include #include "../../model/world/cellcoordinates.hpp" namespace osg { class PositionAttitudeTransform; class Group; } namespace CSVRender { class CellArrow; class CellArrowTag : public TagBase { CellArrow *mArrow; public: CellArrowTag (CellArrow *arrow); CellArrow *getCellArrow() const; QString getToolTip (bool hideBasics) const override; }; class CellArrow { public: enum Direction { Direction_North = 1, Direction_West = 2, Direction_South = 4, Direction_East = 8 }; private: // not implemented CellArrow (const CellArrow&); CellArrow& operator= (const CellArrow&); Direction mDirection; osg::Group* mParentNode; osg::ref_ptr mBaseNode; CSMWorld::CellCoordinates mCoordinates; void adjustTransform(); void buildShape(); public: CellArrow (osg::Group *cellNode, Direction direction, const CSMWorld::CellCoordinates& coordinates); ~CellArrow(); CSMWorld::CellCoordinates getCoordinates() const; Direction getDirection() const; }; } #endif ================================================ FILE: apps/opencs/view/render/cellborder.cpp ================================================ #include "cellborder.hpp" #include #include #include #include #include #include "mask.hpp" #include "../../model/world/cellcoordinates.hpp" const int CSVRender::CellBorder::CellSize = ESM::Land::REAL_SIZE; /* The number of vertices per cell border is equal to the number of vertices per edge minus the duplicated corner vertices. An additional vertex to close the loop is NOT needed. */ const int CSVRender::CellBorder::VertexCount = (ESM::Land::LAND_SIZE * 4) - 4; CSVRender::CellBorder::CellBorder(osg::Group* cellNode, const CSMWorld::CellCoordinates& coords) : mParentNode(cellNode) { mBorderGeometry = new osg::Geometry(); mBaseNode = new osg::PositionAttitudeTransform(); mBaseNode->setNodeMask(Mask_CellBorder); mBaseNode->setPosition(osg::Vec3f(coords.getX() * CellSize, coords.getY() * CellSize, 10)); mBaseNode->addChild(mBorderGeometry); mParentNode->addChild(mBaseNode); } CSVRender::CellBorder::~CellBorder() { mParentNode->removeChild(mBaseNode); } void CSVRender::CellBorder::buildShape(const ESM::Land& esmLand) { const ESM::Land::LandData* landData = esmLand.getLandData(ESM::Land::DATA_VHGT); if (!landData) return; mBaseNode->removeChild(mBorderGeometry); mBorderGeometry = new osg::Geometry(); osg::ref_ptr vertices = new osg::Vec3Array(); int x = 0; int y = 0; /* Traverse the cell border counter-clockwise starting at the SW corner vertex (0, 0). Each loop starts at a corner vertex and ends right before the next corner vertex. */ for (; x < ESM::Land::LAND_SIZE - 1; ++x) vertices->push_back(osg::Vec3f(scaleToWorld(x), scaleToWorld(y), landData->mHeights[landIndex(x, y)])); x = ESM::Land::LAND_SIZE - 1; for (; y < ESM::Land::LAND_SIZE - 1; ++y) vertices->push_back(osg::Vec3f(scaleToWorld(x), scaleToWorld(y), landData->mHeights[landIndex(x, y)])); y = ESM::Land::LAND_SIZE - 1; for (; x > 0; --x) vertices->push_back(osg::Vec3f(scaleToWorld(x), scaleToWorld(y), landData->mHeights[landIndex(x, y)])); x = 0; for (; y > 0; --y) vertices->push_back(osg::Vec3f(scaleToWorld(x), scaleToWorld(y), landData->mHeights[landIndex(x, y)])); mBorderGeometry->setVertexArray(vertices); osg::ref_ptr colors = new osg::Vec4Array(); colors->push_back(osg::Vec4f(0.f, 0.5f, 0.f, 1.f)); mBorderGeometry->setColorArray(colors, osg::Array::BIND_PER_PRIMITIVE_SET); osg::ref_ptr primitives = new osg::DrawElementsUShort(osg::PrimitiveSet::LINE_STRIP, VertexCount + 1); // Assign one primitive to each vertex. for (size_t i = 0; i < VertexCount; ++i) primitives->setElement(i, i); // Assign the last primitive to the first vertex to close the loop. primitives->setElement(VertexCount, 0); mBorderGeometry->addPrimitiveSet(primitives); mBorderGeometry->getOrCreateStateSet()->setMode(GL_LIGHTING, osg::StateAttribute::OFF); mBaseNode->addChild(mBorderGeometry); } size_t CSVRender::CellBorder::landIndex(int x, int y) { return static_cast(y) * ESM::Land::LAND_SIZE + x; } float CSVRender::CellBorder::scaleToWorld(int value) { return (CellSize + 128) * (float)value / ESM::Land::LAND_SIZE; } ================================================ FILE: apps/opencs/view/render/cellborder.hpp ================================================ #ifndef OPENCS_VIEW_CELLBORDER_H #define OPENCS_VIEW_CELLBORDER_H #include #include namespace osg { class Geometry; class Group; class PositionAttitudeTransform; } namespace ESM { struct Land; } namespace CSMWorld { class CellCoordinates; } namespace CSVRender { class CellBorder { public: CellBorder(osg::Group* cellNode, const CSMWorld::CellCoordinates& coords); ~CellBorder(); void buildShape(const ESM::Land& esmLand); private: static const int CellSize; static const int VertexCount; size_t landIndex(int x, int y); float scaleToWorld(int val); // unimplemented CellBorder(const CellBorder&); CellBorder& operator=(const CellBorder&); osg::Group* mParentNode; osg::ref_ptr mBaseNode; osg::ref_ptr mBorderGeometry; }; } #endif ================================================ FILE: apps/opencs/view/render/cellmarker.cpp ================================================ #include "cellmarker.hpp" #include #include #include #include #include CSVRender::CellMarkerTag::CellMarkerTag(CellMarker *marker) : TagBase(Mask_CellMarker), mMarker(marker) {} CSVRender::CellMarker *CSVRender::CellMarkerTag::getCellMarker() const { return mMarker; } void CSVRender::CellMarker::buildMarker() { const int characterSize = 20; // Set up attributes of marker text. osg::ref_ptr markerText (new osgText::Text); markerText->setLayout(osgText::Text::LEFT_TO_RIGHT); markerText->setCharacterSize(characterSize); markerText->setAlignment(osgText::Text::CENTER_CENTER); markerText->setDrawMode(osgText::Text::TEXT | osgText::Text::FILLEDBOUNDINGBOX); // If cell exists then show black bounding box otherwise show red. if (mExists) { markerText->setBoundingBoxColor(osg::Vec4f(0.0f, 0.0f, 0.0f, 1.0f)); } else { markerText->setBoundingBoxColor(osg::Vec4f(1.0f, 0.0f, 0.0f, 1.0f)); } // Add text containing cell's coordinates. std::string coordinatesText = std::to_string(mCoordinates.getX()) + "," + std::to_string(mCoordinates.getY()); markerText->setText(coordinatesText); // Add text to marker node. osg::ref_ptr geode (new osg::Geode); geode->addDrawable(markerText); mMarkerNode->addChild(geode); } void CSVRender::CellMarker::positionMarker() { const int cellSize = Constants::CellSizeInUnits; const int markerHeight = 0; // Move marker to center of cell. int x = (mCoordinates.getX() * cellSize) + (cellSize / 2); int y = (mCoordinates.getY() * cellSize) + (cellSize / 2); mMarkerNode->setPosition(osg::Vec3f(x, y, markerHeight)); } CSVRender::CellMarker::CellMarker( osg::Group *cellNode, const CSMWorld::CellCoordinates& coordinates, const bool cellExists ) : mCellNode(cellNode), mCoordinates(coordinates), mExists(cellExists) { // Set up node for cell marker. mMarkerNode = new osg::AutoTransform(); mMarkerNode->setAutoRotateMode(osg::AutoTransform::ROTATE_TO_SCREEN); mMarkerNode->setAutoScaleToScreen(true); mMarkerNode->getOrCreateStateSet()->setMode(GL_DEPTH_TEST, osg::StateAttribute::OFF); mMarkerNode->getOrCreateStateSet()->setRenderBinDetails(11, "RenderBin"); osg::ref_ptr mat = new osg::Material; mat->setColorMode(osg::Material::AMBIENT_AND_DIFFUSE); mMarkerNode->getOrCreateStateSet()->setAttribute(mat); mMarkerNode->setUserData(new CellMarkerTag(this)); mMarkerNode->setNodeMask(Mask_CellMarker); mCellNode->addChild(mMarkerNode); buildMarker(); positionMarker(); } CSVRender::CellMarker::~CellMarker() { mCellNode->removeChild(mMarkerNode); } ================================================ FILE: apps/opencs/view/render/cellmarker.hpp ================================================ #ifndef OPENCS_VIEW_CELLMARKER_H #define OPENCS_VIEW_CELLMARKER_H #include "tagbase.hpp" #include #include "../../model/world/cellcoordinates.hpp" namespace osg { class AutoTransform; class Group; } namespace CSVRender { class CellMarker; class CellMarkerTag : public TagBase { private: CellMarker *mMarker; public: CellMarkerTag(CellMarker *marker); CellMarker *getCellMarker() const; }; /// \brief Marker to display cell coordinates. class CellMarker { private: osg::Group* mCellNode; osg::ref_ptr mMarkerNode; CSMWorld::CellCoordinates mCoordinates; bool mExists; // Not implemented. CellMarker(const CellMarker&); CellMarker& operator=(const CellMarker&); /// \brief Build marker containing cell's coordinates. void buildMarker(); /// \brief Position marker at center of cell. void positionMarker(); public: /// \brief Constructor. /// \param cellNode Cell to create marker for. /// \param coordinates Coordinates of cell. /// \param cellExists Whether or not cell exists. CellMarker( osg::Group *cellNode, const CSMWorld::CellCoordinates& coordinates, const bool cellExists); ~CellMarker(); }; } #endif ================================================ FILE: apps/opencs/view/render/cellwater.cpp ================================================ #include "cellwater.hpp" #include #include #include #include #include #include #include #include #include #include #include "../../model/world/cell.hpp" #include "../../model/world/cellcoordinates.hpp" #include "../../model/world/data.hpp" #include "mask.hpp" namespace CSVRender { const int CellWater::CellSize = ESM::Land::REAL_SIZE; CellWater::CellWater(CSMWorld::Data& data, osg::Group* cellNode, const std::string& id, const CSMWorld::CellCoordinates& cellCoords) : mData(data) , mId(id) , mParentNode(cellNode) , mWaterTransform(nullptr) , mWaterNode(nullptr) , mWaterGeometry(nullptr) , mDeleted(false) , mExterior(false) , mHasWater(false) { mWaterTransform = new osg::PositionAttitudeTransform(); mWaterTransform->setPosition(osg::Vec3f(cellCoords.getX() * CellSize + CellSize / 2.f, cellCoords.getY() * CellSize + CellSize / 2.f, 0)); mWaterTransform->setNodeMask(Mask_Water); mParentNode->addChild(mWaterTransform); mWaterNode = new osg::Geode(); mWaterTransform->addChild(mWaterNode); int cellIndex = mData.getCells().searchId(mId); if (cellIndex > -1) { updateCellData(mData.getCells().getRecord(cellIndex)); } // Keep water existence/height up to date QAbstractItemModel* cells = mData.getTableModel(CSMWorld::UniversalId::Type_Cells); connect(cells, SIGNAL(dataChanged(const QModelIndex&, const QModelIndex&)), this, SLOT(cellDataChanged(const QModelIndex&, const QModelIndex&))); } CellWater::~CellWater() { mParentNode->removeChild(mWaterTransform); } void CellWater::updateCellData(const CSMWorld::Record& cellRecord) { mDeleted = cellRecord.isDeleted(); if (!mDeleted) { const CSMWorld::Cell& cell = cellRecord.get(); if (mExterior != cell.isExterior() || mHasWater != cell.hasWater()) { mExterior = cellRecord.get().isExterior(); mHasWater = cellRecord.get().hasWater(); recreate(); } float waterHeight = -1; if (!mExterior) { waterHeight = cellRecord.get().mWater; } osg::Vec3d pos = mWaterTransform->getPosition(); pos.z() = waterHeight; mWaterTransform->setPosition(pos); } else { recreate(); } } void CellWater::reloadAssets() { recreate(); } void CellWater::cellDataChanged(const QModelIndex& topLeft, const QModelIndex& bottomRight) { const CSMWorld::Collection& cells = mData.getCells(); int rowStart = -1; int rowEnd = -1; if (topLeft.parent().isValid()) { rowStart = topLeft.parent().row(); rowEnd = bottomRight.parent().row(); } else { rowStart = topLeft.row(); rowEnd = bottomRight.row(); } for (int row = rowStart; row <= rowEnd; ++row) { const CSMWorld::Record& cellRecord = cells.getRecord(row); if (Misc::StringUtils::lowerCase(cellRecord.get().mId) == mId) updateCellData(cellRecord); } } void CellWater::recreate() { const int InteriorScalar = 20; const int SegmentsPerCell = 1; const int TextureRepeatsPerCell = 6; const float Alpha = 0.5f; const int RenderBin = osg::StateSet::TRANSPARENT_BIN - 1; if (mWaterGeometry) { mWaterNode->removeDrawable(mWaterGeometry); mWaterGeometry = nullptr; } if (mDeleted || !mHasWater) return; float size; int segments; float textureRepeats; if (mExterior) { size = CellSize; segments = SegmentsPerCell; textureRepeats = TextureRepeatsPerCell; } else { size = CellSize * InteriorScalar; segments = SegmentsPerCell * InteriorScalar; textureRepeats = TextureRepeatsPerCell * InteriorScalar; } mWaterGeometry = SceneUtil::createWaterGeometry(size, segments, textureRepeats); mWaterGeometry->setStateSet(SceneUtil::createSimpleWaterStateSet(Alpha, RenderBin)); // Add water texture std::string textureName = Fallback::Map::getString("Water_SurfaceTexture"); textureName = "textures/water/" + textureName + "00.dds"; Resource::ImageManager* imageManager = mData.getResourceSystem()->getImageManager(); osg::ref_ptr waterTexture = new osg::Texture2D(); waterTexture->setImage(imageManager->getImage(textureName)); waterTexture->setWrap(osg::Texture::WRAP_S, osg::Texture::REPEAT); waterTexture->setWrap(osg::Texture::WRAP_T, osg::Texture::REPEAT); mWaterGeometry->getStateSet()->setTextureAttributeAndModes(0, waterTexture, osg::StateAttribute::ON); mWaterNode->addDrawable(mWaterGeometry); } } ================================================ FILE: apps/opencs/view/render/cellwater.hpp ================================================ #ifndef CSV_RENDER_CELLWATER_H #define CSV_RENDER_CELLWATER_H #include #include #include #include #include "../../model/world/record.hpp" namespace osg { class Geode; class Geometry; class Group; class PositionAttitudeTransform; } namespace CSMWorld { struct Cell; class CellCoordinates; class Data; } namespace CSVRender { /// For exterior cells, this adds a patch of water to fit the size of the cell. For interior cells with water, this /// adds a large patch of water much larger than the typical size of a cell. class CellWater : public QObject { Q_OBJECT public: CellWater(CSMWorld::Data& data, osg::Group* cellNode, const std::string& id, const CSMWorld::CellCoordinates& cellCoords); ~CellWater(); void updateCellData(const CSMWorld::Record& cellRecord); void reloadAssets(); private slots: void cellDataChanged(const QModelIndex& topLeft, const QModelIndex& bottomRight); private: void recreate(); static const int CellSize; CSMWorld::Data& mData; std::string mId; osg::Group* mParentNode; osg::ref_ptr mWaterTransform; osg::ref_ptr mWaterNode; osg::ref_ptr mWaterGeometry; bool mDeleted; bool mExterior; bool mHasWater; }; } #endif ================================================ FILE: apps/opencs/view/render/commands.cpp ================================================ #include "commands.hpp" #include #include #include #include "editmode.hpp" #include "terrainselection.hpp" #include "terrainshapemode.hpp" #include "terraintexturemode.hpp" #include "worldspacewidget.hpp" CSVRender::DrawTerrainSelectionCommand::DrawTerrainSelectionCommand(WorldspaceWidget* worldspaceWidget, QUndoCommand* parent) : mWorldspaceWidget(worldspaceWidget) { } void CSVRender::DrawTerrainSelectionCommand::redo() { tryUpdate(); } void CSVRender::DrawTerrainSelectionCommand::undo() { tryUpdate(); } void CSVRender::DrawTerrainSelectionCommand::tryUpdate() { if (!mWorldspaceWidget) { Log(Debug::Verbose) << "Can't update terrain selection, no WorldspaceWidget found!"; return; } auto terrainMode = dynamic_cast(mWorldspaceWidget->getEditMode()); if (!terrainMode) { Log(Debug::Verbose) << "Can't update terrain selection in current EditMode"; return; } terrainMode->getTerrainSelection()->update(); } ================================================ FILE: apps/opencs/view/render/commands.hpp ================================================ #ifndef CSV_RENDER_COMMANDS_HPP #define CSV_RENDER_COMMANDS_HPP #include #include #include "worldspacewidget.hpp" namespace CSVRender { class TerrainSelection; /* Current solution to force a redrawing of the terrain-selection grid when undoing/redoing changes in the editor. This only triggers a simple redraw of the grid, so only use it in conjunction with actual data changes which deform the grid. Please note that this command needs to be put onto the QUndoStack twice: at the start and at the end of the related terrain manipulation. This makes sure that the grid is always updated after all changes have been undone or redone -- but it also means that the selection is redrawn once at the beginning of either action. Future refinement may solve that. */ class DrawTerrainSelectionCommand : public QUndoCommand { private: QPointer mWorldspaceWidget; public: DrawTerrainSelectionCommand(WorldspaceWidget* worldspaceWidget, QUndoCommand* parent = nullptr); void redo() override; void undo() override; void tryUpdate(); }; } #endif ================================================ FILE: apps/opencs/view/render/editmode.cpp ================================================ #include "editmode.hpp" #include "tagbase.hpp" #include "worldspacewidget.hpp" CSVRender::WorldspaceWidget& CSVRender::EditMode::getWorldspaceWidget() { return *mWorldspaceWidget; } CSVRender::EditMode::EditMode (WorldspaceWidget *worldspaceWidget, const QIcon& icon, unsigned int mask, const QString& tooltip, QWidget *parent) : ModeButton (icon, tooltip, parent), mWorldspaceWidget (worldspaceWidget), mMask (mask) {} unsigned int CSVRender::EditMode::getInteractionMask() const { return mMask; } void CSVRender::EditMode::activate (CSVWidget::SceneToolbar *toolbar) { mWorldspaceWidget->setInteractionMask (mMask); mWorldspaceWidget->clearSelection (~mMask); } void CSVRender::EditMode::setEditLock (bool locked) { } void CSVRender::EditMode::primaryOpenPressed (const WorldspaceHitResult& hit) {} void CSVRender::EditMode::primaryEditPressed (const WorldspaceHitResult& hit) {} void CSVRender::EditMode::secondaryEditPressed (const WorldspaceHitResult& hit) {} void CSVRender::EditMode::primarySelectPressed (const WorldspaceHitResult& hit) {} void CSVRender::EditMode::secondarySelectPressed (const WorldspaceHitResult& hit) {} bool CSVRender::EditMode::primaryEditStartDrag (const QPoint& pos) { return false; } bool CSVRender::EditMode::secondaryEditStartDrag (const QPoint& pos) { return false; } bool CSVRender::EditMode::primarySelectStartDrag (const QPoint& pos) { return false; } bool CSVRender::EditMode::secondarySelectStartDrag (const QPoint& pos) { return false; } void CSVRender::EditMode::drag (const QPoint& pos, int diffX, int diffY, double speedFactor) {} void CSVRender::EditMode::dragCompleted(const QPoint& pos) {} void CSVRender::EditMode::dragAborted() {} void CSVRender::EditMode::dragWheel (int diff, double speedFactor) {} void CSVRender::EditMode::dragEnterEvent (QDragEnterEvent *event) {} void CSVRender::EditMode::dropEvent (QDropEvent *event) {} void CSVRender::EditMode::dragMoveEvent (QDragMoveEvent *event) {} void CSVRender::EditMode::mouseMoveEvent (QMouseEvent *event) {} int CSVRender::EditMode::getSubMode() const { return -1; } ================================================ FILE: apps/opencs/view/render/editmode.hpp ================================================ #ifndef CSV_RENDER_EDITMODE_H #define CSV_RENDER_EDITMODE_H #include #include "../widget/modebutton.hpp" class QDragEnterEvent; class QDropEvent; class QDragMoveEvent; class QPoint; namespace CSVRender { class WorldspaceWidget; struct WorldspaceHitResult; class TagBase; class EditMode : public CSVWidget::ModeButton { Q_OBJECT WorldspaceWidget *mWorldspaceWidget; unsigned int mMask; protected: WorldspaceWidget& getWorldspaceWidget(); public: EditMode (WorldspaceWidget *worldspaceWidget, const QIcon& icon, unsigned int mask, const QString& tooltip = "", QWidget *parent = nullptr); unsigned int getInteractionMask() const; void activate (CSVWidget::SceneToolbar *toolbar) override; /// Default-implementation: Ignored. virtual void setEditLock (bool locked); /// Default-implementation: Ignored. virtual void primaryOpenPressed (const WorldspaceHitResult& hit); /// Default-implementation: Ignored. virtual void primaryEditPressed (const WorldspaceHitResult& hit); /// Default-implementation: Ignored. virtual void secondaryEditPressed (const WorldspaceHitResult& hit); /// Default-implementation: Ignored. virtual void primarySelectPressed (const WorldspaceHitResult& hit); /// Default-implementation: Ignored. virtual void secondarySelectPressed (const WorldspaceHitResult& hit); /// Default-implementation: ignore and return false /// /// \return Drag accepted? virtual bool primaryEditStartDrag (const QPoint& pos); /// Default-implementation: ignore and return false /// /// \return Drag accepted? virtual bool secondaryEditStartDrag (const QPoint& pos); /// Default-implementation: ignore and return false /// /// \return Drag accepted? virtual bool primarySelectStartDrag (const QPoint& pos); /// Default-implementation: ignore and return false /// /// \return Drag accepted? virtual bool secondarySelectStartDrag (const QPoint& pos); /// Default-implementation: ignored virtual void drag (const QPoint& pos, int diffX, int diffY, double speedFactor); /// Default-implementation: ignored virtual void dragCompleted(const QPoint& pos); /// Default-implementation: ignored /// /// \note dragAborted will not be called, if the drag is aborted via changing /// editing mode virtual void dragAborted(); /// Default-implementation: ignored virtual void dragWheel (int diff, double speedFactor); /// Default-implementation: ignored void dragEnterEvent (QDragEnterEvent *event) override; /// Default-implementation: ignored void dropEvent (QDropEvent *event) override; /// Default-implementation: ignored void dragMoveEvent (QDragMoveEvent *event) override; void mouseMoveEvent (QMouseEvent *event) override; /// Default: return -1 virtual int getSubMode() const; }; } #endif ================================================ FILE: apps/opencs/view/render/instancedragmodes.hpp ================================================ #ifndef CSV_WIDGET_INSTANCEDRAGMODES_H #define CSV_WIDGET_INSTANCEDRAGMODES_H namespace CSVRender { enum DragMode { DragMode_None, DragMode_Move, DragMode_Rotate, DragMode_Scale, DragMode_Select_Only, DragMode_Select_Add, DragMode_Select_Remove, DragMode_Select_Invert }; } #endif ================================================ FILE: apps/opencs/view/render/instancemode.cpp ================================================ #include "instancemode.hpp" #include #include #include #include "../../model/prefs/state.hpp" #include #include #include #include #include "../../model/world/idtable.hpp" #include "../../model/world/idtree.hpp" #include "../../model/world/commands.hpp" #include "../../model/world/commandmacro.hpp" #include "../../model/prefs/shortcut.hpp" #include "../widget/scenetoolbar.hpp" #include "../widget/scenetoolmode.hpp" #include "mask.hpp" #include "object.hpp" #include "worldspacewidget.hpp" #include "pagedworldspacewidget.hpp" #include "instanceselectionmode.hpp" #include "instancemovemode.hpp" int CSVRender::InstanceMode::getSubModeFromId (const std::string& id) const { return id=="move" ? 0 : (id=="rotate" ? 1 : 2); } osg::Vec3f CSVRender::InstanceMode::quatToEuler(const osg::Quat& rot) const { float x, y, z; float test = 2 * (rot.w() * rot.y() + rot.x() * rot.z()); if (std::abs(test) >= 1.f) { x = atan2(rot.x(), rot.w()); y = (test > 0) ? (osg::PI / 2) : (-osg::PI / 2); z = 0; } else { x = std::atan2(2 * (rot.w() * rot.x() - rot.y() * rot.z()), 1 - 2 * (rot.x() * rot.x() + rot.y() * rot.y())); y = std::asin(test); z = std::atan2(2 * (rot.w() * rot.z() - rot.x() * rot.y()), 1 - 2 * (rot.y() * rot.y() + rot.z() * rot.z())); } return osg::Vec3f(-x, -y, -z); } osg::Quat CSVRender::InstanceMode::eulerToQuat(const osg::Vec3f& euler) const { osg::Quat xr = osg::Quat(-euler[0], osg::Vec3f(1,0,0)); osg::Quat yr = osg::Quat(-euler[1], osg::Vec3f(0,1,0)); osg::Quat zr = osg::Quat(-euler[2], osg::Vec3f(0,0,1)); return zr * yr * xr; } osg::Vec3f CSVRender::InstanceMode::getSelectionCenter(const std::vector >& selection) const { osg::Vec3f center = osg::Vec3f(0, 0, 0); int objectCount = 0; for (std::vector >::const_iterator iter (selection.begin()); iter!=selection.end(); ++iter) { if (CSVRender::ObjectTag *objectTag = dynamic_cast (iter->get())) { const ESM::Position& position = objectTag->mObject->getPosition(); center += osg::Vec3f(position.pos[0], position.pos[1], position.pos[2]); ++objectCount; } } if (objectCount > 0) center /= objectCount; return center; } osg::Vec3f CSVRender::InstanceMode::getScreenCoords(const osg::Vec3f& pos) { osg::Matrix viewMatrix = getWorldspaceWidget().getCamera()->getViewMatrix(); osg::Matrix projMatrix = getWorldspaceWidget().getCamera()->getProjectionMatrix(); osg::Matrix windowMatrix = getWorldspaceWidget().getCamera()->getViewport()->computeWindowMatrix(); osg::Matrix combined = viewMatrix * projMatrix * windowMatrix; return pos * combined; } osg::Vec3f CSVRender::InstanceMode::getProjectionSpaceCoords(const osg::Vec3f& pos) { osg::Matrix viewMatrix = getWorldspaceWidget().getCamera()->getViewMatrix(); osg::Matrix projMatrix = getWorldspaceWidget().getCamera()->getProjectionMatrix(); osg::Matrix combined = viewMatrix * projMatrix; return pos * combined; } osg::Vec3f CSVRender::InstanceMode::getMousePlaneCoords(const QPoint& point, const osg::Vec3d& dragStart) { osg::Matrix viewMatrix; viewMatrix.invert(getWorldspaceWidget().getCamera()->getViewMatrix()); osg::Matrix projMatrix; projMatrix.invert(getWorldspaceWidget().getCamera()->getProjectionMatrix()); osg::Matrix combined = projMatrix * viewMatrix; /* calculate viewport normalized coordinates note: is there a reason to use getCamera()->getViewport()->computeWindowMatrix() instead? */ float x = (point.x() * 2) / getWorldspaceWidget().getCamera()->getViewport()->width() - 1.0f; float y = 1.0f - (point.y() * 2) / getWorldspaceWidget().getCamera()->getViewport()->height(); osg::Vec3f mousePlanePoint = osg::Vec3f(x, y, dragStart.z()) * combined; return mousePlanePoint; } CSVRender::InstanceMode::InstanceMode (WorldspaceWidget *worldspaceWidget, osg::ref_ptr parentNode, QWidget *parent) : EditMode (worldspaceWidget, QIcon (":scenetoolbar/editing-instance"), Mask_Reference | Mask_Terrain, "Instance editing", parent), mSubMode (nullptr), mSubModeId ("move"), mSelectionMode (nullptr), mDragMode (DragMode_None), mDragAxis (-1), mLocked (false), mUnitScaleDist(1), mParentNode (parentNode) { connect(this, SIGNAL(requestFocus(const std::string&)), worldspaceWidget, SIGNAL(requestFocus(const std::string&))); CSMPrefs::Shortcut* deleteShortcut = new CSMPrefs::Shortcut("scene-delete", worldspaceWidget); connect(deleteShortcut, SIGNAL(activated(bool)), this, SLOT(deleteSelectedInstances(bool))); // Following classes could be simplified by using QSignalMapper, which is obsolete in Qt5.10, but not in Qt4.8 and Qt5.14 CSMPrefs::Shortcut* dropToCollisionShortcut = new CSMPrefs::Shortcut("scene-instance-drop-collision", worldspaceWidget); connect(dropToCollisionShortcut, SIGNAL(activated()), this, SLOT(dropSelectedInstancesToCollision())); CSMPrefs::Shortcut* dropToTerrainLevelShortcut = new CSMPrefs::Shortcut("scene-instance-drop-terrain", worldspaceWidget); connect(dropToTerrainLevelShortcut, SIGNAL(activated()), this, SLOT(dropSelectedInstancesToTerrain())); CSMPrefs::Shortcut* dropToCollisionShortcut2 = new CSMPrefs::Shortcut("scene-instance-drop-collision-separately", worldspaceWidget); connect(dropToCollisionShortcut2, SIGNAL(activated()), this, SLOT(dropSelectedInstancesToCollisionSeparately())); CSMPrefs::Shortcut* dropToTerrainLevelShortcut2 = new CSMPrefs::Shortcut("scene-instance-drop-terrain-separately", worldspaceWidget); connect(dropToTerrainLevelShortcut2, SIGNAL(activated()), this, SLOT(dropSelectedInstancesToTerrainSeparately())); } void CSVRender::InstanceMode::activate (CSVWidget::SceneToolbar *toolbar) { if (!mSubMode) { mSubMode = new CSVWidget::SceneToolMode (toolbar, "Edit Sub-Mode"); mSubMode->addButton (new InstanceMoveMode (this), "move"); mSubMode->addButton (":scenetoolbar/transform-rotate", "rotate", "Rotate selected instances" "
  • Use {scene-edit-primary} to rotate instances freely
  • " "
  • Use {scene-edit-secondary} to rotate instances within the grid
  • " "
  • The center of the view acts as the axis of rotation
  • " "
" "Grid rotate not implemented yet"); mSubMode->addButton (":scenetoolbar/transform-scale", "scale", "Scale selected instances" "
  • Use {scene-edit-primary} to scale instances freely
  • " "
  • Use {scene-edit-secondary} to scale instances along the grid
  • " "
  • The scaling rate is based on how close the start of a drag is to the center of the screen
  • " "
" "Grid scale not implemented yet"); mSubMode->setButton (mSubModeId); connect (mSubMode, SIGNAL (modeChanged (const std::string&)), this, SLOT (subModeChanged (const std::string&))); } if (!mSelectionMode) mSelectionMode = new InstanceSelectionMode (toolbar, getWorldspaceWidget(), mParentNode); mDragMode = DragMode_None; EditMode::activate (toolbar); toolbar->addTool (mSubMode); toolbar->addTool (mSelectionMode); std::string subMode = mSubMode->getCurrentId(); getWorldspaceWidget().setSubMode (getSubModeFromId (subMode), Mask_Reference); } void CSVRender::InstanceMode::deactivate (CSVWidget::SceneToolbar *toolbar) { mDragMode = DragMode_None; getWorldspaceWidget().reset (Mask_Reference); if (mSelectionMode) { toolbar->removeTool (mSelectionMode); delete mSelectionMode; mSelectionMode = nullptr; } if (mSubMode) { toolbar->removeTool (mSubMode); delete mSubMode; mSubMode = nullptr; } EditMode::deactivate (toolbar); } void CSVRender::InstanceMode::setEditLock (bool locked) { mLocked = locked; if (mLocked) getWorldspaceWidget().abortDrag(); } void CSVRender::InstanceMode::primaryEditPressed (const WorldspaceHitResult& hit) { if (CSMPrefs::get()["3D Scene Input"]["context-select"].isTrue()) primarySelectPressed (hit); } void CSVRender::InstanceMode::primaryOpenPressed (const WorldspaceHitResult& hit) { if(hit.tag) { if (CSVRender::ObjectTag *objectTag = dynamic_cast (hit.tag.get())) { const std::string refId = objectTag->mObject->getReferenceId(); emit requestFocus(refId); } } } void CSVRender::InstanceMode::secondaryEditPressed (const WorldspaceHitResult& hit) { if (CSMPrefs::get()["3D Scene Input"]["context-select"].isTrue()) secondarySelectPressed (hit); } void CSVRender::InstanceMode::primarySelectPressed (const WorldspaceHitResult& hit) { getWorldspaceWidget().clearSelection (Mask_Reference); if (hit.tag) { if (CSVRender::ObjectTag *objectTag = dynamic_cast (hit.tag.get())) { // hit an Object, select it CSVRender::Object* object = objectTag->mObject; object->setSelected (true); return; } } } void CSVRender::InstanceMode::secondarySelectPressed (const WorldspaceHitResult& hit) { if (hit.tag) { if (CSVRender::ObjectTag *objectTag = dynamic_cast (hit.tag.get())) { // hit an Object, toggle its selection state CSVRender::Object* object = objectTag->mObject; object->setSelected (!object->getSelected()); return; } } } bool CSVRender::InstanceMode::primaryEditStartDrag (const QPoint& pos) { if (mDragMode!=DragMode_None || mLocked) return false; WorldspaceHitResult hit = getWorldspaceWidget().mousePick (pos, getWorldspaceWidget().getInteractionMask()); std::vector > selection = getWorldspaceWidget().getSelection (Mask_Reference); if (selection.empty()) { // Only change selection at the start of drag if no object is already selected if (hit.tag && CSMPrefs::get()["3D Scene Input"]["context-select"].isTrue()) { getWorldspaceWidget().clearSelection (Mask_Reference); if (CSVRender::ObjectTag *objectTag = dynamic_cast (hit.tag.get())) { CSVRender::Object* object = objectTag->mObject; object->setSelected (true); } } selection = getWorldspaceWidget().getSelection (Mask_Reference); if (selection.empty()) return false; } for (std::vector >::iterator iter (selection.begin()); iter!=selection.end(); ++iter) { if (CSVRender::ObjectTag *objectTag = dynamic_cast (iter->get())) { if (mSubModeId == "move") { objectTag->mObject->setEdited (Object::Override_Position); mDragMode = DragMode_Move; } else if (mSubModeId == "rotate") { objectTag->mObject->setEdited (Object::Override_Rotation); mDragMode = DragMode_Rotate; } else if (mSubModeId == "scale") { objectTag->mObject->setEdited (Object::Override_Scale); mDragMode = DragMode_Scale; // Calculate scale factor std::vector > editedSelection = getWorldspaceWidget().getEdited (Mask_Reference); osg::Vec3f center = getScreenCoords(getSelectionCenter(editedSelection)); int widgetHeight = getWorldspaceWidget().height(); float dx = pos.x() - center.x(); float dy = (widgetHeight - pos.y()) - center.y(); mUnitScaleDist = std::sqrt(dx * dx + dy * dy); } } } if (CSVRender::ObjectMarkerTag *objectTag = dynamic_cast (hit.tag.get())) { mDragAxis = objectTag->mAxis; } else mDragAxis = -1; return true; } bool CSVRender::InstanceMode::secondaryEditStartDrag (const QPoint& pos) { if (mLocked) return false; return false; } bool CSVRender::InstanceMode::primarySelectStartDrag (const QPoint& pos) { if (mDragMode!=DragMode_None || mLocked) return false; std::string primarySelectAction = CSMPrefs::get()["3D Scene Editing"]["primary-select-action"].toString(); if ( primarySelectAction == "Select only" ) mDragMode = DragMode_Select_Only; else if ( primarySelectAction == "Add to selection" ) mDragMode = DragMode_Select_Add; else if ( primarySelectAction == "Remove from selection" ) mDragMode = DragMode_Select_Remove; else if ( primarySelectAction == "Invert selection" ) mDragMode = DragMode_Select_Invert; WorldspaceHitResult hit = getWorldspaceWidget().mousePick (pos, getWorldspaceWidget().getInteractionMask()); mSelectionMode->setDragStart(hit.worldPos); return true; } bool CSVRender::InstanceMode::secondarySelectStartDrag (const QPoint& pos) { if (mDragMode!=DragMode_None || mLocked) return false; std::string secondarySelectAction = CSMPrefs::get()["3D Scene Editing"]["secondary-select-action"].toString(); if ( secondarySelectAction == "Select only" ) mDragMode = DragMode_Select_Only; else if ( secondarySelectAction == "Add to selection" ) mDragMode = DragMode_Select_Add; else if ( secondarySelectAction == "Remove from selection" ) mDragMode = DragMode_Select_Remove; else if ( secondarySelectAction == "Invert selection" ) mDragMode = DragMode_Select_Invert; WorldspaceHitResult hit = getWorldspaceWidget().mousePick (pos, getWorldspaceWidget().getInteractionMask()); mSelectionMode->setDragStart(hit.worldPos); return true; } void CSVRender::InstanceMode::drag (const QPoint& pos, int diffX, int diffY, double speedFactor) { osg::Vec3f offset; osg::Quat rotation; std::vector > selection = getWorldspaceWidget().getEdited (Mask_Reference); if (mDragMode == DragMode_Move) { osg::Vec3f eye, centre, up; getWorldspaceWidget().getCamera()->getViewMatrix().getLookAt (eye, centre, up); if (diffY) { offset += up * diffY * speedFactor; } if (diffX) { offset += ((centre-eye) ^ up) * diffX * speedFactor; } if (mDragAxis!=-1) { for (int i=0; i<3; ++i) { if (i!=mDragAxis) offset[i] = 0; } } } else if (mDragMode == DragMode_Rotate) { osg::Vec3f eye, centre, up; getWorldspaceWidget().getCamera()->getViewMatrix().getLookAt (eye, centre, up); float angle; osg::Vec3f axis; if (mDragAxis == -1) { // Free rotate float rotationFactor = CSMPrefs::get()["3D Scene Input"]["rotate-factor"].toDouble() * speedFactor; osg::Quat cameraRotation = getWorldspaceWidget().getCamera()->getInverseViewMatrix().getRotate(); osg::Vec3f camForward = centre - eye; osg::Vec3f screenDir = cameraRotation * osg::Vec3f(diffX, diffY, 0); screenDir.normalize(); angle = std::sqrt(diffX*diffX + diffY*diffY) * rotationFactor; axis = screenDir ^ camForward; } else { // Global axis rotation osg::Vec3f camBack = eye - centre; for (int i = 0; i < 3; ++i) { if (i == mDragAxis) axis[i] = 1; else axis[i] = 0; } // Flip axis if facing opposite side if (camBack * axis < 0) axis *= -1; // Convert coordinate system osg::Vec3f screenCenter = getScreenCoords(getSelectionCenter(selection)); int widgetHeight = getWorldspaceWidget().height(); float newX = pos.x() - screenCenter.x(); float newY = (widgetHeight - pos.y()) - screenCenter.y(); float oldX = newX - diffX; float oldY = newY - diffY; // diffY appears to already be flipped osg::Vec3f oldVec = osg::Vec3f(oldX, oldY, 0); oldVec.normalize(); osg::Vec3f newVec = osg::Vec3f(newX, newY, 0); newVec.normalize(); // Find angle and axis of rotation angle = std::acos(oldVec * newVec) * speedFactor; if (((oldVec ^ newVec) * camBack < 0) ^ (camBack.z() < 0)) angle *= -1; } rotation = osg::Quat(angle, axis); } else if (mDragMode == DragMode_Scale) { osg::Vec3f center = getScreenCoords(getSelectionCenter(selection)); // Calculate scaling distance/rate int widgetHeight = getWorldspaceWidget().height(); float dx = pos.x() - center.x(); float dy = (widgetHeight - pos.y()) - center.y(); float dist = std::sqrt(dx * dx + dy * dy); float scale = dist / mUnitScaleDist; // Only uniform scaling is currently supported offset = osg::Vec3f(scale, scale, scale); } else if (mSelectionMode->getCurrentId() == "cube-centre") { osg::Vec3f mousePlanePoint = getMousePlaneCoords(pos, getProjectionSpaceCoords(mSelectionMode->getDragStart())); mSelectionMode->drawSelectionCubeCentre (mousePlanePoint); return; } else if (mSelectionMode->getCurrentId() == "cube-corner") { osg::Vec3f mousePlanePoint = getMousePlaneCoords(pos, getProjectionSpaceCoords(mSelectionMode->getDragStart())); mSelectionMode->drawSelectionCubeCorner (mousePlanePoint); return; } else if (mSelectionMode->getCurrentId() == "sphere") { osg::Vec3f mousePlanePoint = getMousePlaneCoords(pos, getProjectionSpaceCoords(mSelectionMode->getDragStart())); mSelectionMode->drawSelectionSphere (mousePlanePoint); return; } // Apply for (std::vector >::iterator iter (selection.begin()); iter!=selection.end(); ++iter) { if (CSVRender::ObjectTag *objectTag = dynamic_cast (iter->get())) { if (mDragMode == DragMode_Move) { ESM::Position position = objectTag->mObject->getPosition(); for (int i=0; i<3; ++i) { position.pos[i] += offset[i]; } objectTag->mObject->setPosition(position.pos); } else if (mDragMode == DragMode_Rotate) { ESM::Position position = objectTag->mObject->getPosition(); osg::Quat currentRot = eulerToQuat(osg::Vec3f(position.rot[0], position.rot[1], position.rot[2])); osg::Quat combined = currentRot * rotation; osg::Vec3f euler = quatToEuler(combined); // There appears to be a very rare rounding error that can cause asin to return NaN if (!euler.isNaN()) { position.rot[0] = euler.x(); position.rot[1] = euler.y(); position.rot[2] = euler.z(); } objectTag->mObject->setRotation(position.rot); } else if (mDragMode == DragMode_Scale) { // Reset scale objectTag->mObject->setEdited(0); objectTag->mObject->setEdited(Object::Override_Scale); float scale = objectTag->mObject->getScale(); scale *= offset.x(); objectTag->mObject->setScale (scale); } } } } void CSVRender::InstanceMode::dragCompleted(const QPoint& pos) { std::vector > selection = getWorldspaceWidget().getEdited (Mask_Reference); QUndoStack& undoStack = getWorldspaceWidget().getDocument().getUndoStack(); QString description; switch (mDragMode) { case DragMode_Move: description = "Move Instances"; break; case DragMode_Rotate: description = "Rotate Instances"; break; case DragMode_Scale: description = "Scale Instances"; break; case DragMode_Select_Only : handleSelectDrag(pos); return; break; case DragMode_Select_Add : handleSelectDrag(pos); return; break; case DragMode_Select_Remove : handleSelectDrag(pos); return; break; case DragMode_Select_Invert : handleSelectDrag(pos); return; break; case DragMode_None: break; } CSMWorld::CommandMacro macro (undoStack, description); for (std::vector >::iterator iter (selection.begin()); iter!=selection.end(); ++iter) { if (CSVRender::ObjectTag *objectTag = dynamic_cast (iter->get())) { objectTag->mObject->apply (macro); } } mDragMode = DragMode_None; } void CSVRender::InstanceMode::dragAborted() { getWorldspaceWidget().reset (Mask_Reference); mDragMode = DragMode_None; } void CSVRender::InstanceMode::dragWheel (int diff, double speedFactor) { if (mDragMode==DragMode_Move) { osg::Vec3f eye; osg::Vec3f centre; osg::Vec3f up; getWorldspaceWidget().getCamera()->getViewMatrix().getLookAt (eye, centre, up); osg::Vec3f offset = centre - eye; offset.normalize(); offset *= diff * speedFactor; std::vector > selection = getWorldspaceWidget().getEdited (Mask_Reference); for (std::vector >::iterator iter (selection.begin()); iter!=selection.end(); ++iter) { if (CSVRender::ObjectTag *objectTag = dynamic_cast (iter->get())) { ESM::Position position = objectTag->mObject->getPosition(); for (int i=0; i<3; ++i) position.pos[i] += offset[i]; objectTag->mObject->setPosition (position.pos); } } } } void CSVRender::InstanceMode::dragEnterEvent (QDragEnterEvent *event) { if (const CSMWorld::TableMimeData* mime = dynamic_cast (event->mimeData())) { if (!mime->fromDocument (getWorldspaceWidget().getDocument())) return; if (mime->holdsType (CSMWorld::UniversalId::Type_Referenceable)) event->accept(); } } void CSVRender::InstanceMode::dropEvent (QDropEvent* event) { if (const CSMWorld::TableMimeData* mime = dynamic_cast (event->mimeData())) { CSMDoc::Document& document = getWorldspaceWidget().getDocument(); if (!mime->fromDocument (document)) return; WorldspaceHitResult hit = getWorldspaceWidget().mousePick (event->pos(), getWorldspaceWidget().getInteractionMask()); std::string cellId = getWorldspaceWidget().getCellId (hit.worldPos); CSMWorld::IdTree& cellTable = dynamic_cast ( *document.getData().getTableModel (CSMWorld::UniversalId::Type_Cells)); bool noCell = document.getData().getCells().searchId (cellId)==-1; if (noCell) { std::string mode = CSMPrefs::get()["3D Scene Editing"]["outside-drop"].toString(); // target cell does not exist if (mode=="Discard") return; if (mode=="Create cell and insert") { std::unique_ptr createCommand ( new CSMWorld::CreateCommand (cellTable, cellId)); int parentIndex = cellTable.findColumnIndex (CSMWorld::Columns::ColumnId_Cell); int index = cellTable.findNestedColumnIndex (parentIndex, CSMWorld::Columns::ColumnId_Interior); createCommand->addNestedValue (parentIndex, index, false); document.getUndoStack().push (createCommand.release()); if (CSVRender::PagedWorldspaceWidget *paged = dynamic_cast (&getWorldspaceWidget())) { CSMWorld::CellSelection selection = paged->getCellSelection(); selection.add (CSMWorld::CellCoordinates::fromId (cellId).first); paged->setCellSelection (selection); } } } else if (CSVRender::PagedWorldspaceWidget *paged = dynamic_cast (&getWorldspaceWidget())) { CSMWorld::CellSelection selection = paged->getCellSelection(); if (!selection.has (CSMWorld::CellCoordinates::fromId (cellId).first)) { // target cell exists, but is not shown std::string mode = CSMPrefs::get()["3D Scene Editing"]["outside-visible-drop"].toString(); if (mode=="Discard") return; if (mode=="Show cell and insert") { selection.add (CSMWorld::CellCoordinates::fromId (cellId).first); paged->setCellSelection (selection); } } } CSMWorld::IdTable& referencesTable = dynamic_cast ( *document.getData().getTableModel (CSMWorld::UniversalId::Type_References)); bool dropped = false; std::vector ids = mime->getData(); for (std::vector::const_iterator iter (ids.begin()); iter!=ids.end(); ++iter) if (mime->isReferencable (iter->getType())) { // create reference std::unique_ptr createCommand ( new CSMWorld::CreateCommand ( referencesTable, document.getData().getReferences().getNewId())); createCommand->addValue (referencesTable.findColumnIndex ( CSMWorld::Columns::ColumnId_Cell), QString::fromUtf8 (cellId.c_str())); createCommand->addValue (referencesTable.findColumnIndex ( CSMWorld::Columns::ColumnId_PositionXPos), hit.worldPos.x()); createCommand->addValue (referencesTable.findColumnIndex ( CSMWorld::Columns::ColumnId_PositionYPos), hit.worldPos.y()); createCommand->addValue (referencesTable.findColumnIndex ( CSMWorld::Columns::ColumnId_PositionZPos), hit.worldPos.z()); createCommand->addValue (referencesTable.findColumnIndex ( CSMWorld::Columns::ColumnId_ReferenceableId), QString::fromUtf8 (iter->getId().c_str())); document.getUndoStack().push (createCommand.release()); dropped = true; } if (dropped) event->accept(); } } int CSVRender::InstanceMode::getSubMode() const { return mSubMode ? getSubModeFromId (mSubMode->getCurrentId()) : 0; } void CSVRender::InstanceMode::subModeChanged (const std::string& id) { mSubModeId = id; getWorldspaceWidget().abortDrag(); getWorldspaceWidget().setSubMode (getSubModeFromId (id), Mask_Reference); } void CSVRender::InstanceMode::handleSelectDrag(const QPoint& pos) { osg::Vec3f mousePlanePoint = getMousePlaneCoords(pos, getProjectionSpaceCoords(mSelectionMode->getDragStart())); mSelectionMode->dragEnded (mousePlanePoint, mDragMode); mDragMode = DragMode_None; } void CSVRender::InstanceMode::deleteSelectedInstances(bool active) { std::vector > selection = getWorldspaceWidget().getSelection (Mask_Reference); if (selection.empty()) return; CSMDoc::Document& document = getWorldspaceWidget().getDocument(); CSMWorld::IdTable& referencesTable = dynamic_cast ( *document.getData().getTableModel (CSMWorld::UniversalId::Type_References)); QUndoStack& undoStack = document.getUndoStack(); CSMWorld::CommandMacro macro (undoStack, "Delete Instances"); for(osg::ref_ptr tag: selection) if (CSVRender::ObjectTag *objectTag = dynamic_cast (tag.get())) macro.push(new CSMWorld::DeleteCommand(referencesTable, objectTag->mObject->getReferenceId())); getWorldspaceWidget().clearSelection (Mask_Reference); } void CSVRender::InstanceMode::dropInstance(CSVRender::Object* object, float dropHeight) { object->setEdited(Object::Override_Position); ESM::Position position = object->getPosition(); position.pos[2] -= dropHeight; object->setPosition(position.pos); } float CSVRender::InstanceMode::calculateDropHeight(DropMode dropMode, CSVRender::Object* object, float objectHeight) { osg::Vec3d point = object->getPosition().asVec3(); osg::Vec3d start = point; start.z() += objectHeight; osg::Vec3d end = point; end.z() = std::numeric_limits::lowest(); osg::ref_ptr intersector (new osgUtil::LineSegmentIntersector( osgUtil::Intersector::MODEL, start, end) ); intersector->setIntersectionLimit(osgUtil::LineSegmentIntersector::NO_LIMIT); osgUtil::IntersectionVisitor visitor(intersector); if (dropMode & Terrain) visitor.setTraversalMask(Mask_Terrain); if (dropMode & Collision) visitor.setTraversalMask(Mask_Terrain | Mask_Reference); mParentNode->accept(visitor); osgUtil::LineSegmentIntersector::Intersections::iterator it = intersector->getIntersections().begin(); if (it != intersector->getIntersections().end()) { osgUtil::LineSegmentIntersector::Intersection intersection = *it; float collisionLevel = intersection.getWorldIntersectPoint().z(); return point.z() - collisionLevel + objectHeight; } return 0.0f; } void CSVRender::InstanceMode::dropSelectedInstancesToCollision() { handleDropMethod(Collision, "Drop instances to next collision"); } void CSVRender::InstanceMode::dropSelectedInstancesToTerrain() { handleDropMethod(Terrain, "Drop instances to terrain level"); } void CSVRender::InstanceMode::dropSelectedInstancesToCollisionSeparately() { handleDropMethod(CollisionSep, "Drop instances to next collision level separately"); } void CSVRender::InstanceMode::dropSelectedInstancesToTerrainSeparately() { handleDropMethod(TerrainSep, "Drop instances to terrain level separately"); } void CSVRender::InstanceMode::handleDropMethod(DropMode dropMode, QString commandMsg) { std::vector > selection = getWorldspaceWidget().getSelection (Mask_Reference); if (selection.empty()) return; CSMDoc::Document& document = getWorldspaceWidget().getDocument(); QUndoStack& undoStack = document.getUndoStack(); CSMWorld::CommandMacro macro (undoStack, commandMsg); DropObjectHeightHandler dropObjectDataHandler(&getWorldspaceWidget()); if(dropMode & Separate) { int counter = 0; for (osg::ref_ptr tag : selection) if (CSVRender::ObjectTag* objectTag = dynamic_cast(tag.get())) { float objectHeight = dropObjectDataHandler.mObjectHeights[counter]; float dropHeight = calculateDropHeight(dropMode, objectTag->mObject, objectHeight); dropInstance(objectTag->mObject, dropHeight); objectTag->mObject->apply(macro); counter++; } } else { float smallestDropHeight = std::numeric_limits::max(); int counter = 0; for (osg::ref_ptr tag : selection) if (CSVRender::ObjectTag* objectTag = dynamic_cast(tag.get())) { float objectHeight = dropObjectDataHandler.mObjectHeights[counter]; float thisDrop = calculateDropHeight(dropMode, objectTag->mObject, objectHeight); if (thisDrop < smallestDropHeight) smallestDropHeight = thisDrop; counter++; } for (osg::ref_ptr tag : selection) if (CSVRender::ObjectTag* objectTag = dynamic_cast(tag.get())) { dropInstance(objectTag->mObject, smallestDropHeight); objectTag->mObject->apply(macro); } } } CSVRender::DropObjectHeightHandler::DropObjectHeightHandler(WorldspaceWidget* worldspacewidget) : mWorldspaceWidget(worldspacewidget) { std::vector > selection = mWorldspaceWidget->getSelection (Mask_Reference); for(osg::ref_ptr tag: selection) { if (CSVRender::ObjectTag *objectTag = dynamic_cast (tag.get())) { osg::ref_ptr objectNodeWithGUI = objectTag->mObject->getRootNode(); osg::ref_ptr objectNodeWithoutGUI = objectTag->mObject->getBaseNode(); osg::ComputeBoundsVisitor computeBounds; computeBounds.setTraversalMask(Mask_Reference); objectNodeWithoutGUI->accept(computeBounds); osg::BoundingBox bounds = computeBounds.getBoundingBox(); float boundingBoxOffset = 0.0f; if (bounds.valid()) boundingBoxOffset = bounds.zMin(); mObjectHeights.emplace_back(boundingBoxOffset); mOldMasks.emplace_back(objectNodeWithGUI->getNodeMask()); objectNodeWithGUI->setNodeMask(0); } } } CSVRender::DropObjectHeightHandler::~DropObjectHeightHandler() { std::vector > selection = mWorldspaceWidget->getSelection (Mask_Reference); int counter = 0; for(osg::ref_ptr tag: selection) { if (CSVRender::ObjectTag *objectTag = dynamic_cast (tag.get())) { osg::ref_ptr objectNodeWithGUI = objectTag->mObject->getRootNode(); objectNodeWithGUI->setNodeMask(mOldMasks[counter]); counter++; } } } ================================================ FILE: apps/opencs/view/render/instancemode.hpp ================================================ #ifndef CSV_RENDER_INSTANCEMODE_H #define CSV_RENDER_INSTANCEMODE_H #include #include #include #include #include #include "editmode.hpp" #include "instancedragmodes.hpp" namespace CSVWidget { class SceneToolMode; } namespace CSVRender { class TagBase; class InstanceSelectionMode; class Object; class InstanceMode : public EditMode { Q_OBJECT enum DropMode { Separate = 0b1, Collision = 0b10, Terrain = 0b100, CollisionSep = Collision | Separate, TerrainSep = Terrain | Separate, }; CSVWidget::SceneToolMode *mSubMode; std::string mSubModeId; InstanceSelectionMode *mSelectionMode; DragMode mDragMode; int mDragAxis; bool mLocked; float mUnitScaleDist; osg::ref_ptr mParentNode; int getSubModeFromId (const std::string& id) const; osg::Vec3f quatToEuler(const osg::Quat& quat) const; osg::Quat eulerToQuat(const osg::Vec3f& euler) const; osg::Vec3f getSelectionCenter(const std::vector >& selection) const; osg::Vec3f getScreenCoords(const osg::Vec3f& pos); osg::Vec3f getProjectionSpaceCoords(const osg::Vec3f& pos); osg::Vec3f getMousePlaneCoords(const QPoint& point, const osg::Vec3d& dragStart); void handleSelectDrag(const QPoint& pos); void dropInstance(CSVRender::Object* object, float dropHeight); float calculateDropHeight(DropMode dropMode, CSVRender::Object* object, float objectHeight); public: InstanceMode (WorldspaceWidget *worldspaceWidget, osg::ref_ptr parentNode, QWidget *parent = nullptr); void activate (CSVWidget::SceneToolbar *toolbar) override; void deactivate (CSVWidget::SceneToolbar *toolbar) override; void setEditLock (bool locked) override; void primaryOpenPressed (const WorldspaceHitResult& hit) override; void primaryEditPressed (const WorldspaceHitResult& hit) override; void secondaryEditPressed (const WorldspaceHitResult& hit) override; void primarySelectPressed (const WorldspaceHitResult& hit) override; void secondarySelectPressed (const WorldspaceHitResult& hit) override; bool primaryEditStartDrag (const QPoint& pos) override; bool secondaryEditStartDrag (const QPoint& pos) override; bool primarySelectStartDrag(const QPoint& pos) override; bool secondarySelectStartDrag(const QPoint& pos) override; void drag (const QPoint& pos, int diffX, int diffY, double speedFactor) override; void dragCompleted(const QPoint& pos) override; /// \note dragAborted will not be called, if the drag is aborted via changing /// editing mode void dragAborted() override; void dragWheel (int diff, double speedFactor) override; void dragEnterEvent (QDragEnterEvent *event) override; void dropEvent (QDropEvent *event) override; int getSubMode() const override; signals: void requestFocus (const std::string& id); private slots: void subModeChanged (const std::string& id); void deleteSelectedInstances(bool active); void dropSelectedInstancesToCollision(); void dropSelectedInstancesToTerrain(); void dropSelectedInstancesToCollisionSeparately(); void dropSelectedInstancesToTerrainSeparately(); void handleDropMethod(DropMode dropMode, QString commandMsg); }; /// \brief Helper class to handle object mask data in safe way class DropObjectHeightHandler { public: DropObjectHeightHandler(WorldspaceWidget* worldspacewidget); ~DropObjectHeightHandler(); std::vector mObjectHeights; private: WorldspaceWidget* mWorldspaceWidget; std::vector mOldMasks; }; } #endif ================================================ FILE: apps/opencs/view/render/instancemovemode.cpp ================================================ #include "instancemovemode.hpp" CSVRender::InstanceMoveMode::InstanceMoveMode (QWidget *parent) : ModeButton (QIcon (QPixmap (":scenetoolbar/transform-move")), "Move selected instances" "
  • Use {scene-edit-primary} to move instances around freely
  • " "
  • Use {scene-edit-secondary} to move instances around within the grid
  • " "
" "Grid move not implemented yet", parent) {} ================================================ FILE: apps/opencs/view/render/instancemovemode.hpp ================================================ #ifndef CSV_RENDER_INSTANCEMOVEMODE_H #define CSV_RENDER_INSTANCEMOVEMODE_H #include "../widget/modebutton.hpp" namespace CSVRender { class InstanceMoveMode : public CSVWidget::ModeButton { Q_OBJECT public: InstanceMoveMode (QWidget *parent = nullptr); }; } #endif ================================================ FILE: apps/opencs/view/render/instanceselectionmode.cpp ================================================ #include "instanceselectionmode.hpp" #include #include #include #include #include #include #include #include "../../model/world/idtable.hpp" #include "../../model/world/commands.hpp" #include "instancedragmodes.hpp" #include "worldspacewidget.hpp" #include "object.hpp" namespace CSVRender { InstanceSelectionMode::InstanceSelectionMode(CSVWidget::SceneToolbar* parent, WorldspaceWidget& worldspaceWidget, osg::Group *cellNode) : SelectionMode(parent, worldspaceWidget, Mask_Reference), mParentNode(cellNode) { mSelectSame = new QAction("Extend selection to instances with same object ID", this); mDeleteSelection = new QAction("Delete selected instances", this); connect(mSelectSame, SIGNAL(triggered()), this, SLOT(selectSame())); connect(mDeleteSelection, SIGNAL(triggered()), this, SLOT(deleteSelection())); } InstanceSelectionMode::~InstanceSelectionMode() { mParentNode->removeChild(mBaseNode); } void InstanceSelectionMode::setDragStart(const osg::Vec3d& dragStart) { mDragStart = dragStart; } const osg::Vec3d& InstanceSelectionMode::getDragStart() { return mDragStart; } void InstanceSelectionMode::dragEnded(const osg::Vec3d& dragEndPoint, DragMode dragMode) { float dragDistance = (mDragStart - dragEndPoint).length(); if (mBaseNode) mParentNode->removeChild (mBaseNode); if (getCurrentId() == "cube-centre") { osg::Vec3d pointA(mDragStart[0] - dragDistance, mDragStart[1] - dragDistance, mDragStart[2] - dragDistance); osg::Vec3d pointB(mDragStart[0] + dragDistance, mDragStart[1] + dragDistance, mDragStart[2] + dragDistance); getWorldspaceWidget().selectInsideCube(pointA, pointB, dragMode); } else if (getCurrentId() == "cube-corner") { getWorldspaceWidget().selectInsideCube(mDragStart, dragEndPoint, dragMode); } else if (getCurrentId() == "sphere") { getWorldspaceWidget().selectWithinDistance(mDragStart, dragDistance, dragMode); } } void InstanceSelectionMode::drawSelectionCubeCentre(const osg::Vec3f& mousePlanePoint) { float dragDistance = (mDragStart - mousePlanePoint).length(); drawSelectionCube(mDragStart, dragDistance); } void InstanceSelectionMode::drawSelectionCubeCorner(const osg::Vec3f& mousePlanePoint) { drawSelectionBox(mDragStart, mousePlanePoint); } void InstanceSelectionMode::drawSelectionBox(const osg::Vec3d& pointA, const osg::Vec3d& pointB) { if (mBaseNode) mParentNode->removeChild (mBaseNode); mBaseNode = new osg::PositionAttitudeTransform; mBaseNode->setPosition(pointA); osg::ref_ptr geometry (new osg::Geometry); osg::Vec3Array *vertices = new osg::Vec3Array; vertices->push_back (osg::Vec3f (0.0f, 0.0f, 0.0f)); vertices->push_back (osg::Vec3f (0.0f, 0.0f, pointB[2] - pointA[2])); vertices->push_back (osg::Vec3f (0.0f, pointB[1] - pointA[1], 0.0f)); vertices->push_back (osg::Vec3f (0.0f, pointB[1] - pointA[1], pointB[2] - pointA[2])); vertices->push_back (osg::Vec3f (pointB[0] - pointA[0], 0.0f, 0.0f)); vertices->push_back (osg::Vec3f (pointB[0] - pointA[0], 0.0f, pointB[2] - pointA[2])); vertices->push_back (osg::Vec3f (pointB[0] - pointA[0], pointB[1] - pointA[1], 0.0f)); vertices->push_back (osg::Vec3f (pointB[0] - pointA[0], pointB[1] - pointA[1], pointB[2] - pointA[2])); geometry->setVertexArray (vertices); osg::DrawElementsUShort *primitives = new osg::DrawElementsUShort (osg::PrimitiveSet::TRIANGLES, 0); // top primitives->push_back (2); primitives->push_back (1); primitives->push_back (0); primitives->push_back (3); primitives->push_back (1); primitives->push_back (2); // bottom primitives->push_back (4); primitives->push_back (5); primitives->push_back (6); primitives->push_back (6); primitives->push_back (5); primitives->push_back (7); // sides primitives->push_back (1); primitives->push_back (4); primitives->push_back (0); primitives->push_back (4); primitives->push_back (1); primitives->push_back (5); primitives->push_back (4); primitives->push_back (2); primitives->push_back (0); primitives->push_back (6); primitives->push_back (2); primitives->push_back (4); primitives->push_back (6); primitives->push_back (3); primitives->push_back (2); primitives->push_back (7); primitives->push_back (3); primitives->push_back (6); primitives->push_back (1); primitives->push_back (3); primitives->push_back (5); primitives->push_back (5); primitives->push_back (3); primitives->push_back (7); geometry->addPrimitiveSet (primitives); osg::Vec4Array *colours = new osg::Vec4Array; colours->push_back (osg::Vec4f (0.3f, 0.3f, 0.5f, 0.2f)); colours->push_back (osg::Vec4f (0.9f, 0.9f, 1.0f, 0.2f)); colours->push_back (osg::Vec4f (0.3f, 0.3f, 0.4f, 0.2f)); colours->push_back (osg::Vec4f (0.9f, 0.9f, 1.0f, 0.2f)); colours->push_back (osg::Vec4f (0.3f, 0.3f, 0.4f, 0.2f)); colours->push_back (osg::Vec4f (0.9f, 0.9f, 1.0f, 0.2f)); colours->push_back (osg::Vec4f (0.3f, 0.3f, 0.4f, 0.2f)); colours->push_back (osg::Vec4f (0.9f, 0.9f, 1.0f, 0.2f)); geometry->setColorArray (colours, osg::Array::BIND_PER_VERTEX); geometry->getOrCreateStateSet()->setMode (GL_LIGHTING, osg::StateAttribute::OFF); geometry->getOrCreateStateSet()->setMode (GL_BLEND, osg::StateAttribute::ON); geometry->getOrCreateStateSet()->setMode(GL_CULL_FACE, osg::StateAttribute::OFF); geometry->getOrCreateStateSet()->setRenderingHint(osg::StateSet::TRANSPARENT_BIN); mBaseNode->addChild (geometry); mParentNode->addChild(mBaseNode); } void InstanceSelectionMode::drawSelectionCube(const osg::Vec3d& point, float radius) { if (mBaseNode) mParentNode->removeChild (mBaseNode); mBaseNode = new osg::PositionAttitudeTransform; mBaseNode->setPosition(point); osg::ref_ptr geometry (new osg::Geometry); osg::Vec3Array *vertices = new osg::Vec3Array; for (int i = 0; i < 2; ++i) { float height = i ? -radius : radius; vertices->push_back (osg::Vec3f (height, -radius, -radius)); vertices->push_back (osg::Vec3f (height, -radius, radius)); vertices->push_back (osg::Vec3f (height, radius, -radius)); vertices->push_back (osg::Vec3f (height, radius, radius)); } geometry->setVertexArray (vertices); osg::DrawElementsUShort *primitives = new osg::DrawElementsUShort (osg::PrimitiveSet::TRIANGLES, 0); // top primitives->push_back (2); primitives->push_back (1); primitives->push_back (0); primitives->push_back (3); primitives->push_back (1); primitives->push_back (2); // bottom primitives->push_back (4); primitives->push_back (5); primitives->push_back (6); primitives->push_back (6); primitives->push_back (5); primitives->push_back (7); // sides primitives->push_back (1); primitives->push_back (4); primitives->push_back (0); primitives->push_back (4); primitives->push_back (1); primitives->push_back (5); primitives->push_back (4); primitives->push_back (2); primitives->push_back (0); primitives->push_back (6); primitives->push_back (2); primitives->push_back (4); primitives->push_back (6); primitives->push_back (3); primitives->push_back (2); primitives->push_back (7); primitives->push_back (3); primitives->push_back (6); primitives->push_back (1); primitives->push_back (3); primitives->push_back (5); primitives->push_back (5); primitives->push_back (3); primitives->push_back (7); geometry->addPrimitiveSet (primitives); osg::Vec4Array *colours = new osg::Vec4Array; colours->push_back (osg::Vec4f (0.3f, 0.3f, 0.5f, 0.2f)); colours->push_back (osg::Vec4f (0.9f, 0.9f, 1.0f, 0.2f)); colours->push_back (osg::Vec4f (0.3f, 0.3f, 0.4f, 0.2f)); colours->push_back (osg::Vec4f (0.9f, 0.9f, 1.0f, 0.2f)); colours->push_back (osg::Vec4f (0.3f, 0.3f, 0.4f, 0.2f)); colours->push_back (osg::Vec4f (0.9f, 0.9f, 1.0f, 0.2f)); colours->push_back (osg::Vec4f (0.3f, 0.3f, 0.4f, 0.2f)); colours->push_back (osg::Vec4f (0.9f, 0.9f, 1.0f, 0.2f)); geometry->setColorArray (colours, osg::Array::BIND_PER_VERTEX); geometry->getOrCreateStateSet()->setMode (GL_LIGHTING, osg::StateAttribute::OFF); geometry->getOrCreateStateSet()->setMode (GL_BLEND, osg::StateAttribute::ON); geometry->getOrCreateStateSet()->setMode(GL_CULL_FACE, osg::StateAttribute::OFF); geometry->getOrCreateStateSet()->setRenderingHint(osg::StateSet::TRANSPARENT_BIN); mBaseNode->addChild (geometry); mParentNode->addChild(mBaseNode); } void InstanceSelectionMode::drawSelectionSphere(const osg::Vec3f& mousePlanePoint) { float dragDistance = (mDragStart - mousePlanePoint).length(); drawSelectionSphere(mDragStart, dragDistance); } void InstanceSelectionMode::drawSelectionSphere(const osg::Vec3d& point, float radius) { if (mBaseNode) mParentNode->removeChild (mBaseNode); mBaseNode = new osg::PositionAttitudeTransform; mBaseNode->setPosition(point); osg::ref_ptr geometry (new osg::Geometry); osg::Vec3Array *vertices = new osg::Vec3Array; int resolution = 32; float radiusPerResolution = radius / resolution; float reciprocalResolution = 1.0f / resolution; float doubleReciprocalRes = reciprocalResolution * 2; osg::Vec4Array *colours = new osg::Vec4Array; for (float i = 0.0; i <= resolution; i += 2) { float iShifted = (static_cast(i) - resolution / 2.0f); // i - 16 = -16 ... 16 float xPercentile = iShifted * doubleReciprocalRes; float x = xPercentile * radius; float thisRadius = sqrt (radius * radius - x * x); //the next row float iShifted2 = (static_cast(i + 1) - resolution / 2.0f); float xPercentile2 = iShifted2 * doubleReciprocalRes; float x2 = xPercentile2 * radius; float thisRadius2 = sqrt (radius * radius - x2 * x2); for (int j = 0; j < resolution; ++j) { float vertexX = thisRadius * sin(j * reciprocalResolution * osg::PI * 2); float vertexY = i * radiusPerResolution * 2 - radius; float vertexZ = thisRadius * cos(j * reciprocalResolution * osg::PI * 2); float heightPercentage = (vertexZ + radius) / (radius * 2); vertices->push_back (osg::Vec3f (vertexX, vertexY, vertexZ)); colours->push_back (osg::Vec4f (heightPercentage, heightPercentage, heightPercentage, 0.3f)); float vertexNextRowX = thisRadius2 * sin(j * reciprocalResolution * osg::PI * 2); float vertexNextRowY = (i + 1) * radiusPerResolution * 2 - radius; float vertexNextRowZ = thisRadius2 * cos(j * reciprocalResolution * osg::PI * 2); float heightPercentageNextRow = (vertexZ + radius) / (radius * 2); vertices->push_back (osg::Vec3f (vertexNextRowX, vertexNextRowY, vertexNextRowZ)); colours->push_back (osg::Vec4f (heightPercentageNextRow, heightPercentageNextRow, heightPercentageNextRow, 0.3f)); } } geometry->setVertexArray (vertices); osg::DrawElementsUShort *primitives = new osg::DrawElementsUShort (osg::PrimitiveSet::TRIANGLE_STRIP, 0); for (int i = 0; i < resolution; ++i) { //Even for (int j = 0; j < resolution * 2; ++j) { if (i * resolution * 2 + j > static_cast(vertices->size()) - 1) continue; primitives->push_back (i * resolution * 2 + j); } if (i * resolution * 2 > static_cast(vertices->size()) - 1) continue; primitives->push_back (i * resolution * 2); primitives->push_back (i * resolution * 2 + 1); //Odd for (int j = 1; j < resolution * 2 - 2; j += 2) { if ((i + 1) * resolution * 2 + j - 1 > static_cast(vertices->size()) - 1) continue; primitives->push_back ((i + 1) * resolution * 2 + j - 1); primitives->push_back (i * resolution * 2 + j + 2); } if ((i + 2) * resolution * 2 - 2 > static_cast(vertices->size()) - 1) continue; primitives->push_back ((i + 2) * resolution * 2 - 2); primitives->push_back (i * resolution * 2 + 1); primitives->push_back ((i + 1) * resolution * 2); } geometry->addPrimitiveSet (primitives); geometry->setColorArray (colours, osg::Array::BIND_PER_VERTEX); geometry->getOrCreateStateSet()->setMode (GL_LIGHTING, osg::StateAttribute::OFF); geometry->getOrCreateStateSet()->setMode (GL_BLEND, osg::StateAttribute::ON); geometry->getOrCreateStateSet()->setMode(GL_CULL_FACE, osg::StateAttribute::OFF); geometry->getOrCreateStateSet()->setRenderingHint(osg::StateSet::TRANSPARENT_BIN); mBaseNode->addChild (geometry); mParentNode->addChild(mBaseNode); } bool InstanceSelectionMode::createContextMenu(QMenu* menu) { if (menu) { SelectionMode::createContextMenu(menu); menu->addAction(mSelectSame); menu->addAction(mDeleteSelection); } return true; } void InstanceSelectionMode::selectSame() { getWorldspaceWidget().selectAllWithSameParentId(Mask_Reference); } void InstanceSelectionMode::deleteSelection() { std::vector > selection = getWorldspaceWidget().getSelection(Mask_Reference); CSMWorld::IdTable& referencesTable = dynamic_cast( *getWorldspaceWidget().getDocument().getData().getTableModel(CSMWorld::UniversalId::Type_References)); for (std::vector >::iterator iter = selection.begin(); iter != selection.end(); ++iter) { CSMWorld::DeleteCommand* command = new CSMWorld::DeleteCommand(referencesTable, static_cast(iter->get())->mObject->getReferenceId()); getWorldspaceWidget().getDocument().getUndoStack().push(command); } } } ================================================ FILE: apps/opencs/view/render/instanceselectionmode.hpp ================================================ #ifndef CSV_RENDER_INSTANCE_SELECTION_MODE_H #define CSV_RENDER_INSTANCE_SELECTION_MODE_H #include #include #include #include "selectionmode.hpp" #include "instancedragmodes.hpp" namespace CSVRender { class InstanceSelectionMode : public SelectionMode { Q_OBJECT public: InstanceSelectionMode(CSVWidget::SceneToolbar* parent, WorldspaceWidget& worldspaceWidget, osg::Group *cellNode); ~InstanceSelectionMode(); /// Store the worldspace-coordinate when drag begins void setDragStart(const osg::Vec3d& dragStart); /// Store the worldspace-coordinate when drag begins const osg::Vec3d& getDragStart(); /// Store the screen-coordinate when drag begins void setScreenDragStart(const QPoint& dragStartPoint); /// Apply instance selection changes void dragEnded(const osg::Vec3d& dragEndPoint, DragMode dragMode); void drawSelectionCubeCentre(const osg::Vec3f& mousePlanePoint ); void drawSelectionCubeCorner(const osg::Vec3f& mousePlanePoint ); void drawSelectionSphere(const osg::Vec3f& mousePlanePoint ); protected: /// Add context menu items to \a menu. /// /// \attention menu can be a 0-pointer /// /// \return Have there been any menu items to be added (if menu is 0 and there /// items to be added, the function must return true anyway. bool createContextMenu(QMenu* menu) override; private: void drawSelectionBox(const osg::Vec3d& pointA, const osg::Vec3d& pointB); void drawSelectionCube(const osg::Vec3d& point, float radius); void drawSelectionSphere(const osg::Vec3d& point, float radius); QAction* mDeleteSelection; QAction* mSelectSame; osg::Vec3d mDragStart; osg::Group* mParentNode; osg::ref_ptr mBaseNode; private slots: void deleteSelection(); void selectSame(); }; } #endif ================================================ FILE: apps/opencs/view/render/lighting.cpp ================================================ #include "lighting.hpp" #include #include #include #include class DayNightSwitchVisitor : public osg::NodeVisitor { public: DayNightSwitchVisitor(int index) : osg::NodeVisitor(TRAVERSE_ALL_CHILDREN) , mIndex(index) { } void apply(osg::Switch &switchNode) override { if (switchNode.getName() == Constants::NightDayLabel) switchNode.setSingleChildOn(mIndex); traverse(switchNode); } private: int mIndex; }; CSVRender::Lighting::~Lighting() {} void CSVRender::Lighting::updateDayNightMode(int index) { if (mRootNode == nullptr) return; DayNightSwitchVisitor visitor(index); mRootNode->accept(visitor); } ================================================ FILE: apps/opencs/view/render/lighting.hpp ================================================ #ifndef OPENCS_VIEW_LIGHTING_H #define OPENCS_VIEW_LIGHTING_H #include namespace osg { class Vec4f; class LightSource; class Group; } namespace CSVRender { class Lighting { public: Lighting() : mRootNode(nullptr) {} virtual ~Lighting(); virtual void activate (osg::Group* rootNode, bool isExterior) = 0; virtual void deactivate() = 0; virtual osg::Vec4f getAmbientColour(osg::Vec4f* defaultAmbient) = 0; protected: void updateDayNightMode(int index); osg::ref_ptr mLightSource; osg::Group* mRootNode; }; } #endif ================================================ FILE: apps/opencs/view/render/lightingbright.cpp ================================================ #include "lightingbright.hpp" #include CSVRender::LightingBright::LightingBright() {} void CSVRender::LightingBright::activate (osg::Group* rootNode, bool /*isExterior*/) { mRootNode = rootNode; mLightSource = (new osg::LightSource); osg::ref_ptr light (new osg::Light); light->setAmbient(osg::Vec4f(0.f, 0.f, 0.f, 1.f)); light->setPosition(osg::Vec4f(0.f, 0.f, 1.f, 0.f)); light->setDiffuse(osg::Vec4f(1.f, 1.f, 1.f, 1.f)); light->setSpecular(osg::Vec4f(0.f, 0.f, 0.f, 0.f)); light->setConstantAttenuation(1.f); mLightSource->setLight(light); mRootNode->addChild(mLightSource); updateDayNightMode(0); } void CSVRender::LightingBright::deactivate() { if (mRootNode && mLightSource.get()) mRootNode->removeChild(mLightSource); } osg::Vec4f CSVRender::LightingBright::getAmbientColour(osg::Vec4f* /*defaultAmbient*/) { return osg::Vec4f(1.f, 1.f, 1.f, 1.f); } ================================================ FILE: apps/opencs/view/render/lightingbright.hpp ================================================ #ifndef OPENCS_VIEW_LIGHTING_BRIGHT_H #define OPENCS_VIEW_LIGHTING_BRIGHT_H #include "lighting.hpp" namespace osg { class Light; class Group; } namespace CSVRender { class LightingBright : public Lighting { public: LightingBright(); void activate (osg::Group* rootNode, bool /*isExterior*/) override; void deactivate() override; osg::Vec4f getAmbientColour(osg::Vec4f* defaultAmbient) override; }; } #endif ================================================ FILE: apps/opencs/view/render/lightingday.cpp ================================================ #include "lightingday.hpp" #include CSVRender::LightingDay::LightingDay(){} void CSVRender::LightingDay::activate (osg::Group* rootNode, bool /*isExterior*/) { mRootNode = rootNode; mLightSource = new osg::LightSource; osg::ref_ptr light (new osg::Light); light->setPosition(osg::Vec4f(0.f, 0.f, 1.f, 0.f)); light->setAmbient(osg::Vec4f(0.f, 0.f, 0.f, 1.f)); light->setDiffuse(osg::Vec4f(1.f, 1.f, 1.f, 1.f)); light->setSpecular(osg::Vec4f(0.f, 0.f, 0.f, 0.f)); light->setConstantAttenuation(1.f); mLightSource->setLight(light); mRootNode->addChild(mLightSource); updateDayNightMode(0); } void CSVRender::LightingDay::deactivate() { if (mRootNode && mLightSource.get()) mRootNode->removeChild(mLightSource); } osg::Vec4f CSVRender::LightingDay::getAmbientColour(osg::Vec4f *defaultAmbient) { if (defaultAmbient) return *defaultAmbient; else return osg::Vec4f(0.7f, 0.7f, 0.7f, 1.f); } ================================================ FILE: apps/opencs/view/render/lightingday.hpp ================================================ #ifndef OPENCS_VIEW_LIGHTING_DAY_H #define OPENCS_VIEW_LIGHTING_DAY_H #include "lighting.hpp" namespace CSVRender { class LightingDay : public Lighting { public: LightingDay(); void activate (osg::Group* rootNode, bool /*isExterior*/) override; void deactivate() override; osg::Vec4f getAmbientColour(osg::Vec4f *defaultAmbient) override; }; } #endif ================================================ FILE: apps/opencs/view/render/lightingnight.cpp ================================================ #include "lightingnight.hpp" #include CSVRender::LightingNight::LightingNight() {} void CSVRender::LightingNight::activate (osg::Group* rootNode, bool isExterior) { mRootNode = rootNode; mLightSource = new osg::LightSource; osg::ref_ptr light (new osg::Light); light->setPosition(osg::Vec4f(0.f, 0.f, 1.f, 0.f)); light->setAmbient(osg::Vec4f(0.f, 0.f, 0.f, 1.f)); light->setDiffuse(osg::Vec4f(0.2f, 0.2f, 0.2f, 1.f)); light->setSpecular(osg::Vec4f(0.f, 0.f, 0.f, 0.f)); light->setConstantAttenuation(1.f); mLightSource->setLight(light); mRootNode->addChild(mLightSource); updateDayNightMode(isExterior ? 1 : 0); } void CSVRender::LightingNight::deactivate() { if (mRootNode && mLightSource.get()) mRootNode->removeChild(mLightSource); } osg::Vec4f CSVRender::LightingNight::getAmbientColour(osg::Vec4f *defaultAmbient) { if (defaultAmbient) return *defaultAmbient; else return osg::Vec4f(0.2f, 0.2f, 0.2f, 1.f); } ================================================ FILE: apps/opencs/view/render/lightingnight.hpp ================================================ #ifndef OPENCS_VIEW_LIGHTING_NIGHT_H #define OPENCS_VIEW_LIGHTING_NIGHT_H #include "lighting.hpp" namespace CSVRender { class LightingNight : public Lighting { public: LightingNight(); void activate (osg::Group* rootNode, bool isExterior) override; void deactivate() override; osg::Vec4f getAmbientColour(osg::Vec4f *defaultAmbient) override; }; } #endif ================================================ FILE: apps/opencs/view/render/mask.hpp ================================================ #ifndef CSV_RENDER_ELEMENTS_H #define CSV_RENDER_ELEMENTS_H namespace CSVRender { /// Node masks used on the OSG scene graph in OpenMW-CS. /// @note See the respective file in OpenMW (apps/openmw/mwrender/vismask.hpp) /// for general usage hints about node masks. /// @copydoc MWRender::VisMask enum Mask : unsigned int { // elements that are part of the actual scene Mask_Reference = 0x2, Mask_Pathgrid = 0x4, Mask_Water = 0x8, Mask_Fog = 0x10, Mask_Terrain = 0x20, // used within models Mask_ParticleSystem = 0x100, Mask_Lighting = 0x200, // control elements Mask_CellMarker = 0x10000, Mask_CellArrow = 0x20000, Mask_CellBorder = 0x40000 }; } #endif ================================================ FILE: apps/opencs/view/render/object.cpp ================================================ #include "object.hpp" #include #include #include #include #include #include #include #include #include #include #include #include #include "../../model/world/data.hpp" #include "../../model/world/ref.hpp" #include "../../model/world/refidcollection.hpp" #include "../../model/world/commands.hpp" #include "../../model/world/universalid.hpp" #include "../../model/world/commandmacro.hpp" #include "../../model/world/cellcoordinates.hpp" #include "../../model/prefs/state.hpp" #include #include #include #include #include "actor.hpp" #include "mask.hpp" const float CSVRender::Object::MarkerShaftWidth = 30; const float CSVRender::Object::MarkerShaftBaseLength = 70; const float CSVRender::Object::MarkerHeadWidth = 50; const float CSVRender::Object::MarkerHeadLength = 50; namespace { osg::ref_ptr createErrorCube() { osg::ref_ptr shape(new osg::Box(osg::Vec3f(0,0,0), 50.f)); osg::ref_ptr shapedrawable(new osg::ShapeDrawable); shapedrawable->setShape(shape); osg::ref_ptr geode (new osg::Geode); geode->addDrawable(shapedrawable); return geode; } } CSVRender::ObjectTag::ObjectTag (Object* object) : TagBase (Mask_Reference), mObject (object) {} QString CSVRender::ObjectTag::getToolTip (bool hideBasics) const { return QString::fromUtf8 (mObject->getReferenceableId().c_str()); } CSVRender::ObjectMarkerTag::ObjectMarkerTag (Object* object, int axis) : ObjectTag (object), mAxis (axis) {} void CSVRender::Object::clear() { } void CSVRender::Object::update() { clear(); const CSMWorld::RefIdCollection& referenceables = mData.getReferenceables(); const int TypeIndex = referenceables.findColumnIndex(CSMWorld::Columns::ColumnId_RecordType); const int ModelIndex = referenceables.findColumnIndex (CSMWorld::Columns::ColumnId_Model); int index = referenceables.searchId (mReferenceableId); const ESM::Light* light = nullptr; mBaseNode->removeChildren(0, mBaseNode->getNumChildren()); if (index == -1) { mBaseNode->addChild(createErrorCube()); return; } /// \todo check for Deleted state (error 1) int recordType = referenceables.getData(index, TypeIndex).toInt(); std::string model = referenceables.getData(index, ModelIndex).toString().toUtf8().constData(); if (recordType == CSMWorld::UniversalId::Type_Light) { light = &dynamic_cast& >(referenceables.getRecord(index)).get(); if (model.empty()) model = "marker_light.nif"; } if (recordType == CSMWorld::UniversalId::Type_CreatureLevelledList) { if (model.empty()) model = "marker_creature.nif"; } try { if (recordType == CSMWorld::UniversalId::Type_Npc || recordType == CSMWorld::UniversalId::Type_Creature) { if (!mActor) mActor.reset(new Actor(mReferenceableId, mData)); mActor->update(); mBaseNode->addChild(mActor->getBaseNode()); } else if (!model.empty()) { std::string path = "meshes\\" + model; mResourceSystem->getSceneManager()->getInstance(path, mBaseNode); } else { throw std::runtime_error(mReferenceableId + " has no model"); } } catch (std::exception& e) { // TODO: use error marker mesh Log(Debug::Error) << e.what(); } if (light) { bool isExterior = false; // FIXME SceneUtil::addLight(mBaseNode, light, Mask_ParticleSystem, Mask_Lighting, isExterior); } } void CSVRender::Object::adjustTransform() { if (mReferenceId.empty()) return; ESM::Position position = getPosition(); // position mRootNode->setPosition(mForceBaseToZero ? osg::Vec3() : osg::Vec3f(position.pos[0], position.pos[1], position.pos[2])); // orientation osg::Quat xr (-position.rot[0], osg::Vec3f(1,0,0)); osg::Quat yr (-position.rot[1], osg::Vec3f(0,1,0)); osg::Quat zr (-position.rot[2], osg::Vec3f(0,0,1)); mBaseNode->setAttitude(zr*yr*xr); float scale = getScale(); mBaseNode->setScale(osg::Vec3(scale, scale, scale)); } const CSMWorld::CellRef& CSVRender::Object::getReference() const { if (mReferenceId.empty()) throw std::logic_error ("object does not represent a reference"); return mData.getReferences().getRecord (mReferenceId).get(); } void CSVRender::Object::updateMarker() { for (int i=0; i<3; ++i) { if (mMarker[i]) { mRootNode->removeChild (mMarker[i]); mMarker[i] = osg::ref_ptr(); } if (mSelected) { if (mSubMode==0) { mMarker[i] = makeMoveOrScaleMarker (i); mMarker[i]->setUserData(new ObjectMarkerTag (this, i)); mRootNode->addChild (mMarker[i]); } else if (mSubMode==1) { mMarker[i] = makeRotateMarker (i); mMarker[i]->setUserData(new ObjectMarkerTag (this, i)); mRootNode->addChild (mMarker[i]); } else if (mSubMode==2) { mMarker[i] = makeMoveOrScaleMarker (i); mMarker[i]->setUserData(new ObjectMarkerTag (this, i)); mRootNode->addChild (mMarker[i]); } } } } osg::ref_ptr CSVRender::Object::makeMoveOrScaleMarker (int axis) { osg::ref_ptr geometry (new osg::Geometry); float shaftLength = MarkerShaftBaseLength + mBaseNode->getBound().radius(); // shaft osg::Vec3Array *vertices = new osg::Vec3Array; for (int i=0; i<2; ++i) { float length = i ? shaftLength : MarkerShaftWidth; vertices->push_back (getMarkerPosition (-MarkerShaftWidth/2, -MarkerShaftWidth/2, length, axis)); vertices->push_back (getMarkerPosition (-MarkerShaftWidth/2, MarkerShaftWidth/2, length, axis)); vertices->push_back (getMarkerPosition (MarkerShaftWidth/2, MarkerShaftWidth/2, length, axis)); vertices->push_back (getMarkerPosition (MarkerShaftWidth/2, -MarkerShaftWidth/2, length, axis)); } // head backside vertices->push_back (getMarkerPosition (-MarkerHeadWidth/2, -MarkerHeadWidth/2, shaftLength, axis)); vertices->push_back (getMarkerPosition (-MarkerHeadWidth/2, MarkerHeadWidth/2, shaftLength, axis)); vertices->push_back (getMarkerPosition (MarkerHeadWidth/2, MarkerHeadWidth/2, shaftLength, axis)); vertices->push_back (getMarkerPosition (MarkerHeadWidth/2, -MarkerHeadWidth/2, shaftLength, axis)); // head vertices->push_back (getMarkerPosition (0, 0, shaftLength+MarkerHeadLength, axis)); geometry->setVertexArray (vertices); osg::DrawElementsUShort *primitives = new osg::DrawElementsUShort (osg::PrimitiveSet::TRIANGLES, 0); // shaft for (int i=0; i<4; ++i) { int i2 = i==3 ? 0 : i+1; primitives->push_back (i); primitives->push_back (4+i); primitives->push_back (i2); primitives->push_back (4+i); primitives->push_back (4+i2); primitives->push_back (i2); } // cap primitives->push_back (0); primitives->push_back (1); primitives->push_back (2); primitives->push_back (2); primitives->push_back (3); primitives->push_back (0); // head, backside primitives->push_back (0+8); primitives->push_back (1+8); primitives->push_back (2+8); primitives->push_back (2+8); primitives->push_back (3+8); primitives->push_back (0+8); for (int i=0; i<4; ++i) { primitives->push_back (12); primitives->push_back (8+(i==3 ? 0 : i+1)); primitives->push_back (8+i); } geometry->addPrimitiveSet (primitives); osg::Vec4Array *colours = new osg::Vec4Array; for (int i=0; i<8; ++i) colours->push_back (osg::Vec4f (axis==0 ? 1.0f : 0.2f, axis==1 ? 1.0f : 0.2f, axis==2 ? 1.0f : 0.2f, mMarkerTransparency)); for (int i=8; i<8+4+1; ++i) colours->push_back (osg::Vec4f (axis==0 ? 1.0f : 0.0f, axis==1 ? 1.0f : 0.0f, axis==2 ? 1.0f : 0.0f, mMarkerTransparency)); geometry->setColorArray (colours, osg::Array::BIND_PER_VERTEX); setupCommonMarkerState(geometry); osg::ref_ptr geode (new osg::Geode); geode->addDrawable (geometry); return geode; } osg::ref_ptr CSVRender::Object::makeRotateMarker (int axis) { const float InnerRadius = std::max(MarkerShaftBaseLength, mBaseNode->getBound().radius()); 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 VerticesPerSegment = 4; const size_t IndicesPerSegment = 24; const size_t VertexCount = SegmentCount * VerticesPerSegment; const size_t IndexCount = SegmentCount * IndicesPerSegment; const float Angle = 2 * osg::PI / SegmentCount; const unsigned short IndexPattern[IndicesPerSegment] = { 0, 4, 5, 0, 5, 1, 2, 6, 4, 2, 4, 0, 3, 7, 6, 3, 6, 2, 1, 5, 7, 1, 7, 3 }; osg::ref_ptr geometry = new osg::Geometry(); osg::ref_ptr vertices = new osg::Vec3Array(VertexCount); osg::ref_ptr colors = new osg::Vec4Array(1); osg::ref_ptr primitives = new osg::DrawElementsUShort(osg::PrimitiveSet::TRIANGLES, IndexCount); // prevent some depth collision issues from overlaps osg::Vec3f offset = getMarkerPosition(0, MarkerShaftWidth/4, 0, axis); for (size_t i = 0; i < SegmentCount; ++i) { size_t index = i * VerticesPerSegment; float innerX = InnerRadius * std::cos(i * Angle); float innerY = InnerRadius * std::sin(i * Angle); float outerX = OuterRadius * std::cos(i * Angle); float outerY = OuterRadius * std::sin(i * Angle); vertices->at(index++) = getMarkerPosition(innerX, innerY, MarkerShaftWidth / 2, axis) + offset; vertices->at(index++) = getMarkerPosition(innerX, innerY, -MarkerShaftWidth / 2, axis) + offset; vertices->at(index++) = getMarkerPosition(outerX, outerY, MarkerShaftWidth / 2, axis) + offset; vertices->at(index++) = getMarkerPosition(outerX, outerY, -MarkerShaftWidth / 2, axis) + offset; } colors->at(0) = osg::Vec4f ( axis==0 ? 1.0f : 0.2f, axis==1 ? 1.0f : 0.2f, axis==2 ? 1.0f : 0.2f, mMarkerTransparency); for (size_t i = 0; i < SegmentCount; ++i) { size_t indices[IndicesPerSegment]; for (size_t j = 0; j < IndicesPerSegment; ++j) { indices[j] = i * VerticesPerSegment + j; if (indices[j] >= VertexCount) indices[j] -= VertexCount; } size_t elementOffset = i * IndicesPerSegment; for (size_t j = 0; j < IndicesPerSegment; ++j) { primitives->setElement(elementOffset++, indices[IndexPattern[j]]); } } geometry->setVertexArray(vertices); geometry->setColorArray(colors, osg::Array::BIND_OVERALL); geometry->addPrimitiveSet(primitives); setupCommonMarkerState(geometry); osg::ref_ptr geode = new osg::Geode(); geode->addDrawable (geometry); return geode; } void CSVRender::Object::setupCommonMarkerState(osg::ref_ptr geometry) { osg::ref_ptr state = geometry->getOrCreateStateSet(); state->setMode(GL_LIGHTING, osg::StateAttribute::OFF); state->setMode(GL_BLEND, osg::StateAttribute::ON); state->setRenderingHint(osg::StateSet::TRANSPARENT_BIN); } osg::Vec3f CSVRender::Object::getMarkerPosition (float x, float y, float z, int axis) { switch (axis) { case 2: return osg::Vec3f (x, y, z); case 0: return osg::Vec3f (z, x, y); case 1: return osg::Vec3f (y, z, x); default: throw std::logic_error ("invalid axis for marker geometry"); } } CSVRender::Object::Object (CSMWorld::Data& data, osg::Group* parentNode, const std::string& id, bool referenceable, bool forceBaseToZero) : mData (data), mBaseNode(nullptr), mSelected(false), mParentNode(parentNode), mResourceSystem(data.getResourceSystem().get()), mForceBaseToZero (forceBaseToZero), mScaleOverride (1), mOverrideFlags (0), mSubMode (-1), mMarkerTransparency(0.5f) { mRootNode = new osg::PositionAttitudeTransform; mBaseNode = new osg::PositionAttitudeTransform; mBaseNode->addCullCallback(new SceneUtil::LightListCallback); mOutline = new osgFX::Scribe; mBaseNode->setUserData(new ObjectTag(this)); mRootNode->addChild (mBaseNode); parentNode->addChild (mRootNode); mRootNode->setNodeMask(Mask_Reference); if (referenceable) { mReferenceableId = id; } else { mReferenceId = id; mReferenceableId = getReference().mRefID; } adjustTransform(); update(); updateMarker(); } CSVRender::Object::~Object() { clear(); mParentNode->removeChild (mRootNode); } void CSVRender::Object::setSelected(bool selected) { mSelected = selected; mOutline->removeChild(mBaseNode); mRootNode->removeChild(mOutline); mRootNode->removeChild(mBaseNode); if (selected) { mOutline->addChild(mBaseNode); mRootNode->addChild(mOutline); } else mRootNode->addChild(mBaseNode); mMarkerTransparency = CSMPrefs::get()["Rendering"]["object-marker-alpha"].toDouble(); updateMarker(); } bool CSVRender::Object::getSelected() const { return mSelected; } osg::ref_ptr CSVRender::Object::getRootNode() { return mRootNode; } osg::ref_ptr CSVRender::Object::getBaseNode() { return mBaseNode; } bool CSVRender::Object::referenceableDataChanged (const QModelIndex& topLeft, const QModelIndex& bottomRight) { const CSMWorld::RefIdCollection& referenceables = mData.getReferenceables(); int index = referenceables.searchId (mReferenceableId); if (index!=-1 && index>=topLeft.row() && index<=bottomRight.row()) { adjustTransform(); update(); updateMarker(); return true; } return false; } bool CSVRender::Object::referenceableAboutToBeRemoved (const QModelIndex& parent, int start, int end) { const CSMWorld::RefIdCollection& referenceables = mData.getReferenceables(); int index = referenceables.searchId (mReferenceableId); if (index!=-1 && index>=start && index<=end) { // Deletion of referenceable-type objects is handled outside of Object. if (!mReferenceId.empty()) { adjustTransform(); update(); return true; } } return false; } bool CSVRender::Object::referenceDataChanged (const QModelIndex& topLeft, const QModelIndex& bottomRight) { if (mReferenceId.empty()) return false; const CSMWorld::RefCollection& references = mData.getReferences(); int index = references.searchId (mReferenceId); if (index!=-1 && index>=topLeft.row() && index<=bottomRight.row()) { int columnIndex = references.findColumnIndex (CSMWorld::Columns::ColumnId_ReferenceableId); adjustTransform(); if (columnIndex>=topLeft.column() && columnIndex<=bottomRight.row()) { mReferenceableId = references.getData (index, columnIndex).toString().toUtf8().constData(); update(); updateMarker(); } return true; } return false; } void CSVRender::Object::reloadAssets() { update(); updateMarker(); } std::string CSVRender::Object::getReferenceId() const { return mReferenceId; } std::string CSVRender::Object::getReferenceableId() const { return mReferenceableId; } osg::ref_ptr CSVRender::Object::getTag() const { return static_cast (mBaseNode->getUserData()); } bool CSVRender::Object::isEdited() const { return mOverrideFlags; } void CSVRender::Object::setEdited (int flags) { bool discard = mOverrideFlags & ~flags; int added = flags & ~mOverrideFlags; mOverrideFlags = flags; if (added & Override_Position) for (int i=0; i<3; ++i) mPositionOverride.pos[i] = getReference().mPos.pos[i]; if (added & Override_Rotation) for (int i=0; i<3; ++i) mPositionOverride.rot[i] = getReference().mPos.rot[i]; if (added & Override_Scale) mScaleOverride = getReference().mScale; if (discard) adjustTransform(); } ESM::Position CSVRender::Object::getPosition() const { ESM::Position position = getReference().mPos; if (mOverrideFlags & Override_Position) for (int i=0; i<3; ++i) position.pos[i] = mPositionOverride.pos[i]; if (mOverrideFlags & Override_Rotation) for (int i=0; i<3; ++i) position.rot[i] = mPositionOverride.rot[i]; return position; } float CSVRender::Object::getScale() const { return (mOverrideFlags & Override_Scale) ? mScaleOverride : getReference().mScale; } void CSVRender::Object::setPosition (const float position[3]) { mOverrideFlags |= Override_Position; for (int i=0; i<3; ++i) mPositionOverride.pos[i] = position[i]; adjustTransform(); } void CSVRender::Object::setRotation (const float rotation[3]) { mOverrideFlags |= Override_Rotation; for (int i=0; i<3; ++i) mPositionOverride.rot[i] = rotation[i]; adjustTransform(); } void CSVRender::Object::setScale (float scale) { mOverrideFlags |= Override_Scale; mScaleOverride = scale; adjustTransform(); } void CSVRender::Object::setMarkerTransparency(float value) { mMarkerTransparency = value; updateMarker(); } void CSVRender::Object::apply (CSMWorld::CommandMacro& commands) { const CSMWorld::RefCollection& collection = mData.getReferences(); QAbstractItemModel *model = mData.getTableModel (CSMWorld::UniversalId::Type_References); int recordIndex = collection.getIndex (mReferenceId); if (mOverrideFlags & Override_Position) { //Do cell check first so positions can be compared const CSMWorld::CellRef& ref = collection.getRecord(recordIndex).get(); if (CSMWorld::CellCoordinates::isExteriorCell(ref.mCell)) { // Find cell index at new position std::pair cellIndex = CSMWorld::CellCoordinates::coordinatesToCellIndex( mPositionOverride.pos[0], mPositionOverride.pos[1]); std::pair originalIndex = ref.getCellIndex(); int cellColumn = collection.findColumnIndex (static_cast ( CSMWorld::Columns::ColumnId_Cell)); int refNumColumn = collection.findColumnIndex (static_cast ( CSMWorld::Columns::ColumnId_RefNum)); if (cellIndex != originalIndex) { /// \todo figure out worldspace (not important until multiple worldspaces are supported) std::string cellId = CSMWorld::CellCoordinates (cellIndex).getId (""); commands.push (new CSMWorld::ModifyCommand (*model, model->index (recordIndex, cellColumn), QString::fromUtf8 (cellId.c_str()))); commands.push (new CSMWorld::ModifyCommand( *model, model->index (recordIndex, refNumColumn), 0)); } } for (int i=0; i<3; ++i) { int column = collection.findColumnIndex (static_cast ( CSMWorld::Columns::ColumnId_PositionXPos+i)); commands.push (new CSMWorld::ModifyCommand (*model, model->index (recordIndex, column), mPositionOverride.pos[i])); } } if (mOverrideFlags & Override_Rotation) { for (int i=0; i<3; ++i) { int column = collection.findColumnIndex (static_cast ( CSMWorld::Columns::ColumnId_PositionXRot+i)); commands.push (new CSMWorld::ModifyCommand (*model, model->index (recordIndex, column), osg::RadiansToDegrees(mPositionOverride.rot[i]))); } } if (mOverrideFlags & Override_Scale) { int column = collection.findColumnIndex (CSMWorld::Columns::ColumnId_Scale); commands.push (new CSMWorld::ModifyCommand (*model, model->index (recordIndex, column), mScaleOverride)); } mOverrideFlags = 0; } void CSVRender::Object::setSubMode (int subMode) { if (subMode!=mSubMode) { mSubMode = subMode; updateMarker(); } } void CSVRender::Object::reset() { mOverrideFlags = 0; adjustTransform(); updateMarker(); } ================================================ FILE: apps/opencs/view/render/object.hpp ================================================ #ifndef OPENCS_VIEW_OBJECT_H #define OPENCS_VIEW_OBJECT_H #include #include #include #include #include #include #include "tagbase.hpp" class QModelIndex; class QUndoStack; namespace osg { class PositionAttitudeTransform; class Group; class Node; class Geode; } namespace osgFX { class Scribe; } namespace Resource { class ResourceSystem; } namespace CSMWorld { class Data; struct CellRef; class CommandMacro; } namespace CSVRender { class Actor; class Object; // An object to attach as user data to the osg::Node, allows us to get an Object back from a Node when we are doing a ray query class ObjectTag : public TagBase { public: ObjectTag (Object* object); Object* mObject; QString getToolTip (bool hideBasics) const override; }; class ObjectMarkerTag : public ObjectTag { public: ObjectMarkerTag (Object* object, int axis); int mAxis; }; class Object { public: enum OverrideFlags { Override_Position = 1, Override_Rotation = 2, Override_Scale = 4 }; private: static const float MarkerShaftWidth; static const float MarkerShaftBaseLength; static const float MarkerHeadWidth; static const float MarkerHeadLength; CSMWorld::Data& mData; std::string mReferenceId; std::string mReferenceableId; osg::ref_ptr mRootNode; osg::ref_ptr mBaseNode; osg::ref_ptr mOutline; bool mSelected; osg::Group* mParentNode; Resource::ResourceSystem* mResourceSystem; bool mForceBaseToZero; ESM::Position mPositionOverride; float mScaleOverride; int mOverrideFlags; osg::ref_ptr mMarker[3]; int mSubMode; float mMarkerTransparency; std::unique_ptr mActor; /// Not implemented Object (const Object&); /// Not implemented Object& operator= (const Object&); /// Remove object from node (includes deleting) void clear(); /// Update model /// @note Make sure adjustTransform() was called first so world space particles get positioned correctly void update(); /// Adjust position, orientation and scale void adjustTransform(); /// Throws an exception if *this was constructed with referenceable const CSMWorld::CellRef& getReference() const; void updateMarker(); osg::ref_ptr makeMoveOrScaleMarker (int axis); osg::ref_ptr makeRotateMarker (int axis); /// Sets up a stateset with properties common to all marker types. void setupCommonMarkerState(osg::ref_ptr geometry); osg::Vec3f getMarkerPosition (float x, float y, float z, int axis); public: Object (CSMWorld::Data& data, osg::Group *cellNode, const std::string& id, bool referenceable, bool forceBaseToZero = false); /// \param forceBaseToZero If this is a reference ignore the coordinates and place /// it at 0, 0, 0 instead. ~Object(); /// Mark the object as selected, selected objects show an outline effect void setSelected(bool selected); bool getSelected() const; /// Get object node with GUI graphics osg::ref_ptr getRootNode(); /// Get object node without GUI graphics osg::ref_ptr getBaseNode(); /// \return Did this call result in a modification of the visual representation of /// this object? bool referenceableDataChanged (const QModelIndex& topLeft, const QModelIndex& bottomRight); /// \return Did this call result in a modification of the visual representation of /// this object? bool referenceableAboutToBeRemoved (const QModelIndex& parent, int start, int end); /// \return Did this call result in a modification of the visual representation of /// this object? bool referenceDataChanged (const QModelIndex& topLeft, const QModelIndex& bottomRight); /// Reloads the underlying asset void reloadAssets(); /// Returns an empty string if this is a refereceable-type object. std::string getReferenceId() const; std::string getReferenceableId() const; osg::ref_ptr getTag() const; /// Is there currently an editing operation running on this object? bool isEdited() const; void setEdited (int flags); ESM::Position getPosition() const; float getScale() const; /// Set override position. void setPosition (const float position[3]); /// Set override rotation void setRotation (const float rotation[3]); /// Set override scale void setScale (float scale); void setMarkerTransparency(float value); /// Apply override changes via command and end edit mode void apply (CSMWorld::CommandMacro& commands); void setSubMode (int subMode); /// Erase all overrides and restore the visual representation of the object to its /// true state. void reset(); }; } #endif ================================================ FILE: apps/opencs/view/render/orbitcameramode.cpp ================================================ #include "orbitcameramode.hpp" #include #include "../../model/prefs/shortcut.hpp" #include "worldspacewidget.hpp" namespace CSVRender { OrbitCameraMode::OrbitCameraMode(WorldspaceWidget* worldspaceWidget, const QIcon& icon, const QString& tooltip, QWidget* parent) : ModeButton(icon, tooltip, parent) , mWorldspaceWidget(worldspaceWidget) , mCenterOnSelection(nullptr) { mCenterShortcut = new CSMPrefs::Shortcut("orbit-center-selection", worldspaceWidget); mCenterShortcut->enable(false); connect(mCenterShortcut, SIGNAL(activated()), this, SLOT(centerSelection())); } OrbitCameraMode::~OrbitCameraMode() { } void OrbitCameraMode::activate(CSVWidget::SceneToolbar* toolbar) { mCenterOnSelection = new QAction("Center on selected object", this); mCenterShortcut->associateAction(mCenterOnSelection); connect(mCenterOnSelection, SIGNAL(triggered()), this, SLOT(centerSelection())); mCenterShortcut->enable(true); } void OrbitCameraMode::deactivate(CSVWidget::SceneToolbar* toolbar) { mCenterShortcut->associateAction(nullptr); mCenterShortcut->enable(false); } bool OrbitCameraMode::createContextMenu(QMenu* menu) { if (menu) { menu->addAction(mCenterOnSelection); } return true; } void OrbitCameraMode::centerSelection() { mWorldspaceWidget->centerOrbitCameraOnSelection(); } } ================================================ FILE: apps/opencs/view/render/orbitcameramode.hpp ================================================ #ifndef CSV_RENDER_ORBITCAMERAPICKMODE_H #define CSV_RENDER_ORBITCAMERAPICKMODE_H #include #include "../widget/modebutton.hpp" namespace CSMPrefs { class Shortcut; } namespace CSVRender { class WorldspaceWidget; class OrbitCameraMode : public CSVWidget::ModeButton { Q_OBJECT public: OrbitCameraMode(WorldspaceWidget* worldspaceWidget, const QIcon& icon, const QString& tooltip = "", QWidget* parent = nullptr); ~OrbitCameraMode(); void activate(CSVWidget::SceneToolbar* toolbar) override; void deactivate(CSVWidget::SceneToolbar* toolbar) override; bool createContextMenu(QMenu* menu) override; private: WorldspaceWidget* mWorldspaceWidget; QAction* mCenterOnSelection; CSMPrefs::Shortcut* mCenterShortcut; private slots: void centerSelection(); }; } #endif ================================================ FILE: apps/opencs/view/render/pagedworldspacewidget.cpp ================================================ #include "pagedworldspacewidget.hpp" #include #include #include #include #include #include #include "../../model/prefs/shortcut.hpp" #include "../../model/world/idtable.hpp" #include "../widget/scenetooltoggle2.hpp" #include "../widget/scenetoolmode.hpp" #include "editmode.hpp" #include "mask.hpp" #include "cameracontroller.hpp" #include "cellarrow.hpp" #include "terraintexturemode.hpp" #include "terrainshapemode.hpp" bool CSVRender::PagedWorldspaceWidget::adjustCells() { bool modified = false; const CSMWorld::IdCollection& cells = mDocument.getData().getCells(); { // remove/update std::map::iterator iter (mCells.begin()); while (iter!=mCells.end()) { if (!mSelection.has (iter->first)) { // remove delete iter->second; mCells.erase (iter++); modified = true; } else { // update int index = cells.searchId (iter->first.getId (mWorldspace)); bool deleted = index==-1 || cells.getRecord (index).mState==CSMWorld::RecordBase::State_Deleted; if (deleted!=iter->second->isDeleted()) { modified = true; std::unique_ptr cell (new Cell (mDocument.getData(), mRootNode, iter->first.getId (mWorldspace), deleted)); delete iter->second; iter->second = cell.release(); } else if (!deleted) { // delete state has not changed -> just update // TODO check if name or region field has changed (cell marker) // FIXME: config setting //std::string name = cells.getRecord(index).get().mName; //std::string region = cells.getRecord(index).get().mRegion; modified = true; } ++iter; } } } // add for (CSMWorld::CellSelection::Iterator iter (mSelection.begin()); iter!=mSelection.end(); ++iter) { if (mCells.find (*iter)==mCells.end()) { addCellToScene (*iter); modified = true; } } if (modified) { for (std::map::const_iterator iter (mCells.begin()); iter!=mCells.end(); ++iter) { int mask = 0; for (int i=CellArrow::Direction_North; i<=CellArrow::Direction_East; i *= 2) { CSMWorld::CellCoordinates coordinates (iter->second->getCoordinates()); switch (i) { case CellArrow::Direction_North: coordinates = coordinates.move (0, 1); break; case CellArrow::Direction_West: coordinates = coordinates.move (-1, 0); break; case CellArrow::Direction_South: coordinates = coordinates.move (0, -1); break; case CellArrow::Direction_East: coordinates = coordinates.move (1, 0); break; } if (!mSelection.has (coordinates)) mask |= i; } iter->second->setCellArrows (mask); } } return modified; } void CSVRender::PagedWorldspaceWidget::addVisibilitySelectorButtons ( CSVWidget::SceneToolToggle2 *tool) { WorldspaceWidget::addVisibilitySelectorButtons (tool); tool->addButton (Button_Terrain, Mask_Terrain, "Terrain"); tool->addButton (Button_Fog, Mask_Fog, "Fog", "", true); } void CSVRender::PagedWorldspaceWidget::addEditModeSelectorButtons ( CSVWidget::SceneToolMode *tool) { WorldspaceWidget::addEditModeSelectorButtons (tool); /// \todo replace EditMode with suitable subclasses tool->addButton ( new TerrainShapeMode (this, mRootNode, tool), "terrain-shape"); tool->addButton ( new TerrainTextureMode (this, mRootNode, tool), "terrain-texture"); tool->addButton ( new EditMode (this, QIcon (":placeholder"), Mask_Reference, "Terrain vertex paint editing"), "terrain-vertex"); tool->addButton ( new EditMode (this, QIcon (":placeholder"), Mask_Reference, "Terrain movement"), "terrain-move"); } void CSVRender::PagedWorldspaceWidget::handleInteractionPress (const WorldspaceHitResult& hit, InteractionType type) { if (hit.tag && hit.tag->getMask()==Mask_CellArrow) { if (CellArrowTag *cellArrowTag = dynamic_cast (hit.tag.get())) { CellArrow *arrow = cellArrowTag->getCellArrow(); CSMWorld::CellCoordinates coordinates = arrow->getCoordinates(); CellArrow::Direction direction = arrow->getDirection(); int x = 0; int y = 0; switch (direction) { case CellArrow::Direction_North: y = 1; break; case CellArrow::Direction_West: x = -1; break; case CellArrow::Direction_South: y = -1; break; case CellArrow::Direction_East: x = 1; break; } bool modified = false; if (type == InteractionType_PrimarySelect) { addCellSelection (x, y); modified = true; } else if (type == InteractionType_SecondarySelect) { moveCellSelection (x, y); modified = true; } else // Primary/SecondaryEdit { CSMWorld::CellCoordinates newCoordinates = coordinates.move (x, y); if (mCells.find (newCoordinates)==mCells.end()) { addCellToScene (newCoordinates); mSelection.add (newCoordinates); modified = true; } if (type == InteractionType_SecondaryEdit) { if (mCells.find (coordinates)!=mCells.end()) { removeCellFromScene (coordinates); mSelection.remove (coordinates); modified = true; } } } if (modified) adjustCells(); return; } } WorldspaceWidget::handleInteractionPress (hit, type); } void CSVRender::PagedWorldspaceWidget::referenceableDataChanged (const QModelIndex& topLeft, const QModelIndex& bottomRight) { for (std::map::iterator iter (mCells.begin()); iter!=mCells.end(); ++iter) if (iter->second->referenceableDataChanged (topLeft, bottomRight)) flagAsModified(); } void CSVRender::PagedWorldspaceWidget::referenceableAboutToBeRemoved ( const QModelIndex& parent, int start, int end) { for (std::map::iterator iter (mCells.begin()); iter!=mCells.end(); ++iter) if (iter->second->referenceableAboutToBeRemoved (parent, start, end)) flagAsModified(); } void CSVRender::PagedWorldspaceWidget::referenceableAdded (const QModelIndex& parent, int start, int end) { CSMWorld::IdTable& referenceables = dynamic_cast ( *mDocument.getData().getTableModel (CSMWorld::UniversalId::Type_Referenceables)); for (std::map::iterator iter (mCells.begin()); iter!=mCells.end(); ++iter) { QModelIndex topLeft = referenceables.index (start, 0); QModelIndex bottomRight = referenceables.index (end, referenceables.columnCount()); if (iter->second->referenceableDataChanged (topLeft, bottomRight)) flagAsModified(); } } void CSVRender::PagedWorldspaceWidget::referenceDataChanged (const QModelIndex& topLeft, const QModelIndex& bottomRight) { for (std::map::iterator iter (mCells.begin()); iter!=mCells.end(); ++iter) if (iter->second->referenceDataChanged (topLeft, bottomRight)) flagAsModified(); } void CSVRender::PagedWorldspaceWidget::referenceAboutToBeRemoved (const QModelIndex& parent, int start, int end) { for (std::map::iterator iter (mCells.begin()); iter!=mCells.end(); ++iter) if (iter->second->referenceAboutToBeRemoved (parent, start, end)) flagAsModified(); } void CSVRender::PagedWorldspaceWidget::referenceAdded (const QModelIndex& parent, int start, int end) { for (std::map::iterator iter (mCells.begin()); iter!=mCells.end(); ++iter) if (iter->second->referenceAdded (parent, start, end)) flagAsModified(); } void CSVRender::PagedWorldspaceWidget::pathgridDataChanged (const QModelIndex& topLeft, const QModelIndex& bottomRight) { const CSMWorld::SubCellCollection& pathgrids = mDocument.getData().getPathgrids(); int rowStart = -1; int rowEnd = -1; if (topLeft.parent().isValid()) { rowStart = topLeft.parent().row(); rowEnd = bottomRight.parent().row(); } else { rowStart = topLeft.row(); rowEnd = bottomRight.row(); } for (int row = rowStart; row <= rowEnd; ++row) { const CSMWorld::Pathgrid& pathgrid = pathgrids.getRecord(row).get(); CSMWorld::CellCoordinates coords = CSMWorld::CellCoordinates(pathgrid.mData.mX, pathgrid.mData.mY); std::map::iterator searchResult = mCells.find(coords); if (searchResult != mCells.end()) { searchResult->second->pathgridModified(); flagAsModified(); } } } void CSVRender::PagedWorldspaceWidget::pathgridAboutToBeRemoved (const QModelIndex& parent, int start, int end) { const CSMWorld::SubCellCollection& pathgrids = mDocument.getData().getPathgrids(); if (!parent.isValid()) { // Pathgrid going to be deleted for (int row = start; row <= end; ++row) { const CSMWorld::Pathgrid& pathgrid = pathgrids.getRecord(row).get(); CSMWorld::CellCoordinates coords = CSMWorld::CellCoordinates(pathgrid.mData.mX, pathgrid.mData.mY); std::map::iterator searchResult = mCells.find(coords); if (searchResult != mCells.end()) { searchResult->second->pathgridRemoved(); flagAsModified(); } } } } void CSVRender::PagedWorldspaceWidget::pathgridAdded(const QModelIndex& parent, int start, int end) { const CSMWorld::SubCellCollection& pathgrids = mDocument.getData().getPathgrids(); if (!parent.isValid()) { for (int row = start; row <= end; ++row) { const CSMWorld::Pathgrid& pathgrid = pathgrids.getRecord(row).get(); CSMWorld::CellCoordinates coords = CSMWorld::CellCoordinates(pathgrid.mData.mX, pathgrid.mData.mY); std::map::iterator searchResult = mCells.find(coords); if (searchResult != mCells.end()) { searchResult->second->pathgridModified(); flagAsModified(); } } } } void CSVRender::PagedWorldspaceWidget::landDataChanged (const QModelIndex& topLeft, const QModelIndex& bottomRight) { for (int r = topLeft.row(); r <= bottomRight.row(); ++r) { std::string id = mDocument.getData().getLand().getId(r); auto cellIt = mCells.find(CSMWorld::CellCoordinates::fromId(id).first); if (cellIt != mCells.end()) { cellIt->second->landDataChanged(topLeft, bottomRight); flagAsModified(); } } } void CSVRender::PagedWorldspaceWidget::landAboutToBeRemoved (const QModelIndex& parent, int start, int end) { for (int r = start; r <= end; ++r) { std::string id = mDocument.getData().getLand().getId(r); auto cellIt = mCells.find(CSMWorld::CellCoordinates::fromId(id).first); if (cellIt != mCells.end()) { cellIt->second->landAboutToBeRemoved(parent, start, end); flagAsModified(); } } } void CSVRender::PagedWorldspaceWidget::landAdded (const QModelIndex& parent, int start, int end) { for (int r = start; r <= end; ++r) { std::string id = mDocument.getData().getLand().getId(r); auto cellIt = mCells.find(CSMWorld::CellCoordinates::fromId(id).first); if (cellIt != mCells.end()) { cellIt->second->landAdded(parent, start, end); flagAsModified(); } } } void CSVRender::PagedWorldspaceWidget::landTextureDataChanged (const QModelIndex& topLeft, const QModelIndex& bottomRight) { for (auto cellIt : mCells) cellIt.second->landTextureChanged(topLeft, bottomRight); flagAsModified(); } void CSVRender::PagedWorldspaceWidget::landTextureAboutToBeRemoved (const QModelIndex& parent, int start, int end) { for (auto cellIt : mCells) cellIt.second->landTextureAboutToBeRemoved(parent, start, end); flagAsModified(); } void CSVRender::PagedWorldspaceWidget::landTextureAdded (const QModelIndex& parent, int start, int end) { for (auto cellIt : mCells) cellIt.second->landTextureAdded(parent, start, end); flagAsModified(); } std::string CSVRender::PagedWorldspaceWidget::getStartupInstruction() { osg::Vec3d eye, center, up; mView->getCamera()->getViewMatrixAsLookAt(eye, center, up); osg::Vec3d position = eye; std::ostringstream stream; stream << "player->position " << position.x() << ", " << position.y() << ", " << position.z() << ", 0"; return stream.str(); } void CSVRender::PagedWorldspaceWidget::addCellToScene ( const CSMWorld::CellCoordinates& coordinates) { const CSMWorld::IdCollection& cells = mDocument.getData().getCells(); int index = cells.searchId (coordinates.getId (mWorldspace)); bool deleted = index==-1 || cells.getRecord (index).mState==CSMWorld::RecordBase::State_Deleted; std::unique_ptr cell ( new Cell (mDocument.getData(), mRootNode, coordinates.getId (mWorldspace), deleted)); EditMode *editMode = getEditMode(); cell->setSubMode (editMode->getSubMode(), editMode->getInteractionMask()); mCells.insert (std::make_pair (coordinates, cell.release())); } void CSVRender::PagedWorldspaceWidget::removeCellFromScene ( const CSMWorld::CellCoordinates& coordinates) { std::map::iterator iter = mCells.find (coordinates); if (iter!=mCells.end()) { delete iter->second; mCells.erase (iter); } } void CSVRender::PagedWorldspaceWidget::addCellSelection (int x, int y) { CSMWorld::CellSelection newSelection = mSelection; newSelection.move (x, y); for (CSMWorld::CellSelection::Iterator iter (newSelection.begin()); iter!=newSelection.end(); ++iter) { if (mCells.find (*iter)==mCells.end()) { addCellToScene (*iter); mSelection.add (*iter); } } } void CSVRender::PagedWorldspaceWidget::moveCellSelection (int x, int y) { CSMWorld::CellSelection newSelection = mSelection; newSelection.move (x, y); for (CSMWorld::CellSelection::Iterator iter (mSelection.begin()); iter!=mSelection.end(); ++iter) { if (!newSelection.has (*iter)) removeCellFromScene (*iter); } for (CSMWorld::CellSelection::Iterator iter (newSelection.begin()); iter!=newSelection.end(); ++iter) { if (!mSelection.has (*iter)) addCellToScene (*iter); } mSelection = newSelection; } void CSVRender::PagedWorldspaceWidget::addCellToSceneFromCamera (int offsetX, int offsetY) { osg::Vec3f eye, center, up; getCamera()->getViewMatrixAsLookAt(eye, center, up); int cellX = (int)std::floor(center.x() / Constants::CellSizeInUnits) + offsetX; int cellY = (int)std::floor(center.y() / Constants::CellSizeInUnits) + offsetY; CSMWorld::CellCoordinates cellCoordinates(cellX, cellY); if (!mSelection.has(cellCoordinates)) { addCellToScene(cellCoordinates); mSelection.add(cellCoordinates); adjustCells(); } } CSVRender::PagedWorldspaceWidget::PagedWorldspaceWidget (QWidget* parent, CSMDoc::Document& document) : WorldspaceWidget (document, parent), mDocument (document), mWorldspace ("std::default"), mControlElements(nullptr), mDisplayCellCoord(true) { QAbstractItemModel *cells = document.getData().getTableModel (CSMWorld::UniversalId::Type_Cells); connect (cells, SIGNAL (dataChanged (const QModelIndex&, const QModelIndex&)), this, SLOT (cellDataChanged (const QModelIndex&, const QModelIndex&))); connect (cells, SIGNAL (rowsRemoved (const QModelIndex&, int, int)), this, SLOT (cellRemoved (const QModelIndex&, int, int))); connect (cells, SIGNAL (rowsInserted (const QModelIndex&, int, int)), this, SLOT (cellAdded (const QModelIndex&, int, int))); connect (&document.getData(), SIGNAL (assetTablesChanged ()), this, SLOT (assetTablesChanged ())); QAbstractItemModel *lands = document.getData().getTableModel (CSMWorld::UniversalId::Type_Lands); connect (lands, SIGNAL (dataChanged (const QModelIndex&, const QModelIndex&)), this, SLOT (landDataChanged (const QModelIndex&, const QModelIndex&))); connect (lands, SIGNAL (rowsAboutToBeRemoved (const QModelIndex&, int, int)), this, SLOT (landAboutToBeRemoved (const QModelIndex&, int, int))); connect (lands, SIGNAL (rowsInserted (const QModelIndex&, int, int)), this, SLOT (landAdded (const QModelIndex&, int, int))); QAbstractItemModel *ltexs = document.getData().getTableModel (CSMWorld::UniversalId::Type_LandTextures); connect (ltexs, SIGNAL (dataChanged (const QModelIndex&, const QModelIndex&)), this, SLOT (landTextureDataChanged (const QModelIndex&, const QModelIndex&))); connect (ltexs, SIGNAL (rowsAboutToBeRemoved (const QModelIndex&, int, int)), this, SLOT (landTextureAboutToBeRemoved (const QModelIndex&, int, int))); connect (ltexs, SIGNAL (rowsInserted (const QModelIndex&, int, int)), this, SLOT (landTextureAdded (const QModelIndex&, int, int))); // Shortcuts CSMPrefs::Shortcut* loadCameraCellShortcut = new CSMPrefs::Shortcut("scene-load-cam-cell", this); connect(loadCameraCellShortcut, SIGNAL(activated()), this, SLOT(loadCameraCell())); CSMPrefs::Shortcut* loadCameraEastCellShortcut = new CSMPrefs::Shortcut("scene-load-cam-eastcell", this); connect(loadCameraEastCellShortcut, SIGNAL(activated()), this, SLOT(loadEastCell())); CSMPrefs::Shortcut* loadCameraNorthCellShortcut = new CSMPrefs::Shortcut("scene-load-cam-northcell", this); connect(loadCameraNorthCellShortcut, SIGNAL(activated()), this, SLOT(loadNorthCell())); CSMPrefs::Shortcut* loadCameraWestCellShortcut = new CSMPrefs::Shortcut("scene-load-cam-westcell", this); connect(loadCameraWestCellShortcut, SIGNAL(activated()), this, SLOT(loadWestCell())); CSMPrefs::Shortcut* loadCameraSouthCellShortcut = new CSMPrefs::Shortcut("scene-load-cam-southcell", this); connect(loadCameraSouthCellShortcut, SIGNAL(activated()), this, SLOT(loadSouthCell())); } CSVRender::PagedWorldspaceWidget::~PagedWorldspaceWidget() { for (std::map::iterator iter (mCells.begin()); iter!=mCells.end(); ++iter) { delete iter->second; } } void CSVRender::PagedWorldspaceWidget::useViewHint (const std::string& hint) { if (!hint.empty()) { CSMWorld::CellSelection selection; if (hint[0]=='c') { // syntax: c:#x1 y1; #x2 y2 (number of coordinate pairs can be 0 or larger) char ignore; std::istringstream stream (hint.c_str()); if (stream >> ignore) { char ignore1; // : or ; char ignore2; // # // Current coordinate int x, y; // Loop through all the coordinates to add them to selection while (stream >> ignore1 >> ignore2 >> x >> y) selection.add (CSMWorld::CellCoordinates (x, y)); // Mark that camera needs setup mCamPositionSet=false; } } else if (hint[0]=='r') { // syntax r:ref#number (e.g. r:ref#100) char ignore; std::istringstream stream (hint.c_str()); if (stream >> ignore) // ignore r { char ignore1; // : or ; std::string refCode; // ref#number (e.g. ref#100) while (stream >> ignore1 >> refCode) {} //Find out cell coordinate CSMWorld::IdTable& references = dynamic_cast ( *mDocument.getData().getTableModel (CSMWorld::UniversalId::Type_References)); int cellColumn = references.findColumnIndex(CSMWorld::Columns::ColumnId_Cell); QVariant cell = references.data(references.getModelIndex(refCode, cellColumn)).value(); QString cellqs = cell.toString(); std::istringstream streamCellCoord (cellqs.toStdString().c_str()); if (streamCellCoord >> ignore) //ignore # { // Current coordinate int x, y; // Loop through all the coordinates to add them to selection while (streamCellCoord >> x >> y) selection.add (CSMWorld::CellCoordinates (x, y)); // Mark that camera needs setup mCamPositionSet=false; } } } setCellSelection (selection); } } void CSVRender::PagedWorldspaceWidget::setCellSelection (const CSMWorld::CellSelection& selection) { mSelection = selection; if (adjustCells()) flagAsModified(); emit cellSelectionChanged (mSelection); } const CSMWorld::CellSelection& CSVRender::PagedWorldspaceWidget::getCellSelection() const { return mSelection; } std::pair< int, int > CSVRender::PagedWorldspaceWidget::getCoordinatesFromId (const std::string& record) const { std::istringstream stream (record.c_str()); char ignore; int x, y; stream >> ignore >> x >> y; return std::make_pair(x, y); } bool CSVRender::PagedWorldspaceWidget::handleDrop ( const std::vector< CSMWorld::UniversalId >& universalIdData, DropType type) { if (WorldspaceWidget::handleDrop (universalIdData, type)) return true; if (type!=Type_CellsExterior) return false; bool selectionChanged = false; for (unsigned i = 0; i < universalIdData.size(); ++i) { std::pair coordinates(getCoordinatesFromId(universalIdData[i].getId())); if (mSelection.add(CSMWorld::CellCoordinates(coordinates.first, coordinates.second))) { selectionChanged = true; } } if (selectionChanged) { if (adjustCells()) flagAsModified(); emit cellSelectionChanged(mSelection); } return true; } CSVRender::WorldspaceWidget::dropRequirments CSVRender::PagedWorldspaceWidget::getDropRequirements (CSVRender::WorldspaceWidget::DropType type) const { dropRequirments requirements = WorldspaceWidget::getDropRequirements (type); if (requirements!=ignored) return requirements; switch (type) { case Type_CellsExterior: return canHandle; case Type_CellsInterior: return needUnpaged; default: return ignored; } } unsigned int CSVRender::PagedWorldspaceWidget::getVisibilityMask() const { return WorldspaceWidget::getVisibilityMask() | mControlElements->getSelectionMask(); } void CSVRender::PagedWorldspaceWidget::clearSelection (int elementMask) { for (std::map::iterator iter = mCells.begin(); iter!=mCells.end(); ++iter) iter->second->setSelection (elementMask, Cell::Selection_Clear); flagAsModified(); } void CSVRender::PagedWorldspaceWidget::invertSelection (int elementMask) { for (std::map::iterator iter = mCells.begin(); iter!=mCells.end(); ++iter) iter->second->setSelection (elementMask, Cell::Selection_Invert); flagAsModified(); } void CSVRender::PagedWorldspaceWidget::selectAll (int elementMask) { for (std::map::iterator iter = mCells.begin(); iter!=mCells.end(); ++iter) iter->second->setSelection (elementMask, Cell::Selection_All); flagAsModified(); } void CSVRender::PagedWorldspaceWidget::selectAllWithSameParentId (int elementMask) { for (std::map::iterator iter = mCells.begin(); iter!=mCells.end(); ++iter) iter->second->selectAllWithSameParentId (elementMask); flagAsModified(); } void CSVRender::PagedWorldspaceWidget::selectInsideCube(const osg::Vec3d& pointA, const osg::Vec3d& pointB, DragMode dragMode) { for (auto& cell : mCells) { cell.second->selectInsideCube (pointA, pointB, dragMode); } } void CSVRender::PagedWorldspaceWidget::selectWithinDistance(const osg::Vec3d& point, float distance, DragMode dragMode) { for (auto& cell : mCells) { cell.second->selectWithinDistance (point, distance, dragMode); } } std::string CSVRender::PagedWorldspaceWidget::getCellId (const osg::Vec3f& point) const { CSMWorld::CellCoordinates cellCoordinates ( static_cast (std::floor (point.x() / Constants::CellSizeInUnits)), static_cast (std::floor (point.y() / Constants::CellSizeInUnits))); return cellCoordinates.getId (mWorldspace); } CSVRender::Cell* CSVRender::PagedWorldspaceWidget::getCell(const osg::Vec3d& point) const { CSMWorld::CellCoordinates coords( static_cast (std::floor (point.x() / Constants::CellSizeInUnits)), static_cast (std::floor (point.y() / Constants::CellSizeInUnits))); std::map::const_iterator searchResult = mCells.find(coords); if (searchResult != mCells.end()) return searchResult->second; else return nullptr; } CSVRender::Cell* CSVRender::PagedWorldspaceWidget::getCell(const CSMWorld::CellCoordinates& coords) const { std::map::const_iterator searchResult = mCells.find(coords); if (searchResult != mCells.end()) return searchResult->second; else return nullptr; } void CSVRender::PagedWorldspaceWidget::setCellAlteredHeight(const CSMWorld::CellCoordinates& coords, int inCellX, int inCellY, float height) { std::map::iterator searchResult = mCells.find(coords); if (searchResult != mCells.end()) searchResult->second->setAlteredHeight(inCellX, inCellY, height); } float* CSVRender::PagedWorldspaceWidget::getCellAlteredHeight(const CSMWorld::CellCoordinates& coords, int inCellX, int inCellY) { std::map::iterator searchResult = mCells.find(coords); if (searchResult != mCells.end()) return searchResult->second->getAlteredHeight(inCellX, inCellY); return nullptr; } void CSVRender::PagedWorldspaceWidget::resetAllAlteredHeights() { for (const auto& cell : mCells) cell.second->resetAlteredHeights(); } std::vector > CSVRender::PagedWorldspaceWidget::getSelection ( unsigned int elementMask) const { std::vector > result; for (std::map::const_iterator iter = mCells.begin(); iter!=mCells.end(); ++iter) { std::vector > cellResult = iter->second->getSelection (elementMask); result.insert (result.end(), cellResult.begin(), cellResult.end()); } return result; } std::vector > CSVRender::PagedWorldspaceWidget::getEdited ( unsigned int elementMask) const { std::vector > result; for (std::map::const_iterator iter = mCells.begin(); iter!=mCells.end(); ++iter) { std::vector > cellResult = iter->second->getEdited (elementMask); result.insert (result.end(), cellResult.begin(), cellResult.end()); } return result; } void CSVRender::PagedWorldspaceWidget::setSubMode (int subMode, unsigned int elementMask) { for (std::map::const_iterator iter = mCells.begin(); iter!=mCells.end(); ++iter) iter->second->setSubMode (subMode, elementMask); } void CSVRender::PagedWorldspaceWidget::reset (unsigned int elementMask) { for (std::map::const_iterator iter = mCells.begin(); iter!=mCells.end(); ++iter) iter->second->reset (elementMask); } CSVWidget::SceneToolToggle2 *CSVRender::PagedWorldspaceWidget::makeControlVisibilitySelector ( CSVWidget::SceneToolbar *parent) { mControlElements = new CSVWidget::SceneToolToggle2 (parent, "Controls & Guides Visibility", ":scenetoolbar/scene-view-marker-c", ":scenetoolbar/scene-view-marker-"); mControlElements->addButton (1, Mask_CellMarker, "Cell Marker"); mControlElements->addButton (2, Mask_CellArrow, "Cell Arrows"); mControlElements->addButton (4, Mask_CellBorder, "Cell Border"); mControlElements->setSelectionMask (0xffffffff); connect (mControlElements, SIGNAL (selectionChanged()), this, SLOT (elementSelectionChanged())); return mControlElements; } void CSVRender::PagedWorldspaceWidget::cellDataChanged (const QModelIndex& topLeft, const QModelIndex& bottomRight) { /// \todo check if no selected cell is affected and do not update, if that is the case if (adjustCells()) flagAsModified(); } void CSVRender::PagedWorldspaceWidget::cellRemoved (const QModelIndex& parent, int start, int end) { if (adjustCells()) flagAsModified(); } void CSVRender::PagedWorldspaceWidget::cellAdded (const QModelIndex& index, int start, int end) { /// \todo check if no selected cell is affected and do not update, if that is the case if (adjustCells()) flagAsModified(); } void CSVRender::PagedWorldspaceWidget::assetTablesChanged() { std::map::iterator iter = mCells.begin(); for ( ; iter != mCells.end(); ++iter) { iter->second->reloadAssets(); } } void CSVRender::PagedWorldspaceWidget::loadCameraCell() { addCellToSceneFromCamera(0, 0); } void CSVRender::PagedWorldspaceWidget::loadEastCell() { addCellToSceneFromCamera(1, 0); } void CSVRender::PagedWorldspaceWidget::loadNorthCell() { addCellToSceneFromCamera(0, 1); } void CSVRender::PagedWorldspaceWidget::loadWestCell() { addCellToSceneFromCamera(-1, 0); } void CSVRender::PagedWorldspaceWidget::loadSouthCell() { addCellToSceneFromCamera(0, -1); } ================================================ FILE: apps/opencs/view/render/pagedworldspacewidget.hpp ================================================ #ifndef OPENCS_VIEW_PAGEDWORLDSPACEWIDGET_H #define OPENCS_VIEW_PAGEDWORLDSPACEWIDGET_H #include #include "../../model/world/cellselection.hpp" #include "worldspacewidget.hpp" #include "cell.hpp" #include "instancedragmodes.hpp" namespace CSVWidget { class SceneToolToggle; class SceneToolToggle2; } namespace CSVRender { class TextOverlay; class OverlayMask; class PagedWorldspaceWidget : public WorldspaceWidget { Q_OBJECT CSMDoc::Document& mDocument; CSMWorld::CellSelection mSelection; std::map mCells; std::string mWorldspace; CSVWidget::SceneToolToggle2 *mControlElements; bool mDisplayCellCoord; private: std::pair getCoordinatesFromId(const std::string& record) const; /// Bring mCells into sync with mSelection again. /// /// \return Any cells added or removed? bool adjustCells(); void referenceableDataChanged (const QModelIndex& topLeft, const QModelIndex& bottomRight) override; void referenceableAboutToBeRemoved (const QModelIndex& parent, int start, int end) override; void referenceableAdded (const QModelIndex& index, int start, int end) override; void referenceDataChanged (const QModelIndex& topLeft, const QModelIndex& bottomRight) override; void referenceAboutToBeRemoved (const QModelIndex& parent, int start, int end) override; void referenceAdded (const QModelIndex& index, int start, int end) override; void pathgridDataChanged (const QModelIndex& topLeft, const QModelIndex& bottomRight) override; void pathgridAboutToBeRemoved (const QModelIndex& parent, int start, int end) override; void pathgridAdded (const QModelIndex& parent, int start, int end) override; std::string getStartupInstruction() override; /// \note Does not update the view or any cell marker void addCellToScene (const CSMWorld::CellCoordinates& coordinates); /// \note Does not update the view or any cell marker /// /// \note Calling this function for a cell that is not in the selection is a no-op. void removeCellFromScene (const CSMWorld::CellCoordinates& coordinates); /// \note Does not update the view or any cell marker void addCellSelection (int x, int y); /// \note Does not update the view or any cell marker void moveCellSelection (int x, int y); void addCellToSceneFromCamera (int offsetX, int offsetY); public: PagedWorldspaceWidget (QWidget *parent, CSMDoc::Document& document); ///< \note Sets the cell area selection to an invalid value to indicate that currently /// no cells are displayed. The cells to be displayed will be specified later through /// hint system. virtual ~PagedWorldspaceWidget(); /// Decodes the the hint string to set of cell that are rendered. void useViewHint (const std::string& hint) override; void setCellSelection(const CSMWorld::CellSelection& selection); const CSMWorld::CellSelection& getCellSelection() const; /// \return Drop handled? bool handleDrop (const std::vector& data, DropType type) override; dropRequirments getDropRequirements(DropType type) const override; /// \attention The created tool is not added to the toolbar (via addTool). Doing /// that is the responsibility of the calling function. virtual CSVWidget::SceneToolToggle2 *makeControlVisibilitySelector ( CSVWidget::SceneToolbar *parent); unsigned int getVisibilityMask() const override; /// \param elementMask Elements to be affected by the clear operation void clearSelection (int elementMask) override; /// \param elementMask Elements to be affected by the select operation void invertSelection (int elementMask) override; /// \param elementMask Elements to be affected by the select operation void selectAll (int elementMask) override; // Select everything that references the same ID as at least one of the elements // already selected // /// \param elementMask Elements to be affected by the select operation void selectAllWithSameParentId (int elementMask) override; void selectInsideCube(const osg::Vec3d& pointA, const osg::Vec3d& pointB, DragMode dragMode) override; void selectWithinDistance(const osg::Vec3d& point, float distance, DragMode dragMode) override; std::string getCellId (const osg::Vec3f& point) const override; Cell* getCell(const osg::Vec3d& point) const override; Cell* getCell(const CSMWorld::CellCoordinates& coords) const override; void setCellAlteredHeight(const CSMWorld::CellCoordinates& coords, int inCellX, int inCellY, float height); float* getCellAlteredHeight(const CSMWorld::CellCoordinates& coords, int inCellX, int inCellY); void resetAllAlteredHeights(); std::vector > getSelection (unsigned int elementMask) const override; std::vector > getEdited (unsigned int elementMask) const override; void setSubMode (int subMode, unsigned int elementMask) override; /// Erase all overrides and restore the visual representation to its true state. void reset (unsigned int elementMask) override; protected: void addVisibilitySelectorButtons (CSVWidget::SceneToolToggle2 *tool) override; void addEditModeSelectorButtons (CSVWidget::SceneToolMode *tool) override; void handleInteractionPress (const WorldspaceHitResult& hit, InteractionType type) override; signals: void cellSelectionChanged (const CSMWorld::CellSelection& selection); private slots: virtual void cellDataChanged (const QModelIndex& topLeft, const QModelIndex& bottomRight); virtual void cellRemoved (const QModelIndex& parent, int start, int end); virtual void cellAdded (const QModelIndex& index, int start, int end); virtual void landDataChanged (const QModelIndex& topLeft, const QModelIndex& botomRight); virtual void landAboutToBeRemoved (const QModelIndex& parent, int start, int end); virtual void landAdded (const QModelIndex& parent, int start, int end); virtual void landTextureDataChanged (const QModelIndex& topLeft, const QModelIndex& botomRight); virtual void landTextureAboutToBeRemoved (const QModelIndex& parent, int start, int end); virtual void landTextureAdded (const QModelIndex& parent, int start, int end); void assetTablesChanged (); void loadCameraCell(); void loadEastCell(); void loadNorthCell(); void loadWestCell(); void loadSouthCell(); }; } #endif ================================================ FILE: apps/opencs/view/render/pathgrid.cpp ================================================ #include "pathgrid.hpp" #include #include #include #include #include #include #include #include #include "../../model/world/cell.hpp" #include "../../model/world/commands.hpp" #include "../../model/world/commandmacro.hpp" #include "../../model/world/data.hpp" #include "../../model/world/idtree.hpp" namespace CSVRender { class PathgridNodeCallback : public osg::NodeCallback { public: void operator()(osg::Node* node, osg::NodeVisitor* nv) override { PathgridTag* tag = static_cast(node->getUserData()); tag->getPathgrid()->update(); } }; PathgridTag::PathgridTag(Pathgrid* pathgrid) : TagBase(Mask_Pathgrid), mPathgrid(pathgrid) { } Pathgrid* PathgridTag::getPathgrid() const { return mPathgrid; } QString PathgridTag::getToolTip(bool hideBasics) const { QString text("Pathgrid: "); text += mPathgrid->getId().c_str(); return text; } Pathgrid::Pathgrid(CSMWorld::Data& data, osg::Group* parent, const std::string& pathgridId, const CSMWorld::CellCoordinates& coordinates) : mData(data) , mPathgridCollection(mData.getPathgrids()) , mId(pathgridId) , mCoords(coordinates) , mInterior(false) , mDragOrigin(0) , mChangeGeometry(true) , mRemoveGeometry(false) , mUseOffset(true) , mParent(parent) , mPathgridGeometry(nullptr) , mDragGeometry(nullptr) , mTag(new PathgridTag(this)) { const float CoordScalar = ESM::Land::REAL_SIZE; mBaseNode = new osg::PositionAttitudeTransform (); mBaseNode->setPosition(osg::Vec3f(mCoords.getX() * CoordScalar, mCoords.getY() * CoordScalar, 0.f)); mBaseNode->setUserData(mTag); mBaseNode->setUpdateCallback(new PathgridNodeCallback()); mBaseNode->setNodeMask(Mask_Pathgrid); mParent->addChild(mBaseNode); mPathgridGeode = new osg::Geode(); mBaseNode->addChild(mPathgridGeode); recreateGeometry(); int index = mData.getCells().searchId(mId); if (index != -1) { const CSMWorld::Cell& cell = mData.getCells().getRecord(index).get(); mInterior = cell.mData.mFlags & ESM::Cell::Interior; } } Pathgrid::~Pathgrid() { mParent->removeChild(mBaseNode); } const CSMWorld::CellCoordinates& Pathgrid::getCoordinates() const { return mCoords; } const std::string& Pathgrid::getId() const { return mId; } bool Pathgrid::isSelected() const { return !mSelected.empty(); } const Pathgrid::NodeList& Pathgrid::getSelected() const { return mSelected; } void Pathgrid::selectAll() { mSelected.clear(); const CSMWorld::Pathgrid* source = getPathgridSource(); if (source) { for (unsigned short i = 0; i < static_cast(source->mPoints.size()); ++i) mSelected.push_back(i); createSelectedGeometry(*source); } else { removeSelectedGeometry(); } } void Pathgrid::toggleSelected(unsigned short node) { NodeList::iterator searchResult = std::find(mSelected.begin(), mSelected.end(), node); if (searchResult != mSelected.end()) { mSelected.erase(searchResult); } else { mSelected.push_back(node); } createSelectedGeometry(); } void Pathgrid::invertSelected() { NodeList temp = NodeList(mSelected); mSelected.clear(); const CSMWorld::Pathgrid* source = getPathgridSource(); if (source) { for (unsigned short i = 0; i < static_cast(source->mPoints.size()); ++i) { if (std::find(temp.begin(), temp.end(), i) == temp.end()) mSelected.push_back(i); } createSelectedGeometry(*source); } else { removeSelectedGeometry(); } } void Pathgrid::clearSelected() { mSelected.clear(); removeSelectedGeometry(); } void Pathgrid::moveSelected(const osg::Vec3d& offset) { mUseOffset = true; mMoveOffset += offset; recreateGeometry(); } void Pathgrid::setDragOrigin(unsigned short node) { mDragOrigin = node; } void Pathgrid::setDragEndpoint(unsigned short node) { const CSMWorld::Pathgrid* source = getPathgridSource(); if (source) { const CSMWorld::Pathgrid::Point& pointA = source->mPoints[mDragOrigin]; const CSMWorld::Pathgrid::Point& pointB = source->mPoints[node]; osg::Vec3f start = osg::Vec3f(pointA.mX, pointA.mY, pointA.mZ + SceneUtil::DiamondHalfHeight); osg::Vec3f end = osg::Vec3f(pointB.mX, pointB.mY, pointB.mZ + SceneUtil::DiamondHalfHeight); createDragGeometry(start, end, true); } } void Pathgrid::setDragEndpoint(const osg::Vec3d& pos) { const CSMWorld::Pathgrid* source = getPathgridSource(); if (source) { const CSMWorld::Pathgrid::Point& point = source->mPoints[mDragOrigin]; osg::Vec3f start = osg::Vec3f(point.mX, point.mY, point.mZ + SceneUtil::DiamondHalfHeight); osg::Vec3f end = pos - mBaseNode->getPosition(); createDragGeometry(start, end, false); } } void Pathgrid::resetIndicators() { mUseOffset = false; mMoveOffset.set(0, 0, 0); mPathgridGeode->removeDrawable(mDragGeometry); mDragGeometry = nullptr; } void Pathgrid::applyPoint(CSMWorld::CommandMacro& commands, const osg::Vec3d& worldPos) { CSMWorld::IdTree* model = &dynamic_cast(*mData.getTableModel(CSMWorld::UniversalId::Type_Pathgrids)); const CSMWorld::Pathgrid* source = getPathgridSource(); if (source) { osg::Vec3d localCoords = worldPos - mBaseNode->getPosition(); int posX = clampToCell(static_cast(localCoords.x())); int posY = clampToCell(static_cast(localCoords.y())); int posZ = clampToCell(static_cast(localCoords.z())); int recordIndex = mPathgridCollection.getIndex (mId); int parentColumn = mPathgridCollection.findColumnIndex(CSMWorld::Columns::ColumnId_PathgridPoints); int posXColumn = mPathgridCollection.searchNestedColumnIndex(parentColumn, CSMWorld::Columns::ColumnId_PathgridPosX); int posYColumn = mPathgridCollection.searchNestedColumnIndex(parentColumn, CSMWorld::Columns::ColumnId_PathgridPosY); int posZColumn = mPathgridCollection.searchNestedColumnIndex(parentColumn, CSMWorld::Columns::ColumnId_PathgridPosZ); QModelIndex parent = model->index(recordIndex, parentColumn); int row = static_cast(source->mPoints.size()); // Add node to end of list commands.push(new CSMWorld::AddNestedCommand(*model, mId, row, parentColumn)); commands.push(new CSMWorld::ModifyCommand(*model, model->index(row, posXColumn, parent), posX)); commands.push(new CSMWorld::ModifyCommand(*model, model->index(row, posYColumn, parent), posY)); commands.push(new CSMWorld::ModifyCommand(*model, model->index(row, posZColumn, parent), posZ)); } else { int index = mPathgridCollection.searchId(mId); if (index == -1) { // Does not exist commands.push(new CSMWorld::CreatePathgridCommand(*model, mId)); } else { source = &mPathgridCollection.getRecord(index).get(); // Deleted, so revert and remove all data commands.push(new CSMWorld::RevertCommand(*model, mId)); int parentColumn = mPathgridCollection.findColumnIndex(CSMWorld::Columns::ColumnId_PathgridPoints); for (int row = source->mPoints.size() - 1; row >= 0; --row) { commands.push(new CSMWorld::DeleteNestedCommand(*model, mId, row, parentColumn)); } parentColumn = mPathgridCollection.findColumnIndex(CSMWorld::Columns::ColumnId_PathgridEdges); for (int row = source->mEdges.size() - 1; row >= 0; --row) { commands.push(new CSMWorld::DeleteNestedCommand(*model, mId, row, parentColumn)); } } } } void Pathgrid::applyPosition(CSMWorld::CommandMacro& commands) { const CSMWorld::Pathgrid* source = getPathgridSource(); if (source) { osg::Vec3d localCoords = mMoveOffset; int offsetX = static_cast(localCoords.x()); int offsetY = static_cast(localCoords.y()); int offsetZ = static_cast(localCoords.z()); QAbstractItemModel* model = mData.getTableModel(CSMWorld::UniversalId::Type_Pathgrids); int recordIndex = mPathgridCollection.getIndex(mId); int parentColumn = mPathgridCollection.findColumnIndex(CSMWorld::Columns::ColumnId_PathgridPoints); int posXColumn = mPathgridCollection.searchNestedColumnIndex(parentColumn, CSMWorld::Columns::ColumnId_PathgridPosX); int posYColumn = mPathgridCollection.searchNestedColumnIndex(parentColumn, CSMWorld::Columns::ColumnId_PathgridPosY); int posZColumn = mPathgridCollection.searchNestedColumnIndex(parentColumn, CSMWorld::Columns::ColumnId_PathgridPosZ); QModelIndex parent = model->index(recordIndex, parentColumn); for (size_t i = 0; i < mSelected.size(); ++i) { const CSMWorld::Pathgrid::Point& point = source->mPoints[mSelected[i]]; int row = static_cast(mSelected[i]); commands.push(new CSMWorld::ModifyCommand(*model, model->index(row, posXColumn, parent), clampToCell(point.mX + offsetX))); commands.push(new CSMWorld::ModifyCommand(*model, model->index(row, posYColumn, parent), clampToCell(point.mY + offsetY))); commands.push(new CSMWorld::ModifyCommand(*model, model->index(row, posZColumn, parent), clampToCell(point.mZ + offsetZ))); } } } void Pathgrid::applyEdge(CSMWorld::CommandMacro& commands, unsigned short node1, unsigned short node2) { const CSMWorld::Pathgrid* source = getPathgridSource(); if (source) { addEdge(commands, *source, node1, node2); } } void Pathgrid::applyEdges(CSMWorld::CommandMacro& commands, unsigned short node) { const CSMWorld::Pathgrid* source = getPathgridSource(); if (source) { for (size_t i = 0; i < mSelected.size(); ++i) { addEdge(commands, *source, node, mSelected[i]); } } } void Pathgrid::applyRemoveNodes(CSMWorld::CommandMacro& commands) { const CSMWorld::Pathgrid* source = getPathgridSource(); if (source) { CSMWorld::IdTree* model = &dynamic_cast(*mData.getTableModel(CSMWorld::UniversalId::Type_Pathgrids)); // Want to remove nodes from end of list first std::sort(mSelected.begin(), mSelected.end(), std::greater()); int recordIndex = mPathgridCollection.getIndex(mId); int parentColumn = mPathgridCollection.findColumnIndex(CSMWorld::Columns::ColumnId_PathgridPoints); for (std::vector::iterator row = mSelected.begin(); row != mSelected.end(); ++row) { commands.push(new CSMWorld::DeleteNestedCommand(*model, mId, static_cast(*row), parentColumn)); } // Fix/remove edges std::set > edgeRowsToRemove; parentColumn = mPathgridCollection.findColumnIndex(CSMWorld::Columns::ColumnId_PathgridEdges); int edge0Column = mPathgridCollection.searchNestedColumnIndex(parentColumn, CSMWorld::Columns::ColumnId_PathgridEdge0); int edge1Column = mPathgridCollection.searchNestedColumnIndex(parentColumn, CSMWorld::Columns::ColumnId_PathgridEdge1); QModelIndex parent = model->index(recordIndex, parentColumn); for (size_t edge = 0; edge < source->mEdges.size(); ++edge) { int adjustment0 = 0; int adjustment1 = 0; // Determine necessary adjustment for (std::vector::iterator point = mSelected.begin(); point != mSelected.end(); ++point) { if (source->mEdges[edge].mV0 == *point || source->mEdges[edge].mV1 == *point) { edgeRowsToRemove.insert(static_cast(edge)); adjustment0 = 0; // No need to adjust, its getting removed adjustment1 = 0; break; } if (source->mEdges[edge].mV0 > *point) --adjustment0; if (source->mEdges[edge].mV1 > *point) --adjustment1; } if (adjustment0 != 0) { int adjustedEdge = source->mEdges[edge].mV0 + adjustment0; commands.push(new CSMWorld::ModifyCommand(*model, model->index(edge, edge0Column, parent), adjustedEdge)); } if (adjustment1 != 0) { int adjustedEdge = source->mEdges[edge].mV1 + adjustment1; commands.push(new CSMWorld::ModifyCommand(*model, model->index(edge, edge1Column, parent), adjustedEdge)); } } std::set >::iterator row; for (row = edgeRowsToRemove.begin(); row != edgeRowsToRemove.end(); ++row) { commands.push(new CSMWorld::DeleteNestedCommand(*model, mId, *row, parentColumn)); } } clearSelected(); } void Pathgrid::applyRemoveEdges(CSMWorld::CommandMacro& commands) { const CSMWorld::Pathgrid* source = getPathgridSource(); if (source) { // Want to remove from end of row first std::set > rowsToRemove; for (size_t i = 0; i <= mSelected.size(); ++i) { for (size_t j = i + 1; j < mSelected.size(); ++j) { int row = edgeExists(*source, mSelected[i], mSelected[j]); if (row != -1) { rowsToRemove.insert(row); } row = edgeExists(*source, mSelected[j], mSelected[i]); if (row != -1) { rowsToRemove.insert(row); } } } CSMWorld::IdTree* model = &dynamic_cast(*mData.getTableModel(CSMWorld::UniversalId::Type_Pathgrids)); int parentColumn = mPathgridCollection.findColumnIndex(CSMWorld::Columns::ColumnId_PathgridEdges); std::set >::iterator row; for (row = rowsToRemove.begin(); row != rowsToRemove.end(); ++row) { commands.push(new CSMWorld::DeleteNestedCommand(*model, mId, *row, parentColumn)); } } } osg::ref_ptr Pathgrid::getTag() const { return mTag; } void Pathgrid::recreateGeometry() { mChangeGeometry = true; } void Pathgrid::removeGeometry() { mRemoveGeometry = true; } void Pathgrid::update() { if (mRemoveGeometry) { removePathgridGeometry(); removeSelectedGeometry(); } else if (mChangeGeometry) { createGeometry(); } mChangeGeometry = false; mRemoveGeometry = false; } void Pathgrid::createGeometry() { const CSMWorld::Pathgrid* source = getPathgridSource(); if (source) { CSMWorld::Pathgrid temp; if (mUseOffset) { temp = *source; for (NodeList::iterator it = mSelected.begin(); it != mSelected.end(); ++it) { temp.mPoints[*it].mX += mMoveOffset.x(); temp.mPoints[*it].mY += mMoveOffset.y(); temp.mPoints[*it].mZ += mMoveOffset.z(); } source = &temp; } removePathgridGeometry(); mPathgridGeometry = SceneUtil::createPathgridGeometry(*source); mPathgridGeode->addDrawable(mPathgridGeometry); createSelectedGeometry(*source); } else { removePathgridGeometry(); removeSelectedGeometry(); } } void Pathgrid::createSelectedGeometry() { const CSMWorld::Pathgrid* source = getPathgridSource(); if (source) { createSelectedGeometry(*source); } else { removeSelectedGeometry(); } } void Pathgrid::createSelectedGeometry(const CSMWorld::Pathgrid& source) { removeSelectedGeometry(); mSelectedGeometry = SceneUtil::createPathgridSelectedWireframe(source, mSelected); mPathgridGeode->addDrawable(mSelectedGeometry); } void Pathgrid::removePathgridGeometry() { if (mPathgridGeometry) { mPathgridGeode->removeDrawable(mPathgridGeometry); mPathgridGeometry = nullptr; } } void Pathgrid::removeSelectedGeometry() { if (mSelectedGeometry) { mPathgridGeode->removeDrawable(mSelectedGeometry); mSelectedGeometry = nullptr; } } void Pathgrid::createDragGeometry(const osg::Vec3f& start, const osg::Vec3f& end, bool valid) { if (mDragGeometry) mPathgridGeode->removeDrawable(mDragGeometry); mDragGeometry = new osg::Geometry(); osg::ref_ptr vertices = new osg::Vec3Array(2); osg::ref_ptr colors = new osg::Vec4Array(1); osg::ref_ptr indices = new osg::DrawElementsUShort(osg::PrimitiveSet::LINES, 2); (*vertices)[0] = start; (*vertices)[1] = end; if (valid) { (*colors)[0] = osg::Vec4f(0.91f, 0.66f, 1.f, 1.f); } else { (*colors)[0] = osg::Vec4f(1.f, 0.f, 0.f, 1.f); } indices->setElement(0, 0); indices->setElement(1, 1); mDragGeometry->setVertexArray(vertices); mDragGeometry->setColorArray(colors, osg::Array::BIND_OVERALL); mDragGeometry->addPrimitiveSet(indices); mDragGeometry->getOrCreateStateSet()->setMode(GL_LIGHTING, osg::StateAttribute::OFF); mPathgridGeode->addDrawable(mDragGeometry); } const CSMWorld::Pathgrid* Pathgrid::getPathgridSource() { int index = mPathgridCollection.searchId(mId); if (index != -1 && !mPathgridCollection.getRecord(index).isDeleted()) { return &mPathgridCollection.getRecord(index).get(); } return nullptr; } int Pathgrid::edgeExists(const CSMWorld::Pathgrid& source, unsigned short node1, unsigned short node2) { for (size_t i = 0; i < source.mEdges.size(); ++i) { if (source.mEdges[i].mV0 == node1 && source.mEdges[i].mV1 == node2) return static_cast(i); } return -1; } void Pathgrid::addEdge(CSMWorld::CommandMacro& commands, const CSMWorld::Pathgrid& source, unsigned short node1, unsigned short node2) { CSMWorld::IdTree* model = &dynamic_cast(*mData.getTableModel(CSMWorld::UniversalId::Type_Pathgrids)); int recordIndex = mPathgridCollection.getIndex(mId); int parentColumn = mPathgridCollection.findColumnIndex(CSMWorld::Columns::ColumnId_PathgridEdges); int edge0Column = mPathgridCollection.searchNestedColumnIndex(parentColumn, CSMWorld::Columns::ColumnId_PathgridEdge0); int edge1Column = mPathgridCollection.searchNestedColumnIndex(parentColumn, CSMWorld::Columns::ColumnId_PathgridEdge1); QModelIndex parent = model->index(recordIndex, parentColumn); int row = static_cast(source.mEdges.size()); if (edgeExists(source, node1, node2) == -1) { commands.push(new CSMWorld::AddNestedCommand(*model, mId, row, parentColumn)); commands.push(new CSMWorld::ModifyCommand(*model, model->index(row, edge0Column, parent), node1)); commands.push(new CSMWorld::ModifyCommand(*model, model->index(row, edge1Column, parent), node2)); ++row; } if (edgeExists(source, node2, node1) == -1) { commands.push(new CSMWorld::AddNestedCommand(*model, mId, row, parentColumn)); commands.push(new CSMWorld::ModifyCommand(*model, model->index(row, edge0Column, parent), node2)); commands.push(new CSMWorld::ModifyCommand(*model, model->index(row, edge1Column, parent), node1)); } } int Pathgrid::clampToCell(int v) { const int CellExtent = ESM::Land::REAL_SIZE; if (mInterior) return v; else if (v > CellExtent) return CellExtent; else if (v < 0) return 0; else return v; } } ================================================ FILE: apps/opencs/view/render/pathgrid.hpp ================================================ #ifndef CSV_RENDER_PATHGRID_H #define CSV_RENDER_PATHGRID_H #include #include #include #include #include "../../model/world/cellcoordinates.hpp" #include "../../model/world/idcollection.hpp" #include "../../model/world/subcellcollection.hpp" #include "tagbase.hpp" namespace osg { class Geode; class Geometry; class Group; class PositionAttitudeTransform; } namespace CSMWorld { class CommandMacro; class Data; struct Pathgrid; } namespace CSVRender { class Pathgrid; class PathgridTag : public TagBase { public: PathgridTag (Pathgrid* pathgrid); Pathgrid* getPathgrid () const; QString getToolTip (bool hideBasics) const override; private: Pathgrid* mPathgrid; }; class Pathgrid { public: typedef std::vector NodeList; Pathgrid(CSMWorld::Data& data, osg::Group* parent, const std::string& pathgridId, const CSMWorld::CellCoordinates& coordinates); ~Pathgrid(); const CSMWorld::CellCoordinates& getCoordinates() const; const std::string& getId() const; bool isSelected() const; const NodeList& getSelected() const; void selectAll(); void toggleSelected(unsigned short node); // Adds to end of vector void invertSelected(); void clearSelected(); void moveSelected(const osg::Vec3d& offset); void setDragOrigin(unsigned short node); void setDragEndpoint(unsigned short node); void setDragEndpoint(const osg::Vec3d& pos); void resetIndicators(); void applyPoint(CSMWorld::CommandMacro& commands, const osg::Vec3d& worldPos); void applyPosition(CSMWorld::CommandMacro& commands); void applyEdge(CSMWorld::CommandMacro& commands, unsigned short node1, unsigned short node2); void applyEdges(CSMWorld::CommandMacro& commands, unsigned short node); void applyRemoveNodes(CSMWorld::CommandMacro& commands); void applyRemoveEdges(CSMWorld::CommandMacro& commands); osg::ref_ptr getTag() const; void recreateGeometry(); void removeGeometry(); void update(); private: CSMWorld::Data& mData; CSMWorld::SubCellCollection& mPathgridCollection; std::string mId; CSMWorld::CellCoordinates mCoords; bool mInterior; NodeList mSelected; osg::Vec3d mMoveOffset; unsigned short mDragOrigin; bool mChangeGeometry; bool mRemoveGeometry; bool mUseOffset; osg::Group* mParent; osg::ref_ptr mBaseNode; osg::ref_ptr mPathgridGeode; osg::ref_ptr mPathgridGeometry; osg::ref_ptr mSelectedGeometry; osg::ref_ptr mDragGeometry; osg::ref_ptr mTag; void createGeometry(); void createSelectedGeometry(); void createSelectedGeometry(const CSMWorld::Pathgrid& source); void removePathgridGeometry(); void removeSelectedGeometry(); void createDragGeometry(const osg::Vec3f& start, const osg::Vec3f& end, bool valid); const CSMWorld::Pathgrid* getPathgridSource(); int edgeExists(const CSMWorld::Pathgrid& source, unsigned short node1, unsigned short node2); void addEdge(CSMWorld::CommandMacro& commands, const CSMWorld::Pathgrid& source, unsigned short node1, unsigned short node2); void removeEdge(CSMWorld::CommandMacro& commands, const CSMWorld::Pathgrid& source, unsigned short node1, unsigned short node2); int clampToCell(int v); }; } #endif ================================================ FILE: apps/opencs/view/render/pathgridmode.cpp ================================================ #include "pathgridmode.hpp" #include #include #include #include "../../model/prefs/state.hpp" #include "../../model/world/commands.hpp" #include "../../model/world/commandmacro.hpp" #include "../widget/scenetoolbar.hpp" #include "cell.hpp" #include "mask.hpp" #include "pathgrid.hpp" #include "pathgridselectionmode.hpp" #include "worldspacewidget.hpp" namespace CSVRender { PathgridMode::PathgridMode(WorldspaceWidget* worldspaceWidget, QWidget* parent) : EditMode(worldspaceWidget, QIcon(":placeholder"), Mask_Pathgrid | Mask_Terrain | Mask_Reference, getTooltip(), parent) , mDragMode(DragMode_None) , mFromNode(0) , mSelectionMode(nullptr) { } QString PathgridMode::getTooltip() { return QString( "Pathgrid editing" "
  • Press {scene-edit-primary} to add a node to the cursor location
  • " "
  • Press {scene-edit-secondary} to connect the selected nodes to the node beneath the cursor
  • " "
  • Press {scene-edit-primary} and drag to move selected nodes
  • " "
  • Press {scene-edit-secondary} and drag to connect one node to another
  • " "

Note: Only a single cell's pathgrid may be edited at a time"); } void PathgridMode::activate(CSVWidget::SceneToolbar* toolbar) { if (!mSelectionMode) { mSelectionMode = new PathgridSelectionMode(toolbar, getWorldspaceWidget()); } EditMode::activate(toolbar); toolbar->addTool(mSelectionMode); } void PathgridMode::deactivate(CSVWidget::SceneToolbar* toolbar) { if (mSelectionMode) { toolbar->removeTool (mSelectionMode); delete mSelectionMode; mSelectionMode = nullptr; } } void PathgridMode::primaryOpenPressed(const WorldspaceHitResult& hitResult) { } void PathgridMode::primaryEditPressed(const WorldspaceHitResult& hitResult) { if (CSMPrefs::get()["3D Scene Input"]["context-select"].isTrue() && dynamic_cast(hitResult.tag.get())) { primarySelectPressed(hitResult); } else if (Cell* cell = getWorldspaceWidget().getCell (hitResult.worldPos)) { if (cell->getPathgrid()) { // Add node QUndoStack& undoStack = getWorldspaceWidget().getDocument().getUndoStack(); QString description = "Add node"; CSMWorld::CommandMacro macro(undoStack, description); cell->getPathgrid()->applyPoint(macro, hitResult.worldPos); } } } void PathgridMode::secondaryEditPressed(const WorldspaceHitResult& hit) { if (hit.tag) { if (PathgridTag* tag = dynamic_cast(hit.tag.get())) { if (tag->getPathgrid()->isSelected()) { unsigned short node = SceneUtil::getPathgridNode(static_cast(hit.index0)); QUndoStack& undoStack = getWorldspaceWidget().getDocument().getUndoStack(); QString description = "Connect node to selected nodes"; CSMWorld::CommandMacro macro(undoStack, description); tag->getPathgrid()->applyEdges(macro, node); } } } } void PathgridMode::primarySelectPressed(const WorldspaceHitResult& hit) { getWorldspaceWidget().clearSelection(Mask_Pathgrid); if (hit.tag) { if (PathgridTag* tag = dynamic_cast(hit.tag.get())) { mLastId = tag->getPathgrid()->getId(); unsigned short node = SceneUtil::getPathgridNode(static_cast(hit.index0)); tag->getPathgrid()->toggleSelected(node); } } } void PathgridMode::secondarySelectPressed(const WorldspaceHitResult& hit) { if (hit.tag) { if (PathgridTag* tag = dynamic_cast(hit.tag.get())) { if (tag->getPathgrid()->getId() != mLastId) { getWorldspaceWidget().clearSelection(Mask_Pathgrid); mLastId = tag->getPathgrid()->getId(); } unsigned short node = SceneUtil::getPathgridNode(static_cast(hit.index0)); tag->getPathgrid()->toggleSelected(node); return; } } getWorldspaceWidget().clearSelection(Mask_Pathgrid); } bool PathgridMode::primaryEditStartDrag(const QPoint& pos) { std::vector > selection = getWorldspaceWidget().getSelection (Mask_Pathgrid); if (CSMPrefs::get()["3D Scene Input"]["context-select"].isTrue()) { WorldspaceHitResult hit = getWorldspaceWidget().mousePick (pos, getWorldspaceWidget().getInteractionMask()); if (dynamic_cast(hit.tag.get())) { primarySelectPressed(hit); selection = getWorldspaceWidget().getSelection (Mask_Pathgrid); } } if (!selection.empty()) { mDragMode = DragMode_Move; return true; } return false; } bool PathgridMode::secondaryEditStartDrag(const QPoint& pos) { WorldspaceHitResult hit = getWorldspaceWidget().mousePick (pos, getWorldspaceWidget().getInteractionMask()); if (hit.tag) { if (PathgridTag* tag = dynamic_cast(hit.tag.get())) { mDragMode = DragMode_Edge; mEdgeId = tag->getPathgrid()->getId(); mFromNode = SceneUtil::getPathgridNode(static_cast(hit.index0)); tag->getPathgrid()->setDragOrigin(mFromNode); return true; } } return false; } void PathgridMode::drag(const QPoint& pos, int diffX, int diffY, double speedFactor) { if (mDragMode == DragMode_Move) { std::vector > selection = getWorldspaceWidget().getSelection(Mask_Pathgrid); for (std::vector >::iterator it = selection.begin(); it != selection.end(); ++it) { if (PathgridTag* tag = dynamic_cast(it->get())) { osg::Vec3d eye, center, up, offset; getWorldspaceWidget().getCamera()->getViewMatrix().getLookAt (eye, center, up); offset = (up * diffY * speedFactor) + (((center - eye) ^ up) * diffX * speedFactor); tag->getPathgrid()->moveSelected(offset); } } } else if (mDragMode == DragMode_Edge) { WorldspaceHitResult hit = getWorldspaceWidget().mousePick (pos, getWorldspaceWidget().getInteractionMask()); Cell* cell = getWorldspaceWidget().getCell(hit.worldPos); if (cell && cell->getPathgrid()) { PathgridTag* tag = nullptr; if (hit.tag && (tag = dynamic_cast(hit.tag.get())) && tag->getPathgrid()->getId() == mEdgeId) { unsigned short node = SceneUtil::getPathgridNode(static_cast(hit.index0)); cell->getPathgrid()->setDragEndpoint(node); } else { cell->getPathgrid()->setDragEndpoint(hit.worldPos); } } } } void PathgridMode::dragCompleted(const QPoint& pos) { if (mDragMode == DragMode_Move) { std::vector > selection = getWorldspaceWidget().getSelection (Mask_Pathgrid); for (std::vector >::iterator it = selection.begin(); it != selection.end(); ++it) { if (PathgridTag* tag = dynamic_cast(it->get())) { QUndoStack& undoStack = getWorldspaceWidget().getDocument().getUndoStack(); QString description = "Move pathgrid node(s)"; CSMWorld::CommandMacro macro(undoStack, description); tag->getPathgrid()->applyPosition(macro); } } } else if (mDragMode == DragMode_Edge) { WorldspaceHitResult hit = getWorldspaceWidget().mousePick(pos, getWorldspaceWidget().getInteractionMask()); if (hit.tag) { if (PathgridTag* tag = dynamic_cast(hit.tag.get())) { if (tag->getPathgrid()->getId() == mEdgeId) { unsigned short toNode = SceneUtil::getPathgridNode(static_cast(hit.index0)); QUndoStack& undoStack = getWorldspaceWidget().getDocument().getUndoStack(); QString description = "Add edge between nodes"; CSMWorld::CommandMacro macro(undoStack, description); tag->getPathgrid()->applyEdge(macro, mFromNode, toNode); } } } mEdgeId.clear(); mFromNode = 0; } mDragMode = DragMode_None; getWorldspaceWidget().reset(Mask_Pathgrid); } void PathgridMode::dragAborted() { getWorldspaceWidget().reset(Mask_Pathgrid); } } ================================================ FILE: apps/opencs/view/render/pathgridmode.hpp ================================================ #ifndef CSV_RENDER_PATHGRIDMODE_H #define CSV_RENDER_PATHGRIDMODE_H #include #include "editmode.hpp" namespace CSVRender { class PathgridSelectionMode; class PathgridMode : public EditMode { Q_OBJECT public: PathgridMode(WorldspaceWidget* worldspace, QWidget* parent=nullptr); void activate(CSVWidget::SceneToolbar* toolbar) override; void deactivate(CSVWidget::SceneToolbar* toolbar) override; void primaryOpenPressed(const WorldspaceHitResult& hit) override; void primaryEditPressed(const WorldspaceHitResult& hit) override; void secondaryEditPressed(const WorldspaceHitResult& hit) override; void primarySelectPressed(const WorldspaceHitResult& hit) override; void secondarySelectPressed(const WorldspaceHitResult& hit) override; bool primaryEditStartDrag (const QPoint& pos) override; bool secondaryEditStartDrag (const QPoint& pos) override; void drag (const QPoint& pos, int diffX, int diffY, double speedFactor) override; void dragCompleted(const QPoint& pos) override; /// \note dragAborted will not be called, if the drag is aborted via changing /// editing mode void dragAborted() override; private: enum DragMode { DragMode_None, DragMode_Move, DragMode_Edge }; DragMode mDragMode; std::string mLastId, mEdgeId; unsigned short mFromNode; PathgridSelectionMode* mSelectionMode; QString getTooltip(); }; } #endif ================================================ FILE: apps/opencs/view/render/pathgridselectionmode.cpp ================================================ #include "pathgridselectionmode.hpp" #include #include #include "../../model/world/idtable.hpp" #include "../../model/world/commands.hpp" #include "../../model/world/commandmacro.hpp" #include "worldspacewidget.hpp" #include "pathgrid.hpp" namespace CSVRender { PathgridSelectionMode::PathgridSelectionMode(CSVWidget::SceneToolbar* parent, WorldspaceWidget& worldspaceWidget) : SelectionMode(parent, worldspaceWidget, Mask_Pathgrid) { mRemoveSelectedNodes = new QAction("Remove selected nodes", this); mRemoveSelectedEdges = new QAction("Remove edges between selected nodes", this); connect(mRemoveSelectedNodes, SIGNAL(triggered()), this, SLOT(removeSelectedNodes())); connect(mRemoveSelectedEdges, SIGNAL(triggered()), this, SLOT(removeSelectedEdges())); } bool PathgridSelectionMode::createContextMenu(QMenu* menu) { if (menu) { SelectionMode::createContextMenu(menu); menu->addAction(mRemoveSelectedNodes); menu->addAction(mRemoveSelectedEdges); } return true; } void PathgridSelectionMode::removeSelectedNodes() { std::vector > selection = getWorldspaceWidget().getSelection (Mask_Pathgrid); for (std::vector >::iterator it = selection.begin(); it != selection.end(); ++it) { if (PathgridTag* tag = dynamic_cast(it->get())) { QUndoStack& undoStack = getWorldspaceWidget().getDocument().getUndoStack(); QString description = "Remove selected nodes"; CSMWorld::CommandMacro macro(undoStack, description); tag->getPathgrid()->applyRemoveNodes(macro); } } } void PathgridSelectionMode::removeSelectedEdges() { std::vector > selection = getWorldspaceWidget().getSelection (Mask_Pathgrid); for (std::vector >::iterator it = selection.begin(); it != selection.end(); ++it) { if (PathgridTag* tag = dynamic_cast(it->get())) { QUndoStack& undoStack = getWorldspaceWidget().getDocument().getUndoStack(); QString description = "Remove edges between selected nodes"; CSMWorld::CommandMacro macro(undoStack, description); tag->getPathgrid()->applyRemoveEdges(macro); } } } } ================================================ FILE: apps/opencs/view/render/pathgridselectionmode.hpp ================================================ #ifndef CSV_RENDER_PATHGRID_SELECTION_MODE_H #define CSV_RENDER_PATHGRID_SELECTION_MODE_H #include "selectionmode.hpp" namespace CSVRender { class PathgridSelectionMode : public SelectionMode { Q_OBJECT public: PathgridSelectionMode(CSVWidget::SceneToolbar* parent, WorldspaceWidget& worldspaceWidget); protected: /// Add context menu items to \a menu. /// /// \attention menu can be a 0-pointer /// /// \return Have there been any menu items to be added (if menu is 0 and there /// items to be added, the function must return true anyway. bool createContextMenu(QMenu* menu) override; private: QAction* mRemoveSelectedNodes; QAction* mRemoveSelectedEdges; private slots: void removeSelectedNodes(); void removeSelectedEdges(); }; } #endif ================================================ FILE: apps/opencs/view/render/previewwidget.cpp ================================================ #include "previewwidget.hpp" #include "../../model/world/data.hpp" #include "../../model/world/idtable.hpp" CSVRender::PreviewWidget::PreviewWidget (CSMWorld::Data& data, const std::string& id, bool referenceable, QWidget *parent) : SceneWidget (data.getResourceSystem(), parent), mData (data), mObject(data, mRootNode, id, referenceable) { selectNavigationMode("orbit"); QAbstractItemModel *referenceables = mData.getTableModel (CSMWorld::UniversalId::Type_Referenceables); connect (referenceables, SIGNAL (dataChanged (const QModelIndex&, const QModelIndex&)), this, SLOT (referenceableDataChanged (const QModelIndex&, const QModelIndex&))); connect (referenceables, SIGNAL (rowsAboutToBeRemoved (const QModelIndex&, int, int)), this, SLOT (referenceableAboutToBeRemoved (const QModelIndex&, int, int))); connect (&mData, SIGNAL (assetTablesChanged ()), this, SLOT (assetTablesChanged ())); setExterior(false); if (!referenceable) { QAbstractItemModel *references = mData.getTableModel (CSMWorld::UniversalId::Type_References); connect (references, SIGNAL (dataChanged (const QModelIndex&, const QModelIndex&)), this, SLOT (referenceDataChanged (const QModelIndex&, const QModelIndex&))); connect (references, SIGNAL (rowsAboutToBeRemoved (const QModelIndex&, int, int)), this, SLOT (referenceAboutToBeRemoved (const QModelIndex&, int, int))); } } void CSVRender::PreviewWidget::referenceableDataChanged (const QModelIndex& topLeft, const QModelIndex& bottomRight) { if (mObject.referenceableDataChanged (topLeft, bottomRight)) flagAsModified(); if (mObject.getReferenceId().empty()) { CSMWorld::IdTable& referenceables = dynamic_cast ( *mData.getTableModel (CSMWorld::UniversalId::Type_Referenceables)); QModelIndex index = referenceables.getModelIndex (mObject.getReferenceableId(), referenceables.findColumnIndex (CSMWorld::Columns::ColumnId_Modification)); if (referenceables.data (index).toInt()==CSMWorld::RecordBase::State_Deleted) emit closeRequest(); } } void CSVRender::PreviewWidget::referenceableAboutToBeRemoved (const QModelIndex& parent, int start, int end) { if (mObject.referenceableAboutToBeRemoved (parent, start, end)) flagAsModified(); if (mObject.getReferenceableId().empty()) return; CSMWorld::IdTable& referenceables = dynamic_cast ( *mData.getTableModel (CSMWorld::UniversalId::Type_Referenceables)); QModelIndex index = referenceables.getModelIndex (mObject.getReferenceableId(), 0); if (index.row()>=start && index.row()<=end) { if (mObject.getReferenceId().empty()) { // this is a preview for a referenceble emit closeRequest(); } } } void CSVRender::PreviewWidget::referenceDataChanged (const QModelIndex& topLeft, const QModelIndex& bottomRight) { if (mObject.referenceDataChanged (topLeft, bottomRight)) flagAsModified(); if (mObject.getReferenceId().empty()) return; CSMWorld::IdTable& references = dynamic_cast ( *mData.getTableModel (CSMWorld::UniversalId::Type_References)); // check for deleted state { QModelIndex index = references.getModelIndex (mObject.getReferenceId(), references.findColumnIndex (CSMWorld::Columns::ColumnId_Modification)); if (references.data (index).toInt()==CSMWorld::RecordBase::State_Deleted) { emit closeRequest(); return; } } int columnIndex = references.findColumnIndex (CSMWorld::Columns::ColumnId_ReferenceableId); QModelIndex index = references.getModelIndex (mObject.getReferenceId(), columnIndex); if (index.row()>=topLeft.row() && index.row()<=bottomRight.row()) if (index.column()>=topLeft.column() && index.column()<=bottomRight.row()) emit referenceableIdChanged (mObject.getReferenceableId()); } void CSVRender::PreviewWidget::referenceAboutToBeRemoved (const QModelIndex& parent, int start, int end) { if (mObject.getReferenceId().empty()) return; CSMWorld::IdTable& references = dynamic_cast ( *mData.getTableModel (CSMWorld::UniversalId::Type_References)); QModelIndex index = references.getModelIndex (mObject.getReferenceId(), 0); if (index.row()>=start && index.row()<=end) emit closeRequest(); } void CSVRender::PreviewWidget::assetTablesChanged () { mObject.reloadAssets(); } ================================================ FILE: apps/opencs/view/render/previewwidget.hpp ================================================ #ifndef OPENCS_VIEW_PREVIEWWIDGET_H #define OPENCS_VIEW_PREVIEWWIDGET_H #include "scenewidget.hpp" #include "object.hpp" class QModelIndex; namespace VFS { class Manager; } namespace CSMWorld { class Data; } namespace CSVRender { class PreviewWidget : public SceneWidget { Q_OBJECT CSMWorld::Data& mData; CSVRender::Object mObject; public: PreviewWidget (CSMWorld::Data& data, const std::string& id, bool referenceable, QWidget *parent = nullptr); signals: void closeRequest(); void referenceableIdChanged (const std::string& id); private slots: void referenceableDataChanged (const QModelIndex& topLeft, const QModelIndex& bottomRight); void referenceableAboutToBeRemoved (const QModelIndex& parent, int start, int end); void referenceDataChanged (const QModelIndex& topLeft, const QModelIndex& bottomRight); void referenceAboutToBeRemoved (const QModelIndex& parent, int start, int end); void assetTablesChanged (); }; } #endif ================================================ FILE: apps/opencs/view/render/scenewidget.cpp ================================================ #include "scenewidget.hpp" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "../widget/scenetoolmode.hpp" #include "../../model/prefs/state.hpp" #include "../../model/prefs/shortcut.hpp" #include "lighting.hpp" #include "mask.hpp" #include "cameracontroller.hpp" namespace CSVRender { RenderWidget::RenderWidget(QWidget *parent, Qt::WindowFlags f) : QWidget(parent, f) , mRootNode(nullptr) { osgViewer::CompositeViewer& viewer = CompositeViewer::get(); osg::DisplaySettings* ds = osg::DisplaySettings::instance().get(); //ds->setNumMultiSamples(8); osg::ref_ptr traits = new osg::GraphicsContext::Traits; traits->windowName = ""; traits->windowDecoration = true; traits->x = 0; traits->y = 0; traits->width = width(); traits->height = height(); traits->doubleBuffer = true; traits->alpha = ds->getMinimumNumAlphaBits(); traits->stencil = ds->getMinimumNumStencilBits(); traits->sampleBuffers = ds->getMultiSamples(); traits->samples = ds->getNumMultiSamples(); // Doesn't make much sense as we're running on demand updates, and there seems to be a bug with the refresh rate when running multiple QGLWidgets traits->vsync = false; mView = new osgViewer::View; updateCameraParameters( traits->width / static_cast(traits->height) ); osg::ref_ptr window = new osgQt::GraphicsWindowQt(traits.get()); QLayout* layout = new QHBoxLayout(this); layout->setContentsMargins(0, 0, 0, 0); layout->addWidget(window->getGLWidget()); setLayout(layout); mView->getCamera()->setGraphicsContext(window); mView->getCamera()->setViewport( new osg::Viewport(0, 0, traits->width, traits->height) ); SceneUtil::LightManager* lightMgr = new SceneUtil::LightManager; lightMgr->setStartLight(1); lightMgr->setLightingMask(Mask_Lighting); mRootNode = lightMgr; mView->getCamera()->getOrCreateStateSet()->setMode(GL_NORMALIZE, osg::StateAttribute::ON); mView->getCamera()->getOrCreateStateSet()->setMode(GL_CULL_FACE, osg::StateAttribute::ON); osg::ref_ptr defaultMat (new osg::Material); defaultMat->setColorMode(osg::Material::OFF); defaultMat->setAmbient(osg::Material::FRONT_AND_BACK, osg::Vec4f(1,1,1,1)); 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)); mView->getCamera()->getOrCreateStateSet()->setAttribute(defaultMat); mView->setSceneData(mRootNode); // Add ability to signal osg to show its statistics for debugging purposes mView->addEventHandler(new osgViewer::StatsHandler); viewer.addView(mView); viewer.setDone(false); viewer.realize(); } RenderWidget::~RenderWidget() { try { CompositeViewer::get().removeView(mView); #if OSG_VERSION_LESS_THAN(3,6,5) // before OSG 3.6.4, the default font was a static object, and if it wasn't attached to the scene when a graphics context was destroyed, it's program wouldn't be released. // 3.6.4 moved it into the object cache, which meant it usually got released, but not here. // 3.6.5 improved cleanup with osgViewer::CompositeViewer::removeView so it more reliably released associated state for objects in the object cache. osg::ref_ptr graphicsContext = mView->getCamera()->getGraphicsContext(); osgText::Font::getDefaultFont()->releaseGLObjects(graphicsContext->getState()); #endif } catch(const std::exception& e) { Log(Debug::Error) << "Error in the destructor: " << e.what(); } } void RenderWidget::flagAsModified() { mView->requestRedraw(); } void RenderWidget::setVisibilityMask(unsigned int mask) { mView->getCamera()->setCullMask(mask | Mask_ParticleSystem | Mask_Lighting); } osg::Camera *RenderWidget::getCamera() { return mView->getCamera(); } void RenderWidget::toggleRenderStats() { osgViewer::GraphicsWindow* window = static_cast(mView->getCamera()->getGraphicsContext()); window->getEventQueue()->keyPress(osgGA::GUIEventAdapter::KEY_S); window->getEventQueue()->keyRelease(osgGA::GUIEventAdapter::KEY_S); } // -------------------------------------------------- CompositeViewer::CompositeViewer() : mSimulationTime(0.0) { // TODO: Upgrade osgQt to support osgViewer::ViewerBase::DrawThreadPerContext // https://gitlab.com/OpenMW/openmw/-/issues/5481 setThreadingModel(osgViewer::ViewerBase::SingleThreaded); #if OSG_VERSION_GREATER_OR_EQUAL(3,5,5) setUseConfigureAffinity(false); #endif // disable the default setting of viewer.done() by pressing Escape. setKeyEventSetsDone(0); // Only render when the camera position changed, or content flagged dirty //setRunFrameScheme(osgViewer::ViewerBase::ON_DEMAND); setRunFrameScheme(osgViewer::ViewerBase::CONTINUOUS); connect( &mTimer, SIGNAL(timeout()), this, SLOT(update()) ); mTimer.start( 10 ); int frameRateLimit = CSMPrefs::get()["Rendering"]["framerate-limit"].toInt(); setRunMaxFrameRate(frameRateLimit); } CompositeViewer &CompositeViewer::get() { static CompositeViewer sThis; return sThis; } void CompositeViewer::update() { double dt = mFrameTimer.time_s(); mFrameTimer.setStartTick(); emit simulationUpdated(dt); mSimulationTime += dt; frame(mSimulationTime); double minFrameTime = _runMaxFrameRate > 0.0 ? 1.0 / _runMaxFrameRate : 0.0; if (dt < minFrameTime) { std::this_thread::sleep_for(std::chrono::duration(minFrameTime - dt)); } } // --------------------------------------------------- SceneWidget::SceneWidget(std::shared_ptr resourceSystem, QWidget *parent, Qt::WindowFlags f, bool retrieveInput) : RenderWidget(parent, f) , mResourceSystem(resourceSystem) , mLighting(nullptr) , mHasDefaultAmbient(false) , mIsExterior(true) , mPrevMouseX(0) , mPrevMouseY(0) , mCamPositionSet(false) { mFreeCamControl = new FreeCameraController(this); mOrbitCamControl = new OrbitCameraController(this); mCurrentCamControl = mFreeCamControl; mOrbitCamControl->setPickingMask(Mask_Reference | Mask_Terrain); mOrbitCamControl->setConstRoll( CSMPrefs::get()["3D Scene Input"]["navi-orbit-const-roll"].isTrue() ); // set up gradient view or configured clear color QColor bgColour = CSMPrefs::get()["Rendering"]["scene-day-background-colour"].toColor(); if (CSMPrefs::get()["Rendering"]["scene-use-gradient"].isTrue()) { QColor gradientColour = CSMPrefs::get()["Rendering"]["scene-day-gradient-colour"].toColor(); mGradientCamera = createGradientCamera(bgColour, gradientColour); mView->getCamera()->setClearMask(0); mView->getCamera()->addChild(mGradientCamera.get()); } else { mView->getCamera()->setClearColor(osg::Vec4( bgColour.redF(), bgColour.greenF(), bgColour.blueF(), 1.0f )); } // we handle lighting manually mView->setLightingMode(osgViewer::View::NO_LIGHT); setLighting(&mLightingDay); mResourceSystem->getSceneManager()->setParticleSystemMask(Mask_ParticleSystem); // Recieve mouse move event even if mouse button is not pressed setMouseTracking(true); setFocusPolicy(Qt::ClickFocus); connect (&CSMPrefs::State::get(), SIGNAL (settingChanged (const CSMPrefs::Setting *)), this, SLOT (settingChanged (const CSMPrefs::Setting *))); // TODO update this outside of the constructor where virtual methods can be used if (retrieveInput) { CSMPrefs::get()["3D Scene Input"].update(); CSMPrefs::get()["Tooltips"].update(); } connect (&CompositeViewer::get(), SIGNAL (simulationUpdated(double)), this, SLOT (update(double))); // Shortcuts CSMPrefs::Shortcut* focusToolbarShortcut = new CSMPrefs::Shortcut("scene-focus-toolbar", this); connect(focusToolbarShortcut, SIGNAL(activated()), this, SIGNAL(focusToolbarRequest())); CSMPrefs::Shortcut* renderStatsShortcut = new CSMPrefs::Shortcut("scene-render-stats", this); connect(renderStatsShortcut, SIGNAL(activated()), this, SLOT(toggleRenderStats())); } SceneWidget::~SceneWidget() { // Since we're holding on to the resources past the existence of this graphics context, we'll need to manually release the created objects mResourceSystem->releaseGLObjects(mView->getCamera()->getGraphicsContext()->getState()); } osg::ref_ptr SceneWidget::createGradientRectangle(QColor bgColour, QColor gradientColour) { osg::ref_ptr geometry = new osg::Geometry; osg::ref_ptr vertices = new osg::Vec3Array; vertices->push_back(osg::Vec3(0.0f, 0.0f, -1.0f)); vertices->push_back(osg::Vec3(1.0f, 0.0f, -1.0f)); vertices->push_back(osg::Vec3(0.0f, 1.0f, -1.0f)); vertices->push_back(osg::Vec3(1.0f, 1.0f, -1.0f)); geometry->setVertexArray(vertices); osg::ref_ptr primitives = new osg::DrawElementsUShort (osg::PrimitiveSet::TRIANGLES, 0); // triangle 1 primitives->push_back (0); primitives->push_back (1); primitives->push_back (2); // triangle 2 primitives->push_back (2); primitives->push_back (1); primitives->push_back (3); geometry->addPrimitiveSet(primitives); osg::ref_ptr colours = new osg::Vec4ubArray; colours->push_back(osg::Vec4ub(gradientColour.red(), gradientColour.green(), gradientColour.blue(), 1.0f)); colours->push_back(osg::Vec4ub(gradientColour.red(), gradientColour.green(), gradientColour.blue(), 1.0f)); colours->push_back(osg::Vec4ub(bgColour.red(), bgColour.green(), bgColour.blue(), 1.0f)); colours->push_back(osg::Vec4ub(bgColour.red(), bgColour.green(), bgColour.blue(), 1.0f)); geometry->setColorArray(colours, osg::Array::BIND_PER_VERTEX); geometry->getOrCreateStateSet()->setMode(GL_LIGHTING, osg::StateAttribute::OFF); geometry->getOrCreateStateSet()->setMode(GL_DEPTH_TEST, osg::StateAttribute::OFF); return geometry; } osg::ref_ptr SceneWidget::createGradientCamera(QColor bgColour, QColor gradientColour) { osg::ref_ptr camera = new osg::Camera(); camera->setReferenceFrame(osg::Transform::ABSOLUTE_RF); camera->setProjectionMatrix(osg::Matrix::ortho2D(0, 1.0f, 0, 1.0f)); camera->setReferenceFrame(osg::Transform::ABSOLUTE_RF); camera->setViewMatrix(osg::Matrix::identity()); camera->setClearMask(GL_DEPTH_BUFFER_BIT | GL_COLOR_BUFFER_BIT); camera->setAllowEventFocus(false); // draw subgraph before main camera view. camera->setRenderOrder(osg::Camera::PRE_RENDER); camera->getOrCreateStateSet()->setMode(GL_DEPTH_TEST, osg::StateAttribute::OFF); osg::ref_ptr gradientQuad = createGradientRectangle(bgColour, gradientColour); camera->addChild(gradientQuad); return camera; } void SceneWidget::updateGradientCamera(QColor bgColour, QColor gradientColour) { osg::ref_ptr gradientRect = createGradientRectangle(bgColour, gradientColour); // Replaces previous rectangle mGradientCamera->setChild(0, gradientRect.get()); } void SceneWidget::setLighting(Lighting *lighting) { if (mLighting) mLighting->deactivate(); mLighting = lighting; mLighting->activate (mRootNode, mIsExterior); osg::Vec4f ambient = mLighting->getAmbientColour(mHasDefaultAmbient ? &mDefaultAmbient : nullptr); setAmbient(ambient); flagAsModified(); } void SceneWidget::setAmbient(const osg::Vec4f& ambient) { osg::ref_ptr stateset = new osg::StateSet; osg::ref_ptr lightmodel = new osg::LightModel; lightmodel->setAmbientIntensity(ambient); stateset->setMode(GL_LIGHTING, osg::StateAttribute::ON); stateset->setMode(GL_LIGHT0, osg::StateAttribute::ON); stateset->setAttributeAndModes(lightmodel, osg::StateAttribute::ON); mRootNode->setStateSet(stateset); } void SceneWidget::selectLightingMode (const std::string& mode) { QColor backgroundColour; QColor gradientColour; if (mode == "day") { backgroundColour = CSMPrefs::get()["Rendering"]["scene-day-background-colour"].toColor(); gradientColour = CSMPrefs::get()["Rendering"]["scene-day-gradient-colour"].toColor(); setLighting(&mLightingDay); } else if (mode == "night") { backgroundColour = CSMPrefs::get()["Rendering"]["scene-night-background-colour"].toColor(); gradientColour = CSMPrefs::get()["Rendering"]["scene-night-gradient-colour"].toColor(); setLighting(&mLightingNight); } else if (mode == "bright") { backgroundColour = CSMPrefs::get()["Rendering"]["scene-bright-background-colour"].toColor(); gradientColour = CSMPrefs::get()["Rendering"]["scene-bright-gradient-colour"].toColor(); setLighting(&mLightingBright); } if (CSMPrefs::get()["Rendering"]["scene-use-gradient"].isTrue()) { if (mGradientCamera.get() != nullptr) { // we can go ahead and update since this camera still exists updateGradientCamera(backgroundColour, gradientColour); if (!mView->getCamera()->containsNode(mGradientCamera.get())) { // need to re-attach the gradient camera mView->getCamera()->setClearMask(0); mView->getCamera()->addChild(mGradientCamera.get()); } } else { // need to create the gradient camera mGradientCamera = createGradientCamera(backgroundColour, gradientColour); mView->getCamera()->setClearMask(0); mView->getCamera()->addChild(mGradientCamera.get()); } } else { // Fall back to using the clear color for the camera mView->getCamera()->setClearMask(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); mView->getCamera()->setClearColor(osg::Vec4( backgroundColour.redF(), backgroundColour.greenF(), backgroundColour.blueF(), 1.0f )); if (mGradientCamera.get() != nullptr && mView->getCamera()->containsNode(mGradientCamera.get())) { // Remove the child to prevent the gradient from rendering mView->getCamera()->removeChild(mGradientCamera.get()); } } } CSVWidget::SceneToolMode *SceneWidget::makeLightingSelector (CSVWidget::SceneToolbar *parent) { CSVWidget::SceneToolMode *tool = new CSVWidget::SceneToolMode (parent, "Lighting Mode"); /// \todo replace icons tool->addButton (":scenetoolbar/day", "day", "Day" "

  • Cell specific ambient in interiors
  • " "
  • Low ambient in exteriors
  • " "
  • Strong directional light source
  • " "
  • This mode closely resembles day time in-game
"); tool->addButton (":scenetoolbar/night", "night", "Night" "
  • Cell specific ambient in interiors
  • " "
  • Low ambient in exteriors
  • " "
  • Weak directional light source
  • " "
  • This mode closely resembles night time in-game
"); tool->addButton (":scenetoolbar/bright", "bright", "Bright" "
  • Maximum ambient
  • " "
  • Strong directional light source
"); connect (tool, SIGNAL (modeChanged (const std::string&)), this, SLOT (selectLightingMode (const std::string&))); return tool; } void SceneWidget::setDefaultAmbient (const osg::Vec4f& colour) { mDefaultAmbient = colour; mHasDefaultAmbient = true; setAmbient(mLighting->getAmbientColour(&mDefaultAmbient)); } void SceneWidget::setExterior (bool isExterior) { mIsExterior = isExterior; } void SceneWidget::mouseMoveEvent (QMouseEvent *event) { mCurrentCamControl->handleMouseMoveEvent(event->x() - mPrevMouseX, event->y() - mPrevMouseY); mPrevMouseX = event->x(); mPrevMouseY = event->y(); } void SceneWidget::wheelEvent(QWheelEvent *event) { mCurrentCamControl->handleMouseScrollEvent(event->angleDelta().y()); } void SceneWidget::update(double dt) { if (mCamPositionSet) { mCurrentCamControl->update(dt); } else { mCurrentCamControl->setup(mRootNode, Mask_Reference | Mask_Terrain, CameraController::WorldUp); mCamPositionSet = true; } } void SceneWidget::settingChanged (const CSMPrefs::Setting *setting) { if (*setting=="3D Scene Input/p-navi-free-sensitivity") { mFreeCamControl->setCameraSensitivity(setting->toDouble()); } else if (*setting=="3D Scene Input/p-navi-orbit-sensitivity") { mOrbitCamControl->setCameraSensitivity(setting->toDouble()); } else if (*setting=="3D Scene Input/p-navi-free-invert") { mFreeCamControl->setInverted(setting->isTrue()); } else if (*setting=="3D Scene Input/p-navi-orbit-invert") { mOrbitCamControl->setInverted(setting->isTrue()); } else if (*setting=="3D Scene Input/s-navi-sensitivity") { mFreeCamControl->setSecondaryMovementMultiplier(setting->toDouble()); mOrbitCamControl->setSecondaryMovementMultiplier(setting->toDouble()); } else if (*setting=="3D Scene Input/navi-wheel-factor") { mFreeCamControl->setWheelMovementMultiplier(setting->toDouble()); mOrbitCamControl->setWheelMovementMultiplier(setting->toDouble()); } else if (*setting=="3D Scene Input/navi-free-lin-speed") { mFreeCamControl->setLinearSpeed(setting->toDouble()); } else if (*setting=="3D Scene Input/navi-free-rot-speed") { mFreeCamControl->setRotationalSpeed(setting->toDouble()); } else if (*setting=="3D Scene Input/navi-free-speed-mult") { mFreeCamControl->setSpeedMultiplier(setting->toDouble()); } else if (*setting=="3D Scene Input/navi-orbit-rot-speed") { mOrbitCamControl->setOrbitSpeed(setting->toDouble()); } else if (*setting=="3D Scene Input/navi-orbit-speed-mult") { mOrbitCamControl->setOrbitSpeedMultiplier(setting->toDouble()); } else if (*setting=="3D Scene Input/navi-orbit-const-roll") { mOrbitCamControl->setConstRoll(setting->isTrue()); } else if (*setting=="Rendering/framerate-limit") { CompositeViewer::get().setRunMaxFrameRate(setting->toInt()); } else if (*setting=="Rendering/camera-fov" || *setting=="Rendering/camera-ortho" || *setting=="Rendering/camera-ortho-size") { updateCameraParameters(); } } void RenderWidget::updateCameraParameters(double overrideAspect) { const float nearDist = 1.0; const float farDist = 1000.0; if (CSMPrefs::get()["Rendering"]["camera-ortho"].isTrue()) { const float size = CSMPrefs::get()["Rendering"]["camera-ortho-size"].toInt(); const float aspect = overrideAspect >= 0.0 ? overrideAspect : (width() / static_cast(height())); const float halfH = size * 10.0; const float halfW = halfH * aspect; mView->getCamera()->setProjectionMatrixAsOrtho( -halfW, halfW, -halfH, halfH, nearDist, farDist); } else { mView->getCamera()->setProjectionMatrixAsPerspective( CSMPrefs::get()["Rendering"]["camera-fov"].toInt(), static_cast(width())/static_cast(height()), nearDist, farDist); } } void SceneWidget::selectNavigationMode (const std::string& mode) { if (mode=="1st") { mCurrentCamControl->setCamera(nullptr); mCurrentCamControl = mFreeCamControl; mFreeCamControl->setCamera(getCamera()); mFreeCamControl->fixUpAxis(CameraController::WorldUp); } else if (mode=="free") { mCurrentCamControl->setCamera(nullptr); mCurrentCamControl = mFreeCamControl; mFreeCamControl->setCamera(getCamera()); mFreeCamControl->unfixUpAxis(); } else if (mode=="orbit") { mCurrentCamControl->setCamera(nullptr); mCurrentCamControl = mOrbitCamControl; mOrbitCamControl->setCamera(getCamera()); mOrbitCamControl->reset(); } } } ================================================ FILE: apps/opencs/view/render/scenewidget.hpp ================================================ #ifndef OPENCS_VIEW_SCENEWIDGET_H #define OPENCS_VIEW_SCENEWIDGET_H #include #include #include #include #include #include #include "lightingday.hpp" #include "lightingnight.hpp" #include "lightingbright.hpp" namespace Resource { class ResourceSystem; } namespace osg { class Group; class Camera; } namespace CSVWidget { class SceneToolMode; class SceneToolbar; } namespace CSMPrefs { class Setting; } namespace CSVRender { class CameraController; class FreeCameraController; class OrbitCameraController; class Lighting; class RenderWidget : public QWidget { Q_OBJECT public: RenderWidget(QWidget* parent = nullptr, Qt::WindowFlags f = Qt::WindowFlags()); virtual ~RenderWidget(); /// Initiates a request to redraw the view void flagAsModified(); void setVisibilityMask(unsigned int mask); osg::Camera *getCamera(); protected: osg::ref_ptr mView; osg::ref_ptr mRootNode; void updateCameraParameters(double overrideAspect = -1.0); QTimer mTimer; protected slots: void toggleRenderStats(); }; /// Extension of RenderWidget to support lighting mode selection & toolbar class SceneWidget : public RenderWidget { Q_OBJECT public: SceneWidget(std::shared_ptr resourceSystem, QWidget* parent = nullptr, Qt::WindowFlags f = Qt::WindowFlags(), bool retrieveInput = true); virtual ~SceneWidget(); CSVWidget::SceneToolMode *makeLightingSelector (CSVWidget::SceneToolbar *parent); ///< \attention The created tool is not added to the toolbar (via addTool). Doing that /// is the responsibility of the calling function. void setDefaultAmbient (const osg::Vec4f& colour); ///< \note The actual ambient colour may differ based on lighting settings. void setExterior (bool isExterior); protected: void setLighting (Lighting *lighting); ///< \attention The ownership of \a lighting is not transferred to *this. void setAmbient(const osg::Vec4f& ambient); void mouseMoveEvent (QMouseEvent *event) override; void wheelEvent (QWheelEvent *event) override; osg::ref_ptr createGradientRectangle(QColor bgColour, QColor gradientColour); osg::ref_ptr createGradientCamera(QColor bgColour, QColor gradientColour); void updateGradientCamera(QColor bgColour, QColor gradientColour); std::shared_ptr mResourceSystem; Lighting* mLighting; osg::ref_ptr mGradientCamera; osg::Vec4f mDefaultAmbient; bool mHasDefaultAmbient; bool mIsExterior; LightingDay mLightingDay; LightingNight mLightingNight; LightingBright mLightingBright; int mPrevMouseX, mPrevMouseY; /// Tells update that camera isn't set bool mCamPositionSet; FreeCameraController* mFreeCamControl; OrbitCameraController* mOrbitCamControl; CameraController* mCurrentCamControl; public slots: void update(double dt); protected slots: virtual void settingChanged (const CSMPrefs::Setting *setting); void selectNavigationMode (const std::string& mode); private slots: void selectLightingMode (const std::string& mode); signals: void focusToolbarRequest(); }; // There are rendering glitches when using multiple Viewer instances, work around using CompositeViewer with multiple views class CompositeViewer : public QObject, public osgViewer::CompositeViewer { Q_OBJECT public: CompositeViewer(); static CompositeViewer& get(); QTimer mTimer; private: osg::Timer mFrameTimer; double mSimulationTime; public slots: void update(); signals: void simulationUpdated(double dt); }; } #endif ================================================ FILE: apps/opencs/view/render/selectionmode.cpp ================================================ #include "selectionmode.hpp" #include #include #include "worldspacewidget.hpp" namespace CSVRender { SelectionMode::SelectionMode(CSVWidget::SceneToolbar* parent, WorldspaceWidget& worldspaceWidget, unsigned int interactionMask) : SceneToolMode(parent, "Selection mode") , mWorldspaceWidget(worldspaceWidget) , mInteractionMask(interactionMask) { addButton(":scenetoolbar/selection-mode-cube", "cube-centre", "Centred cube" "
  • Drag with {scene-select-primary} for primary select or {scene-select-secondary} for secondary select " "from the centre of the selection cube outwards.
  • " "
  • The selection cube is aligned to the word space axis
  • " "
  • If context selection mode is enabled, a drag with {scene-edit-primary} or {scene-edit-secondary} not " "starting on an instance will have the same effect
  • " "
"); addButton(":scenetoolbar/selection-mode-cube-corner", "cube-corner", "Cube corner to corner" "
  • Drag with {scene-select-primary} for primary select or {scene-select-secondary} for secondary select " "from one corner of the selection cube to the opposite corner
  • " "
  • The selection cube is aligned to the word space axis
  • " "
  • If context selection mode is enabled, a drag with {scene-edit-primary} or {scene-edit-secondary} not " "starting on an instance will have the same effect
  • " "
"); addButton(":scenetoolbar/selection-mode-cube-sphere", "sphere", "Centred sphere" "
  • Drag with {scene-select-primary} for primary select or {scene-select-secondary} for secondary select " "from the centre of the selection sphere outwards
  • " "
  • If context selection mode is enabled, a drag with {scene-edit-primary} or {scene-edit-secondary} not " "starting on an instance will have the same effect
  • " "
"); mSelectAll = new QAction("Select all", this); mDeselectAll = new QAction("Clear selection", this); mInvertSelection = new QAction("Invert selection", this); connect(mSelectAll, SIGNAL(triggered()), this, SLOT(selectAll())); connect(mDeselectAll, SIGNAL(triggered()), this, SLOT(clearSelection())); connect(mInvertSelection, SIGNAL(triggered()), this, SLOT(invertSelection())); } WorldspaceWidget& SelectionMode::getWorldspaceWidget() { return mWorldspaceWidget; } bool SelectionMode::createContextMenu (QMenu* menu) { if (menu) { menu->addAction(mSelectAll); menu->addAction(mDeselectAll); menu->addAction(mInvertSelection); } return true; } void SelectionMode::selectAll() { getWorldspaceWidget().selectAll(mInteractionMask); } void SelectionMode::clearSelection() { getWorldspaceWidget().clearSelection(mInteractionMask); } void SelectionMode::invertSelection() { getWorldspaceWidget().invertSelection(mInteractionMask); } } ================================================ FILE: apps/opencs/view/render/selectionmode.hpp ================================================ #ifndef CSV_RENDER_SELECTION_MODE_H #define CSV_RENDER_SELECTION_MODE_H #include "../widget/scenetoolmode.hpp" #include "mask.hpp" class QAction; namespace CSVRender { class WorldspaceWidget; class SelectionMode : public CSVWidget::SceneToolMode { Q_OBJECT public: SelectionMode(CSVWidget::SceneToolbar* parent, WorldspaceWidget& worldspaceWidget, unsigned int interactionMask); protected: WorldspaceWidget& getWorldspaceWidget(); /// Add context menu items to \a menu. /// /// \attention menu can be a 0-pointer /// /// \return Have there been any menu items to be added (if menu is 0 and there /// items to be added, the function must return true anyway. bool createContextMenu (QMenu* menu) override; private: WorldspaceWidget& mWorldspaceWidget; unsigned int mInteractionMask; QAction* mSelectAll; QAction* mDeselectAll; QAction* mInvertSelection; protected slots: virtual void selectAll(); virtual void clearSelection(); virtual void invertSelection(); }; } #endif ================================================ FILE: apps/opencs/view/render/tagbase.cpp ================================================ #include "tagbase.hpp" CSVRender::TagBase::TagBase (Mask mask) : mMask (mask) {} CSVRender::Mask CSVRender::TagBase::getMask() const { return mMask; } QString CSVRender::TagBase::getToolTip (bool hideBasics) const { return ""; } ================================================ FILE: apps/opencs/view/render/tagbase.hpp ================================================ #ifndef OPENCS_VIEW_TAGBASE_H #define OPENCS_VIEW_TAGBASE_H #include #include #include "mask.hpp" namespace CSVRender { class TagBase : public osg::Referenced { Mask mMask; public: TagBase (Mask mask); Mask getMask() const; virtual QString getToolTip (bool hideBasics) const; }; } #endif ================================================ FILE: apps/opencs/view/render/terrainselection.cpp ================================================ #include "terrainselection.hpp" #include #include #include #include #include #include "../../model/world/cellcoordinates.hpp" #include "../../model/world/columnimp.hpp" #include "../../model/world/idtable.hpp" #include "cell.hpp" #include "worldspacewidget.hpp" CSVRender::TerrainSelection::TerrainSelection(osg::Group* parentNode, WorldspaceWidget *worldspaceWidget, TerrainSelectionType type): mParentNode(parentNode), mWorldspaceWidget (worldspaceWidget), mDraggedOperationFlag(false), mSelectionType(type) { mGeometry = new osg::Geometry(); mSelectionNode = new osg::Group(); mSelectionNode->addChild(mGeometry); activate(); } CSVRender::TerrainSelection::~TerrainSelection() { deactivate(); } std::vector> CSVRender::TerrainSelection::getTerrainSelection() const { return mSelection; } void CSVRender::TerrainSelection::onlySelect(const std::vector> &localPositions) { mSelection = localPositions; update(); } void CSVRender::TerrainSelection::addSelect(const std::vector>& localPositions, bool toggleInProgress) { handleSelection(localPositions, toggleInProgress, SelectionMethod::AddSelect); } void CSVRender::TerrainSelection::removeSelect(const std::vector>& localPositions, bool toggleInProgress) { handleSelection(localPositions, toggleInProgress, SelectionMethod::RemoveSelect); } void CSVRender::TerrainSelection::toggleSelect(const std::vector>& localPositions, bool toggleInProgress) { handleSelection(localPositions, toggleInProgress, SelectionMethod::ToggleSelect); } void CSVRender::TerrainSelection::activate() { if (!mParentNode->containsNode(mSelectionNode)) mParentNode->addChild(mSelectionNode); } void CSVRender::TerrainSelection::deactivate() { mParentNode->removeChild(mSelectionNode); } void CSVRender::TerrainSelection::update() { mSelectionNode->removeChild(mGeometry); mGeometry = new osg::Geometry(); const osg::ref_ptr vertices (new osg::Vec3Array); switch (mSelectionType) { case TerrainSelectionType::Texture : drawTextureSelection(vertices); break; case TerrainSelectionType::Shape : drawShapeSelection(vertices); break; } mGeometry->setVertexArray(vertices); osg::ref_ptr drawArrays = new osg::DrawArrays(osg::PrimitiveSet::LINES); drawArrays->setCount(vertices->size()); if (vertices->size() != 0) mGeometry->addPrimitiveSet(drawArrays); mSelectionNode->addChild(mGeometry); } void CSVRender::TerrainSelection::drawShapeSelection(const osg::ref_ptr vertices) { if (!mSelection.empty()) { for (std::pair &localPos : mSelection) { int x (localPos.first); int y (localPos.second); float xWorldCoord(CSMWorld::CellCoordinates::vertexGlobalToWorldCoords(x)); float yWorldCoord(CSMWorld::CellCoordinates::vertexGlobalToWorldCoords(y)); osg::Vec3f pointXY(xWorldCoord, yWorldCoord, calculateLandHeight(x, y) + 2); vertices->push_back(pointXY); vertices->push_back(osg::Vec3f(xWorldCoord, CSMWorld::CellCoordinates::vertexGlobalToWorldCoords(y - 1), calculateLandHeight(x, y - 1) + 2)); vertices->push_back(pointXY); vertices->push_back(osg::Vec3f(CSMWorld::CellCoordinates::vertexGlobalToWorldCoords(x - 1), yWorldCoord, calculateLandHeight(x - 1, y) + 2)); const auto north = std::find(mSelection.begin(), mSelection.end(), std::make_pair(x, y + 1)); if (north == mSelection.end()) { vertices->push_back(pointXY); vertices->push_back(osg::Vec3f(xWorldCoord, CSMWorld::CellCoordinates::vertexGlobalToWorldCoords(y + 1), calculateLandHeight(x, y + 1) + 2)); } const auto east = std::find(mSelection.begin(), mSelection.end(), std::make_pair(x + 1, y)); if (east == mSelection.end()) { vertices->push_back(pointXY); vertices->push_back(osg::Vec3f(CSMWorld::CellCoordinates::vertexGlobalToWorldCoords(x + 1), yWorldCoord, calculateLandHeight(x + 1, y) + 2)); } } } } void CSVRender::TerrainSelection::drawTextureSelection(const osg::ref_ptr vertices) { if (!mSelection.empty()) { const int landHeightsNudge = (ESM::Land::REAL_SIZE / ESM::Land::LAND_SIZE) / (ESM::Land::LAND_SIZE - 1); // Does this work with all land size configurations? const int textureSizeToLandSizeModifier = (ESM::Land::LAND_SIZE - 1) / ESM::Land::LAND_TEXTURE_SIZE; for (std::pair &localPos : mSelection) { int x (localPos.first); int y (localPos.second); // convert texture selection to global vertex coordinates at selection box corners int x1 = x * textureSizeToLandSizeModifier + landHeightsNudge; int x2 = x * textureSizeToLandSizeModifier + textureSizeToLandSizeModifier + landHeightsNudge; int y1 = y * textureSizeToLandSizeModifier - landHeightsNudge; int y2 = y * textureSizeToLandSizeModifier + textureSizeToLandSizeModifier - landHeightsNudge; // Draw edges (check all sides, draw lines between vertices, +1 height to keep lines above ground) // Check adjancent selections, draw lines only to edges of the selection const auto north = std::find(mSelection.begin(), mSelection.end(), std::make_pair(x, y + 1)); if (north == mSelection.end()) { for(int i = 1; i < (textureSizeToLandSizeModifier + 1); i++) { float drawPreviousX = CSMWorld::CellCoordinates::textureGlobalXToWorldCoords(x) + (i - 1) * (ESM::Land::REAL_SIZE / (ESM::Land::LAND_SIZE - 1)); float drawCurrentX = CSMWorld::CellCoordinates::textureGlobalXToWorldCoords(x) + i * (ESM::Land::REAL_SIZE / (ESM::Land::LAND_SIZE - 1)); vertices->push_back(osg::Vec3f(drawPreviousX, CSMWorld::CellCoordinates::textureGlobalYToWorldCoords(y + 1), calculateLandHeight(x1+(i-1), y2)+2)); vertices->push_back(osg::Vec3f(drawCurrentX, CSMWorld::CellCoordinates::textureGlobalYToWorldCoords(y + 1), calculateLandHeight(x1+i, y2)+2)); } } const auto south = std::find(mSelection.begin(), mSelection.end(), std::make_pair(x, y - 1)); if (south == mSelection.end()) { for(int i = 1; i < (textureSizeToLandSizeModifier + 1); i++) { float drawPreviousX = CSMWorld::CellCoordinates::textureGlobalXToWorldCoords(x) + (i - 1) *(ESM::Land::REAL_SIZE / (ESM::Land::LAND_SIZE - 1)); float drawCurrentX = CSMWorld::CellCoordinates::textureGlobalXToWorldCoords(x) + i * (ESM::Land::REAL_SIZE / (ESM::Land::LAND_SIZE - 1)); vertices->push_back(osg::Vec3f(drawPreviousX, CSMWorld::CellCoordinates::textureGlobalYToWorldCoords(y), calculateLandHeight(x1+(i-1), y1)+2)); vertices->push_back(osg::Vec3f(drawCurrentX, CSMWorld::CellCoordinates::textureGlobalYToWorldCoords(y), calculateLandHeight(x1+i, y1)+2)); } } const auto east = std::find(mSelection.begin(), mSelection.end(), std::make_pair(x + 1, y)); if (east == mSelection.end()) { for(int i = 1; i < (textureSizeToLandSizeModifier + 1); i++) { float drawPreviousY = CSMWorld::CellCoordinates::textureGlobalYToWorldCoords(y) + (i - 1) * (ESM::Land::REAL_SIZE / (ESM::Land::LAND_SIZE - 1)); float drawCurrentY = CSMWorld::CellCoordinates::textureGlobalYToWorldCoords(y) + i * (ESM::Land::REAL_SIZE / (ESM::Land::LAND_SIZE - 1)); vertices->push_back(osg::Vec3f(CSMWorld::CellCoordinates::textureGlobalXToWorldCoords(x + 1), drawPreviousY, calculateLandHeight(x2, y1+(i-1))+2)); vertices->push_back(osg::Vec3f(CSMWorld::CellCoordinates::textureGlobalXToWorldCoords(x + 1), drawCurrentY, calculateLandHeight(x2, y1+i)+2)); } } const auto west = std::find(mSelection.begin(), mSelection.end(), std::make_pair(x - 1, y)); if (west == mSelection.end()) { for(int i = 1; i < (textureSizeToLandSizeModifier + 1); i++) { float drawPreviousY = CSMWorld::CellCoordinates::textureGlobalYToWorldCoords(y) + (i - 1) * (ESM::Land::REAL_SIZE / (ESM::Land::LAND_SIZE - 1)); float drawCurrentY = CSMWorld::CellCoordinates::textureGlobalYToWorldCoords(y) + i * (ESM::Land::REAL_SIZE / (ESM::Land::LAND_SIZE - 1)); vertices->push_back(osg::Vec3f(CSMWorld::CellCoordinates::textureGlobalXToWorldCoords(x), drawPreviousY, calculateLandHeight(x1, y1+(i-1))+2)); vertices->push_back(osg::Vec3f(CSMWorld::CellCoordinates::textureGlobalXToWorldCoords(x), drawCurrentY, calculateLandHeight(x1, y1+i)+2)); } } } } } void CSVRender::TerrainSelection::handleSelection(const std::vector>& localPositions, bool toggleInProgress, SelectionMethod selectionMethod) { if (toggleInProgress) { for (auto const& localPos : localPositions) { auto iterTemp = std::find(mTemporarySelection.begin(), mTemporarySelection.end(), localPos); mDraggedOperationFlag = true; if (iterTemp == mTemporarySelection.end()) { auto iter = std::find(mSelection.begin(), mSelection.end(), localPos); switch (selectionMethod) { case SelectionMethod::AddSelect: if (iter == mSelection.end()) { mSelection.emplace_back(localPos); } break; case SelectionMethod::RemoveSelect: if (iter != mSelection.end()) { mSelection.erase(iter); } break; case SelectionMethod::ToggleSelect: if (iter == mSelection.end()) { mSelection.emplace_back(localPos); } else { mSelection.erase(iter); } break; default: break; } } mTemporarySelection.push_back(localPos); } } else if (mDraggedOperationFlag == false) { for (auto const& localPos : localPositions) { const auto iter = std::find(mSelection.begin(), mSelection.end(), localPos); switch (selectionMethod) { case SelectionMethod::AddSelect: if (iter == mSelection.end()) { mSelection.emplace_back(localPos); } break; case SelectionMethod::RemoveSelect: if (iter != mSelection.end()) { mSelection.erase(iter); } break; case SelectionMethod::ToggleSelect: if (iter == mSelection.end()) { mSelection.emplace_back(localPos); } else { mSelection.erase(iter); } break; default: break; } } } else { mDraggedOperationFlag = false; mTemporarySelection.clear(); } update(); } bool CSVRender::TerrainSelection::noCell(const std::string& cellId) { CSMDoc::Document& document = mWorldspaceWidget->getDocument(); const CSMWorld::IdCollection& cellCollection = document.getData().getCells(); return cellCollection.searchId (cellId) == -1; } bool CSVRender::TerrainSelection::noLand(const std::string& cellId) { CSMDoc::Document& document = mWorldspaceWidget->getDocument(); const CSMWorld::IdCollection& landCollection = document.getData().getLand(); return landCollection.searchId (cellId) == -1; } bool CSVRender::TerrainSelection::noLandLoaded(const std::string& cellId) { CSMDoc::Document& document = mWorldspaceWidget->getDocument(); const CSMWorld::IdCollection& landCollection = document.getData().getLand(); return !landCollection.getRecord(cellId).get().isDataLoaded(ESM::Land::DATA_VNML); } bool CSVRender::TerrainSelection::isLandLoaded(const std::string& cellId) { if (!noCell(cellId) && !noLand(cellId) && !noLandLoaded(cellId)) return true; return false; } int CSVRender::TerrainSelection::calculateLandHeight(int x, int y) // global vertex coordinates { int cellX = std::floor(static_cast(x) / (ESM::Land::LAND_SIZE - 1)); int cellY = std::floor(static_cast(y) / (ESM::Land::LAND_SIZE - 1)); int localX = x - cellX * (ESM::Land::LAND_SIZE - 1); int localY = y - cellY * (ESM::Land::LAND_SIZE - 1); CSMWorld::CellCoordinates coords (cellX, cellY); float landHeight = 0.f; if (CSVRender::Cell* cell = dynamic_cast(mWorldspaceWidget->getCell(coords))) { landHeight = cell->getSumOfAlteredAndTrueHeight(cellX, cellY, localX, localY); } else if (isLandLoaded(CSMWorld::CellCoordinates::generateId(cellX, cellY))) { CSMDoc::Document& document = mWorldspaceWidget->getDocument(); CSMWorld::IdTable& landTable = dynamic_cast ( *document.getData().getTableModel (CSMWorld::UniversalId::Type_Land)); std::string cellId = CSMWorld::CellCoordinates::generateId(cellX, cellY); int landshapeColumn = landTable.findColumnIndex(CSMWorld::Columns::ColumnId_LandHeightsIndex); const CSMWorld::LandHeightsColumn::DataType mPointer = landTable.data(landTable.getModelIndex(cellId, landshapeColumn)).value(); return mPointer[localY*ESM::Land::LAND_SIZE + localX]; } return landHeight; } ================================================ FILE: apps/opencs/view/render/terrainselection.hpp ================================================ #ifndef CSV_RENDER_TERRAINSELECTION_H #define CSV_RENDER_TERRAINSELECTION_H #include #include #include #include #include #include #include "../../model/world/cellcoordinates.hpp" namespace osg { class Group; } namespace CSVRender { struct WorldspaceHitResult; class WorldspaceWidget; enum class TerrainSelectionType { Texture, Shape }; enum class SelectionMethod { OnlySelect, AddSelect, RemoveSelect, ToggleSelect }; /// \brief Class handling the terrain selection data and rendering class TerrainSelection { public: TerrainSelection(osg::Group* parentNode, WorldspaceWidget *worldspaceWidget, TerrainSelectionType type); ~TerrainSelection(); void onlySelect(const std::vector> &localPositions); void addSelect(const std::vector>& localPositions, bool toggleInProgress); void removeSelect(const std::vector>& localPositions, bool toggleInProgress); void toggleSelect(const std::vector> &localPositions, bool toggleInProgress); void activate(); void deactivate(); std::vector> getTerrainSelection() const; void update(); protected: void drawShapeSelection(const osg::ref_ptr vertices); void drawTextureSelection(const osg::ref_ptr vertices); int calculateLandHeight(int x, int y); private: void handleSelection(const std::vector>& localPositions, bool toggleInProgress, SelectionMethod selectionMethod); bool noCell(const std::string& cellId); bool noLand(const std::string& cellId); bool noLandLoaded(const std::string& cellId); bool isLandLoaded(const std::string& cellId); osg::Group* mParentNode; WorldspaceWidget *mWorldspaceWidget; osg::ref_ptr mBaseNode; osg::ref_ptr mGeometry; osg::ref_ptr mSelectionNode; std::vector> mSelection; // Global terrain selection coordinate in either vertex or texture units std::vector> mTemporarySelection; // Used during toggle to compare the most recent drag operation bool mDraggedOperationFlag; //true during drag operation, false when click-operation TerrainSelectionType mSelectionType; }; } #endif ================================================ FILE: apps/opencs/view/render/terrainshapemode.cpp ================================================ #include "terrainshapemode.hpp" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "../widget/brushshapes.hpp" #include "../widget/modebutton.hpp" #include "../widget/scenetoolbar.hpp" #include "../widget/scenetoolshapebrush.hpp" #include "../../model/doc/document.hpp" #include "../../model/prefs/state.hpp" #include "../../model/world/commands.hpp" #include "../../model/world/data.hpp" #include "../../model/world/idtable.hpp" #include "../../model/world/idtree.hpp" #include "../../model/world/land.hpp" #include "../../model/world/tablemimedata.hpp" #include "../../model/world/universalid.hpp" #include "brushdraw.hpp" #include "commands.hpp" #include "editmode.hpp" #include "pagedworldspacewidget.hpp" #include "mask.hpp" #include "tagbase.hpp" #include "terrainselection.hpp" #include "worldspacewidget.hpp" CSVRender::TerrainShapeMode::TerrainShapeMode (WorldspaceWidget *worldspaceWidget, osg::Group* parentNode, QWidget *parent) : EditMode (worldspaceWidget, QIcon {":scenetoolbar/editing-terrain-shape"}, Mask_Terrain, "Terrain land editing", parent), mParentNode(parentNode) { } void CSVRender::TerrainShapeMode::activate(CSVWidget::SceneToolbar* toolbar) { if (!mTerrainShapeSelection) { mTerrainShapeSelection.reset(new TerrainSelection(mParentNode, &getWorldspaceWidget(), TerrainSelectionType::Shape)); } if(!mShapeBrushScenetool) { mShapeBrushScenetool = new CSVWidget::SceneToolShapeBrush (toolbar, "scenetoolshapebrush", getWorldspaceWidget().getDocument()); connect(mShapeBrushScenetool, SIGNAL (clicked()), mShapeBrushScenetool, SLOT (activate())); connect(mShapeBrushScenetool->mShapeBrushWindow, SIGNAL(passBrushSize(int)), this, SLOT(setBrushSize(int))); connect(mShapeBrushScenetool->mShapeBrushWindow, SIGNAL(passBrushShape(CSVWidget::BrushShape)), this, SLOT(setBrushShape(CSVWidget::BrushShape))); connect(mShapeBrushScenetool->mShapeBrushWindow->mSizeSliders->mBrushSizeSlider, SIGNAL(valueChanged(int)), this, SLOT(setBrushSize(int))); connect(mShapeBrushScenetool->mShapeBrushWindow->mToolSelector, SIGNAL(currentIndexChanged(int)), this, SLOT(setShapeEditTool(int))); connect(mShapeBrushScenetool->mShapeBrushWindow->mToolStrengthSlider, SIGNAL(valueChanged(int)), this, SLOT(setShapeEditToolStrength(int))); } if (!mBrushDraw) mBrushDraw.reset(new BrushDraw(mParentNode)); EditMode::activate(toolbar); toolbar->addTool (mShapeBrushScenetool); } void CSVRender::TerrainShapeMode::deactivate(CSVWidget::SceneToolbar* toolbar) { if(mShapeBrushScenetool) { toolbar->removeTool (mShapeBrushScenetool); } if (mTerrainShapeSelection) { mTerrainShapeSelection.reset(); } if (mBrushDraw) mBrushDraw.reset(); EditMode::deactivate(toolbar); } void CSVRender::TerrainShapeMode::primaryOpenPressed (const WorldspaceHitResult& hit) // Apply changes here { } void CSVRender::TerrainShapeMode::primaryEditPressed(const WorldspaceHitResult& hit) { if (hit.hit && hit.tag == nullptr) { if (mShapeEditTool == ShapeEditTool_Flatten) setFlattenToolTargetHeight(hit); if (mDragMode == InteractionType_PrimaryEdit && mShapeEditTool != ShapeEditTool_Drag) { editTerrainShapeGrid(CSMWorld::CellCoordinates::toVertexCoords(hit.worldPos), true); applyTerrainEditChanges(); } if (mDragMode == InteractionType_PrimarySelect) { selectTerrainShapes(CSMWorld::CellCoordinates::toVertexCoords(hit.worldPos), 0, true); } if (mDragMode == InteractionType_SecondarySelect) { selectTerrainShapes(CSMWorld::CellCoordinates::toVertexCoords(hit.worldPos), 1, true); } } clearTransientEdits(); } void CSVRender::TerrainShapeMode::primarySelectPressed(const WorldspaceHitResult& hit) { if(hit.hit && hit.tag == nullptr) { selectTerrainShapes(CSMWorld::CellCoordinates::toVertexCoords(hit.worldPos), 0, false); } } void CSVRender::TerrainShapeMode::secondarySelectPressed(const WorldspaceHitResult& hit) { if(hit.hit && hit.tag == nullptr) { selectTerrainShapes(CSMWorld::CellCoordinates::toVertexCoords(hit.worldPos), 1, false); } } bool CSVRender::TerrainShapeMode::primaryEditStartDrag (const QPoint& pos) { WorldspaceHitResult hit = getWorldspaceWidget().mousePick (pos, getWorldspaceWidget().getInteractionMask()); mDragMode = InteractionType_PrimaryEdit; if (hit.hit && hit.tag == nullptr) { mEditingPos = hit.worldPos; mIsEditing = true; if (mShapeEditTool == ShapeEditTool_Flatten) setFlattenToolTargetHeight(hit); } return true; } bool CSVRender::TerrainShapeMode::secondaryEditStartDrag (const QPoint& pos) { return false; } bool CSVRender::TerrainShapeMode::primarySelectStartDrag (const QPoint& pos) { WorldspaceHitResult hit = getWorldspaceWidget().mousePick (pos, getWorldspaceWidget().getInteractionMask()); mDragMode = InteractionType_PrimarySelect; if (!hit.hit || hit.tag != nullptr) { mDragMode = InteractionType_None; return false; } selectTerrainShapes(CSMWorld::CellCoordinates::toVertexCoords(hit.worldPos), 0, true); return false; } bool CSVRender::TerrainShapeMode::secondarySelectStartDrag (const QPoint& pos) { WorldspaceHitResult hit = getWorldspaceWidget().mousePick (pos, getWorldspaceWidget().getInteractionMask()); mDragMode = InteractionType_SecondarySelect; if (!hit.hit || hit.tag != nullptr) { mDragMode = InteractionType_None; return false; } selectTerrainShapes(CSMWorld::CellCoordinates::toVertexCoords(hit.worldPos), 1, true); return false; } void CSVRender::TerrainShapeMode::drag (const QPoint& pos, int diffX, int diffY, double speedFactor) { if (mDragMode == InteractionType_PrimaryEdit) { WorldspaceHitResult hit = getWorldspaceWidget().mousePick (pos, getWorldspaceWidget().getInteractionMask()); mTotalDiffY += diffY; if (mIsEditing) { if (mShapeEditTool == ShapeEditTool_Drag) editTerrainShapeGrid(CSMWorld::CellCoordinates::toVertexCoords(mEditingPos), true); else editTerrainShapeGrid(CSMWorld::CellCoordinates::toVertexCoords(hit.worldPos), true); } } if (mDragMode == InteractionType_PrimarySelect) { WorldspaceHitResult hit = getWorldspaceWidget().mousePick (pos, getWorldspaceWidget().getInteractionMask()); if (hit.hit && hit.tag == nullptr) selectTerrainShapes(CSMWorld::CellCoordinates::toVertexCoords(hit.worldPos), 0, true); } if (mDragMode == InteractionType_SecondarySelect) { WorldspaceHitResult hit = getWorldspaceWidget().mousePick (pos, getWorldspaceWidget().getInteractionMask()); if (hit.hit && hit.tag == nullptr) selectTerrainShapes(CSMWorld::CellCoordinates::toVertexCoords(hit.worldPos), 1, true); } } void CSVRender::TerrainShapeMode::dragCompleted(const QPoint& pos) { if (mDragMode == InteractionType_PrimaryEdit) { applyTerrainEditChanges(); clearTransientEdits(); } } void CSVRender::TerrainShapeMode::dragAborted() { clearTransientEdits(); } void CSVRender::TerrainShapeMode::dragWheel (int diff, double speedFactor) { } void CSVRender::TerrainShapeMode::sortAndLimitAlteredCells() { bool passing = false; int passes = 0; std::sort(mAlteredCells.begin(), mAlteredCells.end()); mAlteredCells.erase(std::unique(mAlteredCells.begin(), mAlteredCells.end()), mAlteredCells.end()); while (!passing) // Multiple passes are needed when steepness problems arise for both x and y axis simultaneously { passing = true; for(CSMWorld::CellCoordinates cellCoordinates: mAlteredCells) { limitAlteredHeights(cellCoordinates); } std::reverse(mAlteredCells.begin(), mAlteredCells.end()); //Instead of alphabetical order, this should be fixed to sort cells by cell coordinates for(CSMWorld::CellCoordinates cellCoordinates: mAlteredCells) { if (!limitAlteredHeights(cellCoordinates, true)) passing = false; } ++passes; if (passes > 2) { Log(Debug::Warning) << "Warning: User edit exceeds accepted slope steepness. Automatic limiting has failed, edit has been discarded."; clearTransientEdits(); return; } } } void CSVRender::TerrainShapeMode::clearTransientEdits() { mTotalDiffY = 0; mIsEditing = false; mAlteredCells.clear(); if (CSVRender::PagedWorldspaceWidget *paged = dynamic_cast (&getWorldspaceWidget())) paged->resetAllAlteredHeights(); mTerrainShapeSelection->update(); } void CSVRender::TerrainShapeMode::applyTerrainEditChanges() { CSMDoc::Document& document = getWorldspaceWidget().getDocument(); CSMWorld::IdTable& landTable = dynamic_cast ( *document.getData().getTableModel (CSMWorld::UniversalId::Type_Land)); CSMWorld::IdTable& ltexTable = dynamic_cast ( *document.getData().getTableModel (CSMWorld::UniversalId::Type_LandTextures)); int landshapeColumn = landTable.findColumnIndex(CSMWorld::Columns::ColumnId_LandHeightsIndex); int landnormalsColumn = landTable.findColumnIndex(CSMWorld::Columns::ColumnId_LandNormalsIndex); QUndoStack& undoStack = document.getUndoStack(); sortAndLimitAlteredCells(); undoStack.beginMacro ("Edit shape and normal records"); // One command at the beginning of the macro for redrawing the terrain-selection grid when undoing the changes. undoStack.push(new DrawTerrainSelectionCommand(&getWorldspaceWidget())); for(CSMWorld::CellCoordinates cellCoordinates: mAlteredCells) { std::string cellId = CSMWorld::CellCoordinates::generateId(cellCoordinates.getX(), cellCoordinates.getY()); undoStack.push (new CSMWorld::TouchLandCommand(landTable, ltexTable, cellId)); const CSMWorld::LandHeightsColumn::DataType landShapePointer = landTable.data(landTable.getModelIndex(cellId, landshapeColumn)).value(); CSMWorld::LandHeightsColumn::DataType landShapeNew(landShapePointer); CSVRender::PagedWorldspaceWidget *paged = dynamic_cast (&getWorldspaceWidget()); // Generate land height record for(int i = 0; i < ESM::Land::LAND_SIZE; ++i) { for(int j = 0; j < ESM::Land::LAND_SIZE; ++j) { if (paged && paged->getCellAlteredHeight(cellCoordinates, i, j)) landShapeNew[j * ESM::Land::LAND_SIZE + i] = landShapePointer[j * ESM::Land::LAND_SIZE + i] + *paged->getCellAlteredHeight(cellCoordinates, i, j); else landShapeNew[j * ESM::Land::LAND_SIZE + i] = 0; } } pushEditToCommand(landShapeNew, document, landTable, cellId); } for(CSMWorld::CellCoordinates cellCoordinates: mAlteredCells) { std::string cellId = CSMWorld::CellCoordinates::generateId(cellCoordinates.getX(), cellCoordinates.getY()); const CSMWorld::LandHeightsColumn::DataType landShapePointer = landTable.data(landTable.getModelIndex(cellId, landshapeColumn)).value(); const CSMWorld::LandHeightsColumn::DataType landRightShapePointer = landTable.data(landTable.getModelIndex(CSMWorld::CellCoordinates::generateId(cellCoordinates.getX() + 1, cellCoordinates.getY()), landshapeColumn)).value(); const CSMWorld::LandHeightsColumn::DataType landDownShapePointer = landTable.data(landTable.getModelIndex(CSMWorld::CellCoordinates::generateId(cellCoordinates.getX(), cellCoordinates.getY() + 1), landshapeColumn)).value(); const CSMWorld::LandNormalsColumn::DataType landNormalsPointer = landTable.data(landTable.getModelIndex(cellId, landnormalsColumn)).value(); CSMWorld::LandNormalsColumn::DataType landNormalsNew(landNormalsPointer); // Generate land normals record for(int i = 0; i < ESM::Land::LAND_SIZE; ++i) { for(int j = 0; j < ESM::Land::LAND_SIZE; ++j) { osg::Vec3f v1(128, 0, 0); osg::Vec3f v2(0, 128, 0); if (i < ESM::Land::LAND_SIZE - 1) v1.z() = landShapePointer[j * ESM::Land::LAND_SIZE + i + 1] - landShapePointer[j * ESM::Land::LAND_SIZE + i]; else { std::string shiftedCellId = CSMWorld::CellCoordinates::generateId(cellCoordinates.getX() + 1, cellCoordinates.getY()); if (isLandLoaded(shiftedCellId)) v1.z() = landRightShapePointer[j * ESM::Land::LAND_SIZE + 1] - landShapePointer[j * ESM::Land::LAND_SIZE + i]; } if (j < ESM::Land::LAND_SIZE - 1) v2.z() = landShapePointer[(j + 1) * ESM::Land::LAND_SIZE + i] - landShapePointer[j * ESM::Land::LAND_SIZE + i]; else { std::string shiftedCellId = CSMWorld::CellCoordinates::generateId(cellCoordinates.getX(), cellCoordinates.getY() + 1); if (isLandLoaded(shiftedCellId)) v2.z() = landDownShapePointer[ESM::Land::LAND_SIZE + i] - landShapePointer[j * ESM::Land::LAND_SIZE + i]; } osg::Vec3f normal = v1 ^ v2; const float hyp = normal.length() / 127.0f; normal /= hyp; landNormalsNew[(j * ESM::Land::LAND_SIZE + i) * 3 + 0] = normal.x(); landNormalsNew[(j * ESM::Land::LAND_SIZE + i) * 3 + 1] = normal.y(); landNormalsNew[(j * ESM::Land::LAND_SIZE + i) * 3 + 2] = normal.z(); } } pushNormalsEditToCommand(landNormalsNew, document, landTable, cellId); } // One command at the end of the macro for redrawing the terrain-selection grid when redoing the changes. undoStack.push(new DrawTerrainSelectionCommand(&getWorldspaceWidget())); undoStack.endMacro(); clearTransientEdits(); } float CSVRender::TerrainShapeMode::calculateBumpShape(float distance, int radius, float height) { float distancePerRadius = distance / radius; return height - height * (3 * distancePerRadius * distancePerRadius - 2 * distancePerRadius * distancePerRadius * distancePerRadius); } void CSVRender::TerrainShapeMode::editTerrainShapeGrid(const std::pair& vertexCoords, bool dragOperation) { int r = mBrushSize / 2; if (r == 0) r = 1; // Prevent division by zero later, which might happen when mBrushSize == 1 if (CSVRender::PagedWorldspaceWidget *paged = dynamic_cast (&getWorldspaceWidget())) { if (mShapeEditTool == ShapeEditTool_Drag) paged->resetAllAlteredHeights(); } if (mBrushShape == CSVWidget::BrushShape_Point) { std::string cellId = CSMWorld::CellCoordinates::vertexGlobalToCellId(vertexCoords); CSMWorld::CellCoordinates cellCoords = CSMWorld::CellCoordinates::fromId(cellId).first; int x = CSMWorld::CellCoordinates::vertexGlobalToInCellCoords(vertexCoords.first); int y = CSMWorld::CellCoordinates::vertexGlobalToInCellCoords(vertexCoords.second); if (mShapeEditTool == ShapeEditTool_Drag) alterHeight(cellCoords, x, y, mTotalDiffY); if (mShapeEditTool == ShapeEditTool_PaintToRaise || mShapeEditTool == ShapeEditTool_PaintToLower) { alterHeight(cellCoords, x, y, mShapeEditToolStrength); float smoothMultiplier = static_cast(CSMPrefs::get()["3D Scene Editing"]["landedit-post-smoothstrength"].toDouble()); if (CSMPrefs::get()["3D Scene Editing"]["landedit-post-smoothpainting"].isTrue()) smoothHeight(cellCoords, x, y, mShapeEditToolStrength * smoothMultiplier); } if (mShapeEditTool == ShapeEditTool_Smooth) smoothHeight(cellCoords, x, y, mShapeEditToolStrength); if (mShapeEditTool == ShapeEditTool_Flatten) flattenHeight(cellCoords, x, y, mShapeEditToolStrength, mTargetHeight); } if (mBrushShape == CSVWidget::BrushShape_Square) { for(int i = vertexCoords.first - r; i <= vertexCoords.first + r; ++i) { for(int j = vertexCoords.second - r; j <= vertexCoords.second + r; ++j) { std::string cellId = CSMWorld::CellCoordinates::vertexGlobalToCellId(std::make_pair(i, j)); CSMWorld::CellCoordinates cellCoords = CSMWorld::CellCoordinates::fromId(cellId).first; int x = CSMWorld::CellCoordinates::vertexGlobalToInCellCoords(i); int y = CSMWorld::CellCoordinates::vertexGlobalToInCellCoords(j); if (mShapeEditTool == ShapeEditTool_Drag) alterHeight(cellCoords, x, y, mTotalDiffY); if (mShapeEditTool == ShapeEditTool_PaintToRaise || mShapeEditTool == ShapeEditTool_PaintToLower) { alterHeight(cellCoords, x, y, mShapeEditToolStrength); float smoothMultiplier = static_cast(CSMPrefs::get()["3D Scene Editing"]["landedit-post-smoothstrength"].toDouble()); if (CSMPrefs::get()["3D Scene Editing"]["landedit-post-smoothpainting"].isTrue()) smoothHeight(cellCoords, x, y, mShapeEditToolStrength * smoothMultiplier); } if (mShapeEditTool == ShapeEditTool_Smooth) smoothHeight(cellCoords, x, y, mShapeEditToolStrength); if (mShapeEditTool == ShapeEditTool_Flatten) flattenHeight(cellCoords, x, y, mShapeEditToolStrength, mTargetHeight); } } } if (mBrushShape == CSVWidget::BrushShape_Circle) { for(int i = vertexCoords.first - r; i <= vertexCoords.first + r; ++i) { for(int j = vertexCoords.second - r; j <= vertexCoords.second + r; ++j) { std::string cellId = CSMWorld::CellCoordinates::vertexGlobalToCellId(std::make_pair(i, j)); CSMWorld::CellCoordinates cellCoords = CSMWorld::CellCoordinates::fromId(cellId).first; int x = CSMWorld::CellCoordinates::vertexGlobalToInCellCoords(i); int y = CSMWorld::CellCoordinates::vertexGlobalToInCellCoords(j); int distanceX = abs(i - vertexCoords.first); int distanceY = abs(j - vertexCoords.second); float distance = sqrt(pow(distanceX, 2)+pow(distanceY, 2)); float smoothedByDistance = 0.0f; if (mShapeEditTool == ShapeEditTool_Drag) smoothedByDistance = calculateBumpShape(distance, r, mTotalDiffY); if (mShapeEditTool == ShapeEditTool_PaintToRaise || mShapeEditTool == ShapeEditTool_PaintToLower) smoothedByDistance = calculateBumpShape(distance, r, r + mShapeEditToolStrength); // Using floating-point radius here to prevent selecting too few vertices. if (distance <= mBrushSize / 2.0f) { if (mShapeEditTool == ShapeEditTool_Drag) alterHeight(cellCoords, x, y, smoothedByDistance); if (mShapeEditTool == ShapeEditTool_PaintToRaise || mShapeEditTool == ShapeEditTool_PaintToLower) { alterHeight(cellCoords, x, y, smoothedByDistance); float smoothMultiplier = static_cast(CSMPrefs::get()["3D Scene Editing"]["landedit-post-smoothstrength"].toDouble()); if (CSMPrefs::get()["3D Scene Editing"]["landedit-post-smoothpainting"].isTrue()) smoothHeight(cellCoords, x, y, mShapeEditToolStrength * smoothMultiplier); } if (mShapeEditTool == ShapeEditTool_Smooth) smoothHeight(cellCoords, x, y, mShapeEditToolStrength); if (mShapeEditTool == ShapeEditTool_Flatten) flattenHeight(cellCoords, x, y, mShapeEditToolStrength, mTargetHeight); } } } } if (mBrushShape == CSVWidget::BrushShape_Custom) { if(!mCustomBrushShape.empty()) { for(auto const& value: mCustomBrushShape) { std::string cellId = CSMWorld::CellCoordinates::vertexGlobalToCellId(std::make_pair(vertexCoords.first + value.first, vertexCoords.second + value.second)); CSMWorld::CellCoordinates cellCoords = CSMWorld::CellCoordinates::fromId(cellId).first; int x = CSMWorld::CellCoordinates::vertexGlobalToInCellCoords(vertexCoords.first + value.first); int y = CSMWorld::CellCoordinates::vertexGlobalToInCellCoords(vertexCoords.second + value.second); if (mShapeEditTool == ShapeEditTool_Drag) alterHeight(cellCoords, x, y, mTotalDiffY); if (mShapeEditTool == ShapeEditTool_PaintToRaise || mShapeEditTool == ShapeEditTool_PaintToLower) { alterHeight(cellCoords, x, y, mShapeEditToolStrength); float smoothMultiplier = static_cast(CSMPrefs::get()["3D Scene Editing"]["landedit-post-smoothstrength"].toDouble()); if (CSMPrefs::get()["3D Scene Editing"]["landedit-post-smoothpainting"].isTrue()) smoothHeight(cellCoords, x, y, mShapeEditToolStrength * smoothMultiplier); } if (mShapeEditTool == ShapeEditTool_Smooth) smoothHeight(cellCoords, x, y, mShapeEditToolStrength); if (mShapeEditTool == ShapeEditTool_Flatten) flattenHeight(cellCoords, x, y, mShapeEditToolStrength, mTargetHeight); } } } mTerrainShapeSelection->update(); } void CSVRender::TerrainShapeMode::setFlattenToolTargetHeight(const WorldspaceHitResult& hit) { std::pair vertexCoords = CSMWorld::CellCoordinates::toVertexCoords(hit.worldPos); std::string cellId = CSMWorld::CellCoordinates::vertexGlobalToCellId(vertexCoords); int inCellX = CSMWorld::CellCoordinates::vertexGlobalToInCellCoords(vertexCoords.first); int inCellY = CSMWorld::CellCoordinates::vertexGlobalToInCellCoords(vertexCoords.second); CSMDoc::Document& document = getWorldspaceWidget().getDocument(); CSMWorld::IdTable& landTable = dynamic_cast ( *document.getData().getTableModel (CSMWorld::UniversalId::Type_Land)); int landshapeColumn = landTable.findColumnIndex(CSMWorld::Columns::ColumnId_LandHeightsIndex); const CSMWorld::LandHeightsColumn::DataType landShapePointer = landTable.data(landTable.getModelIndex(cellId, landshapeColumn)).value(); mTargetHeight = landShapePointer[inCellY * ESM::Land::LAND_SIZE + inCellX]; } void CSVRender::TerrainShapeMode::alterHeight(const CSMWorld::CellCoordinates& cellCoords, int inCellX, int inCellY, float alteredHeight, bool useTool) { std::string cellId = CSMWorld::CellCoordinates::generateId(cellCoords.getX(), cellCoords.getY()); if (!(allowLandShapeEditing(cellId, useTool) && (useTool || (isLandLoaded(cellId))))) return; CSVRender::PagedWorldspaceWidget *paged = dynamic_cast (&getWorldspaceWidget()); if (!paged) return; std::string cellLeftId = CSMWorld::CellCoordinates::generateId(cellCoords.getX() - 1, cellCoords.getY()); std::string cellRightId = CSMWorld::CellCoordinates::generateId(cellCoords.getX() + 1, cellCoords.getY()); std::string cellUpId = CSMWorld::CellCoordinates::generateId(cellCoords.getX(), cellCoords.getY() - 1); std::string cellDownId = CSMWorld::CellCoordinates::generateId(cellCoords.getX(), cellCoords.getY() + 1); std::string cellUpLeftId = CSMWorld::CellCoordinates::generateId(cellCoords.getX() - 1, cellCoords.getY() - 1); std::string cellUpRightId = CSMWorld::CellCoordinates::generateId(cellCoords.getX() + 1, cellCoords.getY() - 1); std::string cellDownLeftId = CSMWorld::CellCoordinates::generateId(cellCoords.getX() - 1, cellCoords.getY() + 1); std::string cellDownRightId = CSMWorld::CellCoordinates::generateId(cellCoords.getX() + 1, cellCoords.getY() + 1); if (useTool) { mAlteredCells.emplace_back(cellCoords); if (mShapeEditTool == ShapeEditTool_Drag) { // Get distance from modified land, alter land change based on zoom osg::Vec3d eye, center, up; paged->getCamera()->getViewMatrixAsLookAt(eye, center, up); osg::Vec3d distance = eye - mEditingPos; alteredHeight = alteredHeight * (distance.length() / 500); } if (mShapeEditTool == ShapeEditTool_PaintToRaise) alteredHeight = *paged->getCellAlteredHeight(cellCoords, inCellX, inCellY) + alteredHeight; if (mShapeEditTool == ShapeEditTool_PaintToLower) alteredHeight = *paged->getCellAlteredHeight(cellCoords, inCellX, inCellY) - alteredHeight; if (mShapeEditTool == ShapeEditTool_Smooth) alteredHeight = *paged->getCellAlteredHeight(cellCoords, inCellX, inCellY) + alteredHeight; } if (inCellX != 0 && inCellY != 0 && inCellX != ESM::Land::LAND_SIZE - 1 && inCellY != ESM::Land::LAND_SIZE - 1) paged->setCellAlteredHeight(cellCoords, inCellX, inCellY, alteredHeight); // Change values of cornering cells if ((inCellX == 0 && inCellY == 0) && (useTool || isLandLoaded(cellUpLeftId))) { if(allowLandShapeEditing(cellUpLeftId, useTool) && allowLandShapeEditing(cellLeftId, useTool) && allowLandShapeEditing(cellUpId, useTool)) { CSMWorld::CellCoordinates cornerCellCoords = cellCoords.move(-1, -1); if (useTool && std::find(mAlteredCells.begin(), mAlteredCells.end(), cornerCellCoords) == mAlteredCells.end()) mAlteredCells.emplace_back(cornerCellCoords); paged->setCellAlteredHeight(cornerCellCoords, ESM::Land::LAND_SIZE - 1, ESM::Land::LAND_SIZE - 1, alteredHeight); } else return; } else if ((inCellX == 0 && inCellY == ESM::Land::LAND_SIZE - 1) && (useTool || isLandLoaded(cellDownLeftId))) { if (allowLandShapeEditing(cellDownLeftId, useTool) && allowLandShapeEditing(cellLeftId, useTool) && allowLandShapeEditing(cellDownId, useTool)) { CSMWorld::CellCoordinates cornerCellCoords = cellCoords.move(-1, 1); if (useTool && std::find(mAlteredCells.begin(), mAlteredCells.end(), cornerCellCoords) == mAlteredCells.end()) mAlteredCells.emplace_back(cornerCellCoords); paged->setCellAlteredHeight(cornerCellCoords, ESM::Land::LAND_SIZE - 1, 0, alteredHeight); } else return; } else if ((inCellX == ESM::Land::LAND_SIZE - 1 && inCellY == 0) && (useTool || isLandLoaded(cellUpRightId))) { if (allowLandShapeEditing(cellUpRightId, useTool) && allowLandShapeEditing(cellRightId, useTool) && allowLandShapeEditing(cellUpId, useTool)) { CSMWorld::CellCoordinates cornerCellCoords = cellCoords.move(1, -1); if (useTool && std::find(mAlteredCells.begin(), mAlteredCells.end(), cornerCellCoords) == mAlteredCells.end()) mAlteredCells.emplace_back(cornerCellCoords); paged->setCellAlteredHeight(cornerCellCoords, 0, ESM::Land::LAND_SIZE - 1, alteredHeight); } else return; } else if ((inCellX == ESM::Land::LAND_SIZE - 1 && inCellY == ESM::Land::LAND_SIZE - 1) && (useTool || isLandLoaded(cellDownRightId))) { if(allowLandShapeEditing(cellDownRightId, useTool) && allowLandShapeEditing(cellRightId, useTool) && allowLandShapeEditing(cellDownId, useTool)) { CSMWorld::CellCoordinates cornerCellCoords = cellCoords.move(1, 1); if (useTool && std::find(mAlteredCells.begin(), mAlteredCells.end(), cornerCellCoords) == mAlteredCells.end()) mAlteredCells.emplace_back(cornerCellCoords); paged->setCellAlteredHeight(cornerCellCoords, 0, 0, alteredHeight); } else return; } // Change values of edging cells if ((inCellX == 0) && (useTool || isLandLoaded(cellLeftId))) { if(allowLandShapeEditing(cellLeftId, useTool)) { CSMWorld::CellCoordinates edgeCellCoords = cellCoords.move(-1, 0); if (useTool && std::find(mAlteredCells.begin(), mAlteredCells.end(), edgeCellCoords) == mAlteredCells.end()) mAlteredCells.emplace_back(edgeCellCoords); paged->setCellAlteredHeight(cellCoords, inCellX, inCellY, alteredHeight); paged->setCellAlteredHeight(edgeCellCoords, ESM::Land::LAND_SIZE - 1, inCellY, alteredHeight); } } if ((inCellY == 0) && (useTool || isLandLoaded(cellUpId))) { if(allowLandShapeEditing(cellUpId, useTool)) { CSMWorld::CellCoordinates edgeCellCoords = cellCoords.move(0, -1); if (useTool && std::find(mAlteredCells.begin(), mAlteredCells.end(), edgeCellCoords) == mAlteredCells.end()) mAlteredCells.emplace_back(edgeCellCoords); paged->setCellAlteredHeight(cellCoords, inCellX, inCellY, alteredHeight); paged->setCellAlteredHeight(edgeCellCoords, inCellX, ESM::Land::LAND_SIZE - 1, alteredHeight); } } if ((inCellX == ESM::Land::LAND_SIZE - 1) && (useTool || isLandLoaded(cellRightId))) { if(allowLandShapeEditing(cellRightId, useTool)) { CSMWorld::CellCoordinates edgeCellCoords = cellCoords.move(1, 0); if (useTool && std::find(mAlteredCells.begin(), mAlteredCells.end(), edgeCellCoords) == mAlteredCells.end()) mAlteredCells.emplace_back(edgeCellCoords); paged->setCellAlteredHeight(cellCoords, inCellX, inCellY, alteredHeight); paged->setCellAlteredHeight(edgeCellCoords, 0, inCellY, alteredHeight); } } if ((inCellY == ESM::Land::LAND_SIZE - 1) && (useTool || isLandLoaded(cellDownId))) { if(allowLandShapeEditing(cellDownId, useTool)) { CSMWorld::CellCoordinates edgeCellCoords = cellCoords.move(0, 1); if (useTool && std::find(mAlteredCells.begin(), mAlteredCells.end(), edgeCellCoords) == mAlteredCells.end()) mAlteredCells.emplace_back(edgeCellCoords); paged->setCellAlteredHeight(cellCoords, inCellX, inCellY, alteredHeight); paged->setCellAlteredHeight(edgeCellCoords, inCellX, 0, alteredHeight); } } } void CSVRender::TerrainShapeMode::smoothHeight(const CSMWorld::CellCoordinates& cellCoords, int inCellX, int inCellY, int toolStrength) { if (CSVRender::PagedWorldspaceWidget *paged = dynamic_cast (&getWorldspaceWidget())) { CSMDoc::Document& document = getWorldspaceWidget().getDocument(); CSMWorld::IdTable& landTable = dynamic_cast ( *document.getData().getTableModel (CSMWorld::UniversalId::Type_Land)); int landshapeColumn = landTable.findColumnIndex(CSMWorld::Columns::ColumnId_LandHeightsIndex); std::string cellId = CSMWorld::CellCoordinates::generateId(cellCoords.getX(), cellCoords.getY()); const CSMWorld::LandHeightsColumn::DataType landShapePointer = landTable.data(landTable.getModelIndex(cellId, landshapeColumn)).value(); // ### Variable naming key ### // Variables here hold either the real value, or the altered value of current edit. // this = this Cell // left = x - 1, up = y - 1, right = x + 1, down = y + 1 // Altered = transient edit (in current edited) float thisAlteredHeight = 0.0f; if (paged->getCellAlteredHeight(cellCoords, inCellX, inCellY) != nullptr) thisAlteredHeight = *paged->getCellAlteredHeight(cellCoords, inCellX, inCellY); float thisHeight = landShapePointer[inCellY * ESM::Land::LAND_SIZE + inCellX]; float leftHeight = 0.0f; float leftAlteredHeight = 0.0f; float upAlteredHeight = 0.0f; float rightHeight = 0.0f; float rightAlteredHeight = 0.0f; float downHeight = 0.0f; float downAlteredHeight = 0.0f; float upHeight = 0.0f; if(allowLandShapeEditing(cellId)) { //Get key values for calculating average, handle cell edges, check for null pointers if (inCellX == 0) { cellId = CSMWorld::CellCoordinates::generateId(cellCoords.getX() - 1, cellCoords.getY()); const CSMWorld::LandHeightsColumn::DataType landLeftShapePointer = landTable.data(landTable.getModelIndex(cellId, landshapeColumn)).value(); leftHeight = landLeftShapePointer[inCellY * ESM::Land::LAND_SIZE + (ESM::Land::LAND_SIZE - 2)]; if (paged->getCellAlteredHeight(cellCoords.move(-1, 0), inCellX, ESM::Land::LAND_SIZE - 2)) leftAlteredHeight = *paged->getCellAlteredHeight(cellCoords.move(-1, 0), ESM::Land::LAND_SIZE - 2, inCellY); } if (inCellY == 0) { cellId = CSMWorld::CellCoordinates::generateId(cellCoords.getX(), cellCoords.getY() - 1); const CSMWorld::LandHeightsColumn::DataType landUpShapePointer = landTable.data(landTable.getModelIndex(cellId, landshapeColumn)).value(); upHeight = landUpShapePointer[(ESM::Land::LAND_SIZE - 2) * ESM::Land::LAND_SIZE + inCellX]; if (paged->getCellAlteredHeight(cellCoords.move(0, -1), inCellX, ESM::Land::LAND_SIZE - 2)) upAlteredHeight = *paged->getCellAlteredHeight(cellCoords.move(0, -1), inCellX, ESM::Land::LAND_SIZE - 2); } if (inCellX > 0) { leftHeight = landShapePointer[inCellY * ESM::Land::LAND_SIZE + inCellX - 1]; leftAlteredHeight = *paged->getCellAlteredHeight(cellCoords, inCellX - 1, inCellY); } if (inCellY > 0) { upHeight = landShapePointer[(inCellY - 1) * ESM::Land::LAND_SIZE + inCellX]; upAlteredHeight = *paged->getCellAlteredHeight(cellCoords, inCellX, inCellY - 1); } if (inCellX == ESM::Land::LAND_SIZE - 1) { cellId = CSMWorld::CellCoordinates::generateId(cellCoords.getX() + 1, cellCoords.getY()); const CSMWorld::LandHeightsColumn::DataType landRightShapePointer = landTable.data(landTable.getModelIndex(cellId, landshapeColumn)).value(); rightHeight = landRightShapePointer[inCellY * ESM::Land::LAND_SIZE + 1]; if (paged->getCellAlteredHeight(cellCoords.move(1, 0), 1, inCellY)) { rightAlteredHeight = *paged->getCellAlteredHeight(cellCoords.move(1, 0), 1, inCellY); } } if (inCellY == ESM::Land::LAND_SIZE - 1) { cellId = CSMWorld::CellCoordinates::generateId(cellCoords.getX(), cellCoords.getY() + 1); const CSMWorld::LandHeightsColumn::DataType landDownShapePointer = landTable.data(landTable.getModelIndex(cellId, landshapeColumn)).value(); downHeight = landDownShapePointer[1 * ESM::Land::LAND_SIZE + inCellX]; if (paged->getCellAlteredHeight(cellCoords.move(0, 1), inCellX, 1)) { downAlteredHeight = *paged->getCellAlteredHeight(cellCoords.move(0, 1), inCellX, 1); } } if (inCellX < ESM::Land::LAND_SIZE - 1) { rightHeight = landShapePointer[inCellY * ESM::Land::LAND_SIZE + inCellX + 1]; if(paged->getCellAlteredHeight(cellCoords, inCellX + 1, inCellY)) rightAlteredHeight = *paged->getCellAlteredHeight(cellCoords, inCellX + 1, inCellY); } if (inCellY < ESM::Land::LAND_SIZE - 1) { downHeight = landShapePointer[(inCellY + 1) * ESM::Land::LAND_SIZE + inCellX]; if(paged->getCellAlteredHeight(cellCoords, inCellX, inCellY + 1)) downAlteredHeight = *paged->getCellAlteredHeight(cellCoords, inCellX, inCellY + 1); } float averageHeight = (upHeight + downHeight + rightHeight + leftHeight + upAlteredHeight + downAlteredHeight + rightAlteredHeight + leftAlteredHeight) / 4; if ((thisHeight + thisAlteredHeight) != averageHeight) mAlteredCells.emplace_back(cellCoords); if (toolStrength > abs(thisHeight + thisAlteredHeight - averageHeight)) toolStrength = abs(thisHeight + thisAlteredHeight - averageHeight); if (thisHeight + thisAlteredHeight > averageHeight) alterHeight(cellCoords, inCellX, inCellY, - toolStrength); if (thisHeight + thisAlteredHeight < averageHeight) alterHeight(cellCoords, inCellX, inCellY, + toolStrength); } } } void CSVRender::TerrainShapeMode::flattenHeight(const CSMWorld::CellCoordinates& cellCoords, int inCellX, int inCellY, int toolStrength, int targetHeight) { CSMDoc::Document& document = getWorldspaceWidget().getDocument(); CSMWorld::IdTable& landTable = dynamic_cast ( *document.getData().getTableModel (CSMWorld::UniversalId::Type_Land)); int landshapeColumn = landTable.findColumnIndex(CSMWorld::Columns::ColumnId_LandHeightsIndex); float thisHeight = 0.0f; float thisAlteredHeight = 0.0f; std::string cellId = CSMWorld::CellCoordinates::generateId(cellCoords.getX(), cellCoords.getY()); if (CSVRender::PagedWorldspaceWidget *paged = dynamic_cast (&getWorldspaceWidget())) { if (!noCell(cellId) && !noLand(cellId)) { const CSMWorld::LandHeightsColumn::DataType landShapePointer = landTable.data(landTable.getModelIndex(cellId, landshapeColumn)).value(); if(paged->getCellAlteredHeight(cellCoords, inCellX, inCellY)) thisAlteredHeight = *paged->getCellAlteredHeight(cellCoords, inCellX, inCellY); thisHeight = landShapePointer[inCellY * ESM::Land::LAND_SIZE + inCellX]; } } if (toolStrength > abs(thisHeight - targetHeight) && toolStrength > 8.0f) toolStrength = abs(thisHeight - targetHeight); //Cut down excessive changes if (thisHeight + thisAlteredHeight > targetHeight) alterHeight(cellCoords, inCellX, inCellY, thisAlteredHeight - toolStrength); if (thisHeight + thisAlteredHeight < targetHeight) alterHeight(cellCoords, inCellX, inCellY, thisAlteredHeight + toolStrength); } void CSVRender::TerrainShapeMode::updateKeyHeightValues(const CSMWorld::CellCoordinates& cellCoords, int inCellX, int inCellY, float* thisHeight, float* thisAlteredHeight, float* leftHeight, float* leftAlteredHeight, float* upHeight, float* upAlteredHeight, float* rightHeight, float* rightAlteredHeight, float* downHeight, float* downAlteredHeight) { CSMDoc::Document& document = getWorldspaceWidget().getDocument(); CSMWorld::IdTable& landTable = dynamic_cast ( *document.getData().getTableModel (CSMWorld::UniversalId::Type_Land)); int landshapeColumn = landTable.findColumnIndex(CSMWorld::Columns::ColumnId_LandHeightsIndex); std::string cellId = CSMWorld::CellCoordinates::generateId(cellCoords.getX(), cellCoords.getY()); std::string cellLeftId = CSMWorld::CellCoordinates::generateId(cellCoords.getX() - 1, cellCoords.getY()); std::string cellUpId = CSMWorld::CellCoordinates::generateId(cellCoords.getX(), cellCoords.getY() - 1); std::string cellRightId = CSMWorld::CellCoordinates::generateId(cellCoords.getX() + 1, cellCoords.getY()); std::string cellDownId = CSMWorld::CellCoordinates::generateId(cellCoords.getX(), cellCoords.getY() + 1); *thisHeight = 0.0f; // real + altered height *thisAlteredHeight = 0.0f; // only altered height *leftHeight = 0.0f; *leftAlteredHeight = 0.0f; *upHeight = 0.0f; *upAlteredHeight = 0.0f; *rightHeight = 0.0f; *rightAlteredHeight = 0.0f; *downHeight = 0.0f; *downAlteredHeight = 0.0f; if (CSVRender::PagedWorldspaceWidget *paged = dynamic_cast (&getWorldspaceWidget())) { if (!noCell(cellId) && !noLand(cellId)) { const CSMWorld::LandHeightsColumn::DataType landShapePointer = landTable.data(landTable.getModelIndex(cellId, landshapeColumn)).value(); if(paged->getCellAlteredHeight(cellCoords, inCellX, inCellY)) *thisAlteredHeight = *paged->getCellAlteredHeight(cellCoords, inCellX, inCellY); *thisHeight = landShapePointer[inCellY * ESM::Land::LAND_SIZE + inCellX] + *thisAlteredHeight; // Default to the same value as thisHeight, which happens in the case of cell edge where next cell/land is not found, // which is to prevent unnecessary action at limitHeightChange(). *leftHeight = *thisHeight; *upHeight = *thisHeight; *rightHeight = *thisHeight; *downHeight = *thisHeight; //If at edge, get values from neighboring cell if (inCellX == 0) { if(isLandLoaded(cellLeftId)) { const CSMWorld::LandHeightsColumn::DataType landLeftShapePointer = landTable.data(landTable.getModelIndex(cellLeftId, landshapeColumn)).value(); *leftHeight = landLeftShapePointer[inCellY * ESM::Land::LAND_SIZE + (ESM::Land::LAND_SIZE - 2)]; if (paged->getCellAlteredHeight(cellCoords.move(-1, 0), ESM::Land::LAND_SIZE - 2, inCellY)) { *leftAlteredHeight = *paged->getCellAlteredHeight(cellCoords.move(-1, 0), ESM::Land::LAND_SIZE - 2, inCellY); *leftHeight += *leftAlteredHeight; } } } if (inCellY == 0) { if(isLandLoaded(cellUpId)) { const CSMWorld::LandHeightsColumn::DataType landUpShapePointer = landTable.data(landTable.getModelIndex(cellUpId, landshapeColumn)).value(); *upHeight = landUpShapePointer[(ESM::Land::LAND_SIZE - 2) * ESM::Land::LAND_SIZE + inCellX]; if (paged->getCellAlteredHeight(cellCoords.move(0,-1), inCellX, ESM::Land::LAND_SIZE - 2)) { *upAlteredHeight = *paged->getCellAlteredHeight(cellCoords.move(0, -1), inCellX, ESM::Land::LAND_SIZE - 2); *upHeight += *upAlteredHeight; } } } if (inCellX == ESM::Land::LAND_SIZE - 1) { if(isLandLoaded(cellRightId)) { const CSMWorld::LandHeightsColumn::DataType landRightShapePointer = landTable.data(landTable.getModelIndex(cellRightId, landshapeColumn)).value(); *rightHeight = landRightShapePointer[inCellY * ESM::Land::LAND_SIZE + 1]; if (paged->getCellAlteredHeight(cellCoords.move(1, 0), 1, inCellY)) { *rightAlteredHeight = *paged->getCellAlteredHeight(cellCoords.move(1, 0), 1, inCellY); *rightHeight += *rightAlteredHeight; } } } if (inCellY == ESM::Land::LAND_SIZE - 1) { if(isLandLoaded(cellDownId)) { const CSMWorld::LandHeightsColumn::DataType landDownShapePointer = landTable.data(landTable.getModelIndex(cellDownId, landshapeColumn)).value(); *downHeight = landDownShapePointer[ESM::Land::LAND_SIZE + inCellX]; if (paged->getCellAlteredHeight(cellCoords.move(0, 1), inCellX, 1)) { *downAlteredHeight = *paged->getCellAlteredHeight(cellCoords.move(0, 1), inCellX, 1); *downHeight += *downAlteredHeight; } } } //If not at edge, get values from the same cell if (inCellX != 0) { *leftHeight = landShapePointer[inCellY * ESM::Land::LAND_SIZE + inCellX - 1]; if (paged->getCellAlteredHeight(cellCoords, inCellX - 1, inCellY)) *leftAlteredHeight = *paged->getCellAlteredHeight(cellCoords, inCellX - 1, inCellY); *leftHeight += *leftAlteredHeight; } if (inCellY != 0) { *upHeight = landShapePointer[(inCellY - 1) * ESM::Land::LAND_SIZE + inCellX]; if (paged->getCellAlteredHeight(cellCoords, inCellX, inCellY - 1)) *upAlteredHeight = *paged->getCellAlteredHeight(cellCoords, inCellX, inCellY - 1); *upHeight += *upAlteredHeight; } if (inCellX != ESM::Land::LAND_SIZE - 1) { *rightHeight = landShapePointer[inCellY * ESM::Land::LAND_SIZE + inCellX + 1]; if (paged->getCellAlteredHeight(cellCoords, inCellX + 1, inCellY)) *rightAlteredHeight = *paged->getCellAlteredHeight(cellCoords, inCellX + 1, inCellY); *rightHeight += *rightAlteredHeight; } if (inCellY != ESM::Land::LAND_SIZE - 1) { *downHeight = landShapePointer[(inCellY + 1) * ESM::Land::LAND_SIZE + inCellX]; if (paged->getCellAlteredHeight(cellCoords, inCellX, inCellY + 1)) *downAlteredHeight = *paged->getCellAlteredHeight(cellCoords, inCellX, inCellY + 1); *downHeight += *downAlteredHeight; } } } } void CSVRender::TerrainShapeMode::compareAndLimit(const CSMWorld::CellCoordinates& cellCoords, int inCellX, int inCellY, float* limitedAlteredHeightXAxis, float* limitedAlteredHeightYAxis, bool* steepnessIsWithinLimits) { if (limitedAlteredHeightXAxis) { if (limitedAlteredHeightYAxis) { if(std::abs(*limitedAlteredHeightXAxis) >= std::abs(*limitedAlteredHeightYAxis)) { alterHeight(cellCoords, inCellX, inCellY, *limitedAlteredHeightXAxis, false); *steepnessIsWithinLimits = false; } else { alterHeight(cellCoords, inCellX, inCellY, *limitedAlteredHeightYAxis, false); *steepnessIsWithinLimits = false; } } else { alterHeight(cellCoords, inCellX, inCellY, *limitedAlteredHeightXAxis, false); *steepnessIsWithinLimits = false; } } else if (limitedAlteredHeightYAxis) { alterHeight(cellCoords, inCellX, inCellY, *limitedAlteredHeightYAxis, false); *steepnessIsWithinLimits = false; } } bool CSVRender::TerrainShapeMode::limitAlteredHeights(const CSMWorld::CellCoordinates& cellCoords, bool reverseMode) { CSMDoc::Document& document = getWorldspaceWidget().getDocument(); CSMWorld::IdTable& landTable = dynamic_cast (*document.getData().getTableModel(CSMWorld::UniversalId::Type_Land)); int landshapeColumn = landTable.findColumnIndex(CSMWorld::Columns::ColumnId_LandHeightsIndex); std::string cellId = CSMWorld::CellCoordinates::generateId(cellCoords.getX(), cellCoords.getY()); int limitHeightChange = 1016.0f; // Limited by save format bool steepnessIsWithinLimits = true; if (isLandLoaded(cellId)) { const CSMWorld::LandHeightsColumn::DataType landShapePointer = landTable.data(landTable.getModelIndex(cellId, landshapeColumn)).value(); float thisHeight = 0.0f; float thisAlteredHeight = 0.0f; float leftHeight = 0.0f; float leftAlteredHeight = 0.0f; float upHeight = 0.0f; float upAlteredHeight = 0.0f; float rightHeight = 0.0f; float rightAlteredHeight = 0.0f; float downHeight = 0.0f; float downAlteredHeight = 0.0f; if (!reverseMode) { for(int inCellY = 0; inCellY < ESM::Land::LAND_SIZE; ++inCellY) { for(int inCellX = 0; inCellX < ESM::Land::LAND_SIZE; ++inCellX) { std::unique_ptr limitedAlteredHeightXAxis(nullptr); std::unique_ptr limitedAlteredHeightYAxis(nullptr); updateKeyHeightValues(cellCoords, inCellX, inCellY, &thisHeight, &thisAlteredHeight, &leftHeight, &leftAlteredHeight, &upHeight, &upAlteredHeight, &rightHeight, &rightAlteredHeight, &downHeight, &downAlteredHeight); // Check for height limits on x-axis if (leftHeight - thisHeight > limitHeightChange) limitedAlteredHeightXAxis.reset(new float(leftHeight - limitHeightChange - (thisHeight - thisAlteredHeight))); else if (leftHeight - thisHeight < -limitHeightChange) limitedAlteredHeightXAxis.reset(new float(leftHeight + limitHeightChange - (thisHeight - thisAlteredHeight))); // Check for height limits on y-axis if (upHeight - thisHeight > limitHeightChange) limitedAlteredHeightYAxis.reset(new float(upHeight - limitHeightChange - (thisHeight - thisAlteredHeight))); else if (upHeight - thisHeight < -limitHeightChange) limitedAlteredHeightYAxis.reset(new float(upHeight + limitHeightChange - (thisHeight - thisAlteredHeight))); // Limit altered height value based on x or y, whichever is the smallest compareAndLimit(cellCoords, inCellX, inCellY, limitedAlteredHeightXAxis.get(), limitedAlteredHeightYAxis.get(), &steepnessIsWithinLimits); } } } if (reverseMode) { for(int inCellY = ESM::Land::LAND_SIZE - 1; inCellY >= 0; --inCellY) { for(int inCellX = ESM::Land::LAND_SIZE - 1; inCellX >= 0; --inCellX) { std::unique_ptr limitedAlteredHeightXAxis(nullptr); std::unique_ptr limitedAlteredHeightYAxis(nullptr); updateKeyHeightValues(cellCoords, inCellX, inCellY, &thisHeight, &thisAlteredHeight, &leftHeight, &leftAlteredHeight, &upHeight, &upAlteredHeight, &rightHeight, &rightAlteredHeight, &downHeight, &downAlteredHeight); // Check for height limits on x-axis if (rightHeight - thisHeight > limitHeightChange) limitedAlteredHeightXAxis.reset(new float(rightHeight - limitHeightChange - (thisHeight - thisAlteredHeight))); else if (rightHeight - thisHeight < -limitHeightChange) limitedAlteredHeightXAxis.reset(new float(rightHeight + limitHeightChange - (thisHeight - thisAlteredHeight))); // Check for height limits on y-axis if (downHeight - thisHeight > limitHeightChange) limitedAlteredHeightYAxis.reset(new float(downHeight - limitHeightChange - (thisHeight - thisAlteredHeight))); else if (downHeight - thisHeight < -limitHeightChange) limitedAlteredHeightYAxis.reset(new float(downHeight + limitHeightChange - (thisHeight - thisAlteredHeight))); // Limit altered height value based on x or y, whichever is the smallest compareAndLimit(cellCoords, inCellX, inCellY, limitedAlteredHeightXAxis.get(), limitedAlteredHeightYAxis.get(), &steepnessIsWithinLimits); } } } } return steepnessIsWithinLimits; } bool CSVRender::TerrainShapeMode::isInCellSelection(int globalSelectionX, int globalSelectionY) { if (CSVRender::PagedWorldspaceWidget *paged = dynamic_cast (&getWorldspaceWidget())) { std::pair vertexCoords = std::make_pair(globalSelectionX, globalSelectionY); std::string cellId = CSMWorld::CellCoordinates::vertexGlobalToCellId(vertexCoords); return paged->getCellSelection().has(CSMWorld::CellCoordinates::fromId(cellId).first) && isLandLoaded(cellId); } return false; } void CSVRender::TerrainShapeMode::handleSelection(int globalSelectionX, int globalSelectionY, std::vector>* selections) { if (isInCellSelection(globalSelectionX, globalSelectionY)) selections->emplace_back(globalSelectionX, globalSelectionY); else { int moduloX = globalSelectionX % (ESM::Land::LAND_SIZE - 1); int moduloY = globalSelectionY % (ESM::Land::LAND_SIZE - 1); bool xIsAtCellBorder = moduloX == 0; bool yIsAtCellBorder = moduloY == 0; if (!xIsAtCellBorder && !yIsAtCellBorder) return; int selectionX = globalSelectionX; int selectionY = globalSelectionY; /* The northern and eastern edges don't belong to the current cell. If the corresponding adjacent cell is not loaded, some special handling is necessary to select border vertices. */ if (xIsAtCellBorder && yIsAtCellBorder) { /* Handle the NW, NE, and SE corner vertices. NW corner: (+1, -1) offset to reach current cell. NE corner: (-1, -1) offset to reach current cell. SE corner: (-1, +1) offset to reach current cell. */ if (isInCellSelection(globalSelectionX - 1, globalSelectionY - 1) || isInCellSelection(globalSelectionX + 1, globalSelectionY - 1) || isInCellSelection(globalSelectionX - 1, globalSelectionY + 1)) { selections->emplace_back(globalSelectionX, globalSelectionY); } } else if (xIsAtCellBorder) { selectionX--; } else if (yIsAtCellBorder) { selectionY--; } if (isInCellSelection(selectionX, selectionY)) selections->emplace_back(globalSelectionX, globalSelectionY); } } void CSVRender::TerrainShapeMode::selectTerrainShapes(const std::pair& vertexCoords, unsigned char selectMode, bool dragOperation) { int r = mBrushSize / 2; std::vector> selections; if (mBrushShape == CSVWidget::BrushShape_Point) { handleSelection(vertexCoords.first, vertexCoords.second, &selections); } if (mBrushShape == CSVWidget::BrushShape_Square) { for(int i = vertexCoords.first - r; i <= vertexCoords.first + r; ++i) { for(int j = vertexCoords.second - r; j <= vertexCoords.second + r; ++j) { handleSelection(i, j, &selections); } } } if (mBrushShape == CSVWidget::BrushShape_Circle) { for(int i = vertexCoords.first - r; i <= vertexCoords.first + r; ++i) { for(int j = vertexCoords.second - r; j <= vertexCoords.second + r; ++j) { int distanceX = abs(i - vertexCoords.first); int distanceY = abs(j - vertexCoords.second); float distance = sqrt(pow(distanceX, 2)+pow(distanceY, 2)); // Using floating-point radius here to prevent selecting too few vertices. if (distance <= mBrushSize / 2.0f) handleSelection(i, j, &selections); } } } if (mBrushShape == CSVWidget::BrushShape_Custom) { if(!mCustomBrushShape.empty()) { for(auto const& value: mCustomBrushShape) { std::pair localVertexCoords (vertexCoords.first + value.first, vertexCoords.second + value.second); handleSelection(localVertexCoords.first, localVertexCoords.second, &selections); } } } std::string selectAction; if (selectMode == 0) selectAction = CSMPrefs::get()["3D Scene Editing"]["primary-select-action"].toString(); else selectAction = CSMPrefs::get()["3D Scene Editing"]["secondary-select-action"].toString(); if (selectAction == "Select only") mTerrainShapeSelection->onlySelect(selections); else if (selectAction == "Add to selection") mTerrainShapeSelection->addSelect(selections, dragOperation); else if (selectAction == "Remove from selection") mTerrainShapeSelection->removeSelect(selections, dragOperation); else if (selectAction == "Invert selection") mTerrainShapeSelection->toggleSelect(selections, dragOperation); } void CSVRender::TerrainShapeMode::pushEditToCommand(const CSMWorld::LandHeightsColumn::DataType& newLandGrid, CSMDoc::Document& document, CSMWorld::IdTable& landTable, const std::string& cellId) { QVariant changedLand; changedLand.setValue(newLandGrid); QModelIndex index(landTable.getModelIndex (cellId, landTable.findColumnIndex (CSMWorld::Columns::ColumnId_LandHeightsIndex))); QUndoStack& undoStack = document.getUndoStack(); undoStack.push (new CSMWorld::ModifyCommand(landTable, index, changedLand)); } void CSVRender::TerrainShapeMode::pushNormalsEditToCommand(const CSMWorld::LandNormalsColumn::DataType& newLandGrid, CSMDoc::Document& document, CSMWorld::IdTable& landTable, const std::string& cellId) { QVariant changedLand; changedLand.setValue(newLandGrid); QModelIndex index(landTable.getModelIndex (cellId, landTable.findColumnIndex (CSMWorld::Columns::ColumnId_LandNormalsIndex))); QUndoStack& undoStack = document.getUndoStack(); undoStack.push (new CSMWorld::ModifyCommand(landTable, index, changedLand)); } bool CSVRender::TerrainShapeMode::noCell(const std::string& cellId) { CSMDoc::Document& document = getWorldspaceWidget().getDocument(); const CSMWorld::IdCollection& cellCollection = document.getData().getCells(); return cellCollection.searchId (cellId) == -1; } bool CSVRender::TerrainShapeMode::noLand(const std::string& cellId) { CSMDoc::Document& document = getWorldspaceWidget().getDocument(); const CSMWorld::IdCollection& landCollection = document.getData().getLand(); return landCollection.searchId (cellId) == -1; } bool CSVRender::TerrainShapeMode::noLandLoaded(const std::string& cellId) { CSMDoc::Document& document = getWorldspaceWidget().getDocument(); const CSMWorld::IdCollection& landCollection = document.getData().getLand(); return !landCollection.getRecord(cellId).get().isDataLoaded(ESM::Land::DATA_VNML); } bool CSVRender::TerrainShapeMode::isLandLoaded(const std::string& cellId) { if (!noCell(cellId) && !noLand(cellId) && !noLandLoaded(cellId)) return true; return false; } void CSVRender::TerrainShapeMode::createNewLandData(const CSMWorld::CellCoordinates& cellCoords) { CSMDoc::Document& document = getWorldspaceWidget().getDocument(); CSMWorld::IdTable& landTable = dynamic_cast ( *document.getData().getTableModel (CSMWorld::UniversalId::Type_Land)); CSMWorld::IdTable& ltexTable = dynamic_cast ( *document.getData().getTableModel (CSMWorld::UniversalId::Type_LandTextures)); int landshapeColumn = landTable.findColumnIndex(CSMWorld::Columns::ColumnId_LandHeightsIndex); int landnormalsColumn = landTable.findColumnIndex(CSMWorld::Columns::ColumnId_LandNormalsIndex); float defaultHeight = 0.f; int averageDivider = 0; CSMWorld::CellCoordinates cellLeftCoords = cellCoords.move(-1, 0); CSMWorld::CellCoordinates cellRightCoords = cellCoords.move(1, 0); CSMWorld::CellCoordinates cellUpCoords = cellCoords.move(0, -1); CSMWorld::CellCoordinates cellDownCoords = cellCoords.move(0, 1); std::string cellId = CSMWorld::CellCoordinates::generateId(cellCoords.getX(), cellCoords.getY()); std::string cellLeftId = CSMWorld::CellCoordinates::generateId(cellLeftCoords.getX(), cellLeftCoords.getY()); std::string cellRightId = CSMWorld::CellCoordinates::generateId(cellRightCoords.getX(), cellRightCoords.getY()); std::string cellUpId = CSMWorld::CellCoordinates::generateId(cellUpCoords.getX(), cellUpCoords.getY()); std::string cellDownId = CSMWorld::CellCoordinates::generateId(cellDownCoords.getX(), cellDownCoords.getY()); float leftCellSampleHeight = 0.0f; float rightCellSampleHeight = 0.0f; float upCellSampleHeight = 0.0f; float downCellSampleHeight = 0.0f; const CSMWorld::LandHeightsColumn::DataType landShapePointer = landTable.data(landTable.getModelIndex(cellId, landshapeColumn)).value(); const CSMWorld::LandNormalsColumn::DataType landNormalsPointer = landTable.data(landTable.getModelIndex(cellId, landnormalsColumn)).value(); CSMWorld::LandHeightsColumn::DataType landShapeNew(landShapePointer); CSMWorld::LandNormalsColumn::DataType landNormalsNew(landNormalsPointer); if (CSVRender::PagedWorldspaceWidget *paged = dynamic_cast (&getWorldspaceWidget())) { if (isLandLoaded(cellLeftId)) { const CSMWorld::LandHeightsColumn::DataType landLeftShapePointer = landTable.data(landTable.getModelIndex(cellLeftId, landshapeColumn)).value(); ++averageDivider; leftCellSampleHeight = landLeftShapePointer[(ESM::Land::LAND_SIZE / 2) * ESM::Land::LAND_SIZE + ESM::Land::LAND_SIZE - 1]; if(paged->getCellAlteredHeight(cellLeftCoords, ESM::Land::LAND_SIZE - 1, ESM::Land::LAND_SIZE / 2)) leftCellSampleHeight += *paged->getCellAlteredHeight(cellLeftCoords, ESM::Land::LAND_SIZE - 1, ESM::Land::LAND_SIZE / 2); } if (isLandLoaded(cellRightId)) { const CSMWorld::LandHeightsColumn::DataType landRightShapePointer = landTable.data(landTable.getModelIndex(cellRightId, landshapeColumn)).value(); ++averageDivider; rightCellSampleHeight = landRightShapePointer[(ESM::Land::LAND_SIZE / 2) * ESM::Land::LAND_SIZE]; if(paged->getCellAlteredHeight(cellRightCoords, 0, ESM::Land::LAND_SIZE / 2)) rightCellSampleHeight += *paged->getCellAlteredHeight(cellRightCoords, 0, ESM::Land::LAND_SIZE / 2); } if (isLandLoaded(cellUpId)) { const CSMWorld::LandHeightsColumn::DataType landUpShapePointer = landTable.data(landTable.getModelIndex(cellUpId, landshapeColumn)).value(); ++averageDivider; upCellSampleHeight = landUpShapePointer[(ESM::Land::LAND_SIZE - 1) * ESM::Land::LAND_SIZE + (ESM::Land::LAND_SIZE / 2)]; if(paged->getCellAlteredHeight(cellUpCoords, ESM::Land::LAND_SIZE / 2, ESM::Land::LAND_SIZE - 1)) upCellSampleHeight += *paged->getCellAlteredHeight(cellUpCoords, ESM::Land::LAND_SIZE / 2, ESM::Land::LAND_SIZE - 1); } if (isLandLoaded(cellDownId)) { const CSMWorld::LandHeightsColumn::DataType landDownShapePointer = landTable.data(landTable.getModelIndex(cellDownId, landshapeColumn)).value(); ++averageDivider; downCellSampleHeight = landDownShapePointer[ESM::Land::LAND_SIZE / 2]; if(paged->getCellAlteredHeight(cellDownCoords, ESM::Land::LAND_SIZE / 2, 0)) downCellSampleHeight += *paged->getCellAlteredHeight(cellDownCoords, ESM::Land::LAND_SIZE / 2, 0); } } if (averageDivider > 0) defaultHeight = (leftCellSampleHeight + rightCellSampleHeight + upCellSampleHeight + downCellSampleHeight) / averageDivider; for(int i = 0; i < ESM::Land::LAND_SIZE; ++i) { for(int j = 0; j < ESM::Land::LAND_SIZE; ++j) { landShapeNew[j * ESM::Land::LAND_SIZE + i] = defaultHeight; landNormalsNew[(j * ESM::Land::LAND_SIZE + i) * 3 + 0] = 0; landNormalsNew[(j * ESM::Land::LAND_SIZE + i) * 3 + 1] = 0; landNormalsNew[(j * ESM::Land::LAND_SIZE + i) * 3 + 2] = 127; } } QVariant changedShape; changedShape.setValue(landShapeNew); QVariant changedNormals; changedNormals.setValue(landNormalsNew); QModelIndex indexShape(landTable.getModelIndex (cellId, landTable.findColumnIndex (CSMWorld::Columns::ColumnId_LandHeightsIndex))); QModelIndex indexNormal(landTable.getModelIndex (cellId, landTable.findColumnIndex (CSMWorld::Columns::ColumnId_LandNormalsIndex))); document.getUndoStack().push (new CSMWorld::TouchLandCommand(landTable, ltexTable, cellId)); document.getUndoStack().push (new CSMWorld::ModifyCommand(landTable, indexShape, changedShape)); document.getUndoStack().push (new CSMWorld::ModifyCommand(landTable, indexNormal, changedNormals)); } bool CSVRender::TerrainShapeMode::allowLandShapeEditing(const std::string& cellId, bool useTool) { CSMDoc::Document& document = getWorldspaceWidget().getDocument(); CSMWorld::IdTable& landTable = dynamic_cast ( *document.getData().getTableModel (CSMWorld::UniversalId::Type_Land)); CSMWorld::IdTree& cellTable = dynamic_cast ( *document.getData().getTableModel (CSMWorld::UniversalId::Type_Cells)); if (noCell(cellId)) { std::string mode = CSMPrefs::get()["3D Scene Editing"]["outside-landedit"].toString(); // target cell does not exist if (mode=="Discard") return false; if (mode=="Create cell and land, then edit" && useTool) { std::unique_ptr createCommand ( new CSMWorld::CreateCommand (cellTable, cellId)); int parentIndex = cellTable.findColumnIndex (CSMWorld::Columns::ColumnId_Cell); int index = cellTable.findNestedColumnIndex (parentIndex, CSMWorld::Columns::ColumnId_Interior); createCommand->addNestedValue (parentIndex, index, false); document.getUndoStack().push (createCommand.release()); if (CSVRender::PagedWorldspaceWidget *paged = dynamic_cast (&getWorldspaceWidget())) { CSMWorld::CellSelection selection = paged->getCellSelection(); selection.add (CSMWorld::CellCoordinates::fromId (cellId).first); paged->setCellSelection (selection); } } } else if (CSVRender::PagedWorldspaceWidget *paged = dynamic_cast (&getWorldspaceWidget())) { CSMWorld::CellSelection selection = paged->getCellSelection(); if (!selection.has (CSMWorld::CellCoordinates::fromId (cellId).first)) { // target cell exists, but is not shown std::string mode = CSMPrefs::get()["3D Scene Editing"]["outside-visible-landedit"].toString(); if (mode=="Discard") return false; if (mode=="Show cell and edit" && useTool) { selection.add (CSMWorld::CellCoordinates::fromId (cellId).first); paged->setCellSelection (selection); } } } if (noLand(cellId)) { std::string mode = CSMPrefs::get()["3D Scene Editing"]["outside-landedit"].toString(); // target cell does not exist if (mode=="Discard") return false; if (mode=="Create cell and land, then edit" && useTool) { document.getUndoStack().push (new CSMWorld::CreateCommand (landTable, cellId)); createNewLandData(CSMWorld::CellCoordinates::fromId(cellId).first); fixEdges(CSMWorld::CellCoordinates::fromId(cellId).first); sortAndLimitAlteredCells(); } } else if (noLandLoaded(cellId)) { std::string mode = CSMPrefs::get()["3D Scene Editing"]["outside-landedit"].toString(); if (mode=="Discard") return false; if (mode=="Create cell and land, then edit" && useTool) { createNewLandData(CSMWorld::CellCoordinates::fromId(cellId).first); fixEdges(CSMWorld::CellCoordinates::fromId(cellId).first); sortAndLimitAlteredCells(); } } if (useTool && (noCell(cellId) || noLand(cellId) || noLandLoaded(cellId))) { Log(Debug::Warning) << "Land creation failed at cell id: " << cellId; return false; } return true; } void CSVRender::TerrainShapeMode::fixEdges(CSMWorld::CellCoordinates cellCoords) { CSMDoc::Document& document = getWorldspaceWidget().getDocument(); CSMWorld::IdTable& landTable = dynamic_cast ( *document.getData().getTableModel (CSMWorld::UniversalId::Type_Land)); int landshapeColumn = landTable.findColumnIndex(CSMWorld::Columns::ColumnId_LandHeightsIndex); std::string cellId = CSMWorld::CellCoordinates::generateId(cellCoords.getX(), cellCoords.getY()); std::string cellLeftId = CSMWorld::CellCoordinates::generateId(cellCoords.getX() - 1, cellCoords.getY()); std::string cellRightId = CSMWorld::CellCoordinates::generateId(cellCoords.getX() + 1, cellCoords.getY()); std::string cellUpId = CSMWorld::CellCoordinates::generateId(cellCoords.getX(), cellCoords.getY() - 1); std::string cellDownId = CSMWorld::CellCoordinates::generateId(cellCoords.getX(), cellCoords.getY() + 1); const CSMWorld::LandHeightsColumn::DataType landShapePointer = landTable.data(landTable.getModelIndex(cellId, landshapeColumn)).value(); const CSMWorld::LandHeightsColumn::DataType landLeftShapePointer = landTable.data(landTable.getModelIndex(cellLeftId, landshapeColumn)).value(); const CSMWorld::LandHeightsColumn::DataType landRightShapePointer = landTable.data(landTable.getModelIndex(cellRightId, landshapeColumn)).value(); const CSMWorld::LandHeightsColumn::DataType landUpShapePointer = landTable.data(landTable.getModelIndex(cellUpId, landshapeColumn)).value(); const CSMWorld::LandHeightsColumn::DataType landDownShapePointer = landTable.data(landTable.getModelIndex(cellDownId, landshapeColumn)).value(); CSMWorld::LandHeightsColumn::DataType landShapeNew(landShapePointer); for(int i = 0; i < ESM::Land::LAND_SIZE; ++i) { if (isLandLoaded(cellLeftId) && landShapePointer[i * ESM::Land::LAND_SIZE] != landLeftShapePointer[i * ESM::Land::LAND_SIZE + ESM::Land::LAND_SIZE - 1]) landShapeNew[i * ESM::Land::LAND_SIZE] = landLeftShapePointer[i * ESM::Land::LAND_SIZE + ESM::Land::LAND_SIZE - 1]; if (isLandLoaded(cellRightId) && landShapePointer[i * ESM::Land::LAND_SIZE + ESM::Land::LAND_SIZE - 1] != landRightShapePointer[i * ESM::Land::LAND_SIZE]) landShapeNew[i * ESM::Land::LAND_SIZE + ESM::Land::LAND_SIZE - 1] = landRightShapePointer[i * ESM::Land::LAND_SIZE]; if (isLandLoaded(cellUpId) && landShapePointer[i] != landUpShapePointer[(ESM::Land::LAND_SIZE - 1) * ESM::Land::LAND_SIZE + i]) landShapeNew[i] = landUpShapePointer[(ESM::Land::LAND_SIZE - 1) * ESM::Land::LAND_SIZE + i]; if (isLandLoaded(cellDownId) && landShapePointer[(ESM::Land::LAND_SIZE - 1) * ESM::Land::LAND_SIZE + i] != landDownShapePointer[i]) landShapeNew[(ESM::Land::LAND_SIZE - 1) * ESM::Land::LAND_SIZE + i] = landDownShapePointer[i]; } QVariant changedLand; changedLand.setValue(landShapeNew); QModelIndex index(landTable.getModelIndex (cellId, landTable.findColumnIndex (CSMWorld::Columns::ColumnId_LandHeightsIndex))); QUndoStack& undoStack = document.getUndoStack(); undoStack.push (new CSMWorld::ModifyCommand(landTable, index, changedLand)); } void CSVRender::TerrainShapeMode::dragMoveEvent (QDragMoveEvent *event) { } void CSVRender::TerrainShapeMode::mouseMoveEvent (QMouseEvent *event) { WorldspaceHitResult hit = getWorldspaceWidget().mousePick(event->pos(), getInteractionMask()); if (hit.hit && mBrushDraw && !(mShapeEditTool == ShapeEditTool_Drag && mIsEditing)) mBrushDraw->update(hit.worldPos, mBrushSize, mBrushShape); if (!hit.hit && mBrushDraw && !(mShapeEditTool == ShapeEditTool_Drag && mIsEditing)) mBrushDraw->hide(); } std::shared_ptr CSVRender::TerrainShapeMode::getTerrainSelection() { return mTerrainShapeSelection; } void CSVRender::TerrainShapeMode::setBrushSize(int brushSize) { mBrushSize = brushSize; } void CSVRender::TerrainShapeMode::setBrushShape(CSVWidget::BrushShape brushShape) { mBrushShape = brushShape; //Set custom brush shape if (mBrushShape == CSVWidget::BrushShape_Custom && !mTerrainShapeSelection->getTerrainSelection().empty()) { auto terrainSelection = mTerrainShapeSelection->getTerrainSelection(); int selectionCenterX = 0; int selectionCenterY = 0; int selectionAmount = 0; for(auto const& value: terrainSelection) { selectionCenterX = selectionCenterX + value.first; selectionCenterY = selectionCenterY + value.second; ++selectionAmount; } if (selectionAmount != 0) { selectionCenterX /= selectionAmount; selectionCenterY /= selectionAmount; } mCustomBrushShape.clear(); std::pair differentialPos {}; for(auto const& value: terrainSelection) { differentialPos.first = value.first - selectionCenterX; differentialPos.second = value.second - selectionCenterY; mCustomBrushShape.push_back(differentialPos); } } } void CSVRender::TerrainShapeMode::setShapeEditTool(int shapeEditTool) { mShapeEditTool = shapeEditTool; } void CSVRender::TerrainShapeMode::setShapeEditToolStrength(int shapeEditToolStrength) { mShapeEditToolStrength = shapeEditToolStrength; } CSVRender::PagedWorldspaceWidget& CSVRender::TerrainShapeMode::getPagedWorldspaceWidget() { return dynamic_cast(getWorldspaceWidget()); } ================================================ FILE: apps/opencs/view/render/terrainshapemode.hpp ================================================ #ifndef CSV_RENDER_TERRAINSHAPEMODE_H #define CSV_RENDER_TERRAINSHAPEMODE_H #include "editmode.hpp" #include #include #include #include #ifndef Q_MOC_RUN #include "../../model/world/data.hpp" #include "../../model/world/land.hpp" #include "../../model/doc/document.hpp" #include "../../model/world/commands.hpp" #include "../../model/world/idtable.hpp" #include "../../model/world/landtexture.hpp" #include "../widget/brushshapes.hpp" #endif #include "brushdraw.hpp" #include "terrainselection.hpp" namespace CSVWidget { class SceneToolShapeBrush; } namespace CSVRender { class PagedWorldspaceWidget; /// \brief EditMode for handling the terrain shape editing class TerrainShapeMode : public EditMode { Q_OBJECT public: enum InteractionType { InteractionType_PrimaryEdit, InteractionType_PrimarySelect, InteractionType_SecondaryEdit, InteractionType_SecondarySelect, InteractionType_None }; enum ShapeEditTool { ShapeEditTool_Drag = 0, ShapeEditTool_PaintToRaise = 1, ShapeEditTool_PaintToLower = 2, ShapeEditTool_Smooth = 3, ShapeEditTool_Flatten = 4 }; /// Editmode for terrain shape grid TerrainShapeMode(WorldspaceWidget*, osg::Group* parentNode, QWidget* parent = nullptr); void primaryOpenPressed (const WorldspaceHitResult& hit) override; /// Create single command for one-click shape editing void primaryEditPressed (const WorldspaceHitResult& hit) override; /// Open brush settings window void primarySelectPressed(const WorldspaceHitResult&) override; void secondarySelectPressed(const WorldspaceHitResult&) override; void activate(CSVWidget::SceneToolbar*) override; void deactivate(CSVWidget::SceneToolbar*) override; /// Start shape editing command macro bool primaryEditStartDrag (const QPoint& pos) override; bool secondaryEditStartDrag (const QPoint& pos) override; bool primarySelectStartDrag (const QPoint& pos) override; bool secondarySelectStartDrag (const QPoint& pos) override; /// Handle shape edit behavior during dragging void drag (const QPoint& pos, int diffX, int diffY, double speedFactor) override; /// End shape editing command macro void dragCompleted(const QPoint& pos) override; /// Cancel shape editing, and reset all pending changes void dragAborted() override; void dragWheel (int diff, double speedFactor) override; void dragMoveEvent (QDragMoveEvent *event) override; void mouseMoveEvent (QMouseEvent *event) override; std::shared_ptr getTerrainSelection(); private: /// Remove duplicates and sort mAlteredCells, then limitAlteredHeights forward and reverse void sortAndLimitAlteredCells(); /// Reset everything in the current edit void clearTransientEdits(); /// Move pending alteredHeights changes to omwgame/omwaddon -data void applyTerrainEditChanges(); /// Handle brush mechanics for shape editing void editTerrainShapeGrid (const std::pair& vertexCoords, bool dragOperation); /// Calculate height, when aiming for bump-shaped terrain change float calculateBumpShape(float distance, int radius, float height); /// set the target height for flatten tool void setFlattenToolTargetHeight(const WorldspaceHitResult& hit); /// Do a single height alteration for transient shape edit map void alterHeight(const CSMWorld::CellCoordinates& cellCoords, int inCellX, int inCellY, float alteredHeight, bool useTool = true); /// Do a single smoothing height alteration for transient shape edit map void smoothHeight(const CSMWorld::CellCoordinates& cellCoords, int inCellX, int inCellY, int toolStrength); /// Do a single flattening height alteration for transient shape edit map void flattenHeight(const CSMWorld::CellCoordinates& cellCoords, int inCellX, int inCellY, int toolStrength, int targetHeight); /// Get altered height values around one vertex void updateKeyHeightValues(const CSMWorld::CellCoordinates& cellCoords, int inCellX, int inCellY, float* thisHeight, float* thisAlteredHeight, float* leftHeight, float* leftAlteredHeight, float* upHeight, float* upAlteredHeight, float* rightHeight, float* rightAlteredHeight, float* downHeight, float* downAlteredHeight); ///Limit steepness based on either X or Y and return false if steepness is limited void compareAndLimit(const CSMWorld::CellCoordinates& cellCoords, int inCellX, int inCellY, float* limitedAlteredHeightXAxis, float* limitedAlteredHeightYAxis, bool* steepnessIsWithinLimits); /// Check that the edit doesn't break save format limits, fix if necessary, return true if slope steepness is within limits bool limitAlteredHeights(const CSMWorld::CellCoordinates& cellCoords, bool reverseMode = false); /// Check if global selection coordinate belongs to cell in view bool isInCellSelection(int globalSelectionX, int globalSelectionY); /// Select vertex at global selection coordinate void handleSelection(int globalSelectionX, int globalSelectionY, std::vector>* selections); /// Handle brush mechanics for terrain shape selection void selectTerrainShapes (const std::pair& vertexCoords, unsigned char selectMode, bool dragOperation); /// Push terrain shape edits to command macro void pushEditToCommand (const CSMWorld::LandHeightsColumn::DataType& newLandGrid, CSMDoc::Document& document, CSMWorld::IdTable& landTable, const std::string& cellId); /// Push land normals edits to command macro void pushNormalsEditToCommand(const CSMWorld::LandNormalsColumn::DataType& newLandGrid, CSMDoc::Document& document, CSMWorld::IdTable& landTable, const std::string& cellId); bool noCell(const std::string& cellId); bool noLand(const std::string& cellId); bool noLandLoaded(const std::string& cellId); bool isLandLoaded(const std::string& cellId); /// Create new blank height record and new normals, if there are valid adjancent cell, take sample points and set the average height based on that void createNewLandData(const CSMWorld::CellCoordinates& cellCoords); /// Create new cell and land if needed, only user tools may ask for opening new cells (useTool == false is for automated land changes) bool allowLandShapeEditing(const std::string& textureFileName, bool useTool = true); /// Bind the edging vertice to the values of the adjancent cells void fixEdges(CSMWorld::CellCoordinates cellCoords); std::string mBrushTexture; int mBrushSize = 1; CSVWidget::BrushShape mBrushShape = CSVWidget::BrushShape_Point; std::unique_ptr mBrushDraw; std::vector> mCustomBrushShape; CSVWidget::SceneToolShapeBrush *mShapeBrushScenetool = nullptr; int mDragMode = InteractionType_None; osg::Group* mParentNode; bool mIsEditing = false; std::shared_ptr mTerrainShapeSelection; int mTotalDiffY = 0; std::vector mAlteredCells; osg::Vec3d mEditingPos; int mShapeEditTool = ShapeEditTool_Drag; int mShapeEditToolStrength = 8; int mTargetHeight = 0; PagedWorldspaceWidget& getPagedWorldspaceWidget(); public slots: void setBrushSize(int brushSize); void setBrushShape(CSVWidget::BrushShape brushShape); void setShapeEditTool(int shapeEditTool); void setShapeEditToolStrength(int shapeEditToolStrength); }; } #endif ================================================ FILE: apps/opencs/view/render/terrainstorage.cpp ================================================ #include "terrainstorage.hpp" #include "../../model/world/land.hpp" #include "../../model/world/landtexture.hpp" #include namespace CSVRender { TerrainStorage::TerrainStorage(const CSMWorld::Data &data) : ESMTerrain::Storage(data.getResourceSystem()->getVFS()) , mData(data) { resetHeights(); } osg::ref_ptr TerrainStorage::getLand(int cellX, int cellY) { // The cell isn't guaranteed to have Land. This is because the terrain implementation // has to wrap the vertices of the last row and column to the next cell, which may be a nonexisting cell int index = mData.getLand().searchId(CSMWorld::Land::createUniqueRecordId(cellX, cellY)); if (index == -1) return nullptr; const ESM::Land& land = mData.getLand().getRecord(index).get(); return new ESMTerrain::LandObject(&land, ESM::Land::DATA_VHGT | ESM::Land::DATA_VNML | ESM::Land::DATA_VCLR | ESM::Land::DATA_VTEX); } const ESM::LandTexture* TerrainStorage::getLandTexture(int index, short plugin) { int row = mData.getLandTextures().searchId(CSMWorld::LandTexture::createUniqueRecordId(plugin, index)); if (row == -1) return nullptr; return &mData.getLandTextures().getRecord(row).get(); } void TerrainStorage::setAlteredHeight(int inCellX, int inCellY, float height) { mAlteredHeight[inCellY*ESM::Land::LAND_SIZE + inCellX] = height - fmod(height, 8); //Limit to divisible by 8 to avoid cell seam breakage } void TerrainStorage::resetHeights() { std::fill(std::begin(mAlteredHeight), std::end(mAlteredHeight), 0); } float TerrainStorage::getSumOfAlteredAndTrueHeight(int cellX, int cellY, int inCellX, int inCellY) { float height = 0.f; osg::ref_ptr land = getLand (cellX, cellY); if (land) { const ESM::Land::LandData* data = land ? land->getData(ESM::Land::DATA_VHGT) : nullptr; if (data) height = getVertexHeight(data, inCellX, inCellY); } else return height; return mAlteredHeight[inCellY*ESM::Land::LAND_SIZE + inCellX] + height; } float* TerrainStorage::getAlteredHeight(int inCellX, int inCellY) { return &mAlteredHeight[inCellY*ESM::Land::LAND_SIZE + inCellX]; } void TerrainStorage::getBounds(float &minX, float &maxX, float &minY, float &maxY) { // not needed at the moment - this returns the bounds of the whole world, but we only edit individual cells throw std::runtime_error("getBounds not implemented"); } int TerrainStorage::getThisHeight(int col, int row, const ESM::Land::LandData *heightData) const { return heightData->mHeights[col*ESM::Land::LAND_SIZE + row] + mAlteredHeight[static_cast(col*ESM::Land::LAND_SIZE + row)]; } int TerrainStorage::getLeftHeight(int col, int row, const ESM::Land::LandData *heightData) const { return heightData->mHeights[(col)*ESM::Land::LAND_SIZE + row - 1] + mAlteredHeight[static_cast((col)*ESM::Land::LAND_SIZE + row - 1)]; } int TerrainStorage::getRightHeight(int col, int row, const ESM::Land::LandData *heightData) const { return heightData->mHeights[col*ESM::Land::LAND_SIZE + row + 1] + mAlteredHeight[static_cast(col*ESM::Land::LAND_SIZE + row + 1)]; } int TerrainStorage::getUpHeight(int col, int row, const ESM::Land::LandData *heightData) const { return heightData->mHeights[(col - 1)*ESM::Land::LAND_SIZE + row] + mAlteredHeight[static_cast((col - 1)*ESM::Land::LAND_SIZE + row)]; } int TerrainStorage::getDownHeight(int col, int row, const ESM::Land::LandData *heightData) const { return heightData->mHeights[(col + 1)*ESM::Land::LAND_SIZE + row] + mAlteredHeight[static_cast((col + 1)*ESM::Land::LAND_SIZE + row)]; } int TerrainStorage::getHeightDifferenceToLeft(int col, int row, const ESM::Land::LandData *heightData) const { return abs(getThisHeight(col, row, heightData) - getLeftHeight(col, row, heightData)); } int TerrainStorage::getHeightDifferenceToRight(int col, int row, const ESM::Land::LandData *heightData) const { return abs(getThisHeight(col, row, heightData) - getRightHeight(col, row, heightData)); } int TerrainStorage::getHeightDifferenceToUp(int col, int row, const ESM::Land::LandData *heightData) const { return abs(getThisHeight(col, row, heightData) - getUpHeight(col, row, heightData)); } int TerrainStorage::getHeightDifferenceToDown(int col, int row, const ESM::Land::LandData *heightData) const { return abs(getThisHeight(col, row, heightData) - getDownHeight(col, row, heightData)); } bool TerrainStorage::leftOrUpIsOverTheLimit(int col, int row, int heightWarningLimit, const ESM::Land::LandData *heightData) const { return getHeightDifferenceToLeft(col, row, heightData) >= heightWarningLimit || getHeightDifferenceToUp(col, row, heightData) >= heightWarningLimit; } bool TerrainStorage::rightOrDownIsOverTheLimit(int col, int row, int heightWarningLimit, const ESM::Land::LandData *heightData) const { return getHeightDifferenceToRight(col, row, heightData) >= heightWarningLimit || getHeightDifferenceToDown(col, row, heightData) >= heightWarningLimit; } void TerrainStorage::adjustColor(int col, int row, const ESM::Land::LandData *heightData, osg::Vec4ub& color) const { // Highlight broken height changes int heightWarningLimit = 1024; if (((col > 0 && row > 0) && leftOrUpIsOverTheLimit(col, row, heightWarningLimit, heightData)) || ((col < ESM::Land::LAND_SIZE - 1 && row < ESM::Land::LAND_SIZE - 1) && rightOrDownIsOverTheLimit(col, row, heightWarningLimit, heightData))) { color.r() = 255; color.g() = 0; color.b() = 0; } } float TerrainStorage::getAlteredHeight(int col, int row) const { return mAlteredHeight[static_cast(col*ESM::Land::LAND_SIZE + row)]; } } ================================================ FILE: apps/opencs/view/render/terrainstorage.hpp ================================================ #ifndef OPENCS_RENDER_TERRAINSTORAGE_H #define OPENCS_RENDER_TERRAINSTORAGE_H #include #include #include "../../model/world/data.hpp" namespace CSVRender { /** * @brief A bridge between the terrain component and OpenCS's terrain data storage. */ class TerrainStorage : public ESMTerrain::Storage { public: TerrainStorage(const CSMWorld::Data& data); void setAlteredHeight(int inCellX, int inCellY, float heightMap); void resetHeights(); bool useAlteration() const override { return true; } float getSumOfAlteredAndTrueHeight(int cellX, int cellY, int inCellX, int inCellY); float* getAlteredHeight(int inCellX, int inCellY); private: const CSMWorld::Data& mData; std::array mAlteredHeight; osg::ref_ptr getLand (int cellX, int cellY) override; const ESM::LandTexture* getLandTexture(int index, short plugin) override; void getBounds(float& minX, float& maxX, float& minY, float& maxY) override; int getThisHeight(int col, int row, const ESM::Land::LandData *heightData) const; int getLeftHeight(int col, int row, const ESM::Land::LandData *heightData) const; int getRightHeight(int col, int row, const ESM::Land::LandData *heightData) const; int getUpHeight(int col, int row, const ESM::Land::LandData *heightData) const; int getDownHeight(int col, int row, const ESM::Land::LandData *heightData) const; int getHeightDifferenceToLeft(int col, int row, const ESM::Land::LandData *heightData) const; int getHeightDifferenceToRight(int col, int row, const ESM::Land::LandData *heightData) const; int getHeightDifferenceToUp(int col, int row, const ESM::Land::LandData *heightData) const; int getHeightDifferenceToDown(int col, int row, const ESM::Land::LandData *heightData) const; bool leftOrUpIsOverTheLimit(int col, int row, int heightWarningLimit, const ESM::Land::LandData *heightData) const; bool rightOrDownIsOverTheLimit(int col, int row, int heightWarningLimit, const ESM::Land::LandData *heightData) const; void adjustColor(int col, int row, const ESM::Land::LandData *heightData, osg::Vec4ub& color) const override; float getAlteredHeight(int col, int row) const override; }; } #endif ================================================ FILE: apps/opencs/view/render/terraintexturemode.cpp ================================================ #include "terraintexturemode.hpp" #include #include #include #include #include #include #include #include #include #include "../widget/modebutton.hpp" #include "../widget/scenetoolbar.hpp" #include "../widget/scenetooltexturebrush.hpp" #include "../../model/doc/document.hpp" #include "../../model/prefs/state.hpp" #include "../../model/world/commands.hpp" #include "../../model/world/data.hpp" #include "../../model/world/idtable.hpp" #include "../../model/world/idtree.hpp" #include "../../model/world/landtexture.hpp" #include "../../model/world/tablemimedata.hpp" #include "../../model/world/universalid.hpp" #include "../widget/brushshapes.hpp" #include "brushdraw.hpp" #include "editmode.hpp" #include "pagedworldspacewidget.hpp" #include "mask.hpp" #include "object.hpp" // Something small needed regarding pointers from here () #include "worldspacewidget.hpp" CSVRender::TerrainTextureMode::TerrainTextureMode (WorldspaceWidget *worldspaceWidget, osg::Group* parentNode, QWidget *parent) : EditMode (worldspaceWidget, QIcon {":scenetoolbar/editing-terrain-texture"}, Mask_Terrain | Mask_Reference, "Terrain texture editing", parent), mBrushTexture("L0#0"), mBrushSize(1), mBrushShape(CSVWidget::BrushShape_Point), mTextureBrushScenetool(nullptr), mDragMode(InteractionType_None), mParentNode(parentNode), mIsEditing(false) { } void CSVRender::TerrainTextureMode::activate(CSVWidget::SceneToolbar* toolbar) { if(!mTextureBrushScenetool) { mTextureBrushScenetool = new CSVWidget::SceneToolTextureBrush (toolbar, "scenetooltexturebrush", getWorldspaceWidget().getDocument()); connect(mTextureBrushScenetool, SIGNAL (clicked()), mTextureBrushScenetool, SLOT (activate())); connect(mTextureBrushScenetool->mTextureBrushWindow, SIGNAL(passBrushSize(int)), this, SLOT(setBrushSize(int))); connect(mTextureBrushScenetool->mTextureBrushWindow, SIGNAL(passBrushShape(CSVWidget::BrushShape)), this, SLOT(setBrushShape(CSVWidget::BrushShape))); connect(mTextureBrushScenetool->mTextureBrushWindow->mSizeSliders->mBrushSizeSlider, SIGNAL(valueChanged(int)), this, SLOT(setBrushSize(int))); connect(mTextureBrushScenetool, SIGNAL(passTextureId(std::string)), this, SLOT(setBrushTexture(std::string))); connect(mTextureBrushScenetool->mTextureBrushWindow, SIGNAL(passTextureId(std::string)), this, SLOT(setBrushTexture(std::string))); connect(mTextureBrushScenetool, SIGNAL(passEvent(QDropEvent*)), this, SLOT(handleDropEvent(QDropEvent*))); connect(this, SIGNAL(passBrushTexture(std::string)), mTextureBrushScenetool->mTextureBrushWindow, SLOT(setBrushTexture(std::string))); connect(this, SIGNAL(passBrushTexture(std::string)), mTextureBrushScenetool, SLOT(updateBrushHistory(std::string))); } if (!mTerrainTextureSelection) { mTerrainTextureSelection.reset(new TerrainSelection(mParentNode, &getWorldspaceWidget(), TerrainSelectionType::Texture)); } if (!mBrushDraw) mBrushDraw.reset(new BrushDraw(mParentNode, true)); EditMode::activate(toolbar); toolbar->addTool (mTextureBrushScenetool); } void CSVRender::TerrainTextureMode::deactivate(CSVWidget::SceneToolbar* toolbar) { if(mTextureBrushScenetool) { toolbar->removeTool (mTextureBrushScenetool); delete mTextureBrushScenetool; mTextureBrushScenetool = nullptr; } if (mTerrainTextureSelection) { mTerrainTextureSelection.reset(); } if (mBrushDraw) mBrushDraw.reset(); EditMode::deactivate(toolbar); } void CSVRender::TerrainTextureMode::primaryOpenPressed(const WorldspaceHitResult& hit) // Apply changes here { } void CSVRender::TerrainTextureMode::primaryEditPressed(const WorldspaceHitResult& hit) // Apply changes here { CSMDoc::Document& document = getWorldspaceWidget().getDocument(); CSMWorld::IdTable& landTable = dynamic_cast ( *document.getData().getTableModel (CSMWorld::UniversalId::Type_Land)); CSMWorld::IdTable& ltexTable = dynamic_cast ( *document.getData().getTableModel (CSMWorld::UniversalId::Type_LandTextures)); mCellId = getWorldspaceWidget().getCellId (hit.worldPos); QUndoStack& undoStack = document.getUndoStack(); CSMWorld::IdCollection& landtexturesCollection = document.getData().getLandTextures(); int index = landtexturesCollection.searchId(mBrushTexture); if (index != -1 && !landtexturesCollection.getRecord(index).isDeleted() && hit.hit && hit.tag == nullptr) { undoStack.beginMacro ("Edit texture records"); if(allowLandTextureEditing(mCellId)) { undoStack.push (new CSMWorld::TouchLandCommand(landTable, ltexTable, mCellId)); editTerrainTextureGrid(hit); } undoStack.endMacro(); } } void CSVRender::TerrainTextureMode::primarySelectPressed(const WorldspaceHitResult& hit) { if(hit.hit && hit.tag == nullptr) { selectTerrainTextures(CSMWorld::CellCoordinates::toTextureCoords(hit.worldPos), 0, false); } } void CSVRender::TerrainTextureMode::secondarySelectPressed(const WorldspaceHitResult& hit) { if(hit.hit && hit.tag == nullptr) { selectTerrainTextures(CSMWorld::CellCoordinates::toTextureCoords(hit.worldPos), 1, false); } } bool CSVRender::TerrainTextureMode::primaryEditStartDrag (const QPoint& pos) { WorldspaceHitResult hit = getWorldspaceWidget().mousePick (pos, getWorldspaceWidget().getInteractionMask()); CSMDoc::Document& document = getWorldspaceWidget().getDocument(); CSMWorld::IdTable& landTable = dynamic_cast ( *document.getData().getTableModel (CSMWorld::UniversalId::Type_Land)); CSMWorld::IdTable& ltexTable = dynamic_cast ( *document.getData().getTableModel (CSMWorld::UniversalId::Type_LandTextures)); mCellId = getWorldspaceWidget().getCellId (hit.worldPos); QUndoStack& undoStack = document.getUndoStack(); mDragMode = InteractionType_PrimaryEdit; CSMWorld::IdCollection& landtexturesCollection = document.getData().getLandTextures(); int index = landtexturesCollection.searchId(mBrushTexture); if (index != -1 && !landtexturesCollection.getRecord(index).isDeleted() && hit.hit && hit.tag == nullptr) { undoStack.beginMacro ("Edit texture records"); mIsEditing = true; if(allowLandTextureEditing(mCellId)) { undoStack.push (new CSMWorld::TouchLandCommand(landTable, ltexTable, mCellId)); editTerrainTextureGrid(hit); } } return true; } bool CSVRender::TerrainTextureMode::secondaryEditStartDrag (const QPoint& pos) { return false; } bool CSVRender::TerrainTextureMode::primarySelectStartDrag (const QPoint& pos) { WorldspaceHitResult hit = getWorldspaceWidget().mousePick (pos, getWorldspaceWidget().getInteractionMask()); mDragMode = InteractionType_PrimarySelect; if (!hit.hit || hit.tag != nullptr) { mDragMode = InteractionType_None; return false; } selectTerrainTextures(CSMWorld::CellCoordinates::toTextureCoords(hit.worldPos), 0, true); return false; } bool CSVRender::TerrainTextureMode::secondarySelectStartDrag (const QPoint& pos) { WorldspaceHitResult hit = getWorldspaceWidget().mousePick (pos, getWorldspaceWidget().getInteractionMask()); mDragMode = InteractionType_SecondarySelect; if (!hit.hit || hit.tag != nullptr) { mDragMode = InteractionType_None; return false; } selectTerrainTextures(CSMWorld::CellCoordinates::toTextureCoords(hit.worldPos), 1, true); return false; } void CSVRender::TerrainTextureMode::drag (const QPoint& pos, int diffX, int diffY, double speedFactor) { if (mDragMode == InteractionType_PrimaryEdit) { WorldspaceHitResult hit = getWorldspaceWidget().mousePick (pos, getWorldspaceWidget().getInteractionMask()); std::string cellId = getWorldspaceWidget().getCellId (hit.worldPos); CSMDoc::Document& document = getWorldspaceWidget().getDocument(); CSMWorld::IdCollection& landtexturesCollection = document.getData().getLandTextures(); int index = landtexturesCollection.searchId(mBrushTexture); if (index != -1 && !landtexturesCollection.getRecord(index).isDeleted() && hit.hit && hit.tag == nullptr) { editTerrainTextureGrid(hit); } } if (mDragMode == InteractionType_PrimarySelect) { WorldspaceHitResult hit = getWorldspaceWidget().mousePick (pos, getWorldspaceWidget().getInteractionMask()); if (hit.hit && hit.tag == nullptr) selectTerrainTextures(CSMWorld::CellCoordinates::toTextureCoords(hit.worldPos), 0, true); } if (mDragMode == InteractionType_SecondarySelect) { WorldspaceHitResult hit = getWorldspaceWidget().mousePick (pos, getWorldspaceWidget().getInteractionMask()); if (hit.hit && hit.tag == nullptr) selectTerrainTextures(CSMWorld::CellCoordinates::toTextureCoords(hit.worldPos), 1, true); } } void CSVRender::TerrainTextureMode::dragCompleted(const QPoint& pos) { if (mDragMode == InteractionType_PrimaryEdit && mIsEditing) { CSMDoc::Document& document = getWorldspaceWidget().getDocument(); QUndoStack& undoStack = document.getUndoStack(); CSMWorld::IdCollection& landtexturesCollection = document.getData().getLandTextures(); int index = landtexturesCollection.searchId(mBrushTexture); if (index != -1 && !landtexturesCollection.getRecord(index).isDeleted()) { undoStack.endMacro(); mIsEditing = false; } } } void CSVRender::TerrainTextureMode::dragAborted() { } void CSVRender::TerrainTextureMode::dragWheel (int diff, double speedFactor) { } void CSVRender::TerrainTextureMode::handleDropEvent (QDropEvent *event) { const CSMWorld::TableMimeData* mime = dynamic_cast (event->mimeData()); if (!mime) // May happen when non-records (e.g. plain text) are dragged and dropped return; if (mime->holdsType (CSMWorld::UniversalId::Type_LandTexture)) { const std::vector ids = mime->getData(); for (const CSMWorld::UniversalId& uid : ids) { mBrushTexture = uid.getId(); emit passBrushTexture(mBrushTexture); } } if (mime->holdsType (CSMWorld::UniversalId::Type_Texture)) { const std::vector ids = mime->getData(); for (const CSMWorld::UniversalId& uid : ids) { std::string textureFileName = uid.toString(); createTexture(textureFileName); emit passBrushTexture(mBrushTexture); } } } void CSVRender::TerrainTextureMode::editTerrainTextureGrid(const WorldspaceHitResult& hit) { CSMDoc::Document& document = getWorldspaceWidget().getDocument(); CSMWorld::IdTable& landTable = dynamic_cast ( *document.getData().getTableModel (CSMWorld::UniversalId::Type_Land)); mCellId = getWorldspaceWidget().getCellId (hit.worldPos); if(allowLandTextureEditing(mCellId)) {} std::pair cellCoordinates_pair = CSMWorld::CellCoordinates::fromId (mCellId); int cellX = cellCoordinates_pair.first.getX(); int cellY = cellCoordinates_pair.first.getY(); // The coordinates of hit in mCellId int xHitInCell (float(((hit.worldPos.x() - (cellX* cellSize)) * landTextureSize / cellSize) - 0.25)); int yHitInCell (float(((hit.worldPos.y() - (cellY* cellSize)) * landTextureSize / cellSize) + 0.25)); if (xHitInCell < 0) { xHitInCell = xHitInCell + landTextureSize; cellX = cellX - 1; } if (yHitInCell > 15) { yHitInCell = yHitInCell - landTextureSize; cellY = cellY + 1; } mCellId = CSMWorld::CellCoordinates::generateId(cellX, cellY); if(allowLandTextureEditing(mCellId)) {} std::string iteratedCellId; int textureColumn = landTable.findColumnIndex(CSMWorld::Columns::ColumnId_LandTexturesIndex); std::size_t hashlocation = mBrushTexture.find('#'); std::string mBrushTextureInt = mBrushTexture.substr (hashlocation+1); int brushInt = stoi(mBrushTexture.substr (hashlocation+1))+1; // All indices are offset by +1 int r = static_cast(mBrushSize) / 2; if (mBrushShape == CSVWidget::BrushShape_Point) { CSMWorld::LandTexturesColumn::DataType newTerrainPointer = landTable.data(landTable.getModelIndex(mCellId, textureColumn)).value(); CSMWorld::LandTexturesColumn::DataType newTerrain(newTerrainPointer); if(allowLandTextureEditing(mCellId)) { newTerrain[yHitInCell*landTextureSize+xHitInCell] = brushInt; pushEditToCommand(newTerrain, document, landTable, mCellId); } } if (mBrushShape == CSVWidget::BrushShape_Square) { int upperLeftCellX = cellX - std::floor(r / landTextureSize); int upperLeftCellY = cellY - std::floor(r / landTextureSize); if (xHitInCell - (r % landTextureSize) < 0) upperLeftCellX--; if (yHitInCell - (r % landTextureSize) < 0) upperLeftCellY--; int lowerrightCellX = cellX + std::floor(r / landTextureSize); int lowerrightCellY = cellY + std::floor(r / landTextureSize); if (xHitInCell + (r % landTextureSize) > landTextureSize - 1) lowerrightCellX++; if (yHitInCell + (r % landTextureSize) > landTextureSize - 1) lowerrightCellY++; for(int i_cell = upperLeftCellX; i_cell <= lowerrightCellX; i_cell++) { for(int j_cell = upperLeftCellY; j_cell <= lowerrightCellY; j_cell++) { iteratedCellId = CSMWorld::CellCoordinates::generateId(i_cell, j_cell); if(allowLandTextureEditing(iteratedCellId)) { CSMWorld::LandTexturesColumn::DataType newTerrainPointer = landTable.data(landTable.getModelIndex(iteratedCellId, textureColumn)).value(); CSMWorld::LandTexturesColumn::DataType newTerrain(newTerrainPointer); for(int i = 0; i < landTextureSize; i++) { for(int j = 0; j < landTextureSize; j++) { if (i_cell == cellX && j_cell == cellY && abs(i-xHitInCell) < r && abs(j-yHitInCell) < r) { newTerrain[j*landTextureSize+i] = brushInt; } else { int distanceX(0); int distanceY(0); if (i_cell < cellX) distanceX = xHitInCell + landTextureSize * abs(i_cell-cellX) - i; if (j_cell < cellY) distanceY = yHitInCell + landTextureSize * abs(j_cell-cellY) - j; if (i_cell > cellX) distanceX = -xHitInCell + landTextureSize * abs(i_cell-cellX) + i; if (j_cell > cellY) distanceY = -yHitInCell + landTextureSize * abs(j_cell-cellY) + j; if (i_cell == cellX) distanceX = abs(i-xHitInCell); if (j_cell == cellY) distanceY = abs(j-yHitInCell); if (distanceX < r && distanceY < r) newTerrain[j*landTextureSize+i] = brushInt; } } } pushEditToCommand(newTerrain, document, landTable, iteratedCellId); } } } } if (mBrushShape == CSVWidget::BrushShape_Circle) { int upperLeftCellX = cellX - std::floor(r / landTextureSize); int upperLeftCellY = cellY - std::floor(r / landTextureSize); if (xHitInCell - (r % landTextureSize) < 0) upperLeftCellX--; if (yHitInCell - (r % landTextureSize) < 0) upperLeftCellY--; int lowerrightCellX = cellX + std::floor(r / landTextureSize); int lowerrightCellY = cellY + std::floor(r / landTextureSize); if (xHitInCell + (r % landTextureSize) > landTextureSize - 1) lowerrightCellX++; if (yHitInCell + (r % landTextureSize) > landTextureSize - 1) lowerrightCellY++; for(int i_cell = upperLeftCellX; i_cell <= lowerrightCellX; i_cell++) { for(int j_cell = upperLeftCellY; j_cell <= lowerrightCellY; j_cell++) { iteratedCellId = CSMWorld::CellCoordinates::generateId(i_cell, j_cell); if(allowLandTextureEditing(iteratedCellId)) { CSMWorld::LandTexturesColumn::DataType newTerrainPointer = landTable.data(landTable.getModelIndex(iteratedCellId, textureColumn)).value(); CSMWorld::LandTexturesColumn::DataType newTerrain(newTerrainPointer); for(int i = 0; i < landTextureSize; i++) { for(int j = 0; j < landTextureSize; j++) { if (i_cell == cellX && j_cell == cellY && abs(i-xHitInCell) < r && abs(j-yHitInCell) < r) { int distanceX = abs(i-xHitInCell); int distanceY = abs(j-yHitInCell); float distance = std::round(sqrt(pow(distanceX, 2)+pow(distanceY, 2))); float rf = static_cast(mBrushSize) / 2; if (distance < rf) newTerrain[j*landTextureSize+i] = brushInt; } else { int distanceX(0); int distanceY(0); if (i_cell < cellX) distanceX = xHitInCell + landTextureSize * abs(i_cell-cellX) - i; if (j_cell < cellY) distanceY = yHitInCell + landTextureSize * abs(j_cell-cellY) - j; if (i_cell > cellX) distanceX = -xHitInCell + landTextureSize * abs(i_cell-cellX) + i; if (j_cell > cellY) distanceY = -yHitInCell + landTextureSize * abs(j_cell-cellY) + j; if (i_cell == cellX) distanceX = abs(i-xHitInCell); if (j_cell == cellY) distanceY = abs(j-yHitInCell); float distance = std::round(sqrt(pow(distanceX, 2)+pow(distanceY, 2))); float rf = static_cast(mBrushSize) / 2; if (distance < rf) newTerrain[j*landTextureSize+i] = brushInt; } } } pushEditToCommand(newTerrain, document, landTable, iteratedCellId); } } } } if (mBrushShape == CSVWidget::BrushShape_Custom) { CSMWorld::LandTexturesColumn::DataType newTerrainPointer = landTable.data(landTable.getModelIndex(mCellId, textureColumn)).value(); CSMWorld::LandTexturesColumn::DataType newTerrain(newTerrainPointer); if(allowLandTextureEditing(mCellId) && !mCustomBrushShape.empty()) { for(auto const& value: mCustomBrushShape) { if(yHitInCell + value.second >= 0 && yHitInCell + value.second <= 15 && xHitInCell + value.first >= 0 && xHitInCell + value.first <= 15) { newTerrain[(yHitInCell+value.second)*landTextureSize+xHitInCell+value.first] = brushInt; } else { int cellXDifference = std::floor(1.0f*(xHitInCell + value.first)/landTextureSize); int cellYDifference = std::floor(1.0f*(yHitInCell + value.second)/landTextureSize); int xInOtherCell = xHitInCell + value.first - cellXDifference * landTextureSize; int yInOtherCell = yHitInCell + value.second - cellYDifference * landTextureSize; std::string cellId = CSMWorld::CellCoordinates::generateId(cellX+cellXDifference, cellY+cellYDifference); if (allowLandTextureEditing(cellId)) { CSMWorld::LandTexturesColumn::DataType newTerrainPointerOtherCell = landTable.data(landTable.getModelIndex(cellId, textureColumn)).value(); CSMWorld::LandTexturesColumn::DataType newTerrainOtherCell(newTerrainPointerOtherCell); newTerrainOtherCell[yInOtherCell*landTextureSize+xInOtherCell] = brushInt; pushEditToCommand(newTerrainOtherCell, document, landTable, cellId); } } } pushEditToCommand(newTerrain, document, landTable, mCellId); } } } bool CSVRender::TerrainTextureMode::isInCellSelection(int globalSelectionX, int globalSelectionY) { if (CSVRender::PagedWorldspaceWidget *paged = dynamic_cast (&getWorldspaceWidget())) { std::pair textureCoords = std::make_pair(globalSelectionX, globalSelectionY); std::string cellId = CSMWorld::CellCoordinates::textureGlobalToCellId(textureCoords); return paged->getCellSelection().has(CSMWorld::CellCoordinates::fromId(cellId).first); } return false; } void CSVRender::TerrainTextureMode::selectTerrainTextures(const std::pair& texCoords, unsigned char selectMode, bool dragOperation) { int r = mBrushSize / 2; std::vector> selections; if (mBrushShape == CSVWidget::BrushShape_Point) { if (isInCellSelection(texCoords.first, texCoords.second)) selections.emplace_back(texCoords); } if (mBrushShape == CSVWidget::BrushShape_Square) { for (int i = -r; i <= r; i++) { for (int j = -r; j <= r; j++) { int x = i + texCoords.first; int y = j + texCoords.second; if (isInCellSelection(x, y)) { selections.emplace_back(x, y); } } } } if (mBrushShape == CSVWidget::BrushShape_Circle) { for (int i = -r; i <= r; i++) { for (int j = -r; j <= r; j++) { osg::Vec2f coords(i,j); float rf = static_cast(mBrushSize) / 2; if (std::round(coords.length()) < rf) { int x = i + texCoords.first; int y = j + texCoords.second; if (isInCellSelection(x, y)) { selections.emplace_back(x, y); } } } } } if (mBrushShape == CSVWidget::BrushShape_Custom) { if(!mCustomBrushShape.empty()) { for(auto const& value: mCustomBrushShape) { int x = texCoords.first + value.first; int y = texCoords.second + value.second; if (isInCellSelection(x, y)) { selections.emplace_back(x, y); } } } } if(selectMode == 0) mTerrainTextureSelection->onlySelect(selections); if(selectMode == 1) mTerrainTextureSelection->toggleSelect(selections, dragOperation); } void CSVRender::TerrainTextureMode::pushEditToCommand(CSMWorld::LandTexturesColumn::DataType& newLandGrid, CSMDoc::Document& document, CSMWorld::IdTable& landTable, std::string cellId) { CSMWorld::IdTable& ltexTable = dynamic_cast ( *document.getData().getTableModel (CSMWorld::UniversalId::Type_LandTextures)); QVariant changedLand; changedLand.setValue(newLandGrid); QModelIndex index(landTable.getModelIndex (cellId, landTable.findColumnIndex (CSMWorld::Columns::ColumnId_LandTexturesIndex))); QUndoStack& undoStack = document.getUndoStack(); undoStack.push (new CSMWorld::ModifyCommand(landTable, index, changedLand)); undoStack.push (new CSMWorld::TouchLandCommand(landTable, ltexTable, cellId)); } void CSVRender::TerrainTextureMode::createTexture(std::string textureFileName) { CSMDoc::Document& document = getWorldspaceWidget().getDocument(); CSMWorld::IdTable& ltexTable = dynamic_cast ( *document.getData().getTableModel (CSMWorld::UniversalId::Type_LandTextures)); QUndoStack& undoStack = document.getUndoStack(); std::string newId; int counter=0; bool freeIndexFound = false; do { const size_t maxCounter = std::numeric_limits::max() - 1; try { newId = CSMWorld::LandTexture::createUniqueRecordId(0, counter); if (ltexTable.getRecord(newId).isDeleted() == 0) counter = (counter + 1) % maxCounter; } catch (const std::exception&) { newId = CSMWorld::LandTexture::createUniqueRecordId(0, counter); freeIndexFound = true; } } while (freeIndexFound == false); std::size_t idlocation = textureFileName.find("Texture: "); textureFileName = textureFileName.substr (idlocation + 9); QVariant textureNameVariant; QVariant textureFileNameVariant; textureFileNameVariant.setValue(QString::fromStdString(textureFileName)); undoStack.beginMacro ("Add land texture record"); undoStack.push (new CSMWorld::CreateCommand (ltexTable, newId)); QModelIndex index(ltexTable.getModelIndex (newId, ltexTable.findColumnIndex (CSMWorld::Columns::ColumnId_Texture))); undoStack.push (new CSMWorld::ModifyCommand(ltexTable, index, textureFileNameVariant)); undoStack.endMacro(); mBrushTexture = newId; } bool CSVRender::TerrainTextureMode::allowLandTextureEditing(std::string cellId) { CSMDoc::Document& document = getWorldspaceWidget().getDocument(); CSMWorld::IdTable& landTable = dynamic_cast ( *document.getData().getTableModel (CSMWorld::UniversalId::Type_Land)); CSMWorld::IdTree& cellTable = dynamic_cast ( *document.getData().getTableModel (CSMWorld::UniversalId::Type_Cells)); bool noCell = document.getData().getCells().searchId (cellId)==-1; bool noLand = document.getData().getLand().searchId (cellId)==-1; if (noCell) { std::string mode = CSMPrefs::get()["3D Scene Editing"]["outside-landedit"].toString(); // target cell does not exist if (mode=="Discard") return false; if (mode=="Create cell and land, then edit") { std::unique_ptr createCommand ( new CSMWorld::CreateCommand (cellTable, cellId)); int parentIndex = cellTable.findColumnIndex (CSMWorld::Columns::ColumnId_Cell); int index = cellTable.findNestedColumnIndex (parentIndex, CSMWorld::Columns::ColumnId_Interior); createCommand->addNestedValue (parentIndex, index, false); document.getUndoStack().push (createCommand.release()); if (CSVRender::PagedWorldspaceWidget *paged = dynamic_cast (&getWorldspaceWidget())) { CSMWorld::CellSelection selection = paged->getCellSelection(); selection.add (CSMWorld::CellCoordinates::fromId (cellId).first); paged->setCellSelection (selection); } } } else if (CSVRender::PagedWorldspaceWidget *paged = dynamic_cast (&getWorldspaceWidget())) { CSMWorld::CellSelection selection = paged->getCellSelection(); if (!selection.has (CSMWorld::CellCoordinates::fromId (cellId).first)) { // target cell exists, but is not shown std::string mode = CSMPrefs::get()["3D Scene Editing"]["outside-visible-landedit"].toString(); if (mode=="Discard") return false; if (mode=="Show cell and edit") { selection.add (CSMWorld::CellCoordinates::fromId (cellId).first); paged->setCellSelection (selection); } } } if (noLand) { std::string mode = CSMPrefs::get()["3D Scene Editing"]["outside-landedit"].toString(); // target cell does not exist if (mode=="Discard") return false; if (mode=="Create cell and land, then edit") { document.getUndoStack().push (new CSMWorld::CreateCommand (landTable, cellId)); } } return true; } void CSVRender::TerrainTextureMode::dragMoveEvent (QDragMoveEvent *event) { } void CSVRender::TerrainTextureMode::mouseMoveEvent (QMouseEvent *event) { WorldspaceHitResult hit = getWorldspaceWidget().mousePick(event->pos(), getInteractionMask()); if (hit.hit && mBrushDraw) mBrushDraw->update(hit.worldPos, mBrushSize, mBrushShape); if (!hit.hit && mBrushDraw) mBrushDraw->hide(); } std::shared_ptr CSVRender::TerrainTextureMode::getTerrainSelection() { return mTerrainTextureSelection; } void CSVRender::TerrainTextureMode::setBrushSize(int brushSize) { mBrushSize = brushSize; } void CSVRender::TerrainTextureMode::setBrushShape(CSVWidget::BrushShape brushShape) { mBrushShape = brushShape; //Set custom brush shape if (mBrushShape == CSVWidget::BrushShape_Custom && !mTerrainTextureSelection->getTerrainSelection().empty()) { auto terrainSelection = mTerrainTextureSelection->getTerrainSelection(); int selectionCenterX = 0; int selectionCenterY = 0; int selectionAmount = 0; for(auto const& value: terrainSelection) { selectionCenterX += value.first; selectionCenterY += value.second; ++selectionAmount; } if (selectionAmount != 0) { selectionCenterX /= selectionAmount; selectionCenterY /= selectionAmount; } mCustomBrushShape.clear(); for (auto const& value: terrainSelection) mCustomBrushShape.emplace_back(value.first - selectionCenterX, value.second - selectionCenterY); } } void CSVRender::TerrainTextureMode::setBrushTexture(std::string brushTexture) { mBrushTexture = brushTexture; } ================================================ FILE: apps/opencs/view/render/terraintexturemode.hpp ================================================ #ifndef CSV_RENDER_TERRAINTEXTUREMODE_H #define CSV_RENDER_TERRAINTEXTUREMODE_H #include "editmode.hpp" #include #include #include #include #ifndef Q_MOC_RUN #include "../../model/world/data.hpp" #include "../../model/world/land.hpp" #include "../../model/doc/document.hpp" #include "../../model/world/commands.hpp" #include "../../model/world/idtable.hpp" #include "../../model/world/landtexture.hpp" #include "../widget/brushshapes.hpp" #include "brushdraw.hpp" #endif #include "terrainselection.hpp" namespace osg { class Group; } namespace CSVWidget { class SceneToolTextureBrush; } namespace CSVRender { class TerrainTextureMode : public EditMode { Q_OBJECT public: enum InteractionType { InteractionType_PrimaryEdit, InteractionType_PrimarySelect, InteractionType_SecondaryEdit, InteractionType_SecondarySelect, InteractionType_None }; /// \brief Editmode for terrain texture grid TerrainTextureMode(WorldspaceWidget*, osg::Group* parentNode, QWidget* parent = nullptr); void primaryOpenPressed (const WorldspaceHitResult& hit) override; /// \brief Create single command for one-click texture editing void primaryEditPressed (const WorldspaceHitResult& hit) override; /// \brief Open brush settings window void primarySelectPressed(const WorldspaceHitResult&) override; void secondarySelectPressed(const WorldspaceHitResult&) override; void activate(CSVWidget::SceneToolbar*) override; void deactivate(CSVWidget::SceneToolbar*) override; /// \brief Start texture editing command macro bool primaryEditStartDrag (const QPoint& pos) override; bool secondaryEditStartDrag (const QPoint& pos) override; bool primarySelectStartDrag (const QPoint& pos) override; bool secondarySelectStartDrag (const QPoint& pos) override; /// \brief Handle texture edit behavior during dragging void drag (const QPoint& pos, int diffX, int diffY, double speedFactor) override; /// \brief End texture editing command macro void dragCompleted(const QPoint& pos) override; void dragAborted() override; void dragWheel (int diff, double speedFactor) override; void dragMoveEvent (QDragMoveEvent *event) override; void mouseMoveEvent (QMouseEvent *event) override; std::shared_ptr getTerrainSelection(); private: /// \brief Handle brush mechanics, maths regarding worldspace hit etc. void editTerrainTextureGrid (const WorldspaceHitResult& hit); /// \brief Check if global selection coordinate belongs to cell in view bool isInCellSelection(int globalSelectionX, int globalSelectionY); /// \brief Handle brush mechanics for texture selection void selectTerrainTextures (const std::pair& texCoords, unsigned char selectMode, bool dragOperation); /// \brief Push texture edits to command macro void pushEditToCommand (CSMWorld::LandTexturesColumn::DataType& newLandGrid, CSMDoc::Document& document, CSMWorld::IdTable& landTable, std::string cellId); /// \brief Create new land texture record from texture asset void createTexture(std::string textureFileName); /// \brief Create new cell and land if needed bool allowLandTextureEditing(std::string textureFileName); std::string mCellId; std::string mBrushTexture; int mBrushSize; CSVWidget::BrushShape mBrushShape; std::unique_ptr mBrushDraw; std::vector> mCustomBrushShape; CSVWidget::SceneToolTextureBrush *mTextureBrushScenetool; int mDragMode; osg::Group* mParentNode; bool mIsEditing; std::shared_ptr mTerrainTextureSelection; const int cellSize {ESM::Land::REAL_SIZE}; const int landTextureSize {ESM::Land::LAND_TEXTURE_SIZE}; signals: void passBrushTexture(std::string brushTexture); public slots: void handleDropEvent(QDropEvent *event); void setBrushSize(int brushSize); void setBrushShape(CSVWidget::BrushShape brushShape); void setBrushTexture(std::string brushShape); }; } #endif ================================================ FILE: apps/opencs/view/render/unpagedworldspacewidget.cpp ================================================ #include "unpagedworldspacewidget.hpp" #include #include #include #include "../../model/doc/document.hpp" #include "../../model/world/data.hpp" #include "../../model/world/idtable.hpp" #include "../../model/world/tablemimedata.hpp" #include "../widget/scenetooltoggle2.hpp" #include "cameracontroller.hpp" #include "mask.hpp" #include "tagbase.hpp" void CSVRender::UnpagedWorldspaceWidget::update() { const CSMWorld::Record& record = dynamic_cast&> (mCellsModel->getRecord (mCellId)); osg::Vec4f colour = SceneUtil::colourFromRGB(record.get().mAmbi.mAmbient); setDefaultAmbient (colour); bool isInterior = (record.get().mData.mFlags & ESM::Cell::Interior) != 0; bool behaveLikeExterior = (record.get().mData.mFlags & ESM::Cell::QuasiEx) != 0; setExterior(behaveLikeExterior || !isInterior); /// \todo deal with mSunlight and mFog/mForDensity flagAsModified(); } CSVRender::UnpagedWorldspaceWidget::UnpagedWorldspaceWidget (const std::string& cellId, CSMDoc::Document& document, QWidget* parent) : WorldspaceWidget (document, parent), mDocument(document), mCellId (cellId) { mCellsModel = &dynamic_cast ( *document.getData().getTableModel (CSMWorld::UniversalId::Type_Cells)); mReferenceablesModel = &dynamic_cast ( *document.getData().getTableModel (CSMWorld::UniversalId::Type_Referenceables)); connect (mCellsModel, SIGNAL (dataChanged (const QModelIndex&, const QModelIndex&)), this, SLOT (cellDataChanged (const QModelIndex&, const QModelIndex&))); connect (mCellsModel, SIGNAL (rowsAboutToBeRemoved (const QModelIndex&, int, int)), this, SLOT (cellRowsAboutToBeRemoved (const QModelIndex&, int, int))); connect (&document.getData(), SIGNAL (assetTablesChanged ()), this, SLOT (assetTablesChanged ())); update(); mCell.reset (new Cell (document.getData(), mRootNode, mCellId)); } void CSVRender::UnpagedWorldspaceWidget::cellDataChanged (const QModelIndex& topLeft, const QModelIndex& bottomRight) { int index = mCellsModel->findColumnIndex (CSMWorld::Columns::ColumnId_Modification); QModelIndex cellIndex = mCellsModel->getModelIndex (mCellId, index); if (cellIndex.row()>=topLeft.row() && cellIndex.row()<=bottomRight.row()) { if (mCellsModel->data (cellIndex).toInt()==CSMWorld::RecordBase::State_Deleted) { emit closeRequest(); } else { /// \todo possible optimisation: check columns and update only if relevant columns have /// changed update(); } } } void CSVRender::UnpagedWorldspaceWidget::cellRowsAboutToBeRemoved (const QModelIndex& parent, int start, int end) { QModelIndex cellIndex = mCellsModel->getModelIndex (mCellId, 0); if (cellIndex.row()>=start && cellIndex.row()<=end) emit closeRequest(); } void CSVRender::UnpagedWorldspaceWidget::assetTablesChanged() { if (mCell) mCell->reloadAssets(); } bool CSVRender::UnpagedWorldspaceWidget::handleDrop (const std::vector& universalIdData, DropType type) { if (WorldspaceWidget::handleDrop (universalIdData, type)) return true; if (type!=Type_CellsInterior) return false; mCellId = universalIdData.begin()->getId(); mCell.reset (new Cell (getDocument().getData(), mRootNode, mCellId)); mCamPositionSet = false; mOrbitCamControl->reset(); update(); emit cellChanged(*universalIdData.begin()); return true; } void CSVRender::UnpagedWorldspaceWidget::clearSelection (int elementMask) { mCell->setSelection (elementMask, Cell::Selection_Clear); flagAsModified(); } void CSVRender::UnpagedWorldspaceWidget::invertSelection (int elementMask) { mCell->setSelection (elementMask, Cell::Selection_Invert); flagAsModified(); } void CSVRender::UnpagedWorldspaceWidget::selectAll (int elementMask) { mCell->setSelection (elementMask, Cell::Selection_All); flagAsModified(); } void CSVRender::UnpagedWorldspaceWidget::selectAllWithSameParentId (int elementMask) { mCell->selectAllWithSameParentId (elementMask); flagAsModified(); } void CSVRender::UnpagedWorldspaceWidget::selectInsideCube(const osg::Vec3d& pointA, const osg::Vec3d& pointB, DragMode dragMode) { mCell->selectInsideCube (pointA, pointB, dragMode); } void CSVRender::UnpagedWorldspaceWidget::selectWithinDistance(const osg::Vec3d& point, float distance, DragMode dragMode) { mCell->selectWithinDistance (point, distance, dragMode); } std::string CSVRender::UnpagedWorldspaceWidget::getCellId (const osg::Vec3f& point) const { return mCellId; } CSVRender::Cell* CSVRender::UnpagedWorldspaceWidget::getCell(const osg::Vec3d& point) const { return mCell.get(); } CSVRender::Cell* CSVRender::UnpagedWorldspaceWidget::getCell(const CSMWorld::CellCoordinates& coords) const { return mCell.get(); } std::vector > CSVRender::UnpagedWorldspaceWidget::getSelection ( unsigned int elementMask) const { return mCell->getSelection (elementMask); } std::vector > CSVRender::UnpagedWorldspaceWidget::getEdited ( unsigned int elementMask) const { return mCell->getEdited (elementMask); } void CSVRender::UnpagedWorldspaceWidget::setSubMode (int subMode, unsigned int elementMask) { mCell->setSubMode (subMode, elementMask); } void CSVRender::UnpagedWorldspaceWidget::reset (unsigned int elementMask) { mCell->reset (elementMask); } void CSVRender::UnpagedWorldspaceWidget::referenceableDataChanged (const QModelIndex& topLeft, const QModelIndex& bottomRight) { if (mCell.get()) if (mCell.get()->referenceableDataChanged (topLeft, bottomRight)) flagAsModified(); } void CSVRender::UnpagedWorldspaceWidget::referenceableAboutToBeRemoved ( const QModelIndex& parent, int start, int end) { if (mCell.get()) if (mCell.get()->referenceableAboutToBeRemoved (parent, start, end)) flagAsModified(); } void CSVRender::UnpagedWorldspaceWidget::referenceableAdded (const QModelIndex& parent, int start, int end) { if (mCell.get()) { QModelIndex topLeft = mReferenceablesModel->index (start, 0); QModelIndex bottomRight = mReferenceablesModel->index (end, mReferenceablesModel->columnCount()); if (mCell.get()->referenceableDataChanged (topLeft, bottomRight)) flagAsModified(); } } void CSVRender::UnpagedWorldspaceWidget::referenceDataChanged (const QModelIndex& topLeft, const QModelIndex& bottomRight) { if (mCell.get()) if (mCell.get()->referenceDataChanged (topLeft, bottomRight)) flagAsModified(); } void CSVRender::UnpagedWorldspaceWidget::referenceAboutToBeRemoved (const QModelIndex& parent, int start, int end) { if (mCell.get()) if (mCell.get()->referenceAboutToBeRemoved (parent, start, end)) flagAsModified(); } void CSVRender::UnpagedWorldspaceWidget::referenceAdded (const QModelIndex& parent, int start, int end) { if (mCell.get()) if (mCell.get()->referenceAdded (parent, start, end)) flagAsModified(); } void CSVRender::UnpagedWorldspaceWidget::pathgridDataChanged (const QModelIndex& topLeft, const QModelIndex& bottomRight) { const CSMWorld::SubCellCollection& pathgrids = mDocument.getData().getPathgrids(); int rowStart = -1; int rowEnd = -1; if (topLeft.parent().isValid()) { rowStart = topLeft.parent().row(); rowEnd = bottomRight.parent().row(); } else { rowStart = topLeft.row(); rowEnd = bottomRight.row(); } for (int row = rowStart; row <= rowEnd; ++row) { const CSMWorld::Pathgrid& pathgrid = pathgrids.getRecord(row).get(); if (mCellId == pathgrid.mId) { mCell->pathgridModified(); flagAsModified(); return; } } } void CSVRender::UnpagedWorldspaceWidget::pathgridAboutToBeRemoved (const QModelIndex& parent, int start, int end) { const CSMWorld::SubCellCollection& pathgrids = mDocument.getData().getPathgrids(); if (!parent.isValid()) { // Pathgrid going to be deleted for (int row = start; row <= end; ++row) { const CSMWorld::Pathgrid& pathgrid = pathgrids.getRecord(row).get(); if (mCellId == pathgrid.mId) { mCell->pathgridRemoved(); flagAsModified(); return; } } } } void CSVRender::UnpagedWorldspaceWidget::pathgridAdded (const QModelIndex& parent, int start, int end) { const CSMWorld::SubCellCollection& pathgrids = mDocument.getData().getPathgrids(); if (!parent.isValid()) { for (int row = start; row <= end; ++row) { const CSMWorld::Pathgrid& pathgrid = pathgrids.getRecord(row).get(); if (mCellId == pathgrid.mId) { mCell->pathgridModified(); flagAsModified(); return; } } } } void CSVRender::UnpagedWorldspaceWidget::addVisibilitySelectorButtons ( CSVWidget::SceneToolToggle2 *tool) { WorldspaceWidget::addVisibilitySelectorButtons (tool); tool->addButton (Button_Terrain, Mask_Terrain, "Terrain", "", true); tool->addButton (Button_Fog, Mask_Fog, "Fog"); } std::string CSVRender::UnpagedWorldspaceWidget::getStartupInstruction() { osg::Vec3d eye, center, up; mView->getCamera()->getViewMatrixAsLookAt(eye, center, up); osg::Vec3d position = eye; std::ostringstream stream; stream << "player->positionCell " << position.x() << ", " << position.y() << ", " << position.z() << ", 0, \"" << mCellId << "\""; return stream.str(); } CSVRender::WorldspaceWidget::dropRequirments CSVRender::UnpagedWorldspaceWidget::getDropRequirements (CSVRender::WorldspaceWidget::DropType type) const { dropRequirments requirements = WorldspaceWidget::getDropRequirements (type); if (requirements!=ignored) return requirements; switch(type) { case Type_CellsInterior: return canHandle; case Type_CellsExterior: return needPaged; default: return ignored; } } ================================================ FILE: apps/opencs/view/render/unpagedworldspacewidget.hpp ================================================ #ifndef OPENCS_VIEW_UNPAGEDWORLDSPACEWIDGET_H #define OPENCS_VIEW_UNPAGEDWORLDSPACEWIDGET_H #include #include #include "worldspacewidget.hpp" #include "cell.hpp" class QModelIndex; namespace CSMDoc { class Document; } namespace CSMWorld { class IdTable; class CellCoordinates; } namespace CSVRender { class UnpagedWorldspaceWidget : public WorldspaceWidget { Q_OBJECT CSMDoc::Document& mDocument; std::string mCellId; CSMWorld::IdTable *mCellsModel; CSMWorld::IdTable *mReferenceablesModel; std::unique_ptr mCell; void update(); public: UnpagedWorldspaceWidget (const std::string& cellId, CSMDoc::Document& document, QWidget *parent); dropRequirments getDropRequirements(DropType type) const override; /// \return Drop handled? bool handleDrop (const std::vector& data, DropType type) override; /// \param elementMask Elements to be affected by the clear operation void clearSelection (int elementMask) override; /// \param elementMask Elements to be affected by the select operation void invertSelection (int elementMask) override; /// \param elementMask Elements to be affected by the select operation void selectAll (int elementMask) override; // Select everything that references the same ID as at least one of the elements // already selected // /// \param elementMask Elements to be affected by the select operation void selectAllWithSameParentId (int elementMask) override; void selectInsideCube(const osg::Vec3d& pointA, const osg::Vec3d& pointB, DragMode dragMode) override; void selectWithinDistance(const osg::Vec3d& point, float distance, DragMode dragMode) override; std::string getCellId (const osg::Vec3f& point) const override; Cell* getCell(const osg::Vec3d& point) const override; Cell* getCell(const CSMWorld::CellCoordinates& coords) const override; std::vector > getSelection (unsigned int elementMask) const override; std::vector > getEdited (unsigned int elementMask) const override; void setSubMode (int subMode, unsigned int elementMask) override; /// Erase all overrides and restore the visual representation to its true state. void reset (unsigned int elementMask) override; private: void referenceableDataChanged (const QModelIndex& topLeft, const QModelIndex& bottomRight) override; void referenceableAboutToBeRemoved (const QModelIndex& parent, int start, int end) override; void referenceableAdded (const QModelIndex& index, int start, int end) override; void referenceDataChanged (const QModelIndex& topLeft, const QModelIndex& bottomRight) override; void referenceAboutToBeRemoved (const QModelIndex& parent, int start, int end) override; void referenceAdded (const QModelIndex& index, int start, int end) override; void pathgridDataChanged (const QModelIndex& topLeft, const QModelIndex& bottomRight) override; void pathgridAboutToBeRemoved (const QModelIndex& parent, int start, int end) override; void pathgridAdded (const QModelIndex& parent, int start, int end) override; std::string getStartupInstruction() override; protected: void addVisibilitySelectorButtons (CSVWidget::SceneToolToggle2 *tool) override; private slots: void cellDataChanged (const QModelIndex& topLeft, const QModelIndex& bottomRight); void cellRowsAboutToBeRemoved (const QModelIndex& parent, int start, int end); void assetTablesChanged (); signals: void cellChanged(const CSMWorld::UniversalId& id); }; } #endif ================================================ FILE: apps/opencs/view/render/worldspacewidget.cpp ================================================ #include "worldspacewidget.hpp" #include #include #include #include #include #include #include #include #include #include #include "../../model/world/universalid.hpp" #include "../../model/world/idtable.hpp" #include "../../model/prefs/shortcut.hpp" #include "../../model/prefs/state.hpp" #include "../render/orbitcameramode.hpp" #include "../widget/scenetoolmode.hpp" #include "../widget/scenetooltoggle2.hpp" #include "../widget/scenetoolrun.hpp" #include "object.hpp" #include "mask.hpp" #include "instancemode.hpp" #include "pathgridmode.hpp" #include "cameracontroller.hpp" CSVRender::WorldspaceWidget::WorldspaceWidget (CSMDoc::Document& document, QWidget* parent) : SceneWidget (document.getData().getResourceSystem(), parent, Qt::WindowFlags(), false) , mSceneElements(nullptr) , mRun(nullptr) , mDocument(document) , mInteractionMask (0) , mEditMode (nullptr) , mLocked (false) , mDragMode(InteractionType_None) , mDragging (false) , mDragX(0) , mDragY(0) , mSpeedMode(false) , mDragFactor(0) , mDragWheelFactor(0) , mDragShiftFactor(0) , mToolTipPos (-1, -1) , mShowToolTips(false) , mToolTipDelay(0) , mInConstructor(true) { setAcceptDrops(true); QAbstractItemModel *referenceables = document.getData().getTableModel (CSMWorld::UniversalId::Type_Referenceables); connect (referenceables, SIGNAL (dataChanged (const QModelIndex&, const QModelIndex&)), this, SLOT (referenceableDataChanged (const QModelIndex&, const QModelIndex&))); connect (referenceables, SIGNAL (rowsAboutToBeRemoved (const QModelIndex&, int, int)), this, SLOT (referenceableAboutToBeRemoved (const QModelIndex&, int, int))); connect (referenceables, SIGNAL (rowsInserted (const QModelIndex&, int, int)), this, SLOT (referenceableAdded (const QModelIndex&, int, int))); QAbstractItemModel *references = document.getData().getTableModel (CSMWorld::UniversalId::Type_References); connect (references, SIGNAL (dataChanged (const QModelIndex&, const QModelIndex&)), this, SLOT (referenceDataChanged (const QModelIndex&, const QModelIndex&))); connect (references, SIGNAL (rowsAboutToBeRemoved (const QModelIndex&, int, int)), this, SLOT (referenceAboutToBeRemoved (const QModelIndex&, int, int))); connect (references, SIGNAL (rowsInserted (const QModelIndex&, int, int)), this, SLOT (referenceAdded (const QModelIndex&, int, int))); QAbstractItemModel *pathgrids = document.getData().getTableModel (CSMWorld::UniversalId::Type_Pathgrids); connect (pathgrids, SIGNAL (dataChanged (const QModelIndex&, const QModelIndex&)), this, SLOT (pathgridDataChanged (const QModelIndex&, const QModelIndex&))); connect (pathgrids, SIGNAL (rowsAboutToBeRemoved (const QModelIndex&, int, int)), this, SLOT (pathgridAboutToBeRemoved (const QModelIndex&, int, int))); connect (pathgrids, SIGNAL (rowsInserted (const QModelIndex&, int, int)), this, SLOT (pathgridAdded (const QModelIndex&, int, int))); QAbstractItemModel *debugProfiles = document.getData().getTableModel (CSMWorld::UniversalId::Type_DebugProfiles); connect (debugProfiles, SIGNAL (dataChanged (const QModelIndex&, const QModelIndex&)), this, SLOT (debugProfileDataChanged (const QModelIndex&, const QModelIndex&))); connect (debugProfiles, SIGNAL (rowsAboutToBeRemoved (const QModelIndex&, int, int)), this, SLOT (debugProfileAboutToBeRemoved (const QModelIndex&, int, int))); mToolTipDelayTimer.setSingleShot (true); connect (&mToolTipDelayTimer, SIGNAL (timeout()), this, SLOT (showToolTip())); CSMPrefs::get()["3D Scene Input"].update(); CSMPrefs::get()["Tooltips"].update(); // Shortcuts CSMPrefs::Shortcut* primaryEditShortcut = new CSMPrefs::Shortcut("scene-edit-primary", "scene-speed-modifier", CSMPrefs::Shortcut::SM_Detach, this); CSMPrefs::Shortcut* primaryOpenShortcut = new CSMPrefs::Shortcut("scene-open-primary", this); connect(primaryOpenShortcut, SIGNAL(activated(bool)), this, SLOT(primaryOpen(bool))); connect(primaryEditShortcut, SIGNAL(activated(bool)), this, SLOT(primaryEdit(bool))); connect(primaryEditShortcut, SIGNAL(secondary(bool)), this, SLOT(speedMode(bool))); CSMPrefs::Shortcut* secondaryEditShortcut = new CSMPrefs::Shortcut("scene-edit-secondary", this); connect(secondaryEditShortcut, SIGNAL(activated(bool)), this, SLOT(secondaryEdit(bool))); CSMPrefs::Shortcut* primarySelectShortcut = new CSMPrefs::Shortcut("scene-select-primary", this); connect(primarySelectShortcut, SIGNAL(activated(bool)), this, SLOT(primarySelect(bool))); CSMPrefs::Shortcut* secondarySelectShortcut = new CSMPrefs::Shortcut("scene-select-secondary", this); connect(secondarySelectShortcut, SIGNAL(activated(bool)), this, SLOT(secondarySelect(bool))); CSMPrefs::Shortcut* abortShortcut = new CSMPrefs::Shortcut("scene-edit-abort", this); connect(abortShortcut, SIGNAL(activated()), this, SLOT(abortDrag())); mInConstructor = false; } CSVRender::WorldspaceWidget::~WorldspaceWidget () { } void CSVRender::WorldspaceWidget::settingChanged (const CSMPrefs::Setting *setting) { if (*setting=="3D Scene Input/drag-factor") mDragFactor = setting->toDouble(); else if (*setting=="3D Scene Input/drag-wheel-factor") mDragWheelFactor = setting->toDouble(); else if (*setting=="3D Scene Input/drag-shift-factor") mDragShiftFactor = setting->toDouble(); else if (*setting=="Rendering/object-marker-alpha" && !mInConstructor) { float alpha = setting->toDouble(); // getSelection is virtual, thus this can not be called from the constructor auto selection = getSelection(Mask_Reference); for (osg::ref_ptr tag : selection) { if (auto objTag = dynamic_cast(tag.get())) objTag->mObject->setMarkerTransparency(alpha); } } else if (*setting=="Tooltips/scene-delay") mToolTipDelay = setting->toInt(); else if (*setting=="Tooltips/scene") mShowToolTips = setting->isTrue(); else SceneWidget::settingChanged(setting); } void CSVRender::WorldspaceWidget::useViewHint (const std::string& hint) {} void CSVRender::WorldspaceWidget::selectDefaultNavigationMode() { selectNavigationMode("1st"); } void CSVRender::WorldspaceWidget::centerOrbitCameraOnSelection() { std::vector > selection = getSelection(~0u); for (std::vector >::iterator it = selection.begin(); it!=selection.end(); ++it) { if (CSVRender::ObjectTag *objectTag = dynamic_cast (it->get())) { mOrbitCamControl->setCenter(objectTag->mObject->getPosition().asVec3()); } } } CSVWidget::SceneToolMode *CSVRender::WorldspaceWidget::makeNavigationSelector ( CSVWidget::SceneToolbar *parent) { CSVWidget::SceneToolMode *tool = new CSVWidget::SceneToolMode (parent, "Camera Mode"); /// \todo replace icons /// \todo consider user-defined button-mapping tool->addButton (":scenetoolbar/1st-person", "1st", "First Person" "
  • Camera is held upright
  • " "
  • Mouse-Look while holding {scene-navi-primary}
  • " "
  • Movement keys: {free-forward}(forward), {free-left}(left), {free-backward}(back), {free-right}(right)
  • " "
  • Strafing (also vertically) by holding {scene-navi-secondary}
  • " "
  • Mouse wheel moves the camera forward/backward
  • " "
  • Hold {scene-speed-modifier} to speed up movement
  • " "
"); tool->addButton (":scenetoolbar/free-camera", "free", "Free Camera" "
  • Mouse-Look while holding {scene-navi-primary}
  • " "
  • Movement keys: {free-forward}(forward), {free-left}(left), {free-backward}(back), {free-right}(right)
  • " "
  • Roll camera with {free-roll-left} and {free-roll-right} keys
  • " "
  • Strafing (also vertically) by holding {scene-navi-secondary}
  • " "
  • Mouse wheel moves the camera forward/backward
  • " "
  • Hold {free-forward:mod} to speed up movement
  • " "
"); tool->addButton( new CSVRender::OrbitCameraMode(this, QIcon(":scenetoolbar/orbiting-camera"), "Orbiting Camera" "
  • Always facing the centre point
  • " "
  • Rotate around the centre point via {orbit-up}, {orbit-left}, {orbit-down}, {orbit-right} or by moving " "the mouse while holding {scene-navi-primary}
  • " "
  • Roll camera with {orbit-roll-left} and {orbit-roll-right} keys
  • " "
  • Strafing (also vertically) by holding {scene-navi-secondary} (includes relocation of the centre point)
  • " "
  • Mouse wheel moves camera away or towards centre point but can not pass through it
  • " "
  • Hold {scene-speed-modifier} to speed up movement
  • " "
", tool), "orbit"); connect (tool, SIGNAL (modeChanged (const std::string&)), this, SLOT (selectNavigationMode (const std::string&))); return tool; } CSVWidget::SceneToolToggle2 *CSVRender::WorldspaceWidget::makeSceneVisibilitySelector (CSVWidget::SceneToolbar *parent) { mSceneElements = new CSVWidget::SceneToolToggle2 (parent, "Scene Element Visibility", ":scenetoolbar/scene-view-c", ":scenetoolbar/scene-view-"); addVisibilitySelectorButtons (mSceneElements); mSceneElements->setSelectionMask (0xffffffff); connect (mSceneElements, SIGNAL (selectionChanged()), this, SLOT (elementSelectionChanged())); return mSceneElements; } CSVWidget::SceneToolRun *CSVRender::WorldspaceWidget::makeRunTool ( CSVWidget::SceneToolbar *parent) { CSMWorld::IdTable& debugProfiles = dynamic_cast ( *mDocument.getData().getTableModel (CSMWorld::UniversalId::Type_DebugProfiles)); std::vector profiles; int idColumn = debugProfiles.findColumnIndex (CSMWorld::Columns::ColumnId_Id); int stateColumn = debugProfiles.findColumnIndex (CSMWorld::Columns::ColumnId_Modification); int defaultColumn = debugProfiles.findColumnIndex ( CSMWorld::Columns::ColumnId_DefaultProfile); int size = debugProfiles.rowCount(); for (int i=0; i& data) { DropType output = Type_Other; for (std::vector::const_iterator iter (data.begin()); iter!=data.end(); ++iter) { DropType type = Type_Other; if (iter->getType()==CSMWorld::UniversalId::Type_Cell || iter->getType()==CSMWorld::UniversalId::Type_Cell_Missing) { type = iter->getId().substr (0, 1)=="#" ? Type_CellsExterior : Type_CellsInterior; } else if (iter->getType()==CSMWorld::UniversalId::Type_DebugProfile) type = Type_DebugProfile; if (iter==data.begin()) output = type; else if (output!=type) // mixed types -> ignore return Type_Other; } return output; } CSVRender::WorldspaceWidget::dropRequirments CSVRender::WorldspaceWidget::getDropRequirements (DropType type) const { if (type==Type_DebugProfile) return canHandle; return ignored; } bool CSVRender::WorldspaceWidget::handleDrop (const std::vector& universalIdData, DropType type) { if (type==Type_DebugProfile) { if (mRun) { for (std::vector::const_iterator iter (universalIdData.begin()); iter!=universalIdData.end(); ++iter) mRun->addProfile (iter->getId()); } return true; } return false; } unsigned int CSVRender::WorldspaceWidget::getVisibilityMask() const { return mSceneElements->getSelectionMask(); } void CSVRender::WorldspaceWidget::setInteractionMask (unsigned int mask) { mInteractionMask = mask | Mask_CellMarker | Mask_CellArrow; } unsigned int CSVRender::WorldspaceWidget::getInteractionMask() const { return mInteractionMask & getVisibilityMask(); } void CSVRender::WorldspaceWidget::setEditLock (bool locked) { dynamic_cast (*mEditMode->getCurrent()).setEditLock (locked); } void CSVRender::WorldspaceWidget::addVisibilitySelectorButtons ( CSVWidget::SceneToolToggle2 *tool) { tool->addButton (Button_Reference, Mask_Reference, "Instances"); tool->addButton (Button_Water, Mask_Water, "Water"); tool->addButton (Button_Pathgrid, Mask_Pathgrid, "Pathgrid"); } void CSVRender::WorldspaceWidget::addEditModeSelectorButtons (CSVWidget::SceneToolMode *tool) { /// \todo replace EditMode with suitable subclasses tool->addButton (new InstanceMode (this, mRootNode, tool), "object"); tool->addButton (new PathgridMode (this, tool), "pathgrid"); } CSMDoc::Document& CSVRender::WorldspaceWidget::getDocument() { return mDocument; } CSVRender::WorldspaceHitResult CSVRender::WorldspaceWidget::mousePick (const QPoint& localPos, unsigned int interactionMask) const { // (0,0) is considered the lower left corner of an OpenGL window int x = localPos.x(); int y = height() - localPos.y(); // Convert from screen space to world space osg::Matrixd wpvMat; wpvMat.preMult (mView->getCamera()->getViewport()->computeWindowMatrix()); wpvMat.preMult (mView->getCamera()->getProjectionMatrix()); wpvMat.preMult (mView->getCamera()->getViewMatrix()); wpvMat = osg::Matrixd::inverse (wpvMat); osg::Vec3d start = wpvMat.preMult (osg::Vec3d(x, y, 0)); osg::Vec3d end = wpvMat.preMult (osg::Vec3d(x, y, 1)); osg::Vec3d direction = end - start; // Get intersection osg::ref_ptr intersector (new osgUtil::LineSegmentIntersector( osgUtil::Intersector::MODEL, start, end)); intersector->setIntersectionLimit(osgUtil::LineSegmentIntersector::NO_LIMIT); osgUtil::IntersectionVisitor visitor(intersector); visitor.setTraversalMask(interactionMask); mView->getCamera()->accept(visitor); // Get relevant data for (osgUtil::LineSegmentIntersector::Intersections::iterator it = intersector->getIntersections().begin(); it != intersector->getIntersections().end(); ++it) { osgUtil::LineSegmentIntersector::Intersection intersection = *it; // reject back-facing polygons if (direction * intersection.getWorldIntersectNormal() > 0) { continue; } for (std::vector::iterator nodeIter = intersection.nodePath.begin(); nodeIter != intersection.nodePath.end(); ++nodeIter) { osg::Node* node = *nodeIter; if (osg::ref_ptr tag = dynamic_cast(node->getUserData())) { WorldspaceHitResult hit = { true, tag, 0, 0, 0, intersection.getWorldIntersectPoint() }; if (intersection.indexList.size() >= 3) { hit.index0 = intersection.indexList[0]; hit.index1 = intersection.indexList[1]; hit.index2 = intersection.indexList[2]; } return hit; } } // Something untagged, probably terrain WorldspaceHitResult hit = { true, nullptr, 0, 0, 0, intersection.getWorldIntersectPoint() }; if (intersection.indexList.size() >= 3) { hit.index0 = intersection.indexList[0]; hit.index1 = intersection.indexList[1]; hit.index2 = intersection.indexList[2]; } return hit; } // Default placement direction.normalize(); direction *= CSMPrefs::get()["3D Scene Editing"]["distance"].toInt(); WorldspaceHitResult hit = { false, nullptr, 0, 0, 0, start + direction }; return hit; } CSVRender::EditMode *CSVRender::WorldspaceWidget::getEditMode() { return dynamic_cast (mEditMode->getCurrent()); } void CSVRender::WorldspaceWidget::abortDrag() { if (mDragging) { EditMode& editMode = dynamic_cast (*mEditMode->getCurrent()); editMode.dragAborted(); mDragging = false; mDragMode = InteractionType_None; } } void CSVRender::WorldspaceWidget::dragEnterEvent (QDragEnterEvent* event) { const CSMWorld::TableMimeData* mime = dynamic_cast (event->mimeData()); if (!mime) // May happen when non-records (e.g. plain text) are dragged and dropped return; if (mime->fromDocument (mDocument)) { if (mime->holdsType (CSMWorld::UniversalId::Type_Cell) || mime->holdsType (CSMWorld::UniversalId::Type_Cell_Missing) || mime->holdsType (CSMWorld::UniversalId::Type_DebugProfile)) { // These drops are handled through the subview object. event->accept(); } else dynamic_cast (*mEditMode->getCurrent()).dragEnterEvent (event); } } void CSVRender::WorldspaceWidget::dragMoveEvent(QDragMoveEvent *event) { const CSMWorld::TableMimeData* mime = dynamic_cast (event->mimeData()); if (!mime) // May happen when non-records (e.g. plain text) are dragged and dropped return; if (mime->fromDocument (mDocument)) { if (mime->holdsType (CSMWorld::UniversalId::Type_Cell) || mime->holdsType (CSMWorld::UniversalId::Type_Cell_Missing) || mime->holdsType (CSMWorld::UniversalId::Type_DebugProfile)) { // These drops are handled through the subview object. event->accept(); } else dynamic_cast (*mEditMode->getCurrent()).dragMoveEvent (event); } } void CSVRender::WorldspaceWidget::dropEvent (QDropEvent* event) { const CSMWorld::TableMimeData* mime = dynamic_cast (event->mimeData()); if (!mime) // May happen when non-records (e.g. plain text) are dragged and dropped return; if (mime->fromDocument (mDocument)) { if (mime->holdsType (CSMWorld::UniversalId::Type_Cell) || mime->holdsType (CSMWorld::UniversalId::Type_Cell_Missing) || mime->holdsType (CSMWorld::UniversalId::Type_DebugProfile)) { emit dataDropped(mime->getData()); } else dynamic_cast (*mEditMode->getCurrent()).dropEvent (event); } } void CSVRender::WorldspaceWidget::runRequest (const std::string& profile) { mDocument.startRunning (profile, getStartupInstruction()); } void CSVRender::WorldspaceWidget::debugProfileDataChanged (const QModelIndex& topLeft, const QModelIndex& bottomRight) { if (!mRun) return; CSMWorld::IdTable& debugProfiles = dynamic_cast ( *mDocument.getData().getTableModel (CSMWorld::UniversalId::Type_DebugProfiles)); int idColumn = debugProfiles.findColumnIndex (CSMWorld::Columns::ColumnId_Id); int stateColumn = debugProfiles.findColumnIndex (CSMWorld::Columns::ColumnId_Modification); for (int i=topLeft.row(); i<=bottomRight.row(); ++i) { int state = debugProfiles.data (debugProfiles.index (i, stateColumn)).toInt(); // As of version 0.33 this case can not happen because debug profiles exist only in // project or session scope, which means they will never be in deleted state. But we // are adding the code for the sake of completeness and to avoid surprises if debug // profile ever get extended to content scope. if (state==CSMWorld::RecordBase::State_Deleted) mRun->removeProfile (debugProfiles.data ( debugProfiles.index (i, idColumn)).toString().toUtf8().constData()); } } void CSVRender::WorldspaceWidget::debugProfileAboutToBeRemoved (const QModelIndex& parent, int start, int end) { if (parent.isValid()) return; if (!mRun) return; CSMWorld::IdTable& debugProfiles = dynamic_cast ( *mDocument.getData().getTableModel (CSMWorld::UniversalId::Type_DebugProfiles)); int idColumn = debugProfiles.findColumnIndex (CSMWorld::Columns::ColumnId_Id); for (int i=start; i<=end; ++i) { mRun->removeProfile (debugProfiles.data ( debugProfiles.index (i, idColumn)).toString().toUtf8().constData()); } } void CSVRender::WorldspaceWidget::editModeChanged (const std::string& id) { dynamic_cast (*mEditMode->getCurrent()).setEditLock (mLocked); mDragging = false; mDragMode = InteractionType_None; } void CSVRender::WorldspaceWidget::showToolTip() { if (mShowToolTips) { QPoint pos = QCursor::pos(); WorldspaceHitResult hit = mousePick (mapFromGlobal (pos), getInteractionMask()); if (hit.tag) { bool hideBasics = CSMPrefs::get()["Tooltips"]["scene-hide-basic"].isTrue(); QToolTip::showText (pos, hit.tag->getToolTip (hideBasics), this); } } } void CSVRender::WorldspaceWidget::elementSelectionChanged() { setVisibilityMask (getVisibilityMask()); flagAsModified(); updateOverlay(); } void CSVRender::WorldspaceWidget::updateOverlay() { } void CSVRender::WorldspaceWidget::mouseMoveEvent (QMouseEvent *event) { dynamic_cast (*mEditMode->getCurrent()).mouseMoveEvent (event); if (mDragging) { int diffX = event->x() - mDragX; int diffY = (height() - event->y()) - mDragY; mDragX = event->x(); mDragY = height() - event->y(); double factor = mDragFactor; if (mSpeedMode) factor *= mDragShiftFactor; EditMode& editMode = dynamic_cast (*mEditMode->getCurrent()); editMode.drag (event->pos(), diffX, diffY, factor); } else if (mDragMode != InteractionType_None) { EditMode& editMode = dynamic_cast (*mEditMode->getCurrent()); if (mDragMode == InteractionType_PrimaryEdit) mDragging = editMode.primaryEditStartDrag (event->pos()); else if (mDragMode == InteractionType_SecondaryEdit) mDragging = editMode.secondaryEditStartDrag (event->pos()); else if (mDragMode == InteractionType_PrimarySelect) mDragging = editMode.primarySelectStartDrag (event->pos()); else if (mDragMode == InteractionType_SecondarySelect) mDragging = editMode.secondarySelectStartDrag (event->pos()); if (mDragging) { mDragX = event->localPos().x(); mDragY = height() - event->localPos().y(); } } else { if (event->globalPos()!=mToolTipPos) { mToolTipPos = event->globalPos(); if (mShowToolTips) { QToolTip::hideText(); mToolTipDelayTimer.start (mToolTipDelay); } } SceneWidget::mouseMoveEvent(event); } } void CSVRender::WorldspaceWidget::wheelEvent (QWheelEvent *event) { if (mDragging) { double factor = mDragWheelFactor; if (mSpeedMode) factor *= mDragShiftFactor; EditMode& editMode = dynamic_cast (*mEditMode->getCurrent()); editMode.dragWheel (event->angleDelta().y(), factor); } else SceneWidget::wheelEvent(event); } void CSVRender::WorldspaceWidget::handleInteractionPress (const WorldspaceHitResult& hit, InteractionType type) { EditMode& editMode = dynamic_cast (*mEditMode->getCurrent()); if (type == InteractionType_PrimaryEdit) editMode.primaryEditPressed (hit); else if (type == InteractionType_SecondaryEdit) editMode.secondaryEditPressed (hit); else if (type == InteractionType_PrimarySelect) editMode.primarySelectPressed (hit); else if (type == InteractionType_SecondarySelect) editMode.secondarySelectPressed (hit); else if (type == InteractionType_PrimaryOpen) editMode.primaryOpenPressed (hit); } void CSVRender::WorldspaceWidget::primaryOpen(bool activate) { handleInteraction(InteractionType_PrimaryOpen, activate); } void CSVRender::WorldspaceWidget::primaryEdit(bool activate) { handleInteraction(InteractionType_PrimaryEdit, activate); } void CSVRender::WorldspaceWidget::secondaryEdit(bool activate) { handleInteraction(InteractionType_SecondaryEdit, activate); } void CSVRender::WorldspaceWidget::primarySelect(bool activate) { handleInteraction(InteractionType_PrimarySelect, activate); } void CSVRender::WorldspaceWidget::secondarySelect(bool activate) { handleInteraction(InteractionType_SecondarySelect, activate); } void CSVRender::WorldspaceWidget::speedMode(bool activate) { mSpeedMode = activate; } void CSVRender::WorldspaceWidget::handleInteraction(InteractionType type, bool activate) { if (activate) { if (!mDragging) mDragMode = type; } else { mDragMode = InteractionType_None; if (mDragging) { EditMode* editMode = getEditMode(); editMode->dragCompleted(mapFromGlobal(QCursor::pos())); mDragging = false; } else { WorldspaceHitResult hit = mousePick(mapFromGlobal(QCursor::pos()), getInteractionMask()); handleInteractionPress(hit, type); } } } ================================================ FILE: apps/opencs/view/render/worldspacewidget.hpp ================================================ #ifndef OPENCS_VIEW_WORLDSPACEWIDGET_H #define OPENCS_VIEW_WORLDSPACEWIDGET_H #include #include #include "../../model/doc/document.hpp" #include "../../model/world/tablemimedata.hpp" #include "instancedragmodes.hpp" #include "scenewidget.hpp" #include "mask.hpp" namespace CSMPrefs { class Setting; } namespace CSMWorld { class CellCoordinates; class UniversalId; } namespace CSVWidget { class SceneToolMode; class SceneToolToggle2; class SceneToolbar; class SceneToolRun; } namespace CSVRender { class TagBase; class Cell; class CellArrow; class EditMode; struct WorldspaceHitResult { bool hit; osg::ref_ptr tag; unsigned int index0, index1, index2; // indices of mesh vertices osg::Vec3d worldPos; }; class WorldspaceWidget : public SceneWidget { Q_OBJECT CSVWidget::SceneToolToggle2 *mSceneElements; CSVWidget::SceneToolRun *mRun; CSMDoc::Document& mDocument; unsigned int mInteractionMask; CSVWidget::SceneToolMode *mEditMode; bool mLocked; int mDragMode; bool mDragging; int mDragX; int mDragY; bool mSpeedMode; double mDragFactor; double mDragWheelFactor; double mDragShiftFactor; QTimer mToolTipDelayTimer; QPoint mToolTipPos; bool mShowToolTips; int mToolTipDelay; bool mInConstructor; public: enum DropType { Type_CellsInterior, Type_CellsExterior, Type_Other, Type_DebugProfile }; enum dropRequirments { canHandle, needPaged, needUnpaged, ignored //either mixed cells, or not cells }; enum InteractionType { InteractionType_PrimaryEdit, InteractionType_PrimarySelect, InteractionType_SecondaryEdit, InteractionType_SecondarySelect, InteractionType_PrimaryOpen, InteractionType_None }; WorldspaceWidget (CSMDoc::Document& document, QWidget *parent = nullptr); ~WorldspaceWidget (); CSVWidget::SceneToolMode *makeNavigationSelector (CSVWidget::SceneToolbar *parent); ///< \attention The created tool is not added to the toolbar (via addTool). Doing that /// is the responsibility of the calling function. /// \attention The created tool is not added to the toolbar (via addTool). Doing /// that is the responsibility of the calling function. CSVWidget::SceneToolToggle2 *makeSceneVisibilitySelector ( CSVWidget::SceneToolbar *parent); /// \attention The created tool is not added to the toolbar (via addTool). Doing /// that is the responsibility of the calling function. CSVWidget::SceneToolRun *makeRunTool (CSVWidget::SceneToolbar *parent); /// \attention The created tool is not added to the toolbar (via addTool). Doing /// that is the responsibility of the calling function. CSVWidget::SceneToolMode *makeEditModeSelector (CSVWidget::SceneToolbar *parent); void selectDefaultNavigationMode(); void centerOrbitCameraOnSelection(); static DropType getDropType(const std::vector& data); virtual dropRequirments getDropRequirements(DropType type) const; virtual void useViewHint (const std::string& hint); ///< Default-implementation: ignored. /// \return Drop handled? virtual bool handleDrop (const std::vector& data, DropType type); virtual unsigned int getVisibilityMask() const; /// \note This function will implicitly add elements that are independent of the /// selected edit mode. virtual void setInteractionMask (unsigned int mask); /// \note This function will only return those elements that are both visible and /// marked for interaction. unsigned int getInteractionMask() const; virtual void setEditLock (bool locked); CSMDoc::Document& getDocument(); /// \param elementMask Elements to be affected by the clear operation virtual void clearSelection (int elementMask) = 0; /// \param elementMask Elements to be affected by the select operation virtual void invertSelection (int elementMask) = 0; /// \param elementMask Elements to be affected by the select operation virtual void selectAll (int elementMask) = 0; // Select everything that references the same ID as at least one of the elements // already selected // /// \param elementMask Elements to be affected by the select operation virtual void selectAllWithSameParentId (int elementMask) = 0; virtual void selectInsideCube(const osg::Vec3d& pointA, const osg::Vec3d& pointB, DragMode dragMode) = 0; virtual void selectWithinDistance(const osg::Vec3d& point, float distance, DragMode dragMode) = 0; /// Return the next intersection with scene elements matched by /// \a interactionMask based on \a localPos and the camera vector. /// If there is no such intersection, instead a point "in front" of \a localPos will be /// returned. WorldspaceHitResult mousePick (const QPoint& localPos, unsigned int interactionMask) const; virtual std::string getCellId (const osg::Vec3f& point) const = 0; /// \note Returns the cell if it exists, otherwise a null pointer virtual Cell* getCell(const osg::Vec3d& point) const = 0; virtual Cell* getCell(const CSMWorld::CellCoordinates& coords) const = 0; virtual std::vector > getSelection (unsigned int elementMask) const = 0; virtual std::vector > getEdited (unsigned int elementMask) const = 0; virtual void setSubMode (int subMode, unsigned int elementMask) = 0; /// Erase all overrides and restore the visual representation to its true state. virtual void reset (unsigned int elementMask) = 0; EditMode *getEditMode(); protected: /// Visual elements in a scene /// @note do not change the enumeration values, they are used in pre-existing button file names! enum ButtonId { Button_Reference = 0x1, Button_Pathgrid = 0x2, Button_Water = 0x4, Button_Fog = 0x8, Button_Terrain = 0x10 }; virtual void addVisibilitySelectorButtons (CSVWidget::SceneToolToggle2 *tool); virtual void addEditModeSelectorButtons (CSVWidget::SceneToolMode *tool); virtual void updateOverlay(); void mouseMoveEvent (QMouseEvent *event) override; void wheelEvent (QWheelEvent *event) override; virtual void handleInteractionPress (const WorldspaceHitResult& hit, InteractionType type); void settingChanged (const CSMPrefs::Setting *setting) override; bool getSpeedMode(); private: void dragEnterEvent(QDragEnterEvent *event) override; void dropEvent(QDropEvent* event) override; void dragMoveEvent(QDragMoveEvent *event) override; virtual std::string getStartupInstruction() = 0; void handleInteraction(InteractionType type, bool activate); public slots: /// \note Drags will be automatically aborted when the aborting is triggered /// (either explicitly or implicitly) from within this class. This function only /// needs to be called, when the drag abort is triggered externally (e.g. from /// an edit mode). void abortDrag(); private slots: virtual void referenceableDataChanged (const QModelIndex& topLeft, const QModelIndex& bottomRight) = 0; virtual void referenceableAboutToBeRemoved (const QModelIndex& parent, int start, int end) = 0; virtual void referenceableAdded (const QModelIndex& index, int start, int end) = 0; virtual void referenceDataChanged (const QModelIndex& topLeft, const QModelIndex& bottomRight) = 0; virtual void referenceAboutToBeRemoved (const QModelIndex& parent, int start, int end) = 0; virtual void referenceAdded (const QModelIndex& index, int start, int end) = 0; virtual void pathgridDataChanged (const QModelIndex& topLeft, const QModelIndex& bottomRight) = 0; virtual void pathgridAboutToBeRemoved (const QModelIndex& parent, int start, int end) = 0; virtual void pathgridAdded (const QModelIndex& parent, int start, int end) = 0; virtual void runRequest (const std::string& profile); void debugProfileDataChanged (const QModelIndex& topLeft, const QModelIndex& bottomRight); void debugProfileAboutToBeRemoved (const QModelIndex& parent, int start, int end); void editModeChanged (const std::string& id); void showToolTip(); void primaryOpen(bool activate); void primaryEdit(bool activate); void secondaryEdit(bool activate); void primarySelect(bool activate); void secondarySelect(bool activate); void speedMode(bool activate); protected slots: void elementSelectionChanged(); signals: void closeRequest(); void dataDropped(const std::vector& data); void requestFocus (const std::string& id); friend class MouseState; }; } #endif ================================================ FILE: apps/opencs/view/tools/merge.cpp ================================================ #include "merge.hpp" #include #include #include #include #include #include #include #include "../../model/doc/document.hpp" #include "../../model/doc/documentmanager.hpp" #include "../doc/filewidget.hpp" #include "../doc/adjusterwidget.hpp" void CSVTools::Merge::keyPressEvent (QKeyEvent *event) { if (event->key()==Qt::Key_Escape) { event->accept(); cancel(); } else QWidget::keyPressEvent (event); } CSVTools::Merge::Merge (CSMDoc::DocumentManager& documentManager, QWidget *parent) : QWidget (parent), mDocument (nullptr), mDocumentManager (documentManager) { setWindowTitle ("Merge Content Files into a new Game File"); QVBoxLayout *mainLayout = new QVBoxLayout; setLayout (mainLayout); QSplitter *splitter = new QSplitter (Qt::Horizontal, this); mainLayout->addWidget (splitter, 1); // left panel (files to be merged) QWidget *left = new QWidget (this); left->setContentsMargins (0, 0, 0, 0); splitter->addWidget (left); QVBoxLayout *leftLayout = new QVBoxLayout; left->setLayout (leftLayout); leftLayout->addWidget (new QLabel ("Files to be merged", this)); mFiles = new QListWidget (this); leftLayout->addWidget (mFiles, 1); // right panel (new game file) QWidget *right = new QWidget (this); right->setContentsMargins (0, 0, 0, 0); splitter->addWidget (right); QVBoxLayout *rightLayout = new QVBoxLayout; rightLayout->setAlignment (Qt::AlignTop); right->setLayout (rightLayout); rightLayout->addWidget (new QLabel ("New game file", this)); mNewFile = new CSVDoc::FileWidget (this); mNewFile->setType (false); mNewFile->extensionLabelIsVisible (true); rightLayout->addWidget (mNewFile); mAdjuster = new CSVDoc::AdjusterWidget (this); rightLayout->addWidget (mAdjuster); connect (mNewFile, SIGNAL (nameChanged (const QString&, bool)), mAdjuster, SLOT (setName (const QString&, bool))); connect (mAdjuster, SIGNAL (stateChanged (bool)), this, SLOT (stateChanged (bool))); // buttons QDialogButtonBox *buttons = new QDialogButtonBox (QDialogButtonBox::Cancel, Qt::Horizontal, this); connect (buttons->button (QDialogButtonBox::Cancel), SIGNAL (clicked()), this, SLOT (cancel())); mOkay = new QPushButton ("Merge", this); connect (mOkay, SIGNAL (clicked()), this, SLOT (accept())); mOkay->setDefault (true); buttons->addButton (mOkay, QDialogButtonBox::AcceptRole); mainLayout->addWidget (buttons); } void CSVTools::Merge::configure (CSMDoc::Document *document) { mDocument = document; mNewFile->setName (""); // content files while (mFiles->count()) delete mFiles->takeItem (0); std::vector files = document->getContentFiles(); for (std::vector::const_iterator iter (files.begin()); iter!=files.end(); ++iter) mFiles->addItem (QString::fromUtf8 (iter->filename().string().c_str())); } void CSVTools::Merge::setLocalData (const boost::filesystem::path& localData) { mAdjuster->setLocalData (localData); } CSMDoc::Document *CSVTools::Merge::getDocument() const { return mDocument; } void CSVTools::Merge::cancel() { mDocument = nullptr; hide(); } void CSVTools::Merge::accept() { if ((mDocument->getState() & CSMDoc::State_Merging)==0) { std::vector< boost::filesystem::path > files (1, mAdjuster->getPath()); std::unique_ptr target ( mDocumentManager.makeDocument (files, files[0], true)); mDocument->runMerge (std::move(target)); hide(); } } void CSVTools::Merge::stateChanged (bool valid) { mOkay->setEnabled (valid); } ================================================ FILE: apps/opencs/view/tools/merge.hpp ================================================ #ifndef CSV_TOOLS_REPORTTABLE_H #define CSV_TOOLS_REPORTTABLE_H #include #ifndef Q_MOC_RUN #include #endif class QPushButton; class QListWidget; namespace CSMDoc { class Document; class DocumentManager; } namespace CSVDoc { class FileWidget; class AdjusterWidget; } namespace CSVTools { class Merge : public QWidget { Q_OBJECT CSMDoc::Document *mDocument; QPushButton *mOkay; QListWidget *mFiles; CSVDoc::FileWidget *mNewFile; CSVDoc::AdjusterWidget *mAdjuster; CSMDoc::DocumentManager& mDocumentManager; void keyPressEvent (QKeyEvent *event) override; public: Merge (CSMDoc::DocumentManager& documentManager, QWidget *parent = nullptr); /// Configure dialogue for a new merge void configure (CSMDoc::Document *document); void setLocalData (const boost::filesystem::path& localData); CSMDoc::Document *getDocument() const; public slots: void cancel(); private slots: void accept(); void stateChanged (bool valid); }; } #endif ================================================ FILE: apps/opencs/view/tools/reportsubview.cpp ================================================ #include "reportsubview.hpp" #include "reporttable.hpp" CSVTools::ReportSubView::ReportSubView (const CSMWorld::UniversalId& id, CSMDoc::Document& document) : CSVDoc::SubView (id), mDocument (document), mRefreshState (0) { if (id.getType()==CSMWorld::UniversalId::Type_VerificationResults) mRefreshState = CSMDoc::State_Verifying; setWidget (mTable = new ReportTable (document, id, false, mRefreshState, this)); connect (mTable, SIGNAL (editRequest (const CSMWorld::UniversalId&, const std::string&)), SIGNAL (focusId (const CSMWorld::UniversalId&, const std::string&))); if (mRefreshState==CSMDoc::State_Verifying) { connect (mTable, SIGNAL (refreshRequest()), this, SLOT (refreshRequest())); connect (&document, SIGNAL (stateChanged (int, CSMDoc::Document *)), mTable, SLOT (stateChanged (int, CSMDoc::Document *))); } } void CSVTools::ReportSubView::setEditLock (bool locked) { // ignored. We don't change document state anyway. } void CSVTools::ReportSubView::refreshRequest() { if (!(mDocument.getState() & mRefreshState)) { if (mRefreshState==CSMDoc::State_Verifying) { mTable->clear(); mDocument.verify (getUniversalId()); } } } ================================================ FILE: apps/opencs/view/tools/reportsubview.hpp ================================================ #ifndef CSV_TOOLS_REPORTSUBVIEW_H #define CSV_TOOLS_REPORTSUBVIEW_H #include "../doc/subview.hpp" class QTableView; class QModelIndex; namespace CSMDoc { class Document; } namespace CSVTools { class ReportTable; class ReportSubView : public CSVDoc::SubView { Q_OBJECT ReportTable *mTable; CSMDoc::Document& mDocument; int mRefreshState; public: ReportSubView (const CSMWorld::UniversalId& id, CSMDoc::Document& document); void setEditLock (bool locked) override; private slots: void refreshRequest(); }; } #endif ================================================ FILE: apps/opencs/view/tools/reporttable.cpp ================================================ #include "reporttable.hpp" #include #include #include #include #include #include #include #include #include #include #include "../../model/tools/reportmodel.hpp" #include "../../model/prefs/state.hpp" #include "../../model/prefs/shortcut.hpp" #include "../../view/world/idtypedelegate.hpp" namespace CSVTools { class RichTextDelegate : public QStyledItemDelegate { public: RichTextDelegate (QObject *parent = nullptr); void paint(QPainter *painter, const QStyleOptionViewItem& option, const QModelIndex& index) const override; }; } CSVTools::RichTextDelegate::RichTextDelegate (QObject *parent) : QStyledItemDelegate (parent) {} void CSVTools::RichTextDelegate::paint(QPainter *painter, const QStyleOptionViewItem& option, const QModelIndex& index) const { QTextDocument document; QVariant value = index.data (Qt::DisplayRole); if (value.isValid() && !value.isNull()) { document.setHtml (value.toString()); painter->translate (option.rect.topLeft()); document.drawContents (painter); painter->translate (-option.rect.topLeft()); } } void CSVTools::ReportTable::contextMenuEvent (QContextMenuEvent *event) { QModelIndexList selectedRows = selectionModel()->selectedRows(); // create context menu QMenu menu (this); if (!selectedRows.empty()) { menu.addAction (mShowAction); menu.addAction (mRemoveAction); bool found = false; for (QModelIndexList::const_iterator iter (selectedRows.begin()); iter!=selectedRows.end(); ++iter) { QString hint = mProxyModel->data (mProxyModel->index (iter->row(), 2)).toString(); if (!hint.isEmpty() && hint[0]=='R') { found = true; break; } } if (found) menu.addAction (mReplaceAction); } if (mRefreshAction) menu.addAction (mRefreshAction); menu.exec (event->globalPos()); } void CSVTools::ReportTable::mouseMoveEvent (QMouseEvent *event) { if (event->buttons() & Qt::LeftButton) startDragFromTable (*this); } void CSVTools::ReportTable::mouseDoubleClickEvent (QMouseEvent *event) { Qt::KeyboardModifiers modifiers = event->modifiers() & (Qt::ShiftModifier | Qt::ControlModifier); QModelIndex index = currentIndex(); selectionModel()->select (index, QItemSelectionModel::Clear | QItemSelectionModel::Select | QItemSelectionModel::Rows); std::map::iterator iter = mDoubleClickActions.find (modifiers); if (iter==mDoubleClickActions.end()) { event->accept(); return; } switch (iter->second) { case Action_None: event->accept(); break; case Action_Edit: event->accept(); showSelection(); break; case Action_Remove: event->accept(); removeSelection(); break; case Action_EditAndRemove: event->accept(); showSelection(); removeSelection(); break; } } CSVTools::ReportTable::ReportTable (CSMDoc::Document& document, const CSMWorld::UniversalId& id, bool richTextDescription, int refreshState, QWidget *parent) : CSVWorld::DragRecordTable (document, parent), mModel (document.getReport (id)), mRefreshAction (nullptr), mRefreshState (refreshState) { horizontalHeader()->setSectionResizeMode (QHeaderView::Interactive); horizontalHeader()->setStretchLastSection (true); verticalHeader()->hide(); setSortingEnabled (true); setSelectionBehavior (QAbstractItemView::SelectRows); setSelectionMode (QAbstractItemView::ExtendedSelection); mProxyModel = new QSortFilterProxyModel (this); mProxyModel->setSortCaseSensitivity(Qt::CaseInsensitive); mProxyModel->setSourceModel (mModel); mProxyModel->setSortRole(Qt::UserRole); setModel (mProxyModel); setColumnHidden (2, true); mIdTypeDelegate = CSVWorld::IdTypeDelegateFactory().makeDelegate (nullptr, mDocument, this); setItemDelegateForColumn (0, mIdTypeDelegate); if (richTextDescription) setItemDelegateForColumn (mModel->columnCount()-1, new RichTextDelegate (this)); mShowAction = new QAction (tr ("Show"), this); connect (mShowAction, SIGNAL (triggered()), this, SLOT (showSelection())); addAction (mShowAction); CSMPrefs::Shortcut* showShortcut = new CSMPrefs::Shortcut("reporttable-show", this); showShortcut->associateAction(mShowAction); mRemoveAction = new QAction (tr ("Remove from list"), this); connect (mRemoveAction, SIGNAL (triggered()), this, SLOT (removeSelection())); addAction (mRemoveAction); CSMPrefs::Shortcut* removeShortcut = new CSMPrefs::Shortcut("reporttable-remove", this); removeShortcut->associateAction(mRemoveAction); mReplaceAction = new QAction (tr ("Replace"), this); connect (mReplaceAction, SIGNAL (triggered()), this, SIGNAL (replaceRequest())); addAction (mReplaceAction); CSMPrefs::Shortcut* replaceShortcut = new CSMPrefs::Shortcut("reporttable-replace", this); replaceShortcut->associateAction(mReplaceAction); if (mRefreshState) { mRefreshAction = new QAction (tr ("Refresh"), this); mRefreshAction->setEnabled (!(mDocument.getState() & mRefreshState)); connect (mRefreshAction, SIGNAL (triggered()), this, SIGNAL (refreshRequest())); addAction (mRefreshAction); CSMPrefs::Shortcut* refreshShortcut = new CSMPrefs::Shortcut("reporttable-refresh", this); refreshShortcut->associateAction(mRefreshAction); } mDoubleClickActions.insert (std::make_pair (Qt::NoModifier, Action_Edit)); mDoubleClickActions.insert (std::make_pair (Qt::ShiftModifier, Action_Remove)); mDoubleClickActions.insert (std::make_pair (Qt::ControlModifier, Action_EditAndRemove)); connect (&CSMPrefs::State::get(), SIGNAL (settingChanged (const CSMPrefs::Setting *)), this, SLOT (settingChanged (const CSMPrefs::Setting *))); CSMPrefs::get()["Reports"].update(); } std::vector CSVTools::ReportTable::getDraggedRecords() const { std::vector ids; QModelIndexList selectedRows = selectionModel()->selectedRows(); for (QModelIndexList::const_iterator iter (selectedRows.begin()); iter!=selectedRows.end(); ++iter) { ids.push_back (mModel->getUniversalId (mProxyModel->mapToSource (*iter).row())); } return ids; } std::vector CSVTools::ReportTable::getReplaceIndices (bool selection) const { std::vector indices; if (selection) { QModelIndexList selectedRows = selectionModel()->selectedRows(); std::vector rows; for (QModelIndexList::const_iterator iter (selectedRows.begin()); iter!=selectedRows.end(); ++iter) { rows.push_back (mProxyModel->mapToSource (*iter).row()); } std::sort (rows.begin(), rows.end()); for (std::vector::const_iterator iter (rows.begin()); iter!=rows.end(); ++iter) { QString hint = mModel->data (mModel->index (*iter, 2)).toString(); if (!hint.isEmpty() && hint[0]=='R') indices.push_back (*iter); } } else { for (int i=0; irowCount(); ++i) { QString hint = mModel->data (mModel->index (i, 2)).toString(); if (!hint.isEmpty() && hint[0]=='R') indices.push_back (i); } } return indices; } void CSVTools::ReportTable::flagAsReplaced (int index) { mModel->flagAsReplaced (index); } void CSVTools::ReportTable::settingChanged (const CSMPrefs::Setting *setting) { if (setting->getParent()->getKey()=="Reports") { QString base ("double"); QString key = setting->getKey().c_str(); if (key.startsWith (base)) { QString modifierString = key.mid (base.size()); Qt::KeyboardModifiers modifiers; if (modifierString=="-s") modifiers = Qt::ShiftModifier; else if (modifierString=="-c") modifiers = Qt::ControlModifier; else if (modifierString=="-sc") modifiers = Qt::ShiftModifier | Qt::ControlModifier; DoubleClickAction action = Action_None; std::string value = setting->toString(); if (value=="Edit") action = Action_Edit; else if (value=="Remove") action = Action_Remove; else if (value=="Edit And Remove") action = Action_EditAndRemove; mDoubleClickActions[modifiers] = action; return; } } else if (*setting=="Records/type-format") mIdTypeDelegate->settingChanged (setting); } void CSVTools::ReportTable::showSelection() { QModelIndexList selectedRows = selectionModel()->selectedRows(); for (QModelIndexList::const_iterator iter (selectedRows.begin()); iter!=selectedRows.end(); ++iter) { int row = mProxyModel->mapToSource (*iter).row(); emit editRequest (mModel->getUniversalId (row), mModel->getHint (row)); } } void CSVTools::ReportTable::removeSelection() { QModelIndexList selectedRows = selectionModel()->selectedRows(); std::vector rows; for (QModelIndexList::iterator iter (selectedRows.begin()); iter!=selectedRows.end(); ++iter) { rows.push_back (mProxyModel->mapToSource (*iter).row()); } std::sort (rows.begin(), rows.end()); for (std::vector::const_reverse_iterator iter (rows.rbegin()); iter!=rows.rend(); ++iter) mProxyModel->removeRows (*iter, 1); selectionModel()->clear(); } void CSVTools::ReportTable::clear() { mModel->clear(); } void CSVTools::ReportTable::stateChanged (int state, CSMDoc::Document *document) { if (mRefreshAction) mRefreshAction->setEnabled (!(state & mRefreshState)); } ================================================ FILE: apps/opencs/view/tools/reporttable.hpp ================================================ #ifndef CSV_TOOLS_REPORTTABLE_H #define CSV_TOOLS_REPORTTABLE_H #include #include "../world/dragrecordtable.hpp" class QAction; class QSortFilterProxyModel; namespace CSMTools { class ReportModel; } namespace CSMPrefs { class Setting; } namespace CSVWorld { class CommandDelegate; } namespace CSVTools { class ReportTable : public CSVWorld::DragRecordTable { Q_OBJECT enum DoubleClickAction { Action_None, Action_Edit, Action_Remove, Action_EditAndRemove }; QSortFilterProxyModel *mProxyModel; CSMTools::ReportModel *mModel; CSVWorld::CommandDelegate *mIdTypeDelegate; QAction *mShowAction; QAction *mRemoveAction; QAction *mReplaceAction; QAction *mRefreshAction; std::map mDoubleClickActions; int mRefreshState; private: void contextMenuEvent (QContextMenuEvent *event) override; void mouseMoveEvent (QMouseEvent *event) override; void mouseDoubleClickEvent (QMouseEvent *event) override; public: /// \param richTextDescription Use rich text in the description column. /// \param refreshState Document state to check for refresh function. If value is /// 0 no refresh function exists. If the document current has the specified state /// the refresh function is disabled. ReportTable (CSMDoc::Document& document, const CSMWorld::UniversalId& id, bool richTextDescription, int refreshState = 0, QWidget *parent = nullptr); std::vector getDraggedRecords() const override; void clear(); /// Return indices of rows that are suitable for replacement. /// /// \param selection Only list selected rows. /// /// \return rows in the original model std::vector getReplaceIndices (bool selection) const; /// \param index row in the original model void flagAsReplaced (int index); private slots: void settingChanged (const CSMPrefs::Setting *setting); void showSelection(); void removeSelection(); public slots: void stateChanged (int state, CSMDoc::Document *document); signals: void editRequest (const CSMWorld::UniversalId& id, const std::string& hint); void replaceRequest(); void refreshRequest(); }; } #endif ================================================ FILE: apps/opencs/view/tools/searchbox.cpp ================================================ #include "searchbox.hpp" #include #include #include #include #include "../../model/world/columns.hpp" #include "../../model/tools/search.hpp" void CSVTools::SearchBox::updateSearchButton() { if (!mSearchEnabled) mSearch.setEnabled (false); else { switch (mMode.currentIndex()) { case 0: case 1: case 2: case 3: mSearch.setEnabled (!mText.text().isEmpty()); break; case 4: mSearch.setEnabled (true); break; } } } CSVTools::SearchBox::SearchBox (QWidget *parent) : QWidget (parent), mSearch (tr("Search")), mSearchEnabled (false), mReplace (tr("Replace All")) { mLayout = new QGridLayout (this); // search panel std::vector> states = CSMWorld::Columns::getEnums (CSMWorld::Columns::ColumnId_Modification); states.resize (states.size()-1); // ignore erased state for (std::vector>::const_iterator iter (states.begin()); iter!=states.end(); ++iter) mRecordState.addItem (QString::fromUtf8 (iter->second.c_str())); mMode.addItem (tr("Text")); mMode.addItem (tr("Text (RegEx)")); mMode.addItem (tr("ID")); mMode.addItem (tr("ID (RegEx)")); mMode.addItem (tr("Record State")); connect (&mMode, SIGNAL (activated (int)), this, SLOT (modeSelected (int))); mLayout->addWidget (&mMode, 0, 0); connect (&mText, SIGNAL (textChanged (const QString&)), this, SLOT (textChanged (const QString&))); connect (&mText, SIGNAL (returnPressed()), this, SLOT (startSearch())); mInput.insertWidget (0, &mText); mInput.insertWidget (1, &mRecordState); mLayout->addWidget (&mInput, 0, 1); mCaseSensitive.setText (tr ("Case")); mLayout->addWidget (&mCaseSensitive, 0, 2); connect (&mSearch, SIGNAL (clicked (bool)), this, SLOT (startSearch (bool))); mLayout->addWidget (&mSearch, 0, 3); // replace panel mReplaceInput.insertWidget (0, &mReplaceText); mReplaceInput.insertWidget (1, &mReplacePlaceholder); mLayout->addWidget (&mReplaceInput, 1, 1); mLayout->addWidget (&mReplace, 1, 3); // layout adjustments mLayout->setColumnMinimumWidth (2, 50); mLayout->setColumnStretch (1, 1); mLayout->setContentsMargins (0, 0, 0, 0); connect (&mReplace, (SIGNAL (clicked (bool))), this, SLOT (replaceAll (bool))); // update modeSelected (0); updateSearchButton(); } void CSVTools::SearchBox::setSearchMode (bool enabled) { mSearchEnabled = enabled; updateSearchButton(); } CSMTools::Search CSVTools::SearchBox::getSearch() const { CSMTools::Search::Type type = static_cast (mMode.currentIndex()); bool caseSensitive = mCaseSensitive.isChecked(); switch (type) { case CSMTools::Search::Type_Text: case CSMTools::Search::Type_Id: return CSMTools::Search (type, caseSensitive, std::string (mText.text().toUtf8().data())); case CSMTools::Search::Type_TextRegEx: case CSMTools::Search::Type_IdRegEx: return CSMTools::Search (type, caseSensitive, QRegExp (mText.text().toUtf8().data(), Qt::CaseInsensitive)); case CSMTools::Search::Type_RecordState: return CSMTools::Search (type, caseSensitive, mRecordState.currentIndex()); case CSMTools::Search::Type_None: break; } throw std::logic_error ("invalid search mode index"); } std::string CSVTools::SearchBox::getReplaceText() const { CSMTools::Search::Type type = static_cast (mMode.currentIndex()); switch (type) { case CSMTools::Search::Type_Text: case CSMTools::Search::Type_TextRegEx: case CSMTools::Search::Type_Id: case CSMTools::Search::Type_IdRegEx: return mReplaceText.text().toUtf8().data(); default: throw std::logic_error ("Invalid search mode for replace"); } } void CSVTools::SearchBox::setEditLock (bool locked) { mReplace.setEnabled (!locked); } void CSVTools::SearchBox::focus() { mInput.currentWidget()->setFocus(); } void CSVTools::SearchBox::modeSelected (int index) { switch (index) { case CSMTools::Search::Type_Text: case CSMTools::Search::Type_TextRegEx: case CSMTools::Search::Type_Id: case CSMTools::Search::Type_IdRegEx: mInput.setCurrentIndex (0); mReplaceInput.setCurrentIndex (0); break; case CSMTools::Search::Type_RecordState: mInput.setCurrentIndex (1); mReplaceInput.setCurrentIndex (1); break; } mInput.currentWidget()->setFocus(); updateSearchButton(); } void CSVTools::SearchBox::textChanged (const QString& text) { updateSearchButton(); } void CSVTools::SearchBox::startSearch (bool checked) { if (mSearch.isEnabled()) emit startSearch (getSearch()); } void CSVTools::SearchBox::replaceAll (bool checked) { emit replaceAll(); } ================================================ FILE: apps/opencs/view/tools/searchbox.hpp ================================================ #ifndef CSV_TOOLS_SEARCHBOX_H #define CSV_TOOLS_SEARCHBOX_H #include #include #include #include #include #include #include class QGridLayout; namespace CSMTools { class Search; } namespace CSVTools { class SearchBox : public QWidget { Q_OBJECT QStackedWidget mInput; QLineEdit mText; QComboBox mRecordState; QCheckBox mCaseSensitive; QPushButton mSearch; QGridLayout *mLayout; QComboBox mMode; bool mSearchEnabled; QStackedWidget mReplaceInput; QLineEdit mReplaceText; QLabel mReplacePlaceholder; QPushButton mReplace; private: void updateSearchButton(); public: SearchBox (QWidget *parent = nullptr); void setSearchMode (bool enabled); CSMTools::Search getSearch() const; std::string getReplaceText() const; void setEditLock (bool locked); void focus(); private slots: void modeSelected (int index); void textChanged (const QString& text); void startSearch (bool checked = true); void replaceAll (bool checked); signals: void startSearch (const CSMTools::Search& search); void replaceAll(); }; } #endif ================================================ FILE: apps/opencs/view/tools/searchsubview.cpp ================================================ #include "searchsubview.hpp" #include #include "../../model/doc/document.hpp" #include "../../model/doc/state.hpp" #include "../../model/tools/search.hpp" #include "../../model/tools/reportmodel.hpp" #include "../../model/world/idtablebase.hpp" #include "../../model/prefs/state.hpp" #include "../world/tablebottombox.hpp" #include "../world/creator.hpp" #include "reporttable.hpp" #include "searchbox.hpp" void CSVTools::SearchSubView::replace (bool selection) { if (mLocked) return; std::vector indices = mTable->getReplaceIndices (selection); std::string replace = mSearchBox.getReplaceText(); const CSMTools::ReportModel& model = dynamic_cast (*mTable->model()); bool autoDelete = CSMPrefs::get()["Search & Replace"]["auto-delete"].isTrue(); CSMTools::Search search (mSearch); CSMWorld::IdTableBase *currentTable = nullptr; // We are running through the indices in reverse order to avoid messing up multiple results // in a single string. for (std::vector::const_reverse_iterator iter (indices.rbegin()); iter!=indices.rend(); ++iter) { CSMWorld::UniversalId id = model.getUniversalId (*iter); CSMWorld::UniversalId::Type type = CSMWorld::UniversalId::getParentType (id.getType()); CSMWorld::IdTableBase *table = &dynamic_cast ( *mDocument.getData().getTableModel (type)); if (table!=currentTable) { search.configure (table); currentTable = table; } std::string hint = model.getHint (*iter); if (search.verify (mDocument, table, id, hint)) { search.replace (mDocument, table, id, hint, replace); mTable->flagAsReplaced (*iter); if (autoDelete) mTable->model()->removeRows (*iter, 1); } } } void CSVTools::SearchSubView::showEvent (QShowEvent *event) { CSVDoc::SubView::showEvent (event); mSearchBox.focus(); } CSVTools::SearchSubView::SearchSubView (const CSMWorld::UniversalId& id, CSMDoc::Document& document) : CSVDoc::SubView (id), mDocument (document), mLocked (false) { QVBoxLayout *layout = new QVBoxLayout; layout->addWidget (&mSearchBox); layout->addWidget (mTable = new ReportTable (document, id, true), 2); layout->addWidget (mBottom = new CSVWorld::TableBottomBox (CSVWorld::NullCreatorFactory(), document, id, this), 0); QWidget *widget = new QWidget; widget->setLayout (layout); setWidget (widget); stateChanged (document.getState(), &document); connect (mTable, SIGNAL (editRequest (const CSMWorld::UniversalId&, const std::string&)), SIGNAL (focusId (const CSMWorld::UniversalId&, const std::string&))); connect (mTable, SIGNAL (replaceRequest()), this, SLOT (replaceRequest())); connect (&document, SIGNAL (stateChanged (int, CSMDoc::Document *)), this, SLOT (stateChanged (int, CSMDoc::Document *))); connect (&mSearchBox, SIGNAL (startSearch (const CSMTools::Search&)), this, SLOT (startSearch (const CSMTools::Search&))); connect (&mSearchBox, SIGNAL (replaceAll()), this, SLOT (replaceAllRequest())); connect (document.getReport (id), SIGNAL (rowsRemoved (const QModelIndex&, int, int)), this, SLOT (tableSizeUpdate())); connect (document.getReport (id), SIGNAL (rowsInserted (const QModelIndex&, int, int)), this, SLOT (tableSizeUpdate())); connect (&document, SIGNAL (operationDone (int, bool)), this, SLOT (operationDone (int, bool))); } void CSVTools::SearchSubView::setEditLock (bool locked) { mLocked = locked; mSearchBox.setEditLock (locked); } void CSVTools::SearchSubView::setStatusBar (bool show) { mBottom->setStatusBar(show); } void CSVTools::SearchSubView::stateChanged (int state, CSMDoc::Document *document) { mSearchBox.setSearchMode (!(state & CSMDoc::State_Searching)); } void CSVTools::SearchSubView::startSearch (const CSMTools::Search& search) { CSMPrefs::Category& settings = CSMPrefs::get()["Search & Replace"]; mSearch = search; mSearch.setPadding (settings["char-before"].toInt(), settings["char-after"].toInt()); mTable->clear(); mDocument.runSearch (getUniversalId(), mSearch); } void CSVTools::SearchSubView::replaceRequest() { replace (true); } void CSVTools::SearchSubView::replaceAllRequest() { replace (false); } void CSVTools::SearchSubView::tableSizeUpdate() { mBottom->tableSizeChanged (mDocument.getReport (getUniversalId())->rowCount(), 0, 0); } void CSVTools::SearchSubView::operationDone (int type, bool failed) { if (type==CSMDoc::State_Searching && !failed && !mDocument.getReport (getUniversalId())->rowCount()) { mBottom->setStatusMessage ("No Results"); } } ================================================ FILE: apps/opencs/view/tools/searchsubview.hpp ================================================ #ifndef CSV_TOOLS_SEARCHSUBVIEW_H #define CSV_TOOLS_SEARCHSUBVIEW_H #include "../../model/tools/search.hpp" #include "../doc/subview.hpp" #include "searchbox.hpp" class QTableView; class QModelIndex; namespace CSMDoc { class Document; } namespace CSVWorld { class TableBottomBox; } namespace CSVTools { class ReportTable; class SearchSubView : public CSVDoc::SubView { Q_OBJECT ReportTable *mTable; SearchBox mSearchBox; CSMDoc::Document& mDocument; CSMTools::Search mSearch; bool mLocked; CSVWorld::TableBottomBox *mBottom; private: void replace (bool selection); protected: void showEvent (QShowEvent *event) override; public: SearchSubView (const CSMWorld::UniversalId& id, CSMDoc::Document& document); void setEditLock (bool locked) override; void setStatusBar (bool show) override; private slots: void stateChanged (int state, CSMDoc::Document *document); void startSearch (const CSMTools::Search& search); void replaceRequest(); void replaceAllRequest(); void tableSizeUpdate(); void operationDone (int type, bool failed); }; } #endif ================================================ FILE: apps/opencs/view/tools/subviews.cpp ================================================ #include "subviews.hpp" #include "../doc/subviewfactoryimp.hpp" #include "reportsubview.hpp" #include "searchsubview.hpp" void CSVTools::addSubViewFactories (CSVDoc::SubViewFactoryManager& manager) { manager.add (CSMWorld::UniversalId::Type_VerificationResults, new CSVDoc::SubViewFactory); manager.add (CSMWorld::UniversalId::Type_LoadErrorLog, new CSVDoc::SubViewFactory); manager.add (CSMWorld::UniversalId::Type_Search, new CSVDoc::SubViewFactory); } ================================================ FILE: apps/opencs/view/tools/subviews.hpp ================================================ #ifndef CSV_TOOLS_SUBVIEWS_H #define CSV_TOOLS_SUBVIEWS_H namespace CSVDoc { class SubViewFactoryManager; } namespace CSVTools { void addSubViewFactories (CSVDoc::SubViewFactoryManager& manager); } #endif ================================================ FILE: apps/opencs/view/widget/brushshapes.hpp ================================================ #ifndef CSV_WIDGET_BRUSHSHAPES_H #define CSV_WIDGET_BRUSHSHAPES_H namespace CSVWidget { enum BrushShape { BrushShape_Point, BrushShape_Square, BrushShape_Circle, BrushShape_Custom }; } #endif ================================================ FILE: apps/opencs/view/widget/coloreditor.cpp ================================================ #include "coloreditor.hpp" #include #include #include #include #include #include #include "colorpickerpopup.hpp" CSVWidget::ColorEditor::ColorEditor(const QColor &color, QWidget *parent, const bool popupOnStart) : ColorEditor(parent, popupOnStart) { setColor(color); } CSVWidget::ColorEditor::ColorEditor(const int colorInt, QWidget *parent, const bool popupOnStart) : ColorEditor(parent, popupOnStart) { setColor(colorInt); } CSVWidget::ColorEditor::ColorEditor(QWidget *parent, const bool popupOnStart) : QPushButton(parent), mColorPicker(new ColorPickerPopup(this)), mPopupOnStart(popupOnStart) { connect(this, SIGNAL(clicked()), this, SLOT(showPicker())); connect(mColorPicker, SIGNAL(colorChanged(const QColor &)), this, SLOT(pickerColorChanged(const QColor &))); } void CSVWidget::ColorEditor::paintEvent(QPaintEvent *event) { QPushButton::paintEvent(event); QRect buttonRect = rect(); QRect coloredRect(buttonRect.x() + qRound(buttonRect.width() / 4.0), buttonRect.y() + qRound(buttonRect.height() / 4.0), buttonRect.width() / 2, buttonRect.height() / 2); QPainter painter(this); painter.fillRect(coloredRect, mColor); painter.setPen(Qt::black); painter.drawRect(coloredRect); } void CSVWidget::ColorEditor::showEvent(QShowEvent *event) { QPushButton::showEvent(event); if (isVisible() && mPopupOnStart) { setChecked(true); showPicker(); mPopupOnStart = false; } } QColor CSVWidget::ColorEditor::color() const { return mColor; } int CSVWidget::ColorEditor::colorInt() const { return (mColor.blue() << 16) | (mColor.green() << 8) | (mColor.red()); } void CSVWidget::ColorEditor::setColor(const QColor &color) { mColor = color; update(); } void CSVWidget::ColorEditor::setColor(const int colorInt) { // Color RGB values are stored in given integer. // First byte is red, second byte is green, third byte is blue. QColor color = QColor(colorInt & 0xff, (colorInt >> 8) & 0xff, (colorInt >> 16) & 0xff); setColor(color); } void CSVWidget::ColorEditor::showPicker() { mColorPicker->showPicker(calculatePopupPosition(), mColor); emit pickingFinished(); } void CSVWidget::ColorEditor::pickerColorChanged(const QColor &color) { mColor = color; update(); } QPoint CSVWidget::ColorEditor::calculatePopupPosition() { QRect editorGeometry = geometry(); QRect popupGeometry = mColorPicker->geometry(); QRect screenGeometry = QGuiApplication::primaryScreen()->geometry(); // Center the popup horizontally relative to the editor int localPopupX = (editorGeometry.width() - popupGeometry.width()) / 2; // Popup position need to be specified in global coords QPoint popupPosition = mapToGlobal(QPoint(localPopupX, editorGeometry.height())); // Make sure that the popup isn't out of the screen if (popupPosition.x() < screenGeometry.left()) { popupPosition.setX(screenGeometry.left() + 1); } else if (popupPosition.x() + popupGeometry.width() > screenGeometry.right()) { popupPosition.setX(screenGeometry.right() - popupGeometry.width() - 1); } if (popupPosition.y() + popupGeometry.height() > screenGeometry.bottom()) { // Place the popup above the editor popupPosition.setY(popupPosition.y() - popupGeometry.height() - editorGeometry.height() - 1); } return popupPosition; } ================================================ FILE: apps/opencs/view/widget/coloreditor.hpp ================================================ #ifndef CSV_WIDGET_COLOREDITOR_HPP #define CSV_WIDGET_COLOREDITOR_HPP #include class QColor; class QPoint; class QSize; namespace CSVWidget { class ColorPickerPopup; class ColorEditor : public QPushButton { Q_OBJECT QColor mColor; ColorPickerPopup *mColorPicker; bool mPopupOnStart; QPoint calculatePopupPosition(); public: ColorEditor(const QColor &color, QWidget *parent = nullptr, const bool popupOnStart = false); ColorEditor(const int colorInt, QWidget *parent = nullptr, const bool popupOnStart = false); QColor color() const; /// \return Color RGB value encoded in an int. int colorInt() const; void setColor(const QColor &color); /// \brief Set color using given int value. /// \param colorInt RGB color value encoded as an integer. void setColor(const int colorInt); protected: void paintEvent(QPaintEvent *event) override; void showEvent(QShowEvent *event) override; private: ColorEditor(QWidget *parent = nullptr, const bool popupOnStart = false); private slots: void showPicker(); void pickerColorChanged(const QColor &color); signals: void pickingFinished(); }; } #endif ================================================ FILE: apps/opencs/view/widget/colorpickerpopup.cpp ================================================ #include "colorpickerpopup.hpp" #include #include #include #include #include #include CSVWidget::ColorPickerPopup::ColorPickerPopup(QWidget *parent) : QFrame(parent) { setWindowFlags(Qt::Popup); setFrameStyle(QFrame::Box | QFrame::Plain); hide(); mColorPicker = new QColorDialog(this); mColorPicker->setWindowFlags(Qt::Widget); mColorPicker->setOptions(QColorDialog::NoButtons | QColorDialog::DontUseNativeDialog); mColorPicker->installEventFilter(this); connect(mColorPicker, SIGNAL(currentColorChanged(const QColor &)), this, SIGNAL(colorChanged(const QColor &))); QVBoxLayout *layout = new QVBoxLayout(this); layout->addWidget(mColorPicker); layout->setAlignment(Qt::AlignTop | Qt::AlignLeft); layout->setContentsMargins(0, 0, 0, 0); setLayout(layout); setFixedSize(mColorPicker->size()); } void CSVWidget::ColorPickerPopup::showPicker(const QPoint &position, const QColor &initialColor) { QRect geometry = this->geometry(); geometry.moveTo(position); setGeometry(geometry); // Calling getColor() creates a blocking dialog that will continue execution once the user chooses OK or Cancel QColor color = mColorPicker->getColor(initialColor); if (color.isValid()) mColorPicker->setCurrentColor(color); } void CSVWidget::ColorPickerPopup::mousePressEvent(QMouseEvent *event) { QPushButton *button = qobject_cast(parentWidget()); if (button != nullptr) { QStyleOptionButton option; option.init(button); QRect buttonRect = option.rect; buttonRect.moveTo(button->mapToGlobal(buttonRect.topLeft())); // If the mouse is pressed above the pop-up parent, // the pop-up will be hidden and the pressed signal won't be repeated for the parent if (buttonRect.contains(event->globalPos()) || buttonRect.contains(event->pos())) { setAttribute(Qt::WA_NoMouseReplay); } } QFrame::mousePressEvent(event); } bool CSVWidget::ColorPickerPopup::eventFilter(QObject *object, QEvent *event) { if (object == mColorPicker && event->type() == QEvent::KeyPress) { QKeyEvent *keyEvent = static_cast(event); // Prevent QColorDialog from closing when Escape is pressed. // Instead, hide the popup. if (keyEvent->key() == Qt::Key_Escape) { hide(); return true; } } return QFrame::eventFilter(object, event); } ================================================ FILE: apps/opencs/view/widget/colorpickerpopup.hpp ================================================ #ifndef CSVWIDGET_COLORPICKERPOPUP_HPP #define CSVWIDGET_COLORPICKERPOPUP_HPP #include class QColorDialog; namespace CSVWidget { class ColorPickerPopup : public QFrame { Q_OBJECT QColorDialog *mColorPicker; public: explicit ColorPickerPopup(QWidget *parent); void showPicker(const QPoint &position, const QColor &initialColor); protected: void mousePressEvent(QMouseEvent *event) override; bool eventFilter(QObject *object, QEvent *event) override; signals: void colorChanged(const QColor &color); }; } #endif ================================================ FILE: apps/opencs/view/widget/completerpopup.cpp ================================================ #include "completerpopup.hpp" CSVWidget::CompleterPopup::CompleterPopup(QWidget *parent) : QListView(parent) { setEditTriggers(QAbstractItemView::NoEditTriggers); setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); setSelectionBehavior(QAbstractItemView::SelectRows); setSelectionMode(QAbstractItemView::SingleSelection); } int CSVWidget::CompleterPopup::sizeHintForRow(int row) const { if (model() == nullptr) { return -1; } if (row < 0 || row >= model()->rowCount()) { return -1; } ensurePolished(); QModelIndex index = model()->index(row, modelColumn()); QStyleOptionViewItem option = viewOptions(); QAbstractItemDelegate *delegate = itemDelegate(index); return delegate->sizeHint(option, index).height(); } ================================================ FILE: apps/opencs/view/widget/completerpopup.hpp ================================================ #ifndef CSV_WIDGET_COMPLETERPOPUP_HPP #define CSV_WIDGET_COMPLETERPOPUP_HPP #include namespace CSVWidget { class CompleterPopup : public QListView { public: CompleterPopup(QWidget *parent = nullptr); int sizeHintForRow(int row) const override; }; } #endif ================================================ FILE: apps/opencs/view/widget/droplineedit.cpp ================================================ #include "droplineedit.hpp" #include #include "../../model/world/tablemimedata.hpp" #include "../../model/world/universalid.hpp" #include "../world/dragdroputils.hpp" CSVWidget::DropLineEdit::DropLineEdit(CSMWorld::ColumnBase::Display type, QWidget *parent) : QLineEdit(parent), mDropType(type) { setAcceptDrops(true); } void CSVWidget::DropLineEdit::dragEnterEvent(QDragEnterEvent *event) { if (CSVWorld::DragDropUtils::canAcceptData(*event, mDropType)) { event->acceptProposedAction(); } } void CSVWidget::DropLineEdit::dragMoveEvent(QDragMoveEvent *event) { if (CSVWorld::DragDropUtils::canAcceptData(*event, mDropType)) { event->accept(); } } void CSVWidget::DropLineEdit::dropEvent(QDropEvent *event) { if (CSVWorld::DragDropUtils::canAcceptData(*event, mDropType)) { CSMWorld::UniversalId id = CSVWorld::DragDropUtils::getAcceptedData(*event, mDropType); setText(QString::fromUtf8(id.getId().c_str())); emit tableMimeDataDropped(id, CSVWorld::DragDropUtils::getTableMimeData(*event)->getDocumentPtr()); } } ================================================ FILE: apps/opencs/view/widget/droplineedit.hpp ================================================ #ifndef CSV_WIDGET_DROPLINEEDIT_HPP #define CSV_WIDGET_DROPLINEEDIT_HPP #include #include "../../model/world/columnbase.hpp" namespace CSMDoc { class Document; } namespace CSMWorld { class TableMimeData; class UniversalId; } namespace CSVWidget { class DropLineEdit : public QLineEdit { Q_OBJECT CSMWorld::ColumnBase::Display mDropType; ///< The accepted Display type for this LineEdit. public: DropLineEdit(CSMWorld::ColumnBase::Display type, QWidget *parent = nullptr); protected: void dragEnterEvent(QDragEnterEvent *event) override; void dragMoveEvent(QDragMoveEvent *event) override; void dropEvent(QDropEvent *event) override; signals: void tableMimeDataDropped(const CSMWorld::UniversalId &id, const CSMDoc::Document *document); }; } #endif ================================================ FILE: apps/opencs/view/widget/modebutton.cpp ================================================ #include "modebutton.hpp" CSVWidget::ModeButton::ModeButton (const QIcon& icon, const QString& tooltip, QWidget *parent) : PushButton (icon, Type_Mode, tooltip, parent) {} void CSVWidget::ModeButton::activate (SceneToolbar *toolbar) {} void CSVWidget::ModeButton::deactivate (SceneToolbar *toolbar) {} bool CSVWidget::ModeButton::createContextMenu (QMenu *menu) { return false; } ================================================ FILE: apps/opencs/view/widget/modebutton.hpp ================================================ #ifndef CSV_WIDGET_MODEBUTTON_H #define CSV_WIDGET_MODEBUTTON_H #include "pushbutton.hpp" class QMenu; namespace CSVWidget { class SceneToolbar; /// \brief Specialist PushButton of Type_Mode for use in SceneToolMode class ModeButton : public PushButton { Q_OBJECT public: ModeButton (const QIcon& icon, const QString& tooltip = "", QWidget *parent = nullptr); /// Default-Implementation: do nothing virtual void activate (SceneToolbar *toolbar); /// Default-Implementation: do nothing virtual void deactivate (SceneToolbar *toolbar); /// Add context menu items to \a menu. Default-implementation: return false /// /// \attention menu can be a 0-pointer /// /// \return Have there been any menu items to be added (if menu is 0 and there /// items to be added, the function must return true anyway. virtual bool createContextMenu (QMenu *menu); }; } #endif ================================================ FILE: apps/opencs/view/widget/pushbutton.cpp ================================================ #include "pushbutton.hpp" #include #include #include "../../model/prefs/state.hpp" #include "../../model/prefs/shortcutmanager.hpp" void CSVWidget::PushButton::processShortcuts() { mProcessedToolTip = CSMPrefs::State::get().getShortcutManager().processToolTip(mToolTip); } void CSVWidget::PushButton::setExtendedToolTip() { QString tooltip = mProcessedToolTip; if (tooltip.isEmpty()) tooltip = "(Tool tip not implemented yet)"; switch (mType) { case Type_TopMode: tooltip += "

(left click to change mode)"; break; case Type_TopAction: break; case Type_Mode: tooltip += "

(left click to activate," "
shift-left click to activate and keep panel open)"; break; case Type_Toggle: tooltip += "

(left click to "; tooltip += isChecked() ? "disable" : "enable"; tooltip += "

shift-left click to "; tooltip += isChecked() ? "disable" : "enable"; tooltip += " and keep panel open)"; break; } setToolTip (tooltip); } void CSVWidget::PushButton::keyPressEvent (QKeyEvent *event) { if (event->key()!=Qt::Key_Shift) mKeepOpen = false; QPushButton::keyPressEvent (event); } void CSVWidget::PushButton::keyReleaseEvent (QKeyEvent *event) { if (event->key()==Qt::Key_Space) mKeepOpen = event->modifiers() & Qt::ShiftModifier; QPushButton::keyReleaseEvent (event); } void CSVWidget::PushButton::mouseReleaseEvent (QMouseEvent *event) { mKeepOpen = event->button()==Qt::LeftButton && (event->modifiers() & Qt::ShiftModifier); QPushButton::mouseReleaseEvent (event); } CSVWidget::PushButton::PushButton (const QIcon& icon, Type type, const QString& tooltip, QWidget *parent) : QPushButton (icon, "", parent), mKeepOpen (false), mType (type), mToolTip (tooltip) { if (type==Type_Mode || type==Type_Toggle) { setCheckable (true); connect (this, SIGNAL (toggled (bool)), this, SLOT (checkedStateChanged (bool))); } setCheckable (type==Type_Mode || type==Type_Toggle); processShortcuts(); setExtendedToolTip(); connect (&CSMPrefs::State::get(), SIGNAL (settingChanged (const CSMPrefs::Setting *)), this, SLOT (settingChanged (const CSMPrefs::Setting *))); } CSVWidget::PushButton::PushButton (Type type, const QString& tooltip, QWidget *parent) : QPushButton (parent), mKeepOpen (false), mType (type), mToolTip (tooltip) { setCheckable (type==Type_Mode || type==Type_Toggle); processShortcuts(); setExtendedToolTip(); } bool CSVWidget::PushButton::hasKeepOpen() const { return mKeepOpen; } QString CSVWidget::PushButton::getBaseToolTip() const { return mProcessedToolTip; } CSVWidget::PushButton::Type CSVWidget::PushButton::getType() const { return mType; } void CSVWidget::PushButton::checkedStateChanged (bool checked) { setExtendedToolTip(); } void CSVWidget::PushButton::settingChanged (const CSMPrefs::Setting *setting) { if (setting->getParent()->getKey() == "Key Bindings") { processShortcuts(); setExtendedToolTip(); } } ================================================ FILE: apps/opencs/view/widget/pushbutton.hpp ================================================ #ifndef CSV_WIDGET_PUSHBUTTON_H #define CSV_WIDGET_PUSHBUTTON_H #include namespace CSMPrefs { class Setting; } namespace CSVWidget { class PushButton : public QPushButton { Q_OBJECT public: enum Type { Type_TopMode, // top level button for mode selector panel Type_TopAction, // top level button that triggers an action Type_Mode, // mode button Type_Toggle }; private: bool mKeepOpen; Type mType; QString mToolTip; QString mProcessedToolTip; private: void processShortcuts(); void setExtendedToolTip(); protected: void keyPressEvent (QKeyEvent *event) override; void keyReleaseEvent (QKeyEvent *event) override; void mouseReleaseEvent (QMouseEvent *event) override; public: /// \param push Do not maintain a toggle state PushButton (const QIcon& icon, Type type, const QString& tooltip = "", QWidget *parent = nullptr); /// \param push Do not maintain a toggle state PushButton (Type type, const QString& tooltip = "", QWidget *parent = nullptr); bool hasKeepOpen() const; /// Return tooltip used at construction (without any button-specific modifications) QString getBaseToolTip() const; Type getType() const; private slots: void checkedStateChanged (bool checked); void settingChanged (const CSMPrefs::Setting *setting); }; } #endif ================================================ FILE: apps/opencs/view/widget/scenetool.cpp ================================================ #include "scenetool.hpp" #include #include "scenetoolbar.hpp" CSVWidget::SceneTool::SceneTool (SceneToolbar *parent, Type type) : PushButton (type, "", parent) { setSizePolicy (QSizePolicy (QSizePolicy::Fixed, QSizePolicy::Fixed)); setIconSize (QSize (parent->getIconSize(), parent->getIconSize())); setFixedSize (parent->getButtonSize(), parent->getButtonSize()); connect (this, SIGNAL (clicked()), this, SLOT (openRequest())); } void CSVWidget::SceneTool::activate() {} void CSVWidget::SceneTool::mouseReleaseEvent (QMouseEvent *event) { if (getType()==Type_TopAction && event->button()==Qt::RightButton) showPanel (parentWidget()->mapToGlobal (pos())); else PushButton::mouseReleaseEvent (event); } void CSVWidget::SceneTool::openRequest() { if (getType()==Type_TopAction) activate(); else showPanel (parentWidget()->mapToGlobal (pos())); } ================================================ FILE: apps/opencs/view/widget/scenetool.hpp ================================================ #ifndef CSV_WIDGET_SCENETOOL_H #define CSV_WIDGET_SCENETOOL_H #include "pushbutton.hpp" namespace CSVWidget { class SceneToolbar; ///< \brief Tool base class class SceneTool : public PushButton { Q_OBJECT public: SceneTool (SceneToolbar *parent, Type type = Type_TopMode); virtual void showPanel (const QPoint& position) = 0; /// This function will only called for buttons of type Type_TopAction. The default /// implementation is empty. virtual void activate(); protected: void mouseReleaseEvent (QMouseEvent *event) override; private slots: void openRequest(); }; } #endif ================================================ FILE: apps/opencs/view/widget/scenetoolbar.cpp ================================================ #include "scenetoolbar.hpp" #include #include "../../model/prefs/shortcut.hpp" #include "scenetool.hpp" void CSVWidget::SceneToolbar::focusInEvent (QFocusEvent *event) { QWidget::focusInEvent (event); if (mLayout->count()) dynamic_cast (*mLayout->itemAt (0)).widget()->setFocus(); } CSVWidget::SceneToolbar::SceneToolbar (int buttonSize, QWidget *parent) : QWidget (parent), mButtonSize (buttonSize), mIconSize (buttonSize-6) { setFixedWidth (mButtonSize); mLayout = new QVBoxLayout (this); mLayout->setAlignment (Qt::AlignTop); mLayout->setContentsMargins (QMargins (0, 0, 0, 0)); setLayout (mLayout); CSMPrefs::Shortcut* focusSceneShortcut = new CSMPrefs::Shortcut("scene-focus-toolbar", this); connect(focusSceneShortcut, SIGNAL(activated()), this, SIGNAL(focusSceneRequest())); } void CSVWidget::SceneToolbar::addTool (SceneTool *tool, SceneTool *insertPoint) { if (!insertPoint) mLayout->addWidget (tool, 0, Qt::AlignTop); else { int index = mLayout->indexOf (insertPoint); mLayout->insertWidget (index+1, tool, 0, Qt::AlignTop); } } void CSVWidget::SceneToolbar::removeTool (SceneTool *tool) { mLayout->removeWidget (tool); } int CSVWidget::SceneToolbar::getButtonSize() const { return mButtonSize; } int CSVWidget::SceneToolbar::getIconSize() const { return mIconSize; } ================================================ FILE: apps/opencs/view/widget/scenetoolbar.hpp ================================================ #ifndef CSV_WIDGET_SCENETOOLBAR_H #define CSV_WIDGET_SCENETOOLBAR_H #include class QVBoxLayout; namespace CSVWidget { class SceneTool; class SceneToolbar : public QWidget { Q_OBJECT QVBoxLayout *mLayout; int mButtonSize; int mIconSize; protected: void focusInEvent (QFocusEvent *event) override; public: SceneToolbar (int buttonSize, QWidget *parent = nullptr); /// If insertPoint==0, insert \a tool at the end of the scrollbar. Otherwise /// insert tool after \a insertPoint. void addTool (SceneTool *tool, SceneTool *insertPoint = nullptr); void removeTool (SceneTool *tool); int getButtonSize() const; int getIconSize() const; signals: void focusSceneRequest(); }; } #endif ================================================ FILE: apps/opencs/view/widget/scenetoolmode.cpp ================================================ #include "scenetoolmode.hpp" #include #include #include #include #include #include #include "scenetoolbar.hpp" #include "modebutton.hpp" void CSVWidget::SceneToolMode::contextMenuEvent (QContextMenuEvent *event) { QMenu menu (this); if (createContextMenu (&menu)) menu.exec (event->globalPos()); } bool CSVWidget::SceneToolMode::createContextMenu (QMenu *menu) { if (mCurrent) return mCurrent->createContextMenu (menu); return false; } void CSVWidget::SceneToolMode::adjustToolTip (const ModeButton *activeMode) { QString toolTip = mToolTip; toolTip += "

Currently selected: " + activeMode->getBaseToolTip(); toolTip += "

(left click to change mode)"; if (createContextMenu (nullptr)) toolTip += "
(right click to access context menu)"; setToolTip (toolTip); } void CSVWidget::SceneToolMode::setButton (std::map::iterator iter) { for (std::map::const_iterator iter2 = mButtons.begin(); iter2!=mButtons.end(); ++iter2) iter2->first->setChecked (iter2==iter); setIcon (iter->first->icon()); adjustToolTip (iter->first); if (mCurrent!=iter->first) { if (mCurrent) mCurrent->deactivate (mToolbar); mCurrent = iter->first; mCurrent->activate (mToolbar); } emit modeChanged (iter->second); } CSVWidget::SceneToolMode::SceneToolMode (SceneToolbar *parent, const QString& toolTip) : SceneTool (parent), mButtonSize (parent->getButtonSize()), mIconSize (parent->getIconSize()), mToolTip (toolTip), mFirst (nullptr), mCurrent (nullptr), mToolbar (parent) { mPanel = new QFrame (this, Qt::Popup); mLayout = new QHBoxLayout (mPanel); mLayout->setContentsMargins (QMargins (0, 0, 0, 0)); mPanel->setLayout (mLayout); } void CSVWidget::SceneToolMode::showPanel (const QPoint& position) { mPanel->move (position); mPanel->show(); if (mFirst) mFirst->setFocus (Qt::OtherFocusReason); } void CSVWidget::SceneToolMode::addButton (const std::string& icon, const std::string& id, const QString& tooltip) { ModeButton *button = new ModeButton (QIcon (QPixmap (icon.c_str())), tooltip, mPanel); addButton (button, id); } void CSVWidget::SceneToolMode::addButton (ModeButton *button, const std::string& id) { button->setParent (mPanel); button->setSizePolicy (QSizePolicy (QSizePolicy::Fixed, QSizePolicy::Fixed)); button->setIconSize (QSize (mIconSize, mIconSize)); button->setFixedSize (mButtonSize, mButtonSize); mLayout->addWidget (button); mButtons.insert (std::make_pair (button, id)); connect (button, SIGNAL (clicked()), this, SLOT (selected())); if (mButtons.size()==1) { mFirst = mCurrent = button; setIcon (button->icon()); button->setChecked (true); adjustToolTip (button); mCurrent->activate (mToolbar); } } CSVWidget::ModeButton *CSVWidget::SceneToolMode::getCurrent() { return mCurrent; } std::string CSVWidget::SceneToolMode::getCurrentId() const { return mButtons.find (mCurrent)->second; } void CSVWidget::SceneToolMode::setButton (const std::string& id) { for (std::map::iterator iter = mButtons.begin(); iter!=mButtons.end(); ++iter) if (iter->second==id) { setButton (iter); break; } } bool CSVWidget::SceneToolMode::event(QEvent* event) { if (event->type() == QEvent::ToolTip) { adjustToolTip(mCurrent); } return SceneTool::event(event); } void CSVWidget::SceneToolMode::selected() { std::map::iterator iter = mButtons.find (dynamic_cast (sender())); if (iter!=mButtons.end()) { if (!iter->first->hasKeepOpen()) mPanel->hide(); setButton (iter); } } ================================================ FILE: apps/opencs/view/widget/scenetoolmode.hpp ================================================ #ifndef CSV_WIDGET_SCENETOOL_MODE_H #define CSV_WIDGET_SCENETOOL_MODE_H #include "scenetool.hpp" #include class QHBoxLayout; class QMenu; class QEvent; namespace CSVWidget { class SceneToolbar; class ModeButton; ///< \brief Mode selector tool class SceneToolMode : public SceneTool { Q_OBJECT QWidget *mPanel; QHBoxLayout *mLayout; std::map mButtons; // widget, id int mButtonSize; int mIconSize; QString mToolTip; PushButton *mFirst; ModeButton *mCurrent; SceneToolbar *mToolbar; void adjustToolTip (const ModeButton *activeMode); void contextMenuEvent (QContextMenuEvent *event) override; /// Add context menu items to \a menu. Default-implementation: Pass on request to /// current mode button or return false, if there is no current mode button. /// /// \attention menu can be a 0-pointer /// /// \return Have there been any menu items to be added (if menu is 0 and there /// items to be added, the function must return true anyway. virtual bool createContextMenu (QMenu *menu); void setButton (std::map::iterator iter); protected: bool event(QEvent* event) override; public: SceneToolMode (SceneToolbar *parent, const QString& toolTip); void showPanel (const QPoint& position) override; void addButton (const std::string& icon, const std::string& id, const QString& tooltip = ""); /// The ownership of \a button is transferred to *this. void addButton (ModeButton *button, const std::string& id); /// Will return a 0-pointer only if the mode does not have any buttons yet. ModeButton *getCurrent(); /// Must not be called if there aren't any buttons yet. std::string getCurrentId() const; /// Manually change the current mode void setButton (const std::string& id); signals: void modeChanged (const std::string& id); private slots: void selected(); }; } #endif ================================================ FILE: apps/opencs/view/widget/scenetoolrun.cpp ================================================ #include "scenetoolrun.hpp" #include #include #include #include #include #include void CSVWidget::SceneToolRun::adjustToolTips() { QString toolTip = mToolTip; if (mSelected==mProfiles.end()) toolTip += "

No debug profile selected (function disabled)"; else { toolTip += "

Debug profile: " + QString::fromUtf8 (mSelected->c_str()); toolTip += "

(right click to switch to a different profile)"; } setToolTip (toolTip); } void CSVWidget::SceneToolRun::updateIcon() { setDisabled (mSelected==mProfiles.end()); } void CSVWidget::SceneToolRun::updatePanel() { mTable->setRowCount (static_cast(mProfiles.size())); int i = 0; for (std::set::const_iterator iter (mProfiles.begin()); iter!=mProfiles.end(); ++iter, ++i) { mTable->setItem (i, 0, new QTableWidgetItem (QString::fromUtf8 (iter->c_str()))); mTable->setItem (i, 1, new QTableWidgetItem ( QApplication::style()->standardIcon (QStyle::SP_TitleBarCloseButton), "")); } } CSVWidget::SceneToolRun::SceneToolRun (SceneToolbar *parent, const QString& toolTip, const QString& icon, const std::vector& profiles) : SceneTool (parent, Type_TopAction), mProfiles (profiles.begin(), profiles.end()), mSelected (mProfiles.begin()), mToolTip (toolTip) { setIcon (QIcon (icon)); updateIcon(); adjustToolTips(); mPanel = new QFrame (this, Qt::Popup); QHBoxLayout *layout = new QHBoxLayout (mPanel); layout->setContentsMargins (QMargins (0, 0, 0, 0)); mTable = new QTableWidget (0, 2, this); mTable->setShowGrid (false); mTable->verticalHeader()->hide(); mTable->horizontalHeader()->hide(); mTable->horizontalHeader()->setSectionResizeMode (0, QHeaderView::Stretch); mTable->horizontalHeader()->setSectionResizeMode (1, QHeaderView::ResizeToContents); mTable->setSelectionMode (QAbstractItemView::NoSelection); layout->addWidget (mTable); connect (mTable, SIGNAL (clicked (const QModelIndex&)), this, SLOT (clicked (const QModelIndex&))); } void CSVWidget::SceneToolRun::showPanel (const QPoint& position) { updatePanel(); mPanel->move (position); mPanel->show(); } void CSVWidget::SceneToolRun::activate() { if (mSelected!=mProfiles.end()) emit runRequest (*mSelected); } void CSVWidget::SceneToolRun::removeProfile (const std::string& profile) { std::set::iterator iter = mProfiles.find (profile); if (iter!=mProfiles.end()) { if (iter==mSelected) { if (iter!=mProfiles.begin()) --mSelected; else ++mSelected; } mProfiles.erase (iter); if (mSelected==mProfiles.end()) updateIcon(); adjustToolTips(); } } void CSVWidget::SceneToolRun::addProfile (const std::string& profile) { std::set::iterator iter = mProfiles.find (profile); if (iter==mProfiles.end()) { mProfiles.insert (profile); if (mSelected==mProfiles.end()) { mSelected = mProfiles.begin(); updateIcon(); } adjustToolTips(); } } void CSVWidget::SceneToolRun::clicked (const QModelIndex& index) { if (index.column()==0) { // select profile mSelected = mProfiles.begin(); std::advance (mSelected, index.row()); mPanel->hide(); adjustToolTips(); } else if (index.column()==1) { // remove profile from list std::set::iterator iter = mProfiles.begin(); std::advance (iter, index.row()); removeProfile (*iter); updatePanel(); } } ================================================ FILE: apps/opencs/view/widget/scenetoolrun.hpp ================================================ #ifndef CSV_WIDGET_SCENETOOLRUN_H #define CSV_WIDGET_SCENETOOLRUN_H #include #include #include "scenetool.hpp" class QFrame; class QTableWidget; class QModelIndex; namespace CSVWidget { class SceneToolRun : public SceneTool { Q_OBJECT std::set mProfiles; std::set::iterator mSelected; QString mToolTip; QFrame *mPanel; QTableWidget *mTable; private: void adjustToolTips(); void updateIcon(); void updatePanel(); public: SceneToolRun (SceneToolbar *parent, const QString& toolTip, const QString& icon, const std::vector& profiles); void showPanel (const QPoint& position) override; void activate() override; /// \attention This function does not remove the profile from the profile selection /// panel. void removeProfile (const std::string& profile); /// \attention This function doe not add the profile to the profile selection /// panel. This only happens when the panel is re-opened. /// /// \note Adding profiles that are already listed is a no-op. void addProfile (const std::string& profile); private slots: void clicked (const QModelIndex& index); signals: void runRequest (const std::string& profile); }; } #endif ================================================ FILE: apps/opencs/view/widget/scenetoolshapebrush.cpp ================================================ #include "scenetoolshapebrush.hpp" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "brushshapes.hpp" #include "scenetool.hpp" #include "../../model/doc/document.hpp" #include "../../model/prefs/state.hpp" #include "../../model/world/commands.hpp" CSVWidget::ShapeBrushSizeControls::ShapeBrushSizeControls(const QString &title, QWidget *parent) : QGroupBox(title, parent) { mBrushSizeSlider->setTickPosition(QSlider::TicksBothSides); mBrushSizeSlider->setTickInterval(10); mBrushSizeSlider->setRange(1, CSMPrefs::get()["3D Scene Editing"]["shapebrush-maximumsize"].toInt()); mBrushSizeSlider->setSingleStep(1); mBrushSizeSpinBox->setRange(1, CSMPrefs::get()["3D Scene Editing"]["shapebrush-maximumsize"].toInt()); mBrushSizeSpinBox->setSingleStep(1); QHBoxLayout *layoutSliderSize = new QHBoxLayout; layoutSliderSize->addWidget(mBrushSizeSlider); layoutSliderSize->addWidget(mBrushSizeSpinBox); connect(mBrushSizeSlider, SIGNAL(valueChanged(int)), mBrushSizeSpinBox, SLOT(setValue(int))); connect(mBrushSizeSpinBox, SIGNAL(valueChanged(int)), mBrushSizeSlider, SLOT(setValue(int))); setLayout(layoutSliderSize); } CSVWidget::ShapeBrushWindow::ShapeBrushWindow(CSMDoc::Document& document, QWidget *parent) : QFrame(parent, Qt::Popup), mDocument(document) { mButtonPoint = new QPushButton(QIcon (QPixmap (":scenetoolbar/brush-point")), "", this); mButtonSquare = new QPushButton(QIcon (QPixmap (":scenetoolbar/brush-square")), "", this); mButtonCircle = new QPushButton(QIcon (QPixmap (":scenetoolbar/brush-circle")), "", this); mButtonCustom = new QPushButton(QIcon (QPixmap (":scenetoolbar/brush-custom")), "", this); mSizeSliders = new ShapeBrushSizeControls("Brush size", this); QVBoxLayout *layoutMain = new QVBoxLayout; layoutMain->setSpacing(0); layoutMain->setContentsMargins(4,0,4,4); QHBoxLayout *layoutHorizontal = new QHBoxLayout; layoutHorizontal->setSpacing(0); layoutHorizontal->setContentsMargins (QMargins (0, 0, 0, 0)); configureButtonInitialSettings(mButtonPoint); configureButtonInitialSettings(mButtonSquare); configureButtonInitialSettings(mButtonCircle); configureButtonInitialSettings(mButtonCustom); mButtonPoint->setToolTip (toolTipPoint); mButtonSquare->setToolTip (toolTipSquare); mButtonCircle->setToolTip (toolTipCircle); mButtonCustom->setToolTip (toolTipCustom); QButtonGroup* brushButtonGroup = new QButtonGroup(this); brushButtonGroup->addButton(mButtonPoint); brushButtonGroup->addButton(mButtonSquare); brushButtonGroup->addButton(mButtonCircle); brushButtonGroup->addButton(mButtonCustom); brushButtonGroup->setExclusive(true); layoutHorizontal->addWidget(mButtonPoint, 0, Qt::AlignTop); layoutHorizontal->addWidget(mButtonSquare, 0, Qt::AlignTop); layoutHorizontal->addWidget(mButtonCircle, 0, Qt::AlignTop); layoutHorizontal->addWidget(mButtonCustom, 0, Qt::AlignTop); mHorizontalGroupBox = new QGroupBox(tr("")); mHorizontalGroupBox->setLayout(layoutHorizontal); mToolSelector = new QComboBox(this); mToolSelector->addItem(tr("Height (drag)")); mToolSelector->addItem(tr("Height, raise (paint)")); mToolSelector->addItem(tr("Height, lower (paint)")); mToolSelector->addItem(tr("Smooth (paint)")); mToolSelector->addItem(tr("Flatten (paint)")); QLabel *brushStrengthLabel = new QLabel(this); brushStrengthLabel->setText("Brush strength:"); mToolStrengthSlider = new QSlider(Qt::Horizontal); mToolStrengthSlider->setTickPosition(QSlider::TicksBothSides); mToolStrengthSlider->setTickInterval(8); mToolStrengthSlider->setRange(8, 128); mToolStrengthSlider->setSingleStep(8); mToolStrengthSlider->setValue(8); layoutMain->addWidget(mHorizontalGroupBox); layoutMain->addWidget(mSizeSliders); layoutMain->addWidget(mToolSelector); layoutMain->addWidget(brushStrengthLabel); layoutMain->addWidget(mToolStrengthSlider); setLayout(layoutMain); connect(mButtonPoint, SIGNAL(clicked()), this, SLOT(setBrushShape())); connect(mButtonSquare, SIGNAL(clicked()), this, SLOT(setBrushShape())); connect(mButtonCircle, SIGNAL(clicked()), this, SLOT(setBrushShape())); connect(mButtonCustom, SIGNAL(clicked()), this, SLOT(setBrushShape())); } void CSVWidget::ShapeBrushWindow::configureButtonInitialSettings(QPushButton *button) { button->setSizePolicy (QSizePolicy (QSizePolicy::Fixed, QSizePolicy::Fixed)); button->setContentsMargins (QMargins (0, 0, 0, 0)); button->setIconSize (QSize (48-6, 48-6)); button->setFixedSize (48, 48); button->setCheckable(true); } void CSVWidget::ShapeBrushWindow::setBrushSize(int brushSize) { mBrushSize = brushSize; emit passBrushSize(mBrushSize); } void CSVWidget::ShapeBrushWindow::setBrushShape() { if(mButtonPoint->isChecked()) mBrushShape = BrushShape_Point; if(mButtonSquare->isChecked()) mBrushShape = BrushShape_Square; if(mButtonCircle->isChecked()) mBrushShape = BrushShape_Circle; if(mButtonCustom->isChecked()) mBrushShape = BrushShape_Custom; emit passBrushShape(mBrushShape); } void CSVWidget::SceneToolShapeBrush::adjustToolTips() { } CSVWidget::SceneToolShapeBrush::SceneToolShapeBrush (SceneToolbar *parent, const QString& toolTip, CSMDoc::Document& document) : SceneTool (parent, Type_TopAction), mToolTip (toolTip), mDocument (document), mShapeBrushWindow(new ShapeBrushWindow(document, this)) { setAcceptDrops(true); connect(mShapeBrushWindow, SIGNAL(passBrushShape(CSVWidget::BrushShape)), this, SLOT(setButtonIcon(CSVWidget::BrushShape))); setButtonIcon(mShapeBrushWindow->mBrushShape); mPanel = new QFrame (this, Qt::Popup); QHBoxLayout *layout = new QHBoxLayout (mPanel); layout->setContentsMargins (QMargins (0, 0, 0, 0)); mTable = new QTableWidget (0, 2, this); mTable->setShowGrid (true); mTable->verticalHeader()->hide(); mTable->horizontalHeader()->hide(); mTable->horizontalHeader()->setSectionResizeMode (0, QHeaderView::Stretch); mTable->horizontalHeader()->setSectionResizeMode (1, QHeaderView::Stretch); mTable->setSelectionMode (QAbstractItemView::NoSelection); layout->addWidget (mTable); connect (mTable, SIGNAL (clicked (const QModelIndex&)), this, SLOT (clicked (const QModelIndex&))); } void CSVWidget::SceneToolShapeBrush::setButtonIcon (CSVWidget::BrushShape brushShape) { QString tooltip = "Change brush settings

Currently selected: "; switch (brushShape) { case BrushShape_Point: setIcon (QIcon (QPixmap (":scenetoolbar/brush-point"))); tooltip += mShapeBrushWindow->toolTipPoint; break; case BrushShape_Square: setIcon (QIcon (QPixmap (":scenetoolbar/brush-square"))); tooltip += mShapeBrushWindow->toolTipSquare; break; case BrushShape_Circle: setIcon (QIcon (QPixmap (":scenetoolbar/brush-circle"))); tooltip += mShapeBrushWindow->toolTipCircle; break; case BrushShape_Custom: setIcon (QIcon (QPixmap (":scenetoolbar/brush-custom"))); tooltip += mShapeBrushWindow->toolTipCustom; break; } setToolTip (tooltip); } void CSVWidget::SceneToolShapeBrush::showPanel (const QPoint& position) { } void CSVWidget::SceneToolShapeBrush::updatePanel () { } void CSVWidget::SceneToolShapeBrush::clicked (const QModelIndex& index) { } void CSVWidget::SceneToolShapeBrush::activate () { QPoint position = QCursor::pos(); mShapeBrushWindow->mSizeSliders->mBrushSizeSlider->setRange(1, CSMPrefs::get()["3D Scene Editing"]["shapebrush-maximumsize"].toInt()); mShapeBrushWindow->mSizeSliders->mBrushSizeSpinBox->setRange(1, CSMPrefs::get()["3D Scene Editing"]["shapebrush-maximumsize"].toInt()); mShapeBrushWindow->move (position); mShapeBrushWindow->show(); } void CSVWidget::SceneToolShapeBrush::dragEnterEvent (QDragEnterEvent *event) { emit passEvent(event); event->accept(); } void CSVWidget::SceneToolShapeBrush::dropEvent (QDropEvent *event) { emit passEvent(event); event->accept(); } ================================================ FILE: apps/opencs/view/widget/scenetoolshapebrush.hpp ================================================ #ifndef CSV_WIDGET_SCENETOOLSHAPEBRUSH_H #define CSV_WIDGET_SCENETOOLSHAPEBRUSH_H #include #include #include #include #include #include #include #include #include #include #include #include #ifndef Q_MOC_RUN #include "brushshapes.hpp" #include "scenetool.hpp" #include "../../model/doc/document.hpp" #endif class QTableWidget; namespace CSVRender { class TerrainShapeMode; } namespace CSVWidget { /// \brief Layout-box for some brush button settings class ShapeBrushSizeControls : public QGroupBox { Q_OBJECT public: ShapeBrushSizeControls(const QString &title, QWidget *parent); private: QSlider *mBrushSizeSlider = new QSlider(Qt::Horizontal); QSpinBox *mBrushSizeSpinBox = new QSpinBox; friend class SceneToolShapeBrush; friend class CSVRender::TerrainShapeMode; }; /// \brief Brush settings window class ShapeBrushWindow : public QFrame { Q_OBJECT public: ShapeBrushWindow(CSMDoc::Document& document, QWidget *parent = nullptr); void configureButtonInitialSettings(QPushButton *button); const QString toolTipPoint = "Paint single point"; const QString toolTipSquare = "Paint with square brush"; const QString toolTipCircle = "Paint with circle brush"; const QString toolTipCustom = "Paint with custom brush, defined by terrain selection"; private: CSVWidget::BrushShape mBrushShape = CSVWidget::BrushShape_Point; int mBrushSize = 1; CSMDoc::Document& mDocument; QGroupBox *mHorizontalGroupBox; QComboBox *mToolSelector; QSlider *mToolStrengthSlider; QPushButton *mButtonPoint; QPushButton *mButtonSquare; QPushButton *mButtonCircle; QPushButton *mButtonCustom; ShapeBrushSizeControls* mSizeSliders; friend class SceneToolShapeBrush; friend class CSVRender::TerrainShapeMode; public slots: void setBrushShape(); void setBrushSize(int brushSize); signals: void passBrushSize (int brushSize); void passBrushShape(CSVWidget::BrushShape brushShape); }; class SceneToolShapeBrush : public SceneTool { Q_OBJECT QString mToolTip; CSMDoc::Document& mDocument; QFrame *mPanel; QTableWidget *mTable; ShapeBrushWindow *mShapeBrushWindow; private: void adjustToolTips(); public: SceneToolShapeBrush (SceneToolbar *parent, const QString& toolTip, CSMDoc::Document& document); void showPanel (const QPoint& position) override; void updatePanel (); void dropEvent (QDropEvent *event) override; void dragEnterEvent (QDragEnterEvent *event) override; friend class CSVRender::TerrainShapeMode; public slots: void setButtonIcon(CSVWidget::BrushShape brushShape); void clicked (const QModelIndex& index); void activate() override; signals: void passEvent(QDropEvent *event); void passEvent(QDragEnterEvent *event); }; } #endif ================================================ FILE: apps/opencs/view/widget/scenetooltexturebrush.cpp ================================================ #include "scenetooltexturebrush.hpp" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "scenetool.hpp" #include "../../model/doc/document.hpp" #include "../../model/prefs/state.hpp" #include "../../model/world/commands.hpp" #include "../../model/world/data.hpp" #include "../../model/world/idcollection.hpp" #include "../../model/world/idtable.hpp" #include "../../model/world/landtexture.hpp" #include "../../model/world/universalid.hpp" CSVWidget::BrushSizeControls::BrushSizeControls(const QString &title, QWidget *parent) : QGroupBox(title, parent), mLayoutSliderSize(new QHBoxLayout), mBrushSizeSlider(new QSlider(Qt::Horizontal)), mBrushSizeSpinBox(new QSpinBox) { mBrushSizeSlider->setTickPosition(QSlider::TicksBothSides); mBrushSizeSlider->setTickInterval(10); mBrushSizeSlider->setRange(1, CSMPrefs::get()["3D Scene Editing"]["texturebrush-maximumsize"].toInt()); mBrushSizeSlider->setSingleStep(1); mBrushSizeSpinBox->setRange(1, CSMPrefs::get()["3D Scene Editing"]["texturebrush-maximumsize"].toInt()); mBrushSizeSpinBox->setSingleStep(1); mLayoutSliderSize->addWidget(mBrushSizeSlider); mLayoutSliderSize->addWidget(mBrushSizeSpinBox); connect(mBrushSizeSlider, SIGNAL(valueChanged(int)), mBrushSizeSpinBox, SLOT(setValue(int))); connect(mBrushSizeSpinBox, SIGNAL(valueChanged(int)), mBrushSizeSlider, SLOT(setValue(int))); setLayout(mLayoutSliderSize); } CSVWidget::TextureBrushWindow::TextureBrushWindow(CSMDoc::Document& document, QWidget *parent) : QFrame(parent, Qt::Popup), mDocument(document) { mBrushTextureLabel = "Selected texture: " + mBrushTexture + " "; CSMWorld::IdCollection& landtexturesCollection = mDocument.getData().getLandTextures(); int landTextureFilename = landtexturesCollection.findColumnIndex(CSMWorld::Columns::ColumnId_Texture); int index = landtexturesCollection.searchId(mBrushTexture); if (index != -1 && !landtexturesCollection.getRecord(index).isDeleted()) { mSelectedBrush = new QLabel(QString::fromStdString(mBrushTextureLabel) + landtexturesCollection.getData(index, landTextureFilename).value()); } else { mBrushTextureLabel = "No selected texture or invalid texture"; mSelectedBrush = new QLabel(QString::fromStdString(mBrushTextureLabel)); } mButtonPoint = new QPushButton(QIcon (QPixmap (":scenetoolbar/brush-point")), "", this); mButtonSquare = new QPushButton(QIcon (QPixmap (":scenetoolbar/brush-square")), "", this); mButtonCircle = new QPushButton(QIcon (QPixmap (":scenetoolbar/brush-circle")), "", this); mButtonCustom = new QPushButton(QIcon (QPixmap (":scenetoolbar/brush-custom")), "", this); mSizeSliders = new BrushSizeControls("Brush size", this); QVBoxLayout *layoutMain = new QVBoxLayout; layoutMain->setSpacing(0); layoutMain->setContentsMargins(4,0,4,4); QHBoxLayout *layoutHorizontal = new QHBoxLayout; layoutHorizontal->setSpacing(0); layoutHorizontal->setContentsMargins (QMargins (0, 0, 0, 0)); configureButtonInitialSettings(mButtonPoint); configureButtonInitialSettings(mButtonSquare); configureButtonInitialSettings(mButtonCircle); configureButtonInitialSettings(mButtonCustom); mButtonPoint->setToolTip (toolTipPoint); mButtonSquare->setToolTip (toolTipSquare); mButtonCircle->setToolTip (toolTipCircle); mButtonCustom->setToolTip (toolTipCustom); QButtonGroup* brushButtonGroup = new QButtonGroup(this); brushButtonGroup->addButton(mButtonPoint); brushButtonGroup->addButton(mButtonSquare); brushButtonGroup->addButton(mButtonCircle); brushButtonGroup->addButton(mButtonCustom); brushButtonGroup->setExclusive(true); layoutHorizontal->addWidget(mButtonPoint, 0, Qt::AlignTop); layoutHorizontal->addWidget(mButtonSquare, 0, Qt::AlignTop); layoutHorizontal->addWidget(mButtonCircle, 0, Qt::AlignTop); layoutHorizontal->addWidget(mButtonCustom, 0, Qt::AlignTop); mHorizontalGroupBox = new QGroupBox(tr("")); mHorizontalGroupBox->setLayout(layoutHorizontal); layoutMain->addWidget(mHorizontalGroupBox); layoutMain->addWidget(mSizeSliders); layoutMain->addWidget(mSelectedBrush); setLayout(layoutMain); connect(mButtonPoint, SIGNAL(clicked()), this, SLOT(setBrushShape())); connect(mButtonSquare, SIGNAL(clicked()), this, SLOT(setBrushShape())); connect(mButtonCircle, SIGNAL(clicked()), this, SLOT(setBrushShape())); connect(mButtonCustom, SIGNAL(clicked()), this, SLOT(setBrushShape())); } void CSVWidget::TextureBrushWindow::configureButtonInitialSettings(QPushButton *button) { button->setSizePolicy (QSizePolicy (QSizePolicy::Fixed, QSizePolicy::Fixed)); button->setContentsMargins (QMargins (0, 0, 0, 0)); button->setIconSize (QSize (48-6, 48-6)); button->setFixedSize (48, 48); button->setCheckable(true); } void CSVWidget::TextureBrushWindow::setBrushTexture(std::string brushTexture) { CSMWorld::IdTable& ltexTable = dynamic_cast ( *mDocument.getData().getTableModel (CSMWorld::UniversalId::Type_LandTextures)); QUndoStack& undoStack = mDocument.getUndoStack(); CSMWorld::IdCollection& landtexturesCollection = mDocument.getData().getLandTextures(); int landTextureFilename = landtexturesCollection.findColumnIndex(CSMWorld::Columns::ColumnId_Texture); int index = 0; int pluginInDragged = 0; CSMWorld::LandTexture::parseUniqueRecordId(brushTexture, pluginInDragged, index); std::string newBrushTextureId = CSMWorld::LandTexture::createUniqueRecordId(0, index); int rowInBase = landtexturesCollection.searchId(brushTexture); int rowInNew = landtexturesCollection.searchId(newBrushTextureId); // Check if texture exists in current plugin, and clone if id found in base, otherwise reindex the texture // TO-DO: Handle case when texture is not found in neither base or plugin properly (finding new index is not enough) // TO-DO: Handle conflicting plugins properly if (rowInNew == -1) { if (rowInBase == -1) { int counter=0; bool freeIndexFound = false; const int maxCounter = std::numeric_limits::max() - 1; do { newBrushTextureId = CSMWorld::LandTexture::createUniqueRecordId(0, counter); if (landtexturesCollection.searchId(brushTexture) != -1 && landtexturesCollection.getRecord(brushTexture).isDeleted() == 0 && landtexturesCollection.searchId(newBrushTextureId) != -1 && landtexturesCollection.getRecord(newBrushTextureId).isDeleted() == 0) counter = (counter + 1) % maxCounter; else freeIndexFound = true; } while (freeIndexFound == false || counter < maxCounter); } undoStack.beginMacro ("Add land texture record"); undoStack.push (new CSMWorld::CloneCommand (ltexTable, brushTexture, newBrushTextureId, CSMWorld::UniversalId::Type_LandTexture)); undoStack.endMacro(); } if (index != -1 && !landtexturesCollection.getRecord(index).isDeleted()) { mBrushTextureLabel = "Selected texture: " + newBrushTextureId + " "; mSelectedBrush->setText(QString::fromStdString(mBrushTextureLabel) + landtexturesCollection.getData(index, landTextureFilename).value()); } else { newBrushTextureId = ""; mBrushTextureLabel = "No selected texture or invalid texture"; mSelectedBrush->setText(QString::fromStdString(mBrushTextureLabel)); } mBrushTexture = newBrushTextureId; emit passTextureId(mBrushTexture); emit passBrushShape(mBrushShape); // updates the icon tooltip } void CSVWidget::TextureBrushWindow::setBrushSize(int brushSize) { mBrushSize = brushSize; emit passBrushSize(mBrushSize); } void CSVWidget::TextureBrushWindow::setBrushShape() { if (mButtonPoint->isChecked()) mBrushShape = CSVWidget::BrushShape_Point; if (mButtonSquare->isChecked()) mBrushShape = CSVWidget::BrushShape_Square; if (mButtonCircle->isChecked()) mBrushShape = CSVWidget::BrushShape_Circle; if (mButtonCustom->isChecked()) mBrushShape = CSVWidget::BrushShape_Custom; emit passBrushShape(mBrushShape); } void CSVWidget::SceneToolTextureBrush::adjustToolTips() { } CSVWidget::SceneToolTextureBrush::SceneToolTextureBrush (SceneToolbar *parent, const QString& toolTip, CSMDoc::Document& document) : SceneTool (parent, Type_TopAction), mToolTip (toolTip), mDocument (document), mTextureBrushWindow(new TextureBrushWindow(document, this)) { mBrushHistory.resize(1); mBrushHistory[0] = "L0#0"; setAcceptDrops(true); connect(mTextureBrushWindow, SIGNAL(passBrushShape(CSVWidget::BrushShape)), this, SLOT(setButtonIcon(CSVWidget::BrushShape))); setButtonIcon(mTextureBrushWindow->mBrushShape); mPanel = new QFrame (this, Qt::Popup); QHBoxLayout *layout = new QHBoxLayout (mPanel); layout->setContentsMargins (QMargins (0, 0, 0, 0)); mTable = new QTableWidget (0, 2, this); mTable->setShowGrid (true); mTable->verticalHeader()->hide(); mTable->horizontalHeader()->hide(); mTable->horizontalHeader()->setSectionResizeMode (0, QHeaderView::Stretch); mTable->horizontalHeader()->setSectionResizeMode (1, QHeaderView::Stretch); mTable->setSelectionMode (QAbstractItemView::NoSelection); layout->addWidget (mTable); connect (mTable, SIGNAL (clicked (const QModelIndex&)), this, SLOT (clicked (const QModelIndex&))); } void CSVWidget::SceneToolTextureBrush::setButtonIcon (CSVWidget::BrushShape brushShape) { QString tooltip = "Change brush settings

Currently selected: "; switch (brushShape) { case BrushShape_Point: setIcon (QIcon (QPixmap (":scenetoolbar/brush-point"))); tooltip += mTextureBrushWindow->toolTipPoint; break; case BrushShape_Square: setIcon (QIcon (QPixmap (":scenetoolbar/brush-square"))); tooltip += mTextureBrushWindow->toolTipSquare; break; case BrushShape_Circle: setIcon (QIcon (QPixmap (":scenetoolbar/brush-circle"))); tooltip += mTextureBrushWindow->toolTipCircle; break; case BrushShape_Custom: setIcon (QIcon (QPixmap (":scenetoolbar/brush-custom"))); tooltip += mTextureBrushWindow->toolTipCustom; break; } tooltip += "

(right click to access of previously used brush settings)"; CSMWorld::IdCollection& landtexturesCollection = mDocument.getData().getLandTextures(); int landTextureFilename = landtexturesCollection.findColumnIndex(CSMWorld::Columns::ColumnId_Texture); int index = landtexturesCollection.searchId(mTextureBrushWindow->mBrushTexture); if (index != -1 && !landtexturesCollection.getRecord(index).isDeleted()) { tooltip += "

Selected texture: " + QString::fromStdString(mTextureBrushWindow->mBrushTexture) + " "; tooltip += landtexturesCollection.getData(index, landTextureFilename).value(); } else { tooltip += "

No selected texture or invalid texture"; } tooltip += "
(drop texture here to change)"; setToolTip (tooltip); } void CSVWidget::SceneToolTextureBrush::showPanel (const QPoint& position) { updatePanel(); mPanel->move (position); mPanel->show(); } void CSVWidget::SceneToolTextureBrush::updatePanel() { mTable->setRowCount (mBrushHistory.size()); for (int i = mBrushHistory.size()-1; i >= 0; --i) { CSMWorld::IdCollection& landtexturesCollection = mDocument.getData().getLandTextures(); int landTextureFilename = landtexturesCollection.findColumnIndex(CSMWorld::Columns::ColumnId_Texture); int index = landtexturesCollection.searchId(mBrushHistory[i]); if (index != -1 && !landtexturesCollection.getRecord(index).isDeleted()) { mTable->setItem (i, 1, new QTableWidgetItem (landtexturesCollection.getData(index, landTextureFilename).value())); mTable->setItem (i, 0, new QTableWidgetItem (QString::fromStdString(mBrushHistory[i]))); } else { mTable->setItem (i, 1, new QTableWidgetItem ("Invalid/deleted texture")); mTable->setItem (i, 0, new QTableWidgetItem (QString::fromStdString(mBrushHistory[i]))); } } } void CSVWidget::SceneToolTextureBrush::updateBrushHistory (const std::string& brushTexture) { mBrushHistory.insert(mBrushHistory.begin(), brushTexture); if(mBrushHistory.size() > 5) mBrushHistory.pop_back(); } void CSVWidget::SceneToolTextureBrush::clicked (const QModelIndex& index) { if (index.column()==0 || index.column()==1) { std::string brushTexture = mBrushHistory[index.row()]; std::swap(mBrushHistory[index.row()], mBrushHistory[0]); mTextureBrushWindow->setBrushTexture(brushTexture); emit passTextureId(brushTexture); updatePanel(); mPanel->hide(); } } void CSVWidget::SceneToolTextureBrush::activate () { QPoint position = QCursor::pos(); mTextureBrushWindow->mSizeSliders->mBrushSizeSlider->setRange(1, CSMPrefs::get()["3D Scene Editing"]["texturebrush-maximumsize"].toInt()); mTextureBrushWindow->mSizeSliders->mBrushSizeSpinBox->setRange(1, CSMPrefs::get()["3D Scene Editing"]["texturebrush-maximumsize"].toInt()); mTextureBrushWindow->move (position); mTextureBrushWindow->show(); } void CSVWidget::SceneToolTextureBrush::dragEnterEvent (QDragEnterEvent *event) { emit passEvent(event); event->accept(); } void CSVWidget::SceneToolTextureBrush::dropEvent (QDropEvent *event) { emit passEvent(event); event->accept(); } ================================================ FILE: apps/opencs/view/widget/scenetooltexturebrush.hpp ================================================ #ifndef CSV_WIDGET_SCENETOOLTEXTUREBRUSH_H #define CSV_WIDGET_SCENETOOLTEXTUREBRUSH_H #include #include #include #include #include #include #include #include #include #include #include #ifndef Q_MOC_RUN #include "brushshapes.hpp" #include "scenetool.hpp" #include "../../model/doc/document.hpp" #endif class QTableWidget; namespace CSVRender { class TerrainTextureMode; } namespace CSVWidget { class SceneToolTextureBrush; /// \brief Layout-box for some brush button settings class BrushSizeControls : public QGroupBox { Q_OBJECT public: BrushSizeControls(const QString &title, QWidget *parent); private: QHBoxLayout *mLayoutSliderSize; QSlider *mBrushSizeSlider; QSpinBox *mBrushSizeSpinBox; friend class SceneToolTextureBrush; friend class CSVRender::TerrainTextureMode; }; class SceneToolTextureBrush; /// \brief Brush settings window class TextureBrushWindow : public QFrame { Q_OBJECT public: TextureBrushWindow(CSMDoc::Document& document, QWidget *parent = nullptr); void configureButtonInitialSettings(QPushButton *button); const QString toolTipPoint = "Paint single point"; const QString toolTipSquare = "Paint with square brush"; const QString toolTipCircle = "Paint with circle brush"; const QString toolTipCustom = "Paint custom selection (not implemented yet)"; private: CSVWidget::BrushShape mBrushShape = CSVWidget::BrushShape_Point; int mBrushSize = 1; std::string mBrushTexture = "L0#0"; CSMDoc::Document& mDocument; QLabel *mSelectedBrush; QGroupBox *mHorizontalGroupBox; std::string mBrushTextureLabel; QPushButton *mButtonPoint; QPushButton *mButtonSquare; QPushButton *mButtonCircle; QPushButton *mButtonCustom; BrushSizeControls* mSizeSliders; friend class SceneToolTextureBrush; friend class CSVRender::TerrainTextureMode; public slots: void setBrushTexture(std::string brushTexture); void setBrushShape(); void setBrushSize(int brushSize); signals: void passBrushSize (int brushSize); void passBrushShape(CSVWidget::BrushShape brushShape); void passTextureId(std::string brushTexture); }; class SceneToolTextureBrush : public SceneTool { Q_OBJECT QString mToolTip; CSMDoc::Document& mDocument; QFrame *mPanel; QTableWidget *mTable; std::vector mBrushHistory; TextureBrushWindow *mTextureBrushWindow; private: void adjustToolTips(); public: SceneToolTextureBrush (SceneToolbar *parent, const QString& toolTip, CSMDoc::Document& document); void showPanel (const QPoint& position) override; void updatePanel (); void dropEvent (QDropEvent *event) override; void dragEnterEvent (QDragEnterEvent *event) override; friend class CSVRender::TerrainTextureMode; public slots: void setButtonIcon(CSVWidget::BrushShape brushShape); void updateBrushHistory (const std::string& mBrushTexture); void clicked (const QModelIndex& index); void activate() override; signals: void passEvent(QDropEvent *event); void passEvent(QDragEnterEvent *event); void passTextureId(std::string brushTexture); }; } #endif ================================================ FILE: apps/opencs/view/widget/scenetooltoggle.cpp ================================================ #include "scenetooltoggle.hpp" #include #include #include #include #include #include "scenetoolbar.hpp" #include "pushbutton.hpp" void CSVWidget::SceneToolToggle::adjustToolTip() { QString toolTip = mToolTip; toolTip += "

Currently enabled: "; bool first = true; for (std::map::const_iterator iter (mButtons.begin()); iter!=mButtons.end(); ++iter) if (iter->first->isChecked()) { if (!first) toolTip += ", "; else first = false; toolTip += iter->second.mName; } if (first) toolTip += "none"; toolTip += "

(left click to alter selection)"; setToolTip (toolTip); } void CSVWidget::SceneToolToggle::adjustIcon() { unsigned int selection = getSelectionMask(); if (!selection) setIcon (QIcon (QString::fromUtf8 (mEmptyIcon.c_str()))); else { QPixmap pixmap (48, 48); pixmap.fill (QColor (0, 0, 0, 0)); { QPainter painter (&pixmap); for (std::map::const_iterator iter (mButtons.begin()); iter!=mButtons.end(); ++iter) if (iter->first->isChecked()) { painter.drawImage (getIconBox (iter->second.mIndex), QImage (QString::fromUtf8 (iter->second.mSmallIcon.c_str()))); } } setIcon (pixmap); } } QRect CSVWidget::SceneToolToggle::getIconBox (int index) const { // layout for a 3x3 grid int xMax = 3; int yMax = 3; // icon size int xBorder = 1; int yBorder = 1; int iconXSize = (mIconSize-xBorder*(xMax+1))/xMax; int iconYSize = (mIconSize-yBorder*(yMax+1))/yMax; int y = index / xMax; int x = index % xMax; int total = static_cast(mButtons.size()); int actualYIcons = total/xMax; if (total % xMax) ++actualYIcons; if (actualYIcons!=yMax) { // space out icons vertically, if there aren't enough to populate all rows int diff = yMax - actualYIcons; yBorder += (diff*(yBorder+iconXSize)) / (actualYIcons+1); } if (y==actualYIcons-1) { // generating the last row of icons int actualXIcons = total % xMax; if (actualXIcons) { // space out icons horizontally, if there aren't enough to fill the last row int diff = xMax - actualXIcons; xBorder += (diff*(xBorder+iconXSize)) / (actualXIcons+1); } } return QRect ((iconXSize+xBorder)*x+xBorder, (iconYSize+yBorder)*y+yBorder, iconXSize, iconYSize); } CSVWidget::SceneToolToggle::SceneToolToggle (SceneToolbar *parent, const QString& toolTip, const std::string& emptyIcon) : SceneTool (parent), mEmptyIcon (emptyIcon), mButtonSize (parent->getButtonSize()), mIconSize (parent->getIconSize()), mToolTip (toolTip), mFirst (nullptr) { mPanel = new QFrame (this, Qt::Popup); mLayout = new QHBoxLayout (mPanel); mLayout->setContentsMargins (QMargins (0, 0, 0, 0)); mPanel->setLayout (mLayout); } void CSVWidget::SceneToolToggle::showPanel (const QPoint& position) { mPanel->move (position); mPanel->show(); if (mFirst) mFirst->setFocus (Qt::OtherFocusReason); } void CSVWidget::SceneToolToggle::addButton (const std::string& icon, unsigned int mask, const std::string& smallIcon, const QString& name, const QString& tooltip) { if (mButtons.size()>=9) throw std::runtime_error ("Exceeded number of buttons in toggle type tool"); PushButton *button = new PushButton (QIcon (QPixmap (icon.c_str())), PushButton::Type_Toggle, tooltip.isEmpty() ? name: tooltip, mPanel); button->setSizePolicy (QSizePolicy (QSizePolicy::Fixed, QSizePolicy::Fixed)); button->setIconSize (QSize (mIconSize, mIconSize)); button->setFixedSize (mButtonSize, mButtonSize); mLayout->addWidget (button); ButtonDesc desc; desc.mMask = mask; desc.mSmallIcon = smallIcon; desc.mName = name; desc.mIndex = static_cast(mButtons.size()); mButtons.insert (std::make_pair (button, desc)); connect (button, SIGNAL (clicked()), this, SLOT (selected())); if (mButtons.size()==1) mFirst = button; } unsigned int CSVWidget::SceneToolToggle::getSelectionMask() const { unsigned int selection = 0; for (std::map::const_iterator iter (mButtons.begin()); iter!=mButtons.end(); ++iter) if (iter->first->isChecked()) selection |= iter->second.mMask; return selection; } void CSVWidget::SceneToolToggle::setSelectionMask (unsigned int selection) { for (std::map::iterator iter (mButtons.begin()); iter!=mButtons.end(); ++iter) iter->first->setChecked (selection & iter->second.mMask); adjustToolTip(); adjustIcon(); } void CSVWidget::SceneToolToggle::selected() { std::map::const_iterator iter = mButtons.find (dynamic_cast (sender())); if (iter!=mButtons.end()) { if (!iter->first->hasKeepOpen()) mPanel->hide(); adjustToolTip(); adjustIcon(); emit selectionChanged(); } } ================================================ FILE: apps/opencs/view/widget/scenetooltoggle.hpp ================================================ #ifndef CSV_WIDGET_SCENETOOL_TOGGLE_H #define CSV_WIDGET_SCENETOOL_TOGGLE_H #include "scenetool.hpp" #include class QHBoxLayout; class QRect; namespace CSVWidget { class SceneToolbar; class PushButton; ///< \brief Multi-Toggle tool class SceneToolToggle : public SceneTool { Q_OBJECT struct ButtonDesc { unsigned int mMask; std::string mSmallIcon; QString mName; int mIndex; }; std::string mEmptyIcon; QWidget *mPanel; QHBoxLayout *mLayout; std::map mButtons; // widget, id int mButtonSize; int mIconSize; QString mToolTip; PushButton *mFirst; void adjustToolTip(); void adjustIcon(); QRect getIconBox (int index) const; public: SceneToolToggle (SceneToolbar *parent, const QString& toolTip, const std::string& emptyIcon); void showPanel (const QPoint& position) override; /// \attention After the last button has been added, setSelection must be called at /// least once to finalise the layout. /// /// \note The layout algorithm can not handle more than 9 buttons. To prevent this An /// attempt to add more will result in an exception being thrown. /// The small icons will be sized at (x-4)/3 (where x is the main icon size). void addButton (const std::string& icon, unsigned int mask, const std::string& smallIcon, const QString& name, const QString& tooltip = ""); unsigned int getSelectionMask() const; /// \param or'ed button masks. buttons that do not exist will be ignored. void setSelectionMask (unsigned int selection); signals: void selectionChanged(); private slots: void selected(); }; } #endif ================================================ FILE: apps/opencs/view/widget/scenetooltoggle2.cpp ================================================ #include "scenetooltoggle2.hpp" #include #include #include #include #include #include #include "scenetoolbar.hpp" #include "pushbutton.hpp" void CSVWidget::SceneToolToggle2::adjustToolTip() { QString toolTip = mToolTip; toolTip += "

Currently enabled: "; bool first = true; for (std::map::const_iterator iter (mButtons.begin()); iter!=mButtons.end(); ++iter) if (iter->first->isChecked()) { if (!first) toolTip += ", "; else first = false; toolTip += iter->second.mName; } if (first) toolTip += "none"; toolTip += "

(left click to alter selection)"; setToolTip (toolTip); } void CSVWidget::SceneToolToggle2::adjustIcon() { unsigned int buttonIds = 0; for (std::map::const_iterator iter (mButtons.begin()); iter!=mButtons.end(); ++iter) if (iter->first->isChecked()) buttonIds |= iter->second.mButtonId; std::ostringstream stream; stream << mCompositeIcon << buttonIds; setIcon (QIcon (QString::fromUtf8 (stream.str().c_str()))); } CSVWidget::SceneToolToggle2::SceneToolToggle2 (SceneToolbar *parent, const QString& toolTip, const std::string& compositeIcon, const std::string& singleIcon) : SceneTool (parent), mCompositeIcon (compositeIcon), mSingleIcon (singleIcon), mButtonSize (parent->getButtonSize()), mIconSize (parent->getIconSize()), mToolTip (toolTip), mFirst (nullptr) { mPanel = new QFrame (this, Qt::Popup); mLayout = new QHBoxLayout (mPanel); mLayout->setContentsMargins (QMargins (0, 0, 0, 0)); mPanel->setLayout (mLayout); } void CSVWidget::SceneToolToggle2::showPanel (const QPoint& position) { mPanel->move (position); mPanel->show(); if (mFirst) mFirst->setFocus (Qt::OtherFocusReason); } void CSVWidget::SceneToolToggle2::addButton (unsigned int id, unsigned int mask, const QString& name, const QString& tooltip, bool disabled) { std::ostringstream stream; stream << mSingleIcon << id; PushButton *button = new PushButton (QIcon (QPixmap (stream.str().c_str())), PushButton::Type_Toggle, tooltip.isEmpty() ? name: tooltip, mPanel); button->setSizePolicy (QSizePolicy (QSizePolicy::Fixed, QSizePolicy::Fixed)); button->setIconSize (QSize (mIconSize, mIconSize)); button->setFixedSize (mButtonSize, mButtonSize); if (disabled) button->setDisabled (true); mLayout->addWidget (button); ButtonDesc desc; desc.mButtonId = id; desc.mMask = mask; desc.mName = name; desc.mIndex = static_cast(mButtons.size()); mButtons.insert (std::make_pair (button, desc)); connect (button, SIGNAL (clicked()), this, SLOT (selected())); if (mButtons.size()==1 && !disabled) mFirst = button; } unsigned int CSVWidget::SceneToolToggle2::getSelectionMask() const { unsigned int selection = 0; for (std::map::const_iterator iter (mButtons.begin()); iter!=mButtons.end(); ++iter) if (iter->first->isChecked()) selection |= iter->second.mMask; return selection; } void CSVWidget::SceneToolToggle2::setSelectionMask (unsigned int selection) { for (std::map::iterator iter (mButtons.begin()); iter!=mButtons.end(); ++iter) iter->first->setChecked (selection & iter->second.mMask); adjustToolTip(); adjustIcon(); } void CSVWidget::SceneToolToggle2::selected() { std::map::const_iterator iter = mButtons.find (dynamic_cast (sender())); if (iter!=mButtons.end()) { if (!iter->first->hasKeepOpen()) mPanel->hide(); adjustToolTip(); adjustIcon(); emit selectionChanged(); } } ================================================ FILE: apps/opencs/view/widget/scenetooltoggle2.hpp ================================================ #ifndef CSV_WIDGET_SCENETOOL_TOGGLE2_H #define CSV_WIDGET_SCENETOOL_TOGGLE2_H #include "scenetool.hpp" #include class QHBoxLayout; class QRect; namespace CSVWidget { class SceneToolbar; class PushButton; ///< \brief Multi-Toggle tool /// /// Top level button is using predefined icons instead building a composite icon. class SceneToolToggle2 : public SceneTool { Q_OBJECT struct ButtonDesc { unsigned int mButtonId; unsigned int mMask; QString mName; int mIndex; }; std::string mCompositeIcon; std::string mSingleIcon; QWidget *mPanel; QHBoxLayout *mLayout; std::map mButtons; // widget, id int mButtonSize; int mIconSize; QString mToolTip; PushButton *mFirst; void adjustToolTip(); void adjustIcon(); public: /// The top level icon is compositeIcon + sum of bitpatterns for active buttons (in /// decimal) /// /// The icon for individual toggle buttons is signleIcon + bitmask for button (in /// decimal) SceneToolToggle2 (SceneToolbar *parent, const QString& toolTip, const std::string& compositeIcon, const std::string& singleIcon); void showPanel (const QPoint& position) override; /// \param buttonId used to compose the icon filename /// \param mask used for the reported getSelectionMask() / setSelectionMask() /// \attention After the last button has been added, setSelection must be called at /// least once to finalise the layout. void addButton (unsigned int buttonId, unsigned int mask, const QString& name, const QString& tooltip = "", bool disabled = false); unsigned int getSelectionMask() const; /// \param or'ed button masks. buttons that do not exist will be ignored. void setSelectionMask (unsigned int selection); signals: void selectionChanged(); private slots: void selected(); }; } #endif ================================================ FILE: apps/opencs/view/world/bodypartcreator.cpp ================================================ #include "bodypartcreator.hpp" #include #include "../../model/world/data.hpp" #include "../../model/world/universalid.hpp" std::string CSVWorld::BodyPartCreator::getId() const { std::string id = CSVWorld::GenericCreator::getId(); if (mFirstPerson->isChecked()) { id += ".1st"; } return id; } CSVWorld::BodyPartCreator::BodyPartCreator( CSMWorld::Data& data, QUndoStack& undoStack, const CSMWorld::UniversalId& id ) : GenericCreator(data, undoStack, id) { mFirstPerson = new QCheckBox("First Person", this); insertBeforeButtons(mFirstPerson, false); connect(mFirstPerson, SIGNAL(clicked(bool)), this, SLOT(checkboxClicked())); } std::string CSVWorld::BodyPartCreator::getErrors() const { std::string errors; std::string id = getId(); if (getData().hasId(id)) { errors = "ID is already in use"; } return errors; } void CSVWorld::BodyPartCreator::reset() { CSVWorld::GenericCreator::reset(); mFirstPerson->setChecked(false); } void CSVWorld::BodyPartCreator::checkboxClicked() { update(); } ================================================ FILE: apps/opencs/view/world/bodypartcreator.hpp ================================================ #ifndef BODYPARTCREATOR_HPP #define BODYPARTCREATOR_HPP class QCheckBox; #include "genericcreator.hpp" namespace CSMWorld { class Data; class UniversalId; } namespace CSVWorld { /// \brief Record creator for body parts. class BodyPartCreator : public GenericCreator { Q_OBJECT QCheckBox *mFirstPerson; private: /// \return ID entered by user. std::string getId() const override; public: BodyPartCreator( CSMWorld::Data& data, QUndoStack& undoStack, const CSMWorld::UniversalId& id); /// \return Error description for current user input. std::string getErrors() const override; /// \brief Clear ID and checkbox input widgets. void reset() override; private slots: void checkboxClicked(); }; } #endif // BODYPARTCREATOR_HPP ================================================ FILE: apps/opencs/view/world/cellcreator.cpp ================================================ #include "cellcreator.hpp" #include #include #include #include #include #include "../../model/world/commands.hpp" #include "../../model/world/idtree.hpp" std::string CSVWorld::CellCreator::getId() const { if (mType->currentIndex()==0) return GenericCreator::getId(); std::ostringstream stream; stream << "#" << mX->value() << " " << mY->value(); return stream.str(); } void CSVWorld::CellCreator::configureCreateCommand(CSMWorld::CreateCommand& command) const { CSMWorld::IdTree* model = &dynamic_cast(*getData().getTableModel(getCollectionId())); int parentIndex = model->findColumnIndex(CSMWorld::Columns::ColumnId_Cell); int index = model->findNestedColumnIndex(parentIndex, CSMWorld::Columns::ColumnId_Interior); command.addNestedValue(parentIndex, index, mType->currentIndex() == 0); } CSVWorld::CellCreator::CellCreator (CSMWorld::Data& data, QUndoStack& undoStack, const CSMWorld::UniversalId& id) : GenericCreator (data, undoStack, id) { mY = new QSpinBox (this); mY->setVisible (false); mY->setMinimum (std::numeric_limits::min()); mY->setMaximum (std::numeric_limits::max()); connect (mY, SIGNAL (valueChanged (int)), this, SLOT (valueChanged (int))); insertAtBeginning (mY, true); mYLabel = new QLabel ("Y", this); mYLabel->setVisible (false); insertAtBeginning (mYLabel, false); mX = new QSpinBox (this); mX->setVisible (false); mX->setMinimum (std::numeric_limits::min()); mX->setMaximum (std::numeric_limits::max()); connect (mX, SIGNAL (valueChanged (int)), this, SLOT (valueChanged (int))); insertAtBeginning (mX, true); mXLabel = new QLabel ("X", this); mXLabel->setVisible (false); insertAtBeginning (mXLabel, false); mType = new QComboBox (this); mType->addItem ("Interior Cell"); mType->addItem ("Exterior Cell"); connect (mType, SIGNAL (currentIndexChanged (int)), this, SLOT (setType (int))); insertAtBeginning (mType, false); } void CSVWorld::CellCreator::reset() { mX->setValue (0); mY->setValue (0); mType->setCurrentIndex (0); setType(0); GenericCreator::reset(); } void CSVWorld::CellCreator::setType (int index) { setManualEditing (index==0); mXLabel->setVisible (index==1); mX->setVisible (index==1); mYLabel->setVisible (index==1); mY->setVisible (index==1); update(); } void CSVWorld::CellCreator::valueChanged (int index) { update(); } void CSVWorld::CellCreator::cloneMode(const std::string& originId, const CSMWorld::UniversalId::Type type) { CSVWorld::GenericCreator::cloneMode(originId, type); if (*(originId.begin()) == '#') //if originid points to the exterior cell { setType(1); //enable x and y controls mType->setCurrentIndex(1); } else { setType(0); mType->setCurrentIndex(0); } } std::string CSVWorld::CellCreator::getErrors() const { std::string errors; if (mType->currentIndex() == 0) { errors = GenericCreator::getErrors(); } else if (getData().hasId(getId())) { errors = "The Exterior Cell is already exist"; } return errors; } ================================================ FILE: apps/opencs/view/world/cellcreator.hpp ================================================ #ifndef CSV_WORLD_CELLCREATOR_H #define CSV_WORLD_CELLCREATOR_H class QLabel; class QSpinBox; class QComboBox; #include "genericcreator.hpp" namespace CSVWorld { class CellCreator : public GenericCreator { Q_OBJECT QComboBox *mType; QLabel *mXLabel; QSpinBox *mX; QLabel *mYLabel; QSpinBox *mY; protected: std::string getId() const override; /// Allow subclasses to add additional data to \a command. void configureCreateCommand(CSMWorld::CreateCommand& command) const override; public: CellCreator (CSMWorld::Data& data, QUndoStack& undoStack, const CSMWorld::UniversalId& id); void reset() override; void cloneMode(const std::string& originId, const CSMWorld::UniversalId::Type type) override; std::string getErrors() const override; ///< Return formatted error descriptions for the current state of the creator. if an empty /// string is returned, there is no error. private slots: void setType (int index); void valueChanged (int index); }; } #endif ================================================ FILE: apps/opencs/view/world/colordelegate.cpp ================================================ #include "colordelegate.hpp" #include #include CSVWorld::ColorDelegate::ColorDelegate(CSMWorld::CommandDispatcher *dispatcher, CSMDoc::Document& document, QObject *parent) : CommandDelegate(dispatcher, document, parent) {} void CSVWorld::ColorDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const { int colorInt = index.data().toInt(); QColor color(colorInt & 0xff, (colorInt >> 8) & 0xff, (colorInt >> 16) & 0xff); QRect coloredRect(option.rect.x() + qRound(option.rect.width() / 4.0), option.rect.y() + qRound(option.rect.height() / 4.0), option.rect.width() / 2, option.rect.height() / 2); painter->save(); painter->fillRect(coloredRect, color); painter->setPen(Qt::black); painter->drawRect(coloredRect); painter->restore(); } CSVWorld::CommandDelegate *CSVWorld::ColorDelegateFactory::makeDelegate(CSMWorld::CommandDispatcher *dispatcher, CSMDoc::Document &document, QObject *parent) const { return new ColorDelegate(dispatcher, document, parent); } ================================================ FILE: apps/opencs/view/world/colordelegate.hpp ================================================ #ifndef CSV_WORLD_COLORDELEGATE_HPP #define CSV_WORLD_COLORDELEGATE_HPP #include "util.hpp" class QRect; namespace CSVWidget { class ColorEditButton; } namespace CSVWorld { class ColorDelegate : public CommandDelegate { public: ColorDelegate(CSMWorld::CommandDispatcher *dispatcher, CSMDoc::Document& document, QObject *parent); void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const override; }; class ColorDelegateFactory : public CommandDelegateFactory { public: CommandDelegate *makeDelegate(CSMWorld::CommandDispatcher *dispatcher, CSMDoc::Document &document, QObject *parent) const override; ///< The ownership of the returned CommandDelegate is transferred to the caller. }; } #endif ================================================ FILE: apps/opencs/view/world/creator.cpp ================================================ #include "creator.hpp" #include CSVWorld::Creator::~Creator() {} void CSVWorld::Creator::setScope (unsigned int scope) { if (scope!=CSMWorld::Scope_Content) throw std::logic_error ("Invalid scope in creator"); } CSVWorld::CreatorFactoryBase::~CreatorFactoryBase() {} CSVWorld::Creator *CSVWorld::NullCreatorFactory::makeCreator (CSMDoc::Document& document, const CSMWorld::UniversalId& id) const { return nullptr; } ================================================ FILE: apps/opencs/view/world/creator.hpp ================================================ #ifndef CSV_WORLD_CREATOR_H #define CSV_WORLD_CREATOR_H #include #include #ifndef Q_MOC_RUN #include "../../model/doc/document.hpp" #include "../../model/world/scope.hpp" #include "../../model/world/universalid.hpp" #endif namespace CSMDoc { class Document; } namespace CSVWorld { /// \brief Record creator UI base class class Creator : public QWidget { Q_OBJECT public: virtual ~Creator(); virtual void reset() = 0; virtual void cloneMode(const std::string& originId, const CSMWorld::UniversalId::Type type) = 0; /// Touches a record, if the creator supports it. virtual void touch(const std::vector& ids) = 0; virtual void setEditLock (bool locked) = 0; virtual void toggleWidgets(bool active = true) = 0; /// Default implementation: Throw an exception if scope!=Scope_Content. virtual void setScope (unsigned int scope); /// Focus main input widget virtual void focus() = 0; signals: void done(); void requestFocus (const std::string& id); ///< Request owner of this creator to focus the just created \a id. The owner may /// ignore this request. }; /// \brief Base class for Creator factory class CreatorFactoryBase { public: virtual ~CreatorFactoryBase(); virtual Creator *makeCreator (CSMDoc::Document& document, const CSMWorld::UniversalId& id) const = 0; ///< The ownership of the returned Creator is transferred to the caller. /// /// \note The function can return a 0-pointer, which means no UI for creating/deleting /// records should be provided. }; /// \brief Creator factory that does not produces any creator class NullCreatorFactory : public CreatorFactoryBase { public: Creator *makeCreator (CSMDoc::Document& document, const CSMWorld::UniversalId& id) const override; ///< The ownership of the returned Creator is transferred to the caller. /// /// \note The function always returns 0. }; template class CreatorFactory : public CreatorFactoryBase { public: Creator *makeCreator (CSMDoc::Document& document, const CSMWorld::UniversalId& id) const override; ///< The ownership of the returned Creator is transferred to the caller. /// /// \note The function can return a 0-pointer, which means no UI for creating/deleting /// records should be provided. }; template Creator *CreatorFactory::makeCreator (CSMDoc::Document& document, const CSMWorld::UniversalId& id) const { std::unique_ptr creator (new CreatorT (document.getData(), document.getUndoStack(), id)); creator->setScope (scope); return creator.release(); } } #endif ================================================ FILE: apps/opencs/view/world/datadisplaydelegate.cpp ================================================ #include "datadisplaydelegate.hpp" #include "../../model/prefs/state.hpp" #include #include CSVWorld::DataDisplayDelegate::DataDisplayDelegate(const ValueList &values, const IconList &icons, CSMWorld::CommandDispatcher *dispatcher, CSMDoc::Document& document, const std::string &pageName, const std::string &settingName, QObject *parent) : EnumDelegate (values, dispatcher, document, parent), mDisplayMode (Mode_TextOnly), mIcons (icons), mIconSize (QSize(16, 16)), mHorizontalMargin(QApplication::style()->pixelMetric(QStyle::PM_FocusFrameHMargin) + 1), mTextLeftOffset(8), mSettingKey (pageName + '/' + settingName) { buildPixmaps(); if (!pageName.empty()) updateDisplayMode (CSMPrefs::get()[pageName][settingName].toString()); } void CSVWorld::DataDisplayDelegate::buildPixmaps () { if (!mPixmaps.empty()) mPixmaps.clear(); IconList::iterator it = mIcons.begin(); while (it != mIcons.end()) { mPixmaps.emplace_back (it->mValue, it->mIcon.pixmap (mIconSize) ); ++it; } } void CSVWorld::DataDisplayDelegate::setIconSize(const QSize& size) { mIconSize = size; buildPixmaps(); } void CSVWorld::DataDisplayDelegate::setTextLeftOffset(int offset) { mTextLeftOffset = offset; } QSize CSVWorld::DataDisplayDelegate::sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const { QSize size = EnumDelegate::sizeHint(option, index); int valueIndex = getValueIndex(index); if (valueIndex != -1) { if (mDisplayMode == Mode_IconOnly) { size.setWidth(mIconSize.width() + 2 * mHorizontalMargin); } else if (mDisplayMode == Mode_IconAndText) { size.setWidth(size.width() + mIconSize.width() + mTextLeftOffset); } if (mDisplayMode != Mode_TextOnly) { size.setHeight(qMax(size.height(), mIconSize.height())); } } return size; } void CSVWorld::DataDisplayDelegate::paint (QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const { painter->save(); //default to enum delegate's paint method for text-only conditions if (mDisplayMode == Mode_TextOnly) EnumDelegate::paint(painter, option, index); else { int valueIndex = getValueIndex(index); if (valueIndex != -1) { paintIcon(painter, option, valueIndex); } } painter->restore(); } void CSVWorld::DataDisplayDelegate::paintIcon (QPainter *painter, const QStyleOptionViewItem &option, int index) const { QRect iconRect = option.rect; QRect textRect = iconRect; iconRect.setLeft(iconRect.left() + mHorizontalMargin); iconRect.setRight(option.rect.right() - mHorizontalMargin); if (mDisplayMode == Mode_IconAndText) { iconRect.setWidth(mIconSize.width()); textRect.setLeft(iconRect.right() + mTextLeftOffset); textRect.setRight(option.rect.right() - mHorizontalMargin); QString text = option.fontMetrics.elidedText(mValues.at(index).second, option.textElideMode, textRect.width()); QApplication::style()->drawItemText(painter, textRect, Qt::AlignLeft | Qt::AlignVCenter, option.palette, true, text); } QApplication::style()->drawItemPixmap(painter, iconRect, Qt::AlignCenter, mPixmaps.at(index).second); } void CSVWorld::DataDisplayDelegate::updateDisplayMode (const std::string &mode) { if (mode == "Icon and Text") mDisplayMode = Mode_IconAndText; else if (mode == "Icon Only") mDisplayMode = Mode_IconOnly; else if (mode == "Text Only") mDisplayMode = Mode_TextOnly; } CSVWorld::DataDisplayDelegate::~DataDisplayDelegate() { } void CSVWorld::DataDisplayDelegate::settingChanged (const CSMPrefs::Setting *setting) { if (*setting==mSettingKey) updateDisplayMode (setting->toString()); } void CSVWorld::DataDisplayDelegateFactory::add (int enumValue, const QString& enumName, const QString& iconFilename) { EnumDelegateFactory::add(enumValue, enumName); Icon icon; icon.mValue = enumValue; icon.mName = enumName; icon.mIcon = QIcon(iconFilename); for (auto it=mIcons.begin(); it!=mIcons.end(); ++it) { if (it->mName > enumName) { mIcons.insert(it, icon); return; } } mIcons.push_back(icon); } CSVWorld::CommandDelegate *CSVWorld::DataDisplayDelegateFactory::makeDelegate ( CSMWorld::CommandDispatcher *dispatcher, CSMDoc::Document& document, QObject *parent) const { return new DataDisplayDelegate (mValues, mIcons, dispatcher, document, "", "", parent); } ================================================ FILE: apps/opencs/view/world/datadisplaydelegate.hpp ================================================ #ifndef DATADISPLAYDELEGATE_HPP #define DATADISPLAYDELEGATE_HPP #include #include "enumdelegate.hpp" namespace CSMPrefs { class Setting; } namespace CSVWorld { struct Icon { int mValue; QIcon mIcon; QString mName; }; class DataDisplayDelegate : public EnumDelegate { public: typedef std::vector IconList; typedef std::vector > ValueList; protected: enum DisplayMode { Mode_TextOnly, Mode_IconOnly, Mode_IconAndText }; DisplayMode mDisplayMode; IconList mIcons; private: std::vector > mPixmaps; QSize mIconSize; int mHorizontalMargin; int mTextLeftOffset; std::string mSettingKey; public: DataDisplayDelegate (const ValueList & values, const IconList & icons, CSMWorld::CommandDispatcher *dispatcher, CSMDoc::Document& document, const std::string& pageName, const std::string& settingName, QObject *parent); ~DataDisplayDelegate(); void paint (QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const override; QSize sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const override; /// pass a QSize defining height / width of icon. Default is QSize (16,16). void setIconSize (const QSize& icon); /// offset the horizontal position of the text from the right edge of the icon. Default is 8 pixels. void setTextLeftOffset (int offset); private: /// update the display mode based on a passed string void updateDisplayMode (const std::string &); /// custom paint function for painting the icon. Mode_IconAndText and Mode_Icon only. void paintIcon (QPainter *painter, const QStyleOptionViewItem &option, int i) const; /// rebuild the list of pixmaps from the provided icons (called when icon size is changed) void buildPixmaps(); void settingChanged (const CSMPrefs::Setting *setting) override; }; class DataDisplayDelegateFactory : public EnumDelegateFactory { protected: DataDisplayDelegate::IconList mIcons; public: CommandDelegate *makeDelegate (CSMWorld::CommandDispatcher *dispatcher, CSMDoc::Document& document, QObject *parent) const override; ///< The ownership of the returned CommandDelegate is transferred to the caller. protected: void add (int enumValue, const QString& enumName, const QString& iconFilename); }; } #endif // DATADISPLAYDELEGATE_HPP ================================================ FILE: apps/opencs/view/world/dialoguecreator.cpp ================================================ #include "dialoguecreator.hpp" #include #include "../../model/doc/document.hpp" #include "../../model/world/data.hpp" #include "../../model/world/commands.hpp" #include "../../model/world/columns.hpp" #include "../../model/world/idtable.hpp" void CSVWorld::DialogueCreator::configureCreateCommand (CSMWorld::CreateCommand& command) const { int index = dynamic_cast (*getData().getTableModel (getCollectionId())). findColumnIndex (CSMWorld::Columns::ColumnId_DialogueType); command.addValue (index, mType); } CSVWorld::DialogueCreator::DialogueCreator (CSMWorld::Data& data, QUndoStack& undoStack, const CSMWorld::UniversalId& id, int type) : GenericCreator (data, undoStack, id, true), mType (type) {} CSVWorld::Creator *CSVWorld::TopicCreatorFactory::makeCreator (CSMDoc::Document& document, const CSMWorld::UniversalId& id) const { return new DialogueCreator (document.getData(), document.getUndoStack(), id, ESM::Dialogue::Topic); } CSVWorld::Creator *CSVWorld::JournalCreatorFactory::makeCreator (CSMDoc::Document& document, const CSMWorld::UniversalId& id) const { return new DialogueCreator (document.getData(), document.getUndoStack(), id, ESM::Dialogue::Journal); } ================================================ FILE: apps/opencs/view/world/dialoguecreator.hpp ================================================ #ifndef CSV_WORLD_DIALOGUECREATOR_H #define CSV_WORLD_DIALOGUECREATOR_H #include "genericcreator.hpp" namespace CSVWorld { class DialogueCreator : public GenericCreator { int mType; protected: void configureCreateCommand (CSMWorld::CreateCommand& command) const override; public: DialogueCreator (CSMWorld::Data& data, QUndoStack& undoStack, const CSMWorld::UniversalId& id, int type); }; class TopicCreatorFactory : public CreatorFactoryBase { public: Creator *makeCreator (CSMDoc::Document& document, const CSMWorld::UniversalId& id) const override; ///< The ownership of the returned Creator is transferred to the caller. }; class JournalCreatorFactory : public CreatorFactoryBase { public: Creator *makeCreator (CSMDoc::Document& document, const CSMWorld::UniversalId& id) const override; ///< The ownership of the returned Creator is transferred to the caller. }; } #endif ================================================ FILE: apps/opencs/view/world/dialoguespinbox.cpp ================================================ #include "dialoguespinbox.hpp" #include CSVWorld::DialogueSpinBox::DialogueSpinBox(QWidget *parent) : QSpinBox(parent) { setFocusPolicy(Qt::StrongFocus); } void CSVWorld::DialogueSpinBox::focusInEvent(QFocusEvent *event) { setFocusPolicy(Qt::WheelFocus); QSpinBox::focusInEvent(event); } void CSVWorld::DialogueSpinBox::focusOutEvent(QFocusEvent *event) { setFocusPolicy(Qt::StrongFocus); QSpinBox::focusOutEvent(event); } void CSVWorld::DialogueSpinBox::wheelEvent(QWheelEvent *event) { if (!hasFocus()) event->ignore(); else QSpinBox::wheelEvent(event); } CSVWorld::DialogueDoubleSpinBox::DialogueDoubleSpinBox(QWidget *parent) : QDoubleSpinBox(parent) { setFocusPolicy(Qt::StrongFocus); } void CSVWorld::DialogueDoubleSpinBox::focusInEvent(QFocusEvent *event) { setFocusPolicy(Qt::WheelFocus); QDoubleSpinBox::focusInEvent(event); } void CSVWorld::DialogueDoubleSpinBox::focusOutEvent(QFocusEvent *event) { setFocusPolicy(Qt::StrongFocus); QDoubleSpinBox::focusOutEvent(event); } void CSVWorld::DialogueDoubleSpinBox::wheelEvent(QWheelEvent *event) { if (!hasFocus()) event->ignore(); else QDoubleSpinBox::wheelEvent(event); } ================================================ FILE: apps/opencs/view/world/dialoguespinbox.hpp ================================================ #ifndef CSV_WORLD_DIALOGUESPINBOX_H #define CSV_WORLD_DIALOGUESPINBOX_H #include #include namespace CSVWorld { class DialogueSpinBox : public QSpinBox { Q_OBJECT public: DialogueSpinBox (QWidget *parent = nullptr); protected: void focusInEvent(QFocusEvent *event) override; void focusOutEvent(QFocusEvent *event) override; void wheelEvent(QWheelEvent *event) override; }; class DialogueDoubleSpinBox : public QDoubleSpinBox { Q_OBJECT public: DialogueDoubleSpinBox (QWidget *parent = nullptr); protected: void focusInEvent(QFocusEvent *event) override; void focusOutEvent(QFocusEvent *event) override; void wheelEvent(QWheelEvent *event) override; }; } #endif // CSV_WORLD_DIALOGUESPINBOX_H ================================================ FILE: apps/opencs/view/world/dialoguesubview.cpp ================================================ #include "dialoguesubview.hpp" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "../../model/world/nestedtableproxymodel.hpp" #include "../../model/world/columnbase.hpp" #include "../../model/world/idtable.hpp" #include "../../model/world/idtree.hpp" #include "../../model/world/columns.hpp" #include "../../model/world/record.hpp" #include "../../model/world/tablemimedata.hpp" #include "../../model/world/commands.hpp" #include "../../model/doc/document.hpp" #include "../../model/prefs/state.hpp" #include "../widget/coloreditor.hpp" #include "../widget/droplineedit.hpp" #include "recordstatusdelegate.hpp" #include "util.hpp" #include "tablebottombox.hpp" #include "nestedtable.hpp" #include "recordbuttonbar.hpp" /* ==============================NotEditableSubDelegate========================================== */ CSVWorld::NotEditableSubDelegate::NotEditableSubDelegate(const CSMWorld::IdTable* table, QObject * parent) : QAbstractItemDelegate(parent), mTable(table) {} void CSVWorld::NotEditableSubDelegate::setEditorData (QWidget* editor, const QModelIndex& index) const { QLabel* label = qobject_cast(editor); if(!label) return; QVariant v = index.data(Qt::EditRole); if (!v.isValid()) { v = index.data(Qt::DisplayRole); if (!v.isValid()) { return; } } CSMWorld::Columns::ColumnId columnId = static_cast ( mTable->getColumnId (index.column())); if (QVariant::String == v.type()) { label->setText(v.toString()); } else if (CSMWorld::Columns::hasEnums (columnId)) { int data = v.toInt(); std::vector> enumNames (CSMWorld::Columns::getEnums (columnId)); label->setText(QString::fromUtf8(enumNames.at(data).second.c_str())); } else { label->setText (v.toString()); } } void CSVWorld::NotEditableSubDelegate::setModelData(QWidget* editor, QAbstractItemModel* model, const QModelIndex& index) const { //not editable widgets will not save model data } void CSVWorld::NotEditableSubDelegate::paint (QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& index) const { //does nothing } QSize CSVWorld::NotEditableSubDelegate::sizeHint (const QStyleOptionViewItem& option, const QModelIndex& index) const { return QSize(); } QWidget* CSVWorld::NotEditableSubDelegate::createEditor (QWidget *parent, const QStyleOptionViewItem& option, const QModelIndex& index) const { QLabel *label = new QLabel(parent); label->setTextInteractionFlags (Qt::TextSelectableByMouse); return label; } /* ==============================DialogueDelegateDispatcherProxy========================================== */ CSVWorld::DialogueDelegateDispatcherProxy::refWrapper::refWrapper(const QModelIndex& index) : mIndex(index) {} CSVWorld::DialogueDelegateDispatcherProxy::DialogueDelegateDispatcherProxy(QWidget* editor, CSMWorld::ColumnBase::Display display) : mEditor(editor), mDisplay(display), mIndexWrapper(nullptr) { } void CSVWorld::DialogueDelegateDispatcherProxy::editorDataCommited() { if (mIndexWrapper.get()) { emit editorDataCommited(mEditor, mIndexWrapper->mIndex, mDisplay); } } void CSVWorld::DialogueDelegateDispatcherProxy::setIndex(const QModelIndex& index) { mIndexWrapper.reset(new refWrapper(index)); } QWidget* CSVWorld::DialogueDelegateDispatcherProxy::getEditor() const { return mEditor; } /* ==============================DialogueDelegateDispatcher========================================== */ CSVWorld::DialogueDelegateDispatcher::DialogueDelegateDispatcher(QObject* parent, CSMWorld::IdTable* table, CSMWorld::CommandDispatcher& commandDispatcher, CSMDoc::Document& document, QAbstractItemModel *model) : mParent(parent), mTable(model ? model : table), mCommandDispatcher (commandDispatcher), mDocument (document), mNotEditableDelegate(table, parent) { } CSVWorld::CommandDelegate* CSVWorld::DialogueDelegateDispatcher::makeDelegate(CSMWorld::ColumnBase::Display display) { CommandDelegate *delegate = nullptr; std::map::const_iterator delegateIt(mDelegates.find(display)); if (delegateIt == mDelegates.end()) { delegate = CommandDelegateFactoryCollection::get().makeDelegate ( display, &mCommandDispatcher, mDocument, mParent); mDelegates.insert(std::make_pair(display, delegate)); } else { delegate = delegateIt->second; } return delegate; } void CSVWorld::DialogueDelegateDispatcher::editorDataCommited(QWidget* editor, const QModelIndex& index, CSMWorld::ColumnBase::Display display) { setModelData(editor, mTable, index, display); } void CSVWorld::DialogueDelegateDispatcher::setEditorData (QWidget* editor, const QModelIndex& index) const { CSMWorld::ColumnBase::Display display = CSMWorld::ColumnBase::Display_None; if (index.parent().isValid()) { display = static_cast (static_cast(mTable)->nestedHeaderData (index.parent().column(), index.column(), Qt::Horizontal, CSMWorld::ColumnBase::Role_Display).toInt()); } else { display = static_cast (mTable->headerData (index.column(), Qt::Horizontal, CSMWorld::ColumnBase::Role_Display).toInt()); } QLabel* label = qobject_cast(editor); if(label) { mNotEditableDelegate.setEditorData(label, index); return; } std::map::const_iterator delegateIt(mDelegates.find(display)); if (delegateIt != mDelegates.end()) { delegateIt->second->setEditorData(editor, index, true); } for (unsigned i = 0; i < mProxys.size(); ++i) { if (mProxys[i]->getEditor() == editor) { mProxys[i]->setIndex(index); } } } void CSVWorld::DialogueDelegateDispatcher::setModelData(QWidget* editor, QAbstractItemModel* model, const QModelIndex& index) const { setModelData(editor, model, index, CSMWorld::ColumnBase::Display_None); } void CSVWorld::DialogueDelegateDispatcher::setModelData(QWidget* editor, QAbstractItemModel* model, const QModelIndex& index, CSMWorld::ColumnBase::Display display) const { std::map::const_iterator delegateIt(mDelegates.find(display)); if (delegateIt != mDelegates.end()) { delegateIt->second->setModelData(editor, model, index); } } void CSVWorld::DialogueDelegateDispatcher::paint (QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& index) const { //Does nothing } QSize CSVWorld::DialogueDelegateDispatcher::sizeHint (const QStyleOptionViewItem& option, const QModelIndex& index) const { return QSize(); //silencing warning, otherwise does nothing } QWidget* CSVWorld::DialogueDelegateDispatcher::makeEditor(CSMWorld::ColumnBase::Display display, const QModelIndex& index) { QVariant variant = index.data(); if (!variant.isValid()) { variant = index.data(Qt::DisplayRole); if (!variant.isValid()) { return nullptr; } } QWidget* editor = nullptr; if (! (mTable->flags (index) & Qt::ItemIsEditable)) { return mNotEditableDelegate.createEditor(qobject_cast(mParent), QStyleOptionViewItem(), index); } std::map::iterator delegateIt(mDelegates.find(display)); if (delegateIt != mDelegates.end()) { editor = delegateIt->second->createEditor(qobject_cast(mParent), QStyleOptionViewItem(), index, display); DialogueDelegateDispatcherProxy* proxy = new DialogueDelegateDispatcherProxy(editor, display); // NOTE: For each entry in CSVWorld::CommandDelegate::createEditor() a corresponding entry // is required here if (qobject_cast(editor)) { connect(editor, SIGNAL(editingFinished()), proxy, SLOT(editorDataCommited())); connect(editor, SIGNAL(tableMimeDataDropped(const CSMWorld::UniversalId&, const CSMDoc::Document*)), proxy, SLOT(editorDataCommited())); } else if (qobject_cast(editor)) { connect(editor, SIGNAL(stateChanged(int)), proxy, SLOT(editorDataCommited())); } else if (qobject_cast(editor)) { connect(editor, SIGNAL(textChanged()), proxy, SLOT(editorDataCommited())); } else if (qobject_cast(editor)) { connect(editor, SIGNAL(currentIndexChanged (int)), proxy, SLOT(editorDataCommited())); } else if (qobject_cast(editor) || qobject_cast(editor)) { connect(editor, SIGNAL(editingFinished()), proxy, SLOT(editorDataCommited())); } else if (qobject_cast(editor)) { connect(editor, SIGNAL(pickingFinished()), proxy, SLOT(editorDataCommited())); } else // throw an exception because this is a coding error throw std::logic_error ("Dialogue editor type missing"); connect(proxy, SIGNAL(editorDataCommited(QWidget*, const QModelIndex&, CSMWorld::ColumnBase::Display)), this, SLOT(editorDataCommited(QWidget*, const QModelIndex&, CSMWorld::ColumnBase::Display))); mProxys.push_back(proxy); //deleted in the destructor } return editor; } CSVWorld::DialogueDelegateDispatcher::~DialogueDelegateDispatcher() { for (unsigned i = 0; i < mProxys.size(); ++i) { delete mProxys[i]; //unique_ptr could be handy } } CSVWorld::IdContextMenu::IdContextMenu(QWidget *widget, CSMWorld::ColumnBase::Display display) : QObject(widget), mWidget(widget), mIdType(CSMWorld::TableMimeData::convertEnums(display)) { Q_ASSERT(mWidget != nullptr); Q_ASSERT(CSMWorld::ColumnBase::isId(display)); Q_ASSERT(mIdType != CSMWorld::UniversalId::Type_None); mWidget->setContextMenuPolicy(Qt::CustomContextMenu); connect(mWidget, SIGNAL(customContextMenuRequested(const QPoint &)), this, SLOT(showContextMenu(const QPoint &))); mEditIdAction = new QAction(this); connect(mEditIdAction, SIGNAL(triggered()), this, SLOT(editIdRequest())); QLineEdit *lineEdit = qobject_cast(mWidget); if (lineEdit != nullptr) { mContextMenu = lineEdit->createStandardContextMenu(); } else { mContextMenu = new QMenu(mWidget); } } void CSVWorld::IdContextMenu::excludeId(const std::string &id) { mExcludedIds.insert(id); } QString CSVWorld::IdContextMenu::getWidgetValue() const { QLineEdit *lineEdit = qobject_cast(mWidget); QLabel *label = qobject_cast(mWidget); QString value = ""; if (lineEdit != nullptr) { value = lineEdit->text(); } else if (label != nullptr) { value = label->text(); } return value; } void CSVWorld::IdContextMenu::addEditIdActionToMenu(const QString &text) { mEditIdAction->setText(text); if (mContextMenu->actions().isEmpty()) { mContextMenu->addAction(mEditIdAction); } else if (mContextMenu->actions().first() != mEditIdAction) { QAction *action = mContextMenu->actions().first(); mContextMenu->insertAction(action, mEditIdAction); mContextMenu->insertSeparator(action); } } void CSVWorld::IdContextMenu::removeEditIdActionFromMenu() { if (mContextMenu->actions().isEmpty()) { return; } if (mContextMenu->actions().first() == mEditIdAction) { mContextMenu->removeAction(mEditIdAction); if (!mContextMenu->actions().isEmpty() && mContextMenu->actions().first()->isSeparator()) { mContextMenu->removeAction(mContextMenu->actions().first()); } } } void CSVWorld::IdContextMenu::showContextMenu(const QPoint &pos) { QString value = getWidgetValue(); bool isExcludedId = mExcludedIds.find(value.toUtf8().constData()) != mExcludedIds.end(); if (!value.isEmpty() && !isExcludedId) { addEditIdActionToMenu("Edit '" + value + "'"); } else { removeEditIdActionFromMenu(); } if (!mContextMenu->actions().isEmpty()) { mContextMenu->exec(mWidget->mapToGlobal(pos)); } } void CSVWorld::IdContextMenu::editIdRequest() { CSMWorld::UniversalId editId(mIdType, getWidgetValue().toUtf8().constData()); emit editIdRequest(editId, ""); } /* =============================================================EditWidget===================================================== */ void CSVWorld::EditWidget::createEditorContextMenu(QWidget *editor, CSMWorld::ColumnBase::Display display, int currentRow) const { Q_ASSERT(editor != nullptr); if (CSMWorld::ColumnBase::isId(display) && CSMWorld::TableMimeData::convertEnums(display) != CSMWorld::UniversalId::Type_None) { int idColumn = mTable->findColumnIndex(CSMWorld::Columns::ColumnId_Id); QString id = mTable->data(mTable->index(currentRow, idColumn)).toString(); IdContextMenu *menu = new IdContextMenu(editor, display); // Current ID is already opened, so no need to create Edit 'ID' action for it menu->excludeId(id.toUtf8().constData()); connect(menu, SIGNAL(editIdRequest(const CSMWorld::UniversalId &, const std::string &)), this, SIGNAL(editIdRequest(const CSMWorld::UniversalId &, const std::string &))); } } CSVWorld::EditWidget::~EditWidget() { for (unsigned i = 0; i < mNestedModels.size(); ++i) delete mNestedModels[i]; if (mDispatcher) delete mDispatcher; if (mNestedTableDispatcher) delete mNestedTableDispatcher; } CSVWorld::EditWidget::EditWidget(QWidget *parent, int row, CSMWorld::IdTable* table, CSMWorld::CommandDispatcher& commandDispatcher, CSMDoc::Document& document, bool createAndDelete) : QScrollArea(parent), mWidgetMapper(nullptr), mNestedTableMapper(nullptr), mDispatcher(nullptr), mNestedTableDispatcher(nullptr), mMainWidget(nullptr), mTable(table), mCommandDispatcher (commandDispatcher), mDocument (document) { remake (row); } void CSVWorld::EditWidget::remake(int row) { if (mMainWidget) { QWidget *del = this->takeWidget(); del->deleteLater(); } mMainWidget = new QWidget (this); for (unsigned i = 0; i < mNestedModels.size(); ++i) delete mNestedModels[i]; mNestedModels.clear(); if (mDispatcher) delete mDispatcher; mDispatcher = new DialogueDelegateDispatcher(nullptr/*this*/, mTable, mCommandDispatcher, mDocument); if (mNestedTableDispatcher) delete mNestedTableDispatcher; //not sure if widget mapper can handle deleting the widgets that were mapped if (mWidgetMapper) delete mWidgetMapper; mWidgetMapper = new QDataWidgetMapper (this); mWidgetMapper->setModel(mTable); mWidgetMapper->setItemDelegate(mDispatcher); if (mNestedTableMapper) delete mNestedTableMapper; QFrame* line = new QFrame(mMainWidget); line->setObjectName(QString::fromUtf8("line")); line->setGeometry(QRect(320, 150, 118, 3)); line->setFrameShape(QFrame::HLine); line->setFrameShadow(QFrame::Sunken); QFrame* line2 = new QFrame(mMainWidget); line2->setObjectName(QString::fromUtf8("line")); line2->setGeometry(QRect(320, 150, 118, 3)); line2->setFrameShape(QFrame::HLine); line2->setFrameShadow(QFrame::Sunken); QVBoxLayout *mainLayout = new QVBoxLayout(mMainWidget); QGridLayout *lockedLayout = new QGridLayout(); QGridLayout *unlockedLayout = new QGridLayout(); QVBoxLayout *tablesLayout = new QVBoxLayout(); mainLayout->addLayout(lockedLayout, QSizePolicy::Fixed); mainLayout->addWidget(line, 1); mainLayout->addLayout(unlockedLayout, QSizePolicy::Preferred); mainLayout->addWidget(line2, 1); mainLayout->addLayout(tablesLayout, QSizePolicy::Preferred); mainLayout->addStretch(1); int unlocked = 0; int locked = 0; const int columns = mTable->columnCount(); for (int i=0; iheaderData (i, Qt::Horizontal, CSMWorld::ColumnBase::Role_Flags).toInt(); if (flags & CSMWorld::ColumnBase::Flag_Dialogue) { CSMWorld::ColumnBase::Display display = static_cast (mTable->headerData (i, Qt::Horizontal, CSMWorld::ColumnBase::Role_Display).toInt()); if (mTable->hasChildren(mTable->index(row, i)) && !(flags & CSMWorld::ColumnBase::Flag_Dialogue_List)) { CSMWorld::IdTree* innerTable = &dynamic_cast(*mTable); mNestedModels.push_back(new CSMWorld::NestedTableProxyModel (mTable->index(row, i), display, innerTable)); int idColumn = mTable->findColumnIndex (CSMWorld::Columns::ColumnId_Id); int typeColumn = mTable->findColumnIndex (CSMWorld::Columns::ColumnId_RecordType); CSMWorld::UniversalId id = CSMWorld::UniversalId( static_cast (mTable->data (mTable->index (row, typeColumn)).toInt()), mTable->data (mTable->index (row, idColumn)).toString().toUtf8().constData()); bool editable = true; bool fixedRows = false; QVariant v = mTable->index(row, i).data(); if (v.canConvert()) { assert (QString(v.typeName()) == "CSMWorld::ColumnBase::TableEditModes"); if (v.value() == CSMWorld::ColumnBase::TableEdit_None) editable = false; else if (v.value() == CSMWorld::ColumnBase::TableEdit_FixedRows) fixedRows = true; } // Create and display nested table only if it's editable. if (editable) { NestedTable* table = new NestedTable(mDocument, id, mNestedModels.back(), this, editable, fixedRows); table->resizeColumnsToContents(); int rows = mTable->rowCount(mTable->index(row, i)); int rowHeight = (rows == 0) ? table->horizontalHeader()->height() : table->rowHeight(0); int headerHeight = table->horizontalHeader()->height(); int tableMaxHeight = (5 * rowHeight) + headerHeight + (2 * table->frameWidth()); table->setMinimumHeight(tableMaxHeight); QString headerText = mTable->headerData (i, Qt::Horizontal, Qt::DisplayRole).toString(); QLabel* label = new QLabel (headerText, mMainWidget); label->setSizePolicy (QSizePolicy::Fixed, QSizePolicy::Fixed); tablesLayout->addWidget(label); tablesLayout->addWidget(table); connect(table, SIGNAL(editRequest(const CSMWorld::UniversalId &, const std::string &)), this, SIGNAL(editIdRequest(const CSMWorld::UniversalId &, const std::string &))); } } else if (!(flags & CSMWorld::ColumnBase::Flag_Dialogue_List)) { mDispatcher->makeDelegate (display); QWidget* editor = mDispatcher->makeEditor (display, (mTable->index (row, i))); if (editor) { mWidgetMapper->addMapping (editor, i); QLabel* label = new QLabel (mTable->headerData (i, Qt::Horizontal).toString(), mMainWidget); label->setSizePolicy (QSizePolicy::Fixed, QSizePolicy::Fixed); editor->setSizePolicy (QSizePolicy::MinimumExpanding, QSizePolicy::Fixed); if (! (mTable->flags (mTable->index (row, i)) & Qt::ItemIsEditable)) { lockedLayout->addWidget (label, locked, 0); lockedLayout->addWidget (editor, locked, 1); ++locked; } else { unlockedLayout->addWidget (label, unlocked, 0); unlockedLayout->addWidget (editor, unlocked, 1); ++unlocked; } if(mTable->index(row, i).data().type() == QVariant::UserType) { editor->setEnabled(false); label->setEnabled(false); } createEditorContextMenu(editor, display, row); } } else { CSMWorld::IdTree *tree = static_cast(mTable); mNestedTableMapper = new QDataWidgetMapper (this); mNestedTableMapper->setModel(tree); // FIXME: lack MIME support? mNestedTableDispatcher = new DialogueDelegateDispatcher (nullptr/*this*/, mTable, mCommandDispatcher, mDocument, tree); mNestedTableMapper->setRootIndex (tree->index(row, i)); mNestedTableMapper->setItemDelegate(mNestedTableDispatcher); int columnCount = tree->columnCount(tree->index(row, i)); for (int col = 0; col < columnCount; ++col) { int displayRole = tree->nestedHeaderData (i, col, Qt::Horizontal, CSMWorld::ColumnBase::Role_Display).toInt(); display = static_cast (displayRole); mNestedTableDispatcher->makeDelegate (display); // FIXME: assumed all columns are editable QWidget* editor = mNestedTableDispatcher->makeEditor (display, tree->index (0, col, tree->index(row, i))); if (editor) { mNestedTableMapper->addMapping (editor, col); // Need to use Qt::DisplayRole in order to get the correct string // from CSMWorld::Columns QLabel* label = new QLabel (tree->nestedHeaderData (i, col, Qt::Horizontal, Qt::DisplayRole).toString(), mMainWidget); label->setSizePolicy (QSizePolicy::Fixed, QSizePolicy::Fixed); editor->setSizePolicy (QSizePolicy::MinimumExpanding, QSizePolicy::Fixed); unlockedLayout->addWidget (label, unlocked, 0); unlockedLayout->addWidget (editor, unlocked, 1); ++unlocked; if(tree->index(0, col, tree->index(row, i)).data().type() == QVariant::UserType) { editor->setEnabled(false); label->setEnabled(false); } createEditorContextMenu(editor, display, row); } } mNestedTableMapper->setCurrentModelIndex(tree->index(0, 0, tree->index(row, i))); } } } mWidgetMapper->setCurrentModelIndex(mTable->index(row, 0)); if (unlocked == 0) mainLayout->removeWidget(line); this->setWidget(mMainWidget); this->setWidgetResizable(true); } QVBoxLayout& CSVWorld::SimpleDialogueSubView::getMainLayout() { return *mMainLayout; } CSMWorld::IdTable& CSVWorld::SimpleDialogueSubView::getTable() { return *mTable; } CSMWorld::CommandDispatcher& CSVWorld::SimpleDialogueSubView::getCommandDispatcher() { return mCommandDispatcher; } CSVWorld::EditWidget& CSVWorld::SimpleDialogueSubView::getEditWidget() { return *mEditWidget; } bool CSVWorld::SimpleDialogueSubView::isLocked() const { return mLocked; } CSVWorld::SimpleDialogueSubView::SimpleDialogueSubView (const CSMWorld::UniversalId& id, CSMDoc::Document& document) : SubView (id), mEditWidget(nullptr), mMainLayout(nullptr), mTable(dynamic_cast(document.getData().getTableModel(id))), mLocked(false), mDocument(document), mCommandDispatcher (document, CSMWorld::UniversalId::getParentType (id.getType())) { connect(mTable, SIGNAL(dataChanged (const QModelIndex&, const QModelIndex&)), this, SLOT(dataChanged(const QModelIndex&))); connect(mTable, SIGNAL(rowsAboutToBeRemoved(const QModelIndex&, int, int)), this, SLOT(rowsAboutToBeRemoved(const QModelIndex&, int, int))); updateCurrentId(); QWidget *mainWidget = new QWidget(this); mMainLayout = new QVBoxLayout(mainWidget); setWidget (mainWidget); int idColumn = getTable().findColumnIndex (CSMWorld::Columns::ColumnId_Id); mEditWidget = new EditWidget(mainWidget, mTable->getModelIndex(getUniversalId().getId(), idColumn).row(), mTable, mCommandDispatcher, document, false); mMainLayout->addWidget(mEditWidget); mEditWidget->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::MinimumExpanding); dataChanged(mTable->getModelIndex (getUniversalId().getId(), idColumn)); connect(mEditWidget, SIGNAL(editIdRequest(const CSMWorld::UniversalId &, const std::string &)), this, SIGNAL(focusId(const CSMWorld::UniversalId &, const std::string &))); } void CSVWorld::SimpleDialogueSubView::setEditLock (bool locked) { if (!mEditWidget) // hack to indicate that getUniversalId().getId() is no longer valid return; int idColumn = getTable().findColumnIndex (CSMWorld::Columns::ColumnId_Id); mLocked = locked; QModelIndex currentIndex(mTable->getModelIndex(getUniversalId().getId(), idColumn)); if (currentIndex.isValid()) { CSMWorld::RecordBase::State state = static_cast(mTable->data (mTable->index (currentIndex.row(), 1)).toInt()); mEditWidget->setDisabled (state==CSMWorld::RecordBase::State_Deleted || locked); mCommandDispatcher.setEditLock (locked); } } void CSVWorld::SimpleDialogueSubView::dataChanged (const QModelIndex & index) { int idColumn = getTable().findColumnIndex (CSMWorld::Columns::ColumnId_Id); QModelIndex currentIndex(mTable->getModelIndex(getUniversalId().getId(), idColumn)); if (currentIndex.isValid() && (index.parent().isValid() ? index.parent().row() : index.row()) == currentIndex.row()) { CSMWorld::RecordBase::State state = static_cast(mTable->data (mTable->index (currentIndex.row(), 1)).toInt()); mEditWidget->setDisabled (state==CSMWorld::RecordBase::State_Deleted || mLocked); // Check if the changed data should force refresh (rebuild) the dialogue subview int flags = 0; if (index.parent().isValid()) // TODO: check that index is topLeft { flags = static_cast(mTable)->nestedHeaderData (index.parent().column(), index.column(), Qt::Horizontal, CSMWorld::ColumnBase::Role_Flags).toInt(); } else { flags = mTable->headerData (index.column(), Qt::Horizontal, CSMWorld::ColumnBase::Role_Flags).toInt(); } if (flags & CSMWorld::ColumnBase::Flag_Dialogue_Refresh) { int y = mEditWidget->verticalScrollBar()->value(); mEditWidget->remake (index.parent().isValid() ? index.parent().row() : index.row()); mEditWidget->verticalScrollBar()->setValue(y); } } } void CSVWorld::SimpleDialogueSubView::rowsAboutToBeRemoved(const QModelIndex &parent, int start, int end) { int idColumn = getTable().findColumnIndex (CSMWorld::Columns::ColumnId_Id); QModelIndex currentIndex(mTable->getModelIndex(getUniversalId().getId(), idColumn)); if (!currentIndex.isValid()) { return; } if (currentIndex.parent() == parent && currentIndex.row() >= start && currentIndex.row() <= end) { if(mEditWidget) { delete mEditWidget; mEditWidget = nullptr; } emit closeRequest(this); } } void CSVWorld::SimpleDialogueSubView::updateCurrentId() { std::vector selection; selection.push_back (getUniversalId().getId()); mCommandDispatcher.setSelection(selection); } void CSVWorld::DialogueSubView::addButtonBar() { if (mButtons) return; mButtons = new RecordButtonBar (getUniversalId(), getTable(), mBottom, &getCommandDispatcher(), this); getMainLayout().insertWidget (1, mButtons); // connections connect (mButtons, SIGNAL (showPreview()), this, SLOT (showPreview())); connect (mButtons, SIGNAL (viewRecord()), this, SLOT (viewRecord())); connect (mButtons, SIGNAL (switchToRow (int)), this, SLOT (switchToRow (int))); connect (this, SIGNAL (universalIdChanged (const CSMWorld::UniversalId&)), mButtons, SLOT (universalIdChanged (const CSMWorld::UniversalId&))); } CSVWorld::DialogueSubView::DialogueSubView (const CSMWorld::UniversalId& id, CSMDoc::Document& document, const CreatorFactoryBase& creatorFactory, bool sorting) : SimpleDialogueSubView (id, document), mButtons (nullptr) { // bottom box mBottom = new TableBottomBox (creatorFactory, document, id, this); connect (mBottom, SIGNAL (requestFocus (const std::string&)), this, SLOT (requestFocus (const std::string&))); // layout getMainLayout().addWidget (mBottom); connect (&CSMPrefs::State::get(), SIGNAL (settingChanged (const CSMPrefs::Setting *)), this, SLOT (settingChanged (const CSMPrefs::Setting *))); CSMPrefs::get()["ID Dialogues"].update(); } void CSVWorld::DialogueSubView::setEditLock (bool locked) { SimpleDialogueSubView::setEditLock (locked); if (mButtons) mButtons->setEditLock (locked); } void CSVWorld::DialogueSubView::settingChanged (const CSMPrefs::Setting *setting) { if (*setting=="ID Dialogues/toolbar") { if (setting->isTrue()) { addButtonBar(); } else if (mButtons) { getMainLayout().removeWidget (mButtons); delete mButtons; mButtons = nullptr; } } } void CSVWorld::DialogueSubView::showPreview () { int idColumn = getTable().findColumnIndex (CSMWorld::Columns::ColumnId_Id); QModelIndex currentIndex (getTable().getModelIndex (getUniversalId().getId(), idColumn)); if (currentIndex.isValid() && getTable().getFeatures() & CSMWorld::IdTable::Feature_Preview && currentIndex.row() < getTable().rowCount()) { emit focusId(CSMWorld::UniversalId(CSMWorld::UniversalId::Type_Preview, getUniversalId().getId()), ""); } } void CSVWorld::DialogueSubView::viewRecord () { int idColumn = getTable().findColumnIndex (CSMWorld::Columns::ColumnId_Id); QModelIndex currentIndex (getTable().getModelIndex (getUniversalId().getId(), idColumn)); if (currentIndex.isValid() && currentIndex.row() < getTable().rowCount()) { std::pair params = getTable().view (currentIndex.row()); if (params.first.getType()!=CSMWorld::UniversalId::Type_None) emit focusId (params.first, params.second); } } void CSVWorld::DialogueSubView::switchToRow (int row) { int idColumn = getTable().findColumnIndex (CSMWorld::Columns::ColumnId_Id); std::string id = getTable().data (getTable().index (row, idColumn)).toString().toUtf8().constData(); int typeColumn = getTable().findColumnIndex (CSMWorld::Columns::ColumnId_RecordType); CSMWorld::UniversalId::Type type = static_cast ( getTable().data (getTable().index (row, typeColumn)).toInt()); setUniversalId (CSMWorld::UniversalId (type, id)); updateCurrentId(); getEditWidget().remake (row); int stateColumn = getTable().findColumnIndex (CSMWorld::Columns::ColumnId_Modification); CSMWorld::RecordBase::State state = static_cast ( getTable().data (getTable().index (row, stateColumn)).toInt()); getEditWidget().setDisabled (isLocked() || state==CSMWorld::RecordBase::State_Deleted); } void CSVWorld::DialogueSubView::requestFocus (const std::string& id) { int idColumn = getTable().findColumnIndex (CSMWorld::Columns::ColumnId_Id); QModelIndex index = getTable().getModelIndex (id, idColumn); if (index.isValid()) switchToRow (index.row()); } ================================================ FILE: apps/opencs/view/world/dialoguesubview.hpp ================================================ #ifndef CSV_WORLD_DIALOGUESUBVIEW_H #define CSV_WORLD_DIALOGUESUBVIEW_H #include #include #include #include #include #ifndef Q_MOC_RUN #include "../doc/subview.hpp" #include "../../model/world/columnbase.hpp" #include "../../model/world/commanddispatcher.hpp" #include "../../model/world/universalid.hpp" #endif class QDataWidgetMapper; class QSize; class QEvent; class QLabel; class QVBoxLayout; class QMenu; namespace CSMWorld { class IdTable; class NestedTableProxyModel; } namespace CSMPrefs { class Setting; } namespace CSMDoc { class Document; } namespace CSVWorld { class CommandDelegate; class CreatorFactoryBase; class TableBottomBox; class NotEditableSubDelegate : public QAbstractItemDelegate { const CSMWorld::IdTable* mTable; public: NotEditableSubDelegate(const CSMWorld::IdTable* table, QObject * parent = nullptr); void setEditorData (QWidget* editor, const QModelIndex& index) const override; void setModelData (QWidget* editor, QAbstractItemModel* model, const QModelIndex& index) const override; void paint (QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& index) const override; ///< does nothing QSize sizeHint (const QStyleOptionViewItem& option, const QModelIndex& index) const override; ///< does nothing QWidget *createEditor (QWidget *parent, const QStyleOptionViewItem& option, const QModelIndex& index) const override; }; //this can't be nested into the DialogueDelegateDispatcher, because it needs to emit signals class DialogueDelegateDispatcherProxy : public QObject { Q_OBJECT class refWrapper { public: refWrapper(const QModelIndex& index); const QModelIndex& mIndex; }; QWidget* mEditor; CSMWorld::ColumnBase::Display mDisplay; std::unique_ptr mIndexWrapper; public: DialogueDelegateDispatcherProxy(QWidget* editor, CSMWorld::ColumnBase::Display display); QWidget* getEditor() const; public slots: void editorDataCommited(); void setIndex(const QModelIndex& index); signals: void editorDataCommited(QWidget* editor, const QModelIndex& index, CSMWorld::ColumnBase::Display display); }; class DialogueDelegateDispatcher : public QAbstractItemDelegate { Q_OBJECT std::map mDelegates; QObject* mParent; QAbstractItemModel* mTable; CSMWorld::CommandDispatcher& mCommandDispatcher; CSMDoc::Document& mDocument; NotEditableSubDelegate mNotEditableDelegate; std::vector mProxys; //once we move to the C++11 we should use unique_ptr public: DialogueDelegateDispatcher(QObject* parent, CSMWorld::IdTable* table, CSMWorld::CommandDispatcher& commandDispatcher, CSMDoc::Document& document, QAbstractItemModel* model = nullptr); ~DialogueDelegateDispatcher(); CSVWorld::CommandDelegate* makeDelegate(CSMWorld::ColumnBase::Display display); QWidget* makeEditor(CSMWorld::ColumnBase::Display display, const QModelIndex& index); ///< will return null if delegate is not present, parent of the widget is //same as for dispatcher itself void setEditorData (QWidget* editor, const QModelIndex& index) const override; void setModelData (QWidget* editor, QAbstractItemModel* model, const QModelIndex& index) const override; virtual void setModelData (QWidget* editor, QAbstractItemModel* model, const QModelIndex& index, CSMWorld::ColumnBase::Display display) const; void paint (QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& index) const override; ///< does nothing QSize sizeHint (const QStyleOptionViewItem& option, const QModelIndex& index) const override; ///< does nothing private slots: void editorDataCommited(QWidget* editor, const QModelIndex& index, CSMWorld::ColumnBase::Display display); }; /// A context menu with "Edit 'ID'" action for editors in the dialogue subview class IdContextMenu : public QObject { Q_OBJECT QWidget *mWidget; CSMWorld::UniversalId::Type mIdType; std::set mExcludedIds; ///< A list of IDs that should not have the Edit 'ID' action. QMenu *mContextMenu; QAction *mEditIdAction; QString getWidgetValue() const; void addEditIdActionToMenu(const QString &text); void removeEditIdActionFromMenu(); public: IdContextMenu(QWidget *widget, CSMWorld::ColumnBase::Display display); void excludeId(const std::string &id); private slots: void showContextMenu(const QPoint &pos); void editIdRequest(); signals: void editIdRequest(const CSMWorld::UniversalId &id, const std::string &hint); }; class EditWidget : public QScrollArea { Q_OBJECT QDataWidgetMapper *mWidgetMapper; QDataWidgetMapper *mNestedTableMapper; DialogueDelegateDispatcher *mDispatcher; DialogueDelegateDispatcher *mNestedTableDispatcher; QWidget* mMainWidget; CSMWorld::IdTable* mTable; CSMWorld::CommandDispatcher& mCommandDispatcher; CSMDoc::Document& mDocument; std::vector mNestedModels; //Plain, raw C pointers, deleted in the dtor void createEditorContextMenu(QWidget *editor, CSMWorld::ColumnBase::Display display, int currentRow) const; public: EditWidget (QWidget *parent, int row, CSMWorld::IdTable* table, CSMWorld::CommandDispatcher& commandDispatcher, CSMDoc::Document& document, bool createAndDelete = false); virtual ~EditWidget(); void remake(int row); signals: void editIdRequest(const CSMWorld::UniversalId &id, const std::string &hint); }; class SimpleDialogueSubView : public CSVDoc::SubView { Q_OBJECT EditWidget* mEditWidget; QVBoxLayout* mMainLayout; CSMWorld::IdTable* mTable; bool mLocked; const CSMDoc::Document& mDocument; CSMWorld::CommandDispatcher mCommandDispatcher; protected: QVBoxLayout& getMainLayout(); CSMWorld::IdTable& getTable(); CSMWorld::CommandDispatcher& getCommandDispatcher(); EditWidget& getEditWidget(); void updateCurrentId(); bool isLocked() const; public: SimpleDialogueSubView (const CSMWorld::UniversalId& id, CSMDoc::Document& document); void setEditLock (bool locked) override; private slots: void dataChanged(const QModelIndex & index); ///\brief we need to care for deleting currently edited record void rowsAboutToBeRemoved(const QModelIndex &parent, int start, int end); }; class RecordButtonBar; class DialogueSubView : public SimpleDialogueSubView { Q_OBJECT TableBottomBox* mBottom; RecordButtonBar *mButtons; private: void addButtonBar(); public: DialogueSubView (const CSMWorld::UniversalId& id, CSMDoc::Document& document, const CreatorFactoryBase& creatorFactory, bool sorting = false); void setEditLock (bool locked) override; private slots: void settingChanged (const CSMPrefs::Setting *setting); void showPreview(); void viewRecord(); void switchToRow (int row); void requestFocus (const std::string& id); }; } #endif ================================================ FILE: apps/opencs/view/world/dragdroputils.cpp ================================================ #include "dragdroputils.hpp" #include #include "../../model/world/tablemimedata.hpp" const CSMWorld::TableMimeData *CSVWorld::DragDropUtils::getTableMimeData(const QDropEvent &event) { return dynamic_cast(event.mimeData()); } bool CSVWorld::DragDropUtils::canAcceptData(const QDropEvent &event, CSMWorld::ColumnBase::Display type) { const CSMWorld::TableMimeData *data = getTableMimeData(event); return data != nullptr && data->holdsType(type); } bool CSVWorld::DragDropUtils::isInfo(const QDropEvent &event, CSMWorld::ColumnBase::Display type) { const CSMWorld::TableMimeData *data = getTableMimeData(event); return data != nullptr && ( data->holdsType(CSMWorld::UniversalId::Type_TopicInfo) || data->holdsType(CSMWorld::UniversalId::Type_JournalInfo) ); } CSMWorld::UniversalId CSVWorld::DragDropUtils::getAcceptedData(const QDropEvent &event, CSMWorld::ColumnBase::Display type) { if (canAcceptData(event, type)) { if (const CSMWorld::TableMimeData *data = getTableMimeData(event)) return data->returnMatching(type); } return CSMWorld::UniversalId::Type_None; } ================================================ FILE: apps/opencs/view/world/dragdroputils.hpp ================================================ #ifndef CSV_WORLD_DRAGDROPUTILS_HPP #define CSV_WORLD_DRAGDROPUTILS_HPP #include "../../model/world/columnbase.hpp" class QDropEvent; namespace CSMWorld { class TableMimeData; class UniversalId; } namespace CSVWorld { namespace DragDropUtils { const CSMWorld::TableMimeData *getTableMimeData(const QDropEvent &event); bool canAcceptData(const QDropEvent &event, CSMWorld::ColumnBase::Display type); ///< Checks whether the \a event contains a valid CSMWorld::TableMimeData that holds the \a type bool isInfo(const QDropEvent &event, CSMWorld::ColumnBase::Display type); ///< Info types can be dragged to sort the info table CSMWorld::UniversalId getAcceptedData(const QDropEvent &event, CSMWorld::ColumnBase::Display type); ///< Gets the accepted data from the \a event using the \a type ///< \return Type_None if the \a event data doesn't holds the \a type } } #endif ================================================ FILE: apps/opencs/view/world/dragrecordtable.cpp ================================================ #include "dragrecordtable.hpp" #include #include #include "../../model/doc/document.hpp" #include "../../model/world/tablemimedata.hpp" #include "../../model/world/commands.hpp" #include "dragdroputils.hpp" void CSVWorld::DragRecordTable::startDragFromTable (const CSVWorld::DragRecordTable& table) { std::vector records = table.getDraggedRecords(); if (records.empty()) { return; } CSMWorld::TableMimeData* mime = new CSMWorld::TableMimeData (records, mDocument); QDrag* drag = new QDrag (this); drag->setMimeData (mime); drag->setPixmap (QString::fromUtf8 (mime->getIcon().c_str())); drag->exec (Qt::CopyAction); } CSVWorld::DragRecordTable::DragRecordTable (CSMDoc::Document& document, QWidget* parent) : QTableView(parent), mDocument(document), mEditLock(false) { setAcceptDrops(true); } void CSVWorld::DragRecordTable::setEditLock (bool locked) { mEditLock = locked; } void CSVWorld::DragRecordTable::dragEnterEvent(QDragEnterEvent *event) { event->acceptProposedAction(); } void CSVWorld::DragRecordTable::dragMoveEvent(QDragMoveEvent *event) { QModelIndex index = indexAt(event->pos()); if (CSVWorld::DragDropUtils::canAcceptData(*event, getIndexDisplayType(index)) || CSVWorld::DragDropUtils::isInfo(*event, getIndexDisplayType(index)) ) { if (index.flags() & Qt::ItemIsEditable) { event->accept(); return; } } event->ignore(); } void CSVWorld::DragRecordTable::dropEvent(QDropEvent *event) { QModelIndex index = indexAt(event->pos()); CSMWorld::ColumnBase::Display display = getIndexDisplayType(index); if (CSVWorld::DragDropUtils::canAcceptData(*event, display)) { const CSMWorld::TableMimeData *tableMimeData = CSVWorld::DragDropUtils::getTableMimeData(*event); if (tableMimeData->fromDocument(mDocument)) { CSMWorld::UniversalId id = CSVWorld::DragDropUtils::getAcceptedData(*event, display); QVariant newIndexData = QString::fromUtf8(id.getId().c_str()); QVariant oldIndexData = index.data(Qt::EditRole); if (newIndexData != oldIndexData) { mDocument.getUndoStack().push(new CSMWorld::ModifyCommand(*model(), index, newIndexData)); } } } else if (CSVWorld::DragDropUtils::isInfo(*event, display) && event->source() == this) { emit moveRecordsFromSameTable(event); } } CSMWorld::ColumnBase::Display CSVWorld::DragRecordTable::getIndexDisplayType(const QModelIndex &index) const { Q_ASSERT(model() != nullptr); if (index.isValid()) { QVariant display = model()->headerData(index.column(), Qt::Horizontal, CSMWorld::ColumnBase::Role_Display); if (display.isValid()) { return static_cast(display.toInt()); } } return CSMWorld::ColumnBase::Display_None; } ================================================ FILE: apps/opencs/view/world/dragrecordtable.hpp ================================================ #ifndef CSV_WORLD_DRAGRECORDTABLE_H #define CSV_WORLD_DRAGRECORDTABLE_H #include #include #include "../../model/world/columnbase.hpp" class QWidget; class QAction; namespace CSMDoc { class Document; } namespace CSMWorld { class UniversalId; } namespace CSVWorld { class DragRecordTable : public QTableView { Q_OBJECT protected: CSMDoc::Document& mDocument; bool mEditLock; public: DragRecordTable(CSMDoc::Document& document, QWidget* parent = nullptr); virtual std::vector getDraggedRecords() const = 0; void setEditLock(bool locked); protected: void startDragFromTable(const DragRecordTable& table); void dragEnterEvent(QDragEnterEvent *event) override; void dragMoveEvent(QDragMoveEvent *event) override; void dropEvent(QDropEvent *event) override; private: CSMWorld::ColumnBase::Display getIndexDisplayType(const QModelIndex &index) const; signals: void moveRecordsFromSameTable(QDropEvent *event); }; } #endif ================================================ FILE: apps/opencs/view/world/enumdelegate.cpp ================================================ #include "enumdelegate.hpp" #include #include #include #include #include #include "../../model/world/commands.hpp" int CSVWorld::EnumDelegate::getValueIndex(const QModelIndex &index, int role) const { if (index.isValid() && index.data(role).isValid()) { int value = index.data(role).toInt(); int size = static_cast(mValues.size()); for (int i = 0; i < size; ++i) { if (value == mValues.at(i).first) { return i; } } } return -1; } void CSVWorld::EnumDelegate::setModelDataImp (QWidget *editor, QAbstractItemModel *model, const QModelIndex& index) const { if (QComboBox *comboBox = dynamic_cast (editor)) { QString value = comboBox->currentText(); for (std::vector >::const_iterator iter (mValues.begin()); iter!=mValues.end(); ++iter) if (iter->second==value) { // do nothing if the value has not changed if (model->data(index).toInt() != iter->first) addCommands (model, index, iter->first); break; } } } void CSVWorld::EnumDelegate::addCommands (QAbstractItemModel *model, const QModelIndex& index, int type) const { getUndoStack().push (new CSMWorld::ModifyCommand (*model, index, type)); } CSVWorld::EnumDelegate::EnumDelegate (const std::vector >& values, CSMWorld::CommandDispatcher *dispatcher, CSMDoc::Document& document, QObject *parent) : CommandDelegate (dispatcher, document, parent), mValues (values) { } QWidget *CSVWorld::EnumDelegate::createEditor(QWidget *parent, const QStyleOptionViewItem& option, const QModelIndex& index) const { return createEditor(parent, option, index, CSMWorld::ColumnBase::Display_None); } QWidget *CSVWorld::EnumDelegate::createEditor(QWidget *parent, const QStyleOptionViewItem& option, const QModelIndex& index, CSMWorld::ColumnBase::Display display) const { if (!index.data(Qt::EditRole).isValid() && !index.data(Qt::DisplayRole).isValid()) return nullptr; QComboBox *comboBox = new QComboBox (parent); for (std::vector >::const_iterator iter (mValues.begin()); iter!=mValues.end(); ++iter) comboBox->addItem (iter->second); return comboBox; } void CSVWorld::EnumDelegate::setEditorData (QWidget *editor, const QModelIndex& index, bool tryDisplay) const { if (QComboBox *comboBox = dynamic_cast(editor)) { int role = Qt::EditRole; if (tryDisplay && !index.data(role).isValid()) { role = Qt::DisplayRole; if (!index.data(role).isValid()) { return; } } int valueIndex = getValueIndex(index, role); if (valueIndex != -1) { comboBox->setCurrentIndex(valueIndex); } } } void CSVWorld::EnumDelegate::paint (QPainter *painter, const QStyleOptionViewItem& option, const QModelIndex& index) const { int valueIndex = getValueIndex(index); if (valueIndex != -1) { QStyleOptionViewItem itemOption(option); itemOption.text = mValues.at(valueIndex).second; QApplication::style()->drawControl(QStyle::CE_ItemViewItem, &itemOption, painter); } } QSize CSVWorld::EnumDelegate::sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const { int valueIndex = getValueIndex(index); if (valueIndex != -1) { // Calculate the size hint as for a combobox. // So, the whole text is visible (isn't elided) when the editor is created QStyleOptionComboBox itemOption; itemOption.fontMetrics = option.fontMetrics; itemOption.palette = option.palette; itemOption.rect = option.rect; itemOption.state = option.state; const QString &valueText = mValues.at(valueIndex).second; QSize valueSize = QSize(itemOption.fontMetrics.horizontalAdvance(valueText), itemOption.fontMetrics.height()); itemOption.currentText = valueText; return QApplication::style()->sizeFromContents(QStyle::CT_ComboBox, &itemOption, valueSize); } return option.rect.size(); } CSVWorld::EnumDelegateFactory::EnumDelegateFactory() {} CSVWorld::EnumDelegateFactory::EnumDelegateFactory (const char **names, bool allowNone) { assert (names); if (allowNone) add (-1, ""); for (int i=0; names[i]; ++i) add (i, names[i]); } CSVWorld::EnumDelegateFactory::EnumDelegateFactory (const std::vector>& names, bool allowNone) { if (allowNone) add (-1, ""); int size = static_cast (names.size()); for (int i=0; isecond > name) { mValues.insert(it, pair); return; } } mValues.emplace_back (value, name); } ================================================ FILE: apps/opencs/view/world/enumdelegate.hpp ================================================ #ifndef CSV_WORLD_ENUMDELEGATE_H #define CSV_WORLD_ENUMDELEGATE_H #include #include #include #include #include "util.hpp" namespace CSVWorld { /// \brief Integer value that represents an enum and is interacted with via a combobox class EnumDelegate : public CommandDelegate { protected: std::vector > mValues; int getValueIndex(const QModelIndex &index, int role = Qt::DisplayRole) const; private: void setModelDataImp (QWidget *editor, QAbstractItemModel *model, const QModelIndex& index) const override; virtual void addCommands (QAbstractItemModel *model, const QModelIndex& index, int type) const; public: EnumDelegate (const std::vector >& values, CSMWorld::CommandDispatcher *dispatcher, CSMDoc::Document& document, QObject *parent); QWidget *createEditor(QWidget *parent, const QStyleOptionViewItem& option, const QModelIndex& index) const override; QWidget *createEditor(QWidget *parent, const QStyleOptionViewItem& option, const QModelIndex& index, CSMWorld::ColumnBase::Display display = CSMWorld::ColumnBase::Display_None) const override; void setEditorData (QWidget *editor, const QModelIndex& index, bool tryDisplay = false) const override; void paint (QPainter *painter, const QStyleOptionViewItem& option, const QModelIndex& index) const override; QSize sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const override; }; class EnumDelegateFactory : public CommandDelegateFactory { protected: std::vector > mValues; public: EnumDelegateFactory(); EnumDelegateFactory (const char **names, bool allowNone = false); ///< \param names Array of char pointer with a 0-pointer as end mark /// \param allowNone Use value of -1 for "none selected" (empty string) EnumDelegateFactory (const std::vector>& names, bool allowNone = false); /// \param allowNone Use value of -1 for "none selected" (empty string) CommandDelegate *makeDelegate (CSMWorld::CommandDispatcher *dispatcher, CSMDoc::Document& document, QObject *parent) const override; ///< The ownership of the returned CommandDelegate is transferred to the caller. void add (int value, const QString& name); }; } #endif ================================================ FILE: apps/opencs/view/world/extendedcommandconfigurator.cpp ================================================ #include "extendedcommandconfigurator.hpp" #include #include #include #include #include #include #include "../../model/doc/document.hpp" #include "../../model/world/commanddispatcher.hpp" #include "../../model/world/data.hpp" CSVWorld::ExtendedCommandConfigurator::ExtendedCommandConfigurator(CSMDoc::Document &document, const CSMWorld::UniversalId &id, QWidget *parent) : QWidget(parent), mNumUsedCheckBoxes(0), mNumChecked(0), mMode(Mode_None), mData(document.getData()), mEditLock(false) { mCommandDispatcher = new CSMWorld::CommandDispatcher(document, id, this); connect(&mData, SIGNAL(idListChanged()), this, SLOT(dataIdListChanged())); mPerformButton = new QPushButton(this); mPerformButton->setDefault(true); mPerformButton->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed); connect(mPerformButton, SIGNAL(clicked(bool)), this, SLOT(performExtendedCommand())); mCancelButton = new QPushButton("Cancel", this); mCancelButton->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed); connect(mCancelButton, SIGNAL(clicked(bool)), this, SIGNAL(done())); mTypeGroup = new QGroupBox(this); QGridLayout *groupLayout = new QGridLayout(mTypeGroup); groupLayout->setAlignment(Qt::AlignLeft | Qt::AlignVCenter); mTypeGroup->setLayout(groupLayout); QHBoxLayout *mainLayout = new QHBoxLayout(this); mainLayout->setSizeConstraint(QLayout::SetNoConstraint); mainLayout->setContentsMargins(0, 0, 0, 0); mainLayout->addWidget(mTypeGroup); mainLayout->addWidget(mPerformButton); mainLayout->addWidget(mCancelButton); } void CSVWorld::ExtendedCommandConfigurator::configure(CSVWorld::ExtendedCommandConfigurator::Mode mode, const std::vector &selectedIds) { mMode = mode; if (mMode != Mode_None) { mPerformButton->setText((mMode == Mode_Delete) ? "Extended Delete" : "Extended Revert"); mSelectedIds = selectedIds; mCommandDispatcher->setSelection(mSelectedIds); setupCheckBoxes(mCommandDispatcher->getExtendedTypes()); setupGroupLayout(); lockWidgets(mEditLock); } } void CSVWorld::ExtendedCommandConfigurator::setEditLock(bool locked) { if (mEditLock != locked) { mEditLock = locked; lockWidgets(mEditLock); } } void CSVWorld::ExtendedCommandConfigurator::resizeEvent(QResizeEvent *event) { QWidget::resizeEvent(event); setupGroupLayout(); } void CSVWorld::ExtendedCommandConfigurator::setupGroupLayout() { if (mMode == Mode_None) { return; } int groupWidth = mTypeGroup->geometry().width(); QGridLayout *layout = qobject_cast(mTypeGroup->layout()); // Find the optimal number of rows to place the checkboxes within the available space int divider = 1; do { while (layout->itemAt(0) != nullptr) { layout->removeItem(layout->itemAt(0)); } int counter = 0; int itemsPerRow = mNumUsedCheckBoxes / divider; CheckBoxMap::const_iterator current = mTypeCheckBoxes.begin(); CheckBoxMap::const_iterator end = mTypeCheckBoxes.end(); for (; current != end; ++current) { if (counter < mNumUsedCheckBoxes) { int row = counter / itemsPerRow; int column = counter - (counter / itemsPerRow) * itemsPerRow; layout->addWidget(current->first, row, column); } ++counter; } divider *= 2; } while (groupWidth < mTypeGroup->sizeHint().width() && divider <= mNumUsedCheckBoxes); } void CSVWorld::ExtendedCommandConfigurator::setupCheckBoxes(const std::vector &types) { // Make sure that we have enough checkboxes int numTypes = static_cast(types.size()); int numCheckBoxes = static_cast(mTypeCheckBoxes.size()); if (numTypes > numCheckBoxes) { for (int i = numTypes - numCheckBoxes; i > 0; --i) { QCheckBox *checkBox = new QCheckBox(mTypeGroup); connect(checkBox, SIGNAL(stateChanged(int)), this, SLOT(checkBoxStateChanged(int))); mTypeCheckBoxes.insert(std::make_pair(checkBox, CSMWorld::UniversalId::Type_None)); } } // Set up the checkboxes int counter = 0; CheckBoxMap::iterator current = mTypeCheckBoxes.begin(); CheckBoxMap::iterator end = mTypeCheckBoxes.end(); for (; current != end; ++current) { if (counter < numTypes) { CSMWorld::UniversalId type = types[counter]; current->first->setText(QString::fromUtf8(type.getTypeName().c_str())); current->first->setChecked(true); current->second = type; ++counter; } else { current->first->hide(); } } mNumChecked = mNumUsedCheckBoxes = numTypes; } void CSVWorld::ExtendedCommandConfigurator::lockWidgets(bool locked) { mPerformButton->setEnabled(!mEditLock && mNumChecked > 0); CheckBoxMap::const_iterator current = mTypeCheckBoxes.begin(); CheckBoxMap::const_iterator end = mTypeCheckBoxes.end(); for (int i = 0; current != end && i < mNumUsedCheckBoxes; ++current, ++i) { current->first->setEnabled(!mEditLock); } } void CSVWorld::ExtendedCommandConfigurator::performExtendedCommand() { std::vector types; CheckBoxMap::const_iterator current = mTypeCheckBoxes.begin(); CheckBoxMap::const_iterator end = mTypeCheckBoxes.end(); for (; current != end; ++current) { if (current->first->isChecked()) { types.push_back(current->second); } } mCommandDispatcher->setExtendedTypes(types); if (mMode == Mode_Delete) { mCommandDispatcher->executeExtendedDelete(); } else { mCommandDispatcher->executeExtendedRevert(); } emit done(); } void CSVWorld::ExtendedCommandConfigurator::checkBoxStateChanged(int state) { switch (state) { case Qt::Unchecked: --mNumChecked; break; case Qt::Checked: ++mNumChecked; break; case Qt::PartiallyChecked: // Not used break; } mPerformButton->setEnabled(mNumChecked > 0); } void CSVWorld::ExtendedCommandConfigurator::dataIdListChanged() { bool idsRemoved = false; for (int i = 0; i < static_cast(mSelectedIds.size()); ++i) { if (!mData.hasId(mSelectedIds[i])) { std::swap(mSelectedIds[i], mSelectedIds.back()); mSelectedIds.pop_back(); idsRemoved = true; --i; } } // If all selected IDs were removed, cancel the configurator if (mSelectedIds.empty()) { emit done(); return; } if (idsRemoved) { mCommandDispatcher->setSelection(mSelectedIds); } } ================================================ FILE: apps/opencs/view/world/extendedcommandconfigurator.hpp ================================================ #ifndef CSVWORLD_EXTENDEDCOMMANDCONFIGURATOR_HPP #define CSVWORLD_EXTENDEDCOMMANDCONFIGURATOR_HPP #include #include #include "../../model/world/universalid.hpp" class QPushButton; class QGroupBox; class QCheckBox; class QLabel; class QHBoxLayout; namespace CSMDoc { class Document; } namespace CSMWorld { class CommandDispatcher; class Data; } namespace CSVWorld { class ExtendedCommandConfigurator : public QWidget { Q_OBJECT public: enum Mode { Mode_None, Mode_Delete, Mode_Revert }; private: typedef std::map CheckBoxMap; QPushButton *mPerformButton; QPushButton *mCancelButton; QGroupBox *mTypeGroup; CheckBoxMap mTypeCheckBoxes; int mNumUsedCheckBoxes; int mNumChecked; Mode mMode; CSMWorld::CommandDispatcher *mCommandDispatcher; CSMWorld::Data &mData; std::vector mSelectedIds; bool mEditLock; void setupGroupLayout(); void setupCheckBoxes(const std::vector &types); void lockWidgets(bool locked); public: ExtendedCommandConfigurator(CSMDoc::Document &document, const CSMWorld::UniversalId &id, QWidget *parent = nullptr); void configure(Mode mode, const std::vector &selectedIds); void setEditLock(bool locked); protected: void resizeEvent(QResizeEvent *event) override; private slots: void performExtendedCommand(); void checkBoxStateChanged(int state); void dataIdListChanged(); signals: void done(); }; } #endif ================================================ FILE: apps/opencs/view/world/genericcreator.cpp ================================================ #include "genericcreator.hpp" #include #include #include #include #include #include #include #include #include "../../model/world/commands.hpp" #include "../../model/world/data.hpp" #include "../../model/world/idtable.hpp" #include "idvalidator.hpp" void CSVWorld::GenericCreator::update() { mErrors = getErrors(); mCreate->setToolTip (QString::fromUtf8 (mErrors.c_str())); mId->setToolTip (QString::fromUtf8 (mErrors.c_str())); mCreate->setEnabled (mErrors.empty() && !mLocked); } void CSVWorld::GenericCreator::setManualEditing (bool enabled) { mId->setVisible (enabled); } void CSVWorld::GenericCreator::insertAtBeginning (QWidget *widget, bool stretched) { mLayout->insertWidget (0, widget, stretched ? 1 : 0); } void CSVWorld::GenericCreator::insertBeforeButtons (QWidget *widget, bool stretched) { mLayout->insertWidget (mLayout->count()-2, widget, stretched ? 1 : 0); // Reset tab order relative to buttons. setTabOrder(widget, mCreate); setTabOrder(mCreate, mCancel); } std::string CSVWorld::GenericCreator::getId() const { return mId->text().toUtf8().constData(); } std::string CSVWorld::GenericCreator::getClonedId() const { return mClonedId; } std::string CSVWorld::GenericCreator::getIdValidatorResult() const { std::string errors; if (!mId->hasAcceptableInput()) errors = mValidator->getError(); return errors; } void CSVWorld::GenericCreator::configureCreateCommand (CSMWorld::CreateCommand& command) const {} void CSVWorld::GenericCreator::pushCommand (std::unique_ptr command, const std::string& id) { mUndoStack.push (command.release()); } CSMWorld::Data& CSVWorld::GenericCreator::getData() const { return mData; } QUndoStack& CSVWorld::GenericCreator::getUndoStack() { return mUndoStack; } const CSMWorld::UniversalId& CSVWorld::GenericCreator::getCollectionId() const { return mListId; } std::string CSVWorld::GenericCreator::getNamespace() const { CSMWorld::Scope scope = CSMWorld::Scope_Content; if (mScope) { scope = static_cast (mScope->itemData (mScope->currentIndex()).toInt()); } else { if (mScopes & CSMWorld::Scope_Project) scope = CSMWorld::Scope_Project; else if (mScopes & CSMWorld::Scope_Session) scope = CSMWorld::Scope_Session; } switch (scope) { case CSMWorld::Scope_Content: return ""; case CSMWorld::Scope_Project: return "project::"; case CSMWorld::Scope_Session: return "session::"; } return ""; } void CSVWorld::GenericCreator::updateNamespace() { std::string namespace_ = getNamespace(); mValidator->setNamespace (namespace_); int index = mId->text().indexOf ("::"); if (index==-1) { // no namespace in old text mId->setText (QString::fromUtf8 (namespace_.c_str()) + mId->text()); } else { std::string oldNamespace = Misc::StringUtils::lowerCase (mId->text().left (index).toUtf8().constData()); if (oldNamespace=="project" || oldNamespace=="session") mId->setText (QString::fromUtf8 (namespace_.c_str()) + mId->text().mid (index+2)); } } void CSVWorld::GenericCreator::addScope (const QString& name, CSMWorld::Scope scope, const QString& tooltip) { mScope->addItem (name, static_cast (scope)); mScope->setItemData (mScope->count()-1, tooltip, Qt::ToolTipRole); } CSVWorld::GenericCreator::GenericCreator (CSMWorld::Data& data, QUndoStack& undoStack, const CSMWorld::UniversalId& id, bool relaxedIdRules) : mData (data), mUndoStack (undoStack), mListId (id), mLocked (false), mClonedType (CSMWorld::UniversalId::Type_None), mScopes (CSMWorld::Scope_Content), mScope (nullptr), mScopeLabel (nullptr), mCloneMode (false) { // If the collection ID has a parent type, use it instead. // It will change IDs with Record/SubRecord class (used for creators in Dialogue subviews) // to IDs with general RecordList class (used for creators in Table subviews). CSMWorld::UniversalId::Type listParentType = CSMWorld::UniversalId::getParentType(mListId.getType()); if (listParentType != CSMWorld::UniversalId::Type_None) { mListId = listParentType; } mLayout = new QHBoxLayout; mLayout->setContentsMargins (0, 0, 0, 0); mId = new QLineEdit; mId->setValidator (mValidator = new IdValidator (relaxedIdRules, this)); mLayout->addWidget (mId, 1); mCreate = new QPushButton ("Create"); mLayout->addWidget (mCreate); mCancel = new QPushButton("Cancel"); mLayout->addWidget(mCancel); setLayout (mLayout); connect (mCancel, SIGNAL (clicked (bool)), this, SIGNAL (done())); connect (mCreate, SIGNAL (clicked (bool)), this, SLOT (create())); connect (mId, SIGNAL (textChanged (const QString&)), this, SLOT (textChanged (const QString&))); connect (mId, SIGNAL (returnPressed()), this, SLOT (inputReturnPressed())); connect (&mData, SIGNAL (idListChanged()), this, SLOT (dataIdListChanged())); } void CSVWorld::GenericCreator::setEditLock (bool locked) { mLocked = locked; update(); } void CSVWorld::GenericCreator::reset() { mCloneMode = false; mId->setText (""); update(); updateNamespace(); } std::string CSVWorld::GenericCreator::getErrors() const { std::string errors; if (!mId->hasAcceptableInput()) errors = mValidator->getError(); else if (mData.hasId (getId())) errors = "ID is already in use"; return errors; } void CSVWorld::GenericCreator::textChanged (const QString& text) { update(); } void CSVWorld::GenericCreator::inputReturnPressed() { if (mCreate->isEnabled()) { create(); } } void CSVWorld::GenericCreator::create() { if (!mLocked) { std::string id = getId(); std::unique_ptr command; if (mCloneMode) { command.reset (new CSMWorld::CloneCommand ( dynamic_cast (*mData.getTableModel(mListId)), mClonedId, id, mClonedType)); } else { command.reset (new CSMWorld::CreateCommand ( dynamic_cast (*mData.getTableModel (mListId)), id)); } configureCreateCommand (*command); pushCommand (std::move(command), id); emit done(); emit requestFocus(id); } } void CSVWorld::GenericCreator::cloneMode(const std::string& originId, const CSMWorld::UniversalId::Type type) { mCloneMode = true; mClonedId = originId; mClonedType = type; } void CSVWorld::GenericCreator::touch(const std::vector& ids) { // Combine multiple touch commands into one "macro" command mUndoStack.beginMacro("Touch Records"); CSMWorld::IdTable& table = dynamic_cast(*mData.getTableModel(mListId)); for (const CSMWorld::UniversalId& uid : ids) { CSMWorld::TouchCommand* touchCmd = new CSMWorld::TouchCommand(table, uid.getId()); mUndoStack.push(touchCmd); } // Execute mUndoStack.endMacro(); } void CSVWorld::GenericCreator::toggleWidgets(bool active) { } void CSVWorld::GenericCreator::focus() { mId->setFocus(); } void CSVWorld::GenericCreator::setScope (unsigned int scope) { mScopes = scope; int count = (mScopes & CSMWorld::Scope_Content) + (mScopes & CSMWorld::Scope_Project) + (mScopes & CSMWorld::Scope_Session); // scope selector widget if (count>1) { mScope = new QComboBox (this); insertAtBeginning (mScope, false); if (mScopes & CSMWorld::Scope_Content) addScope ("Content", CSMWorld::Scope_Content, "Record will be stored in the currently edited content file."); if (mScopes & CSMWorld::Scope_Project) addScope ("Project", CSMWorld::Scope_Project, "Record will be stored in a local project file.

" "Record will be created in the reserved namespace \"project\".

" "Record is available when running OpenMW via OpenCS."); if (mScopes & CSMWorld::Scope_Session) addScope ("Session", CSMWorld::Scope_Session, "Record exists only for the duration of the current editing session.

" "Record will be created in the reserved namespace \"session\".

" "Record is not available when running OpenMW via OpenCS."); connect (mScope, SIGNAL (currentIndexChanged (int)), this, SLOT (scopeChanged (int))); mScopeLabel = new QLabel ("Scope", this); insertAtBeginning (mScopeLabel, false); mScope->setCurrentIndex (0); } else { delete mScope; mScope = nullptr; delete mScopeLabel; mScopeLabel = nullptr; } updateNamespace(); } void CSVWorld::GenericCreator::scopeChanged (int index) { update(); updateNamespace(); } void CSVWorld::GenericCreator::dataIdListChanged() { // If the original ID of cloned record was removed, cancel the creator if (mCloneMode && !mData.hasId(mClonedId)) { emit done(); } } ================================================ FILE: apps/opencs/view/world/genericcreator.hpp ================================================ #ifndef CSV_WORLD_GENERICCREATOR_H #define CSV_WORLD_GENERICCREATOR_H #include #include "../../model/world/universalid.hpp" #include "creator.hpp" class QString; class QPushButton; class QLineEdit; class QHBoxLayout; class QComboBox; class QLabel; class QUndoStack; namespace CSMWorld { class CreateCommand; class Data; } namespace CSVWorld { class IdValidator; class GenericCreator : public Creator { Q_OBJECT CSMWorld::Data& mData; QUndoStack& mUndoStack; CSMWorld::UniversalId mListId; QPushButton *mCreate; QPushButton *mCancel; QLineEdit *mId; std::string mErrors; QHBoxLayout *mLayout; bool mLocked; std::string mClonedId; CSMWorld::UniversalId::Type mClonedType; unsigned int mScopes; QComboBox *mScope; QLabel *mScopeLabel; IdValidator *mValidator; protected: bool mCloneMode; protected: void update(); virtual void setManualEditing (bool enabled); ///< Enable/disable manual ID editing (enabled by default). void insertAtBeginning (QWidget *widget, bool stretched); /// \brief Insert given widget before Create and Cancel buttons. /// \param widget Widget to add to layout. /// \param stretched Whether widget should be streched or not. void insertBeforeButtons (QWidget *widget, bool stretched); virtual std::string getId() const; std::string getClonedId() const; virtual std::string getIdValidatorResult() const; /// Allow subclasses to add additional data to \a command. virtual void configureCreateCommand (CSMWorld::CreateCommand& command) const; /// Allow subclasses to wrap the create command together with additional commands /// into a macro. virtual void pushCommand (std::unique_ptr command, const std::string& id); CSMWorld::Data& getData() const; QUndoStack& getUndoStack(); const CSMWorld::UniversalId& getCollectionId() const; std::string getNamespace() const; private: void updateNamespace(); void addScope (const QString& name, CSMWorld::Scope scope, const QString& tooltip); public: GenericCreator (CSMWorld::Data& data, QUndoStack& undoStack, const CSMWorld::UniversalId& id, bool relaxedIdRules = false); void setEditLock (bool locked) override; void reset() override; void toggleWidgets (bool active = true) override; void cloneMode(const std::string& originId, const CSMWorld::UniversalId::Type type) override; void touch(const std::vector& ids) override; virtual std::string getErrors() const; ///< Return formatted error descriptions for the current state of the creator. if an empty /// string is returned, there is no error. void setScope (unsigned int scope) override; /// Focus main input widget void focus() override; private slots: void textChanged (const QString& text); /// \brief Create record if able to after Return key is pressed on input. void inputReturnPressed(); void create(); void scopeChanged (int index); void dataIdListChanged(); }; } #endif ================================================ FILE: apps/opencs/view/world/globalcreator.cpp ================================================ #include "globalcreator.hpp" #include #include "../../model/world/data.hpp" #include "../../model/world/commands.hpp" #include "../../model/world/columns.hpp" #include "../../model/world/idtable.hpp" namespace CSVWorld { void GlobalCreator::configureCreateCommand (CSMWorld::CreateCommand& command) const { CSMWorld::IdTable* table = static_cast(getData().getTableModel(getCollectionId())); int index = table->findColumnIndex(CSMWorld::Columns::ColumnId_ValueType); int type = (int)ESM::VT_Float; command.addValue(index, type); } GlobalCreator::GlobalCreator(CSMWorld::Data& data, QUndoStack& undoStack, const CSMWorld::UniversalId& id) : GenericCreator (data, undoStack, id, true) { } } ================================================ FILE: apps/opencs/view/world/globalcreator.hpp ================================================ #ifndef CSV_WORLD_GLOBALCREATOR_H #define CSV_WORLD_GLOBALCREATOR_H #include "genericcreator.hpp" namespace CSVWorld { class GlobalCreator : public GenericCreator { Q_OBJECT public: GlobalCreator(CSMWorld::Data& data, QUndoStack& undoStack, const CSMWorld::UniversalId& id); protected: void configureCreateCommand(CSMWorld::CreateCommand& command) const override; }; } #endif ================================================ FILE: apps/opencs/view/world/idcompletiondelegate.cpp ================================================ #include "idcompletiondelegate.hpp" #include "../../model/world/idcompletionmanager.hpp" #include "../../model/world/infoselectwrapper.hpp" #include "../widget/droplineedit.hpp" CSVWorld::IdCompletionDelegate::IdCompletionDelegate(CSMWorld::CommandDispatcher *dispatcher, CSMDoc::Document& document, QObject *parent) : CommandDelegate(dispatcher, document, parent) {} QWidget *CSVWorld::IdCompletionDelegate::createEditor(QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index) const { return createEditor(parent, option, index, getDisplayTypeFromIndex(index)); } QWidget *CSVWorld::IdCompletionDelegate::createEditor(QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index, CSMWorld::ColumnBase::Display display) const { if (!index.data(Qt::EditRole).isValid() && !index.data(Qt::DisplayRole).isValid()) { return nullptr; } // The completer for InfoCondVar needs to return a completer based on the first column if (display == CSMWorld::ColumnBase::Display_InfoCondVar) { QModelIndex sibling = index.sibling(index.row(), 0); int conditionFunction = sibling.model()->data(sibling, Qt::EditRole).toInt(); switch (conditionFunction) { case CSMWorld::ConstInfoSelectWrapper::Function_Global: { return createEditor (parent, option, index, CSMWorld::ColumnBase::Display_GlobalVariable); } case CSMWorld::ConstInfoSelectWrapper::Function_Journal: { return createEditor (parent, option, index, CSMWorld::ColumnBase::Display_Journal); } case CSMWorld::ConstInfoSelectWrapper::Function_Item: { return createEditor (parent, option, index, CSMWorld::ColumnBase::Display_Referenceable); } case CSMWorld::ConstInfoSelectWrapper::Function_Dead: case CSMWorld::ConstInfoSelectWrapper::Function_NotId: { return createEditor (parent, option, index, CSMWorld::ColumnBase::Display_Referenceable); } case CSMWorld::ConstInfoSelectWrapper::Function_NotFaction: { return createEditor (parent, option, index, CSMWorld::ColumnBase::Display_Faction); } case CSMWorld::ConstInfoSelectWrapper::Function_NotClass: { return createEditor (parent, option, index, CSMWorld::ColumnBase::Display_Class); } case CSMWorld::ConstInfoSelectWrapper::Function_NotRace: { return createEditor (parent, option, index, CSMWorld::ColumnBase::Display_Race); } case CSMWorld::ConstInfoSelectWrapper::Function_NotCell: { return createEditor (parent, option, index, CSMWorld::ColumnBase::Display_Cell); } case CSMWorld::ConstInfoSelectWrapper::Function_Local: case CSMWorld::ConstInfoSelectWrapper::Function_NotLocal: { return new CSVWidget::DropLineEdit(display, parent); } default: return nullptr; // The rest of them can't be edited anyway } } CSMWorld::IdCompletionManager &completionManager = getDocument().getIdCompletionManager(); CSVWidget::DropLineEdit *editor = new CSVWidget::DropLineEdit(display, parent); editor->setCompleter(completionManager.getCompleter(display).get()); return editor; } CSVWorld::CommandDelegate *CSVWorld::IdCompletionDelegateFactory::makeDelegate(CSMWorld::CommandDispatcher *dispatcher, CSMDoc::Document& document, QObject *parent) const { return new IdCompletionDelegate(dispatcher, document, parent); } ================================================ FILE: apps/opencs/view/world/idcompletiondelegate.hpp ================================================ #ifndef CSV_WORLD_IDCOMPLETIONDELEGATE_HPP #define CSV_WORLD_IDCOMPLETIONDELEGATE_HPP #include "util.hpp" namespace CSVWorld { /// \brief Enables the Id completion for a column class IdCompletionDelegate : public CommandDelegate { public: IdCompletionDelegate(CSMWorld::CommandDispatcher *dispatcher, CSMDoc::Document& document, QObject *parent); QWidget *createEditor (QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index) const override; QWidget *createEditor (QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index, CSMWorld::ColumnBase::Display display) const override; }; class IdCompletionDelegateFactory : public CommandDelegateFactory { public: CommandDelegate *makeDelegate(CSMWorld::CommandDispatcher *dispatcher, CSMDoc::Document& document, QObject *parent) const override; ///< The ownership of the returned CommandDelegate is transferred to the caller. }; } #endif ================================================ FILE: apps/opencs/view/world/idtypedelegate.cpp ================================================ #include "idtypedelegate.hpp" #include "../../model/world/universalid.hpp" CSVWorld::IdTypeDelegate::IdTypeDelegate (const ValueList &values, const IconList &icons, CSMWorld::CommandDispatcher *dispatcher, CSMDoc::Document& document, QObject *parent) : DataDisplayDelegate (values, icons, dispatcher, document, "Records", "type-format", parent) {} CSVWorld::IdTypeDelegateFactory::IdTypeDelegateFactory() { for (int i=0; i (i)); DataDisplayDelegateFactory::add (id.getType(), QString::fromUtf8 (id.getTypeName().c_str()), QString::fromUtf8 (id.getIcon().c_str())); } } CSVWorld::CommandDelegate *CSVWorld::IdTypeDelegateFactory::makeDelegate ( CSMWorld::CommandDispatcher *dispatcher, CSMDoc::Document& document, QObject *parent) const { return new IdTypeDelegate (mValues, mIcons, dispatcher, document, parent); } ================================================ FILE: apps/opencs/view/world/idtypedelegate.hpp ================================================ #ifndef IDTYPEDELEGATE_HPP #define IDTYPEDELEGATE_HPP #include "enumdelegate.hpp" #include "util.hpp" #include "../../model/world/universalid.hpp" #include "datadisplaydelegate.hpp" namespace CSVWorld { class IdTypeDelegate : public DataDisplayDelegate { public: IdTypeDelegate (const ValueList &mValues, const IconList &icons, CSMWorld::CommandDispatcher *dispatcher, CSMDoc::Document& document, QObject *parent); }; class IdTypeDelegateFactory : public DataDisplayDelegateFactory { public: IdTypeDelegateFactory(); CommandDelegate *makeDelegate (CSMWorld::CommandDispatcher *dispatcher, CSMDoc::Document& document, QObject *parent) const override; ///< The ownership of the returned CommandDelegate is transferred to the caller. }; } #endif // REFIDTYPEDELEGATE_HPP ================================================ FILE: apps/opencs/view/world/idvalidator.cpp ================================================ #include "idvalidator.hpp" #include bool CSVWorld::IdValidator::isValid (const QChar& c, bool first) const { if (c.isLetter() || c=='_') return true; if (!first && (c.isDigit() || c.isSpace())) return true; return false; } CSVWorld::IdValidator::IdValidator (bool relaxed, QObject *parent) : QValidator (parent), mRelaxed (relaxed) {} QValidator::State CSVWorld::IdValidator::validate (QString& input, int& pos) const { mError.clear(); if (mRelaxed) { if (input.indexOf ('"')!=-1 || input.indexOf ("::")!=-1 || input.indexOf ("#")!=-1) return QValidator::Invalid; } else { if (input.isEmpty()) { mError = "Missing ID"; return QValidator::Intermediate; } bool first = true; bool scope = false; bool prevScope = false; QString::const_iterator iter = input.begin(); if (!mNamespace.empty()) { std::string namespace_ = input.left (static_cast(mNamespace.size())).toUtf8().constData(); if (Misc::StringUtils::lowerCase (namespace_)!=mNamespace) return QValidator::Invalid; // incorrect namespace iter += namespace_.size(); first = false; prevScope = true; } else { int index = input.indexOf (":"); if (index!=-1) { QString namespace_ = input.left (index); if (namespace_=="project" || namespace_=="session") return QValidator::Invalid; // reserved namespace } } for (; iter!=input.end(); ++iter, first = false) { if (*iter==':') { if (first) return QValidator::Invalid; // scope operator at the beginning if (scope) { scope = false; prevScope = true; } else { if (prevScope) return QValidator::Invalid; // sequence of two scope operators scope = true; } } else if (scope) return QValidator::Invalid; // incomplete scope operator else { prevScope = false; if (!isValid (*iter, first)) return QValidator::Invalid; } } if (scope) { mError = "ID ending with incomplete scope operator"; return QValidator::Intermediate; } if (prevScope) { mError = "ID ending with scope operator"; return QValidator::Intermediate; } } return QValidator::Acceptable; } void CSVWorld::IdValidator::setNamespace (const std::string& namespace_) { mNamespace = Misc::StringUtils::lowerCase (namespace_); } std::string CSVWorld::IdValidator::getError() const { return mError; } ================================================ FILE: apps/opencs/view/world/idvalidator.hpp ================================================ #ifndef CSV_WORLD_IDVALIDATOR_H #define CSV_WORLD_IDVALIDATOR_H #include #include namespace CSVWorld { class IdValidator : public QValidator { bool mRelaxed; std::string mNamespace; mutable std::string mError; private: bool isValid (const QChar& c, bool first) const; public: IdValidator (bool relaxed = false, QObject *parent = nullptr); ///< \param relaxed Relaxed rules for IDs that also functino as user visible text State validate (QString& input, int& pos) const override; void setNamespace (const std::string& namespace_); /// Return a description of the error that resulted in the last call of validate /// returning QValidator::Intermediate. If the last call to validate returned /// a different value (or if there was no such call yet), an empty string is /// returned. std::string getError() const; }; } #endif ================================================ FILE: apps/opencs/view/world/infocreator.cpp ================================================ #include "infocreator.hpp" #include #include #include #include #include "../../model/doc/document.hpp" #include "../../model/world/data.hpp" #include "../../model/world/commands.hpp" #include "../../model/world/columns.hpp" #include "../../model/world/idtable.hpp" #include "../../model/world/idcompletionmanager.hpp" #include "../widget/droplineedit.hpp" std::string CSVWorld::InfoCreator::getId() const { std::string id = Misc::StringUtils::lowerCase (mTopic->text().toUtf8().constData()); std::string unique = QUuid::createUuid().toByteArray().data(); unique.erase (std::remove (unique.begin(), unique.end(), '-'), unique.end()); unique = unique.substr (1, unique.size()-2); return id + '#' + unique; } void CSVWorld::InfoCreator::configureCreateCommand (CSMWorld::CreateCommand& command) const { CSMWorld::IdTable& table = dynamic_cast (*getData().getTableModel (getCollectionId())); CSMWorld::CloneCommand* cloneCommand = dynamic_cast (&command); if (getCollectionId() == CSMWorld::UniversalId::Type_TopicInfos) { if (!cloneCommand) { command.addValue (table.findColumnIndex(CSMWorld::Columns::ColumnId_Topic), mTopic->text()); command.addValue (table.findColumnIndex(CSMWorld::Columns::ColumnId_Rank), -1); command.addValue (table.findColumnIndex(CSMWorld::Columns::ColumnId_Gender), -1); command.addValue (table.findColumnIndex(CSMWorld::Columns::ColumnId_PcRank), -1); } else { cloneCommand->setOverrideValue(table.findColumnIndex(CSMWorld::Columns::ColumnId_Topic), mTopic->text()); } } else { if (!cloneCommand) { command.addValue (table.findColumnIndex(CSMWorld::Columns::ColumnId_Journal), mTopic->text()); } else cloneCommand->setOverrideValue(table.findColumnIndex(CSMWorld::Columns::ColumnId_Journal), mTopic->text()); } } CSVWorld::InfoCreator::InfoCreator (CSMWorld::Data& data, QUndoStack& undoStack, const CSMWorld::UniversalId& id, CSMWorld::IdCompletionManager& completionManager) : GenericCreator (data, undoStack, id) { // Determine if we're dealing with topics or journals. CSMWorld::ColumnBase::Display displayType = CSMWorld::ColumnBase::Display_Topic; QString labelText = "Topic"; if (getCollectionId().getType() == CSMWorld::UniversalId::Type_JournalInfos) { displayType = CSMWorld::ColumnBase::Display_Journal; labelText = "Journal"; } QLabel *label = new QLabel (labelText, this); insertBeforeButtons (label, false); // Add topic/journal ID input with auto-completion. // Only existing topic/journal IDs are accepted so no ID validation is performed. mTopic = new CSVWidget::DropLineEdit(displayType, this); mTopic->setCompleter(completionManager.getCompleter(displayType).get()); insertBeforeButtons (mTopic, true); setManualEditing (false); connect (mTopic, SIGNAL (textChanged (const QString&)), this, SLOT (topicChanged())); connect (mTopic, SIGNAL (returnPressed()), this, SLOT (inputReturnPressed())); } void CSVWorld::InfoCreator::cloneMode (const std::string& originId, const CSMWorld::UniversalId::Type type) { CSMWorld::IdTable& infoTable = dynamic_cast (*getData().getTableModel (getCollectionId())); int topicColumn = infoTable.findColumnIndex ( getCollectionId().getType()==CSMWorld::UniversalId::Type_TopicInfos ? CSMWorld::Columns::ColumnId_Topic : CSMWorld::Columns::ColumnId_Journal); mTopic->setText ( infoTable.data (infoTable.getModelIndex (originId, topicColumn)).toString()); GenericCreator::cloneMode (originId, type); } void CSVWorld::InfoCreator::reset() { mTopic->setText (""); GenericCreator::reset(); } std::string CSVWorld::InfoCreator::getErrors() const { // We ignore errors from GenericCreator here, because they can never happen in an InfoCreator. std::string errors; std::string topic = mTopic->text().toUtf8().constData(); if ((getCollectionId().getType()==CSMWorld::UniversalId::Type_TopicInfos ? getData().getTopics() : getData().getJournals()).searchId (topic)==-1) { errors += "Invalid Topic ID"; } return errors; } void CSVWorld::InfoCreator::focus() { mTopic->setFocus(); } void CSVWorld::InfoCreator::topicChanged() { update(); } CSVWorld::Creator *CSVWorld::InfoCreatorFactory::makeCreator(CSMDoc::Document& document, const CSMWorld::UniversalId& id) const { return new InfoCreator(document.getData(), document.getUndoStack(), id, document.getIdCompletionManager()); } ================================================ FILE: apps/opencs/view/world/infocreator.hpp ================================================ #ifndef CSV_WORLD_INFOCREATOR_H #define CSV_WORLD_INFOCREATOR_H #include "genericcreator.hpp" namespace CSMWorld { class InfoCollection; class IdCompletionManager; } namespace CSVWidget { class DropLineEdit; } namespace CSVWorld { class InfoCreator : public GenericCreator { Q_OBJECT CSVWidget::DropLineEdit *mTopic; std::string getId() const override; void configureCreateCommand (CSMWorld::CreateCommand& command) const override; public: InfoCreator (CSMWorld::Data& data, QUndoStack& undoStack, const CSMWorld::UniversalId& id, CSMWorld::IdCompletionManager& completionManager); void cloneMode (const std::string& originId, const CSMWorld::UniversalId::Type type) override; void reset() override; std::string getErrors() const override; ///< Return formatted error descriptions for the current state of the creator. if an empty /// string is returned, there is no error. /// Focus main input widget void focus() override; private slots: void topicChanged(); }; class InfoCreatorFactory : public CreatorFactoryBase { public: Creator *makeCreator (CSMDoc::Document& document, const CSMWorld::UniversalId& id) const override; ///< The ownership of the returned Creator is transferred to the caller. }; } #endif ================================================ FILE: apps/opencs/view/world/landcreator.cpp ================================================ #include "landcreator.hpp" #include #include #include #include "../../model/world/commands.hpp" #include "../../model/world/idtable.hpp" #include "../../model/world/land.hpp" namespace CSVWorld { LandCreator::LandCreator(CSMWorld::Data& data, QUndoStack& undoStack, const CSMWorld::UniversalId& id) : GenericCreator(data, undoStack, id) , mXLabel(nullptr) , mYLabel(nullptr) , mX(nullptr) , mY(nullptr) { const int MaxInt = std::numeric_limits::max(); const int MinInt = std::numeric_limits::min(); setManualEditing(false); mXLabel = new QLabel("X: "); mX = new QSpinBox(); mX->setMinimum(MinInt); mX->setMaximum(MaxInt); insertBeforeButtons(mXLabel, false); insertBeforeButtons(mX, true); mYLabel = new QLabel("Y: "); mY = new QSpinBox(); mY->setMinimum(MinInt); mY->setMaximum(MaxInt); insertBeforeButtons(mYLabel, false); insertBeforeButtons(mY, true); connect (mX, SIGNAL(valueChanged(int)), this, SLOT(coordChanged(int))); connect (mY, SIGNAL(valueChanged(int)), this, SLOT(coordChanged(int))); } void LandCreator::cloneMode(const std::string& originId, const CSMWorld::UniversalId::Type type) { GenericCreator::cloneMode(originId, type); int x = 0, y = 0; CSMWorld::Land::parseUniqueRecordId(originId, x, y); mX->setValue(x); mY->setValue(y); } void LandCreator::touch(const std::vector& ids) { // Combine multiple touch commands into one "macro" command getUndoStack().beginMacro("Touch records"); CSMWorld::IdTable& lands = dynamic_cast(*getData().getTableModel(CSMWorld::UniversalId::Type_Lands)); CSMWorld::IdTable& ltexs = dynamic_cast(*getData().getTableModel(CSMWorld::UniversalId::Type_LandTextures)); for (const CSMWorld::UniversalId& uid : ids) { CSMWorld::TouchLandCommand* touchCmd = new CSMWorld::TouchLandCommand(lands, ltexs, uid.getId()); getUndoStack().push(touchCmd); } // Execute getUndoStack().endMacro(); } void LandCreator::focus() { mX->setFocus(); } void LandCreator::reset() { GenericCreator::reset(); mX->setValue(0); mY->setValue(0); } std::string LandCreator::getErrors() const { if (getData().getLand().searchId(getId()) >= 0) return "A land with that name already exists."; return ""; } std::string LandCreator::getId() const { return CSMWorld::Land::createUniqueRecordId(mX->value(), mY->value()); } void LandCreator::pushCommand(std::unique_ptr command, const std::string& id) { if (mCloneMode) { CSMWorld::IdTable& lands = dynamic_cast(*getData().getTableModel(CSMWorld::UniversalId::Type_Lands)); CSMWorld::IdTable& ltexs = dynamic_cast(*getData().getTableModel(CSMWorld::UniversalId::Type_LandTextures)); getUndoStack().beginMacro(("Clone " + id).c_str()); getUndoStack().push(command.release()); CSMWorld::CopyLandTexturesCommand* ltexCopy = new CSMWorld::CopyLandTexturesCommand(lands, ltexs, getClonedId(), getId()); getUndoStack().push(ltexCopy); getUndoStack().endMacro(); } else getUndoStack().push (command.release()); } void LandCreator::coordChanged(int value) { update(); } } ================================================ FILE: apps/opencs/view/world/landcreator.hpp ================================================ #ifndef CSV_WORLD_LANDCREATOR_H #define CSV_WORLD_LANDCREATOR_H #include "genericcreator.hpp" class QLabel; class QSpinBox; namespace CSVWorld { class LandCreator : public GenericCreator { Q_OBJECT QLabel* mXLabel; QLabel* mYLabel; QSpinBox* mX; QSpinBox* mY; public: LandCreator(CSMWorld::Data& data, QUndoStack& undoStack, const CSMWorld::UniversalId& id); void cloneMode(const std::string& originId, const CSMWorld::UniversalId::Type type) override; void touch(const std::vector& ids) override; void focus() override; void reset() override; std::string getErrors() const override; protected: std::string getId() const override; void pushCommand(std::unique_ptr command, const std::string& id) override; private slots: void coordChanged(int value); }; } #endif ================================================ FILE: apps/opencs/view/world/landtexturecreator.cpp ================================================ #include "landtexturecreator.hpp" #include #include #include #include #include #include "../../model/world/commands.hpp" #include "../../model/world/idtable.hpp" #include "../../model/world/landtexture.hpp" namespace CSVWorld { LandTextureCreator::LandTextureCreator(CSMWorld::Data& data, QUndoStack& undoStack, const CSMWorld::UniversalId& id) : GenericCreator(data, undoStack, id) { // One index is reserved for a default texture const size_t MaxIndex = std::numeric_limits::max() - 1; setManualEditing(false); QLabel* nameLabel = new QLabel("Name"); insertBeforeButtons(nameLabel, false); mNameEdit = new QLineEdit(this); insertBeforeButtons(mNameEdit, true); QLabel* indexLabel = new QLabel("Index"); insertBeforeButtons(indexLabel, false); mIndexBox = new QSpinBox(this); mIndexBox->setMinimum(0); mIndexBox->setMaximum(MaxIndex); insertBeforeButtons(mIndexBox, true); connect(mNameEdit, SIGNAL(textChanged(const QString&)), this, SLOT(nameChanged(const QString&))); connect(mIndexBox, SIGNAL(valueChanged(int)), this, SLOT(indexChanged(int))); } void LandTextureCreator::cloneMode(const std::string& originId, const CSMWorld::UniversalId::Type type) { GenericCreator::cloneMode(originId, type); CSMWorld::IdTable& table = dynamic_cast(*getData().getTableModel(getCollectionId())); int column = table.findColumnIndex(CSMWorld::Columns::ColumnId_TextureNickname); mNameEdit->setText((table.data(table.getModelIndex(originId, column)).toString())); column = table.findColumnIndex(CSMWorld::Columns::ColumnId_TextureIndex); mIndexBox->setValue((table.data(table.getModelIndex(originId, column)).toInt())); } void LandTextureCreator::focus() { mIndexBox->setFocus(); } void LandTextureCreator::reset() { GenericCreator::reset(); mNameEdit->setText(""); mIndexBox->setValue(0); } std::string LandTextureCreator::getErrors() const { if (getData().getLandTextures().searchId(getId()) >= 0) { return "Index is already in use"; } return ""; } void LandTextureCreator::configureCreateCommand(CSMWorld::CreateCommand& command) const { GenericCreator::configureCreateCommand(command); CSMWorld::IdTable& table = dynamic_cast(*getData().getTableModel(getCollectionId())); int column = table.findColumnIndex(CSMWorld::Columns::ColumnId_TextureNickname); command.addValue(column, mName.c_str()); } std::string LandTextureCreator::getId() const { return CSMWorld::LandTexture::createUniqueRecordId(0, mIndexBox->value()); } void LandTextureCreator::nameChanged(const QString& value) { mName = value.toUtf8().constData(); update(); } void LandTextureCreator::indexChanged(int value) { update(); } } ================================================ FILE: apps/opencs/view/world/landtexturecreator.hpp ================================================ #ifndef CSV_WORLD_LANDTEXTURECREATOR_H #define CSV_WORLD_LANDTEXTURECREATOR_H #include #include "genericcreator.hpp" class QLineEdit; class QSpinBox; namespace CSVWorld { class LandTextureCreator : public GenericCreator { Q_OBJECT public: LandTextureCreator(CSMWorld::Data& data, QUndoStack& undoStack, const CSMWorld::UniversalId& id); void cloneMode(const std::string& originId, const CSMWorld::UniversalId::Type type) override; void focus() override; void reset() override; std::string getErrors() const override; protected: void configureCreateCommand(CSMWorld::CreateCommand& command) const override; std::string getId() const override; private slots: void nameChanged(const QString& val); void indexChanged(int val); private: QLineEdit* mNameEdit; QSpinBox* mIndexBox; std::string mName; }; } #endif ================================================ FILE: apps/opencs/view/world/nestedtable.cpp ================================================ #include "nestedtable.hpp" #include #include #include #include #include "../../model/prefs/shortcut.hpp" #include "../../model/world/nestedtableproxymodel.hpp" #include "../../model/world/universalid.hpp" #include "../../model/world/commands.hpp" #include "../../model/world/commanddispatcher.hpp" #include "../../model/world/commandmacro.hpp" #include "tableeditidaction.hpp" #include "util.hpp" CSVWorld::NestedTable::NestedTable(CSMDoc::Document& document, CSMWorld::UniversalId id, CSMWorld::NestedTableProxyModel* model, QWidget* parent, bool editable, bool fixedRows) : DragRecordTable(document, parent), mAddNewRowAction(nullptr), mRemoveRowAction(nullptr), mEditIdAction(nullptr), mModel(model) { mDispatcher = new CSMWorld::CommandDispatcher (document, id, this); setSelectionBehavior (QAbstractItemView::SelectRows); setSelectionMode (QAbstractItemView::ExtendedSelection); horizontalHeader()->setSectionResizeMode (QHeaderView::Interactive); verticalHeader()->hide(); int columns = model->columnCount(QModelIndex()); for(int i = 0 ; i < columns; ++i) { CSMWorld::ColumnBase::Display display = static_cast ( model->headerData (i, Qt::Horizontal, CSMWorld::ColumnBase::Role_Display).toInt()); CommandDelegate *delegate = CommandDelegateFactoryCollection::get().makeDelegate(display, mDispatcher, document, this); setItemDelegateForColumn(i, delegate); } setModel(model); if (editable) { if (!fixedRows) { mAddNewRowAction = new QAction (tr ("Add new row"), this); connect(mAddNewRowAction, SIGNAL(triggered()), this, SLOT(addNewRowActionTriggered())); CSMPrefs::Shortcut* addRowShortcut = new CSMPrefs::Shortcut("table-add", this); addRowShortcut->associateAction(mAddNewRowAction); mRemoveRowAction = new QAction (tr ("Remove rows"), this); connect(mRemoveRowAction, SIGNAL(triggered()), this, SLOT(removeRowActionTriggered())); CSMPrefs::Shortcut* removeRowShortcut = new CSMPrefs::Shortcut("table-remove", this); removeRowShortcut->associateAction(mRemoveRowAction); } mEditIdAction = new TableEditIdAction(*this, this); connect(mEditIdAction, SIGNAL(triggered()), this, SLOT(editCell())); } } std::vector CSVWorld::NestedTable::getDraggedRecords() const { // No drag support for nested tables return std::vector(); } void CSVWorld::NestedTable::contextMenuEvent (QContextMenuEvent *event) { if (!mEditIdAction) return; QModelIndexList selectedRows = selectionModel()->selectedRows(); QMenu menu(this); int currentRow = rowAt(event->y()); int currentColumn = columnAt(event->x()); if (mEditIdAction->isValidIdCell(currentRow, currentColumn)) { mEditIdAction->setCell(currentRow, currentColumn); menu.addAction(mEditIdAction); menu.addSeparator(); } if (mAddNewRowAction && mRemoveRowAction) { menu.addAction(mAddNewRowAction); menu.addAction(mRemoveRowAction); } menu.exec (event->globalPos()); } void CSVWorld::NestedTable::removeRowActionTriggered() { CSMWorld::CommandMacro macro(mDocument.getUndoStack(), selectionModel()->selectedRows().size() > 1 ? tr("Remove rows") : ""); // Remove rows in reverse order for (int i = selectionModel()->selectedRows().size() - 1; i >= 0; --i) { macro.push(new CSMWorld::DeleteNestedCommand(*(mModel->model()), mModel->getParentId(), selectionModel()->selectedRows()[i].row(), mModel->getParentColumn())); } } void CSVWorld::NestedTable::addNewRowActionTriggered() { int row = 0; if (!selectionModel()->selectedRows().empty()) row = selectionModel()->selectedRows().back().row() + 1; mDocument.getUndoStack().push(new CSMWorld::AddNestedCommand(*(mModel->model()), mModel->getParentId(), row, mModel->getParentColumn())); } void CSVWorld::NestedTable::editCell() { emit editRequest(mEditIdAction->getCurrentId(), ""); } ================================================ FILE: apps/opencs/view/world/nestedtable.hpp ================================================ #ifndef CSV_WORLD_NESTEDTABLE_H #define CSV_WORLD_NESTEDTABLE_H #include #include "dragrecordtable.hpp" class QAction; class QContextMenuEvent; namespace CSMWorld { class NestedTableProxyModel; class UniversalId; class CommandDispatcher; } namespace CSMDoc { class Document; } namespace CSVWorld { class TableEditIdAction; class NestedTable : public DragRecordTable { Q_OBJECT QAction *mAddNewRowAction; QAction *mRemoveRowAction; TableEditIdAction *mEditIdAction; CSMWorld::NestedTableProxyModel* mModel; CSMWorld::CommandDispatcher *mDispatcher; public: NestedTable(CSMDoc::Document& document, CSMWorld::UniversalId id, CSMWorld::NestedTableProxyModel* model, QWidget* parent = nullptr, bool editable = true, bool fixedRows = false); std::vector getDraggedRecords() const override; private: void contextMenuEvent (QContextMenuEvent *event) override; private slots: void removeRowActionTriggered(); void addNewRowActionTriggered(); void editCell(); signals: void editRequest(const CSMWorld::UniversalId &id, const std::string &hint); }; } #endif ================================================ FILE: apps/opencs/view/world/pathgridcreator.cpp ================================================ #include "pathgridcreator.hpp" #include #include "../../model/doc/document.hpp" #include "../../model/world/columns.hpp" #include "../../model/world/data.hpp" #include "../../model/world/idcompletionmanager.hpp" #include "../../model/world/idtable.hpp" #include "../widget/droplineedit.hpp" std::string CSVWorld::PathgridCreator::getId() const { return mCell->text().toUtf8().constData(); } CSMWorld::IdTable& CSVWorld::PathgridCreator::getPathgridsTable() const { return dynamic_cast ( *getData().getTableModel(getCollectionId()) ); } CSVWorld::PathgridCreator::PathgridCreator( CSMWorld::Data& data, QUndoStack& undoStack, const CSMWorld::UniversalId& id, CSMWorld::IdCompletionManager& completionManager ) : GenericCreator(data, undoStack, id) { setManualEditing(false); QLabel *label = new QLabel("Cell", this); insertBeforeButtons(label, false); // Add cell ID input with auto-completion. // Only existing cell IDs are accepted so no ID validation is performed. CSMWorld::ColumnBase::Display displayType = CSMWorld::ColumnBase::Display_Cell; mCell = new CSVWidget::DropLineEdit(displayType, this); mCell->setCompleter(completionManager.getCompleter(displayType).get()); insertBeforeButtons(mCell, true); connect(mCell, SIGNAL (textChanged(const QString&)), this, SLOT (cellChanged())); connect(mCell, SIGNAL (returnPressed()), this, SLOT (inputReturnPressed())); } void CSVWorld::PathgridCreator::cloneMode( const std::string& originId, const CSMWorld::UniversalId::Type type) { CSVWorld::GenericCreator::cloneMode(originId, type); // Look up cloned record in pathgrids table and set cell ID text. CSMWorld::IdTable& table = getPathgridsTable(); int column = table.findColumnIndex(CSMWorld::Columns::ColumnId_Id); mCell->setText(table.data(table.getModelIndex(originId, column)).toString()); } std::string CSVWorld::PathgridCreator::getErrors() const { std::string cellId = getId(); // Check user input for any errors. std::string errors; if (cellId.empty()) { errors = "No cell ID selected"; } else if (getData().getPathgrids().searchId(cellId) > -1) { errors = "Pathgrid for selected cell ID already exists"; } else if (getData().getCells().searchId(cellId) == -1) { errors = "Cell with selected cell ID does not exist"; } return errors; } void CSVWorld::PathgridCreator::focus() { mCell->setFocus(); } void CSVWorld::PathgridCreator::reset() { CSVWorld::GenericCreator::reset(); mCell->setText(""); } void CSVWorld::PathgridCreator::cellChanged() { update(); } CSVWorld::Creator *CSVWorld::PathgridCreatorFactory::makeCreator( CSMDoc::Document& document, const CSMWorld::UniversalId& id) const { return new PathgridCreator( document.getData(), document.getUndoStack(), id, document.getIdCompletionManager() ); } ================================================ FILE: apps/opencs/view/world/pathgridcreator.hpp ================================================ #ifndef PATHGRIDCREATOR_HPP #define PATHGRIDCREATOR_HPP #include "genericcreator.hpp" namespace CSMDoc { class Document; } namespace CSMWorld { class Data; class IdCompletionManager; class IdTable; class UniversalId; } namespace CSVWidget { class DropLineEdit; } namespace CSVWorld { /// \brief Record creator for pathgrids. class PathgridCreator : public GenericCreator { Q_OBJECT CSVWidget::DropLineEdit *mCell; private: /// \return Cell ID entered by user. std::string getId() const override; /// \return reference to table containing pathgrids. CSMWorld::IdTable& getPathgridsTable() const; public: PathgridCreator( CSMWorld::Data& data, QUndoStack& undoStack, const CSMWorld::UniversalId& id, CSMWorld::IdCompletionManager& completionManager); /// \brief Set cell ID input widget to ID of record to be cloned. /// \param originId Cell ID to be cloned. /// \param type Type of record to be cloned. void cloneMode( const std::string& originId, const CSMWorld::UniversalId::Type type) override; /// \return Error description for current user input. std::string getErrors() const override; /// \brief Set focus to cell ID input widget. void focus() override; /// \brief Clear cell ID input widget. void reset() override; private slots: /// \brief Check user input for errors. void cellChanged(); }; /// \brief Creator factory for pathgrid record creator. class PathgridCreatorFactory : public CreatorFactoryBase { public: Creator *makeCreator( CSMDoc::Document& document, const CSMWorld::UniversalId& id) const override; }; } #endif // PATHGRIDCREATOR_HPP ================================================ FILE: apps/opencs/view/world/previewsubview.cpp ================================================ #include "previewsubview.hpp" #include #include "../render/previewwidget.hpp" #include "../widget/scenetoolbar.hpp" #include "../widget/scenetoolmode.hpp" CSVWorld::PreviewSubView::PreviewSubView (const CSMWorld::UniversalId& id, CSMDoc::Document& document) : SubView (id), mTitle (id.toString().c_str()) { QHBoxLayout *layout = new QHBoxLayout; if (document.getData().getReferenceables().searchId (id.getId())==-1) { std::string referenceableId = document.getData().getReferences().getRecord (id.getId()).get().mRefID; referenceableIdChanged (referenceableId); mScene = new CSVRender::PreviewWidget (document.getData(), id.getId(), false, this); } else mScene = new CSVRender::PreviewWidget (document.getData(), id.getId(), true, this); CSVWidget::SceneToolbar *toolbar = new CSVWidget::SceneToolbar (48+6, this); CSVWidget::SceneToolMode *lightingTool = mScene->makeLightingSelector (toolbar); toolbar->addTool (lightingTool); layout->addWidget (toolbar, 0); layout->addWidget (mScene, 1); QWidget *widget = new QWidget; widget->setLayout (layout); setWidget (widget); connect (mScene, SIGNAL (closeRequest()), this, SLOT (closeRequest())); connect (mScene, SIGNAL (referenceableIdChanged (const std::string&)), this, SLOT (referenceableIdChanged (const std::string&))); connect (mScene, SIGNAL (focusToolbarRequest()), toolbar, SLOT (setFocus())); connect (toolbar, SIGNAL (focusSceneRequest()), mScene, SLOT (setFocus())); } void CSVWorld::PreviewSubView::setEditLock (bool locked) {} std::string CSVWorld::PreviewSubView::getTitle() const { return mTitle; } void CSVWorld::PreviewSubView::referenceableIdChanged (const std::string& id) { if (id.empty()) mTitle = "Preview: Reference to "; else mTitle = "Preview: Reference to " + id; setWindowTitle (QString::fromUtf8 (mTitle.c_str())); emit updateTitle(); } ================================================ FILE: apps/opencs/view/world/previewsubview.hpp ================================================ #ifndef CSV_WORLD_PREVIEWSUBVIEW_H #define CSV_WORLD_PREVIEWSUBVIEW_H #include "../doc/subview.hpp" namespace CSMDoc { class Document; } namespace CSVRender { class PreviewWidget; } namespace CSVWorld { class PreviewSubView : public CSVDoc::SubView { Q_OBJECT CSVRender::PreviewWidget *mScene; std::string mTitle; public: PreviewSubView (const CSMWorld::UniversalId& id, CSMDoc::Document& document); void setEditLock (bool locked) override; std::string getTitle() const override; private slots: void referenceableIdChanged (const std::string& id); }; } #endif ================================================ FILE: apps/opencs/view/world/recordbuttonbar.cpp ================================================ #include "recordbuttonbar.hpp" #include #include #include "../../model/world/idtable.hpp" #include "../../model/world/commanddispatcher.hpp" #include "../../model/prefs/state.hpp" #include "../world/tablebottombox.hpp" void CSVWorld::RecordButtonBar::updateModificationButtons() { bool createAndDeleteDisabled = !mBottom || !mBottom->canCreateAndDelete() || mLocked; mCloneButton->setDisabled (createAndDeleteDisabled); mAddButton->setDisabled (createAndDeleteDisabled); bool commandDisabled = !mCommandDispatcher || mLocked; mRevertButton->setDisabled (commandDisabled); mDeleteButton->setDisabled (commandDisabled || createAndDeleteDisabled); } void CSVWorld::RecordButtonBar::updatePrevNextButtons() { int rows = mTable.rowCount(); if (rows<=1) { mPrevButton->setDisabled (true); mNextButton->setDisabled (true); } else if (CSMPrefs::get()["General Input"]["cycle"].isTrue()) { mPrevButton->setDisabled (false); mNextButton->setDisabled (false); } else { int row = mTable.getModelIndex (mId.getId(), 0).row(); mPrevButton->setDisabled (row<=0); mNextButton->setDisabled (row>=rows-1); } } CSVWorld::RecordButtonBar::RecordButtonBar (const CSMWorld::UniversalId& id, CSMWorld::IdTable& table, TableBottomBox *bottomBox, CSMWorld::CommandDispatcher *commandDispatcher, QWidget *parent) : QWidget (parent), mId (id), mTable (table), mBottom (bottomBox), mCommandDispatcher (commandDispatcher), mLocked (false) { QHBoxLayout *buttonsLayout = new QHBoxLayout; buttonsLayout->setContentsMargins (0, 0, 0, 0); // left section mPrevButton = new QToolButton (this); mPrevButton->setIcon(QIcon(":record-previous")); mPrevButton->setToolTip ("Switch to previous record"); buttonsLayout->addWidget (mPrevButton, 0); mNextButton = new QToolButton (this); mNextButton->setIcon(QIcon(":/record-next")); mNextButton->setToolTip ("Switch to next record"); buttonsLayout->addWidget (mNextButton, 1); buttonsLayout->addStretch(2); // optional buttons of the right section if (mTable.getFeatures() & CSMWorld::IdTable::Feature_Preview) { QToolButton* previewButton = new QToolButton (this); previewButton->setIcon(QIcon(":edit-preview")); previewButton->setToolTip ("Open a preview of this record"); buttonsLayout->addWidget(previewButton); connect (previewButton, SIGNAL(clicked()), this, SIGNAL (showPreview())); } if (mTable.getFeatures() & CSMWorld::IdTable::Feature_View) { QToolButton* viewButton = new QToolButton (this); viewButton->setIcon(QIcon(":/cell.png")); viewButton->setToolTip ("Open a scene view of the cell this record is located in"); buttonsLayout->addWidget(viewButton); connect (viewButton, SIGNAL(clicked()), this, SIGNAL (viewRecord())); } // right section mCloneButton = new QToolButton (this); mCloneButton->setIcon(QIcon(":edit-clone")); mCloneButton->setToolTip ("Clone record"); buttonsLayout->addWidget(mCloneButton); mAddButton = new QToolButton (this); mAddButton->setIcon(QIcon(":edit-add")); mAddButton->setToolTip ("Add new record"); buttonsLayout->addWidget(mAddButton); mDeleteButton = new QToolButton (this); mDeleteButton->setIcon(QIcon(":edit-delete")); mDeleteButton->setToolTip ("Delete record"); buttonsLayout->addWidget(mDeleteButton); mRevertButton = new QToolButton (this); mRevertButton->setIcon(QIcon(":edit-undo")); mRevertButton->setToolTip ("Revert record"); buttonsLayout->addWidget(mRevertButton); setLayout (buttonsLayout); // connections if(mBottom && mBottom->canCreateAndDelete()) { connect (mAddButton, SIGNAL (clicked()), mBottom, SLOT (createRequest())); connect (mCloneButton, SIGNAL (clicked()), this, SLOT (cloneRequest())); } connect (mNextButton, SIGNAL (clicked()), this, SLOT (nextId())); connect (mPrevButton, SIGNAL (clicked()), this, SLOT (prevId())); if (mCommandDispatcher) { connect (mRevertButton, SIGNAL (clicked()), mCommandDispatcher, SLOT (executeRevert())); connect (mDeleteButton, SIGNAL (clicked()), mCommandDispatcher, SLOT (executeDelete())); } connect (&mTable, SIGNAL (rowsInserted (const QModelIndex&, int, int)), this, SLOT (rowNumberChanged (const QModelIndex&, int, int))); connect (&mTable, SIGNAL (rowsRemoved (const QModelIndex&, int, int)), this, SLOT (rowNumberChanged (const QModelIndex&, int, int))); connect (&CSMPrefs::State::get(), SIGNAL (settingChanged (const CSMPrefs::Setting *)), this, SLOT (settingChanged (const CSMPrefs::Setting *))); updateModificationButtons(); updatePrevNextButtons(); } void CSVWorld::RecordButtonBar::setEditLock (bool locked) { mLocked = locked; updateModificationButtons(); } void CSVWorld::RecordButtonBar::universalIdChanged (const CSMWorld::UniversalId& id) { mId = id; updatePrevNextButtons(); } void CSVWorld::RecordButtonBar::settingChanged (const CSMPrefs::Setting *setting) { if (*setting=="General Input/cycle") updatePrevNextButtons(); } void CSVWorld::RecordButtonBar::cloneRequest() { if (mBottom) { int typeColumn = mTable.findColumnIndex (CSMWorld::Columns::ColumnId_RecordType); QModelIndex typeIndex = mTable.getModelIndex (mId.getId(), typeColumn); CSMWorld::UniversalId::Type type = static_cast ( mTable.data (typeIndex).toInt()); mBottom->cloneRequest (mId.getId(), type); } } void CSVWorld::RecordButtonBar::nextId() { int newRow = mTable.getModelIndex (mId.getId(), 0).row() + 1; if (newRow >= mTable.rowCount()) { if (CSMPrefs::get()["General Input"]["cycle"].isTrue()) newRow = 0; else return; } emit switchToRow (newRow); } void CSVWorld::RecordButtonBar::prevId() { int newRow = mTable.getModelIndex (mId.getId(), 0).row() - 1; if (newRow < 0) { if (CSMPrefs::get()["General Input"]["cycle"].isTrue()) newRow = mTable.rowCount()-1; else return; } emit switchToRow (newRow); } void CSVWorld::RecordButtonBar::rowNumberChanged (const QModelIndex& parent, int start, int end) { updatePrevNextButtons(); } ================================================ FILE: apps/opencs/view/world/recordbuttonbar.hpp ================================================ #ifndef CSV_WORLD_RECORDBUTTONBAR_H #define CSV_WORLD_RECORDBUTTONBAR_H #include #include "../../model/world/universalid.hpp" class QToolButton; class QModelIndex; namespace CSMWorld { class IdTable; class CommandDispatcher; } namespace CSMPrefs { class Setting; } namespace CSVWorld { class TableBottomBox; /// \brief Button bar for use in dialogue-type subviews /// /// Contains the following buttons: /// - next/prev /// - clone /// - add /// - delete /// - revert /// - preview (optional) /// - view (optional) class RecordButtonBar : public QWidget { Q_OBJECT CSMWorld::UniversalId mId; CSMWorld::IdTable& mTable; TableBottomBox *mBottom; CSMWorld::CommandDispatcher *mCommandDispatcher; QToolButton *mPrevButton; QToolButton *mNextButton; QToolButton *mCloneButton; QToolButton *mAddButton; QToolButton *mDeleteButton; QToolButton *mRevertButton; bool mLocked; private: void updateModificationButtons(); void updatePrevNextButtons(); public: RecordButtonBar (const CSMWorld::UniversalId& id, CSMWorld::IdTable& table, TableBottomBox *bottomBox = nullptr, CSMWorld::CommandDispatcher *commandDispatcher = nullptr, QWidget *parent = nullptr); void setEditLock (bool locked); public slots: void universalIdChanged (const CSMWorld::UniversalId& id); private slots: void settingChanged (const CSMPrefs::Setting *setting); void cloneRequest(); void nextId(); void prevId(); void rowNumberChanged (const QModelIndex& parent, int start, int end); signals: void showPreview(); void viewRecord(); void switchToRow (int row); }; } #endif ================================================ FILE: apps/opencs/view/world/recordstatusdelegate.cpp ================================================ #include "recordstatusdelegate.hpp" #include #include #include #include "../../model/world/columns.hpp" CSVWorld::RecordStatusDelegate::RecordStatusDelegate(const ValueList& values, const IconList & icons, CSMWorld::CommandDispatcher *dispatcher, CSMDoc::Document& document, QObject *parent) : DataDisplayDelegate (values, icons, dispatcher, document, "Records", "status-format", parent) {} CSVWorld::CommandDelegate *CSVWorld::RecordStatusDelegateFactory::makeDelegate ( CSMWorld::CommandDispatcher *dispatcher, CSMDoc::Document& document, QObject *parent) const { return new RecordStatusDelegate (mValues, mIcons, dispatcher, document, parent); } CSVWorld::RecordStatusDelegateFactory::RecordStatusDelegateFactory() { std::vector> enums = CSMWorld::Columns::getEnums (CSMWorld::Columns::ColumnId_Modification); static const char *sIcons[] = { ":list-base", ":list-modified", ":list-added", ":list-removed", ":list-removed", 0 }; for (int i=0; sIcons[i]; ++i) { auto& enumPair = enums.at(i); add (enumPair.first, enumPair.second.c_str(), sIcons[i]); } } ================================================ FILE: apps/opencs/view/world/recordstatusdelegate.hpp ================================================ #ifndef RECORDSTATUSDELEGATE_H #define RECORDSTATUSDELEGATE_H #include "util.hpp" #include #include #include "datadisplaydelegate.hpp" #include "../../model/world/record.hpp" class QIcon; class QFont; namespace CSVWorld { class RecordStatusDelegate : public DataDisplayDelegate { public: RecordStatusDelegate (const ValueList& values, const IconList& icons, CSMWorld::CommandDispatcher *dispatcher, CSMDoc::Document& document, QObject *parent = nullptr); }; class RecordStatusDelegateFactory : public DataDisplayDelegateFactory { public: RecordStatusDelegateFactory(); CommandDelegate *makeDelegate (CSMWorld::CommandDispatcher *dispatcher, CSMDoc::Document& document, QObject *parent) const override; ///< The ownership of the returned CommandDelegate is transferred to the caller. }; } #endif // RECORDSTATUSDELEGATE_HPP ================================================ FILE: apps/opencs/view/world/referenceablecreator.cpp ================================================ #include "referenceablecreator.hpp" #include #include #include "../../model/world/universalid.hpp" #include "../../model/world/commands.hpp" void CSVWorld::ReferenceableCreator::configureCreateCommand (CSMWorld::CreateCommand& command) const { command.setType ( static_cast (mType->itemData (mType->currentIndex()).toInt())); } CSVWorld::ReferenceableCreator::ReferenceableCreator (CSMWorld::Data& data, QUndoStack& undoStack, const CSMWorld::UniversalId& id) : GenericCreator (data, undoStack, id) { QLabel *label = new QLabel ("Type", this); insertBeforeButtons (label, false); std::vector types = CSMWorld::UniversalId::listReferenceableTypes(); mType = new QComboBox (this); for (std::vector::const_iterator iter (types.begin()); iter!=types.end(); ++iter) { CSMWorld::UniversalId id2 (*iter, ""); mType->addItem (QIcon (id2.getIcon().c_str()), id2.getTypeName().c_str(), static_cast (id2.getType())); } insertBeforeButtons (mType, false); } void CSVWorld::ReferenceableCreator::reset() { mType->setCurrentIndex (0); GenericCreator::reset(); } void CSVWorld::ReferenceableCreator::cloneMode (const std::string& originId, const CSMWorld::UniversalId::Type type) { GenericCreator::cloneMode (originId, type); mType->setCurrentIndex (mType->findData (static_cast (type))); } void CSVWorld::ReferenceableCreator::toggleWidgets(bool active) { CSVWorld::GenericCreator::toggleWidgets(active); mType->setEnabled(active); } ================================================ FILE: apps/opencs/view/world/referenceablecreator.hpp ================================================ #ifndef CSV_WORLD_REFERENCEABLECREATOR_H #define CSV_WORLD_REFERENCEABLECREATOR_H class QComboBox; #include "genericcreator.hpp" namespace CSVWorld { class ReferenceableCreator : public GenericCreator { Q_OBJECT QComboBox *mType; private: void configureCreateCommand (CSMWorld::CreateCommand& command) const override; public: ReferenceableCreator (CSMWorld::Data& data, QUndoStack& undoStack, const CSMWorld::UniversalId& id); void reset() override; void cloneMode (const std::string& originId, const CSMWorld::UniversalId::Type type) override; void toggleWidgets(bool active = true) override; }; } #endif ================================================ FILE: apps/opencs/view/world/referencecreator.cpp ================================================ #include "referencecreator.hpp" #include #include "../../model/doc/document.hpp" #include "../../model/world/data.hpp" #include "../../model/world/commands.hpp" #include "../../model/world/columns.hpp" #include "../../model/world/idtable.hpp" #include "../../model/world/idcompletionmanager.hpp" #include "../../model/world/commandmacro.hpp" #include "../widget/droplineedit.hpp" std::string CSVWorld::ReferenceCreator::getId() const { return mId; } void CSVWorld::ReferenceCreator::configureCreateCommand (CSMWorld::CreateCommand& command) const { // Set cellID int cellIdColumn = dynamic_cast (*getData().getTableModel (getCollectionId())). findColumnIndex (CSMWorld::Columns::ColumnId_Cell); command.addValue (cellIdColumn, mCell->text()); } CSVWorld::ReferenceCreator::ReferenceCreator (CSMWorld::Data& data, QUndoStack& undoStack, const CSMWorld::UniversalId& id, CSMWorld::IdCompletionManager &completionManager) : GenericCreator (data, undoStack, id) { QLabel *label = new QLabel ("Cell", this); insertBeforeButtons (label, false); // Add cell ID input with auto-completion. // Only existing cell IDs are accepted so no ID validation is performed. mCell = new CSVWidget::DropLineEdit(CSMWorld::ColumnBase::Display_Cell, this); mCell->setCompleter(completionManager.getCompleter(CSMWorld::ColumnBase::Display_Cell).get()); insertBeforeButtons (mCell, true); setManualEditing (false); connect (mCell, SIGNAL (textChanged (const QString&)), this, SLOT (cellChanged())); connect (mCell, SIGNAL (returnPressed()), this, SLOT (inputReturnPressed())); } void CSVWorld::ReferenceCreator::reset() { GenericCreator::reset(); mCell->setText (""); mId = getData().getReferences().getNewId(); } std::string CSVWorld::ReferenceCreator::getErrors() const { // We are ignoring errors coming from GenericCreator here, because the ID of the new // record is internal and requires neither user input nor verification. std::string errors; std::string cell = mCell->text().toUtf8().constData(); if (cell.empty()) errors += "Missing Cell ID"; else if (getData().getCells().searchId (cell)==-1) errors += "Invalid Cell ID"; return errors; } void CSVWorld::ReferenceCreator::focus() { mCell->setFocus(); } void CSVWorld::ReferenceCreator::cellChanged() { update(); } void CSVWorld::ReferenceCreator::cloneMode(const std::string& originId, const CSMWorld::UniversalId::Type type) { CSMWorld::IdTable& referenceTable = dynamic_cast ( *getData().getTableModel (CSMWorld::UniversalId::Type_References)); int cellIdColumn = referenceTable.findColumnIndex (CSMWorld::Columns::ColumnId_Cell); mCell->setText ( referenceTable.data (referenceTable.getModelIndex (originId, cellIdColumn)).toString()); CSVWorld::GenericCreator::cloneMode(originId, type); cellChanged(); //otherwise ok button will remain disabled } CSVWorld::Creator *CSVWorld::ReferenceCreatorFactory::makeCreator (CSMDoc::Document& document, const CSMWorld::UniversalId& id) const { return new ReferenceCreator(document.getData(), document.getUndoStack(), id, document.getIdCompletionManager()); } ================================================ FILE: apps/opencs/view/world/referencecreator.hpp ================================================ #ifndef CSV_WORLD_REFERENCECREATOR_H #define CSV_WORLD_REFERENCECREATOR_H #include "genericcreator.hpp" namespace CSMWorld { class IdCompletionManager; } namespace CSVWidget { class DropLineEdit; } namespace CSVWorld { class ReferenceCreator : public GenericCreator { Q_OBJECT CSVWidget::DropLineEdit *mCell; std::string mId; private: std::string getId() const override; void configureCreateCommand (CSMWorld::CreateCommand& command) const override; public: ReferenceCreator (CSMWorld::Data& data, QUndoStack& undoStack, const CSMWorld::UniversalId& id, CSMWorld::IdCompletionManager &completionManager); void cloneMode(const std::string& originId, const CSMWorld::UniversalId::Type type) override; void reset() override; std::string getErrors() const override; ///< Return formatted error descriptions for the current state of the creator. if an empty /// string is returned, there is no error. /// Focus main input widget void focus() override; private slots: void cellChanged(); }; class ReferenceCreatorFactory : public CreatorFactoryBase { public: Creator *makeCreator (CSMDoc::Document& document, const CSMWorld::UniversalId& id) const override; ///< The ownership of the returned Creator is transferred to the caller. }; } #endif ================================================ FILE: apps/opencs/view/world/regionmap.cpp ================================================ #include "regionmap.hpp" #include #include #include #include #include #include #include "../../model/doc/document.hpp" #include "../../model/world/regionmap.hpp" #include "../../model/world/universalid.hpp" #include "../../model/world/data.hpp" #include "../../model/world/idtable.hpp" #include "../../model/world/commands.hpp" #include "../../model/world/columns.hpp" #include "../../model/world/tablemimedata.hpp" #include "../../model/world/commandmacro.hpp" void CSVWorld::RegionMap::contextMenuEvent (QContextMenuEvent *event) { QMenu menu (this); if (getUnselectedCells().size()>0) menu.addAction (mSelectAllAction); if (selectionModel()->selectedIndexes().size()>0) menu.addAction (mClearSelectionAction); if (getMissingRegionCells().size()>0) menu.addAction (mSelectRegionsAction); int selectedNonExistentCells = getSelectedCells (false, true).size(); if (selectedNonExistentCells>0) { if (selectedNonExistentCells==1) mCreateCellsAction->setText ("Create one Cell"); else { std::ostringstream stream; stream << "Create " << selectedNonExistentCells << " cells"; mCreateCellsAction->setText (QString::fromUtf8 (stream.str().c_str())); } menu.addAction (mCreateCellsAction); } if (getSelectedCells().size()>0) { if (!mRegionId.empty()) { mSetRegionAction->setText (QString::fromUtf8 (("Set Region to " + mRegionId).c_str())); menu.addAction (mSetRegionAction); } menu.addAction (mUnsetRegionAction); menu.addAction (mViewInTableAction); } if (selectionModel()->selectedIndexes().size()>0) menu.addAction (mViewAction); menu.exec (event->globalPos()); } QModelIndexList CSVWorld::RegionMap::getUnselectedCells() const { const QAbstractItemModel *model = QTableView::model(); int rows = model->rowCount(); int columns = model->columnCount(); QModelIndexList selected = selectionModel()->selectedIndexes(); std::sort (selected.begin(), selected.end()); QModelIndexList all; for (int y=0; yindex (y, x); if (model->data (index, Qt::BackgroundRole)!=QBrush (Qt::DiagCrossPattern)) all.push_back (index); } std::sort (all.begin(), all.end()); QModelIndexList list; std::set_difference (all.begin(), all.end(), selected.begin(), selected.end(), std::back_inserter (list)); return list; } QModelIndexList CSVWorld::RegionMap::getSelectedCells (bool existent, bool nonExistent) const { const QAbstractItemModel *model = QTableView::model(); QModelIndexList selected = selectionModel()->selectedIndexes(); QModelIndexList list; for (QModelIndexList::const_iterator iter (selected.begin()); iter!=selected.end(); ++iter) { bool exists = model->data (*iter, Qt::BackgroundRole)!=QBrush (Qt::DiagCrossPattern); if ((exists && existent) || (!exists && nonExistent)) list.push_back (*iter); } return list; } QModelIndexList CSVWorld::RegionMap::getMissingRegionCells() const { const QAbstractItemModel *model = QTableView::model(); QModelIndexList selected = selectionModel()->selectedIndexes(); std::set regions; for (QModelIndexList::const_iterator iter (selected.begin()); iter!=selected.end(); ++iter) { std::string region = model->data (*iter, CSMWorld::RegionMap::Role_Region).toString().toUtf8().constData(); if (!region.empty()) regions.insert (region); } QModelIndexList list; QModelIndexList unselected = getUnselectedCells(); for (QModelIndexList::const_iterator iter (unselected.begin()); iter!=unselected.end(); ++iter) { std::string region = model->data (*iter, CSMWorld::RegionMap::Role_Region).toString().toUtf8().constData(); if (!region.empty() && regions.find (region)!=regions.end()) list.push_back (*iter); } return list; } void CSVWorld::RegionMap::setRegion (const std::string& regionId) { QModelIndexList selected = getSelectedCells(); QAbstractItemModel *regionModel = model(); CSMWorld::IdTable *cellsModel = &dynamic_cast (* mDocument.getData().getTableModel (CSMWorld::UniversalId::Type_Cells)); QString regionId2 = QString::fromUtf8 (regionId.c_str()); CSMWorld::CommandMacro macro (mDocument.getUndoStack(), selected.size()>1 ? tr ("Set Region") : ""); for (QModelIndexList::const_iterator iter (selected.begin()); iter!=selected.end(); ++iter) { std::string cellId = regionModel->data (*iter, CSMWorld::RegionMap::Role_CellId). toString().toUtf8().constData(); QModelIndex index = cellsModel->getModelIndex (cellId, cellsModel->findColumnIndex (CSMWorld::Columns::ColumnId_Region)); macro.push (new CSMWorld::ModifyCommand (*cellsModel, index, regionId2)); } } CSVWorld::RegionMap::RegionMap (const CSMWorld::UniversalId& universalId, CSMDoc::Document& document, QWidget *parent) : DragRecordTable(document, parent) { verticalHeader()->hide(); horizontalHeader()->hide(); setSelectionMode (QAbstractItemView::ExtendedSelection); setModel (document.getData().getTableModel (universalId)); resizeColumnsToContents(); resizeRowsToContents(); mSelectAllAction = new QAction (tr ("Select All"), this); connect (mSelectAllAction, SIGNAL (triggered()), this, SLOT (selectAll())); addAction (mSelectAllAction); mClearSelectionAction = new QAction (tr ("Clear Selection"), this); connect (mClearSelectionAction, SIGNAL (triggered()), this, SLOT (clearSelection())); addAction (mClearSelectionAction); mSelectRegionsAction = new QAction (tr ("Select Regions"), this); connect (mSelectRegionsAction, SIGNAL (triggered()), this, SLOT (selectRegions())); addAction (mSelectRegionsAction); mCreateCellsAction = new QAction (tr ("Create Cells Action"), this); connect (mCreateCellsAction, SIGNAL (triggered()), this, SLOT (createCells())); addAction (mCreateCellsAction); mSetRegionAction = new QAction (tr ("Set Region"), this); connect (mSetRegionAction, SIGNAL (triggered()), this, SLOT (setRegion())); addAction (mSetRegionAction); mUnsetRegionAction = new QAction (tr ("Unset Region"), this); connect (mUnsetRegionAction, SIGNAL (triggered()), this, SLOT (unsetRegion())); addAction (mUnsetRegionAction); mViewAction = new QAction (tr ("View Cells"), this); connect (mViewAction, SIGNAL (triggered()), this, SLOT (view())); addAction (mViewAction); mViewInTableAction = new QAction (tr ("View Cells in Table"), this); connect (mViewInTableAction, SIGNAL (triggered()), this, SLOT (viewInTable())); addAction (mViewInTableAction); setAcceptDrops(true); } void CSVWorld::RegionMap::selectAll() { QModelIndexList unselected = getUnselectedCells(); for (QModelIndexList::const_iterator iter (unselected.begin()); iter!=unselected.end(); ++iter) selectionModel()->select (*iter, QItemSelectionModel::Select); } void CSVWorld::RegionMap::clearSelection() { selectionModel()->clearSelection(); } void CSVWorld::RegionMap::selectRegions() { QModelIndexList unselected = getMissingRegionCells(); for (QModelIndexList::const_iterator iter (unselected.begin()); iter!=unselected.end(); ++iter) selectionModel()->select (*iter, QItemSelectionModel::Select); } void CSVWorld::RegionMap::createCells() { if (mEditLock) return; QModelIndexList selected = getSelectedCells (false, true); CSMWorld::IdTable *cellsModel = &dynamic_cast (* mDocument.getData().getTableModel (CSMWorld::UniversalId::Type_Cells)); CSMWorld::CommandMacro macro (mDocument.getUndoStack(), selected.size()>1 ? tr ("Create cells"): ""); for (QModelIndexList::const_iterator iter (selected.begin()); iter!=selected.end(); ++iter) { std::string cellId = model()->data (*iter, CSMWorld::RegionMap::Role_CellId). toString().toUtf8().constData(); macro.push (new CSMWorld::CreateCommand (*cellsModel, cellId)); } } void CSVWorld::RegionMap::setRegion() { if (mEditLock) return; setRegion (mRegionId); } void CSVWorld::RegionMap::unsetRegion() { if (mEditLock) return; setRegion (""); } void CSVWorld::RegionMap::view() { std::ostringstream hint; hint << "c:"; QModelIndexList selected = selectionModel()->selectedIndexes(); bool first = true; for (QModelIndexList::const_iterator iter (selected.begin()); iter!=selected.end(); ++iter) { std::string cellId = model()->data (*iter, CSMWorld::RegionMap::Role_CellId). toString().toUtf8().constData(); if (first) first = false; else hint << "; "; hint << cellId; } emit editRequest (CSMWorld::UniversalId (CSMWorld::UniversalId::Type_Scene, ESM::CellId::sDefaultWorldspace), hint.str()); } void CSVWorld::RegionMap::viewInTable() { std::ostringstream hint; hint << "f:!or("; QModelIndexList selected = getSelectedCells(); bool first = true; for (QModelIndexList::const_iterator iter (selected.begin()); iter!=selected.end(); ++iter) { std::string cellId = model()->data (*iter, CSMWorld::RegionMap::Role_CellId). toString().toUtf8().constData(); if (first) first = false; else hint << ","; hint << "string(ID,\"" << cellId << "\")"; } hint << ")"; emit editRequest (CSMWorld::UniversalId::Type_Cells, hint.str()); } void CSVWorld::RegionMap::mouseMoveEvent (QMouseEvent* event) { startDragFromTable(*this); } std::vector< CSMWorld::UniversalId > CSVWorld::RegionMap::getDraggedRecords() const { QModelIndexList selected(getSelectedCells(true, false)); std::vector ids; for (const QModelIndex& it : selected) { ids.emplace_back( CSMWorld::UniversalId::Type_Cell, model()->data(it, CSMWorld::RegionMap::Role_CellId).toString().toUtf8().constData()); } selected = getSelectedCells(false, true); for (const QModelIndex& it : selected) { ids.emplace_back( CSMWorld::UniversalId::Type_Cell_Missing, model()->data(it, CSMWorld::RegionMap::Role_CellId).toString().toUtf8().constData()); } return ids; } void CSVWorld::RegionMap::dropEvent (QDropEvent* event) { QModelIndex index = indexAt (event->pos()); bool exists = QTableView::model()->data(index, Qt::BackgroundRole)!=QBrush (Qt::DiagCrossPattern); if (!index.isValid() || !exists) { return; } const CSMWorld::TableMimeData* mime = dynamic_cast (event->mimeData()); if (!mime) // May happen when non-records (e.g. plain text) are dragged and dropped return; if (mime->fromDocument(mDocument) && mime->holdsType(CSMWorld::UniversalId::Type_Region)) { CSMWorld::UniversalId record (mime->returnMatching (CSMWorld::UniversalId::Type_Region)); QAbstractItemModel *regionModel = model(); CSMWorld::IdTable *cellsModel = &dynamic_cast (* mDocument.getData().getTableModel (CSMWorld::UniversalId::Type_Cells)); std::string cellId(regionModel->data (index, CSMWorld::RegionMap::Role_CellId). toString().toUtf8().constData()); QModelIndex index2(cellsModel->getModelIndex (cellId, cellsModel->findColumnIndex (CSMWorld::Columns::ColumnId_Region))); mDocument.getUndoStack().push(new CSMWorld::ModifyCommand (*cellsModel, index2, QString::fromUtf8(record.getId().c_str()))); mRegionId = record.getId(); } } ================================================ FILE: apps/opencs/view/world/regionmap.hpp ================================================ #ifndef CSV_WORLD_REGIONMAP_H #define CSV_WORLD_REGIONMAP_H #include #include #include #include #include "./dragrecordtable.hpp" class QAction; namespace CSMDoc { class Document; } namespace CSMWorld { class UniversalId; } namespace CSVWorld { class RegionMap : public DragRecordTable { Q_OBJECT QAction *mSelectAllAction; QAction *mClearSelectionAction; QAction *mSelectRegionsAction; QAction *mCreateCellsAction; QAction *mSetRegionAction; QAction *mUnsetRegionAction; QAction *mViewAction; QAction *mViewInTableAction; std::string mRegionId; private: void contextMenuEvent (QContextMenuEvent *event) override; QModelIndexList getUnselectedCells() const; ///< \note Non-existent cells are not listed. QModelIndexList getSelectedCells (bool existent = true, bool nonExistent = false) const; ///< \param existent Include existent cells. /// \param nonExistent Include non-existent cells. QModelIndexList getMissingRegionCells() const; ///< Unselected cells within all regions that have at least one selected cell. void setRegion (const std::string& regionId); ///< Set region Id of selected cells. void mouseMoveEvent(QMouseEvent *event) override; void dropEvent(QDropEvent* event) override; public: RegionMap (const CSMWorld::UniversalId& universalId, CSMDoc::Document& document, QWidget *parent = nullptr); std::vector getDraggedRecords() const override; signals: void editRequest (const CSMWorld::UniversalId& id, const std::string& hint); private slots: void selectAll() override; void clearSelection(); void selectRegions(); void createCells(); void setRegion(); void unsetRegion(); void view(); void viewInTable(); }; } #endif ================================================ FILE: apps/opencs/view/world/regionmapsubview.cpp ================================================ #include "regionmapsubview.hpp" #include "regionmap.hpp" CSVWorld::RegionMapSubView::RegionMapSubView (CSMWorld::UniversalId universalId, CSMDoc::Document& document) : CSVDoc::SubView (universalId) { mRegionMap = new RegionMap (universalId, document, this); setWidget (mRegionMap); connect (mRegionMap, SIGNAL (editRequest (const CSMWorld::UniversalId&, const std::string&)), this, SLOT (editRequest (const CSMWorld::UniversalId&, const std::string&))); } void CSVWorld::RegionMapSubView::setEditLock (bool locked) { mRegionMap->setEditLock (locked); } void CSVWorld::RegionMapSubView::editRequest (const CSMWorld::UniversalId& id, const std::string& hint) { focusId (id, hint); } ================================================ FILE: apps/opencs/view/world/regionmapsubview.hpp ================================================ #ifndef CSV_WORLD_REGIONMAPSUBVIEW_H #define CSV_WORLD_REGIONMAPSUBVIEW_H #include "../doc/subview.hpp" class QAction; namespace CSMDoc { class Document; } namespace CSVWorld { class RegionMap; class RegionMapSubView : public CSVDoc::SubView { Q_OBJECT RegionMap *mRegionMap; public: RegionMapSubView (CSMWorld::UniversalId universalId, CSMDoc::Document& document); void setEditLock (bool locked) override; private slots: void editRequest (const CSMWorld::UniversalId& id, const std::string& hint); }; } #endif ================================================ FILE: apps/opencs/view/world/scenesubview.cpp ================================================ #include "scenesubview.hpp" #include #include #include #include #include #include "../../model/doc/document.hpp" #include "../../model/world/cellselection.hpp" #include "../filter/filterbox.hpp" #include "../render/pagedworldspacewidget.hpp" #include "../render/unpagedworldspacewidget.hpp" #include "../widget/scenetoolbar.hpp" #include "../widget/scenetoolmode.hpp" #include "../widget/scenetooltoggle.hpp" #include "../widget/scenetooltoggle2.hpp" #include "../widget/scenetoolrun.hpp" #include "tablebottombox.hpp" #include "creator.hpp" CSVWorld::SceneSubView::SceneSubView (const CSMWorld::UniversalId& id, CSMDoc::Document& document) : SubView (id), mScene(nullptr), mLayout(new QHBoxLayout), mDocument(document), mToolbar(nullptr) { QVBoxLayout *layout = new QVBoxLayout; layout->addWidget (mBottom = new TableBottomBox (NullCreatorFactory(), document, id, this), 0); mLayout->setContentsMargins (QMargins (0, 0, 0, 0)); CSVRender::WorldspaceWidget* worldspaceWidget = nullptr; widgetType whatWidget; if (id.getId()==ESM::CellId::sDefaultWorldspace) { whatWidget = widget_Paged; CSVRender::PagedWorldspaceWidget *newWidget = new CSVRender::PagedWorldspaceWidget (this, document); worldspaceWidget = newWidget; makeConnections(newWidget); } else { whatWidget = widget_Unpaged; CSVRender::UnpagedWorldspaceWidget *newWidget = new CSVRender::UnpagedWorldspaceWidget (id.getId(), document, this); worldspaceWidget = newWidget; makeConnections(newWidget); } replaceToolbarAndWorldspace(worldspaceWidget, makeToolbar(worldspaceWidget, whatWidget)); layout->insertLayout (0, mLayout, 1); CSVFilter::FilterBox *filterBox = new CSVFilter::FilterBox (document.getData(), this); layout->insertWidget (0, filterBox); QWidget *widget = new QWidget; widget->setLayout (layout); setWidget (widget); } void CSVWorld::SceneSubView::makeConnections (CSVRender::UnpagedWorldspaceWidget* widget) { connect (widget, SIGNAL (closeRequest()), this, SLOT (closeRequest())); connect(widget, SIGNAL(dataDropped(const std::vector&)), this, SLOT(handleDrop(const std::vector&))); connect(widget, SIGNAL(cellChanged(const CSMWorld::UniversalId&)), this, SLOT(cellSelectionChanged(const CSMWorld::UniversalId&))); connect(widget, SIGNAL(requestFocus (const std::string&)), this, SIGNAL(requestFocus (const std::string&))); } void CSVWorld::SceneSubView::makeConnections (CSVRender::PagedWorldspaceWidget* widget) { connect (widget, SIGNAL (closeRequest()), this, SLOT (closeRequest())); connect(widget, SIGNAL(dataDropped(const std::vector&)), this, SLOT(handleDrop(const std::vector&))); connect (widget, SIGNAL (cellSelectionChanged (const CSMWorld::CellSelection&)), this, SLOT (cellSelectionChanged (const CSMWorld::CellSelection&))); connect(widget, SIGNAL(requestFocus (const std::string&)), this, SIGNAL(requestFocus (const std::string&))); } CSVWidget::SceneToolbar* CSVWorld::SceneSubView::makeToolbar (CSVRender::WorldspaceWidget* widget, widgetType type) { CSVWidget::SceneToolbar* toolbar = new CSVWidget::SceneToolbar (48+6, this); CSVWidget::SceneToolMode *navigationTool = widget->makeNavigationSelector (toolbar); toolbar->addTool (navigationTool); CSVWidget::SceneToolMode *lightingTool = widget->makeLightingSelector (toolbar); toolbar->addTool (lightingTool); CSVWidget::SceneToolToggle2 *sceneVisibilityTool = widget->makeSceneVisibilitySelector (toolbar); toolbar->addTool (sceneVisibilityTool); if (type==widget_Paged) { CSVWidget::SceneToolToggle2 *controlVisibilityTool = dynamic_cast (*widget). makeControlVisibilitySelector (toolbar); toolbar->addTool (controlVisibilityTool); } CSVWidget::SceneToolRun *runTool = widget->makeRunTool (toolbar); toolbar->addTool (runTool); toolbar->addTool (widget->makeEditModeSelector (toolbar), runTool); return toolbar; } void CSVWorld::SceneSubView::setEditLock (bool locked) { mScene->setEditLock (locked); } void CSVWorld::SceneSubView::setStatusBar (bool show) { mBottom->setStatusBar (show); } void CSVWorld::SceneSubView::useHint (const std::string& hint) { mScene->useViewHint (hint); } std::string CSVWorld::SceneSubView::getTitle() const { return mTitle; } void CSVWorld::SceneSubView::cellSelectionChanged (const CSMWorld::UniversalId& id) { setUniversalId(id); mTitle = "Scene: " + getUniversalId().getId(); setWindowTitle (QString::fromUtf8 (mTitle.c_str())); emit updateTitle(); } void CSVWorld::SceneSubView::cellSelectionChanged (const CSMWorld::CellSelection& selection) { setUniversalId(CSMWorld::UniversalId(CSMWorld::UniversalId::Type_Scene, ESM::CellId::sDefaultWorldspace)); int size = selection.getSize(); std::ostringstream stream; stream << "Scene: " << getUniversalId().getId(); if (size==0) stream << " (empty)"; else if (size==1) { stream << " (" << *selection.begin() << ")"; } else { stream << " (" << selection.getCentre() << " and " << size-1 << " more "; if (size>1) stream << "cells around it)"; else stream << "cell around it)"; } mTitle = stream.str(); setWindowTitle (QString::fromUtf8 (mTitle.c_str())); emit updateTitle(); } void CSVWorld::SceneSubView::handleDrop (const std::vector< CSMWorld::UniversalId >& universalIdData) { CSVRender::PagedWorldspaceWidget* pagedNewWidget = nullptr; CSVRender::UnpagedWorldspaceWidget* unPagedNewWidget = nullptr; CSVWidget::SceneToolbar* toolbar = nullptr; CSVRender::WorldspaceWidget::DropType type = CSVRender::WorldspaceWidget::getDropType (universalIdData); switch (mScene->getDropRequirements (type)) { case CSVRender::WorldspaceWidget::canHandle: mScene->handleDrop (universalIdData, type); break; case CSVRender::WorldspaceWidget::needPaged: pagedNewWidget = new CSVRender::PagedWorldspaceWidget(this, mDocument); toolbar = makeToolbar(pagedNewWidget, widget_Paged); makeConnections(pagedNewWidget); replaceToolbarAndWorldspace(pagedNewWidget, toolbar); mScene->handleDrop (universalIdData, type); break; case CSVRender::WorldspaceWidget::needUnpaged: unPagedNewWidget = new CSVRender::UnpagedWorldspaceWidget(universalIdData.begin()->getId(), mDocument, this); toolbar = makeToolbar(unPagedNewWidget, widget_Unpaged); makeConnections(unPagedNewWidget); replaceToolbarAndWorldspace(unPagedNewWidget, toolbar); cellSelectionChanged(*(universalIdData.begin())); break; case CSVRender::WorldspaceWidget::ignored: return; } } void CSVWorld::SceneSubView::replaceToolbarAndWorldspace (CSVRender::WorldspaceWidget* widget, CSVWidget::SceneToolbar* toolbar) { assert(mLayout); if (mScene) { mLayout->removeWidget(mScene); mScene->deleteLater(); } if (mToolbar) { mLayout->removeWidget(mToolbar); mToolbar->deleteLater(); } mScene = widget; mToolbar = toolbar; connect (mScene, SIGNAL (focusToolbarRequest()), mToolbar, SLOT (setFocus())); connect (mToolbar, SIGNAL (focusSceneRequest()), mScene, SLOT (setFocus())); mLayout->addWidget (mToolbar, 0); mLayout->addWidget (mScene, 1); mScene->selectDefaultNavigationMode(); setFocusProxy (mScene); } ================================================ FILE: apps/opencs/view/world/scenesubview.hpp ================================================ #ifndef CSV_WORLD_SCENESUBVIEW_H #define CSV_WORLD_SCENESUBVIEW_H #include #include "../doc/subview.hpp" class QModelIndex; namespace CSMWorld { class CellSelection; } namespace CSMDoc { class Document; } namespace CSVRender { class WorldspaceWidget; class PagedWorldspaceWidget; class UnpagedWorldspaceWidget; } namespace CSVWidget { class SceneToolbar; class SceneToolMode; } namespace CSVWorld { class Table; class TableBottomBox; class CreatorFactoryBase; class SceneSubView : public CSVDoc::SubView { Q_OBJECT TableBottomBox *mBottom; CSVRender::WorldspaceWidget *mScene; QHBoxLayout* mLayout; CSMDoc::Document& mDocument; CSVWidget::SceneToolbar* mToolbar; std::string mTitle; public: SceneSubView (const CSMWorld::UniversalId& id, CSMDoc::Document& document); void setEditLock (bool locked) override; void setStatusBar (bool show) override; void useHint (const std::string& hint) override; std::string getTitle() const override; private: void makeConnections(CSVRender::PagedWorldspaceWidget* widget); void makeConnections(CSVRender::UnpagedWorldspaceWidget* widget); void replaceToolbarAndWorldspace(CSVRender::WorldspaceWidget* widget, CSVWidget::SceneToolbar* toolbar); enum widgetType { widget_Paged, widget_Unpaged }; CSVWidget::SceneToolbar* makeToolbar(CSVRender::WorldspaceWidget* widget, widgetType type); private slots: void cellSelectionChanged (const CSMWorld::CellSelection& selection); void cellSelectionChanged (const CSMWorld::UniversalId& id); void handleDrop(const std::vector& data); signals: void requestFocus (const std::string& id); }; } #endif ================================================ FILE: apps/opencs/view/world/scriptedit.cpp ================================================ #include "scriptedit.hpp" #include #include #include #include #include #include #include #include "../../model/doc/document.hpp" #include "../../model/world/universalid.hpp" #include "../../model/world/tablemimedata.hpp" #include "../../model/prefs/state.hpp" #include "../../model/prefs/shortcut.hpp" CSVWorld::ScriptEdit::ChangeLock::ChangeLock (ScriptEdit& edit) : mEdit (edit) { ++mEdit.mChangeLocked; } CSVWorld::ScriptEdit::ChangeLock::~ChangeLock() { --mEdit.mChangeLocked; } bool CSVWorld::ScriptEdit::event (QEvent *event) { // ignore undo and redo shortcuts if (event->type()==QEvent::ShortcutOverride) { QKeyEvent *keyEvent = static_cast (event); if (keyEvent->matches (QKeySequence::Undo) || keyEvent->matches (QKeySequence::Redo)) return true; } return QPlainTextEdit::event (event); } CSVWorld::ScriptEdit::ScriptEdit( const CSMDoc::Document& document, ScriptHighlighter::Mode mode, QWidget* parent ) : QPlainTextEdit(parent), mChangeLocked(0), mShowLineNum(false), mLineNumberArea(nullptr), mDefaultFont(font()), mMonoFont(QFont("Monospace")), mTabCharCount(4), mMarkOccurrences(true), mDocument(document), mWhiteListQoutes("^[a-z|_]{1}[a-z|0-9|_]{0,}$", Qt::CaseInsensitive) { wrapLines(false); setTabWidth(); setUndoRedoEnabled (false); // we use OpenCS-wide undo/redo instead mAllowedTypes <associateAction(mCommentAction); mUncommentAction = new QAction (tr ("Uncomment Selection"), this); connect(mUncommentAction, SIGNAL (triggered()), this, SLOT (uncommentSelection())); CSMPrefs::Shortcut *uncommentShortcut = new CSMPrefs::Shortcut("script-editor-uncomment", this); uncommentShortcut->associateAction(mUncommentAction); mHighlighter = new ScriptHighlighter (document.getData(), mode, ScriptEdit::document()); connect (&document.getData(), SIGNAL (idListChanged()), this, SLOT (idListChanged())); connect (&mUpdateTimer, SIGNAL (timeout()), this, SLOT (updateHighlighting())); connect (&CSMPrefs::State::get(), SIGNAL (settingChanged (const CSMPrefs::Setting *)), this, SLOT (settingChanged (const CSMPrefs::Setting *))); { ChangeLock lock (*this); CSMPrefs::get()["Scripts"].update(); } mUpdateTimer.setSingleShot (true); // TODO: provide a font selector dialogue mMonoFont.setStyleHint(QFont::TypeWriter); mLineNumberArea = new LineNumberArea(this); updateLineNumberAreaWidth(0); connect(this, SIGNAL(blockCountChanged(int)), this, SLOT(updateLineNumberAreaWidth(int))); connect(this, SIGNAL(updateRequest(QRect,int)), this, SLOT(updateLineNumberArea(QRect,int))); updateHighlighting(); } void CSVWorld::ScriptEdit::showLineNum(bool show) { if(show!=mShowLineNum) { mShowLineNum = show; updateLineNumberAreaWidth(0); } } bool CSVWorld::ScriptEdit::isChangeLocked() const { return mChangeLocked!=0; } void CSVWorld::ScriptEdit::dragEnterEvent (QDragEnterEvent* event) { const CSMWorld::TableMimeData* mime = dynamic_cast (event->mimeData()); if (!mime) QPlainTextEdit::dragEnterEvent(event); else { setTextCursor (cursorForPosition (event->pos())); event->acceptProposedAction(); } } void CSVWorld::ScriptEdit::dragMoveEvent (QDragMoveEvent* event) { const CSMWorld::TableMimeData* mime = dynamic_cast (event->mimeData()); if (!mime) QPlainTextEdit::dragMoveEvent(event); else { setTextCursor (cursorForPosition (event->pos())); event->accept(); } } void CSVWorld::ScriptEdit::dropEvent (QDropEvent* event) { const CSMWorld::TableMimeData* mime = dynamic_cast (event->mimeData()); if (!mime) // May happen when non-records (e.g. plain text) are dragged and dropped { QPlainTextEdit::dropEvent(event); return; } setTextCursor (cursorForPosition (event->pos())); if (mime->fromDocument (mDocument)) { std::vector records (mime->getData()); for (std::vector::iterator it = records.begin(); it != records.end(); ++it) { if (mAllowedTypes.contains (it->getType())) { if (stringNeedsQuote(it->getId())) { insertPlainText(QString::fromUtf8 (('"' + it->getId() + '"').c_str())); } else { insertPlainText(QString::fromUtf8 (it->getId().c_str())); } } } } } bool CSVWorld::ScriptEdit::stringNeedsQuote (const std::string& id) const { const QString string(QString::fromUtf8(id.c_str())); // is only for c++11, so let's use qregexp for now. //I'm not quite sure when do we need to put quotes. To be safe we will use quotes for anything other than… return !(string.contains(mWhiteListQoutes)); } void CSVWorld::ScriptEdit::setTabWidth() { // Set tab width to specified number of characters using current font. setTabStopDistance(mTabCharCount * fontMetrics().horizontalAdvance(' ')); } void CSVWorld::ScriptEdit::wrapLines(bool wrap) { if (wrap) { setLineWrapMode(QPlainTextEdit::WidgetWidth); } else { setLineWrapMode(QPlainTextEdit::NoWrap); } } void CSVWorld::ScriptEdit::settingChanged(const CSMPrefs::Setting *setting) { // Determine which setting was changed. if (mHighlighter->settingChanged(setting)) { updateHighlighting(); } else if (*setting == "Scripts/mono-font") { setFont(setting->isTrue() ? mMonoFont : mDefaultFont); setTabWidth(); } else if (*setting == "Scripts/show-linenum") { showLineNum(setting->isTrue()); } else if (*setting == "Scripts/wrap-lines") { wrapLines(setting->isTrue()); } else if (*setting == "Scripts/tab-width") { mTabCharCount = setting->toInt(); setTabWidth(); } else if (*setting == "Scripts/highlight-occurrences") { mMarkOccurrences = setting->isTrue(); mHighlighter->setMarkedWord(""); updateHighlighting(); mHighlighter->setMarkOccurrences(mMarkOccurrences); } } void CSVWorld::ScriptEdit::idListChanged() { mHighlighter->invalidateIds(); if (!mUpdateTimer.isActive()) mUpdateTimer.start (0); } void CSVWorld::ScriptEdit::updateHighlighting() { if (isChangeLocked()) return; ChangeLock lock (*this); mHighlighter->rehighlight(); } int CSVWorld::ScriptEdit::lineNumberAreaWidth() { if(!mShowLineNum) return 0; int digits = 1; int max = qMax(1, blockCount()); while (max >= 10) { max /= 10; ++digits; } int space = 3 + fontMetrics().horizontalAdvance(QLatin1Char('9')) * digits; return space; } void CSVWorld::ScriptEdit::updateLineNumberAreaWidth(int /* newBlockCount */) { setViewportMargins(lineNumberAreaWidth(), 0, 0, 0); } void CSVWorld::ScriptEdit::updateLineNumberArea(const QRect &rect, int dy) { if (dy) mLineNumberArea->scroll(0, dy); else mLineNumberArea->update(0, rect.y(), mLineNumberArea->width(), rect.height()); if (rect.contains(viewport()->rect())) updateLineNumberAreaWidth(0); } void CSVWorld::ScriptEdit::markOccurrences() { if (mMarkOccurrences) { QTextCursor cursor = textCursor(); // prevent infinite recursion with cursor.select(), // which ends up calling this function again // could be fixed with blockSignals, but mDocument is const disconnect(this, SIGNAL(cursorPositionChanged()), this, nullptr); cursor.select(QTextCursor::WordUnderCursor); connect(this, SIGNAL(cursorPositionChanged()), this, SLOT(markOccurrences())); QString word = cursor.selectedText(); mHighlighter->setMarkedWord(word.toStdString()); mHighlighter->rehighlight(); } } void CSVWorld::ScriptEdit::commentSelection() { QTextCursor begin = textCursor(); QTextCursor end = begin; begin.setPosition(begin.selectionStart()); begin.movePosition(QTextCursor::StartOfLine); end.setPosition(end.selectionEnd()); end.movePosition(QTextCursor::EndOfLine); begin.beginEditBlock(); for (; begin < end; begin.movePosition(QTextCursor::EndOfLine), begin.movePosition(QTextCursor::Right)) { begin.insertText(";"); } begin.endEditBlock(); } void CSVWorld::ScriptEdit::uncommentSelection() { QTextCursor begin = textCursor(); QTextCursor end = begin; begin.setPosition(begin.selectionStart()); begin.movePosition(QTextCursor::StartOfLine); end.setPosition(end.selectionEnd()); end.movePosition(QTextCursor::EndOfLine); begin.beginEditBlock(); for (; begin < end; begin.movePosition(QTextCursor::EndOfLine), begin.movePosition(QTextCursor::Right)) { begin.select(QTextCursor::LineUnderCursor); QString line = begin.selectedText(); if (line.size() == 0) continue; // get first nonspace character in line int index; for (index = 0; index != line.size(); ++index) { if (!line[index].isSpace()) break; } if (index != line.size() && line[index] == ';') { // remove the semicolon line.remove(index, 1); // put the line back begin.insertText(line); } } begin.endEditBlock(); } void CSVWorld::ScriptEdit::resizeEvent(QResizeEvent *e) { QPlainTextEdit::resizeEvent(e); QRect cr = contentsRect(); mLineNumberArea->setGeometry(QRect(cr.left(), cr.top(), lineNumberAreaWidth(), cr.height())); } void CSVWorld::ScriptEdit::contextMenuEvent(QContextMenuEvent *event) { QMenu *menu = createStandardContextMenu(); // remove redo/undo since they are disabled QList menuActions = menu->actions(); for (QList::iterator i = menuActions.begin(); i < menuActions.end(); ++i) { if ((*i)->text().contains("Undo") || (*i)->text().contains("Redo")) { (*i)->setVisible(false); } } menu->addAction(mCommentAction); menu->addAction(mUncommentAction); menu->exec(event->globalPos()); delete menu; } void CSVWorld::ScriptEdit::lineNumberAreaPaintEvent(QPaintEvent *event) { QPainter painter(mLineNumberArea); QTextBlock block = firstVisibleBlock(); int blockNumber = block.blockNumber(); int top = (int) blockBoundingGeometry(block).translated(contentOffset()).top(); int bottom = top + (int) blockBoundingRect(block).height(); int startBlock = textCursor().blockNumber(); int endBlock = textCursor().blockNumber(); if(textCursor().hasSelection()) { QString str = textCursor().selection().toPlainText(); int offset = str.count("\n"); if(textCursor().position() < textCursor().anchor()) endBlock += offset; else startBlock -= offset; } painter.setBackgroundMode(Qt::OpaqueMode); QFont font = painter.font(); QBrush background = painter.background(); while (block.isValid() && top <= event->rect().bottom()) { if (block.isVisible() && bottom >= event->rect().top()) { QFont newFont = painter.font(); QString number = QString::number(blockNumber + 1); if(blockNumber >= startBlock && blockNumber <= endBlock) { painter.setBackground(Qt::cyan); painter.setPen(Qt::darkMagenta); newFont.setBold(true); } else { painter.setBackground(background); painter.setPen(Qt::black); } painter.setFont(newFont); painter.drawText(0, top, mLineNumberArea->width(), fontMetrics().height(), Qt::AlignRight, number); painter.setFont(font); } block = block.next(); top = bottom; bottom = top + (int) blockBoundingRect(block).height(); ++blockNumber; } } CSVWorld::LineNumberArea::LineNumberArea(ScriptEdit *editor) : QWidget(editor), mScriptEdit(editor) {} QSize CSVWorld::LineNumberArea::sizeHint() const { return QSize(mScriptEdit->lineNumberAreaWidth(), 0); } void CSVWorld::LineNumberArea::paintEvent(QPaintEvent *event) { mScriptEdit->lineNumberAreaPaintEvent(event); } ================================================ FILE: apps/opencs/view/world/scriptedit.hpp ================================================ #ifndef SCRIPTEDIT_H #define SCRIPTEDIT_H #include #include #include #include #include #include #include "../../model/world/universalid.hpp" #include "scripthighlighter.hpp" class QRegExp; namespace CSMDoc { class Document; } namespace CSVWorld { class LineNumberArea; /// \brief Editor for scripts. class ScriptEdit : public QPlainTextEdit { Q_OBJECT public: class ChangeLock { ScriptEdit& mEdit; ChangeLock (const ChangeLock&); ChangeLock& operator= (const ChangeLock&); public: ChangeLock (ScriptEdit& edit); ~ChangeLock(); }; friend class ChangeLock; private: int mChangeLocked; ScriptHighlighter *mHighlighter; QTimer mUpdateTimer; bool mShowLineNum; LineNumberArea *mLineNumberArea; QFont mDefaultFont; QFont mMonoFont; int mTabCharCount; bool mMarkOccurrences; QAction *mCommentAction; QAction *mUncommentAction; protected: bool event (QEvent *event) override; public: ScriptEdit (const CSMDoc::Document& document, ScriptHighlighter::Mode mode, QWidget* parent); /// Should changes to the data be ignored (i.e. not cause updated)? /// /// \note This mechanism is used to avoid infinite update recursions bool isChangeLocked() const; void lineNumberAreaPaintEvent(QPaintEvent *event); int lineNumberAreaWidth(); void showLineNum(bool show); protected: void resizeEvent(QResizeEvent *e) override; void contextMenuEvent(QContextMenuEvent *event) override; private: QVector mAllowedTypes; const CSMDoc::Document& mDocument; const QRegExp mWhiteListQoutes; void dragEnterEvent (QDragEnterEvent* event) override; void dropEvent (QDropEvent* event) override; void dragMoveEvent (QDragMoveEvent* event) override; bool stringNeedsQuote(const std::string& id) const; /// \brief Set tab width for script editor. void setTabWidth(); /// \brief Turn line wrapping in script editor on or off. /// \param wrap Whether or not to wrap lines. void wrapLines(bool wrap); private slots: /// \brief Update editor when related setting is changed. /// \param setting Setting that was changed. void settingChanged(const CSMPrefs::Setting *setting); void idListChanged(); void updateHighlighting(); void updateLineNumberAreaWidth(int newBlockCount); void updateLineNumberArea(const QRect &, int); void markOccurrences(); void commentSelection(); void uncommentSelection(); }; class LineNumberArea : public QWidget { ScriptEdit *mScriptEdit; public: LineNumberArea(ScriptEdit *editor); QSize sizeHint() const override; protected: void paintEvent(QPaintEvent *event) override; }; } #endif // SCRIPTEDIT_H ================================================ FILE: apps/opencs/view/world/scripterrortable.cpp ================================================ #include "scripterrortable.hpp" #include #include #include #include #include #include #include "../../model/doc/document.hpp" #include "../../model/prefs/state.hpp" void CSVWorld::ScriptErrorTable::report (const std::string& message, const Compiler::TokenLoc& loc, Type type) { std::ostringstream stream; stream << message << " (" << loc.mLiteral << ")"; addMessage (stream.str(), type==Compiler::ErrorHandler::WarningMessage ? CSMDoc::Message::Severity_Warning : CSMDoc::Message::Severity_Error, loc.mLine, loc.mColumn-loc.mLiteral.length()); } void CSVWorld::ScriptErrorTable::report (const std::string& message, Type type) { addMessage (message, type==Compiler::ErrorHandler::WarningMessage ? CSMDoc::Message::Severity_Warning : CSMDoc::Message::Severity_Error); } void CSVWorld::ScriptErrorTable::addMessage (const std::string& message, CSMDoc::Message::Severity severity, int line, int column) { int row = rowCount(); setRowCount (row+1); QTableWidgetItem *severityItem = new QTableWidgetItem ( QString::fromUtf8 (CSMDoc::Message::toString (severity).c_str())); severityItem->setFlags (severityItem->flags() ^ Qt::ItemIsEditable); setItem (row, 0, severityItem); if (line!=-1) { QTableWidgetItem *lineItem = new QTableWidgetItem; lineItem->setData (Qt::DisplayRole, line+1); lineItem->setFlags (lineItem->flags() ^ Qt::ItemIsEditable); setItem (row, 1, lineItem); QTableWidgetItem *columnItem = new QTableWidgetItem; columnItem->setData (Qt::DisplayRole, column); columnItem->setFlags (columnItem->flags() ^ Qt::ItemIsEditable); setItem (row, 3, columnItem); } else { QTableWidgetItem *lineItem = new QTableWidgetItem; lineItem->setData (Qt::DisplayRole, "-"); lineItem->setFlags (lineItem->flags() ^ Qt::ItemIsEditable); setItem (row, 1, lineItem); } QTableWidgetItem *messageItem = new QTableWidgetItem (QString::fromUtf8 (message.c_str())); messageItem->setFlags (messageItem->flags() ^ Qt::ItemIsEditable); setItem (row, 2, messageItem); } void CSVWorld::ScriptErrorTable::setWarningsMode (const std::string& value) { if (value=="Ignore") Compiler::ErrorHandler::setWarningsMode (0); else if (value=="Normal") Compiler::ErrorHandler::setWarningsMode (1); else if (value=="Strict") Compiler::ErrorHandler::setWarningsMode (2); } CSVWorld::ScriptErrorTable::ScriptErrorTable (const CSMDoc::Document& document, QWidget *parent) : QTableWidget (parent), mContext (document.getData()) { setColumnCount (4); QStringList headers; headers << "Severity" << "Line" << "Description"; setHorizontalHeaderLabels (headers); horizontalHeader()->setSectionResizeMode (0, QHeaderView::ResizeToContents); horizontalHeader()->setSectionResizeMode (1, QHeaderView::ResizeToContents); horizontalHeader()->setStretchLastSection (true); verticalHeader()->hide(); setColumnHidden (3, true); setSelectionMode (QAbstractItemView::NoSelection); Compiler::registerExtensions (mExtensions); mContext.setExtensions (&mExtensions); connect (&CSMPrefs::State::get(), SIGNAL (settingChanged (const CSMPrefs::Setting *)), this, SLOT (settingChanged (const CSMPrefs::Setting *))); CSMPrefs::get()["Scripts"].update(); connect (this, SIGNAL (cellClicked (int, int)), this, SLOT (cellClicked (int, int))); } void CSVWorld::ScriptErrorTable::update (const std::string& source) { clear(); try { std::istringstream input (source); Compiler::Scanner scanner (*this, input, mContext.getExtensions()); Compiler::FileParser parser (*this, mContext); scanner.scan (parser); } catch (const Compiler::SourceException&) { // error has already been reported via error handler } catch (const std::exception& error) { addMessage (error.what(), CSMDoc::Message::Severity_SeriousError); } } void CSVWorld::ScriptErrorTable::clear() { setRowCount (0); } bool CSVWorld::ScriptErrorTable::clearLocals (const std::string& script) { return mContext.clearLocals (script); } void CSVWorld::ScriptErrorTable::settingChanged (const CSMPrefs::Setting *setting) { if (*setting=="Scripts/warnings") setWarningsMode (setting->toString()); } void CSVWorld::ScriptErrorTable::cellClicked (int row, int column) { if (item (row, 3)) { int scriptLine = item (row, 1)->data (Qt::DisplayRole).toInt(); int scriptColumn = item (row, 3)->data (Qt::DisplayRole).toInt(); emit highlightError (scriptLine-1, scriptColumn); } } ================================================ FILE: apps/opencs/view/world/scripterrortable.hpp ================================================ #ifndef CSV_WORLD_SCRIPTERRORTABLE_H #define CSV_WORLD_SCRIPTERRORTABLE_H #include #include #include #include "../../model/world/scriptcontext.hpp" #include "../../model/doc/messages.hpp" namespace CSMDoc { class Document; } namespace CSMPrefs { class Setting; } namespace CSVWorld { class ScriptErrorTable : public QTableWidget, private Compiler::ErrorHandler { Q_OBJECT Compiler::Extensions mExtensions; CSMWorld::ScriptContext mContext; void report (const std::string& message, const Compiler::TokenLoc& loc, Type type) override; ///< Report error to the user. void report (const std::string& message, Type type) override; ///< Report a file related error void addMessage (const std::string& message, CSMDoc::Message::Severity severity, int line = -1, int column = -1); void setWarningsMode (const std::string& value); public: ScriptErrorTable (const CSMDoc::Document& document, QWidget *parent = nullptr); void update (const std::string& source); void clear(); /// Clear local variable cache for \a script. /// /// \return Were there any locals that needed clearing? bool clearLocals (const std::string& script); private slots: void settingChanged (const CSMPrefs::Setting *setting); void cellClicked (int row, int column); signals: void highlightError (int line, int column); }; } #endif ================================================ FILE: apps/opencs/view/world/scripthighlighter.cpp ================================================ #include "scripthighlighter.hpp" #include #include #include #include "../../model/prefs/setting.hpp" #include "../../model/prefs/category.hpp" bool CSVWorld::ScriptHighlighter::parseInt (int value, const Compiler::TokenLoc& loc, Compiler::Scanner& scanner) { highlight (loc, Type_Int); return true; } bool CSVWorld::ScriptHighlighter::parseFloat (float value, const Compiler::TokenLoc& loc, Compiler::Scanner& scanner) { highlight (loc, Type_Float); return true; } bool CSVWorld::ScriptHighlighter::parseName (const std::string& name, const Compiler::TokenLoc& loc, Compiler::Scanner& scanner) { highlight (loc, mContext.isId (name) ? Type_Id : Type_Name); return true; } bool CSVWorld::ScriptHighlighter::parseKeyword (int keyword, const Compiler::TokenLoc& loc, Compiler::Scanner& scanner) { if (((mMode==Mode_Console || mMode==Mode_Dialogue) && (keyword==Compiler::Scanner::K_begin || keyword==Compiler::Scanner::K_end || keyword==Compiler::Scanner::K_short || keyword==Compiler::Scanner::K_long || keyword==Compiler::Scanner::K_float)) || (mMode==Mode_Console && (keyword==Compiler::Scanner::K_if || keyword==Compiler::Scanner::K_endif || keyword==Compiler::Scanner::K_else || keyword==Compiler::Scanner::K_elseif || keyword==Compiler::Scanner::K_while || keyword==Compiler::Scanner::K_endwhile))) return parseName (loc.mLiteral, loc, scanner); highlight (loc, Type_Keyword); return true; } bool CSVWorld::ScriptHighlighter::parseSpecial (int code, const Compiler::TokenLoc& loc, Compiler::Scanner& scanner) { highlight (loc, Type_Special); return true; } bool CSVWorld::ScriptHighlighter::parseComment (const std::string& comment, const Compiler::TokenLoc& loc, Compiler::Scanner& scanner) { highlight (loc, Type_Comment); return true; } void CSVWorld::ScriptHighlighter::parseEOF (Compiler::Scanner& scanner) {} void CSVWorld::ScriptHighlighter::highlight (const Compiler::TokenLoc& loc, Type type) { // We should take in account multibyte characters int length = 0; const char* token = loc.mLiteral.c_str(); while (*token) length += (*token++ & 0xc0) != 0x80; int index = loc.mColumn; // compensate for bug in Compiler::Scanner (position of token is the character after the token) index -= length; QTextCharFormat scheme = mScheme[type]; if (mMarkOccurrences && type == Type_Name && loc.mLiteral == mMarkedWord) scheme.merge(mScheme[Type_Highlight]); setFormat (index, length, scheme); } CSVWorld::ScriptHighlighter::ScriptHighlighter (const CSMWorld::Data& data, Mode mode, QTextDocument *parent) : QSyntaxHighlighter (parent) , Compiler::Parser (mErrorHandler, mContext) , mContext (data) , mMode (mode) , mMarkOccurrences (false) { QColor color ("black"); QTextCharFormat format; format.setForeground (color); for (int i=0; i<=Type_Id; ++i) mScheme.insert (std::make_pair (static_cast (i), format)); // configure compiler Compiler::registerExtensions (mExtensions); mContext.setExtensions (&mExtensions); } void CSVWorld::ScriptHighlighter::highlightBlock (const QString& text) { std::istringstream stream (text.toUtf8().constData()); Compiler::Scanner scanner (mErrorHandler, stream, mContext.getExtensions()); try { scanner.scan (*this); } catch (...) {} // ignore syntax errors } void CSVWorld::ScriptHighlighter::setMarkOccurrences(bool flag) { mMarkOccurrences = flag; } void CSVWorld::ScriptHighlighter::setMarkedWord(const std::string& name) { mMarkedWord = name; } void CSVWorld::ScriptHighlighter::invalidateIds() { mContext.invalidateIds(); } bool CSVWorld::ScriptHighlighter::settingChanged (const CSMPrefs::Setting *setting) { if (setting->getParent()->getKey()=="Scripts") { static const char *const colours[Type_Id+2] = { "colour-int", "colour-float", "colour-name", "colour-keyword", "colour-special", "colour-comment", "colour-highlight", "colour-id", 0 }; for (int i=0; colours[i]; ++i) if (setting->getKey()==colours[i]) { QTextCharFormat format; if (i == Type_Highlight) format.setBackground (setting->toColor()); else format.setForeground (setting->toColor()); mScheme[static_cast (i)] = format; return true; } } return false; } ================================================ FILE: apps/opencs/view/world/scripthighlighter.hpp ================================================ #ifndef CSV_WORLD_SCRIPTHIGHLIGHTER_H #define CSV_WORLD_SCRIPTHIGHLIGHTER_H #include #include #include #include #include #include #include "../../model/world/scriptcontext.hpp" namespace CSMPrefs { class Setting; } namespace CSVWorld { class ScriptHighlighter : public QSyntaxHighlighter, private Compiler::Parser { public: enum Type { Type_Int = 0, Type_Float = 1, Type_Name = 2, Type_Keyword = 3, Type_Special = 4, Type_Comment = 5, Type_Highlight = 6, Type_Id = 7 }; enum Mode { Mode_General, Mode_Console, Mode_Dialogue }; private: Compiler::NullErrorHandler mErrorHandler; Compiler::Extensions mExtensions; CSMWorld::ScriptContext mContext; std::map mScheme; Mode mMode; bool mMarkOccurrences; std::string mMarkedWord; private: bool parseInt (int value, const Compiler::TokenLoc& loc, Compiler::Scanner& scanner) override; ///< Handle an int token. /// \return fetch another token? bool parseFloat (float value, const Compiler::TokenLoc& loc, Compiler::Scanner& scanner) override; ///< Handle a float token. /// \return fetch another token? bool parseName (const std::string& name, const Compiler::TokenLoc& loc, Compiler::Scanner& scanner) override; ///< Handle a name token. /// \return fetch another token? bool parseKeyword (int keyword, const Compiler::TokenLoc& loc, Compiler::Scanner& scanner) override; ///< Handle a keyword token. /// \return fetch another token? bool parseSpecial (int code, const Compiler::TokenLoc& loc, Compiler::Scanner& scanner) override; ///< Handle a special character token. /// \return fetch another token? bool parseComment (const std::string& comment, const Compiler::TokenLoc& loc, Compiler::Scanner& scanner) override; ///< Handle comment token. /// \return fetch another token? void parseEOF (Compiler::Scanner& scanner) override; ///< Handle EOF token. void highlight (const Compiler::TokenLoc& loc, Type type); public: ScriptHighlighter (const CSMWorld::Data& data, Mode mode, QTextDocument *parent); void highlightBlock (const QString& text) override; void setMarkOccurrences(bool); void setMarkedWord(const std::string& name); void invalidateIds(); bool settingChanged (const CSMPrefs::Setting *setting); }; } #endif ================================================ FILE: apps/opencs/view/world/scriptsubview.cpp ================================================ #include "scriptsubview.hpp" #include #include #include #include #include #include #include "../../model/doc/document.hpp" #include "../../model/world/universalid.hpp" #include "../../model/world/data.hpp" #include "../../model/world/commands.hpp" #include "../../model/world/idtable.hpp" #include "../../model/prefs/state.hpp" #include "scriptedit.hpp" #include "recordbuttonbar.hpp" #include "tablebottombox.hpp" #include "genericcreator.hpp" #include "scripterrortable.hpp" void CSVWorld::ScriptSubView::addButtonBar() { if (mButtons) return; mButtons = new RecordButtonBar (getUniversalId(), *mModel, mBottom, &mCommandDispatcher, this); mLayout.insertWidget (1, mButtons); connect (mButtons, SIGNAL (switchToRow (int)), this, SLOT (switchToRow (int))); connect (this, SIGNAL (universalIdChanged (const CSMWorld::UniversalId&)), mButtons, SLOT (universalIdChanged (const CSMWorld::UniversalId&))); } void CSVWorld::ScriptSubView::recompile() { if (!mCompileDelay->isActive() && !isDeleted()) mCompileDelay->start (CSMPrefs::get()["Scripts"]["compile-delay"].toInt()); } bool CSVWorld::ScriptSubView::isDeleted() const { return mModel->data (mModel->getModelIndex (getUniversalId().getId(), mStateColumn)).toInt() ==CSMWorld::RecordBase::State_Deleted; } void CSVWorld::ScriptSubView::updateDeletedState() { if (isDeleted()) { mErrors->clear(); adjustSplitter(); mEditor->setEnabled (false); } else { mEditor->setEnabled (true); recompile(); } } void CSVWorld::ScriptSubView::adjustSplitter() { QList sizes; if (mErrors->rowCount()) { if (mErrors->height()) return; // keep old height if the error panel was already open sizes << (mMain->height()-mErrorHeight-mMain->handleWidth()) << mErrorHeight; } else { if (mErrors->height()) mErrorHeight = mErrors->height(); sizes << 1 << 0; } mMain->setSizes (sizes); } CSVWorld::ScriptSubView::ScriptSubView (const CSMWorld::UniversalId& id, CSMDoc::Document& document) : SubView (id), mDocument (document), mColumn (-1), mBottom(nullptr), mButtons (nullptr), mCommandDispatcher (document, CSMWorld::UniversalId::getParentType (id.getType())), mErrorHeight (CSMPrefs::get()["Scripts"]["error-height"].toInt()) { std::vector selection (1, id.getId()); mCommandDispatcher.setSelection (selection); mMain = new QSplitter (this); mMain->setOrientation (Qt::Vertical); mLayout.addWidget (mMain, 2); mEditor = new ScriptEdit (mDocument, ScriptHighlighter::Mode_General, this); mMain->addWidget (mEditor); mMain->setCollapsible (0, false); mErrors = new ScriptErrorTable (document, this); mMain->addWidget (mErrors); QList sizes; sizes << 1 << 0; mMain->setSizes (sizes); QWidget *widget = new QWidget (this);; widget->setLayout (&mLayout); setWidget (widget); mModel = &dynamic_cast ( *document.getData().getTableModel (CSMWorld::UniversalId::Type_Scripts)); mColumn = mModel->findColumnIndex (CSMWorld::Columns::ColumnId_ScriptText); mIdColumn = mModel->findColumnIndex (CSMWorld::Columns::ColumnId_Id); mStateColumn = mModel->findColumnIndex (CSMWorld::Columns::ColumnId_Modification); QString source = mModel->data (mModel->getModelIndex (id.getId(), mColumn)).toString(); mEditor->setPlainText (source); // bottom box and buttons mBottom = new TableBottomBox (CreatorFactory(), document, id, this); connect (mBottom, SIGNAL (requestFocus (const std::string&)), this, SLOT (switchToId (const std::string&))); mLayout.addWidget (mBottom); // signals connect (mEditor, SIGNAL (textChanged()), this, SLOT (textChanged())); connect (mModel, SIGNAL (dataChanged (const QModelIndex&, const QModelIndex&)), this, SLOT (dataChanged (const QModelIndex&, const QModelIndex&))); connect (mModel, SIGNAL (rowsAboutToBeRemoved (const QModelIndex&, int, int)), this, SLOT (rowsAboutToBeRemoved (const QModelIndex&, int, int))); updateStatusBar(); connect(mEditor, SIGNAL(cursorPositionChanged()), this, SLOT(updateStatusBar())); mErrors->update (source.toUtf8().constData()); connect (mErrors, SIGNAL (highlightError (int, int)), this, SLOT (highlightError (int, int))); mCompileDelay = new QTimer (this); mCompileDelay->setSingleShot (true); connect (mCompileDelay, SIGNAL (timeout()), this, SLOT (updateRequest())); updateDeletedState(); connect (&CSMPrefs::State::get(), SIGNAL (settingChanged (const CSMPrefs::Setting *)), this, SLOT (settingChanged (const CSMPrefs::Setting *))); CSMPrefs::get()["Scripts"].update(); } void CSVWorld::ScriptSubView::setStatusBar (bool show) { mBottom->setStatusBar (show); } void CSVWorld::ScriptSubView::settingChanged (const CSMPrefs::Setting *setting) { if (*setting=="Scripts/toolbar") { if (setting->isTrue()) { addButtonBar(); } else if (mButtons) { mLayout.removeWidget (mButtons); delete mButtons; mButtons = nullptr; } } else if (*setting=="Scripts/compile-delay") { mCompileDelay->setInterval (setting->toInt()); } else if (*setting=="Scripts/warnings") recompile(); } void CSVWorld::ScriptSubView::updateStatusBar () { mBottom->positionChanged (mEditor->textCursor().blockNumber() + 1, mEditor->textCursor().columnNumber() + 1); } void CSVWorld::ScriptSubView::setEditLock (bool locked) { mEditor->setReadOnly (locked); if (mButtons) mButtons->setEditLock (locked); mCommandDispatcher.setEditLock (locked); } void CSVWorld::ScriptSubView::useHint (const std::string& hint) { if (hint.empty()) return; unsigned line = 0, column = 0; char c; std::istringstream stream (hint.c_str()+1); switch(hint[0]) { case 'R': case 'r': { QModelIndex index = mModel->getModelIndex (getUniversalId().getId(), mColumn); QString source = mModel->data (index).toString(); unsigned stringSize = source.length(); unsigned pos, dummy; if (!(stream >> c >> dummy >> pos) ) return; if (pos > stringSize) { Log(Debug::Warning) << "CSVWorld::ScriptSubView: requested position is higher than actual string length"; pos = stringSize; } for (unsigned i = 0; i <= pos; ++i) { if (source[i] == '\n') { ++line; column = i+1; } } column = pos - column; break; } case 'l': if (!(stream >> c >> line >> column)) return; } QTextCursor cursor = mEditor->textCursor(); cursor.movePosition (QTextCursor::Start); if (cursor.movePosition (QTextCursor::Down, QTextCursor::MoveAnchor, line)) cursor.movePosition (QTextCursor::Right, QTextCursor::MoveAnchor, column); mEditor->setFocus(); mEditor->setTextCursor (cursor); } void CSVWorld::ScriptSubView::textChanged() { if (mEditor->isChangeLocked()) return; ScriptEdit::ChangeLock lock (*mEditor); QString source = mEditor->toPlainText(); mDocument.getUndoStack().push (new CSMWorld::ModifyCommand (*mModel, mModel->getModelIndex (getUniversalId().getId(), mColumn), source)); recompile(); } void CSVWorld::ScriptSubView::dataChanged (const QModelIndex& topLeft, const QModelIndex& bottomRight) { if (mEditor->isChangeLocked()) return; ScriptEdit::ChangeLock lock (*mEditor); bool updateRequired = false; for (int i=topLeft.row(); i<=bottomRight.row(); ++i) { std::string id = mModel->data (mModel->index (i, mIdColumn)).toString().toUtf8().constData(); if (mErrors->clearLocals (id)) updateRequired = true; } QModelIndex index = mModel->getModelIndex (getUniversalId().getId(), mColumn); if (index.row()>=topLeft.row() && index.row()<=bottomRight.row()) { if (mStateColumn>=topLeft.column() && mStateColumn<=bottomRight.column()) updateDeletedState(); if (mColumn>=topLeft.column() && mColumn<=bottomRight.column()) { QString source = mModel->data (index).toString(); QTextCursor cursor = mEditor->textCursor(); mEditor->setPlainText (source); mEditor->setTextCursor (cursor); updateRequired = true; } } if (updateRequired) recompile(); } void CSVWorld::ScriptSubView::rowsAboutToBeRemoved (const QModelIndex& parent, int start, int end) { bool updateRequired = false; for (int i=start; i<=end; ++i) { std::string id = mModel->data (mModel->index (i, mIdColumn)).toString().toUtf8().constData(); if (mErrors->clearLocals (id)) updateRequired = true; } if (updateRequired) recompile(); QModelIndex index = mModel->getModelIndex (getUniversalId().getId(), mColumn); if (!parent.isValid() && index.row()>=start && index.row()<=end) emit closeRequest(); } void CSVWorld::ScriptSubView::switchToRow (int row) { int idColumn = mModel->findColumnIndex (CSMWorld::Columns::ColumnId_Id); std::string id = mModel->data (mModel->index (row, idColumn)).toString().toUtf8().constData(); setUniversalId (CSMWorld::UniversalId (CSMWorld::UniversalId::Type_Script, id)); bool oldSignalsState = mEditor->blockSignals( true ); mEditor->setPlainText( mModel->data(mModel->index(row, mColumn)).toString() ); mEditor->blockSignals( oldSignalsState ); std::vector selection (1, id); mCommandDispatcher.setSelection (selection); updateDeletedState(); } void CSVWorld::ScriptSubView::switchToId (const std::string& id) { switchToRow (mModel->getModelIndex (id, 0).row()); } void CSVWorld::ScriptSubView::highlightError (int line, int column) { QTextCursor cursor = mEditor->textCursor(); cursor.movePosition (QTextCursor::Start); if (cursor.movePosition (QTextCursor::Down, QTextCursor::MoveAnchor, line)) cursor.movePosition (QTextCursor::Right, QTextCursor::MoveAnchor, column); mEditor->setFocus(); mEditor->setTextCursor (cursor); } void CSVWorld::ScriptSubView::updateRequest() { QModelIndex index = mModel->getModelIndex (getUniversalId().getId(), mColumn); QString source = mModel->data (index).toString(); mErrors->update (source.toUtf8().constData()); adjustSplitter(); } ================================================ FILE: apps/opencs/view/world/scriptsubview.hpp ================================================ #ifndef CSV_WORLD_SCRIPTSUBVIEW_H #define CSV_WORLD_SCRIPTSUBVIEW_H #include #include "../../model/world/commanddispatcher.hpp" #include "../doc/subview.hpp" class QModelIndex; class QLabel; class QVBoxLayout; class QSplitter; class QTime; namespace CSMDoc { class Document; } namespace CSMWorld { class IdTable; } namespace CSMPrefs { class Setting; } namespace CSVWorld { class ScriptEdit; class RecordButtonBar; class TableBottomBox; class ScriptErrorTable; class ScriptSubView : public CSVDoc::SubView { Q_OBJECT ScriptEdit *mEditor; CSMDoc::Document& mDocument; CSMWorld::IdTable *mModel; int mColumn; int mIdColumn; int mStateColumn; TableBottomBox *mBottom; RecordButtonBar *mButtons; CSMWorld::CommandDispatcher mCommandDispatcher; QVBoxLayout mLayout; QSplitter *mMain; ScriptErrorTable *mErrors; QTimer *mCompileDelay; int mErrorHeight; private: void addButtonBar(); void recompile(); bool isDeleted() const; void updateDeletedState(); void adjustSplitter(); public: ScriptSubView (const CSMWorld::UniversalId& id, CSMDoc::Document& document); void setEditLock (bool locked) override; void useHint (const std::string& hint) override; void setStatusBar (bool show) override; public slots: void textChanged(); void dataChanged (const QModelIndex& topLeft, const QModelIndex& bottomRight); void rowsAboutToBeRemoved (const QModelIndex& parent, int start, int end); private slots: void settingChanged (const CSMPrefs::Setting *setting); void updateStatusBar(); void switchToRow (int row); void switchToId (const std::string& id); void highlightError (int line, int column); void updateRequest(); }; } #endif ================================================ FILE: apps/opencs/view/world/startscriptcreator.cpp ================================================ #include "startscriptcreator.hpp" #include #include "../../model/doc/document.hpp" #include "../../model/world/columns.hpp" #include "../../model/world/commands.hpp" #include "../../model/world/data.hpp" #include "../../model/world/idcompletionmanager.hpp" #include "../../model/world/idtable.hpp" #include "../widget/droplineedit.hpp" std::string CSVWorld::StartScriptCreator::getId() const { return mScript->text().toUtf8().constData(); } CSMWorld::IdTable& CSVWorld::StartScriptCreator::getStartScriptsTable() const { return dynamic_cast ( *getData().getTableModel(getCollectionId()) ); } CSVWorld::StartScriptCreator::StartScriptCreator( CSMWorld::Data &data, QUndoStack &undoStack, const CSMWorld::UniversalId &id, CSMWorld::IdCompletionManager& completionManager ) : GenericCreator(data, undoStack, id) { setManualEditing(false); // Add script ID input label. QLabel *label = new QLabel("Script", this); insertBeforeButtons(label, false); // Add script ID input with auto-completion. // Only existing script IDs are accepted so no ID validation is performed. CSMWorld::ColumnBase::Display displayType = CSMWorld::ColumnBase::Display_Script; mScript = new CSVWidget::DropLineEdit(displayType, this); mScript->setCompleter(completionManager.getCompleter(displayType).get()); insertBeforeButtons(mScript, true); connect(mScript, SIGNAL (textChanged(const QString&)), this, SLOT (scriptChanged())); connect(mScript, SIGNAL (returnPressed()), this, SLOT (inputReturnPressed())); } void CSVWorld::StartScriptCreator::cloneMode( const std::string& originId, const CSMWorld::UniversalId::Type type) { CSVWorld::GenericCreator::cloneMode(originId, type); // Look up cloned record in start scripts table and set script ID text. CSMWorld::IdTable& table = getStartScriptsTable(); int column = table.findColumnIndex(CSMWorld::Columns::ColumnId_Id); mScript->setText(table.data(table.getModelIndex(originId, column)).toString()); } std::string CSVWorld::StartScriptCreator::getErrors() const { std::string scriptId = getId(); // Check user input for any errors. std::string errors; if (scriptId.empty()) { errors = "No Script ID entered"; } else if (getData().getScripts().searchId(scriptId) == -1) { errors = "Script ID not found"; } else if (getData().getStartScripts().searchId(scriptId) > -1) { errors = "Script with this ID already registered as Start Script"; } return errors; } void CSVWorld::StartScriptCreator::focus() { mScript->setFocus(); } void CSVWorld::StartScriptCreator::reset() { CSVWorld::GenericCreator::reset(); mScript->setText(""); } void CSVWorld::StartScriptCreator::scriptChanged() { update(); } CSVWorld::Creator *CSVWorld::StartScriptCreatorFactory::makeCreator( CSMDoc::Document& document, const CSMWorld::UniversalId& id) const { return new StartScriptCreator( document.getData(), document.getUndoStack(), id, document.getIdCompletionManager() ); } ================================================ FILE: apps/opencs/view/world/startscriptcreator.hpp ================================================ #ifndef STARTSCRIPTCREATOR_HPP #define STARTSCRIPTCREATOR_HPP #include "genericcreator.hpp" namespace CSMWorld { class IdCompletionManager; class IdTable; } namespace CSVWidget { class DropLineEdit; } namespace CSVWorld { /// \brief Record creator for start scripts. class StartScriptCreator : public GenericCreator { Q_OBJECT CSVWidget::DropLineEdit *mScript; private: /// \return script ID entered by user. std::string getId() const override; /// \return reference to table containing start scripts. CSMWorld::IdTable& getStartScriptsTable() const; public: StartScriptCreator( CSMWorld::Data& data, QUndoStack& undoStack, const CSMWorld::UniversalId& id, CSMWorld::IdCompletionManager& completionManager); /// \brief Set script ID input widget to ID of record to be cloned. /// \param originId Script ID to be cloned. /// \param type Type of record to be cloned. void cloneMode( const std::string& originId, const CSMWorld::UniversalId::Type type) override; /// \return Error description for current user input. std::string getErrors() const override; /// \brief Set focus to script ID input widget. void focus() override; /// \brief Clear script ID input widget. void reset() override; private slots: /// \brief Check user input for any errors. void scriptChanged(); }; /// \brief Creator factory for start script record creator. class StartScriptCreatorFactory : public CreatorFactoryBase { public: Creator *makeCreator( CSMDoc::Document& document, const CSMWorld::UniversalId& id) const override; }; } #endif // STARTSCRIPTCREATOR_HPP ================================================ FILE: apps/opencs/view/world/subviews.cpp ================================================ #include "subviews.hpp" #include "../doc/subviewfactoryimp.hpp" #include "tablesubview.hpp" #include "dialoguesubview.hpp" #include "scriptsubview.hpp" #include "regionmapsubview.hpp" #include "genericcreator.hpp" #include "globalcreator.hpp" #include "cellcreator.hpp" #include "referenceablecreator.hpp" #include "referencecreator.hpp" #include "startscriptcreator.hpp" #include "scenesubview.hpp" #include "dialoguecreator.hpp" #include "infocreator.hpp" #include "pathgridcreator.hpp" #include "previewsubview.hpp" #include "bodypartcreator.hpp" #include "landcreator.hpp" #include "landtexturecreator.hpp" void CSVWorld::addSubViewFactories (CSVDoc::SubViewFactoryManager& manager) { // Regular record tables (including references which are actually sub-records, but are promoted // to top-level records within the editor) manager.add (CSMWorld::UniversalId::Type_Gmsts, new CSVDoc::SubViewFactoryWithCreator); manager.add (CSMWorld::UniversalId::Type_Skills, new CSVDoc::SubViewFactoryWithCreator); manager.add (CSMWorld::UniversalId::Type_MagicEffects, new CSVDoc::SubViewFactoryWithCreator); static const CSMWorld::UniversalId::Type sTableTypes[] = { CSMWorld::UniversalId::Type_Classes, CSMWorld::UniversalId::Type_Factions, CSMWorld::UniversalId::Type_Races, CSMWorld::UniversalId::Type_Sounds, CSMWorld::UniversalId::Type_Regions, CSMWorld::UniversalId::Type_Birthsigns, CSMWorld::UniversalId::Type_Spells, CSMWorld::UniversalId::Type_Enchantments, CSMWorld::UniversalId::Type_SoundGens, CSMWorld::UniversalId::Type_None // end marker }; for (int i=0; sTableTypes[i]!=CSMWorld::UniversalId::Type_None; ++i) manager.add (sTableTypes[i], new CSVDoc::SubViewFactoryWithCreator >); manager.add (CSMWorld::UniversalId::Type_BodyParts, new CSVDoc::SubViewFactoryWithCreator >); manager.add (CSMWorld::UniversalId::Type_StartScripts, new CSVDoc::SubViewFactoryWithCreator); manager.add (CSMWorld::UniversalId::Type_Cells, new CSVDoc::SubViewFactoryWithCreator >); manager.add (CSMWorld::UniversalId::Type_Referenceables, new CSVDoc::SubViewFactoryWithCreator >); manager.add (CSMWorld::UniversalId::Type_References, new CSVDoc::SubViewFactoryWithCreator); manager.add (CSMWorld::UniversalId::Type_Topics, new CSVDoc::SubViewFactoryWithCreator); manager.add (CSMWorld::UniversalId::Type_Journals, new CSVDoc::SubViewFactoryWithCreator); manager.add (CSMWorld::UniversalId::Type_TopicInfos, new CSVDoc::SubViewFactoryWithCreator(false)); manager.add (CSMWorld::UniversalId::Type_JournalInfos, new CSVDoc::SubViewFactoryWithCreator(false)); manager.add (CSMWorld::UniversalId::Type_Pathgrids, new CSVDoc::SubViewFactoryWithCreator); manager.add (CSMWorld::UniversalId::Type_Lands, new CSVDoc::SubViewFactoryWithCreator >); manager.add (CSMWorld::UniversalId::Type_LandTextures, new CSVDoc::SubViewFactoryWithCreator >); manager.add (CSMWorld::UniversalId::Type_Globals, new CSVDoc::SubViewFactoryWithCreator >); // Subviews for resources tables manager.add (CSMWorld::UniversalId::Type_Meshes, new CSVDoc::SubViewFactoryWithCreator); manager.add (CSMWorld::UniversalId::Type_Icons, new CSVDoc::SubViewFactoryWithCreator); manager.add (CSMWorld::UniversalId::Type_Musics, new CSVDoc::SubViewFactoryWithCreator); manager.add (CSMWorld::UniversalId::Type_SoundsRes, new CSVDoc::SubViewFactoryWithCreator); manager.add (CSMWorld::UniversalId::Type_Textures, new CSVDoc::SubViewFactoryWithCreator); manager.add (CSMWorld::UniversalId::Type_Videos, new CSVDoc::SubViewFactoryWithCreator); // Subviews for editing/viewing individual records manager.add (CSMWorld::UniversalId::Type_Script, new CSVDoc::SubViewFactory); // Other stuff (combined record tables) manager.add (CSMWorld::UniversalId::Type_RegionMap, new CSVDoc::SubViewFactory); manager.add (CSMWorld::UniversalId::Type_Scene, new CSVDoc::SubViewFactory); // More other stuff manager.add (CSMWorld::UniversalId::Type_Filters, new CSVDoc::SubViewFactoryWithCreator >); manager.add (CSMWorld::UniversalId::Type_DebugProfiles, new CSVDoc::SubViewFactoryWithCreator >); manager.add (CSMWorld::UniversalId::Type_Scripts, new CSVDoc::SubViewFactoryWithCreator >); // Dialogue subviews static const CSMWorld::UniversalId::Type sTableTypes2[] = { CSMWorld::UniversalId::Type_Region, CSMWorld::UniversalId::Type_Spell, CSMWorld::UniversalId::Type_Birthsign, CSMWorld::UniversalId::Type_Global, CSMWorld::UniversalId::Type_Race, CSMWorld::UniversalId::Type_Class, CSMWorld::UniversalId::Type_Sound, CSMWorld::UniversalId::Type_Faction, CSMWorld::UniversalId::Type_Enchantment, CSMWorld::UniversalId::Type_SoundGen, CSMWorld::UniversalId::Type_None // end marker }; for (int i=0; sTableTypes2[i]!=CSMWorld::UniversalId::Type_None; ++i) manager.add (sTableTypes2[i], new CSVDoc::SubViewFactoryWithCreator > (false)); manager.add (CSMWorld::UniversalId::Type_BodyPart, new CSVDoc::SubViewFactoryWithCreator > (false)); manager.add (CSMWorld::UniversalId::Type_StartScript, new CSVDoc::SubViewFactoryWithCreator(false)); manager.add (CSMWorld::UniversalId::Type_Skill, new CSVDoc::SubViewFactoryWithCreator (false)); manager.add (CSMWorld::UniversalId::Type_MagicEffect, new CSVDoc::SubViewFactoryWithCreator (false)); manager.add (CSMWorld::UniversalId::Type_Gmst, new CSVDoc::SubViewFactoryWithCreator (false)); manager.add (CSMWorld::UniversalId::Type_Referenceable, new CSVDoc::SubViewFactoryWithCreator > (false)); manager.add (CSMWorld::UniversalId::Type_Reference, new CSVDoc::SubViewFactoryWithCreator (false)); manager.add (CSMWorld::UniversalId::Type_Cell, new CSVDoc::SubViewFactoryWithCreator > (false)); manager.add (CSMWorld::UniversalId::Type_JournalInfo, new CSVDoc::SubViewFactoryWithCreator (false)); manager.add (CSMWorld::UniversalId::Type_TopicInfo, new CSVDoc::SubViewFactoryWithCreator(false)); manager.add (CSMWorld::UniversalId::Type_Topic, new CSVDoc::SubViewFactoryWithCreator (false)); manager.add (CSMWorld::UniversalId::Type_Journal, new CSVDoc::SubViewFactoryWithCreator (false)); manager.add (CSMWorld::UniversalId::Type_Pathgrid, new CSVDoc::SubViewFactoryWithCreator (false)); manager.add (CSMWorld::UniversalId::Type_Land, new CSVDoc::SubViewFactoryWithCreator >(false)); manager.add (CSMWorld::UniversalId::Type_LandTexture, new CSVDoc::SubViewFactoryWithCreator >(false)); manager.add (CSMWorld::UniversalId::Type_DebugProfile, new CSVDoc::SubViewFactoryWithCreator > (false)); manager.add (CSMWorld::UniversalId::Type_Filter, new CSVDoc::SubViewFactoryWithCreator > (false)); manager.add (CSMWorld::UniversalId::Type_MetaData, new CSVDoc::SubViewFactory); //preview manager.add (CSMWorld::UniversalId::Type_Preview, new CSVDoc::SubViewFactory); } ================================================ FILE: apps/opencs/view/world/subviews.hpp ================================================ #ifndef CSV_WORLD_SUBVIEWS_H #define CSV_WORLD_SUBVIEWS_H namespace CSVDoc { class SubViewFactoryManager; } namespace CSVWorld { void addSubViewFactories (CSVDoc::SubViewFactoryManager& manager); } #endif ================================================ FILE: apps/opencs/view/world/table.cpp ================================================ #include "table.hpp" #include #include #include #include #include #include #include #include #include #include "../../model/doc/document.hpp" #include "../../model/world/commands.hpp" #include "../../model/world/infotableproxymodel.hpp" #include "../../model/world/idtableproxymodel.hpp" #include "../../model/world/idtablebase.hpp" #include "../../model/world/idtable.hpp" #include "../../model/world/landtexturetableproxymodel.hpp" #include "../../model/world/record.hpp" #include "../../model/world/columns.hpp" #include "../../model/world/commanddispatcher.hpp" #include "../../model/prefs/state.hpp" #include "../../model/prefs/shortcut.hpp" #include "tableeditidaction.hpp" #include "util.hpp" void CSVWorld::Table::contextMenuEvent (QContextMenuEvent *event) { // configure dispatcher mDispatcher->setSelection (getSelectedIds()); std::vector extendedTypes = mDispatcher->getExtendedTypes(); mDispatcher->setExtendedTypes (extendedTypes); // create context menu QModelIndexList selectedRows = selectionModel()->selectedRows(); QMenu menu (this); /// \todo add menu items for select all and clear selection int currentRow = rowAt(event->y()); int currentColumn = columnAt(event->x()); if (mEditIdAction->isValidIdCell(currentRow, currentColumn)) { mEditIdAction->setCell(currentRow, currentColumn); menu.addAction(mEditIdAction); menu.addSeparator(); } if (!mEditLock && !(mModel->getFeatures() & CSMWorld::IdTableBase::Feature_Constant)) { if (selectedRows.size()==1) { menu.addAction (mEditAction); if (mCreateAction) menu.addAction(mCloneAction); } if (mTouchAction) menu.addAction (mTouchAction); if (mCreateAction) menu.addAction (mCreateAction); if (mDispatcher->canRevert()) { menu.addAction (mRevertAction); if (!extendedTypes.empty()) menu.addAction (mExtendedRevertAction); } if (mDispatcher->canDelete()) { menu.addAction (mDeleteAction); if (!extendedTypes.empty()) menu.addAction (mExtendedDeleteAction); } if (mModel->getFeatures() & CSMWorld::IdTableBase::Feature_ReorderWithinTopic) { /// \todo allow reordering of multiple rows if (selectedRows.size()==1) { int column = mModel->searchColumnIndex (CSMWorld::Columns::ColumnId_Topic); if (column==-1) column = mModel->searchColumnIndex (CSMWorld::Columns::ColumnId_Journal); if (column!=-1) { int row = mProxyModel->mapToSource ( mProxyModel->index (selectedRows.begin()->row(), 0)).row(); QString curData = mModel->data(mModel->index(row, column)).toString(); if (row > 0) { QString prevData = mModel->data(mModel->index(row - 1, column)).toString(); if (Misc::StringUtils::ciEqual(curData.toStdString(), prevData.toStdString())) { menu.addAction(mMoveUpAction); } } if (row < mModel->rowCount() - 1) { QString nextData = mModel->data(mModel->index(row + 1, column)).toString(); if (Misc::StringUtils::ciEqual(curData.toStdString(), nextData.toStdString())) { menu.addAction(mMoveDownAction); } } } } } } if (selectedRows.size()==1) { int row = selectedRows.begin()->row(); row = mProxyModel->mapToSource (mProxyModel->index (row, 0)).row(); if (mModel->getFeatures() & CSMWorld::IdTableBase::Feature_View) { CSMWorld::UniversalId id = mModel->view (row).first; int index = mDocument.getData().getCells().searchId (id.getId()); // index==-1: the ID references a worldspace instead of a cell (ignore for now and go // ahead) if (index==-1 || !mDocument.getData().getCells().getRecord (index).isDeleted()) menu.addAction (mViewAction); } if (mModel->getFeatures() & CSMWorld::IdTableBase::Feature_Preview) { const CSMWorld::UniversalId id = getUniversalId(currentRow); const CSMWorld::UniversalId::Type type = id.getType(); QModelIndex index = mModel->index (row, mModel->findColumnIndex (CSMWorld::Columns::ColumnId_Modification)); CSMWorld::RecordBase::State state = static_cast ( mModel->data (index).toInt()); if (state!=CSMWorld::RecordBase::State_Deleted && type != CSMWorld::UniversalId::Type_ItemLevelledList) menu.addAction (mPreviewAction); } } if (mHelpAction) menu.addAction (mHelpAction); menu.exec (event->globalPos()); } void CSVWorld::Table::mouseDoubleClickEvent (QMouseEvent *event) { Qt::KeyboardModifiers modifiers = event->modifiers() & (Qt::ShiftModifier | Qt::ControlModifier); QModelIndex index = currentIndex(); selectionModel()->select (index, QItemSelectionModel::Clear | QItemSelectionModel::Select | QItemSelectionModel::Rows); std::map::iterator iter = mDoubleClickActions.find (modifiers); if (iter==mDoubleClickActions.end()) { event->accept(); return; } switch (iter->second) { case Action_None: event->accept(); break; case Action_InPlaceEdit: DragRecordTable::mouseDoubleClickEvent (event); break; case Action_EditRecord: event->accept(); editRecord(); break; case Action_View: event->accept(); viewRecord(); break; case Action_Revert: event->accept(); if (mDispatcher->canRevert()) mDispatcher->executeRevert(); break; case Action_Delete: event->accept(); if (mDispatcher->canDelete()) mDispatcher->executeDelete(); break; case Action_EditRecordAndClose: event->accept(); editRecord(); emit closeRequest(); break; case Action_ViewAndClose: event->accept(); viewRecord(); emit closeRequest(); break; } } CSVWorld::Table::Table (const CSMWorld::UniversalId& id, bool createAndDelete, bool sorting, CSMDoc::Document& document) : DragRecordTable(document), mCreateAction (nullptr), mCloneAction(nullptr), mTouchAction(nullptr), mRecordStatusDisplay (0), mJumpToAddedRecord(false), mUnselectAfterJump(false) { mModel = &dynamic_cast (*mDocument.getData().getTableModel (id)); bool isInfoTable = id.getType() == CSMWorld::UniversalId::Type_TopicInfos || id.getType() == CSMWorld::UniversalId::Type_JournalInfos; bool isLtexTable = (id.getType() == CSMWorld::UniversalId::Type_LandTextures); if (isInfoTable) { mProxyModel = new CSMWorld::InfoTableProxyModel(id.getType(), this); connect (this, &CSVWorld::DragRecordTable::moveRecordsFromSameTable, this, &CSVWorld::Table::moveRecords); } else if (isLtexTable) { mProxyModel = new CSMWorld::LandTextureTableProxyModel (this); } else { mProxyModel = new CSMWorld::IdTableProxyModel (this); } mProxyModel->setSourceModel (mModel); mDispatcher = new CSMWorld::CommandDispatcher (document, id, this); setModel (mProxyModel); horizontalHeader()->setSectionResizeMode (QHeaderView::Interactive); verticalHeader()->hide(); setSelectionBehavior (QAbstractItemView::SelectRows); setSelectionMode (QAbstractItemView::ExtendedSelection); setSortingEnabled (sorting); if (sorting) { sortByColumn (mModel->findColumnIndex(CSMWorld::Columns::ColumnId_Id), Qt::AscendingOrder); } int columns = mModel->columnCount(); for (int i=0; iheaderData (i, Qt::Horizontal, CSMWorld::ColumnBase::Role_Flags).toInt(); if (flags & CSMWorld::ColumnBase::Flag_Table) { CSMWorld::ColumnBase::Display display = static_cast ( mModel->headerData (i, Qt::Horizontal, CSMWorld::ColumnBase::Role_Display).toInt()); CommandDelegate *delegate = CommandDelegateFactoryCollection::get().makeDelegate (display, mDispatcher, document, this); mDelegates.push_back (delegate); setItemDelegateForColumn (i, delegate); } else hideColumn (i); } mEditAction = new QAction (tr ("Edit Record"), this); connect (mEditAction, SIGNAL (triggered()), this, SLOT (editRecord())); mEditAction->setIcon(QIcon(":edit-edit")); addAction (mEditAction); CSMPrefs::Shortcut* editShortcut = new CSMPrefs::Shortcut("table-edit", this); editShortcut->associateAction(mEditAction); if (createAndDelete) { mCreateAction = new QAction (tr ("Add Record"), this); connect (mCreateAction, SIGNAL (triggered()), this, SIGNAL (createRequest())); mCreateAction->setIcon(QIcon(":edit-add")); addAction (mCreateAction); CSMPrefs::Shortcut* createShortcut = new CSMPrefs::Shortcut("table-add", this); createShortcut->associateAction(mCreateAction); mCloneAction = new QAction (tr ("Clone Record"), this); connect(mCloneAction, SIGNAL (triggered()), this, SLOT (cloneRecord())); mCloneAction->setIcon(QIcon(":edit-clone")); addAction(mCloneAction); CSMPrefs::Shortcut* cloneShortcut = new CSMPrefs::Shortcut("table-clone", this); cloneShortcut->associateAction(mCloneAction); } if (mModel->getFeatures() & CSMWorld::IdTableBase::Feature_AllowTouch) { mTouchAction = new QAction(tr("Touch Record"), this); connect(mTouchAction, SIGNAL(triggered()), this, SLOT(touchRecord())); mTouchAction->setIcon(QIcon(":edit-touch")); addAction(mTouchAction); CSMPrefs::Shortcut* touchShortcut = new CSMPrefs::Shortcut("table-touch", this); touchShortcut->associateAction(mTouchAction); } mRevertAction = new QAction (tr ("Revert Record"), this); connect (mRevertAction, SIGNAL (triggered()), mDispatcher, SLOT (executeRevert())); mRevertAction->setIcon(QIcon(":edit-undo")); addAction (mRevertAction); CSMPrefs::Shortcut* revertShortcut = new CSMPrefs::Shortcut("table-revert", this); revertShortcut->associateAction(mRevertAction); mDeleteAction = new QAction (tr ("Delete Record"), this); connect (mDeleteAction, SIGNAL (triggered()), mDispatcher, SLOT (executeDelete())); mDeleteAction->setIcon(QIcon(":edit-delete")); addAction (mDeleteAction); CSMPrefs::Shortcut* deleteShortcut = new CSMPrefs::Shortcut("table-remove", this); deleteShortcut->associateAction(mDeleteAction); mMoveUpAction = new QAction (tr ("Move Up"), this); connect (mMoveUpAction, SIGNAL (triggered()), this, SLOT (moveUpRecord())); mMoveUpAction->setIcon(QIcon(":record-up")); addAction (mMoveUpAction); CSMPrefs::Shortcut* moveUpShortcut = new CSMPrefs::Shortcut("table-moveup", this); moveUpShortcut->associateAction(mMoveUpAction); mMoveDownAction = new QAction (tr ("Move Down"), this); connect (mMoveDownAction, SIGNAL (triggered()), this, SLOT (moveDownRecord())); mMoveDownAction->setIcon(QIcon(":record-down")); addAction (mMoveDownAction); CSMPrefs::Shortcut* moveDownShortcut = new CSMPrefs::Shortcut("table-movedown", this); moveDownShortcut->associateAction(mMoveDownAction); mViewAction = new QAction (tr ("View"), this); connect (mViewAction, SIGNAL (triggered()), this, SLOT (viewRecord())); mViewAction->setIcon(QIcon(":/cell.png")); addAction (mViewAction); CSMPrefs::Shortcut* viewShortcut = new CSMPrefs::Shortcut("table-view", this); viewShortcut->associateAction(mViewAction); mPreviewAction = new QAction (tr ("Preview"), this); connect (mPreviewAction, SIGNAL (triggered()), this, SLOT (previewRecord())); mPreviewAction->setIcon(QIcon(":edit-preview")); addAction (mPreviewAction); CSMPrefs::Shortcut* previewShortcut = new CSMPrefs::Shortcut("table-preview", this); previewShortcut->associateAction(mPreviewAction); mExtendedDeleteAction = new QAction (tr ("Extended Delete Record"), this); connect (mExtendedDeleteAction, SIGNAL (triggered()), this, SLOT (executeExtendedDelete())); mExtendedDeleteAction->setIcon(QIcon(":edit-delete")); addAction (mExtendedDeleteAction); CSMPrefs::Shortcut* extendedDeleteShortcut = new CSMPrefs::Shortcut("table-extendeddelete", this); extendedDeleteShortcut->associateAction(mExtendedDeleteAction); mExtendedRevertAction = new QAction (tr ("Extended Revert Record"), this); connect (mExtendedRevertAction, SIGNAL (triggered()), this, SLOT (executeExtendedRevert())); mExtendedRevertAction->setIcon(QIcon(":edit-undo")); addAction (mExtendedRevertAction); CSMPrefs::Shortcut* extendedRevertShortcut = new CSMPrefs::Shortcut("table-extendedrevert", this); extendedRevertShortcut->associateAction(mExtendedRevertAction); mEditIdAction = new TableEditIdAction (*this, this); connect (mEditIdAction, SIGNAL (triggered()), this, SLOT (editCell())); addAction (mEditIdAction); mHelpAction = new QAction (tr ("Help"), this); connect (mHelpAction, SIGNAL (triggered()), this, SLOT (openHelp())); mHelpAction->setIcon(QIcon(":/info.png")); addAction (mHelpAction); CSMPrefs::Shortcut* openHelpShortcut = new CSMPrefs::Shortcut("help", this); openHelpShortcut->associateAction(mHelpAction); connect (mProxyModel, SIGNAL (rowsRemoved (const QModelIndex&, int, int)), this, SLOT (tableSizeUpdate())); connect (mProxyModel, SIGNAL (rowAdded (const std::string &)), this, SLOT (rowAdded (const std::string &))); /// \note This signal could instead be connected to a slot that filters out changes not affecting /// the records status column (for permanence reasons) connect (mProxyModel, SIGNAL (dataChanged (const QModelIndex&, const QModelIndex&)), this, SLOT (tableSizeUpdate())); connect (selectionModel(), SIGNAL (selectionChanged (const QItemSelection&, const QItemSelection&)), this, SLOT (selectionSizeUpdate ())); setAcceptDrops(true); mDoubleClickActions.insert (std::make_pair (Qt::NoModifier, Action_InPlaceEdit)); mDoubleClickActions.insert (std::make_pair (Qt::ShiftModifier, Action_EditRecord)); mDoubleClickActions.insert (std::make_pair (Qt::ControlModifier, Action_View)); mDoubleClickActions.insert (std::make_pair (Qt::ShiftModifier | Qt::ControlModifier, Action_EditRecordAndClose)); connect (&CSMPrefs::State::get(), SIGNAL (settingChanged (const CSMPrefs::Setting *)), this, SLOT (settingChanged (const CSMPrefs::Setting *))); CSMPrefs::get()["ID Tables"].update(); } void CSVWorld::Table::setEditLock (bool locked) { for (std::vector::iterator iter (mDelegates.begin()); iter!=mDelegates.end(); ++iter) (*iter)->setEditLock (locked); DragRecordTable::setEditLock(locked); mDispatcher->setEditLock (locked); } CSMWorld::UniversalId CSVWorld::Table::getUniversalId (int row) const { row = mProxyModel->mapToSource (mProxyModel->index (row, 0)).row(); int idColumn = mModel->findColumnIndex (CSMWorld::Columns::ColumnId_Id); int typeColumn = mModel->findColumnIndex (CSMWorld::Columns::ColumnId_RecordType); return CSMWorld::UniversalId ( static_cast (mModel->data (mModel->index (row, typeColumn)).toInt()), mModel->data (mModel->index (row, idColumn)).toString().toUtf8().constData()); } std::vector CSVWorld::Table::getSelectedIds() const { std::vector ids; QModelIndexList selectedRows = selectionModel()->selectedRows(); int columnIndex = mModel->findColumnIndex (CSMWorld::Columns::ColumnId_Id); for (QModelIndexList::const_iterator iter (selectedRows.begin()); iter != selectedRows.end(); ++iter) { int row = mProxyModel->mapToSource (mProxyModel->index (iter->row(), 0)).row(); ids.emplace_back(mModel->data (mModel->index (row, columnIndex)).toString().toUtf8().constData()); } return ids; } void CSVWorld::Table::editRecord() { if (!mEditLock || (mModel->getFeatures() & CSMWorld::IdTableBase::Feature_Constant)) { QModelIndexList selectedRows = selectionModel()->selectedRows(); if (selectedRows.size()==1) emit editRequest (getUniversalId (selectedRows.begin()->row()), ""); } } void CSVWorld::Table::cloneRecord() { if (!mEditLock || (mModel->getFeatures() & CSMWorld::IdTableBase::Feature_Constant)) { QModelIndexList selectedRows = selectionModel()->selectedRows(); const CSMWorld::UniversalId& toClone = getUniversalId(selectedRows.begin()->row()); if (selectedRows.size() == 1) { emit cloneRequest (toClone); } } } void CSVWorld::Table::touchRecord() { if (!mEditLock && mModel->getFeatures() & CSMWorld::IdTableBase::Feature_AllowTouch) { std::vector touchIds; QModelIndexList selectedRows = selectionModel()->selectedRows(); for (auto it = selectedRows.begin(); it != selectedRows.end(); ++it) { touchIds.push_back(getUniversalId(it->row())); } emit touchRequest(touchIds); } } void CSVWorld::Table::moveUpRecord() { if (mEditLock || (mModel->getFeatures() & CSMWorld::IdTableBase::Feature_Constant)) return; QModelIndexList selectedRows = selectionModel()->selectedRows(); if (selectedRows.size()==1) { int row2 =selectedRows.begin()->row(); if (row2>0) { int row = row2-1; row = mProxyModel->mapToSource (mProxyModel->index (row, 0)).row(); row2 = mProxyModel->mapToSource (mProxyModel->index (row2, 0)).row(); if (row2<=row) throw std::runtime_error ("Inconsistent row order"); std::vector newOrder (row2-row+1); newOrder[0] = row2-row; newOrder[row2-row] = 0; for (int i=1; i (*mModel), row, newOrder)); } } } void CSVWorld::Table::moveDownRecord() { if (mEditLock || (mModel->getFeatures() & CSMWorld::IdTableBase::Feature_Constant)) return; QModelIndexList selectedRows = selectionModel()->selectedRows(); if (selectedRows.size()==1) { int row =selectedRows.begin()->row(); if (rowrowCount()-1) { int row2 = row+1; row = mProxyModel->mapToSource (mProxyModel->index (row, 0)).row(); row2 = mProxyModel->mapToSource (mProxyModel->index (row2, 0)).row(); if (row2<=row) throw std::runtime_error ("Inconsistent row order"); std::vector newOrder (row2-row+1); newOrder[0] = row2-row; newOrder[row2-row] = 0; for (int i=1; i (*mModel), row, newOrder)); } } } void CSVWorld::Table::moveRecords(QDropEvent *event) { if (mEditLock || (mModel->getFeatures() & CSMWorld::IdTableBase::Feature_Constant)) return; QModelIndex targedIndex = indexAt(event->pos()); QModelIndexList selectedRows = selectionModel()->selectedRows(); int targetRowRaw = targedIndex.row(); int targetRow = mProxyModel->mapToSource (mProxyModel->index (targetRowRaw, 0)).row(); int baseRowRaw = targedIndex.row() - 1; int baseRow = mProxyModel->mapToSource (mProxyModel->index (baseRowRaw, 0)).row(); int highestDifference = 0; for (const auto& thisRowData : selectedRows) { int thisRow = mProxyModel->mapToSource (mProxyModel->index (thisRowData.row(), 0)).row(); if (std::abs(targetRow - thisRow) > highestDifference) highestDifference = std::abs(targetRow - thisRow); if (thisRow - 1 < baseRow) baseRow = thisRow - 1; } std::vector newOrder (highestDifference + 1); for (long unsigned int i = 0; i < newOrder.size(); ++i) { newOrder[i] = i; } if (selectedRows.size() > 1) { Log(Debug::Warning) << "Move operation failed: Moving multiple selections isn't implemented."; return; } for (const auto& thisRowData : selectedRows) { /* Moving algorithm description a) Remove the (ORIGIN + 1)th list member. b) Add (ORIGIN+1)th list member with value TARGET c) If ORIGIN > TARGET,d_INC; ELSE d_DEC d_INC) increase all members after (and including) the TARGET by one, stop before hitting ORIGINth address d_DEC) decrease all members after the ORIGIN by one, stop after hitting address TARGET */ int originRowRaw = thisRowData.row(); int originRow = mProxyModel->mapToSource (mProxyModel->index (originRowRaw, 0)).row(); newOrder.erase(newOrder.begin() + originRow - baseRow - 1); newOrder.emplace(newOrder.begin() + originRow - baseRow - 1, targetRow - baseRow - 1); if (originRow > targetRow) { for (int i = targetRow - baseRow - 1; i < originRow - baseRow - 1; ++i) { ++newOrder[i]; } } else { for (int i = originRow - baseRow; i <= targetRow - baseRow - 1; ++i) { --newOrder[i]; } } } mDocument.getUndoStack().push (new CSMWorld::ReorderRowsCommand ( dynamic_cast (*mModel), baseRow + 1, newOrder)); } void CSVWorld::Table::editCell() { emit editRequest(mEditIdAction->getCurrentId(), ""); } void CSVWorld::Table::openHelp() { Misc::HelpViewer::openHelp("manuals/openmw-cs/tables.html"); } void CSVWorld::Table::viewRecord() { if (!(mModel->getFeatures() & CSMWorld::IdTableBase::Feature_View)) return; QModelIndexList selectedRows = selectionModel()->selectedRows(); if (selectedRows.size()==1) { int row = selectedRows.begin()->row(); row = mProxyModel->mapToSource (mProxyModel->index (row, 0)).row(); std::pair params = mModel->view (row); if (params.first.getType()!=CSMWorld::UniversalId::Type_None) emit editRequest (params.first, params.second); } } void CSVWorld::Table::previewRecord() { QModelIndexList selectedRows = selectionModel()->selectedRows(); if (selectedRows.size()==1) { std::string id = getUniversalId (selectedRows.begin()->row()).getId(); QModelIndex index = mModel->getModelIndex (id, mModel->findColumnIndex (CSMWorld::Columns::ColumnId_Modification)); if (mModel->data (index)!=CSMWorld::RecordBase::State_Deleted) emit editRequest (CSMWorld::UniversalId (CSMWorld::UniversalId::Type_Preview, id), ""); } } void CSVWorld::Table::executeExtendedDelete() { if (CSMPrefs::get()["ID Tables"]["extended-config"].isTrue()) { emit extendedDeleteConfigRequest(getSelectedIds()); } else { QMetaObject::invokeMethod(mDispatcher, "executeExtendedDelete", Qt::QueuedConnection); } } void CSVWorld::Table::executeExtendedRevert() { if (CSMPrefs::get()["ID Tables"]["extended-config"].isTrue()) { emit extendedRevertConfigRequest(getSelectedIds()); } else { QMetaObject::invokeMethod(mDispatcher, "executeExtendedRevert", Qt::QueuedConnection); } } void CSVWorld::Table::settingChanged (const CSMPrefs::Setting *setting) { if (*setting=="ID Tables/jump-to-added") { if (setting->toString()=="Jump and Select") { mJumpToAddedRecord = true; mUnselectAfterJump = false; } else if (setting->toString()=="Jump Only") { mJumpToAddedRecord = true; mUnselectAfterJump = true; } else // No Jump { mJumpToAddedRecord = false; mUnselectAfterJump = false; } } else if (*setting=="Records/type-format" || *setting=="Records/status-format") { int columns = mModel->columnCount(); for (int i=0; i (*delegate).settingChanged (setting); emit dataChanged (mModel->index (0, i), mModel->index (mModel->rowCount()-1, i)); } } else if (setting->getParent()->getKey()=="ID Tables" && setting->getKey().substr (0, 6)=="double") { std::string modifierString = setting->getKey().substr (6); Qt::KeyboardModifiers modifiers; if (modifierString=="-s") modifiers = Qt::ShiftModifier; else if (modifierString=="-c") modifiers = Qt::ControlModifier; else if (modifierString=="-sc") modifiers = Qt::ShiftModifier | Qt::ControlModifier; DoubleClickAction action = Action_None; std::string value = setting->toString(); if (value=="Edit in Place") action = Action_InPlaceEdit; else if (value=="Edit Record") action = Action_EditRecord; else if (value=="View") action = Action_View; else if (value=="Revert") action = Action_Revert; else if (value=="Delete") action = Action_Delete; else if (value=="Edit Record and Close") action = Action_EditRecordAndClose; else if (value=="View and Close") action = Action_ViewAndClose; mDoubleClickActions[modifiers] = action; } } void CSVWorld::Table::tableSizeUpdate() { int size = 0; int deleted = 0; int modified = 0; if (mProxyModel->columnCount()>0) { int rows = mProxyModel->rowCount(); int columnIndex = mModel->searchColumnIndex (CSMWorld::Columns::ColumnId_Modification); if (columnIndex!=-1) { for (int i=0; imapToSource (mProxyModel->index (i, 0)); int state = mModel->data (mModel->index (index.row(), columnIndex)).toInt(); switch (state) { case CSMWorld::RecordBase::State_BaseOnly: ++size; break; case CSMWorld::RecordBase::State_Modified: ++size; ++modified; break; case CSMWorld::RecordBase::State_ModifiedOnly: ++size; ++modified; break; case CSMWorld::RecordBase:: State_Deleted: ++deleted; ++modified; break; } } } else size = rows; } emit tableSizeChanged (size, deleted, modified); } void CSVWorld::Table::selectionSizeUpdate() { emit selectionSizeChanged (selectionModel()->selectedRows().size()); } void CSVWorld::Table::requestFocus (const std::string& id) { QModelIndex index = mProxyModel->getModelIndex (id, 0); if (index.isValid()) { // This will scroll to the row. selectRow (index.row()); // This will actually select it. selectionModel()->select (index, QItemSelectionModel::Select | QItemSelectionModel::Rows); } } void CSVWorld::Table::recordFilterChanged (std::shared_ptr filter) { mProxyModel->setFilter (filter); tableSizeUpdate(); selectionSizeUpdate(); } void CSVWorld::Table::mouseMoveEvent (QMouseEvent* event) { if (event->buttons() & Qt::LeftButton) { startDragFromTable(*this); } } std::vector CSVWorld::Table::getColumnsWithDisplay(CSMWorld::ColumnBase::Display display) const { const int count = mModel->columnCount(); std::vector titles; for (int i = 0; i < count; ++i) { CSMWorld::ColumnBase::Display columndisplay = static_cast (mModel->headerData (i, Qt::Horizontal, CSMWorld::ColumnBase::Role_Display).toInt()); if (display == columndisplay) { titles.emplace_back(mModel->headerData (i, Qt::Horizontal).toString().toUtf8().constData()); } } return titles; } std::vector< CSMWorld::UniversalId > CSVWorld::Table::getDraggedRecords() const { QModelIndexList selectedRows = selectionModel()->selectedRows(); std::vector idToDrag; for (QModelIndex& it : selectedRows) idToDrag.push_back (getUniversalId (it.row())); return idToDrag; } void CSVWorld::Table::rowAdded(const std::string &id) { tableSizeUpdate(); if(mJumpToAddedRecord) { int idColumn = mModel->findColumnIndex(CSMWorld::Columns::ColumnId_Id); selectRow(mProxyModel->getModelIndex(id, idColumn).row()); if(mUnselectAfterJump) clearSelection(); } } ================================================ FILE: apps/opencs/view/world/table.hpp ================================================ #ifndef CSV_WORLD_TABLE_H #define CSV_WORLD_TABLE_H #include #include #include #include "../../model/filter/node.hpp" #include "../../model/world/columnbase.hpp" #include "../../model/world/universalid.hpp" #include "dragrecordtable.hpp" class QAction; namespace CSMDoc { class Document; } namespace CSMWorld { class IdTableProxyModel; class IdTableBase; class CommandDispatcher; } namespace CSMPrefs { class Setting; } namespace CSVWorld { class CommandDelegate; class TableEditIdAction; ///< Table widget class Table : public DragRecordTable { Q_OBJECT enum DoubleClickAction { Action_None, Action_InPlaceEdit, Action_EditRecord, Action_View, Action_Revert, Action_Delete, Action_EditRecordAndClose, Action_ViewAndClose }; std::vector mDelegates; QAction *mEditAction; QAction *mCreateAction; QAction *mCloneAction; QAction *mTouchAction; QAction *mRevertAction; QAction *mDeleteAction; QAction *mMoveUpAction; QAction *mMoveDownAction; QAction *mViewAction; QAction *mPreviewAction; QAction *mExtendedDeleteAction; QAction *mExtendedRevertAction; QAction *mHelpAction; TableEditIdAction *mEditIdAction; CSMWorld::IdTableProxyModel *mProxyModel; CSMWorld::IdTableBase *mModel; int mRecordStatusDisplay; CSMWorld::CommandDispatcher *mDispatcher; std::map mDoubleClickActions; bool mJumpToAddedRecord; bool mUnselectAfterJump; private: void contextMenuEvent (QContextMenuEvent *event) override; void mouseMoveEvent(QMouseEvent *event) override; protected: void mouseDoubleClickEvent (QMouseEvent *event) override; public: Table (const CSMWorld::UniversalId& id, bool createAndDelete, bool sorting, CSMDoc::Document& document); ///< \param createAndDelete Allow creation and deletion of records. /// \param sorting Allow changing order of rows in the view via column headers. virtual void setEditLock (bool locked); CSMWorld::UniversalId getUniversalId (int row) const; std::vector getColumnsWithDisplay(CSMWorld::ColumnBase::Display display) const; std::vector getSelectedIds() const; std::vector getDraggedRecords() const override; signals: void editRequest (const CSMWorld::UniversalId& id, const std::string& hint); void selectionSizeChanged (int size); void tableSizeChanged (int size, int deleted, int modified); ///< \param size Number of not deleted records /// \param deleted Number of deleted records /// \param modified Number of added and modified records void createRequest(); void cloneRequest(const CSMWorld::UniversalId&); void touchRequest(const std::vector& ids); void closeRequest(); void extendedDeleteConfigRequest(const std::vector &selectedIds); void extendedRevertConfigRequest(const std::vector &selectedIds); private slots: void editCell(); static void openHelp(); void editRecord(); void cloneRecord(); void touchRecord(); void moveUpRecord(); void moveDownRecord(); void moveRecords(QDropEvent *event); void viewRecord(); void previewRecord(); void executeExtendedDelete(); void executeExtendedRevert(); public slots: void settingChanged (const CSMPrefs::Setting *setting); void tableSizeUpdate(); void selectionSizeUpdate(); void requestFocus (const std::string& id); void recordFilterChanged (std::shared_ptr filter); void rowAdded(const std::string &id); }; } #endif ================================================ FILE: apps/opencs/view/world/tablebottombox.cpp ================================================ #include "tablebottombox.hpp" #include #include #include #include #include #include #include "creator.hpp" void CSVWorld::TableBottomBox::updateSize() { // Make sure that the size of the bottom box is determined by the currently visible widget for (int i = 0; i < mLayout->count(); ++i) { QSizePolicy::Policy verPolicy = QSizePolicy::Ignored; if (mLayout->widget(i) == mLayout->currentWidget()) { verPolicy = QSizePolicy::Expanding; } mLayout->widget(i)->setSizePolicy(QSizePolicy::Expanding, verPolicy); } } void CSVWorld::TableBottomBox::updateStatus() { if (mShowStatusBar) { if (!mStatusMessage.isEmpty()) { mStatus->setText (mStatusMessage); return; } static const char *sLabels[4] = { "record", "deleted", "touched", "selected" }; static const char *sLabelsPlural[4] = { "records", "deleted", "touched", "selected" }; std::ostringstream stream; bool first = true; for (int i=0; i<4; ++i) { if (mStatusCount[i]>0) { if (first) first = false; else stream << ", "; stream << mStatusCount[i] << ' ' << (mStatusCount[i]==1 ? sLabels[i] : sLabelsPlural[i]); } } if (mHasPosition) { if (!first) stream << " -- "; stream << "(" << mRow << ", " << mColumn << ")"; } mStatus->setText (QString::fromUtf8 (stream.str().c_str())); } } void CSVWorld::TableBottomBox::extendedConfigRequest(CSVWorld::ExtendedCommandConfigurator::Mode mode, const std::vector &selectedIds) { mExtendedConfigurator->configure (mode, selectedIds); mLayout->setCurrentWidget (mExtendedConfigurator); mEditMode = EditMode_ExtendedConfig; setVisible (true); mExtendedConfigurator->setFocus(); } CSVWorld::TableBottomBox::TableBottomBox (const CreatorFactoryBase& creatorFactory, CSMDoc::Document& document, const CSMWorld::UniversalId& id, QWidget *parent) : QWidget (parent), mShowStatusBar (false), mEditMode(EditMode_None), mHasPosition(false), mRow(0), mColumn(0) { for (int i=0; i<4; ++i) mStatusCount[i] = 0; setVisible (false); mLayout = new QStackedLayout; mLayout->setContentsMargins (0, 0, 0, 0); connect (mLayout, SIGNAL (currentChanged (int)), this, SLOT (currentWidgetChanged (int))); mStatus = new QLabel; mStatusBar = new QStatusBar(this); mStatusBar->addWidget (mStatus); mLayout->addWidget (mStatusBar); setLayout (mLayout); mCreator = creatorFactory.makeCreator (document, id); if (mCreator) { mCreator->installEventFilter(this); mLayout->addWidget (mCreator); connect (mCreator, SIGNAL (done()), this, SLOT (requestDone())); connect (mCreator, SIGNAL (requestFocus (const std::string&)), this, SIGNAL (requestFocus (const std::string&))); } mExtendedConfigurator = new ExtendedCommandConfigurator (document, id, this); mExtendedConfigurator->installEventFilter(this); mLayout->addWidget (mExtendedConfigurator); connect (mExtendedConfigurator, SIGNAL (done()), this, SLOT (requestDone())); updateSize(); } void CSVWorld::TableBottomBox::setEditLock (bool locked) { if (mCreator) mCreator->setEditLock (locked); mExtendedConfigurator->setEditLock (locked); } CSVWorld::TableBottomBox::~TableBottomBox() { delete mCreator; } bool CSVWorld::TableBottomBox::eventFilter(QObject *object, QEvent *event) { if (event->type() == QEvent::KeyPress) { QKeyEvent *keyEvent = static_cast(event); if (keyEvent->key() == Qt::Key_Escape) { requestDone(); return true; } } return QWidget::eventFilter(object, event); } void CSVWorld::TableBottomBox::setStatusBar (bool show) { if (show!=mShowStatusBar) { setVisible (show || (mEditMode != EditMode_None)); mShowStatusBar = show; if (show) updateStatus(); } } bool CSVWorld::TableBottomBox::canCreateAndDelete() const { return mCreator; } void CSVWorld::TableBottomBox::requestDone() { if (!mShowStatusBar) setVisible (false); else updateStatus(); mLayout->setCurrentWidget (mStatusBar); mEditMode = EditMode_None; } void CSVWorld::TableBottomBox::currentWidgetChanged(int /*index*/) { updateSize(); } void CSVWorld::TableBottomBox::setStatusMessage (const QString& message) { mStatusMessage = message; updateStatus(); } void CSVWorld::TableBottomBox::selectionSizeChanged (int size) { if (mStatusCount[3]!=size) { mStatusMessage = ""; mStatusCount[3] = size; updateStatus(); } } void CSVWorld::TableBottomBox::tableSizeChanged (int size, int deleted, int modified) { bool changed = false; if (mStatusCount[0]!=size) { mStatusCount[0] = size; changed = true; } if (mStatusCount[1]!=deleted) { mStatusCount[1] = deleted; changed = true; } if (mStatusCount[2]!=modified) { mStatusCount[2] = modified; changed = true; } if (changed) { mStatusMessage = ""; updateStatus(); } } void CSVWorld::TableBottomBox::positionChanged (int row, int column) { mRow = row; mColumn = column; mHasPosition = true; updateStatus(); } void CSVWorld::TableBottomBox::noMorePosition() { mHasPosition = false; updateStatus(); } void CSVWorld::TableBottomBox::createRequest() { mCreator->reset(); mCreator->toggleWidgets(true); mLayout->setCurrentWidget (mCreator); setVisible (true); mEditMode = EditMode_Creation; mCreator->focus(); } void CSVWorld::TableBottomBox::cloneRequest(const std::string& id, const CSMWorld::UniversalId::Type type) { mCreator->reset(); mCreator->cloneMode(id, type); mLayout->setCurrentWidget(mCreator); mCreator->toggleWidgets(false); setVisible (true); mEditMode = EditMode_Creation; mCreator->focus(); } void CSVWorld::TableBottomBox::touchRequest(const std::vector& ids) { mCreator->touch(ids); } void CSVWorld::TableBottomBox::extendedDeleteConfigRequest(const std::vector &selectedIds) { extendedConfigRequest(ExtendedCommandConfigurator::Mode_Delete, selectedIds); } void CSVWorld::TableBottomBox::extendedRevertConfigRequest(const std::vector &selectedIds) { extendedConfigRequest(ExtendedCommandConfigurator::Mode_Revert, selectedIds); } ================================================ FILE: apps/opencs/view/world/tablebottombox.hpp ================================================ #ifndef CSV_WORLD_BOTTOMBOX_H #define CSV_WORLD_BOTTOMBOX_H #include #include #include "extendedcommandconfigurator.hpp" class QLabel; class QStackedLayout; class QStatusBar; namespace CSMDoc { class Document; } namespace CSVWorld { class CreatorFactoryBase; class Creator; class TableBottomBox : public QWidget { Q_OBJECT enum EditMode { EditMode_None, EditMode_Creation, EditMode_ExtendedConfig }; bool mShowStatusBar; QLabel *mStatus; QStatusBar *mStatusBar; int mStatusCount[4]; EditMode mEditMode; Creator *mCreator; ExtendedCommandConfigurator *mExtendedConfigurator; QStackedLayout *mLayout; bool mHasPosition; int mRow; int mColumn; QString mStatusMessage; private: // not implemented TableBottomBox (const TableBottomBox&); TableBottomBox& operator= (const TableBottomBox&); void updateSize(); void updateStatus(); void extendedConfigRequest(ExtendedCommandConfigurator::Mode mode, const std::vector &selectedIds); public: TableBottomBox (const CreatorFactoryBase& creatorFactory, CSMDoc::Document& document, const CSMWorld::UniversalId& id, QWidget *parent = nullptr); virtual ~TableBottomBox(); bool eventFilter(QObject *object, QEvent *event) override; void setEditLock (bool locked); void setStatusBar (bool show); bool canCreateAndDelete() const; ///< Is record creation and deletion supported? /// /// \note The BotomBox does not partake in the deletion of records. void setStatusMessage (const QString& message); signals: void requestFocus (const std::string& id); ///< Request owner of this box to focus the just created \a id. The owner may /// ignore this request. private slots: void requestDone(); ///< \note This slot being called does not imply success. void currentWidgetChanged(int index); public slots: void selectionSizeChanged (int size); void tableSizeChanged (int size, int deleted, int modified); ///< \param size Number of not deleted records /// \param deleted Number of deleted records /// \param modified Number of added and modified records void positionChanged (int row, int column); void noMorePosition(); void createRequest(); void cloneRequest(const std::string& id, const CSMWorld::UniversalId::Type type); void touchRequest(const std::vector&); void extendedDeleteConfigRequest(const std::vector &selectedIds); void extendedRevertConfigRequest(const std::vector &selectedIds); }; } #endif ================================================ FILE: apps/opencs/view/world/tableeditidaction.cpp ================================================ #include "tableeditidaction.hpp" #include #include "../../model/world/tablemimedata.hpp" CSVWorld::TableEditIdAction::CellData CSVWorld::TableEditIdAction::getCellData(int row, int column) const { QModelIndex index = mTable.model()->index(row, column); if (index.isValid()) { QVariant display = mTable.model()->data(index, CSMWorld::ColumnBase::Role_Display); QString value = mTable.model()->data(index).toString(); return std::make_pair(static_cast(display.toInt()), value); } return std::make_pair(CSMWorld::ColumnBase::Display_None, ""); } CSVWorld::TableEditIdAction::TableEditIdAction(const QTableView &table, QWidget *parent) : QAction(parent), mTable(table), mCurrentId(CSMWorld::UniversalId::Type_None) {} void CSVWorld::TableEditIdAction::setCell(int row, int column) { CellData data = getCellData(row, column); CSMWorld::UniversalId::Type idType = CSMWorld::TableMimeData::convertEnums(data.first); if (idType != CSMWorld::UniversalId::Type_None) { mCurrentId = CSMWorld::UniversalId(idType, data.second.toUtf8().constData()); setText("Edit '" + data.second + "'"); } } CSMWorld::UniversalId CSVWorld::TableEditIdAction::getCurrentId() const { return mCurrentId; } bool CSVWorld::TableEditIdAction::isValidIdCell(int row, int column) const { CellData data = getCellData(row, column); CSMWorld::UniversalId::Type idType = CSMWorld::TableMimeData::convertEnums(data.first); return CSMWorld::ColumnBase::isId(data.first) && idType != CSMWorld::UniversalId::Type_None && !data.second.isEmpty(); } ================================================ FILE: apps/opencs/view/world/tableeditidaction.hpp ================================================ #ifndef CSVWORLD_TABLEEDITIDACTION_HPP #define CSVWORLD_TABLEEDITIDACTION_HPP #include #include "../../model/world/columnbase.hpp" #include "../../model/world/universalid.hpp" class QTableView; namespace CSVWorld { class TableEditIdAction : public QAction { const QTableView &mTable; CSMWorld::UniversalId mCurrentId; typedef std::pair CellData; CellData getCellData(int row, int column) const; public: TableEditIdAction(const QTableView &table, QWidget *parent = nullptr); void setCell(int row, int column); CSMWorld::UniversalId getCurrentId() const; bool isValidIdCell(int row, int column) const; }; } #endif ================================================ FILE: apps/opencs/view/world/tablesubview.cpp ================================================ #include "tablesubview.hpp" #include #include #include #include #include #include #include "../../model/doc/document.hpp" #include "../../model/world/tablemimedata.hpp" #include "../doc/sizehint.hpp" #include "../filter/filterbox.hpp" #include "table.hpp" #include "tablebottombox.hpp" #include "creator.hpp" CSVWorld::TableSubView::TableSubView (const CSMWorld::UniversalId& id, CSMDoc::Document& document, const CreatorFactoryBase& creatorFactory, bool sorting) : SubView (id) { QVBoxLayout *layout = new QVBoxLayout; layout->addWidget (mBottom = new TableBottomBox (creatorFactory, document, id, this), 0); layout->insertWidget (0, mTable = new Table (id, mBottom->canCreateAndDelete(), sorting, document), 2); mFilterBox = new CSVFilter::FilterBox (document.getData(), this); layout->insertWidget (0, mFilterBox); CSVDoc::SizeHintWidget *widget = new CSVDoc::SizeHintWidget; widget->setLayout (layout); setWidget (widget); // prefer height of the screen and full width of the table const QRect rect = QApplication::desktop()->screenGeometry(this); int frameHeight = 40; // set a reasonable default QWidget *topLevel = QApplication::topLevelAt(pos()); if (topLevel) frameHeight = topLevel->frameGeometry().height() - topLevel->height(); widget->setSizeHint(QSize(mTable->horizontalHeader()->length(), rect.height()-frameHeight)); connect (mTable, SIGNAL (editRequest (const CSMWorld::UniversalId&, const std::string&)), this, SLOT (editRequest (const CSMWorld::UniversalId&, const std::string&))); connect (mTable, SIGNAL (selectionSizeChanged (int)), mBottom, SLOT (selectionSizeChanged (int))); connect (mTable, SIGNAL (tableSizeChanged (int, int, int)), mBottom, SLOT (tableSizeChanged (int, int, int))); mTable->tableSizeUpdate(); mTable->selectionSizeUpdate(); mTable->viewport()->installEventFilter(this); mBottom->installEventFilter(this); mFilterBox->installEventFilter(this); if (mBottom->canCreateAndDelete()) { connect (mTable, SIGNAL (createRequest()), mBottom, SLOT (createRequest())); connect (mTable, SIGNAL (cloneRequest(const CSMWorld::UniversalId&)), this, SLOT(cloneRequest(const CSMWorld::UniversalId&))); connect (this, SIGNAL(cloneRequest(const std::string&, const CSMWorld::UniversalId::Type)), mBottom, SLOT(cloneRequest(const std::string&, const CSMWorld::UniversalId::Type))); connect (mTable, SIGNAL(touchRequest(const std::vector&)), mBottom, SLOT(touchRequest(const std::vector&))); connect (mTable, SIGNAL(extendedDeleteConfigRequest(const std::vector &)), mBottom, SLOT(extendedDeleteConfigRequest(const std::vector &))); connect (mTable, SIGNAL(extendedRevertConfigRequest(const std::vector &)), mBottom, SLOT(extendedRevertConfigRequest(const std::vector &))); } connect (mBottom, SIGNAL (requestFocus (const std::string&)), mTable, SLOT (requestFocus (const std::string&))); connect (mFilterBox, SIGNAL (recordFilterChanged (std::shared_ptr)), mTable, SLOT (recordFilterChanged (std::shared_ptr))); connect(mFilterBox, SIGNAL(recordDropped(std::vector&, Qt::DropAction)), this, SLOT(createFilterRequest(std::vector&, Qt::DropAction))); connect (mTable, SIGNAL (closeRequest()), this, SLOT (closeRequest())); } void CSVWorld::TableSubView::setEditLock (bool locked) { mTable->setEditLock (locked); mBottom->setEditLock (locked); } void CSVWorld::TableSubView::editRequest (const CSMWorld::UniversalId& id, const std::string& hint) { focusId (id, hint); } void CSVWorld::TableSubView::setStatusBar (bool show) { mBottom->setStatusBar (show); } void CSVWorld::TableSubView::useHint (const std::string& hint) { if (hint.empty()) return; if (hint[0]=='f' && hint.size()>=2) mFilterBox->setRecordFilter (hint.substr (2)); } void CSVWorld::TableSubView::cloneRequest(const CSMWorld::UniversalId& toClone) { emit cloneRequest(toClone.getId(), toClone.getType()); } void CSVWorld::TableSubView::createFilterRequest (std::vector< CSMWorld::UniversalId>& types, Qt::DropAction action) { std::vector > > filterSource; std::vector refIdColumns = mTable->getColumnsWithDisplay(CSMWorld::TableMimeData::convertEnums(CSMWorld::UniversalId::Type_Referenceable)); bool hasRefIdDisplay = !refIdColumns.empty(); for (std::vector::iterator it(types.begin()); it != types.end(); ++it) { CSMWorld::UniversalId::Type type = it->getType(); std::vector col = mTable->getColumnsWithDisplay(CSMWorld::TableMimeData::convertEnums(type)); if(!col.empty()) { filterSource.emplace_back(it->getId(), col); } if(hasRefIdDisplay && CSMWorld::TableMimeData::isReferencable(type)) { filterSource.emplace_back(it->getId(), refIdColumns); } } mFilterBox->createFilterRequest(filterSource, action); } bool CSVWorld::TableSubView::eventFilter (QObject* object, QEvent* event) { if (event->type() == QEvent::Drop) { if (QDropEvent* drop = dynamic_cast(event)) { const CSMWorld::TableMimeData* tableMimeData = dynamic_cast(drop->mimeData()); if (!tableMimeData) // May happen when non-records (e.g. plain text) are dragged and dropped return false; bool handled = tableMimeData->holdsType(CSMWorld::UniversalId::Type_Filter); if (handled) { mFilterBox->setRecordFilter(tableMimeData->returnMatching(CSMWorld::UniversalId::Type_Filter).getId()); } return handled; } } return false; } void CSVWorld::TableSubView::requestFocus (const std::string& id) { mTable->requestFocus(id); } ================================================ FILE: apps/opencs/view/world/tablesubview.hpp ================================================ #ifndef CSV_WORLD_TABLESUBVIEW_H #define CSV_WORLD_TABLESUBVIEW_H #include "../doc/subview.hpp" #include class QModelIndex; namespace CSMWorld { class IdTable; } namespace CSMDoc { class Document; } namespace CSVFilter { class FilterBox; } namespace CSVWorld { class Table; class TableBottomBox; class CreatorFactoryBase; class TableSubView : public CSVDoc::SubView { Q_OBJECT Table *mTable; TableBottomBox *mBottom; CSVFilter::FilterBox *mFilterBox; public: TableSubView (const CSMWorld::UniversalId& id, CSMDoc::Document& document, const CreatorFactoryBase& creatorFactory, bool sorting); void setEditLock (bool locked) override; void setStatusBar (bool show) override; void useHint (const std::string& hint) override; protected: bool eventFilter(QObject* object, QEvent *event) override; signals: void cloneRequest(const std::string&, const CSMWorld::UniversalId::Type); private slots: void editRequest (const CSMWorld::UniversalId& id, const std::string& hint); void cloneRequest (const CSMWorld::UniversalId& toClone); void createFilterRequest(std::vector< CSMWorld::UniversalId >& types, Qt::DropAction action); public slots: void requestFocus (const std::string& id); }; } #endif ================================================ FILE: apps/opencs/view/world/util.cpp ================================================ #include "util.hpp" #include #include #include #include #include #include #include #include #include #include #include #include "../../model/world/commands.hpp" #include "../../model/world/tablemimedata.hpp" #include "../../model/world/commanddispatcher.hpp" #include "../widget/coloreditor.hpp" #include "../widget/droplineedit.hpp" #include "dialoguespinbox.hpp" #include "scriptedit.hpp" CSVWorld::NastyTableModelHack::NastyTableModelHack (QAbstractItemModel& model) : mModel (model) {} int CSVWorld::NastyTableModelHack::rowCount (const QModelIndex & parent) const { return mModel.rowCount (parent); } int CSVWorld::NastyTableModelHack::columnCount (const QModelIndex & parent) const { return mModel.columnCount (parent); } QVariant CSVWorld::NastyTableModelHack::data (const QModelIndex & index, int role) const { return mModel.data (index, role); } bool CSVWorld::NastyTableModelHack::setData ( const QModelIndex &index, const QVariant &value, int role) { mData = value; return true; } QVariant CSVWorld::NastyTableModelHack::getData() const { return mData; } CSVWorld::CommandDelegateFactory::~CommandDelegateFactory() {} CSVWorld::CommandDelegateFactoryCollection *CSVWorld::CommandDelegateFactoryCollection::sThis = nullptr; CSVWorld::CommandDelegateFactoryCollection::CommandDelegateFactoryCollection() { if (sThis) throw std::logic_error ("multiple instances of CSVWorld::CommandDelegateFactoryCollection"); sThis = this; } CSVWorld::CommandDelegateFactoryCollection::~CommandDelegateFactoryCollection() { sThis = nullptr; for (std::map::iterator iter ( mFactories.begin()); iter!=mFactories.end(); ++iter) delete iter->second; } void CSVWorld::CommandDelegateFactoryCollection::add (CSMWorld::ColumnBase::Display display, CommandDelegateFactory *factory) { mFactories.insert (std::make_pair (display, factory)); } CSVWorld::CommandDelegate *CSVWorld::CommandDelegateFactoryCollection::makeDelegate ( CSMWorld::ColumnBase::Display display, CSMWorld::CommandDispatcher *dispatcher, CSMDoc::Document& document, QObject *parent) const { std::map::const_iterator iter = mFactories.find (display); if (iter!=mFactories.end()) return iter->second->makeDelegate (dispatcher, document, parent); return new CommandDelegate (dispatcher, document, parent); } const CSVWorld::CommandDelegateFactoryCollection& CSVWorld::CommandDelegateFactoryCollection::get() { if (!sThis) throw std::logic_error ("no instance of CSVWorld::CommandDelegateFactoryCollection"); return *sThis; } QUndoStack& CSVWorld::CommandDelegate::getUndoStack() const { return mDocument.getUndoStack(); } CSMDoc::Document& CSVWorld::CommandDelegate::getDocument() const { return mDocument; } CSMWorld::ColumnBase::Display CSVWorld::CommandDelegate::getDisplayTypeFromIndex(const QModelIndex &index) const { int rawDisplay = index.data(CSMWorld::ColumnBase::Role_Display).toInt(); return static_cast(rawDisplay); } void CSVWorld::CommandDelegate::setModelDataImp (QWidget *editor, QAbstractItemModel *model, const QModelIndex& index) const { if (!mCommandDispatcher) return; QVariant variant; // Color columns use a custom editor, so we need to fetch selected color from it. CSVWidget::ColorEditor *colorEditor = qobject_cast(editor); if (colorEditor != nullptr) { variant = colorEditor->colorInt(); } else { NastyTableModelHack hack (*model); QStyledItemDelegate::setModelData (editor, &hack, index); variant = hack.getData(); } if ((model->data (index)!=variant) && (model->flags(index) & Qt::ItemIsEditable)) mCommandDispatcher->executeModify (model, index, variant); } CSVWorld::CommandDelegate::CommandDelegate (CSMWorld::CommandDispatcher *commandDispatcher, CSMDoc::Document& document, QObject *parent) : QStyledItemDelegate (parent), mEditLock (false), mCommandDispatcher (commandDispatcher), mDocument (document) {} void CSVWorld::CommandDelegate::setModelData (QWidget *editor, QAbstractItemModel *model, const QModelIndex& index) const { if (!mEditLock) { setModelDataImp (editor, model, index); } ///< \todo provide some kind of feedback to the user, indicating that editing is currently not possible. } QWidget *CSVWorld::CommandDelegate::createEditor (QWidget *parent, const QStyleOptionViewItem& option, const QModelIndex& index) const { CSMWorld::ColumnBase::Display display = getDisplayTypeFromIndex(index); // This createEditor() method is called implicitly from tables. // For boolean values in tables use the default editor (combobox). // Checkboxes is looking ugly in the table view. // TODO: Find a better solution? if (display == CSMWorld::ColumnBase::Display_Boolean) { return QItemEditorFactory::defaultFactory()->createEditor(QVariant::Bool, parent); } // For tables the pop-up of the color editor should appear immediately after the editor creation // (the third parameter of ColorEditor's constructor) else if (display == CSMWorld::ColumnBase::Display_Colour) { return new CSVWidget::ColorEditor(index.data().toInt(), parent, true); } return createEditor (parent, option, index, display); } QWidget *CSVWorld::CommandDelegate::createEditor (QWidget *parent, const QStyleOptionViewItem& option, const QModelIndex& index, CSMWorld::ColumnBase::Display display) const { QVariant variant = index.data(); if (!variant.isValid()) { variant = index.data(Qt::DisplayRole); if (!variant.isValid()) { return nullptr; } } // NOTE: for each editor type (e.g. QLineEdit) there needs to be a corresponding // entry in CSVWorld::DialogueDelegateDispatcher::makeEditor() switch (display) { case CSMWorld::ColumnBase::Display_Colour: { return new CSVWidget::ColorEditor(variant.toInt(), parent); } case CSMWorld::ColumnBase::Display_Integer: { DialogueSpinBox *sb = new DialogueSpinBox(parent); sb->setRange(std::numeric_limits::min(), std::numeric_limits::max()); return sb; } case CSMWorld::ColumnBase::Display_SignedInteger8: { DialogueSpinBox *sb = new DialogueSpinBox(parent); sb->setRange(std::numeric_limits::min(), std::numeric_limits::max()); return sb; } case CSMWorld::ColumnBase::Display_SignedInteger16: { DialogueSpinBox *sb = new DialogueSpinBox(parent); sb->setRange(std::numeric_limits::min(), std::numeric_limits::max()); return sb; } case CSMWorld::ColumnBase::Display_UnsignedInteger8: { DialogueSpinBox *sb = new DialogueSpinBox(parent); sb->setRange(0, std::numeric_limits::max()); return sb; } case CSMWorld::ColumnBase::Display_UnsignedInteger16: { DialogueSpinBox *sb = new DialogueSpinBox(parent); sb->setRange(0, std::numeric_limits::max()); return sb; } case CSMWorld::ColumnBase::Display_Var: return new QLineEdit(parent); case CSMWorld::ColumnBase::Display_Float: { DialogueDoubleSpinBox *dsb = new DialogueDoubleSpinBox(parent); dsb->setRange(-std::numeric_limits::max(), std::numeric_limits::max()); dsb->setSingleStep(0.01f); dsb->setDecimals(3); return dsb; } case CSMWorld::ColumnBase::Display_Double: { DialogueDoubleSpinBox *dsb = new DialogueDoubleSpinBox(parent); dsb->setRange(-std::numeric_limits::max(), std::numeric_limits::max()); dsb->setSingleStep(0.01f); dsb->setDecimals(6); return dsb; } /// \todo implement size limit. QPlainTextEdit does not support a size limit. case CSMWorld::ColumnBase::Display_LongString: case CSMWorld::ColumnBase::Display_LongString256: { QPlainTextEdit *edit = new QPlainTextEdit(parent); edit->setUndoRedoEnabled (false); return edit; } case CSMWorld::ColumnBase::Display_Boolean: return new QCheckBox(parent); case CSMWorld::ColumnBase::Display_ScriptLines: return new ScriptEdit (mDocument, ScriptHighlighter::Mode_Console, parent); case CSMWorld::ColumnBase::Display_String: // For other Display types (that represent record IDs) with drop support IdCompletionDelegate is used return new CSVWidget::DropLineEdit(display, parent); case CSMWorld::ColumnBase::Display_String32: { // For other Display types (that represent record IDs) with drop support IdCompletionDelegate is used CSVWidget::DropLineEdit *widget = new CSVWidget::DropLineEdit(display, parent); widget->setMaxLength (32); return widget; } default: return QStyledItemDelegate::createEditor (parent, option, index); } } void CSVWorld::CommandDelegate::setEditLock (bool locked) { mEditLock = locked; } bool CSVWorld::CommandDelegate::isEditLocked() const { return mEditLock; } void CSVWorld::CommandDelegate::setEditorData (QWidget *editor, const QModelIndex& index) const { setEditorData (editor, index, false); } void CSVWorld::CommandDelegate::setEditorData (QWidget *editor, const QModelIndex& index, bool tryDisplay) const { QVariant variant = index.data(Qt::EditRole); if (tryDisplay) { if (!variant.isValid()) { variant = index.data(Qt::DisplayRole); if (!variant.isValid()) { return; } } QPlainTextEdit* plainTextEdit = qobject_cast(editor); if(plainTextEdit) //for some reason it is easier to brake the loop here { if (plainTextEdit->toPlainText() == variant.toString()) { return; } } } // Color columns use a custom editor, so we need explicitly set a data for it CSVWidget::ColorEditor *colorEditor = qobject_cast(editor); if (colorEditor != nullptr) { colorEditor->setColor(variant.toInt()); return; } QByteArray n = editor->metaObject()->userProperty().name(); if (n == "dateTime") { if (editor->inherits("QTimeEdit")) n = "time"; else if (editor->inherits("QDateEdit")) n = "date"; } if (!n.isEmpty()) { if (!variant.isValid()) variant = QVariant(editor->property(n).userType(), (const void *)nullptr); editor->setProperty(n, variant); } } void CSVWorld::CommandDelegate::settingChanged (const CSMPrefs::Setting *setting) {} ================================================ FILE: apps/opencs/view/world/util.hpp ================================================ #ifndef CSV_WORLD_UTIL_H #define CSV_WORLD_UTIL_H #include #include #include #ifndef Q_MOC_RUN #include "../../model/world/columnbase.hpp" #include "../../model/doc/document.hpp" #endif class QUndoStack; namespace CSMWorld { class TableMimeData; class UniversalId; class CommandDispatcher; } namespace CSMPrefs { class Setting; } namespace CSVWorld { ///< \brief Getting the data out of an editor widget /// /// Really, Qt? Really? class NastyTableModelHack : public QAbstractTableModel { QAbstractItemModel& mModel; QVariant mData; public: NastyTableModelHack (QAbstractItemModel& model); int rowCount (const QModelIndex & parent = QModelIndex()) const override; int columnCount (const QModelIndex & parent = QModelIndex()) const override; QVariant data (const QModelIndex & index, int role = Qt::DisplayRole) const override; bool setData (const QModelIndex &index, const QVariant &value, int role = Qt::EditRole) override; QVariant getData() const; }; class CommandDelegate; class CommandDelegateFactory { public: virtual ~CommandDelegateFactory(); virtual CommandDelegate *makeDelegate (CSMWorld::CommandDispatcher *dispatcher, CSMDoc::Document& document, QObject *parent) const = 0; ///< The ownership of the returned CommandDelegate is transferred to the caller. }; class CommandDelegateFactoryCollection { static CommandDelegateFactoryCollection *sThis; std::map mFactories; private: // not implemented CommandDelegateFactoryCollection (const CommandDelegateFactoryCollection&); CommandDelegateFactoryCollection& operator= (const CommandDelegateFactoryCollection&); public: CommandDelegateFactoryCollection(); ~CommandDelegateFactoryCollection(); void add (CSMWorld::ColumnBase::Display display, CommandDelegateFactory *factory); ///< The ownership of \a factory is transferred to *this. /// /// This function must not be called more than once per value of \a display. CommandDelegate *makeDelegate (CSMWorld::ColumnBase::Display display, CSMWorld::CommandDispatcher *dispatcher, CSMDoc::Document& document, QObject *parent) const; ///< The ownership of the returned CommandDelegate is transferred to the caller. /// /// If no factory is registered for \a display, a CommandDelegate will be returned. static const CommandDelegateFactoryCollection& get(); }; ///< \brief Use commands instead of manipulating the model directly class CommandDelegate : public QStyledItemDelegate { Q_OBJECT bool mEditLock; CSMWorld::CommandDispatcher *mCommandDispatcher; CSMDoc::Document& mDocument; protected: QUndoStack& getUndoStack() const; CSMDoc::Document& getDocument() const; CSMWorld::ColumnBase::Display getDisplayTypeFromIndex(const QModelIndex &index) const; virtual void setModelDataImp (QWidget *editor, QAbstractItemModel *model, const QModelIndex& index) const; public: /// \param commandDispatcher If CommandDelegate will be only be used on read-only /// cells, a 0-pointer can be passed here. CommandDelegate (CSMWorld::CommandDispatcher *commandDispatcher, CSMDoc::Document& document, QObject *parent); void setModelData (QWidget *editor, QAbstractItemModel *model, const QModelIndex& index) const override; QWidget *createEditor (QWidget *parent, const QStyleOptionViewItem& option, const QModelIndex& index) const override; virtual QWidget *createEditor (QWidget *parent, const QStyleOptionViewItem& option, const QModelIndex& index, CSMWorld::ColumnBase::Display display) const; void setEditLock (bool locked); bool isEditLocked() const; ///< \return Does column require update? void setEditorData (QWidget *editor, const QModelIndex& index) const override; virtual void setEditorData (QWidget *editor, const QModelIndex& index, bool tryDisplay) const; /// \attention This is not a slot. For ordering reasons this function needs to be /// called manually from the parent object's settingChanged function. virtual void settingChanged (const CSMPrefs::Setting *setting); }; } #endif ================================================ FILE: apps/opencs/view/world/vartypedelegate.cpp ================================================ #include "vartypedelegate.hpp" #include #include "../../model/world/commands.hpp" #include "../../model/world/columns.hpp" #include "../../model/world/commandmacro.hpp" void CSVWorld::VarTypeDelegate::addCommands (QAbstractItemModel *model, const QModelIndex& index, int type) const { QModelIndex next = model->index (index.row(), index.column()+1); QVariant old = model->data (next); QVariant value; switch (type) { case ESM::VT_Short: case ESM::VT_Int: case ESM::VT_Long: value = old.toInt(); break; case ESM::VT_Float: value = old.toFloat(); break; case ESM::VT_String: value = old.toString(); break; default: break; // ignore the rest } CSMWorld::CommandMacro macro (getUndoStack(), "Modify " + model->headerData (index.column(), Qt::Horizontal, Qt::DisplayRole).toString()); macro.push (new CSMWorld::ModifyCommand (*model, index, type)); macro.push (new CSMWorld::ModifyCommand (*model, next, value)); } CSVWorld::VarTypeDelegate::VarTypeDelegate (const std::vector >& values, CSMWorld::CommandDispatcher *dispatcher, CSMDoc::Document& document, QObject *parent) : EnumDelegate (values, dispatcher, document, parent) {} CSVWorld::VarTypeDelegateFactory::VarTypeDelegateFactory (ESM::VarType type0, ESM::VarType type1, ESM::VarType type2, ESM::VarType type3) { if (type0!=ESM::VT_Unknown) add (type0); if (type1!=ESM::VT_Unknown) add (type1); if (type2!=ESM::VT_Unknown) add (type2); if (type3!=ESM::VT_Unknown) add (type3); } CSVWorld::CommandDelegate *CSVWorld::VarTypeDelegateFactory::makeDelegate ( CSMWorld::CommandDispatcher *dispatcher, CSMDoc::Document& document, QObject *parent) const { return new VarTypeDelegate (mValues, dispatcher, document, parent); } void CSVWorld::VarTypeDelegateFactory::add (ESM::VarType type) { std::vector> enums = CSMWorld::Columns::getEnums (CSMWorld::Columns::ColumnId_ValueType); if (static_cast(type) >= enums.size()) throw std::logic_error ("Unsupported variable type"); mValues.emplace_back(type, QString::fromUtf8 (enums[type].second.c_str())); } ================================================ FILE: apps/opencs/view/world/vartypedelegate.hpp ================================================ #ifndef CSV_WORLD_VARTYPEDELEGATE_H #define CSV_WORLD_VARTYPEDELEGATE_H #include #include "enumdelegate.hpp" namespace CSVWorld { class VarTypeDelegate : public EnumDelegate { private: void addCommands (QAbstractItemModel *model, const QModelIndex& index, int type) const override; public: VarTypeDelegate (const std::vector >& values, CSMWorld::CommandDispatcher *dispatcher, CSMDoc::Document& document, QObject *parent); }; class VarTypeDelegateFactory : public CommandDelegateFactory { std::vector > mValues; public: VarTypeDelegateFactory (ESM::VarType type0 = ESM::VT_Unknown, ESM::VarType type1 = ESM::VT_Unknown, ESM::VarType type2 = ESM::VT_Unknown, ESM::VarType type3 = ESM::VT_Unknown); CommandDelegate *makeDelegate (CSMWorld::CommandDispatcher *dispatcher, CSMDoc::Document& document, QObject *parent) const override; ///< The ownership of the returned CommandDelegate is transferred to the caller. void add (ESM::VarType type); }; } #endif ================================================ FILE: apps/openmw/CMakeLists.txt ================================================ # local files set(GAME main.cpp engine.cpp ${CMAKE_SOURCE_DIR}/files/tes3mp/tes3mp.rc ${CMAKE_SOURCE_DIR}/files/tes3mp/tes3mp.exe.manifest ) if (ANDROID) set(GAME ${GAME} android_main.cpp) endif() set(GAME_HEADER engine.hpp ) source_group(game FILES ${GAME} ${GAME_HEADER}) add_openmw_dir (mwrender actors objects renderingmanager animation rotatecontroller sky npcanimation vismask creatureanimation effectmanager util renderinginterface pathgrid rendermode weaponanimation screenshotmanager bulletdebugdraw globalmap characterpreview camera viewovershoulder localmap water terrainstorage ripplesimulation renderbin actoranimation landmanager navmesh actorspaths recastmesh fogmanager objectpaging groundcover ) add_openmw_dir (mwinput actions actionmanager bindingsmanager controllermanager controlswitch inputmanagerimp mousemanager keyboardmanager sdlmappings sensormanager ) add_openmw_dir (mwgui layout textinput widgets race class birth review windowmanagerimp console dialogue windowbase statswindow messagebox journalwindow charactercreation mapwindow windowpinnablebase tooltips scrollwindow bookwindow resourceskin formatting inventorywindow container hud countdialog tradewindow settingswindow confirmationdialog alchemywindow referenceinterface spellwindow mainmenu quickkeysmenu itemselection spellbuyingwindow loadingscreen levelupdialog waitdialog spellcreationdialog enchantingdialog trainingwindow travelwindow exposedwindow cursor spellicons merchantrepair repair soulgemdialog companionwindow bookpage journalviewmodel journalbooks itemmodel containeritemmodel inventoryitemmodel sortfilteritemmodel itemview tradeitemmodel companionitemmodel pickpocketitemmodel controllers savegamedialog recharge mode videowidget backgroundimage itemwidget screenfader debugwindow spellmodel spellview draganddrop timeadvancer jailscreen itemchargeview keyboardnavigation textcolours statswatcher ) add_openmw_dir (mwdialogue dialoguemanagerimp journalimp journalentry quest topic filter selectwrapper hypertextparser keywordsearch scripttest ) add_openmw_dir (mwscript locals scriptmanagerimp compilercontext interpretercontext cellextensions miscextensions guiextensions soundextensions skyextensions statsextensions containerextensions aiextensions controlextensions extensions globalscripts ref dialogueextensions animationextensions transformationextensions consoleextensions userextensions ) add_openmw_dir (mwsound soundmanagerimp openal_output ffmpeg_decoder sound sound_buffer sound_decoder sound_output loudness movieaudiofactory alext efx efx-presets regionsoundselector watersoundupdater volumesettings ) add_openmw_dir (mwworld refdata worldimp scene globals class action nullaction actionteleport containerstore actiontalk actiontake manualref player cellvisitors failedaction cells localscripts customdata inventorystore ptr actionopen actionread actionharvest actionequip timestamp actionalchemy cellstore actionapply actioneat store esmstore recordcmp fallback actionrepair actionsoulgem livecellref actiondoor contentloader esmloader actiontrap cellreflist cellref weather projectilemanager cellpreloader datetimemanager ) add_openmw_dir (mwphysics physicssystem trace collisiontype actor convert object heightfield closestnotmerayresultcallback contacttestresultcallback deepestnotmecontacttestresultcallback stepper movementsolver projectile actorconvexcallback raycasting mtphysics contacttestwrapper projectileconvexcallback ) add_openmw_dir (mwclass classes activator creature npc weapon armor potion apparatus book clothing container door ingredient creaturelevlist itemlevlist light lockpick misc probe repair static actor bodypart ) add_openmw_dir (mwmechanics mechanicsmanagerimp stat creaturestats magiceffects movement actorutil spelllist drawstate spells activespells npcstats aipackage aisequence aipursue alchemy aiwander aitravel aifollow aiavoiddoor aibreathe aicast aiescort aiface aiactivate aicombat recharge repair enchanting pathfinding pathgrid security spellcasting spellresistance disease pickpocket levelledlist combat steering obstacle autocalcspell difficultyscaling aicombataction actor summoning character actors objects aistate trading weaponpriority spellpriority weapontype spellutil tickableeffects spellabsorption linkedeffects ) add_openmw_dir (mwstate statemanagerimp charactermanager character quicksavemanager ) add_openmw_dir (mwbase environment world scriptmanager dialoguemanager journal soundmanager mechanicsmanager inputmanager windowmanager statemanager ) add_openmw_dir (mwmp Main Networking LocalSystem LocalPlayer DedicatedPlayer PlayerList LocalActor DedicatedActor ActorList ObjectList Worldstate Cell CellController GUIController MechanicsHelper RecordHelper ScriptController ) add_openmw_dir (mwmp/GUI GUIChat GUILogin PlayerMarkerCollection GUIDialogList TextInputDialog ) add_openmw_dir(mwmp/processors BaseClientPacketProcessor SystemProcessor PlayerProcessor ObjectProcessor ActorProcessor WorldstateProcessor ProcessorInitializer ) add_openmw_dir (mwmp/processors/system ProcessorSystemHandshake ) add_openmw_dir (mwmp/processors/actor ProcessorActorAI ProcessorActorAnimFlags ProcessorActorAnimPlay ProcessorActorAttack ProcessorActorAuthority ProcessorActorCast ProcessorActorCellChange ProcessorActorDeath ProcessorActorEquipment ProcessorActorList ProcessorActorPosition ProcessorActorSpeech ProcessorActorSpellsActive ProcessorActorStatsDynamic ProcessorActorTest ) add_openmw_dir (mwmp/processors/player ProcessorChatMessage ProcessorGUIMessageBox ProcessorUserDisconnected ProcessorUserMyID ProcessorGameSettings ProcessorPlayerAlly ProcessorPlayerAnimFlags ProcessorPlayerAnimPlay ProcessorPlayerAttack ProcessorPlayerAttribute ProcessorPlayerBaseInfo ProcessorPlayerBehavior ProcessorPlayerBook ProcessorPlayerBounty ProcessorPlayerCast ProcessorPlayerCellChange ProcessorPlayerCellState ProcessorPlayerCharClass ProcessorPlayerCharGen ProcessorPlayerCooldowns ProcessorPlayerDeath ProcessorPlayerDisposition ProcessorPlayerEquipment ProcessorPlayerFaction ProcessorPlayerInput ProcessorPlayerInventory ProcessorPlayerItemUse ProcessorPlayerJail ProcessorPlayerJournal ProcessorPlayerLevel ProcessorPlayerMiscellaneous ProcessorPlayerMomentum ProcessorPlayerPosition ProcessorPlayerQuickKeys ProcessorPlayerReputation ProcessorPlayerResurrect ProcessorPlayerShapeshift ProcessorPlayerSkill ProcessorPlayerSpeech ProcessorPlayerSpellbook ProcessorPlayerSpellsActive ProcessorPlayerStatsDynamic ProcessorPlayerTopic ) add_openmw_dir (mwmp/processors/object BaseObjectProcessor ProcessorConsoleCommand ProcessorContainer ProcessorDoorDestination ProcessorDoorState ProcessorMusicPlay ProcessorVideoPlay ProcessorObjectActivate ProcessorObjectAnimPlay ProcessorObjectAttach ProcessorObjectDelete ProcessorObjectDialogueChoice ProcessorObjectHit ProcessorObjectLock ProcessorObjectMove ProcessorObjectPlace ProcessorObjectRestock ProcessorObjectRotate ProcessorObjectScale ProcessorObjectSound ProcessorObjectSpawn ProcessorObjectState ProcessorObjectTrap ProcessorClientScriptLocal ProcessorScriptMemberShort ProcessorObjectMiscellaneous ) add_openmw_dir (mwmp/processors/worldstate ProcessorCellReset ProcessorClientScriptGlobal ProcessorClientScriptSettings ProcessorRecordDynamic ProcessorWorldCollisionOverride ProcessorWorldDestinationOverride ProcessorWorldKillCount ProcessorWorldMap ProcessorWorldRegionAuthority ProcessorWorldTime ProcessorWorldWeather ) # Main executable if (NOT ANDROID) openmw_add_executable(tes3mp ${OPENMW_FILES} ${GAME} ${GAME_HEADER} ${APPLE_BUNDLE_RESOURCES} ) else () add_library(tes3mp SHARED ${OPENMW_FILES} ${GAME} ${GAME_HEADER} ) endif () # Sound stuff - here so CMake doesn't stupidly recompile EVERYTHING # when we change the backend. include_directories( ${FFmpeg_INCLUDE_DIRS} ) target_link_libraries(tes3mp # CMake's built-in OSG finder does not use pkgconfig, so we have to # manually ensure the order is correct for inter-library dependencies. # This only makes a difference with `-DOPENMW_USE_SYSTEM_OSG=ON -DOSG_STATIC=ON`. # https://gitlab.kitware.com/cmake/cmake/-/issues/21701 ${OSGPARTICLE_LIBRARIES} ${OSGVIEWER_LIBRARIES} ${OSGGA_LIBRARIES} ${OSGSHADOW_LIBRARIES} ${OSGDB_LIBRARIES} ${OSGUTIL_LIBRARIES} ${OSG_LIBRARIES} ${Boost_SYSTEM_LIBRARY} ${Boost_THREAD_LIBRARY} ${Boost_FILESYSTEM_LIBRARY} ${Boost_PROGRAM_OPTIONS_LIBRARY} ${OPENAL_LIBRARY} ${FFmpeg_LIBRARIES} ${MyGUI_LIBRARIES} ${SDL2_LIBRARY} ${RecastNavigation_LIBRARIES} "osg-ffmpeg-videoplayer" "oics" components ${RakNet_LIBRARY} ) if(OSG_STATIC) unset(_osg_plugins_static_files) add_library(openmw_osg_plugins INTERFACE) foreach(_plugin ${USED_OSG_PLUGINS}) string(TOUPPER ${_plugin} _plugin_uc) if(OPENMW_USE_SYSTEM_OSG) list(APPEND _osg_plugins_static_files ${${_plugin_uc}_LIBRARY}) else() list(APPEND _osg_plugins_static_files $) target_link_libraries(openmw_osg_plugins INTERFACE $) add_dependencies(openmw_osg_plugins ${${_plugin_uc}_LIBRARY}) endif() endforeach() # We use --whole-archive because OSG plugins use registration. get_whole_archive_options(_opts ${_osg_plugins_static_files}) target_link_options(openmw_osg_plugins INTERFACE ${_opts}) target_link_libraries(tes3mp openmw_osg_plugins) if(OPENMW_USE_SYSTEM_OSG) # OSG plugin pkgconfig files are missing these dependencies. # https://github.com/openscenegraph/OpenSceneGraph/issues/1052 target_link_libraries(tes3mp freetype jpeg png) endif() endif(OSG_STATIC) if (ANDROID) target_link_libraries(tes3mp EGL android log z) endif (ANDROID) if (USE_SYSTEM_TINYXML) target_link_libraries(tes3mp ${TinyXML_LIBRARIES}) endif() if (NOT UNIX) target_link_libraries(tes3mp ${SDL2MAIN_LIBRARY}) endif() # Fix for not visible pthreads functions for linker with glibc 2.15 if (UNIX AND NOT APPLE) target_link_libraries(tes3mp ${CMAKE_THREAD_LIBS_INIT}) 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}) add_subdirectory(../../files/ ${CMAKE_CURRENT_BINARY_DIR}/files) configure_file("${OpenMW_BINARY_DIR}/defaults.bin" ${BUNDLE_RESOURCES_DIR} COPYONLY) configure_file("${OpenMW_BINARY_DIR}/openmw.cfg" ${BUNDLE_RESOURCES_DIR} COPYONLY) configure_file("${OpenMW_BINARY_DIR}/gamecontrollerdb.txt" ${BUNDLE_RESOURCES_DIR} COPYONLY) add_custom_command(TARGET tes3mp POST_BUILD COMMAND cp "${OpenMW_BINARY_DIR}/resources/version" "${BUNDLE_RESOURCES_DIR}/resources") find_library(COCOA_FRAMEWORK Cocoa) find_library(IOKIT_FRAMEWORK IOKit) target_link_libraries(tes3mp ${COCOA_FRAMEWORK} ${IOKIT_FRAMEWORK}) if (FFmpeg_FOUND) find_library(COREVIDEO_FRAMEWORK CoreVideo) find_library(VDA_FRAMEWORK VideoDecodeAcceleration) target_link_libraries(tes3mp z ${COREVIDEO_FRAMEWORK} ${VDA_FRAMEWORK}) endif() endif(APPLE) if (BUILD_WITH_CODE_COVERAGE) add_definitions (--coverage) target_link_libraries(tes3mp gcov) endif() if (MSVC) # Debug version needs increased number of sections beyond 2^16 if (CMAKE_CL_64) set (CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} /bigobj") endif (CMAKE_CL_64) endif (MSVC) if (WIN32) INSTALL(TARGETS tes3mp RUNTIME DESTINATION ".") endif (WIN32) ================================================ FILE: apps/openmw/android_main.cpp ================================================ int stderr = 0; // Hack: fix linker error #include "SDL_main.h" #include #include #include /******************************************************************************* Functions called by JNI *******************************************************************************/ #include /* Called before to initialize JNI bindings */ extern void SDL_Android_Init(JNIEnv* env, jclass cls); extern int argcData; extern const char **argvData; void releaseArgv(); extern "C" int Java_org_libsdl_app_SDLActivity_getMouseX(JNIEnv *env, jclass cls, jobject obj) { int ret = 0; SDL_GetMouseState(&ret, nullptr); return ret; } extern "C" int Java_org_libsdl_app_SDLActivity_getMouseY(JNIEnv *env, jclass cls, jobject obj) { int ret = 0; SDL_GetMouseState(nullptr, &ret); return ret; } extern "C" int Java_org_libsdl_app_SDLActivity_isMouseShown(JNIEnv *env, jclass cls, jobject obj) { return SDL_ShowCursor(SDL_QUERY); } extern SDL_Window *Android_Window; extern "C" int SDL_SendMouseMotion(SDL_Window * window, int mouseID, int relative, int x, int y); extern "C" void Java_org_libsdl_app_SDLActivity_sendRelativeMouseMotion(JNIEnv *env, jclass cls, int x, int y) { SDL_SendMouseMotion(Android_Window, 0, 1, x, y); } extern "C" int SDL_SendMouseButton(SDL_Window * window, int mouseID, Uint8 state, Uint8 button); extern "C" void Java_org_libsdl_app_SDLActivity_sendMouseButton(JNIEnv *env, jclass cls, int state, int button) { SDL_SendMouseButton(Android_Window, 0, state, button); } extern "C" int Java_org_libsdl_app_SDLActivity_nativeInit(JNIEnv* env, jclass cls, jobject obj) { setenv("OPENMW_DECOMPRESS_TEXTURES", "1", 1); // On Android, we use a virtual controller with guid="Virtual" SDL_GameControllerAddMapping("5669727475616c000000000000000000,Virtual,a:b0,b:b1,back:b15,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b16,leftshoulder:b6,leftstick:b13,lefttrigger:a5,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:a4,rightx:a2,righty:a3,start:b11,x:b3,y:b4"); return 0; } ================================================ FILE: apps/openmw/doc.hpp ================================================ // Note: This is not a regular source file. /// \ingroup apps /// \defgroup openmw OpenMW /// \namespace OMW /// \ingroup openmw /// \brief Integration of OpenMW-subsystems /// \namespace MWDialogue /// \ingroup openmw /// \brief NPC dialogues /// \namespace MWMechanics /// \ingroup openmw /// \brief Game mechanics and NPC-AI /// \namespace MWSound /// \ingroup openmw /// \brief Sound & music /// \namespace MWGUI /// \ingroup openmw /// \brief HUD and windows /// \namespace MWRender /// \ingroup openmw /// \brief Rendering /// \namespace MWWorld /// \ingroup openmw /// \brief World data /// \namespace MWClass /// \ingroup openmw /// \brief Workaround for non-OOP design of the record system /// \namespace MWInput /// \ingroup openmw /// \brief User input and character controls /// \namespace MWScript /// \ingroup openmw /// \brief MW-specific script extensions and integration of the script system into OpenMW ================================================ FILE: apps/openmw/engine.cpp ================================================ #include "engine.hpp" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include /* Start of tes3mp addition Include additional headers for multiplayer purposes */ #include #include "mwmp/Main.hpp" #include "mwmp/GUIController.hpp" /* End of tes3mp addition */ #include #include #include "mwinput/inputmanagerimp.hpp" #include "mwgui/windowmanagerimp.hpp" #include "mwscript/scriptmanagerimp.hpp" #include "mwscript/interpretercontext.hpp" #include "mwsound/soundmanagerimp.hpp" #include "mwworld/class.hpp" #include "mwworld/player.hpp" #include "mwworld/worldimp.hpp" #include "mwrender/vismask.hpp" #include "mwclass/classes.hpp" #include "mwdialogue/dialoguemanagerimp.hpp" #include "mwdialogue/journalimp.hpp" #include "mwdialogue/scripttest.hpp" #include "mwmechanics/mechanicsmanagerimp.hpp" #include "mwstate/statemanagerimp.hpp" namespace { void checkSDLError(int ret) { if (ret != 0) Log(Debug::Error) << "SDL error: " << SDL_GetError(); } struct UserStats { const std::string mLabel; const std::string mBegin; const std::string mEnd; const std::string mTaken; UserStats(const std::string& label, const std::string& prefix) : mLabel(label), mBegin(prefix + "_time_begin"), mEnd(prefix + "_time_end"), mTaken(prefix + "_time_taken") {} }; enum class UserStatsType : std::size_t { Input, Sound, State, Script, Mechanics, Physics, PhysicsWorker, World, Gui, Number, }; template struct UserStatsValue { static const UserStats sValue; }; template <> const UserStats UserStatsValue::sValue {"Input", "input"}; template <> const UserStats UserStatsValue::sValue {"Sound", "sound"}; template <> const UserStats UserStatsValue::sValue {"State", "state"}; template <> const UserStats UserStatsValue::sValue {"Script", "script"}; template <> const UserStats UserStatsValue::sValue {"Mech", "mechanics"}; template <> const UserStats UserStatsValue::sValue {"Phys", "physics"}; template <> const UserStats UserStatsValue::sValue {" -Async", "physicsworker"}; template <> const UserStats UserStatsValue::sValue {"World", "world"}; template <> const UserStats UserStatsValue::sValue {"Gui", "gui"}; template struct ForEachUserStatsValue { template static void apply(F&& f) { f(UserStatsValue::sValue); using Next = ForEachUserStatsValue(static_cast(type) + 1)>; Next::apply(std::forward(f)); } }; template <> struct ForEachUserStatsValue { template static void apply(F&&) {} }; template void forEachUserStatsValue(F&& f) { ForEachUserStatsValue(0)>::apply(std::forward(f)); } template class ScopedProfile { public: ScopedProfile(osg::Timer_t frameStart, unsigned int frameNumber, const osg::Timer& timer, osg::Stats& stats) : mScopeStart(timer.tick()), mFrameStart(frameStart), mFrameNumber(frameNumber), mTimer(timer), mStats(stats) { } ScopedProfile(const ScopedProfile&) = delete; ScopedProfile& operator=(const ScopedProfile&) = delete; ~ScopedProfile() { if (!mStats.collectStats("engine")) return; const osg::Timer_t end = mTimer.tick(); const UserStats& stats = UserStatsValue::sValue; mStats.setAttribute(mFrameNumber, stats.mBegin, mTimer.delta_s(mFrameStart, mScopeStart)); mStats.setAttribute(mFrameNumber, stats.mTaken, mTimer.delta_s(mScopeStart, end)); mStats.setAttribute(mFrameNumber, stats.mEnd, mTimer.delta_s(mFrameStart, end)); } private: const osg::Timer_t mScopeStart; const osg::Timer_t mFrameStart; const unsigned int mFrameNumber; const osg::Timer& mTimer; osg::Stats& mStats; }; void initStatsHandler(Resource::Profiler& profiler) { const osg::Vec4f textColor(1.f, 1.f, 1.f, 1.f); const osg::Vec4f barColor(1.f, 1.f, 1.f, 1.f); const float multiplier = 1000; const bool average = true; const bool averageInInverseSpace = false; const float maxValue = 10000; forEachUserStatsValue([&] (const UserStats& v) { profiler.addUserStatsLine(v.mLabel, textColor, barColor, v.mTaken, multiplier, average, averageInInverseSpace, v.mBegin, v.mEnd, maxValue); }); // the forEachUserStatsValue loop is "run" at compile time, hence the settings manager is not available. // Unconditionnally add the async physics stats, and then remove it at runtime if necessary if (Settings::Manager::getInt("async num threads", "Physics") == 0) profiler.removeUserStatsLine(" -Async"); } } void OMW::Engine::executeLocalScripts() { MWWorld::LocalScripts& localScripts = mEnvironment.getWorld()->getLocalScripts(); localScripts.startIteration(); std::pair script; while (localScripts.getNext(script)) { MWScript::InterpreterContext interpreterContext ( &script.second.getRefData().getLocals(), script.second); /* Start of tes3mp addition By comparing its name with a list of script names, check if this script is allowed to send packets about its value changes If it is, set a tes3mp-only boolean to true in its interpreterContext */ if (mwmp::Main::isValidPacketScript(script.first)) { interpreterContext.sendPackets = true; } /* End of tes3mp addition */ /* Start of tes3mp addition Mark this InterpreterContext as having a SCRIPT_LOCAL context and as currently running the script with this name, so that packets sent by the Interpreter can have their origin determined by serverside scripts */ interpreterContext.trackContextType(Interpreter::Context::SCRIPT_LOCAL); interpreterContext.trackCurrentScriptName(script.first); /* End of tes3mp addition */ mEnvironment.getScriptManager()->run (script.first, interpreterContext); } } bool OMW::Engine::frame(float frametime) { try { const osg::Timer_t frameStart = mViewer->getStartTick(); const unsigned int frameNumber = mViewer->getFrameStamp()->getFrameNumber(); const osg::Timer* const timer = osg::Timer::instance(); osg::Stats* const stats = mViewer->getViewerStats(); mEnvironment.setFrameDuration(frametime); // update input { ScopedProfile profile(frameStart, frameNumber, *timer, *stats); mEnvironment.getInputManager()->update(frametime, false); } // When the window is minimized, pause the game. Currently this *has* to be here to work around a MyGUI bug. // If we are not currently rendering, then RenderItems will not be reused resulting in a memory leak upon changing widget textures (fixed in MyGUI 3.3.2), // and destroyed widgets will not be deleted (not fixed yet, https://github.com/MyGUI/mygui/issues/21) { ScopedProfile profile(frameStart, frameNumber, *timer, *stats); if (!mEnvironment.getWindowManager()->isWindowVisible()) { mEnvironment.getSoundManager()->pausePlayback(); /* Start of tes3mp change (major) The game cannot be paused in multiplayer, so prevent that from happening even here */ //return false; /* End of tes3mp change (major) */ } else mEnvironment.getSoundManager()->resumePlayback(); // sound if (mUseSound) mEnvironment.getSoundManager()->update(frametime); } /* Start of tes3mp addition Update multiplayer processing for the current frame */ mwmp::Main::frame(frametime); /* End of tes3mp addition */ // Main menu opened? Then scripts are also paused. bool paused = mEnvironment.getWindowManager()->containsMode(MWGui::GM_MainMenu); /* Start of tes3mp change (major) Time should not be frozen in multiplayer, so the paused boolean is always set to false instead */ paused = false; /* End of tes3mp change (major) */ // update game state { ScopedProfile profile(frameStart, frameNumber, *timer, *stats); mEnvironment.getStateManager()->update (frametime); } /* Start of tes3mp change (major) Whether the GUI is active should have no relevance in multiplayer, so the guiActive boolean is always set to false instead */ //bool guiActive = mEnvironment.getWindowManager()->isGuiMode(); bool guiActive = false; /* End of tes3mp change (major) */ { ScopedProfile profile(frameStart, frameNumber, *timer, *stats); if (mEnvironment.getStateManager()->getState() != MWBase::StateManager::State_NoGame) { if (!paused) { if (mEnvironment.getWorld()->getScriptsEnabled()) { // local scripts executeLocalScripts(); // global scripts mEnvironment.getScriptManager()->getGlobalScripts().run(); } mEnvironment.getWorld()->markCellAsUnchanged(); } if (!guiActive) { double hours = (frametime * mEnvironment.getWorld()->getTimeScaleFactor()) / 3600.0; mEnvironment.getWorld()->advanceTime(hours, true); mEnvironment.getWorld()->rechargeItems(frametime, true); } } } // update mechanics { ScopedProfile profile(frameStart, frameNumber, *timer, *stats); if (mEnvironment.getStateManager()->getState() != MWBase::StateManager::State_NoGame) { mEnvironment.getMechanicsManager()->update(frametime, guiActive); } if (mEnvironment.getStateManager()->getState() == MWBase::StateManager::State_Running) { MWWorld::Ptr player = mEnvironment.getWorld()->getPlayerPtr(); /* Start of tes3mp change (major) In multiplayer, the game should not end when the player dies, so the code here has been commented out */ //if(!guiActive && player.getClass().getCreatureStats(player).isDead()) // mEnvironment.getStateManager()->endGame(); /* End of tes3mp change (major) */ } } // update physics { ScopedProfile profile(frameStart, frameNumber, *timer, *stats); if (mEnvironment.getStateManager()->getState() != MWBase::StateManager::State_NoGame) { mEnvironment.getWorld()->updatePhysics(frametime, guiActive, frameStart, frameNumber, *stats); } } // update world { ScopedProfile profile(frameStart, frameNumber, *timer, *stats); if (mEnvironment.getStateManager()->getState() != MWBase::StateManager::State_NoGame) { mEnvironment.getWorld()->update(frametime, guiActive); } } // update GUI { ScopedProfile profile(frameStart, frameNumber, *timer, *stats); mEnvironment.getWindowManager()->update(frametime); } if (stats->collectStats("resource")) { stats->setAttribute(frameNumber, "FrameNumber", frameNumber); mResourceSystem->reportStats(frameNumber, stats); stats->setAttribute(frameNumber, "WorkQueue", mWorkQueue->getNumItems()); stats->setAttribute(frameNumber, "WorkThread", mWorkQueue->getNumActiveThreads()); mEnvironment.reportStats(frameNumber, *stats); } } catch (const std::exception& e) { Log(Debug::Error) << "Error in frame: " << e.what(); } return true; } OMW::Engine::Engine(Files::ConfigurationManager& configurationManager) : mWindow(nullptr) , mEncoding(ToUTF8::WINDOWS_1252) , mEncoder(nullptr) , mScreenCaptureOperation(nullptr) , mSkipMenu (false) , mUseSound (true) , mCompileAll (false) , mCompileAllDialogue (false) , mWarningsMode (1) , mScriptConsoleMode (false) , mActivationDistanceOverride(-1) , mGrab(true) , mExportFonts(false) , mRandomSeed(0) , mScriptContext (nullptr) , mFSStrict (false) , mScriptBlacklistUse (true) , mNewGame (false) , mCfgMgr(configurationManager) { SDL_SetHint(SDL_HINT_ACCELEROMETER_AS_JOYSTICK, "0"); // We use only gamepads Uint32 flags = SDL_INIT_VIDEO|SDL_INIT_NOPARACHUTE|SDL_INIT_GAMECONTROLLER|SDL_INIT_JOYSTICK|SDL_INIT_SENSOR; if(SDL_WasInit(flags) == 0) { SDL_SetMainReady(); if(SDL_Init(flags) != 0) { throw std::runtime_error("Could not initialize SDL! " + std::string(SDL_GetError())); } } } OMW::Engine::~Engine() { /* Start of tes3mp addition Free up memory allocated by multiplayer's GUIController, but make sure mwmp::Main has actually been initialized */ if (mwmp::Main::isInitialized()) mwmp::Main::get().getGUIController()->cleanUp(); /* End of tes3mp addition */ mEnvironment.cleanup(); /* Start of tes3mp addition Free up memory allocated by multiplayer's Main class */ mwmp::Main::destroy(); /* End of tes3mp addition */ delete mScriptContext; mScriptContext = nullptr; mWorkQueue = nullptr; mViewer = nullptr; mResourceSystem.reset(); delete mEncoder; mEncoder = nullptr; if (mWindow) { SDL_DestroyWindow(mWindow); mWindow = nullptr; } SDL_Quit(); /* Start of tes3mp addition Free up memory allocated by multiplayer's logger */ LOG_QUIT(); /* End of tes3mp addition */ } void OMW::Engine::enableFSStrict(bool fsStrict) { mFSStrict = fsStrict; } // Set data dir void OMW::Engine::setDataDirs (const Files::PathContainer& dataDirs) { mDataDirs = dataDirs; mDataDirs.insert(mDataDirs.begin(), (mResDir / "vfs")); mFileCollections = Files::Collections (mDataDirs, !mFSStrict); } // Add BSA archive void OMW::Engine::addArchive (const std::string& archive) { mArchives.push_back(archive); } // Set resource dir void OMW::Engine::setResourceDir (const boost::filesystem::path& parResDir) { mResDir = parResDir; } // Set start cell name void OMW::Engine::setCell (const std::string& cellName) { mCellName = cellName; } void OMW::Engine::addContentFile(const std::string& file) { mContentFiles.push_back(file); } void OMW::Engine::addGroundcoverFile(const std::string& file) { mGroundcoverFiles.emplace_back(file); } void OMW::Engine::setSkipMenu (bool skipMenu, bool newGame) { mSkipMenu = skipMenu; mNewGame = newGame; } std::string OMW::Engine::loadSettings (Settings::Manager & settings) { // Create the settings manager and load default settings file const std::string localdefault = (mCfgMgr.getLocalPath() / "defaults.bin").string(); const std::string globaldefault = (mCfgMgr.getGlobalPath() / "defaults.bin").string(); // prefer local 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."); // load user settings if they exist const std::string settingspath = (mCfgMgr.getUserConfigPath() / "settings.cfg").string(); if (boost::filesystem::exists(settingspath)) settings.loadUser(settingspath); return settingspath; } void OMW::Engine::createWindow(Settings::Manager& settings) { int screen = settings.getInt("screen", "Video"); int width = settings.getInt("resolution x", "Video"); int height = settings.getInt("resolution y", "Video"); bool fullscreen = settings.getBool("fullscreen", "Video"); bool windowBorder = settings.getBool("window border", "Video"); bool vsync = settings.getBool("vsync", "Video"); unsigned int antialiasing = std::max(0, settings.getInt("antialiasing", "Video")); int pos_x = SDL_WINDOWPOS_CENTERED_DISPLAY(screen), pos_y = SDL_WINDOWPOS_CENTERED_DISPLAY(screen); if(fullscreen) { pos_x = SDL_WINDOWPOS_UNDEFINED_DISPLAY(screen); pos_y = SDL_WINDOWPOS_UNDEFINED_DISPLAY(screen); } Uint32 flags = SDL_WINDOW_OPENGL|SDL_WINDOW_SHOWN|SDL_WINDOW_RESIZABLE; if(fullscreen) flags |= SDL_WINDOW_FULLSCREEN; if (!windowBorder) flags |= SDL_WINDOW_BORDERLESS; SDL_SetHint(SDL_HINT_VIDEO_MINIMIZE_ON_FOCUS_LOSS, settings.getBool("minimize on focus loss", "Video") ? "1" : "0"); checkSDLError(SDL_GL_SetAttribute(SDL_GL_RED_SIZE, 8)); checkSDLError(SDL_GL_SetAttribute(SDL_GL_GREEN_SIZE, 8)); checkSDLError(SDL_GL_SetAttribute(SDL_GL_BLUE_SIZE, 8)); checkSDLError(SDL_GL_SetAttribute(SDL_GL_ALPHA_SIZE, 0)); checkSDLError(SDL_GL_SetAttribute(SDL_GL_DEPTH_SIZE, 24)); if (Debug::shouldDebugOpenGL()) checkSDLError(SDL_GL_SetAttribute(SDL_GL_CONTEXT_FLAGS, SDL_GL_CONTEXT_DEBUG_FLAG)); if (antialiasing > 0) { checkSDLError(SDL_GL_SetAttribute(SDL_GL_MULTISAMPLEBUFFERS, 1)); checkSDLError(SDL_GL_SetAttribute(SDL_GL_MULTISAMPLESAMPLES, antialiasing)); } osg::ref_ptr graphicsWindow; while (!graphicsWindow || !graphicsWindow->valid()) { while (!mWindow) { /* Start of tes3mp change (major) Rename the window into TES3MP */ mWindow = SDL_CreateWindow("TES3MP", pos_x, pos_y, width, height, flags); /* End of tes3mp change (major) */ if (!mWindow) { // Try with a lower AA if (antialiasing > 0) { Log(Debug::Warning) << "Warning: " << antialiasing << "x antialiasing not supported, trying " << antialiasing/2; antialiasing /= 2; Settings::Manager::setInt("antialiasing", "Video", antialiasing); checkSDLError(SDL_GL_SetAttribute(SDL_GL_MULTISAMPLESAMPLES, antialiasing)); continue; } else { std::stringstream error; error << "Failed to create SDL window: " << SDL_GetError(); throw std::runtime_error(error.str()); } } } setWindowIcon(); osg::ref_ptr traits = new osg::GraphicsContext::Traits; SDL_GetWindowPosition(mWindow, &traits->x, &traits->y); SDL_GetWindowSize(mWindow, &traits->width, &traits->height); traits->windowName = SDL_GetWindowTitle(mWindow); traits->windowDecoration = !(SDL_GetWindowFlags(mWindow)&SDL_WINDOW_BORDERLESS); traits->screenNum = SDL_GetWindowDisplayIndex(mWindow); traits->vsync = vsync; traits->inheritedWindowData = new SDLUtil::GraphicsWindowSDL2::WindowData(mWindow); graphicsWindow = new SDLUtil::GraphicsWindowSDL2(traits); if (!graphicsWindow->valid()) throw std::runtime_error("Failed to create GraphicsContext"); if (traits->samples < antialiasing) { Log(Debug::Warning) << "Warning: Framebuffer MSAA level is only " << traits->samples << "x instead of " << antialiasing << "x. Trying " << antialiasing / 2 << "x instead."; graphicsWindow->closeImplementation(); SDL_DestroyWindow(mWindow); mWindow = nullptr; antialiasing /= 2; Settings::Manager::setInt("antialiasing", "Video", antialiasing); checkSDLError(SDL_GL_SetAttribute(SDL_GL_MULTISAMPLESAMPLES, antialiasing)); continue; } if (traits->red < 8) Log(Debug::Warning) << "Warning: Framebuffer only has a " << traits->red << " bit red channel."; if (traits->green < 8) Log(Debug::Warning) << "Warning: Framebuffer only has a " << traits->green << " bit green channel."; if (traits->blue < 8) Log(Debug::Warning) << "Warning: Framebuffer only has a " << traits->blue << " bit blue channel."; if (traits->depth < 24) Log(Debug::Warning) << "Warning: Framebuffer only has " << traits->depth << " bits of depth precision."; traits->alpha = 0; // set to 0 to stop ScreenCaptureHandler reading the alpha channel } osg::ref_ptr camera = mViewer->getCamera(); camera->setGraphicsContext(graphicsWindow); camera->setViewport(0, 0, graphicsWindow->getTraits()->width, graphicsWindow->getTraits()->height); if (Debug::shouldDebugOpenGL()) mViewer->setRealizeOperation(new Debug::EnableGLDebugOperation()); mViewer->realize(); mViewer->getEventQueue()->getCurrentEventState()->setWindowRectangle(0, 0, graphicsWindow->getTraits()->width, graphicsWindow->getTraits()->height); } void OMW::Engine::setWindowIcon() { boost::filesystem::ifstream windowIconStream; /* Start of tes3mp change (major) Use TES3MP's logo for the window icon */ std::string windowIcon = (mResDir / "mygui" / "tes3mp_logo.png").string(); /* End of tes3mp change (major) */ windowIconStream.open(windowIcon, std::ios_base::in | std::ios_base::binary); if (windowIconStream.fail()) Log(Debug::Error) << "Error: Failed to open " << windowIcon; osgDB::ReaderWriter* reader = osgDB::Registry::instance()->getReaderWriterForExtension("png"); if (!reader) { Log(Debug::Error) << "Error: Failed to read window icon, no png readerwriter found"; return; } osgDB::ReaderWriter::ReadResult result = reader->readImage(windowIconStream); if (!result.success()) Log(Debug::Error) << "Error: Failed to read " << windowIcon << ": " << result.message() << " code " << result.status(); else { osg::ref_ptr image = result.getImage(); auto surface = SDLUtil::imageToSurface(image, true); SDL_SetWindowIcon(mWindow, surface.get()); } } void OMW::Engine::prepareEngine (Settings::Manager & settings) { mEnvironment.setStateManager ( new MWState::StateManager (mCfgMgr.getUserDataPath() / "saves", mContentFiles.at (0))); createWindow(settings); osg::ref_ptr rootNode (new osg::Group); mViewer->setSceneData(rootNode); mVFS.reset(new VFS::Manager(mFSStrict)); VFS::registerArchives(mVFS.get(), mFileCollections, mArchives, true); mResourceSystem.reset(new Resource::ResourceSystem(mVFS.get())); mResourceSystem->getSceneManager()->setUnRefImageDataAfterApply(false); // keep to Off for now to allow better state sharing mResourceSystem->getSceneManager()->setFilterSettings( Settings::Manager::getString("texture mag filter", "General"), Settings::Manager::getString("texture min filter", "General"), Settings::Manager::getString("texture mipmap", "General"), Settings::Manager::getInt("anisotropy", "General") ); int numThreads = Settings::Manager::getInt("preload num threads", "Cells"); if (numThreads <= 0) throw std::runtime_error("Invalid setting: 'preload num threads' must be >0"); mWorkQueue = new SceneUtil::WorkQueue(numThreads); // Create input and UI first to set up a bootstrapping environment for // showing a loading screen and keeping the window responsive while doing so std::string keybinderUser = (mCfgMgr.getUserConfigPath() / "input_v3.xml").string(); bool keybinderUserExists = boost::filesystem::exists(keybinderUser); if(!keybinderUserExists) { std::string input2 = (mCfgMgr.getUserConfigPath() / "input_v2.xml").string(); if(boost::filesystem::exists(input2)) { boost::filesystem::copy_file(input2, keybinderUser); keybinderUserExists = boost::filesystem::exists(keybinderUser); Log(Debug::Info) << "Loading keybindings file: " << keybinderUser; } } else Log(Debug::Info) << "Loading keybindings file: " << keybinderUser; const std::string userdefault = mCfgMgr.getUserConfigPath().string() + "/gamecontrollerdb.txt"; const std::string localdefault = mCfgMgr.getLocalPath().string() + "/gamecontrollerdb.txt"; const std::string globaldefault = mCfgMgr.getGlobalPath().string() + "/gamecontrollerdb.txt"; std::string userGameControllerdb; if (boost::filesystem::exists(userdefault)){ userGameControllerdb = userdefault; } else userGameControllerdb = ""; std::string gameControllerdb; if (boost::filesystem::exists(localdefault)) gameControllerdb = localdefault; else if (boost::filesystem::exists(globaldefault)) gameControllerdb = globaldefault; else gameControllerdb = ""; //if it doesn't exist, pass in an empty string std::string myguiResources = (mResDir / "mygui").string(); osg::ref_ptr guiRoot = new osg::Group; guiRoot->setName("GUI Root"); guiRoot->setNodeMask(MWRender::Mask_GUI); rootNode->addChild(guiRoot); MWGui::WindowManager* window = new MWGui::WindowManager(mWindow, mViewer, guiRoot, mResourceSystem.get(), mWorkQueue.get(), mCfgMgr.getLogPath().string() + std::string("/"), myguiResources, mScriptConsoleMode, mTranslationDataStorage, mEncoding, mExportFonts, Version::getOpenmwVersionDescription(mResDir.string()), mCfgMgr.getUserConfigPath().string()); mEnvironment.setWindowManager (window); MWInput::InputManager* input = new MWInput::InputManager (mWindow, mViewer, mScreenCaptureHandler, mScreenCaptureOperation, keybinderUser, keybinderUserExists, userGameControllerdb, gameControllerdb, mGrab); mEnvironment.setInputManager (input); // Create sound system mEnvironment.setSoundManager (new MWSound::SoundManager(mVFS.get(), mUseSound)); if (!mSkipMenu) { const std::string& logo = Fallback::Map::getString("Movies_Company_Logo"); if (!logo.empty()) window->playVideo(logo, true); } // Create the world mEnvironment.setWorld( new MWWorld::World (mViewer, rootNode, mResourceSystem.get(), mWorkQueue.get(), mFileCollections, mContentFiles, mGroundcoverFiles, mEncoder, mActivationDistanceOverride, mCellName, mStartupScript, mResDir.string(), mCfgMgr.getUserDataPath().string())); mEnvironment.getWorld()->setupPlayer(); window->setStore(mEnvironment.getWorld()->getStore()); window->initUI(); //Load translation data mTranslationDataStorage.setEncoder(mEncoder); for (size_t i = 0; i < mContentFiles.size(); i++) mTranslationDataStorage.loadTranslationData(mFileCollections, mContentFiles[i]); Compiler::registerExtensions (mExtensions); // Create script system mScriptContext = new MWScript::CompilerContext (MWScript::CompilerContext::Type_Full); mScriptContext->setExtensions (&mExtensions); mEnvironment.setScriptManager (new MWScript::ScriptManager (mEnvironment.getWorld()->getStore(), *mScriptContext, mWarningsMode, mScriptBlacklistUse ? mScriptBlacklist : std::vector())); // Create game mechanics system MWMechanics::MechanicsManager* mechanics = new MWMechanics::MechanicsManager; mEnvironment.setMechanicsManager (mechanics); // Create dialog system mEnvironment.setJournal (new MWDialogue::Journal); mEnvironment.setDialogueManager (new MWDialogue::DialogueManager (mExtensions, mTranslationDataStorage)); mEnvironment.setResourceSystem(mResourceSystem.get()); // scripts if (mCompileAll) { std::pair result = mEnvironment.getScriptManager()->compileAll(); if (result.first) Log(Debug::Info) << "compiled " << result.second << " of " << result.first << " scripts (" << 100*static_cast (result.second)/result.first << "%)"; } if (mCompileAllDialogue) { std::pair result = MWDialogue::ScriptTest::compileAll(&mExtensions, mWarningsMode); if (result.first) Log(Debug::Info) << "compiled " << result.second << " of " << result.first << " dialogue script/actor combinations a(" << 100*static_cast (result.second)/result.first << "%)"; } } class WriteScreenshotToFileOperation : public osgViewer::ScreenCaptureHandler::CaptureOperation { public: WriteScreenshotToFileOperation(const std::string& screenshotPath, const std::string& screenshotFormat) : mScreenshotPath(screenshotPath) , mScreenshotFormat(screenshotFormat) { } void operator()(const osg::Image& image, const unsigned int context_id) override { // Count screenshots. int shotCount = 0; // Find the first unused filename with a do-while std::ostringstream stream; do { // Reset the stream stream.str(""); stream.clear(); stream << mScreenshotPath << "/screenshot" << std::setw(3) << std::setfill('0') << shotCount++ << "." << mScreenshotFormat; } while (boost::filesystem::exists(stream.str())); boost::filesystem::ofstream outStream; outStream.open(boost::filesystem::path(stream.str()), std::ios::binary); osgDB::ReaderWriter* readerwriter = osgDB::Registry::instance()->getReaderWriterForExtension(mScreenshotFormat); if (!readerwriter) { Log(Debug::Error) << "Error: Can't write screenshot, no '" << mScreenshotFormat << "' readerwriter found"; return; } osgDB::ReaderWriter::WriteResult result = readerwriter->writeImage(image, outStream); if (!result.success()) { Log(Debug::Error) << "Error: Can't write screenshot: " << result.message() << " code " << result.status(); } } private: std::string mScreenshotPath; std::string mScreenshotFormat; }; // Initialise and enter main loop. void OMW::Engine::go() { assert (!mContentFiles.empty()); /* Start of tes3mp change (major) Attempt multiplayer initialization and proceed no further if it fails */ if (!mwmp::Main::init(mContentFiles, mFileCollections)) return; /* End of tes3mp change (major) */ Log(Debug::Info) << "OSG version: " << osgGetVersion(); SDL_version sdlVersion; SDL_GetVersion(&sdlVersion); Log(Debug::Info) << "SDL version: " << (int)sdlVersion.major << "." << (int)sdlVersion.minor << "." << (int)sdlVersion.patch; Misc::Rng::init(mRandomSeed); // Load settings Settings::Manager settings; std::string settingspath; settingspath = loadSettings (settings); MWClass::registerClasses(); // Create encoder mEncoder = new ToUTF8::Utf8Encoder(mEncoding); // Setup viewer mViewer = new osgViewer::Viewer; mViewer->setReleaseContextAtEndOfFrameHint(false); #if OSG_VERSION_GREATER_OR_EQUAL(3,5,5) // Do not try to outsmart the OS thread scheduler (see bug #4785). mViewer->setUseConfigureAffinity(false); #endif mScreenCaptureOperation = new WriteScreenshotToFileOperation( mCfgMgr.getScreenshotPath().string(), Settings::Manager::getString("screenshot format", "General")); mScreenCaptureHandler = new osgViewer::ScreenCaptureHandler(mScreenCaptureOperation); mViewer->addEventHandler(mScreenCaptureHandler); mEnvironment.setFrameRateLimit(Settings::Manager::getFloat("framerate limit", "Video")); prepareEngine (settings); std::ofstream stats; if (const auto path = std::getenv("OPENMW_OSG_STATS_FILE")) { stats.open(path, std::ios_base::out); if (stats.is_open()) Log(Debug::Info) << "Stats will be written to: " << path; else Log(Debug::Warning) << "Failed to open file for stats: " << path; } /* Start of tes3mp addition Handle post-initialization for multiplayer classes */ mwmp::Main::postInit(); /* End of tes3mp addition */ /* Start of tes3mp change (major) Always skip the main menu in multiplayer */ mSkipMenu = true; /* End of tes3mp change (major) */ // Setup profiler osg::ref_ptr statshandler = new Resource::Profiler(stats.is_open()); initStatsHandler(*statshandler); mViewer->addEventHandler(statshandler); osg::ref_ptr resourceshandler = new Resource::StatsHandler(stats.is_open()); mViewer->addEventHandler(resourceshandler); if (stats.is_open()) Resource::CollectStatistics(mViewer); // Start the game if (!mSaveGameFile.empty()) { mEnvironment.getStateManager()->loadGame(mSaveGameFile); } else if (!mSkipMenu) { // start in main menu mEnvironment.getWindowManager()->pushGuiMode (MWGui::GM_MainMenu); mEnvironment.getSoundManager()->playTitleMusic(); const std::string& logo = Fallback::Map::getString("Movies_Morrowind_Logo"); if (!logo.empty()) mEnvironment.getWindowManager()->playVideo(logo, true); } else { mEnvironment.getStateManager()->newGame (!mNewGame); } if (!mStartupScript.empty() && mEnvironment.getStateManager()->getState() == MWState::StateManager::State_Running) { mEnvironment.getWindowManager()->executeInConsole(mStartupScript); } // Start the main rendering loop double simulationTime = 0.0; Misc::FrameRateLimiter frameRateLimiter = Misc::makeFrameRateLimiter(mEnvironment.getFrameRateLimit()); const std::chrono::steady_clock::duration maxSimulationInterval(std::chrono::milliseconds(200)); while (!mViewer->done() && !mEnvironment.getStateManager()->hasQuitRequest()) { const double dt = std::chrono::duration_cast>(std::min( frameRateLimiter.getLastFrameDuration(), maxSimulationInterval )).count(); mViewer->advance(simulationTime); if (!frame(dt)) { std::this_thread::sleep_for(std::chrono::milliseconds(5)); continue; } else { mViewer->eventTraversal(); mViewer->updateTraversal(); mEnvironment.getWorld()->updateWindowManager(); mViewer->renderingTraversals(); bool guiActive = mEnvironment.getWindowManager()->isGuiMode(); /* Start of tes3mp change (major) Whether the GUI is active should have no relevance in multiplayer, so the guiActive boolean is always set to false instead */ guiActive = false; /* End of tes3mp change (major) */ if (!guiActive) simulationTime += dt; } if (stats) { const auto frameNumber = mViewer->getFrameStamp()->getFrameNumber(); if (frameNumber >= 2) { mViewer->getViewerStats()->report(stats, frameNumber - 2); osgViewer::Viewer::Cameras cameras; mViewer->getCameras(cameras); for (auto camera : cameras) camera->getStats()->report(stats, frameNumber - 2); } } frameRateLimiter.limit(); } // Save user settings settings.saveUser(settingspath); mViewer->stopThreading(); Log(Debug::Info) << "Quitting peacefully."; } void OMW::Engine::setCompileAll (bool all) { mCompileAll = all; } void OMW::Engine::setCompileAllDialogue (bool all) { mCompileAllDialogue = all; } void OMW::Engine::setSoundUsage(bool soundUsage) { mUseSound = soundUsage; } void OMW::Engine::setEncoding(const ToUTF8::FromType& encoding) { mEncoding = encoding; } void OMW::Engine::setScriptConsoleMode (bool enabled) { mScriptConsoleMode = enabled; } void OMW::Engine::setStartupScript (const std::string& path) { mStartupScript = path; } void OMW::Engine::setActivationDistanceOverride (int distance) { mActivationDistanceOverride = distance; } void OMW::Engine::setWarningsMode (int mode) { mWarningsMode = mode; } void OMW::Engine::setScriptBlacklist (const std::vector& list) { mScriptBlacklist = list; } void OMW::Engine::setScriptBlacklistUse (bool use) { mScriptBlacklistUse = use; } void OMW::Engine::enableFontExport(bool exportFonts) { mExportFonts = exportFonts; } void OMW::Engine::setSaveGameFile(const std::string &savegame) { mSaveGameFile = savegame; } void OMW::Engine::setRandomSeed(unsigned int seed) { mRandomSeed = seed; } ================================================ FILE: apps/openmw/engine.hpp ================================================ #ifndef ENGINE_H #define ENGINE_H #include #include #include #include #include #include #include "mwbase/environment.hpp" #include "mwworld/ptr.hpp" namespace Resource { class ResourceSystem; } namespace SceneUtil { class WorkQueue; } namespace VFS { class Manager; } namespace Compiler { class Context; } namespace Files { struct ConfigurationManager; } namespace osgViewer { class ScreenCaptureHandler; } struct SDL_Window; namespace OMW { /// \brief Main engine class, that brings together all the components of OpenMW class Engine { SDL_Window* mWindow; std::unique_ptr mVFS; std::unique_ptr mResourceSystem; osg::ref_ptr mWorkQueue; MWBase::Environment mEnvironment; ToUTF8::FromType mEncoding; ToUTF8::Utf8Encoder* mEncoder; Files::PathContainer mDataDirs; std::vector mArchives; boost::filesystem::path mResDir; osg::ref_ptr mViewer; osg::ref_ptr mScreenCaptureHandler; osgViewer::ScreenCaptureHandler::CaptureOperation *mScreenCaptureOperation; std::string mCellName; std::vector mContentFiles; std::vector mGroundcoverFiles; bool mSkipMenu; bool mUseSound; bool mCompileAll; bool mCompileAllDialogue; int mWarningsMode; std::string mFocusName; bool mScriptConsoleMode; std::string mStartupScript; int mActivationDistanceOverride; std::string mSaveGameFile; // Grab mouse? bool mGrab; bool mExportFonts; unsigned int mRandomSeed; Compiler::Extensions mExtensions; Compiler::Context *mScriptContext; Files::Collections mFileCollections; bool mFSStrict; Translation::Storage mTranslationDataStorage; std::vector mScriptBlacklist; bool mScriptBlacklistUse; bool mNewGame; // not implemented Engine (const Engine&); Engine& operator= (const Engine&); void executeLocalScripts(); bool frame (float dt); /// Load settings from various files, returns the path to the user settings file std::string loadSettings (Settings::Manager & settings); /// Prepare engine for game play void prepareEngine (Settings::Manager & settings); void createWindow(Settings::Manager& settings); void setWindowIcon(); public: Engine(Files::ConfigurationManager& configurationManager); virtual ~Engine(); /// Enable strict filesystem mode (do not fold case) /// /// \attention The strict mode must be specified before any path-related settings /// are given to the engine. void enableFSStrict(bool fsStrict); /// Set data dirs void setDataDirs(const Files::PathContainer& dataDirs); /// Add BSA archive void addArchive(const std::string& archive); /// Set resource dir void setResourceDir(const boost::filesystem::path& parResDir); /// Set start cell name void setCell(const std::string& cellName); /** * @brief addContentFile - Adds content file (ie. esm/esp, or omwgame/omwaddon) to the content files container. * @param file - filename (extension is required) */ void addContentFile(const std::string& file); void addGroundcoverFile(const std::string& file); /// Disable or enable all sounds void setSoundUsage(bool soundUsage); /// Skip main menu and go directly into the game /// /// \param newGame Start a new game instead off dumping the player into the game /// (ignored if !skipMenu). void setSkipMenu (bool skipMenu, bool newGame); void setGrabMouse(bool grab) { mGrab = grab; } /// Initialise and enter main loop. void go(); /// Compile all scripts (excludign dialogue scripts) at startup? void setCompileAll (bool all); /// Compile all dialogue scripts at startup? void setCompileAllDialogue (bool all); /// Font encoding void setEncoding(const ToUTF8::FromType& encoding); /// Enable console-only script functionality void setScriptConsoleMode (bool enabled); /// Set path for a script that is run on startup in the console. void setStartupScript (const std::string& path); /// Override the game setting specified activation distance. void setActivationDistanceOverride (int distance); void setWarningsMode (int mode); void setScriptBlacklist (const std::vector& list); void setScriptBlacklistUse (bool use); void enableFontExport(bool exportFonts); /// Set the save game file to load after initialising the engine. void setSaveGameFile(const std::string& savegame); void setRandomSeed(unsigned int seed); private: Files::ConfigurationManager& mCfgMgr; }; } #endif /* ENGINE_H */ ================================================ FILE: apps/openmw/main.cpp ================================================ #include #include #include #include #include #include #include #include "engine.hpp" /* Start of tes3mp addition Include the header of the multiplayer's Main class */ #include "mwmp/Main.hpp" /* End of tes3mp addition */ #if defined(_WIN32) #ifndef WIN32_LEAN_AND_MEAN #define WIN32_LEAN_AND_MEAN #endif #include // makes __argc and __argv available on windows #include #endif #if (defined(__APPLE__) || defined(__linux) || defined(__unix) || defined(__posix)) #include #endif /* Start of tes3mp addition Include additional headers for multiplayer purposes */ #include #include #include #include /* End of tes3mp addition */ using namespace Fallback; /** * \brief Parses application command line and calls \ref Cfg::ConfigurationManager * to parse configuration files. * * Results are directly written to \ref Engine class. * * \retval true - Everything goes OK * \retval false - Error */ bool parseOptions (int argc, char** argv, OMW::Engine& engine, Files::ConfigurationManager& cfgMgr) { // Create a local alias for brevity namespace bpo = boost::program_options; typedef std::vector StringsVector; bpo::options_description desc("Syntax: openmw \nAllowed options"); desc.add_options() ("help", "print help message") ("version", "print version information and quit") ("replace", bpo::value()->default_value(Files::EscapeStringVector(), "") ->multitoken()->composing(), "settings where the values from the current source should replace those from lower-priority sources instead of being appended") ("data", bpo::value()->default_value(Files::EscapePathContainer(), "data") ->multitoken()->composing(), "set data directories (later directories have higher priority)") ("data-local", bpo::value()->default_value(Files::EscapePath(), ""), "set local data directory (highest priority)") ("fallback-archive", bpo::value()->default_value(Files::EscapeStringVector(), "fallback-archive") ->multitoken()->composing(), "set fallback BSA archives (later archives have higher priority)") ("resources", bpo::value()->default_value(Files::EscapePath(), "resources"), "set resources directory") ("start", bpo::value()->default_value(""), "set initial cell") ("content", bpo::value()->default_value(Files::EscapeStringVector(), "") ->multitoken()->composing(), "content file(s): esm/esp, or omwgame/omwaddon") ("groundcover", bpo::value()->default_value(Files::EscapeStringVector(), "") ->multitoken()->composing(), "groundcover content file(s): esm/esp, or omwgame/omwaddon") ("no-sound", bpo::value()->implicit_value(true) ->default_value(false), "disable all sounds") ("script-all", bpo::value()->implicit_value(true) ->default_value(false), "compile all scripts (excluding dialogue scripts) at startup") ("script-all-dialogue", bpo::value()->implicit_value(true) ->default_value(false), "compile all dialogue scripts at startup") ("script-console", bpo::value()->implicit_value(true) ->default_value(false), "enable console-only script functionality") ("script-run", bpo::value()->default_value(""), "select a file containing a list of console commands that is executed on startup") ("script-warn", bpo::value()->implicit_value (1) ->default_value (1), "handling of warnings when compiling scripts\n" "\t0 - ignore warning\n" "\t1 - show warning but consider script as correctly compiled anyway\n" "\t2 - treat warnings as errors") ("script-blacklist", bpo::value()->default_value(Files::EscapeStringVector(), "") ->multitoken()->composing(), "ignore the specified script (if the use of the blacklist is enabled)") ("script-blacklist-use", bpo::value()->implicit_value(true) ->default_value(true), "enable script blacklisting") ("load-savegame", bpo::value()->default_value(Files::EscapePath(), ""), "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()->implicit_value(true) ->default_value(false), "skip main menu on game startup") ("new-game", bpo::value()->implicit_value(true) ->default_value(false), "run new game sequence (ignored if skip-menu=0)") ("fs-strict", bpo::value()->implicit_value(true) ->default_value(false), "strict file system handling (no case folding)") ("encoding", bpo::value()-> 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()->default_value(FallbackMap(), "") ->multitoken()->composing(), "fallback values") ("no-grab", bpo::value()->implicit_value(true)->default_value(false), "Don't grab mouse cursor") ("export-fonts", bpo::value()->implicit_value(true) ->default_value(false), "Export Morrowind .fnt fonts to PNG image and XML file in current directory") ("activate-dist", bpo::value ()->default_value (-1), "activation distance override") ("random-seed", bpo::value () ->default_value(Misc::Rng::generateDefaultSeed()), "seed value for random number generator") ; /* Start of tes3mp addition Parse options added by multiplayer */ mwmp::Main::optionsDesc(&desc); /* End of tes3mp addition */ bpo::parsed_options valid_opts = bpo::command_line_parser(argc, argv) .options(desc).allow_unregistered().run(); bpo::variables_map variables; // Runtime options override settings from all configs bpo::store(valid_opts, variables); bpo::notify(variables); if (variables.count ("help")) { getRawStdout() << desc << std::endl; return false; } if (variables.count ("version")) { cfgMgr.readConfiguration(variables, desc, true); Version::Version v = Version::getOpenmwVersion(variables["resources"].as().mPath.string()); getRawStdout() << v.describe() << std::endl; return false; } bpo::variables_map composingVariables = cfgMgr.separateComposingVariables(variables, desc); cfgMgr.readConfiguration(variables, desc); cfgMgr.mergeComposingVariables(variables, composingVariables, desc); Version::Version v = Version::getOpenmwVersion(variables["resources"].as().mPath.string()); /* Start of tes3mp addition Print the multiplayer version first */ Log(Debug::Info) << Utils::getVersionInfo("TES3MP client", TES3MP_VERSION, v.mCommitHash, TES3MP_PROTO_VERSION); /* End of tes3mp addition */ /* Start of tes3mp change (minor) Because there is no need to print the commit hash again, only print OpenMW's version */ Log(Debug::Info) << "OpenMW version " << v.mVersion; /* End of tes3mp change (minor) */ engine.setGrabMouse(!variables["no-grab"].as()); // Font encoding settings std::string encoding(variables["encoding"].as().toStdString()); Log(Debug::Info) << ToUTF8::encodingUsingMessage(encoding); engine.setEncoding(ToUTF8::calculateEncoding(encoding)); // directory settings engine.enableFSStrict(variables["fs-strict"].as()); Files::PathContainer dataDirs(Files::EscapePath::toPathContainer(variables["data"].as())); Files::PathContainer::value_type local(variables["data-local"].as().mPath); if (!local.empty()) dataDirs.push_back(local); cfgMgr.processPaths(dataDirs); engine.setResourceDir(variables["resources"].as().mPath); engine.setDataDirs(dataDirs); // fallback archives StringsVector archives = variables["fallback-archive"].as().toStdStringVector(); for (StringsVector::const_iterator it = archives.begin(); it != archives.end(); ++it) { engine.addArchive(*it); } StringsVector content = variables["content"].as().toStdStringVector(); if (content.empty()) { Log(Debug::Error) << "No content file given (esm/esp, nor omwgame/omwaddon). Aborting..."; return false; } std::set contentDedupe; for (const auto& contentFile : content) { if (!contentDedupe.insert(contentFile).second) { Log(Debug::Error) << "Content file specified more than once: " << contentFile << ". Aborting..."; return false; } } for (auto& file : content) { engine.addContentFile(file); } StringsVector groundcover = variables["groundcover"].as().toStdStringVector(); for (auto& file : groundcover) { engine.addGroundcoverFile(file); } // startup-settings engine.setCell(variables["start"].as().toStdString()); engine.setSkipMenu (variables["skip-menu"].as(), variables["new-game"].as()); if (!variables["skip-menu"].as() && variables["new-game"].as()) Log(Debug::Warning) << "Warning: new-game used without skip-menu -> ignoring it"; // scripts engine.setCompileAll(variables["script-all"].as()); engine.setCompileAllDialogue(variables["script-all-dialogue"].as()); engine.setScriptConsoleMode (variables["script-console"].as()); /* Start of tes3mp change (major) Clients should not be allowed to set any of these unilaterally in multiplayer, so disable them */ /* engine.setStartupScript (variables["script-run"].as().toStdString()); engine.setWarningsMode (variables["script-warn"].as()); engine.setScriptBlacklist (variables["script-blacklist"].as().toStdStringVector()); engine.setScriptBlacklistUse (variables["script-blacklist-use"].as()); engine.setSaveGameFile (variables["load-savegame"].as().mPath.string()); */ /* End of tes3mp change (major) */ // other settings Fallback::Map::init(variables["fallback"].as().mMap); engine.setSoundUsage(!variables["no-sound"].as()); engine.setActivationDistanceOverride (variables["activate-dist"].as()); engine.enableFontExport(variables["export-fonts"].as()); engine.setRandomSeed(variables["random-seed"].as()); /* Start of tes3mp addition Configure multiplayer using parsed variables */ mwmp::Main::configure(&variables); /* End of tes3mp addition */ return true; } namespace { class OSGLogHandler : public osg::NotifyHandler { void notify(osg::NotifySeverity severity, const char* msg) override { // Copy, because osg logging is not thread safe. std::string msgCopy(msg); if (msgCopy.empty()) return; Debug::Level level; switch (severity) { case osg::ALWAYS: case osg::FATAL: level = Debug::Error; break; case osg::WARN: case osg::NOTICE: level = Debug::Warning; break; case osg::INFO: level = Debug::Info; break; case osg::DEBUG_INFO: case osg::DEBUG_FP: default: level = Debug::Debug; } std::string_view s(msgCopy); if (s.size() < 1024) Log(level) << (s.back() == '\n' ? s.substr(0, s.size() - 1) : s); else { while (!s.empty()) { size_t lineSize = 1; while (lineSize < s.size() && s[lineSize - 1] != '\n') lineSize++; Log(level) << s.substr(0, s[lineSize - 1] == '\n' ? lineSize - 1 : lineSize); s = s.substr(lineSize); } } } }; } int runApplication(int argc, char *argv[]) { #ifdef __APPLE__ boost::filesystem::path binary_path = boost::filesystem::system_complete(boost::filesystem::path(argv[0])); boost::filesystem::current_path(binary_path.parent_path()); setenv("OSG_GL_TEXTURE_STORAGE", "OFF", 0); #endif osg::setNotifyHandler(new OSGLogHandler()); Files::ConfigurationManager cfgMgr; std::unique_ptr engine; engine.reset(new OMW::Engine(cfgMgr)); if (parseOptions(argc, argv, *engine, cfgMgr)) { engine->go(); } return 0; } #ifdef ANDROID extern "C" int SDL_main(int argc, char**argv) #else int main(int argc, char**argv) #endif { /* Start of tes3mp addition Initialize the logger added for multiplayer */ LOG_INIT(TimedLog::LOG_INFO); /* End of tes3mp addition */ /* Start of tes3mp change (major) Instead of logging information in openmw.log, use a more descriptive filename that includes a timestamp */ return wrapApplication(&runApplication, argc, argv, "/tes3mp-client-" + TimedLog::getFilenameTimestamp()); /* End of tes3mp change (major) */ } // Platform specific for Windows when there is no console built into the executable. // Windows will call the WinMain function instead of main in this case, the normal // main function is then called with the __argc and __argv parameters. #if defined(_WIN32) && !defined(_CONSOLE) int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nShowCmd) { return main(__argc, __argv); } #endif ================================================ FILE: apps/openmw/mwbase/dialoguemanager.hpp ================================================ #ifndef GAME_MWBASE_DIALOGUEMANAGER_H #define GAME_MWBASE_DIALOGUEMANAGER_H #include #include #include #include namespace Loading { class Listener; } namespace ESM { class ESMReader; class ESMWriter; } namespace MWWorld { class Ptr; } namespace MWBase { /// \brief Interface for dialogue manager (implemented in MWDialogue) class DialogueManager { DialogueManager (const DialogueManager&); ///< not implemented DialogueManager& operator= (const DialogueManager&); ///< not implemented public: class ResponseCallback { public: virtual ~ResponseCallback() = default; virtual void addResponse(const std::string& title, const std::string& text) = 0; }; DialogueManager() {} virtual void clear() = 0; virtual ~DialogueManager() {} virtual bool isInChoice() const = 0; virtual bool startDialogue (const MWWorld::Ptr& actor, ResponseCallback* callback) = 0; virtual bool inJournal (const std::string& topicId, const std::string& infoId) = 0; virtual void addTopic (const std::string& topic) = 0; /* Start of tes3mp addition Make it possible to check whether a topic is known by the player from elsewhere in the code */ virtual bool isNewTopic(const std::string& topic) = 0; /* End of tes3mp addition */ virtual void addChoice (const std::string& text,int choice) = 0; virtual const std::vector >& getChoices() = 0; virtual bool isGoodbye() = 0; virtual void goodbye() = 0; virtual void say(const MWWorld::Ptr &actor, const std::string &topic) = 0; virtual void keywordSelected (const std::string& keyword, ResponseCallback* callback) = 0; virtual void goodbyeSelected() = 0; virtual void questionAnswered (int answer, ResponseCallback* callback) = 0; enum TopicType { Specific = 1, Exhausted = 2 }; enum ServiceType { Any = -1, Barter = 1, Repair = 2, Spells = 3, Training = 4, Travel = 5, Spellmaking = 6, Enchanting = 7 }; virtual std::list getAvailableTopics() = 0; virtual int getTopicFlag(const std::string&) = 0; virtual bool checkServiceRefused (ResponseCallback* callback, ServiceType service = ServiceType::Any) = 0; virtual void persuade (int type, ResponseCallback* callback) = 0; virtual int getTemporaryDispositionChange () const = 0; /// @note Controlled by an option, gets discarded when dialogue ends by default virtual void applyBarterDispositionChange (int delta) = 0; virtual int countSavedGameRecords() const = 0; virtual void write (ESM::ESMWriter& writer, Loading::Listener& progress) const = 0; virtual void readRecord (ESM::ESMReader& reader, uint32_t type) = 0; /// Changes faction1's opinion of faction2 by \a diff. virtual void modFactionReaction (const std::string& faction1, const std::string& faction2, int diff) = 0; virtual void setFactionReaction (const std::string& faction1, const std::string& faction2, int absolute) = 0; /// @return faction1's opinion of faction2 virtual int getFactionReaction (const std::string& faction1, const std::string& faction2) const = 0; /// Removes the last added topic response for the given actor from the journal virtual void clearInfoActor (const MWWorld::Ptr& actor) const = 0; /* Start of tes3mp addition Declare this method here so it can be used from outside of MWDialogue::DialogueManager */ virtual void updateActorKnownTopics() = 0; /* End of tes3mp addition */ /* Start of tes3mp addition Make it possible to get the caption of a voice dialogue */ virtual std::string getVoiceCaption(const std::string& sound) const = 0; /* End of tes3mp addition */ }; } #endif ================================================ FILE: apps/openmw/mwbase/environment.cpp ================================================ #include "environment.hpp" #include #include #include "world.hpp" #include "scriptmanager.hpp" #include "dialoguemanager.hpp" #include "journal.hpp" #include "soundmanager.hpp" #include "mechanicsmanager.hpp" #include "inputmanager.hpp" #include "windowmanager.hpp" #include "statemanager.hpp" MWBase::Environment *MWBase::Environment::sThis = nullptr; MWBase::Environment::Environment() : mWorld (nullptr), mSoundManager (nullptr), mScriptManager (nullptr), mWindowManager (nullptr), mMechanicsManager (nullptr), mDialogueManager (nullptr), mJournal (nullptr), mInputManager (nullptr), mStateManager (nullptr), mResourceSystem (nullptr), mFrameDuration (0), mFrameRateLimit(0.f) { assert (!sThis); sThis = this; } MWBase::Environment::~Environment() { cleanup(); sThis = nullptr; } void MWBase::Environment::setWorld (World *world) { mWorld = world; } void MWBase::Environment::setSoundManager (SoundManager *soundManager) { mSoundManager = soundManager; } void MWBase::Environment::setScriptManager (ScriptManager *scriptManager) { mScriptManager = scriptManager; } void MWBase::Environment::setWindowManager (WindowManager *windowManager) { mWindowManager = windowManager; } void MWBase::Environment::setMechanicsManager (MechanicsManager *mechanicsManager) { mMechanicsManager = mechanicsManager; } void MWBase::Environment::setDialogueManager (DialogueManager *dialogueManager) { mDialogueManager = dialogueManager; } void MWBase::Environment::setJournal (Journal *journal) { mJournal = journal; } void MWBase::Environment::setInputManager (InputManager *inputManager) { mInputManager = inputManager; } void MWBase::Environment::setStateManager (StateManager *stateManager) { mStateManager = stateManager; } void MWBase::Environment::setResourceSystem (Resource::ResourceSystem *resourceSystem) { mResourceSystem = resourceSystem; } void MWBase::Environment::setFrameDuration (float duration) { mFrameDuration = duration; } void MWBase::Environment::setFrameRateLimit(float limit) { mFrameRateLimit = limit; } float MWBase::Environment::getFrameRateLimit() const { return mFrameRateLimit; } MWBase::World *MWBase::Environment::getWorld() const { assert (mWorld); return mWorld; } MWBase::SoundManager *MWBase::Environment::getSoundManager() const { assert (mSoundManager); return mSoundManager; } MWBase::ScriptManager *MWBase::Environment::getScriptManager() const { assert (mScriptManager); return mScriptManager; } MWBase::WindowManager *MWBase::Environment::getWindowManager() const { assert (mWindowManager); return mWindowManager; } MWBase::MechanicsManager *MWBase::Environment::getMechanicsManager() const { assert (mMechanicsManager); return mMechanicsManager; } MWBase::DialogueManager *MWBase::Environment::getDialogueManager() const { assert (mDialogueManager); return mDialogueManager; } MWBase::Journal *MWBase::Environment::getJournal() const { assert (mJournal); return mJournal; } MWBase::InputManager *MWBase::Environment::getInputManager() const { assert (mInputManager); return mInputManager; } MWBase::StateManager *MWBase::Environment::getStateManager() const { assert (mStateManager); return mStateManager; } Resource::ResourceSystem *MWBase::Environment::getResourceSystem() const { return mResourceSystem; } float MWBase::Environment::getFrameDuration() const { return mFrameDuration; } void MWBase::Environment::cleanup() { delete mMechanicsManager; mMechanicsManager = nullptr; delete mDialogueManager; mDialogueManager = nullptr; delete mJournal; mJournal = nullptr; delete mScriptManager; mScriptManager = nullptr; delete mWindowManager; mWindowManager = nullptr; delete mWorld; mWorld = nullptr; delete mSoundManager; mSoundManager = nullptr; delete mInputManager; mInputManager = nullptr; delete mStateManager; mStateManager = nullptr; } const MWBase::Environment& MWBase::Environment::get() { assert (sThis); return *sThis; } void MWBase::Environment::reportStats(unsigned int frameNumber, osg::Stats& stats) const { mMechanicsManager->reportStats(frameNumber, stats); mWorld->reportStats(frameNumber, stats); } ================================================ FILE: apps/openmw/mwbase/environment.hpp ================================================ #ifndef GAME_BASE_ENVIRONMENT_H #define GAME_BASE_ENVIRONMENT_H namespace osg { class Stats; } namespace Resource { class ResourceSystem; } namespace MWBase { class World; class ScriptManager; class DialogueManager; class Journal; class SoundManager; class MechanicsManager; class InputManager; class WindowManager; class StateManager; /// \brief Central hub for mw-subsystems /// /// This class allows each mw-subsystem to access any others subsystem's top-level manager class. /// /// \attention Environment takes ownership of the manager class instances it is handed over in /// the set* functions. class Environment { static Environment *sThis; World *mWorld; SoundManager *mSoundManager; ScriptManager *mScriptManager; WindowManager *mWindowManager; MechanicsManager *mMechanicsManager; DialogueManager *mDialogueManager; Journal *mJournal; InputManager *mInputManager; StateManager *mStateManager; Resource::ResourceSystem *mResourceSystem; float mFrameDuration; float mFrameRateLimit; Environment (const Environment&); ///< not implemented Environment& operator= (const Environment&); ///< not implemented public: Environment(); ~Environment(); void setWorld (World *world); void setSoundManager (SoundManager *soundManager); void setScriptManager (MWBase::ScriptManager *scriptManager); void setWindowManager (WindowManager *windowManager); void setMechanicsManager (MechanicsManager *mechanicsManager); void setDialogueManager (DialogueManager *dialogueManager); void setJournal (Journal *journal); void setInputManager (InputManager *inputManager); void setStateManager (StateManager *stateManager); void setResourceSystem (Resource::ResourceSystem *resourceSystem); void setFrameDuration (float duration); ///< Set length of current frame in seconds. void setFrameRateLimit(float frameRateLimit); float getFrameRateLimit() const; World *getWorld() const; SoundManager *getSoundManager() const; ScriptManager *getScriptManager() const; WindowManager *getWindowManager() const; MechanicsManager *getMechanicsManager() const; DialogueManager *getDialogueManager() const; Journal *getJournal() const; InputManager *getInputManager() const; StateManager *getStateManager() const; Resource::ResourceSystem *getResourceSystem() const; float getFrameDuration() const; void cleanup(); ///< Delete all mw*-subsystems. static const Environment& get(); ///< Return instance of this class. void reportStats(unsigned int frameNumber, osg::Stats& stats) const; }; } #endif ================================================ FILE: apps/openmw/mwbase/inputmanager.hpp ================================================ #ifndef GAME_MWBASE_INPUTMANAGER_H #define GAME_MWBASE_INPUTMANAGER_H #include #include #include #include namespace Loading { class Listener; } namespace ESM { class ESMReader; class ESMWriter; } namespace MWBase { /// \brief Interface for input manager (implemented in MWInput) class InputManager { InputManager (const InputManager&); ///< not implemented InputManager& operator= (const InputManager&); ///< not implemented public: InputManager() {} /// Clear all savegame-specific data virtual void clear() = 0; virtual ~InputManager() {} virtual void update(float dt, bool disableControls, bool disableEvents=false) = 0; virtual void changeInputMode(bool guiMode) = 0; virtual void processChangedSettings(const std::set< std::pair >& changed) = 0; virtual void setDragDrop(bool dragDrop) = 0; 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 std::string getActionDescription (int action) = 0; virtual std::string getActionKeyBindingName (int action) = 0; virtual std::string getActionControllerBindingName (int action) = 0; ///Actions available for binding to keyboard buttons virtual std::vector getActionKeySorting() = 0; ///Actions available for binding to controller buttons virtual std::vector getActionControllerSorting() = 0; virtual int getNumActions() = 0; ///If keyboard is true, only pay attention to keyboard events. If false, only pay attention to controller events (excluding esc) virtual void enableDetectingBindingMode (int action, bool keyboard) = 0; virtual void resetToDefaultKeyBindings() = 0; virtual void resetToDefaultControllerBindings() = 0; /// Returns if the last used input device was a joystick or a keyboard /// @return true if joystick, false otherwise virtual bool joystickLastUsed() = 0; virtual void setJoystickLastUsed(bool enabled) = 0; virtual int countSavedGameRecords() const = 0; virtual void write(ESM::ESMWriter& writer, Loading::Listener& progress) = 0; virtual void readRecord(ESM::ESMReader& reader, uint32_t type) = 0; virtual void resetIdleTime() = 0; virtual void executeAction(int action) = 0; virtual bool controlsDisabled() = 0; }; } #endif ================================================ FILE: apps/openmw/mwbase/journal.hpp ================================================ #ifndef GAME_MWBASE_JOURNAL_H #define GAME_MWBASE_JOURNAL_H #include #include #include #include #include "../mwdialogue/journalentry.hpp" #include "../mwdialogue/topic.hpp" #include "../mwdialogue/quest.hpp" namespace Loading { class Listener; } namespace ESM { class ESMReader; class ESMWriter; } namespace MWBase { /// \brief Interface for the player's journal (implemented in MWDialogue) class Journal { Journal (const Journal&); ///< not implemented Journal& operator= (const Journal&); ///< not implemented public: typedef std::deque TEntryContainer; typedef TEntryContainer::const_iterator TEntryIter; typedef std::map TQuestContainer; // topic, quest typedef TQuestContainer::const_iterator TQuestIter; typedef std::map TTopicContainer; // topic-id, topic-content typedef TTopicContainer::const_iterator TTopicIter; public: Journal() {} virtual void clear() = 0; virtual ~Journal() {} /* Start of tes3mp addition Make it possible to check whether a journal entry already exists from elsewhere in the code */ virtual bool hasEntry(const std::string& id, int index) = 0; /* End of tes3mp addition */ /* Start of tes3mp change (minor) Make it possible to override current time when adding journal entries, by adding optional timestamp override arguments */ virtual void addEntry (const std::string& id, int index, const MWWorld::Ptr& actor, int daysPassed = -1, int month = -1, int day = -1) = 0; ///< Add a journal entry. /// @param actor Used as context for replacing of escape sequences (%name, etc). /* End of tes3mp change (major) */ virtual void setJournalIndex (const std::string& id, int index) = 0; ///< Set the journal index without adding an entry. virtual int getJournalIndex (const std::string& id) const = 0; ///< Get the journal index. virtual void addTopic (const std::string& topicId, const std::string& infoId, const MWWorld::Ptr& actor) = 0; /// \note topicId must be lowercase virtual void removeLastAddedTopicResponse (const std::string& topicId, const std::string& actorName) = 0; ///< Removes the last topic response added for the given topicId and actor name. /// \note topicId must be lowercase virtual TEntryIter begin() const = 0; ///< Iterator pointing to the begin of the main journal. /// /// \note Iterators to main journal entries will never become invalid. virtual TEntryIter end() const = 0; ///< Iterator pointing past the end of the main journal. virtual TQuestIter questBegin() const = 0; ///< Iterator pointing to the first quest (sorted by topic ID) virtual TQuestIter questEnd() const = 0; ///< Iterator pointing past the last quest. virtual TTopicIter topicBegin() const = 0; ///< Iterator pointing to the first topic (sorted by topic ID) /// /// \note The topic ID is identical with the user-visible topic string. virtual TTopicIter topicEnd() const = 0; ///< Iterator pointing past the last topic. virtual int countSavedGameRecords() const = 0; virtual void write (ESM::ESMWriter& writer, Loading::Listener& progress) const = 0; virtual void readRecord (ESM::ESMReader& reader, uint32_t type) = 0; }; } #endif ================================================ FILE: apps/openmw/mwbase/mechanicsmanager.hpp ================================================ #ifndef GAME_MWBASE_MECHANICSMANAGER_H #define GAME_MWBASE_MECHANICSMANAGER_H #include #include #include #include #include #include "../mwmechanics/actorutil.hpp" // For MWMechanics::GreetingState #include "../mwworld/ptr.hpp" namespace osg { class Stats; class Vec3f; } namespace ESM { struct Class; class ESMReader; class ESMWriter; } namespace MWWorld { class Ptr; class CellStore; class CellRef; } namespace Loading { class Listener; } namespace MWBase { /// \brief Interface for game mechanics manager (implemented in MWMechanics) class MechanicsManager { MechanicsManager (const MechanicsManager&); ///< not implemented MechanicsManager& operator= (const MechanicsManager&); ///< not implemented public: MechanicsManager() {} virtual ~MechanicsManager() {} virtual void add (const MWWorld::Ptr& ptr) = 0; ///< Register an object for management virtual void remove (const MWWorld::Ptr& ptr) = 0; ///< Deregister an object for management virtual void updateCell(const MWWorld::Ptr &old, const MWWorld::Ptr &ptr) = 0; ///< Moves an object to a new cell virtual void drop (const MWWorld::CellStore *cellStore) = 0; ///< Deregister all objects in the given cell. virtual void update (float duration, bool paused) = 0; ///< Update objects /// /// \param paused In game type does not currently advance (this usually means some GUI /// component is up). virtual void setPlayerName (const std::string& name) = 0; ///< Set player name. virtual void setPlayerRace (const std::string& id, bool male, const std::string &head, const std::string &hair) = 0; ///< Set player race. virtual void setPlayerBirthsign (const std::string& id) = 0; ///< Set player birthsign. virtual void setPlayerClass (const std::string& id) = 0; ///< Set player class to stock class. virtual void setPlayerClass (const ESM::Class& class_) = 0; ///< Set player class to custom class. virtual void restoreDynamicStats(MWWorld::Ptr actor, double hours, bool sleep) = 0; virtual void rest(double hours, bool sleep) = 0; ///< If the player is sleeping or waiting, this should be called every hour. /// @param sleep is the player sleeping or waiting? virtual int getHoursToRest() const = 0; ///< Calculate how many hours the player needs to rest in order to be fully healed virtual int getBarterOffer(const MWWorld::Ptr& ptr,int basePrice, bool buying) = 0; ///< This is used by every service to determine the price of objects given the trading skills of the player and NPC. virtual int getDerivedDisposition(const MWWorld::Ptr& ptr, bool addTemporaryDispositionChange = true) = 0; ///< Calculate the diposition of an NPC toward the player. virtual int countDeaths (const std::string& id) const = 0; ///< Return the number of deaths for actors with the given ID. /* Start of tes3mp addition Make it possible to set the number of deaths for an actor with the given refId */ virtual void setDeaths(const std::string& refId, int number) = 0; /* End of tes3mp addition */ /// Check if \a observer is potentially aware of \a ptr. Does not do a line of sight check! virtual bool awarenessCheck (const MWWorld::Ptr& ptr, const MWWorld::Ptr& observer) = 0; /// Makes \a ptr fight \a target. Also shouts a combat taunt. virtual void startCombat (const MWWorld::Ptr& ptr, const MWWorld::Ptr& target) = 0; enum OffenseType { OT_Theft, // Taking items owned by an NPC or a faction you are not a member of OT_Assault, // Attacking a peaceful NPC OT_Murder, // Murdering a peaceful NPC OT_Trespassing, // Picking the lock of an owned door/chest OT_SleepingInOwnedBed, // Sleeping in a bed owned by an NPC or a faction you are not a member of OT_Pickpocket // Entering pickpocket mode, leaving it, and being detected. Any items stolen are a separate crime (Theft) }; /** * @note victim may be empty * @param arg Depends on \a type, e.g. for Theft, the value of the item that was stolen. * @param victimAware Is the victim already aware of the crime? * If this parameter is false, it will be determined by a line-of-sight and awareness check. * @return was the crime seen? */ virtual bool commitCrime (const MWWorld::Ptr& ptr, const MWWorld::Ptr& victim, OffenseType type, const std::string& factionId="", int arg=0, bool victimAware=false) = 0; /// @return false if the attack was considered a "friendly hit" and forgiven virtual bool actorAttacked (const MWWorld::Ptr& victim, const MWWorld::Ptr& attacker) = 0; /// Notify that actor was killed, add a murder bounty if applicable /// @note No-op for non-player attackers virtual void actorKilled (const MWWorld::Ptr& victim, const MWWorld::Ptr& attacker) = 0; /// Utility to check if taking this item is illegal and calling commitCrime if so /// @param container The container the item is in; may be empty for an item in the world virtual void itemTaken (const MWWorld::Ptr& ptr, const MWWorld::Ptr& item, const MWWorld::Ptr& container, int count, bool alarm = true) = 0; /// Utility to check if unlocking this object is illegal and calling commitCrime if so virtual void unlockAttempted (const MWWorld::Ptr& ptr, const MWWorld::Ptr& item) = 0; /// Attempt sleeping in a bed. If this is illegal, call commitCrime. /// @return was it illegal, and someone saw you doing it? virtual bool sleepInBed (const MWWorld::Ptr& ptr, const MWWorld::Ptr& bed) = 0; enum PersuasionType { PT_Admire, PT_Intimidate, PT_Taunt, PT_Bribe10, PT_Bribe100, PT_Bribe1000 }; virtual void getPersuasionDispositionChange (const MWWorld::Ptr& npc, PersuasionType type, bool& success, float& tempChange, float& permChange) = 0; ///< Perform a persuasion action on NPC virtual void forceStateUpdate(const MWWorld::Ptr &ptr) = 0; ///< Forces an object to refresh its animation state. virtual bool playAnimationGroup(const MWWorld::Ptr& ptr, const std::string& groupName, int mode, int number=1, bool persist=false) = 0; ///< Run animation for a MW-reference. Calls to this function for references that are currently not /// in the scene should be ignored. /// /// \param mode 0 normal, 1 immediate start, 2 immediate loop /// \param count How many times the animation should be run /// \param persist Whether the animation state should be stored in saved games /// and persist after cell unload. /// \return Success or error virtual void skipAnimation(const MWWorld::Ptr& ptr) = 0; ///< Skip the animation for the given MW-reference for one frame. Calls to this function for /// references that are currently not in the scene should be ignored. virtual bool checkAnimationPlaying(const MWWorld::Ptr& ptr, const std::string& groupName) = 0; /// Save the current animation state of managed references to their RefData. virtual void persistAnimationStates() = 0; /// Update magic effects for an actor. Usually done automatically once per frame, but if we're currently /// paused we may want to do it manually (after equipping permanent enchantment) virtual void updateMagicEffects (const MWWorld::Ptr& ptr) = 0; virtual bool toggleAI() = 0; virtual bool isAIActive() = 0; virtual void getObjectsInRange (const osg::Vec3f& position, float radius, std::vector& objects) = 0; virtual void getActorsInRange(const osg::Vec3f &position, float radius, std::vector &objects) = 0; /// Check if there are actors in selected range virtual bool isAnyActorInRange(const osg::Vec3f &position, float radius) = 0; ///Returns the list of actors which are siding with the given actor in fights /**ie AiFollow or AiEscort is active and the target is the actor **/ virtual std::list getActorsSidingWith(const MWWorld::Ptr& actor) = 0; virtual std::list getActorsFollowing(const MWWorld::Ptr& actor) = 0; virtual std::list getActorsFollowingIndices(const MWWorld::Ptr& actor) = 0; virtual std::map getActorsFollowingByIndex(const MWWorld::Ptr& actor) = 0; ///Returns a list of actors who are fighting the given actor within the fAlarmDistance /** ie AiCombat is active and the target is the actor **/ virtual std::list getActorsFighting(const MWWorld::Ptr& actor) = 0; virtual std::list getEnemiesNearby(const MWWorld::Ptr& actor) = 0; /// Recursive versions of above methods virtual void getActorsFollowing(const MWWorld::Ptr& actor, std::set& out) = 0; virtual void getActorsSidingWith(const MWWorld::Ptr& actor, std::set& out) = 0; virtual void playerLoaded() = 0; virtual int countSavedGameRecords() const = 0; virtual void write (ESM::ESMWriter& writer, Loading::Listener& listener) const = 0; virtual void readRecord (ESM::ESMReader& reader, uint32_t type) = 0; virtual void clear() = 0; virtual bool isAggressive (const MWWorld::Ptr& ptr, const MWWorld::Ptr& target) = 0; /// Resurrects the player if necessary virtual void resurrect(const MWWorld::Ptr& ptr) = 0; virtual bool isCastingSpell (const MWWorld::Ptr& ptr) const = 0; virtual bool isReadyToBlock (const MWWorld::Ptr& ptr) const = 0; virtual bool isAttackingOrSpell(const MWWorld::Ptr &ptr) const = 0; /* Start of tes3mp addition Make it possible to set the attackingOrSpell state from elsewhere in the code */ virtual void setAttackingOrSpell(const MWWorld::Ptr &ptr, bool state) const = 0; /* End of tes3mp addition */ virtual void castSpell(const MWWorld::Ptr& ptr, const std::string spellId, bool manualSpell) = 0; virtual void processChangedSettings (const std::set< std::pair >& settings) = 0; virtual float getActorsProcessingRange() const = 0; virtual void notifyDied(const MWWorld::Ptr& actor) = 0; virtual bool onOpen(const MWWorld::Ptr& ptr) = 0; virtual void onClose(const MWWorld::Ptr& ptr) = 0; /// Check if the target actor was detected by an observer /// If the observer is a non-NPC, check all actors in AI processing distance as observers virtual bool isActorDetected(const MWWorld::Ptr& actor, const MWWorld::Ptr& observer) = 0; virtual void confiscateStolenItems (const MWWorld::Ptr& player, const MWWorld::Ptr& targetContainer) = 0; /// List the owners that the player has stolen this item from (the owner can be an NPC or a faction). /// virtual std::vector > getStolenItemOwners(const std::string& itemid) = 0; /// Has the player stolen this item from the given owner? virtual bool isItemStolenFrom(const std::string& itemid, const MWWorld::Ptr& ptr) = 0; virtual bool isBoundItem(const MWWorld::Ptr& item) = 0; /* Start of tes3mp addition Make it possible to check if an itemId corresponds to a bound item */ virtual bool isBoundItem(std::string itemId) = 0; /* End of tes3mp addition */ virtual bool isAllowedToUse (const MWWorld::Ptr& ptr, const MWWorld::Ptr& target, MWWorld::Ptr& victim) = 0; /// Turn actor into werewolf or normal form. virtual void setWerewolf(const MWWorld::Ptr& actor, bool werewolf) = 0; /// Sets the NPC's Acrobatics skill to match the fWerewolfAcrobatics GMST. /// It only applies to the current form the NPC is in. virtual void applyWerewolfAcrobatics(const MWWorld::Ptr& actor) = 0; virtual void cleanupSummonedCreature(const MWWorld::Ptr& caster, int creatureActorId) = 0; virtual void confiscateStolenItemToOwner(const MWWorld::Ptr &player, const MWWorld::Ptr &item, const MWWorld::Ptr& victim, int count) = 0; virtual bool isAttackPreparing(const MWWorld::Ptr& ptr) = 0; virtual bool isRunning(const MWWorld::Ptr& ptr) = 0; virtual bool isSneaking(const MWWorld::Ptr& ptr) = 0; virtual void reportStats(unsigned int frameNumber, osg::Stats& stats) const = 0; virtual int getGreetingTimer(const MWWorld::Ptr& ptr) const = 0; virtual float getAngleToPlayer(const MWWorld::Ptr& ptr) const = 0; virtual MWMechanics::GreetingState getGreetingState(const MWWorld::Ptr& ptr) const = 0; virtual bool isTurningToPlayer(const MWWorld::Ptr& ptr) const = 0; virtual void restoreStatsAfterCorprus(const MWWorld::Ptr& actor, const std::string& sourceId) = 0; }; } #endif ================================================ FILE: apps/openmw/mwbase/rotationflags.hpp ================================================ #ifndef GAME_MWBASE_ROTATIONFLAGS_H #define GAME_MWBASE_ROTATIONFLAGS_H namespace MWBase { using RotationFlags = unsigned short; enum RotationFlag : RotationFlags { RotationFlag_none = 0, RotationFlag_adjust = 1, RotationFlag_inverseOrder = 1 << 1, }; } #endif ================================================ FILE: apps/openmw/mwbase/scriptmanager.hpp ================================================ #ifndef GAME_MWBASE_SCRIPTMANAGER_H #define GAME_MWBASE_SCRIPTMANAGER_H #include namespace Interpreter { class Context; } namespace Compiler { class Locals; } namespace MWScript { class GlobalScripts; } namespace MWBase { /// \brief Interface for script manager (implemented in MWScript) class ScriptManager { ScriptManager (const ScriptManager&); ///< not implemented ScriptManager& operator= (const ScriptManager&); ///< not implemented public: ScriptManager() {} virtual ~ScriptManager() {} virtual void clear() = 0; virtual bool run (const std::string& name, Interpreter::Context& interpreterContext) = 0; ///< Run the script with the given name (compile first, if not compiled yet) virtual bool compile (const std::string& name) = 0; ///< Compile script with the given namen /// \return Success? virtual std::pair compileAll() = 0; ///< Compile all scripts /// \return count, success virtual const Compiler::Locals& getLocals (const std::string& name) = 0; ///< Return locals for script \a name. virtual MWScript::GlobalScripts& getGlobalScripts() = 0; }; } #endif ================================================ FILE: apps/openmw/mwbase/soundmanager.hpp ================================================ #ifndef GAME_MWBASE_SOUNDMANAGER_H #define GAME_MWBASE_SOUNDMANAGER_H #include #include #include #include "../mwworld/ptr.hpp" #include "../mwsound/type.hpp" namespace MWWorld { class CellStore; } namespace MWSound { // Each entry excepts of MaxCount should be used only in one place enum BlockerType { VideoPlayback, MaxCount }; class Sound; class Stream; struct Sound_Decoder; typedef std::shared_ptr DecoderPtr; /* These must all fit together */ enum class PlayMode { Normal = 0, /* non-looping, affected by environment */ Loop = 1<<0, /* Sound will continually loop until explicitly stopped */ NoEnv = 1<<1, /* Do not apply environment effects (eg, underwater filters) */ RemoveAtDistance = 1<<2, /* (3D only) If the listener gets further than 2000 units away * from the sound source, the sound is removed. * This is weird stuff but apparently how vanilla works for sounds * played by the PlayLoopSound family of script functions. Perhaps * we can make this cut off a more subtle fade later, but have to * be careful to not change the overall volume of areas by too * much. */ NoPlayerLocal = 1<<3, /* (3D only) Don't play the sound local to the listener even if the * player is making it. */ LoopNoEnv = Loop | NoEnv, LoopRemoveAtDistance = Loop | RemoveAtDistance }; // Used for creating a type mask for SoundManager::pauseSounds and resumeSounds inline int operator~(Type a) { return ~static_cast(a); } inline int operator&(Type a, Type b) { return static_cast(a) & static_cast(b); } inline int operator&(int a, Type b) { return a & static_cast(b); } inline int operator|(Type a, Type b) { return static_cast(a) | static_cast(b); } } namespace MWBase { using Sound = MWSound::Sound; using SoundStream = MWSound::Stream; /// \brief Interface for sound manager (implemented in MWSound) class SoundManager { SoundManager (const SoundManager&); ///< not implemented SoundManager& operator= (const SoundManager&); ///< not implemented protected: using PlayMode = MWSound::PlayMode; using Type = MWSound::Type; public: SoundManager() {} virtual ~SoundManager() {} virtual void processChangedSettings(const std::set< std::pair >& settings) = 0; virtual void stopMusic() = 0; ///< Stops music if it's playing virtual void streamMusic(const std::string& filename) = 0; ///< Play a soundifle /// \param filename name of a sound file in "Music/" in the data directory. virtual bool isMusicPlaying() = 0; ///< Returns true if music is playing virtual void playPlaylist(const std::string &playlist) = 0; ///< Start playing music from the selected folder /// \param name of the folder that contains the playlist virtual void playTitleMusic() = 0; ///< Start playing title music virtual void say(const MWWorld::ConstPtr &reference, const std::string& filename) = 0; ///< Make an actor say some text. /// \param filename name of a sound file in "Sound/" in the data directory. virtual void say(const std::string& filename) = 0; ///< Say some text, without an actor ref /// \param filename name of a sound file in "Sound/" in the data directory. virtual bool sayActive(const MWWorld::ConstPtr &reference=MWWorld::ConstPtr()) const = 0; ///< Is actor not speaking? virtual bool sayDone(const MWWorld::ConstPtr &reference=MWWorld::ConstPtr()) const = 0; ///< For scripting backward compatibility virtual void stopSay(const MWWorld::ConstPtr &reference=MWWorld::ConstPtr()) = 0; ///< Stop an actor speaking virtual float getSaySoundLoudness(const MWWorld::ConstPtr& reference) const = 0; ///< Check the currently playing say sound for this actor /// and get an average loudness value (scale [0,1]) at the current time position. /// If the actor is not saying anything, returns 0. virtual SoundStream *playTrack(const MWSound::DecoderPtr& decoder, Type type) = 0; ///< Play a 2D audio track, using a custom decoder. The caller is expected to call /// stopTrack with the returned handle when done. virtual void stopTrack(SoundStream *stream) = 0; ///< Stop the given audio track from playing virtual double getTrackTimeDelay(SoundStream *stream) = 0; ///< Retives the time delay, in seconds, of the audio track (must be a sound /// returned by \ref playTrack). Only intended to be called by the track /// decoder's read method. virtual Sound *playSound(const std::string& soundId, float volume, float pitch, Type type=Type::Sfx, PlayMode mode=PlayMode::Normal, float offset=0) = 0; ///< Play a sound, independently of 3D-position ///< @param offset Number of seconds into the sound to start playback. virtual Sound *playSound3D(const MWWorld::ConstPtr &reference, const std::string& soundId, float volume, float pitch, Type type=Type::Sfx, PlayMode mode=PlayMode::Normal, float offset=0) = 0; ///< Play a 3D sound attached to an MWWorld::Ptr. Will be updated automatically with the Ptr's position, unless Play_NoTrack is specified. ///< @param offset Number of seconds into the sound to start playback. virtual Sound *playSound3D(const osg::Vec3f& initialPos, const std::string& soundId, float volume, float pitch, Type type=Type::Sfx, PlayMode mode=PlayMode::Normal, float offset=0) = 0; ///< Play a 3D sound at \a initialPos. If the sound should be moving, it must be updated using Sound::setPosition. virtual void stopSound(Sound *sound) = 0; ///< Stop the given sound from playing virtual void stopSound3D(const MWWorld::ConstPtr &reference, const std::string& soundId) = 0; ///< Stop the given object from playing the given sound, virtual void stopSound3D(const MWWorld::ConstPtr &reference) = 0; ///< Stop the given object from playing all sounds. virtual void stopSound(const MWWorld::CellStore *cell) = 0; ///< Stop all sounds for the given cell. virtual void fadeOutSound3D(const MWWorld::ConstPtr &reference, const std::string& soundId, float duration) = 0; ///< Fade out given sound (that is already playing) of given object ///< @param reference Reference to object, whose sound is faded out ///< @param soundId ID of the sound to fade out. ///< @param duration Time until volume reaches 0. virtual bool getSoundPlaying(const MWWorld::ConstPtr &reference, const std::string& soundId) const = 0; ///< Is the given sound currently playing on the given object? /// If you want to check if sound played with playSound is playing, use empty Ptr virtual void pauseSounds(MWSound::BlockerType blocker, int types=int(Type::Mask)) = 0; ///< Pauses all currently playing sounds, including music. virtual void resumeSounds(MWSound::BlockerType blocker) = 0; ///< Resumes all previously paused sounds. virtual void pausePlayback() = 0; virtual void resumePlayback() = 0; virtual void update(float duration) = 0; virtual void setListenerPosDir(const osg::Vec3f &pos, const osg::Vec3f &dir, const osg::Vec3f &up, bool underwater) = 0; virtual void updatePtr(const MWWorld::ConstPtr& old, const MWWorld::ConstPtr& updated) = 0; virtual void clear() = 0; }; } #endif ================================================ FILE: apps/openmw/mwbase/statemanager.hpp ================================================ #ifndef GAME_MWSTATE_STATEMANAGER_H #define GAME_MWSTATE_STATEMANAGER_H #include #include namespace MWState { struct Slot; class Character; } namespace MWBase { /// \brief Interface for game state manager (implemented in MWState) class StateManager { public: enum State { State_NoGame, State_Ended, State_Running }; typedef std::list::const_iterator CharacterIterator; private: StateManager (const StateManager&); ///< not implemented StateManager& operator= (const StateManager&); ///< not implemented public: StateManager() {} virtual ~StateManager() {} virtual void requestQuit() = 0; virtual bool hasQuitRequest() const = 0; virtual void askLoadRecent() = 0; virtual State getState() const = 0; virtual void newGame (bool bypass = false) = 0; ///< Start a new game. /// /// \param bypass Skip new game mechanics. virtual void endGame() = 0; virtual void resumeGame() = 0; virtual void deleteGame (const MWState::Character *character, const MWState::Slot *slot) = 0; virtual void saveGame (const std::string& description, const MWState::Slot *slot = nullptr) = 0; ///< Write a saved game to \a slot or create a new slot if \a slot == 0. /// /// \note Slot must belong to the current character. virtual void loadGame (const std::string& filepath) = 0; ///< Load a saved game directly from the given file path. This will search the CharacterManager /// for a Character containing this save file, and set this Character current if one was found. /// Otherwise, a new Character will be created. virtual void loadGame (const MWState::Character *character, const std::string& filepath) = 0; ///< Load a saved game file belonging to the given character. ///Simple saver, writes over the file if already existing /** Used for quick save and autosave **/ virtual void quickSave(std::string = "Quicksave")=0; ///Simple loader, loads the last saved file /** Used for quickload **/ virtual void quickLoad()=0; virtual MWState::Character *getCurrentCharacter () = 0; ///< @note May return null. virtual CharacterIterator characterBegin() = 0; ///< Any call to SaveGame and getCurrentCharacter can invalidate the returned /// iterator. virtual CharacterIterator characterEnd() = 0; virtual void update (float duration) = 0; }; } #endif ================================================ FILE: apps/openmw/mwbase/windowmanager.hpp ================================================ #ifndef GAME_MWBASE_WINDOWMANAGER_H #define GAME_MWBASE_WINDOWMANAGER_H #include #include #include #include #include #include #include "../mwgui/mode.hpp" #include namespace Loading { class Listener; } namespace Translation { class Storage; } namespace MyGUI { class Gui; class Widget; class UString; } namespace ESM { class ESMReader; class ESMWriter; struct CellId; } namespace MWMechanics { class AttributeValue; template class DynamicStat; class SkillValue; } namespace MWWorld { class CellStore; class Ptr; } namespace MWGui { class Layout; class Console; class SpellWindow; class TradeWindow; class TravelWindow; class SpellBuyingWindow; class ConfirmationDialog; class CountDialog; class ScrollWindow; class BookWindow; class InventoryWindow; class ContainerWindow; class DialogueWindow; class WindowModal; class JailScreen; enum ShowInDialogueMode { ShowInDialogueMode_IfPossible, ShowInDialogueMode_Only, ShowInDialogueMode_Never }; struct TextColours; } namespace SFO { class CursorManager; } namespace MWBase { /// \brief Interface for widnow manager (implemented in MWGui) class WindowManager : public SDLUtil::WindowListener { WindowManager (const WindowManager&); ///< not implemented WindowManager& operator= (const WindowManager&); ///< not implemented public: typedef std::vector SkillList; WindowManager() {} virtual ~WindowManager() {} /// @note This method will block until the video finishes playing /// (and will continually update the window while doing so) virtual void playVideo(const std::string& name, bool allowSkipping) = 0; virtual void setNewGame(bool newgame) = 0; virtual void pushGuiMode (MWGui::GuiMode mode, const MWWorld::Ptr& arg) = 0; virtual void pushGuiMode (MWGui::GuiMode mode) = 0; virtual void popGuiMode(bool noSound=false) = 0; virtual void removeGuiMode (MWGui::GuiMode mode, bool noSound=false) = 0; ///< can be anywhere in the stack virtual void goToJail(int days) = 0; virtual void updatePlayer() = 0; virtual MWGui::GuiMode getMode() const = 0; virtual bool containsMode(MWGui::GuiMode) const = 0; virtual bool isGuiMode() const = 0; virtual bool isConsoleMode() const = 0; virtual void toggleVisible (MWGui::GuiWindow wnd) = 0; virtual void forceHide(MWGui::GuiWindow wnd) = 0; virtual void unsetForceHide(MWGui::GuiWindow wnd) = 0; /// Disallow all inventory mode windows virtual void disallowAll() = 0; /// Allow one or more windows virtual void allow (MWGui::GuiWindow wnd) = 0; virtual bool isAllowed (MWGui::GuiWindow wnd) const = 0; /// \todo investigate, if we really need to expose every single lousy UI element to the outside world virtual MWGui::InventoryWindow* getInventoryWindow() = 0; virtual MWGui::CountDialog* getCountDialog() = 0; virtual MWGui::ConfirmationDialog* getConfirmationDialog() = 0; virtual MWGui::TradeWindow* getTradeWindow() = 0; /* Start of tes3mp addition Make it possible to get the ContainerWindow from elsewhere in the code */ virtual MWGui::ContainerWindow* getContainerWindow() = 0; /* End of tes3mp addition */ /* Start of tes3mp addition Make it possible to get the DialogueWindow from elsewhere */ virtual MWGui::DialogueWindow* getDialogueWindow() = 0; /* End of tes3mp addition */ /// Make the player use an item, while updating GUI state accordingly virtual void useItem(const MWWorld::Ptr& item, bool force=false) = 0; virtual void updateSpellWindow() = 0; virtual void setConsoleSelectedObject(const MWWorld::Ptr& object) = 0; /* Start of tes3mp addition Allow the direct setting of a console's Ptr, without the assumption that an object was clicked and that key focus should be restored to the console window, for console commands executed via server scripts */ virtual void setConsolePtr(const MWWorld::Ptr& object) = 0; /* End of tes3mp addition */ /* Start of tes3mp addition Allow the clearing of the console's Ptr from elsewhere in the code, so that Ptrs used in console commands run from server scripts do not stay selected */ virtual void clearConsolePtr() = 0; /* End of tes3mp addition */ /// Set time left for the player to start drowning (update the drowning bar) /// @param time time left to start drowning /// @param maxTime how long we can be underwater (in total) until drowning starts virtual void setDrowningTimeLeft (float time, float maxTime) = 0; virtual void changeCell(const MWWorld::CellStore* cell) = 0; ///< change the active cell /* Start of tes3mp addition Allow the setting of the image data for a global map tile from elsewhere in the code */ virtual void setGlobalMapImage(int cellX, int cellY, const std::vector& imageData) = 0; /* End of tes3mp addition */ virtual void setFocusObject(const MWWorld::Ptr& focus) = 0; virtual void setFocusObjectScreenCoords(float min_x, float min_y, float max_x, float max_y) = 0; virtual void setCursorVisible(bool visible) = 0; virtual void setCursorActive(bool active) = 0; virtual void getMousePosition(int &x, int &y) = 0; virtual void getMousePosition(float &x, float &y) = 0; virtual void setDragDrop(bool dragDrop) = 0; /* Start of tes3mp addition Allow the completion of a drag and drop from elsewhere in the code */ virtual void finishDragDrop() = 0; /* End of tes3mp addition */ virtual bool getWorldMouseOver() = 0; virtual float getScalingFactor() = 0; virtual bool toggleFogOfWar() = 0; virtual bool toggleFullHelp() = 0; ///< show extra info in item tooltips (owner, script) virtual bool getFullHelp() const = 0; virtual void setActiveMap(int x, int y, bool interior) = 0; ///< set the indices of the map texture that should be used /// sets the visibility of the drowning bar virtual void setDrowningBarVisibility(bool visible) = 0; /// sets the visibility of the hud health/magicka/stamina bars virtual void setHMSVisibility(bool visible) = 0; /// sets the visibility of the hud minimap virtual void setMinimapVisibility(bool visible) = 0; virtual void setWeaponVisibility(bool visible) = 0; virtual void setSpellVisibility(bool visible) = 0; virtual void setSneakVisibility(bool visible) = 0; /// activate selected quick key virtual void activateQuickKey (int index) = 0; /// update activated quick key state (if action executing was delayed for some reason) virtual void updateActivatedQuickKey () = 0; /* Start of tes3mp addition Make it possible to add quickKeys from elsewhere in the code */ virtual void setQuickKey(int slot, int quickKeyType, MWWorld::Ptr item, const std::string& spellId = "") = 0; /* End of tes3mp addition */ virtual std::string getSelectedSpell() = 0; virtual void setSelectedSpell(const std::string& spellId, int successChancePercent) = 0; virtual void setSelectedEnchantItem(const MWWorld::Ptr& item) = 0; virtual const MWWorld::Ptr& getSelectedEnchantItem() const = 0; virtual void setSelectedWeapon(const MWWorld::Ptr& item) = 0; virtual const MWWorld::Ptr& getSelectedWeapon() const = 0; virtual int getFontHeight() const = 0; virtual void unsetSelectedSpell() = 0; virtual void unsetSelectedWeapon() = 0; virtual void showCrosshair(bool show) = 0; virtual bool getSubtitlesEnabled() = 0; virtual bool toggleHud() = 0; virtual void disallowMouse() = 0; virtual void allowMouse() = 0; virtual void notifyInputActionBound() = 0; virtual void addVisitedLocation(const std::string& name, int x, int y) = 0; /// Hides dialog and schedules dialog to be deleted. virtual void removeDialog(MWGui::Layout* dialog) = 0; ///Gracefully attempts to exit the topmost GUI mode /** No guarantee of actually closing the window **/ virtual void exitCurrentGuiMode() = 0; virtual void messageBox (const std::string& message, enum MWGui::ShowInDialogueMode showInDialogueMode = MWGui::ShowInDialogueMode_IfPossible) = 0; virtual void staticMessageBox(const std::string& message) = 0; virtual void removeStaticMessageBox() = 0; /* Start of tes3mp change (major) Add a hasServerOrigin boolean to the list of arguments so those messageboxes can be differentiated from client-only ones */ virtual void interactiveMessageBox (const std::string& message, const std::vector& buttons = std::vector(), bool block=false, bool hasServerOrigin=false) = 0; /* End of tes3mp change (major) */ /// returns the index of the pressed button or -1 if no button was pressed (->MessageBoxmanager->InteractiveMessageBox) virtual int readPressedButton() = 0; virtual void update (float duration) = 0; virtual void updateConsoleObjectPtr(const MWWorld::Ptr& currentPtr, const MWWorld::Ptr& newPtr) = 0; /** * Fetches a GMST string from the store, if there is no setting with the given * ID or it is not a string the default string is returned. * * @param id Identifier for the GMST setting, e.g. "aName" * @param default Default value if the GMST setting cannot be used. */ virtual std::string getGameSettingString(const std::string &id, const std::string &default_) = 0; virtual void processChangedSettings(const std::set< std::pair >& changed) = 0; virtual void executeInConsole (const std::string& path) = 0; /* Start of tes3mp addition Allow the execution of console commands from elsewhere in the code */ virtual void executeCommandInConsole(const std::string& command) = 0; /* End of tes3mp addition */ virtual void enableRest() = 0; virtual bool getRestEnabled() = 0; virtual bool getJournalAllowed() = 0; virtual bool getPlayerSleeping() = 0; virtual void wakeUpPlayer() = 0; virtual void showSoulgemDialog (MWWorld::Ptr item) = 0; virtual void changePointer (const std::string& name) = 0; virtual void setEnemy (const MWWorld::Ptr& enemy) = 0; virtual int getMessagesCount() const = 0; virtual const Translation::Storage& getTranslationDataStorage() const = 0; /// Warning: do not use MyGUI::InputManager::setKeyFocusWidget directly. Instead use this. virtual void setKeyFocusWidget (MyGUI::Widget* widget) = 0; virtual void loadUserFonts() = 0; virtual Loading::Listener* getLoadingScreen() = 0; /// Should the cursor be visible? virtual bool getCursorVisible() = 0; /// Clear all savegame-specific data virtual void clear() = 0; virtual void write (ESM::ESMWriter& writer, Loading::Listener& progress) = 0; virtual void readRecord (ESM::ESMReader& reader, uint32_t type) = 0; virtual int countSavedGameRecords() const = 0; /// Does the current stack of GUI-windows permit saving? virtual bool isSavingAllowed() const = 0; /// Send exit command to active Modal window virtual void exitCurrentModal() = 0; /// Sets the current Modal /** Used to send exit command to active Modal when Esc is pressed **/ virtual void addCurrentModal(MWGui::WindowModal* input) = 0; /// Removes the top Modal /** Used when one Modal adds another Modal \param input Pointer to the current modal, to ensure proper modal is removed **/ virtual void removeCurrentModal(MWGui::WindowModal* input) = 0; virtual void pinWindow (MWGui::GuiWindow window) = 0; virtual void toggleMaximized(MWGui::Layout *layout) = 0; /// Fade the screen in, over \a time seconds virtual void fadeScreenIn(const float time, bool clearQueue=true, float delay=0.f) = 0; /// Fade the screen out to black, over \a time seconds virtual void fadeScreenOut(const float time, bool clearQueue=true, float delay=0.f) = 0; /// Fade the screen to a specified percentage of black, over \a time seconds virtual void fadeScreenTo(const int percent, const float time, bool clearQueue=true, float delay=0.f) = 0; /// Darken the screen to a specified percentage virtual void setBlindness(const int percent) = 0; virtual void activateHitOverlay(bool interrupt=true) = 0; virtual void setWerewolfOverlay(bool set) = 0; virtual void toggleConsole() = 0; virtual void toggleDebugWindow() = 0; /// Cycle to next or previous spell virtual void cycleSpell(bool next) = 0; /// Cycle to next or previous weapon virtual void cycleWeapon(bool next) = 0; virtual void playSound(const std::string& soundId, float volume = 1.f, float pitch = 1.f) = 0; // In WindowManager for now since there isn't a VFS singleton virtual std::string correctIconPath(const std::string& path) = 0; virtual std::string correctBookartPath(const std::string& path, int width, int height, bool* exists = nullptr) = 0; virtual std::string correctTexturePath(const std::string& path) = 0; virtual bool textureExists(const std::string& path) = 0; virtual void addCell(MWWorld::CellStore* cell) = 0; virtual void removeCell(MWWorld::CellStore* cell) = 0; virtual void writeFog(MWWorld::CellStore* cell) = 0; virtual const MWGui::TextColours& getTextColours() = 0; virtual bool injectKeyPress(MyGUI::KeyCode key, unsigned int text, bool repeat) = 0; virtual bool injectKeyRelease(MyGUI::KeyCode key) = 0; void windowVisibilityChange(bool visible) override = 0; void windowResized(int x, int y) override = 0; void windowClosed() override = 0; virtual bool isWindowVisible() = 0; virtual void watchActor(const MWWorld::Ptr& ptr) = 0; virtual MWWorld::Ptr getWatchedActor() const = 0; }; } #endif ================================================ FILE: apps/openmw/mwbase/world.hpp ================================================ #ifndef GAME_MWBASE_WORLD_H #define GAME_MWBASE_WORLD_H #include "rotationflags.hpp" #include #include #include #include #include /* Start of tes3mp addition Include additional headers for multiplayer purposes */ #include /* End of tes3mp addition */ #include #include "../mwworld/ptr.hpp" #include "../mwworld/doorstate.hpp" #include "../mwrender/rendermode.hpp" namespace osg { class Vec3f; class Matrixf; class Quat; class Image; class Stats; } namespace Loading { class Listener; } namespace ESM { class ESMReader; class ESMWriter; struct Position; struct Cell; struct Class; struct Container; struct Creature; struct Potion; struct Spell; struct NPC; struct Armor; struct Weapon; struct Clothing; struct Enchantment; struct Book; struct EffectList; struct CreatureLevList; struct ItemLevList; struct TimeStamp; } namespace MWPhysics { class RayCastingInterface; } namespace MWRender { class Animation; } namespace MWMechanics { struct Movement; } namespace DetourNavigator { struct Navigator; } namespace MWWorld { class CellStore; class Player; class LocalScripts; class TimeStamp; class ESMStore; class RefData; typedef std::vector > PtrMovementList; } namespace MWBase { /// \brief Interface for the World (implemented in MWWorld) class World { World (const World&); ///< not implemented World& operator= (const World&); ///< not implemented public: struct DoorMarker { std::string name; float x, y; // world position ESM::CellId dest; }; World() {} virtual ~World() {} virtual void startNewGame (bool bypass) = 0; ///< \param bypass Bypass regular game start. virtual void clear() = 0; virtual int countSavedGameRecords() const = 0; virtual int countSavedGameCells() const = 0; virtual void write (ESM::ESMWriter& writer, Loading::Listener& listener) const = 0; virtual void readRecord (ESM::ESMReader& reader, uint32_t type, const std::map& contentFileMap) = 0; virtual MWWorld::CellStore *getExterior (int x, int y) = 0; virtual MWWorld::CellStore *getInterior (const std::string& name) = 0; virtual MWWorld::CellStore *getCell (const ESM::CellId& id) = 0; virtual void testExteriorCells() = 0; virtual void testInteriorCells() = 0; virtual void useDeathCamera() = 0; virtual void setWaterHeight(const float height) = 0; virtual bool toggleWater() = 0; virtual bool toggleWorld() = 0; virtual bool toggleBorders() = 0; virtual void adjustSky() = 0; virtual MWWorld::Player& getPlayer() = 0; virtual MWWorld::Ptr getPlayerPtr() = 0; virtual MWWorld::ConstPtr getPlayerConstPtr() const = 0; virtual const MWWorld::ESMStore& getStore() const = 0; /* Start of tes3mp addition Make it possible to get the World's ESMStore as a non-const */ virtual MWWorld::ESMStore& getModifiableStore() = 0; /* End of tes3mp addition */ virtual std::vector& getEsmReader() = 0; virtual MWWorld::LocalScripts& getLocalScripts() = 0; virtual bool hasCellChanged() const = 0; ///< Has the set of active cells changed, since the last frame? virtual bool isCellExterior() const = 0; virtual bool isCellQuasiExterior() const = 0; virtual osg::Vec2f getNorthVector (const MWWorld::CellStore* cell) = 0; ///< get north vector for given interior cell virtual void getDoorMarkers (MWWorld::CellStore* cell, std::vector& out) = 0; ///< get a list of teleport door markers for a given cell, to be displayed on the local map /* Start of tes3mp addition Make it possible to check whether global variables exist and to create new ones */ virtual bool hasGlobal(const std::string& name) = 0; virtual void createGlobal(const std::string& name, ESM::VarType varType) = 0; /* End of tes3mp addition */ virtual void setGlobalInt (const std::string& name, int value) = 0; ///< Set value independently from real type. virtual void setGlobalFloat (const std::string& name, float value) = 0; ///< Set value independently from real type. virtual int getGlobalInt (const std::string& name) const = 0; ///< Get value independently from real type. virtual float getGlobalFloat (const std::string& name) const = 0; ///< Get value independently from real type. virtual char getGlobalVariableType (const std::string& name) const = 0; ///< Return ' ', if there is no global variable with this name. virtual std::string getCellName (const MWWorld::CellStore *cell = nullptr) const = 0; ///< Return name of the cell. /// /// \note If cell==0, the cell the player is currently in will be used instead to /// generate a name. virtual std::string getCellName(const ESM::Cell* cell) const = 0; virtual void removeRefScript (MWWorld::RefData *ref) = 0; //< Remove the script attached to ref from mLocalScripts virtual MWWorld::Ptr getPtr (const std::string& name, bool activeOnly) = 0; ///< Return a pointer to a liveCellRef with the given name. /// \param activeOnly do non search inactive cells. virtual MWWorld::Ptr searchPtr (const std::string& name, bool activeOnly, bool searchInContainers = true) = 0; ///< Return a pointer to a liveCellRef with the given name. /// \param activeOnly do non search inactive cells. virtual MWWorld::Ptr searchPtrViaActorId (int actorId) = 0; ///< Search is limited to the active cells. virtual MWWorld::Ptr searchPtrViaRefNum (const std::string& id, const ESM::RefNum& refNum) = 0; /* Start of tes3mp addition Make it possible to find a Ptr in any active cell based on its refNum and mpNum */ virtual MWWorld::Ptr searchPtrViaUniqueIndex(int refNum, int mpNum) = 0; /* End of tes3mp addition */ /* Start of tes3mp addition Make it possible to update all Ptrs in active cells that have a certain refId */ virtual void updatePtrsWithRefId(std::string refId) = 0; /* End of tes3mp addition */ virtual MWWorld::Ptr findContainer (const MWWorld::ConstPtr& ptr) = 0; ///< Return a pointer to a liveCellRef which contains \a ptr. /// \note Search is limited to the active cells. virtual void enable (const MWWorld::Ptr& ptr) = 0; virtual void disable (const MWWorld::Ptr& ptr) = 0; virtual void advanceTime (double hours, bool incremental = false) = 0; ///< Advance in-game time. virtual std::string getMonthName (int month = -1) const = 0; ///< Return name of month (-1: current month) virtual MWWorld::TimeStamp getTimeStamp() const = 0; ///< Return current in-game time and number of day since new game start. virtual ESM::EpochTimeStamp getEpochTimeStamp() const = 0; ///< Return current in-game date and time. virtual bool toggleSky() = 0; ///< \return Resulting mode virtual void changeWeather(const std::string& region, const unsigned int id) = 0; /* Start of tes3mp addition Make it possible to set a specific weather state for a region from elsewhere in the code */ virtual void setRegionWeather(const std::string& region, const unsigned int currentWeather, const unsigned int nextWeather, const unsigned int queuedWeather, const float transitionFactor, bool force) = 0; /* End of tes3mp addition */ /* Start of tes3mp addition Make it possible to check whether the local WeatherManager has the ability to create weather changes */ virtual bool getWeatherCreationState() = 0; /* End of tes3mp addition */ /* Start of tes3mp addition Make it possible to enable and disable the local WeatherManager's ability to create weather changes */ virtual void setWeatherCreationState(bool state) = 0; /* End of tes3mp addition */ /* Start of tes3mp addition Make it possible to send the current weather in a WorldWeather packet when requested from elsewhere in the code */ virtual void sendWeather() = 0; /* End of tes3mp addition */ virtual int getCurrentWeather() const = 0; virtual unsigned int getNightDayMode() const = 0; virtual int getMasserPhase() const = 0; virtual int getSecundaPhase() const = 0; virtual void setMoonColour (bool red) = 0; virtual void modRegion(const std::string ®ionid, const std::vector &chances) = 0; virtual float getTimeScaleFactor() const = 0; virtual void changeToInteriorCell (const std::string& cellName, const ESM::Position& position, bool adjustPlayerPos, bool changeEvent=true) = 0; ///< Move to interior cell. ///< @param changeEvent If false, do not trigger cell change flag or detect worldspace changes virtual void changeToExteriorCell (const ESM::Position& position, bool adjustPlayerPos, bool changeEvent=true) = 0; ///< Move to exterior cell. ///< @param changeEvent If false, do not trigger cell change flag or detect worldspace changes virtual void changeToCell (const ESM::CellId& cellId, const ESM::Position& position, bool adjustPlayerPos, bool changeEvent=true) = 0; ///< @param changeEvent If false, do not trigger cell change flag or detect worldspace changes virtual const ESM::Cell *getExterior (const std::string& cellName) const = 0; ///< Return a cell matching the given name or a 0-pointer, if there is no such cell. virtual void markCellAsUnchanged() = 0; virtual MWWorld::Ptr getFacedObject() = 0; ///< Return pointer to the object the player is looking at, if it is within activation range /* Start of tes3mp addition This has been declared here so it can be accessed from places other than MWWorld::World */ virtual void updateWeather(float duration, bool paused = false) = 0; /* End of tes3mp addition */ /* Start of tes3mp addition This has been declared here so it can be accessed from places other than MWWorld::World */ virtual void PCDropped(const MWWorld::Ptr& item) = 0; /* End of tes3mp addition */ virtual float getDistanceToFacedObject() = 0; virtual float getMaxActivationDistance() = 0; /// Returns a pointer to the object the provided object would hit (if within the /// specified distance), and the point where the hit occurs. This will attempt to /// use the "Head" node, or alternatively the "Bip01 Head" node as a basis. virtual std::pair getHitContact(const MWWorld::ConstPtr &ptr, float distance, std::vector &targets) = 0; virtual void adjustPosition (const MWWorld::Ptr& ptr, bool force) = 0; ///< Adjust position after load to be on ground. Must be called after model load. /// @param force do this even if the ptr is flying virtual void fixPosition () = 0; ///< Attempt to fix position so that the player is not stuck inside the geometry. /// @note No-op for items in containers. Use ContainerStore::removeItem instead. virtual void deleteObject (const MWWorld::Ptr& ptr) = 0; virtual void undeleteObject (const MWWorld::Ptr& ptr) = 0; virtual MWWorld::Ptr moveObject (const MWWorld::Ptr& ptr, float x, float y, float z, bool movePhysics=true, bool moveToActive=false) = 0; ///< @return an updated Ptr in case the Ptr's cell changes virtual MWWorld::Ptr moveObject(const MWWorld::Ptr &ptr, MWWorld::CellStore* newCell, float x, float y, float z, bool movePhysics=true) = 0; ///< @return an updated Ptr virtual MWWorld::Ptr moveObjectBy(const MWWorld::Ptr &ptr, osg::Vec3f vec, bool moveToActive, bool ignoreCollisions) = 0; ///< @return an updated Ptr virtual void scaleObject (const MWWorld::Ptr& ptr, float scale) = 0; virtual void rotateObject(const MWWorld::Ptr& ptr, float x, float y, float z, RotationFlags flags = RotationFlag_inverseOrder) = 0; virtual MWWorld::Ptr placeObject(const MWWorld::ConstPtr& ptr, MWWorld::CellStore* cell, ESM::Position pos) = 0; ///< Place an object. Makes a copy of the Ptr. virtual MWWorld::Ptr safePlaceObject (const MWWorld::ConstPtr& ptr, const MWWorld::ConstPtr& referenceObject, MWWorld::CellStore* referenceCell, int direction, float distance) = 0; ///< Place an object in a safe place next to \a referenceObject. \a direction and \a distance specify the wanted placement /// relative to \a referenceObject (but the object may be placed somewhere else if the wanted location is obstructed). virtual void indexToPosition (int cellX, int cellY, float &x, float &y, bool centre = false) const = 0; ///< Convert cell numbers to position. virtual void positionToIndex (float x, float y, int &cellX, int &cellY) const = 0; ///< Convert position to cell numbers virtual void queueMovement(const MWWorld::Ptr &ptr, const osg::Vec3f &velocity) = 0; ///< Queues movement for \a ptr (in local space), to be applied in the next call to /// doPhysics. /* Start of tes3mp addition Make it possible to set the inertial force of a Ptr directly */ virtual void setInertialForce(const MWWorld::Ptr& ptr, const osg::Vec3f &force) = 0; /* End of tes3mp addition */ /* Start of tes3mp addition Make it possible to set whether a Ptr is on the ground or not, needed for proper synchronization in multiplayer */ virtual void setOnGround(const MWWorld::Ptr& ptr, bool onGround) = 0; /* End of tes3mp addition */ /* Start of tes3mp addition Make it possible to set the physics framerate from elsewhere */ virtual void setPhysicsFramerate(float physFramerate) = 0; /* End of tes3mp addition */ virtual void updateAnimatedCollisionShape(const MWWorld::Ptr &ptr) = 0; virtual const MWPhysics::RayCastingInterface* getRayCasting() const = 0; virtual bool castRay (float x1, float y1, float z1, float x2, float y2, float z2, int mask) = 0; ///< cast a Ray and return true if there is an object in the ray path. virtual bool castRay (float x1, float y1, float z1, float x2, float y2, float z2) = 0; virtual bool castRay(const osg::Vec3f& from, const osg::Vec3f& to, int mask, const MWWorld::ConstPtr& ignore) = 0; virtual void setActorCollisionMode(const MWWorld::Ptr& ptr, bool internal, bool external) = 0; virtual bool isActorCollisionEnabled(const MWWorld::Ptr& ptr) = 0; virtual bool toggleCollisionMode() = 0; ///< Toggle collision mode for player. If disabled player object should ignore /// collisions and gravity. /// \return Resulting mode virtual bool toggleRenderMode (MWRender::RenderMode mode) = 0; ///< Toggle a render mode. ///< \return Resulting mode virtual const ESM::Potion *createRecord (const ESM::Potion& record) = 0; ///< Create a new record (of type potion) in the ESM store. /// \return pointer to created record virtual const ESM::Spell *createRecord (const ESM::Spell& record) = 0; ///< Create a new record (of type spell) in the ESM store. /// \return pointer to created record virtual const ESM::Class *createRecord (const ESM::Class& record) = 0; ///< Create a new record (of type class) in the ESM store. /// \return pointer to created record virtual const ESM::Cell *createRecord (const ESM::Cell& record) = 0; ///< Create a new record (of type cell) in the ESM store. /// \return pointer to created record virtual const ESM::NPC *createRecord(const ESM::NPC &record) = 0; ///< Create a new record (of type npc) in the ESM store. /// \return pointer to created record virtual const ESM::Creature *createRecord (const ESM::Creature &record) = 0; ///< Create a new record (of type creature) in the ESM store. /// \return pointer to created record virtual const ESM::Armor *createRecord (const ESM::Armor& record) = 0; ///< Create a new record (of type armor) in the ESM store. /// \return pointer to created record virtual const ESM::Weapon *createRecord (const ESM::Weapon& record) = 0; ///< Create a new record (of type weapon) in the ESM store. /// \return pointer to created record virtual const ESM::Clothing *createRecord (const ESM::Clothing& record) = 0; ///< Create a new record (of type clothing) in the ESM store. /// \return pointer to created record virtual const ESM::Enchantment *createRecord (const ESM::Enchantment& record) = 0; ///< Create a new record (of type enchantment) in the ESM store. /// \return pointer to created record virtual const ESM::Book *createRecord (const ESM::Book& record) = 0; ///< Create a new record (of type book) in the ESM store. /// \return pointer to created record virtual const ESM::CreatureLevList *createOverrideRecord (const ESM::CreatureLevList& record) = 0; ///< Write this record to the ESM store, allowing it to override a pre-existing record with the same ID. /// \return pointer to created record virtual const ESM::ItemLevList *createOverrideRecord (const ESM::ItemLevList& record) = 0; ///< Write this record to the ESM store, allowing it to override a pre-existing record with the same ID. /// \return pointer to created record virtual const ESM::Creature *createOverrideRecord (const ESM::Creature& record) = 0; ///< Write this record to the ESM store, allowing it to override a pre-existing record with the same ID. /// \return pointer to created record virtual const ESM::NPC *createOverrideRecord (const ESM::NPC& record) = 0; ///< Write this record to the ESM store, allowing it to override a pre-existing record with the same ID. /// \return pointer to created record virtual const ESM::Container *createOverrideRecord (const ESM::Container& record) = 0; ///< Write this record to the ESM store, allowing it to override a pre-existing record with the same ID. /// \return pointer to created record virtual void update (float duration, bool paused) = 0; virtual void updatePhysics (float duration, bool paused, osg::Timer_t frameStart, unsigned int frameNumber, osg::Stats& stats) = 0; virtual void updateWindowManager () = 0; virtual MWWorld::Ptr placeObject (const MWWorld::ConstPtr& object, float cursorX, float cursorY, int amount) = 0; ///< copy and place an object into the gameworld at the specified cursor position /// @param object /// @param cursor X (relative 0-1) /// @param cursor Y (relative 0-1) /// @param number of objects to place virtual MWWorld::Ptr dropObjectOnGround (const MWWorld::Ptr& actor, const MWWorld::ConstPtr& object, int amount) = 0; ///< copy and place an object into the gameworld at the given actor's position /// @param actor giving the dropped object position /// @param object /// @param number of objects to place virtual bool canPlaceObject (float cursorX, float cursorY) = 0; ///< @return true if it is possible to place on object at specified cursor location virtual void processChangedSettings (const std::set< std::pair >& settings) = 0; virtual bool isFlying(const MWWorld::Ptr &ptr) const = 0; virtual bool isSlowFalling(const MWWorld::Ptr &ptr) const = 0; virtual bool isSwimming(const MWWorld::ConstPtr &object) const = 0; virtual bool isWading(const MWWorld::ConstPtr &object) const = 0; ///Is the head of the creature underwater? virtual bool isSubmerged(const MWWorld::ConstPtr &object) const = 0; virtual bool isUnderwater(const MWWorld::CellStore* cell, const osg::Vec3f &pos) const = 0; virtual bool isUnderwater(const MWWorld::ConstPtr &object, const float heightRatio) const = 0; virtual bool isWaterWalkingCastableOnTarget(const MWWorld::ConstPtr &target) const = 0; virtual bool isOnGround(const MWWorld::Ptr &ptr) const = 0; virtual osg::Matrixf getActorHeadTransform(const MWWorld::ConstPtr& actor) const = 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; virtual void saveLoaded() = 0; virtual void setupPlayer() = 0; virtual void renderPlayer() = 0; /// open or close a non-teleport door (depending on current state) virtual void activateDoor(const MWWorld::Ptr& door) = 0; /// update movement state of a non-teleport door as specified /// @param state see MWClass::setDoorState /// @note throws an exception when invoked on a teleport door virtual void activateDoor(const MWWorld::Ptr& door, MWWorld::DoorState state) = 0; /* Start of tes3mp addition Useful self-contained method for saving door states */ virtual void saveDoorState(const MWWorld::Ptr& door, MWWorld::DoorState state) = 0; /* End of tes3mp addition */ /* Start of tes3mp addition Make it possible to check whether a cell is active */ virtual bool isCellActive(const ESM::Cell& cell) = 0; /* End of tes3mp addition */ /* Start of tes3mp addition Make it possible to unload a cell from elsewhere */ virtual void unloadCell(const ESM::Cell& cell) = 0; /* End of tes3mp addition */ /* Start of tes3mp addition Make it possible to unload all active cells from elsewhere */ virtual void unloadActiveCells() = 0; /* End of tes3mp addition */ /* Start of tes3mp addition Clear the CellStore for a specific Cell from elsewhere */ virtual void clearCellStore(const ESM::Cell& cell) = 0; /* End of tes3mp addition */ virtual void getActorsStandingOn (const MWWorld::ConstPtr& object, std::vector &actors) = 0; ///< get a list of actors standing on \a object virtual bool getPlayerStandingOn (const MWWorld::ConstPtr& object) = 0; ///< @return true if the player is standing on \a object virtual bool getActorStandingOn (const MWWorld::ConstPtr& object) = 0; ///< @return true if any actor is standing on \a object virtual bool getPlayerCollidingWith(const MWWorld::ConstPtr& object) = 0; ///< @return true if the player is colliding with \a object virtual bool getActorCollidingWith (const MWWorld::ConstPtr& object) = 0; ///< @return true if any actor is colliding with \a object virtual void hurtStandingActors (const MWWorld::ConstPtr& object, float dmgPerSecond) = 0; ///< Apply a health difference to any actors standing on \a object. /// To hurt actors, healthPerSecond should be a positive value. For a negative value, actors will be healed. virtual void hurtCollidingActors (const MWWorld::ConstPtr& object, float dmgPerSecond) = 0; ///< Apply a health difference to any actors colliding with \a object. /// To hurt actors, healthPerSecond should be a positive value. For a negative value, actors will be healed. virtual float getWindSpeed() = 0; virtual void getContainersOwnedBy (const MWWorld::ConstPtr& npc, std::vector& out) = 0; ///< get all containers in active cells owned by this Npc virtual void getItemsOwnedBy (const MWWorld::ConstPtr& npc, std::vector& out) = 0; ///< get all items in active cells owned by this Npc virtual bool getLOS(const MWWorld::ConstPtr& actor,const MWWorld::ConstPtr& targetActor) = 0; ///< get Line of Sight (morrowind stupid implementation) virtual float getDistToNearestRayHit(const osg::Vec3f& from, const osg::Vec3f& dir, float maxDist, bool includeWater = false) = 0; virtual void enableActorCollision(const MWWorld::Ptr& actor, bool enable) = 0; enum RestPermitted { Rest_Allowed = 0, Rest_OnlyWaiting = 1, Rest_PlayerIsInAir = 2, Rest_PlayerIsUnderwater = 3, Rest_EnemiesAreNearby = 4 }; /// check if the player is allowed to rest virtual RestPermitted canRest() const = 0; /// \todo Probably shouldn't be here virtual MWRender::Animation* getAnimation(const MWWorld::Ptr &ptr) = 0; virtual const MWRender::Animation* getAnimation(const MWWorld::ConstPtr &ptr) const = 0; virtual void reattachPlayerCamera() = 0; /// \todo this does not belong here virtual void screenshot (osg::Image* image, int w, int h) = 0; virtual bool screenshot360 (osg::Image* image) = 0; /// Find default position inside exterior cell specified by name /// \return false if exterior with given name not exists, true otherwise virtual bool findExteriorPosition(const std::string &name, ESM::Position &pos) = 0; /// Find default position inside interior cell specified by name /// \return false if interior with given name not exists, true otherwise virtual bool findInteriorPosition(const std::string &name, ESM::Position &pos) = 0; /// Enables or disables use of teleport spell effects (recall, intervention, etc). virtual void enableTeleporting(bool enable) = 0; /// Returns true if teleport spell effects are allowed. virtual bool isTeleportingEnabled() const = 0; /// Enables or disables use of levitation spell effect. virtual void enableLevitation(bool enable) = 0; /// Returns true if levitation spell effect is allowed. virtual bool isLevitationEnabled() const = 0; virtual bool getGodModeState() const = 0; virtual bool toggleGodMode() = 0; virtual bool toggleScripts() = 0; virtual bool getScriptsEnabled() const = 0; /** * @brief startSpellCast attempt to start casting a spell. Might fail immediately if conditions are not met. * @param actor * @return true if the spell can be casted (i.e. the animation should start) */ virtual bool startSpellCast (const MWWorld::Ptr& actor) = 0; virtual void castSpell (const MWWorld::Ptr& actor, bool manualSpell=false) = 0; virtual void launchMagicBolt (const std::string& spellId, const MWWorld::Ptr& caster, const osg::Vec3f& fallbackDirection) = 0; virtual void launchProjectile (MWWorld::Ptr& actor, MWWorld::Ptr& projectile, const osg::Vec3f& worldPos, const osg::Quat& orient, MWWorld::Ptr& bow, float speed, float attackStrength) = 0; virtual void updateProjectilesCasters() = 0; virtual void applyLoopingParticles(const MWWorld::Ptr& ptr) = 0; virtual const std::vector& getContentFiles() const = 0; virtual void breakInvisibility (const MWWorld::Ptr& actor) = 0; // Allow NPCs to use torches? virtual bool useTorches() const = 0; virtual bool findInteriorPositionInWorldSpace(const MWWorld::CellStore* cell, osg::Vec3f& result) = 0; /// Teleports \a ptr to the closest reference of \a id (e.g. DivineMarker, PrisonMarker, TempleMarker) /// @note id must be lower case virtual void teleportToClosestMarker (const MWWorld::Ptr& ptr, const std::string& id) = 0; enum DetectionType { Detect_Enchantment, Detect_Key, Detect_Creature }; /// List all references (filtered by \a type) detected by \a ptr. The range /// is determined by the current magnitude of the "Detect X" magic effect belonging to \a type. /// @note This also works for references in containers. virtual void listDetectedReferences (const MWWorld::Ptr& ptr, std::vector& out, DetectionType type) = 0; /// Update the value of some globals according to the world state, which may be used by dialogue entries. /// This should be called when initiating a dialogue. virtual void updateDialogueGlobals() = 0; /// Moves all stolen items from \a ptr to the closest evidence chest. virtual void confiscateStolenItems(const MWWorld::Ptr& ptr) = 0; virtual void goToJail () = 0; /// Spawn a random creature from a levelled list next to the player virtual void spawnRandomCreature(const std::string& creatureList) = 0; /// Spawn a blood effect for \a ptr at \a worldPosition virtual void spawnBloodEffect (const MWWorld::Ptr& ptr, const osg::Vec3f& worldPosition) = 0; virtual void spawnEffect (const std::string& model, const std::string& textureOverride, const osg::Vec3f& worldPos, float scale = 1.f, bool isMagicVFX = true) = 0; virtual void explodeSpell(const osg::Vec3f& origin, const ESM::EffectList& effects, const MWWorld::Ptr& caster, const MWWorld::Ptr& ignore, ESM::RangeType rangeType, const std::string& id, const std::string& sourceName, const bool fromProjectile=false) = 0; virtual void activate (const MWWorld::Ptr& object, const MWWorld::Ptr& actor) = 0; /// @see MWWorld::WeatherManager::isInStorm virtual bool isInStorm() const = 0; /// @see MWWorld::WeatherManager::getStormDirection virtual osg::Vec3f getStormDirection() const = 0; /// Resets all actors in the current active cells to their original location within that cell. virtual void resetActors() = 0; virtual bool isWalkingOnWater (const MWWorld::ConstPtr& actor) const = 0; /// Return a vector aiming the actor's weapon towards a target. /// @note The length of the vector is the distance between actor and target. virtual osg::Vec3f aimToTarget(const MWWorld::ConstPtr& actor, const MWWorld::ConstPtr& target, bool isRangedCombat) = 0; /// Return the distance between actor's weapon and target's collision box. virtual float getHitDistance(const MWWorld::ConstPtr& actor, const MWWorld::ConstPtr& target) = 0; virtual void addContainerScripts(const MWWorld::Ptr& reference, MWWorld::CellStore* cell) = 0; virtual void removeContainerScripts(const MWWorld::Ptr& reference) = 0; virtual bool isPlayerInJail() const = 0; virtual void rest(double hours) = 0; virtual void rechargeItems(double duration, bool activeOnly) = 0; virtual void setPlayerTraveling(bool traveling) = 0; virtual bool isPlayerTraveling() const = 0; virtual void rotateWorldObject (const MWWorld::Ptr& ptr, osg::Quat rotate) = 0; /// Return terrain height at \a worldPos position. virtual float getTerrainHeightAt(const osg::Vec3f& worldPos) const = 0; /// Return physical or rendering half extents of the given actor. virtual osg::Vec3f getHalfExtents(const MWWorld::ConstPtr& actor, bool rendering=false) const = 0; /// Export scene graph to a file and return the filename. /// \param ptr object to export scene graph for (if empty, export entire scene graph) virtual std::string exportSceneGraph(const MWWorld::Ptr& ptr) = 0; /// Preload VFX associated with this effect list virtual void preloadEffects(const ESM::EffectList* effectList) = 0; virtual DetourNavigator::Navigator* getNavigator() const = 0; virtual void updateActorPath(const MWWorld::ConstPtr& actor, const std::deque& path, const osg::Vec3f& halfExtents, const osg::Vec3f& start, const osg::Vec3f& end) const = 0; virtual void removeActorPath(const MWWorld::ConstPtr& actor) const = 0; virtual void setNavMeshNumberToRender(const std::size_t value) = 0; /// Return physical half extents of the given actor to be used in pathfinding virtual osg::Vec3f getPathfindingHalfExtents(const MWWorld::ConstPtr& actor) const = 0; virtual bool hasCollisionWithDoor(const MWWorld::ConstPtr& door, const osg::Vec3f& position, const osg::Vec3f& destination) const = 0; virtual bool isAreaOccupiedByOtherActor(const osg::Vec3f& position, const float radius, const MWWorld::ConstPtr& ignore) const = 0; virtual void reportStats(unsigned int frameNumber, osg::Stats& stats) const = 0; virtual std::vector getAll(const std::string& id) = 0; }; } #endif ================================================ FILE: apps/openmw/mwclass/activator.cpp ================================================ #include "activator.hpp" #include #include #include #include "../mwbase/environment.hpp" #include "../mwbase/windowmanager.hpp" #include "../mwbase/world.hpp" #include "../mwworld/cellstore.hpp" #include "../mwworld/esmstore.hpp" #include "../mwworld/ptr.hpp" #include "../mwworld/action.hpp" #include "../mwworld/failedaction.hpp" #include "../mwworld/nullaction.hpp" #include "../mwphysics/physicssystem.hpp" #include "../mwrender/objects.hpp" #include "../mwrender/renderinginterface.hpp" #include "../mwrender/vismask.hpp" #include "../mwgui/tooltips.hpp" #include "../mwmechanics/npcstats.hpp" namespace MWClass { void Activator::insertObjectRendering (const MWWorld::Ptr& ptr, const std::string& model, MWRender::RenderingInterface& renderingInterface) const { if (!model.empty()) { renderingInterface.getObjects().insertModel(ptr, model, true); ptr.getRefData().getBaseNode()->setNodeMask(MWRender::Mask_Static); } } void Activator::insertObject(const MWWorld::Ptr& ptr, const std::string& model, MWPhysics::PhysicsSystem& physics) const { if(!model.empty()) physics.addObject(ptr, model); } std::string Activator::getModel(const MWWorld::ConstPtr &ptr) const { const MWWorld::LiveCellRef *ref = ptr.get(); const std::string &model = ref->mBase->mModel; if (!model.empty()) { return "meshes\\" + model; } return ""; } bool Activator::isActivator() const { return true; } bool Activator::useAnim() const { return true; } std::string Activator::getName (const MWWorld::ConstPtr& ptr) const { const MWWorld::LiveCellRef *ref = ptr.get(); return ref->mBase->mName; } std::string Activator::getScript (const MWWorld::ConstPtr& ptr) const { const MWWorld::LiveCellRef *ref = ptr.get(); return ref->mBase->mScript; } void Activator::registerSelf() { std::shared_ptr instance (new Activator); registerClass (typeid (ESM::Activator).name(), instance); } bool Activator::hasToolTip (const MWWorld::ConstPtr& ptr) const { return !getName(ptr).empty(); } bool Activator::allowTelekinesis(const MWWorld::ConstPtr &ptr) const { return false; } MWGui::ToolTipInfo Activator::getToolTipInfo (const MWWorld::ConstPtr& ptr, int count) const { const MWWorld::LiveCellRef *ref = ptr.get(); MWGui::ToolTipInfo info; info.caption = MyGUI::TextIterator::toTagsString(getName(ptr)) + MWGui::ToolTips::getCountString(count); std::string text; if (MWBase::Environment::get().getWindowManager()->getFullHelp()) { text += MWGui::ToolTips::getCellRefString(ptr.getCellRef()); text += MWGui::ToolTips::getMiscString(ref->mBase->mScript, "Script"); } info.text = text; return info; } std::shared_ptr Activator::activate(const MWWorld::Ptr &ptr, const MWWorld::Ptr &actor) const { if(actor.getClass().isNpc() && actor.getClass().getNpcStats(actor).isWerewolf()) { const MWWorld::ESMStore &store = MWBase::Environment::get().getWorld()->getStore(); const ESM::Sound *sound = store.get().searchRandom("WolfActivator"); std::shared_ptr action(new MWWorld::FailedAction("#{sWerewolfRefusal}")); if(sound) action->setSound(sound->mId); return action; } return std::shared_ptr(new MWWorld::NullAction); } MWWorld::Ptr Activator::copyToCellImpl(const MWWorld::ConstPtr &ptr, MWWorld::CellStore &cell) const { const MWWorld::LiveCellRef *ref = ptr.get(); return MWWorld::Ptr(cell.insert(ref), &cell); } std::string Activator::getSoundIdFromSndGen(const MWWorld::Ptr &ptr, const std::string &name) const { const std::string model = getModel(ptr); // Assume it's not empty, since we wouldn't have gotten the soundgen otherwise const MWWorld::ESMStore &store = MWBase::Environment::get().getWorld()->getStore(); std::string creatureId; for (const ESM::Creature &iter : store.get()) { if (!iter.mModel.empty() && Misc::StringUtils::ciEqual(model, "meshes\\" + iter.mModel)) { creatureId = !iter.mOriginal.empty() ? iter.mOriginal : iter.mId; break; } } int type = getSndGenTypeFromName(name); std::vector fallbacksounds; if (!creatureId.empty()) { std::vector sounds; for (auto sound = store.get().begin(); sound != store.get().end(); ++sound) { if (type == sound->mType && !sound->mCreature.empty() && (Misc::StringUtils::ciEqual(creatureId, sound->mCreature))) sounds.push_back(&*sound); if (type == sound->mType && sound->mCreature.empty()) fallbacksounds.push_back(&*sound); } if (!sounds.empty()) return sounds[Misc::Rng::rollDice(sounds.size())]->mSound; if (!fallbacksounds.empty()) return fallbacksounds[Misc::Rng::rollDice(fallbacksounds.size())]->mSound; } else { // The activator doesn't have a corresponding creature ID, but we can try to use the defaults for (auto sound = store.get().begin(); sound != store.get().end(); ++sound) if (type == sound->mType && sound->mCreature.empty()) fallbacksounds.push_back(&*sound); if (!fallbacksounds.empty()) return fallbacksounds[Misc::Rng::rollDice(fallbacksounds.size())]->mSound; } return std::string(); } int Activator::getSndGenTypeFromName(const std::string &name) { if (name == "left") return ESM::SoundGenerator::LeftFoot; if (name == "right") return ESM::SoundGenerator::RightFoot; if (name == "swimleft") return ESM::SoundGenerator::SwimLeft; if (name == "swimright") return ESM::SoundGenerator::SwimRight; if (name == "moan") return ESM::SoundGenerator::Moan; if (name == "roar") return ESM::SoundGenerator::Roar; if (name == "scream") return ESM::SoundGenerator::Scream; if (name == "land") return ESM::SoundGenerator::Land; throw std::runtime_error(std::string("Unexpected soundgen type: ")+name); } } ================================================ FILE: apps/openmw/mwclass/activator.hpp ================================================ #ifndef GAME_MWCLASS_ACTIVATOR_H #define GAME_MWCLASS_ACTIVATOR_H #include "../mwworld/class.hpp" namespace MWClass { class Activator : public MWWorld::Class { MWWorld::Ptr copyToCellImpl(const MWWorld::ConstPtr &ptr, MWWorld::CellStore &cell) const override; static int getSndGenTypeFromName(const std::string &name); public: void insertObjectRendering (const MWWorld::Ptr& ptr, const std::string& model, MWRender::RenderingInterface& renderingInterface) const override; ///< Add reference into a cell for rendering void insertObject(const MWWorld::Ptr& ptr, const std::string& model, MWPhysics::PhysicsSystem& physics) const override; std::string getName (const MWWorld::ConstPtr& ptr) const override; ///< \return name or ID; can return an empty string. bool hasToolTip (const MWWorld::ConstPtr& ptr) const override; ///< @return true if this object has a tooltip when focused (default implementation: true) bool allowTelekinesis(const MWWorld::ConstPtr& ptr) const override; ///< Return whether this class of object can be activated with telekinesis MWGui::ToolTipInfo getToolTipInfo (const MWWorld::ConstPtr& ptr, int count) const override; ///< @return the content of the tool tip to be displayed. raises exception if the object has no tooltip. std::string getScript (const MWWorld::ConstPtr& ptr) const override; ///< Return name of the script attached to ptr std::shared_ptr activate (const MWWorld::Ptr& ptr, const MWWorld::Ptr& actor) const override; ///< Generate action for activation static void registerSelf(); std::string getModel(const MWWorld::ConstPtr &ptr) const override; bool useAnim() const override; ///< Whether or not to use animated variant of model (default false) bool isActivator() const override; std::string getSoundIdFromSndGen(const MWWorld::Ptr &ptr, const std::string &name) const override; }; } #endif ================================================ FILE: apps/openmw/mwclass/actor.cpp ================================================ #include "actor.hpp" #include #include "../mwbase/environment.hpp" #include "../mwbase/world.hpp" #include "../mwbase/soundmanager.hpp" #include "../mwmechanics/actorutil.hpp" #include "../mwmechanics/creaturestats.hpp" #include "../mwmechanics/movement.hpp" #include "../mwmechanics/magiceffects.hpp" #include "../mwphysics/physicssystem.hpp" #include "../mwworld/inventorystore.hpp" namespace MWClass { Actor::Actor() {} Actor::~Actor() {} void Actor::adjustPosition(const MWWorld::Ptr& ptr, bool force) const { MWBase::Environment::get().getWorld()->adjustPosition(ptr, force); } void Actor::insertObject(const MWWorld::Ptr& ptr, const std::string& model, MWPhysics::PhysicsSystem& physics) const { if (!model.empty()) { physics.addActor(ptr, model); if (getCreatureStats(ptr).isDead() && getCreatureStats(ptr).isDeathAnimationFinished()) MWBase::Environment::get().getWorld()->enableActorCollision(ptr, false); } } bool Actor::useAnim() const { return true; } void Actor::block(const MWWorld::Ptr &ptr) const { const MWWorld::InventoryStore& inv = getInventoryStore(ptr); MWWorld::ConstContainerStoreIterator shield = inv.getSlot(MWWorld::InventoryStore::Slot_CarriedLeft); if (shield == inv.end()) return; MWBase::SoundManager *sndMgr = MWBase::Environment::get().getSoundManager(); switch (shield->getClass().getEquipmentSkill(*shield)) { case ESM::Skill::LightArmor: sndMgr->playSound3D(ptr, "Light Armor Hit", 1.0f, 1.0f); break; case ESM::Skill::MediumArmor: sndMgr->playSound3D(ptr, "Medium Armor Hit", 1.0f, 1.0f); break; case ESM::Skill::HeavyArmor: sndMgr->playSound3D(ptr, "Heavy Armor Hit", 1.0f, 1.0f); break; default: return; } } osg::Vec3f Actor::getRotationVector(const MWWorld::Ptr& ptr) const { MWMechanics::Movement &movement = getMovementSettings(ptr); osg::Vec3f vec(movement.mRotation[0], movement.mRotation[1], movement.mRotation[2]); movement.mRotation[0] = 0.0f; movement.mRotation[1] = 0.0f; movement.mRotation[2] = 0.0f; return vec; } float Actor::getEncumbrance(const MWWorld::Ptr& ptr) const { float weight = getContainerStore(ptr).getWeight(); const MWMechanics::MagicEffects& effects = getCreatureStats(ptr).getMagicEffects(); weight -= effects.get(MWMechanics::EffectKey(ESM::MagicEffect::Feather)).getMagnitude(); if (ptr != MWMechanics::getPlayer() || !MWBase::Environment::get().getWorld()->getGodModeState()) weight += effects.get(MWMechanics::EffectKey(ESM::MagicEffect::Burden)).getMagnitude(); return (weight < 0) ? 0.0f : weight; } bool Actor::allowTelekinesis(const MWWorld::ConstPtr &ptr) const { return false; } bool Actor::isActor() const { return true; } float Actor::getCurrentSpeed(const MWWorld::Ptr& ptr) const { const MWMechanics::Movement& movementSettings = ptr.getClass().getMovementSettings(ptr); float moveSpeed = this->getMaxSpeed(ptr) * movementSettings.mSpeedFactor; if (movementSettings.mIsStrafing) moveSpeed *= 0.75f; return moveSpeed; } } ================================================ FILE: apps/openmw/mwclass/actor.hpp ================================================ #ifndef GAME_MWCLASS_MOBILE_H #define GAME_MWCLASS_MOBILE_H #include "../mwworld/class.hpp" namespace ESM { struct GameSetting; } namespace MWClass { /// \brief Class holding functionality common to Creature and NPC class Actor : public MWWorld::Class { protected: Actor(); public: virtual ~Actor(); void adjustPosition(const MWWorld::Ptr& ptr, bool force) const override; ///< Adjust position to stand on ground. Must be called post model load /// @param force do this even if the ptr is flying void insertObject(const MWWorld::Ptr& ptr, const std::string& model, MWPhysics::PhysicsSystem& physics) const override; bool useAnim() const override; void block(const MWWorld::Ptr &ptr) const override; osg::Vec3f getRotationVector(const MWWorld::Ptr& ptr) const override; ///< Return desired rotations, as euler angles. Sets getMovementSettings(ptr).mRotation to zero. float getEncumbrance(const MWWorld::Ptr& ptr) const override; ///< Returns total weight of objects inside this object (including modifications from magic /// effects). Throws an exception, if the object can't hold other objects. bool allowTelekinesis(const MWWorld::ConstPtr& ptr) const override; ///< Return whether this class of object can be activated with telekinesis bool isActor() const override; /// Return current movement speed. float getCurrentSpeed(const MWWorld::Ptr& ptr) const override; // not implemented Actor(const Actor&); Actor& operator= (const Actor&); }; } #endif ================================================ FILE: apps/openmw/mwclass/apparatus.cpp ================================================ #include "apparatus.hpp" /* Start of tes3mp addition Include additional headers for multiplayer purposes */ #include #include "../mwmp/Main.hpp" #include "../mwmp/Networking.hpp" /* End of tes3mp addition */ #include #include "../mwbase/environment.hpp" #include "../mwbase/windowmanager.hpp" #include "../mwworld/ptr.hpp" #include "../mwworld/actionalchemy.hpp" #include "../mwworld/cellstore.hpp" #include "../mwphysics/physicssystem.hpp" #include "../mwworld/nullaction.hpp" #include "../mwrender/objects.hpp" #include "../mwrender/renderinginterface.hpp" #include "../mwgui/tooltips.hpp" namespace MWClass { void Apparatus::insertObjectRendering (const MWWorld::Ptr& ptr, const std::string& model, MWRender::RenderingInterface& renderingInterface) const { if (!model.empty()) { renderingInterface.getObjects().insertModel(ptr, model); } } void Apparatus::insertObject(const MWWorld::Ptr& ptr, const std::string& model, MWPhysics::PhysicsSystem& physics) const { // TODO: add option somewhere to enable collision for placeable objects /* Start of tes3mp addition Make it possible to enable collision for this object class from a packet */ if (!model.empty()) { mwmp::BaseWorldstate *worldstate = mwmp::Main::get().getNetworking()->getWorldstate(); if (worldstate->hasPlacedObjectCollision || Utils::vectorContains(worldstate->enforcedCollisionRefIds, ptr.getCellRef().getRefId())) { if (worldstate->useActorCollisionForPlacedObjects) physics.addObject(ptr, model, MWPhysics::CollisionType_Actor); else physics.addObject(ptr, model, MWPhysics::CollisionType_World); } } /* End of tes3mp addition */ } std::string Apparatus::getModel(const MWWorld::ConstPtr &ptr) const { const MWWorld::LiveCellRef *ref = ptr.get(); const std::string &model = ref->mBase->mModel; if (!model.empty()) { return "meshes\\" + model; } return ""; } std::string Apparatus::getName (const MWWorld::ConstPtr& ptr) const { const MWWorld::LiveCellRef *ref = ptr.get(); const std::string& name = ref->mBase->mName; return !name.empty() ? name : ref->mBase->mId; } std::shared_ptr Apparatus::activate (const MWWorld::Ptr& ptr, const MWWorld::Ptr& actor) const { return defaultItemActivate(ptr, actor); } std::string Apparatus::getScript (const MWWorld::ConstPtr& ptr) const { const MWWorld::LiveCellRef *ref = ptr.get(); return ref->mBase->mScript; } int Apparatus::getValue (const MWWorld::ConstPtr& ptr) const { const MWWorld::LiveCellRef *ref = ptr.get(); return ref->mBase->mData.mValue; } void Apparatus::registerSelf() { std::shared_ptr instance (new Apparatus); registerClass (typeid (ESM::Apparatus).name(), instance); } std::string Apparatus::getUpSoundId (const MWWorld::ConstPtr& ptr) const { return std::string("Item Apparatus Up"); } std::string Apparatus::getDownSoundId (const MWWorld::ConstPtr& ptr) const { return std::string("Item Apparatus Down"); } std::string Apparatus::getInventoryIcon (const MWWorld::ConstPtr& ptr) const { const MWWorld::LiveCellRef *ref = ptr.get(); return ref->mBase->mIcon; } MWGui::ToolTipInfo Apparatus::getToolTipInfo (const MWWorld::ConstPtr& ptr, int count) const { const MWWorld::LiveCellRef *ref = ptr.get(); MWGui::ToolTipInfo info; info.caption = MyGUI::TextIterator::toTagsString(getName(ptr)) + MWGui::ToolTips::getCountString(count); info.icon = ref->mBase->mIcon; std::string text; text += "\n#{sQuality}: " + MWGui::ToolTips::toString(ref->mBase->mData.mQuality); text += MWGui::ToolTips::getWeightString(ref->mBase->mData.mWeight, "#{sWeight}"); text += MWGui::ToolTips::getValueString(ref->mBase->mData.mValue, "#{sValue}"); if (MWBase::Environment::get().getWindowManager()->getFullHelp()) { text += MWGui::ToolTips::getCellRefString(ptr.getCellRef()); text += MWGui::ToolTips::getMiscString(ref->mBase->mScript, "Script"); } info.text = text; return info; } std::shared_ptr Apparatus::use (const MWWorld::Ptr& ptr, bool force) const { return std::shared_ptr(new MWWorld::ActionAlchemy(force)); } MWWorld::Ptr Apparatus::copyToCellImpl(const MWWorld::ConstPtr &ptr, MWWorld::CellStore &cell) const { const MWWorld::LiveCellRef *ref = ptr.get(); return MWWorld::Ptr(cell.insert(ref), &cell); } bool Apparatus::canSell (const MWWorld::ConstPtr& item, int npcServices) const { return (npcServices & ESM::NPC::Apparatus) != 0; } float Apparatus::getWeight(const MWWorld::ConstPtr &ptr) const { const MWWorld::LiveCellRef *ref = ptr.get(); return ref->mBase->mData.mWeight; } } ================================================ FILE: apps/openmw/mwclass/apparatus.hpp ================================================ #ifndef GAME_MWCLASS_APPARATUS_H #define GAME_MWCLASS_APPARATUS_H #include "../mwworld/class.hpp" namespace MWClass { class Apparatus : public MWWorld::Class { MWWorld::Ptr copyToCellImpl(const MWWorld::ConstPtr &ptr, MWWorld::CellStore &cell) const override; public: float getWeight (const MWWorld::ConstPtr& ptr) const override; void insertObjectRendering (const MWWorld::Ptr& ptr, const std::string& model, MWRender::RenderingInterface& renderingInterface) const override; ///< Add reference into a cell for rendering void insertObject(const MWWorld::Ptr& ptr, const std::string& model, MWPhysics::PhysicsSystem& physics) const override; std::string getName (const MWWorld::ConstPtr& ptr) const override; ///< \return name or ID; can return an empty string. std::shared_ptr activate (const MWWorld::Ptr& ptr, const MWWorld::Ptr& actor) const override; ///< Generate action for activation std::string getScript (const MWWorld::ConstPtr& ptr) const override; ///< Return name of the script attached to ptr int getValue (const MWWorld::ConstPtr& ptr) const override; ///< Return trade value of the object. Throws an exception, if the object can't be traded. MWGui::ToolTipInfo getToolTipInfo (const MWWorld::ConstPtr& ptr, int count) const override; ///< @return the content of the tool tip to be displayed. raises exception if the object has no tooltip. static void registerSelf(); std::string getUpSoundId (const MWWorld::ConstPtr& ptr) const override; ///< Return the pick up sound Id std::string getDownSoundId (const MWWorld::ConstPtr& ptr) const override; ///< Return the put down sound Id std::string getInventoryIcon (const MWWorld::ConstPtr& ptr) const override; ///< Return name of inventory icon. std::shared_ptr use (const MWWorld::Ptr& ptr, bool force=false) const override; ///< Generate action for using via inventory menu std::string getModel(const MWWorld::ConstPtr &ptr) const override; bool canSell (const MWWorld::ConstPtr& item, int npcServices) const override; }; } #endif ================================================ FILE: apps/openmw/mwclass/armor.cpp ================================================ #include "armor.hpp" /* Start of tes3mp addition Include additional headers for multiplayer purposes */ #include #include "../mwmp/Main.hpp" #include "../mwmp/Networking.hpp" #include "../mwmp/Worldstate.hpp" /* End of tes3mp addition */ #include #include #include #include "../mwbase/environment.hpp" #include "../mwbase/world.hpp" #include "../mwbase/windowmanager.hpp" #include "../mwworld/ptr.hpp" #include "../mwworld/actionequip.hpp" #include "../mwworld/inventorystore.hpp" #include "../mwworld/cellstore.hpp" #include "../mwworld/esmstore.hpp" #include "../mwphysics/physicssystem.hpp" #include "../mwworld/nullaction.hpp" #include "../mwworld/containerstore.hpp" #include "../mwrender/objects.hpp" #include "../mwrender/renderinginterface.hpp" #include "../mwmechanics/actorutil.hpp" #include "../mwmechanics/weapontype.hpp" #include "../mwgui/tooltips.hpp" namespace MWClass { void Armor::insertObjectRendering (const MWWorld::Ptr& ptr, const std::string& model, MWRender::RenderingInterface& renderingInterface) const { if (!model.empty()) { renderingInterface.getObjects().insertModel(ptr, model); } } void Armor::insertObject(const MWWorld::Ptr& ptr, const std::string& model, MWPhysics::PhysicsSystem& physics) const { // TODO: add option somewhere to enable collision for placeable objects /* Start of tes3mp addition Make it possible to enable collision for this object class from a packet */ if (!model.empty()) { mwmp::BaseWorldstate *worldstate = mwmp::Main::get().getNetworking()->getWorldstate(); if (worldstate->hasPlacedObjectCollision || Utils::vectorContains(worldstate->enforcedCollisionRefIds, ptr.getCellRef().getRefId())) { if (worldstate->useActorCollisionForPlacedObjects) physics.addObject(ptr, model, MWPhysics::CollisionType_Actor); else physics.addObject(ptr, model, MWPhysics::CollisionType_World); } } /* End of tes3mp addition */ } std::string Armor::getModel(const MWWorld::ConstPtr &ptr) const { const MWWorld::LiveCellRef *ref = ptr.get(); const std::string &model = ref->mBase->mModel; if (!model.empty()) { return "meshes\\" + model; } return ""; } std::string Armor::getName (const MWWorld::ConstPtr& ptr) const { const MWWorld::LiveCellRef *ref = ptr.get(); const std::string& name = ref->mBase->mName; return !name.empty() ? name : ref->mBase->mId; } std::shared_ptr Armor::activate (const MWWorld::Ptr& ptr, const MWWorld::Ptr& actor) const { return defaultItemActivate(ptr, actor); } bool Armor::hasItemHealth (const MWWorld::ConstPtr& ptr) const { return true; } int Armor::getItemMaxHealth (const MWWorld::ConstPtr& ptr) const { const MWWorld::LiveCellRef *ref = ptr.get(); return ref->mBase->mData.mHealth; } std::string Armor::getScript (const MWWorld::ConstPtr& ptr) const { const MWWorld::LiveCellRef *ref = ptr.get(); return ref->mBase->mScript; } std::pair, bool> Armor::getEquipmentSlots (const MWWorld::ConstPtr& ptr) const { const MWWorld::LiveCellRef *ref = ptr.get(); std::vector slots_; const int size = 11; static const int sMapping[size][2] = { { ESM::Armor::Helmet, MWWorld::InventoryStore::Slot_Helmet }, { ESM::Armor::Cuirass, MWWorld::InventoryStore::Slot_Cuirass }, { ESM::Armor::LPauldron, MWWorld::InventoryStore::Slot_LeftPauldron }, { ESM::Armor::RPauldron, MWWorld::InventoryStore::Slot_RightPauldron }, { ESM::Armor::Greaves, MWWorld::InventoryStore::Slot_Greaves }, { ESM::Armor::Boots, MWWorld::InventoryStore::Slot_Boots }, { ESM::Armor::LGauntlet, MWWorld::InventoryStore::Slot_LeftGauntlet }, { ESM::Armor::RGauntlet, MWWorld::InventoryStore::Slot_RightGauntlet }, { ESM::Armor::Shield, MWWorld::InventoryStore::Slot_CarriedLeft }, { ESM::Armor::LBracer, MWWorld::InventoryStore::Slot_LeftGauntlet }, { ESM::Armor::RBracer, MWWorld::InventoryStore::Slot_RightGauntlet } }; for (int i=0; imBase->mData.mType) { slots_.push_back (int (sMapping[i][1])); break; } return std::make_pair (slots_, false); } int Armor::getEquipmentSkill (const MWWorld::ConstPtr& ptr) const { const MWWorld::LiveCellRef *ref = ptr.get(); std::string typeGmst; switch (ref->mBase->mData.mType) { case ESM::Armor::Helmet: typeGmst = "iHelmWeight"; break; case ESM::Armor::Cuirass: typeGmst = "iCuirassWeight"; break; case ESM::Armor::LPauldron: case ESM::Armor::RPauldron: typeGmst = "iPauldronWeight"; break; case ESM::Armor::Greaves: typeGmst = "iGreavesWeight"; break; case ESM::Armor::Boots: typeGmst = "iBootsWeight"; break; case ESM::Armor::LGauntlet: case ESM::Armor::RGauntlet: typeGmst = "iGauntletWeight"; break; case ESM::Armor::Shield: typeGmst = "iShieldWeight"; break; case ESM::Armor::LBracer: case ESM::Armor::RBracer: typeGmst = "iGauntletWeight"; break; } if (typeGmst.empty()) return -1; const MWWorld::Store &gmst = MWBase::Environment::get().getWorld()->getStore().get(); float iWeight = floor(gmst.find(typeGmst)->mValue.getFloat()); float epsilon = 0.0005f; if (ref->mBase->mData.mWeight <= iWeight * gmst.find ("fLightMaxMod")->mValue.getFloat() + epsilon) return ESM::Skill::LightArmor; if (ref->mBase->mData.mWeight <= iWeight * gmst.find ("fMedMaxMod")->mValue.getFloat() + epsilon) return ESM::Skill::MediumArmor; else return ESM::Skill::HeavyArmor; } int Armor::getValue (const MWWorld::ConstPtr& ptr) const { const MWWorld::LiveCellRef *ref = ptr.get(); return ref->mBase->mData.mValue; } void Armor::registerSelf() { std::shared_ptr instance (new Armor); registerClass (typeid (ESM::Armor).name(), instance); } std::string Armor::getUpSoundId (const MWWorld::ConstPtr& ptr) const { int es = getEquipmentSkill(ptr); if (es == ESM::Skill::LightArmor) return std::string("Item Armor Light Up"); else if (es == ESM::Skill::MediumArmor) return std::string("Item Armor Medium Up"); else return std::string("Item Armor Heavy Up"); } std::string Armor::getDownSoundId (const MWWorld::ConstPtr& ptr) const { int es = getEquipmentSkill(ptr); if (es == ESM::Skill::LightArmor) return std::string("Item Armor Light Down"); else if (es == ESM::Skill::MediumArmor) return std::string("Item Armor Medium Down"); else return std::string("Item Armor Heavy Down"); } std::string Armor::getInventoryIcon (const MWWorld::ConstPtr& ptr) const { const MWWorld::LiveCellRef *ref = ptr.get(); return ref->mBase->mIcon; } MWGui::ToolTipInfo Armor::getToolTipInfo (const MWWorld::ConstPtr& ptr, int count) const { const MWWorld::LiveCellRef *ref = ptr.get(); MWGui::ToolTipInfo info; info.caption = MyGUI::TextIterator::toTagsString(getName(ptr)) + MWGui::ToolTips::getCountString(count); info.icon = ref->mBase->mIcon; std::string text; // get armor type string (light/medium/heavy) std::string typeText; if (ref->mBase->mData.mWeight == 0) typeText = ""; else { int armorType = getEquipmentSkill(ptr); if (armorType == ESM::Skill::LightArmor) typeText = "#{sLight}"; else if (armorType == ESM::Skill::MediumArmor) typeText = "#{sMedium}"; else typeText = "#{sHeavy}"; } text += "\n#{sArmorRating}: " + MWGui::ToolTips::toString(static_cast(getEffectiveArmorRating(ptr, MWMechanics::getPlayer()))); int remainingHealth = getItemHealth(ptr); text += "\n#{sCondition}: " + MWGui::ToolTips::toString(remainingHealth) + "/" + MWGui::ToolTips::toString(ref->mBase->mData.mHealth); if (typeText != "") text += "\n#{sWeight}: " + MWGui::ToolTips::toString(ref->mBase->mData.mWeight) + " (" + typeText + ")"; text += MWGui::ToolTips::getValueString(ref->mBase->mData.mValue, "#{sValue}"); if (MWBase::Environment::get().getWindowManager()->getFullHelp()) { text += MWGui::ToolTips::getCellRefString(ptr.getCellRef()); text += MWGui::ToolTips::getMiscString(ref->mBase->mScript, "Script"); } info.enchant = ref->mBase->mEnchant; if (!info.enchant.empty()) info.remainingEnchantCharge = static_cast(ptr.getCellRef().getEnchantmentCharge()); info.text = text; return info; } std::string Armor::getEnchantment (const MWWorld::ConstPtr& ptr) const { const MWWorld::LiveCellRef *ref = ptr.get(); return ref->mBase->mEnchant; } std::string Armor::applyEnchantment(const MWWorld::ConstPtr &ptr, const std::string& enchId, int enchCharge, const std::string& newName) const { const MWWorld::LiveCellRef *ref = ptr.get(); ESM::Armor newItem = *ref->mBase; newItem.mId=""; newItem.mName=newName; newItem.mData.mEnchant=enchCharge; newItem.mEnchant=enchId; /* Start of tes3mp addition Send the newly created record to the server and expect it to be returned with a server-set id */ mwmp::Main::get().getNetworking()->getWorldstate()->sendArmorRecord(&newItem, ref->mBase->mId); /* End of tes3mp addition */ const ESM::Armor *record = MWBase::Environment::get().getWorld()->createRecord (newItem); return record->mId; } float Armor::getEffectiveArmorRating(const MWWorld::ConstPtr &ptr, const MWWorld::Ptr &actor) const { const MWWorld::LiveCellRef *ref = ptr.get(); int armorSkillType = getEquipmentSkill(ptr); float armorSkill = actor.getClass().getSkill(actor, armorSkillType); const MWBase::World *world = MWBase::Environment::get().getWorld(); int iBaseArmorSkill = world->getStore().get().find("iBaseArmorSkill")->mValue.getInteger(); if(ref->mBase->mData.mWeight == 0) return ref->mBase->mData.mArmor; else return ref->mBase->mData.mArmor * armorSkill / static_cast(iBaseArmorSkill); } std::pair Armor::canBeEquipped(const MWWorld::ConstPtr &ptr, const MWWorld::Ptr &npc) const { const MWWorld::InventoryStore& invStore = npc.getClass().getInventoryStore(npc); if (getItemHealth(ptr) == 0) return std::make_pair(0, "#{sInventoryMessage1}"); // slots that this item can be equipped in std::pair, bool> slots_ = getEquipmentSlots(ptr); if (slots_.first.empty()) return std::make_pair(0, ""); if (npc.getClass().isNpc()) { std::string npcRace = npc.get()->mBase->mRace; // Beast races cannot equip shoes / boots, or full helms (head part vs hair part) const ESM::Race* race = MWBase::Environment::get().getWorld()->getStore().get().find(npcRace); if(race->mData.mFlags & ESM::Race::Beast) { std::vector parts = ptr.get()->mBase->mParts.mParts; for(std::vector::iterator itr = parts.begin(); itr != parts.end(); ++itr) { if((*itr).mPart == ESM::PRT_Head) return std::make_pair(0, "#{sNotifyMessage13}"); if((*itr).mPart == ESM::PRT_LFoot || (*itr).mPart == ESM::PRT_RFoot) return std::make_pair(0, "#{sNotifyMessage14}"); } } } for (std::vector::const_iterator slot=slots_.first.begin(); slot!=slots_.first.end(); ++slot) { // If equipping a shield, check if there's a twohanded weapon conflicting with it if(*slot == MWWorld::InventoryStore::Slot_CarriedLeft) { MWWorld::ConstContainerStoreIterator weapon = invStore.getSlot(MWWorld::InventoryStore::Slot_CarriedRight); if(weapon != invStore.end() && weapon->getTypeName() == typeid(ESM::Weapon).name()) { const MWWorld::LiveCellRef *ref = weapon->get(); if (MWMechanics::getWeaponType(ref->mBase->mData.mType)->mFlags & ESM::WeaponType::TwoHanded) return std::make_pair(3,""); } return std::make_pair(1,""); } } return std::make_pair(1,""); } std::shared_ptr Armor::use (const MWWorld::Ptr& ptr, bool force) const { std::shared_ptr action(new MWWorld::ActionEquip(ptr, force)); action->setSound(getUpSoundId(ptr)); return action; } MWWorld::Ptr Armor::copyToCellImpl(const MWWorld::ConstPtr &ptr, MWWorld::CellStore &cell) const { const MWWorld::LiveCellRef *ref = ptr.get(); return MWWorld::Ptr(cell.insert(ref), &cell); } int Armor::getEnchantmentPoints (const MWWorld::ConstPtr& ptr) const { const MWWorld::LiveCellRef *ref = ptr.get(); return ref->mBase->mData.mEnchant; } bool Armor::canSell (const MWWorld::ConstPtr& item, int npcServices) const { return (npcServices & ESM::NPC::Armor) || ((npcServices & ESM::NPC::MagicItems) && !getEnchantment(item).empty()); } float Armor::getWeight(const MWWorld::ConstPtr &ptr) const { const MWWorld::LiveCellRef *ref = ptr.get(); return ref->mBase->mData.mWeight; } } ================================================ FILE: apps/openmw/mwclass/armor.hpp ================================================ #ifndef GAME_MWCLASS_ARMOR_H #define GAME_MWCLASS_ARMOR_H #include "../mwworld/class.hpp" namespace MWClass { class Armor : public MWWorld::Class { MWWorld::Ptr copyToCellImpl(const MWWorld::ConstPtr &ptr, MWWorld::CellStore &cell) const override; public: float getWeight (const MWWorld::ConstPtr& ptr) const override; void insertObjectRendering (const MWWorld::Ptr& ptr, const std::string& model, MWRender::RenderingInterface& renderingInterface) const override; ///< Add reference into a cell for rendering void insertObject(const MWWorld::Ptr& ptr, const std::string& model, MWPhysics::PhysicsSystem& physics) const override; std::string getName (const MWWorld::ConstPtr& ptr) const override; ///< \return name or ID; can return an empty string. std::shared_ptr activate (const MWWorld::Ptr& ptr, const MWWorld::Ptr& actor) const override; ///< Generate action for activation bool hasItemHealth (const MWWorld::ConstPtr& ptr) const override; ///< \return Item health data available? int getItemMaxHealth (const MWWorld::ConstPtr& ptr) const override; ///< Return item max health or throw an exception, if class does not have item health std::string getScript (const MWWorld::ConstPtr& ptr) const override; ///< Return name of the script attached to ptr std::pair, bool> getEquipmentSlots (const MWWorld::ConstPtr& ptr) const override; ///< \return first: Return IDs of the slot this object can be equipped in; second: can object /// stay stacked when equipped? int getEquipmentSkill (const MWWorld::ConstPtr& ptr) const override; /// Return the index of the skill this item corresponds to when equipped or -1, if there is /// no such skill. MWGui::ToolTipInfo getToolTipInfo (const MWWorld::ConstPtr& ptr, int count) const override; ///< @return the content of the tool tip to be displayed. raises exception if the object has no tooltip. int getValue (const MWWorld::ConstPtr& ptr) const override; ///< Return trade value of the object. Throws an exception, if the object can't be traded. static void registerSelf(); std::string getUpSoundId (const MWWorld::ConstPtr& ptr) const override; ///< Return the pick up sound Id std::string getDownSoundId (const MWWorld::ConstPtr& ptr) const override; ///< Return the put down sound Id std::string getInventoryIcon (const MWWorld::ConstPtr& ptr) const override; ///< Return name of inventory icon. std::string getEnchantment (const MWWorld::ConstPtr& ptr) const override; ///< @return the enchantment ID if the object is enchanted, otherwise an empty string std::string applyEnchantment(const MWWorld::ConstPtr &ptr, const std::string& enchId, int enchCharge, const std::string& newName) const override; ///< Creates a new record using \a ptr as template, with the given name and the given enchantment applied to it. std::pair canBeEquipped(const MWWorld::ConstPtr &ptr, const MWWorld::Ptr &npc) const override; ///< Return 0 if player cannot equip item. 1 if can equip. 2 if it's twohanded weapon. 3 if twohanded weapon conflicts with that. \n /// Second item in the pair specifies the error message std::shared_ptr use (const MWWorld::Ptr& ptr, bool force=false) const override; ///< Generate action for using via inventory menu std::string getModel(const MWWorld::ConstPtr &ptr) const override; int getEnchantmentPoints (const MWWorld::ConstPtr& ptr) const override; bool canSell (const MWWorld::ConstPtr& item, int npcServices) const override; /// Get the effective armor rating, factoring in the actor's skills, for the given armor. float getEffectiveArmorRating(const MWWorld::ConstPtr& armor, const MWWorld::Ptr& actor) const override; }; } #endif ================================================ FILE: apps/openmw/mwclass/bodypart.cpp ================================================ #include "bodypart.hpp" #include "../mwrender/renderinginterface.hpp" #include "../mwrender/objects.hpp" #include "../mwworld/cellstore.hpp" namespace MWClass { MWWorld::Ptr BodyPart::copyToCellImpl(const MWWorld::ConstPtr &ptr, MWWorld::CellStore &cell) const { const MWWorld::LiveCellRef *ref = ptr.get(); return MWWorld::Ptr(cell.insert(ref), &cell); } void BodyPart::insertObjectRendering(const MWWorld::Ptr &ptr, const std::string &model, MWRender::RenderingInterface &renderingInterface) const { if (!model.empty()) { renderingInterface.getObjects().insertModel(ptr, model); } } void BodyPart::insertObject(const MWWorld::Ptr &ptr, const std::string &model, MWPhysics::PhysicsSystem &physics) const { } std::string BodyPart::getName(const MWWorld::ConstPtr &ptr) const { return std::string(); } bool BodyPart::hasToolTip(const MWWorld::ConstPtr& ptr) const { return false; } void BodyPart::registerSelf() { std::shared_ptr instance (new BodyPart); registerClass (typeid (ESM::BodyPart).name(), instance); } std::string BodyPart::getModel(const MWWorld::ConstPtr &ptr) const { const MWWorld::LiveCellRef *ref = ptr.get(); const std::string &model = ref->mBase->mModel; if (!model.empty()) { return "meshes\\" + model; } return ""; } } ================================================ FILE: apps/openmw/mwclass/bodypart.hpp ================================================ #ifndef GAME_MWCLASS_BODYPART_H #define GAME_MWCLASS_BODYPART_H #include "../mwworld/class.hpp" namespace MWClass { class BodyPart : public MWWorld::Class { MWWorld::Ptr copyToCellImpl(const MWWorld::ConstPtr &ptr, MWWorld::CellStore &cell) const override; public: void insertObjectRendering (const MWWorld::Ptr& ptr, const std::string& model, MWRender::RenderingInterface& renderingInterface) const override; ///< Add reference into a cell for rendering void insertObject(const MWWorld::Ptr& ptr, const std::string& model, MWPhysics::PhysicsSystem& physics) const override; std::string getName (const MWWorld::ConstPtr& ptr) const override; ///< \return name or ID; can return an empty string. bool hasToolTip (const MWWorld::ConstPtr& ptr) const override; ///< @return true if this object has a tooltip when focused (default implementation: true) static void registerSelf(); std::string getModel(const MWWorld::ConstPtr &ptr) const override; }; } #endif ================================================ FILE: apps/openmw/mwclass/book.cpp ================================================ #include "book.hpp" /* Start of tes3mp addition Include additional headers for multiplayer purposes */ #include #include "../mwmp/Main.hpp" #include "../mwmp/Networking.hpp" /* End of tes3mp addition */ #include #include "../mwbase/environment.hpp" #include "../mwbase/world.hpp" #include "../mwbase/soundmanager.hpp" #include "../mwbase/windowmanager.hpp" #include "../mwworld/ptr.hpp" #include "../mwworld/actionread.hpp" #include "../mwworld/failedaction.hpp" #include "../mwworld/cellstore.hpp" #include "../mwworld/esmstore.hpp" #include "../mwphysics/physicssystem.hpp" #include "../mwrender/objects.hpp" #include "../mwrender/renderinginterface.hpp" #include "../mwgui/tooltips.hpp" #include "../mwmechanics/npcstats.hpp" namespace MWClass { void Book::insertObjectRendering (const MWWorld::Ptr& ptr, const std::string& model, MWRender::RenderingInterface& renderingInterface) const { if (!model.empty()) { renderingInterface.getObjects().insertModel(ptr, model); } } void Book::insertObject(const MWWorld::Ptr& ptr, const std::string& model, MWPhysics::PhysicsSystem& physics) const { // TODO: add option somewhere to enable collision for placeable objects /* Start of tes3mp addition Make it possible to enable collision for this object class from a packet */ if (!model.empty()) { mwmp::BaseWorldstate *worldstate = mwmp::Main::get().getNetworking()->getWorldstate(); if (worldstate->hasPlacedObjectCollision || Utils::vectorContains(worldstate->enforcedCollisionRefIds, ptr.getCellRef().getRefId())) { if (worldstate->useActorCollisionForPlacedObjects) physics.addObject(ptr, model, MWPhysics::CollisionType_Actor); else physics.addObject(ptr, model, MWPhysics::CollisionType_World); } } /* End of tes3mp addition */ } std::string Book::getModel(const MWWorld::ConstPtr &ptr) const { const MWWorld::LiveCellRef *ref = ptr.get(); const std::string &model = ref->mBase->mModel; if (!model.empty()) { return "meshes\\" + model; } return ""; } std::string Book::getName (const MWWorld::ConstPtr& ptr) const { const MWWorld::LiveCellRef *ref = ptr.get(); const std::string& name = ref->mBase->mName; return !name.empty() ? name : ref->mBase->mId; } std::shared_ptr Book::activate (const MWWorld::Ptr& ptr, const MWWorld::Ptr& actor) const { if(actor.getClass().isNpc() && actor.getClass().getNpcStats(actor).isWerewolf()) { const MWWorld::ESMStore &store = MWBase::Environment::get().getWorld()->getStore(); const ESM::Sound *sound = store.get().searchRandom("WolfItem"); std::shared_ptr action(new MWWorld::FailedAction("#{sWerewolfRefusal}")); if(sound) action->setSound(sound->mId); return action; } return std::shared_ptr(new MWWorld::ActionRead(ptr)); } std::string Book::getScript (const MWWorld::ConstPtr& ptr) const { const MWWorld::LiveCellRef *ref = ptr.get(); return ref->mBase->mScript; } int Book::getValue (const MWWorld::ConstPtr& ptr) const { const MWWorld::LiveCellRef *ref = ptr.get(); return ref->mBase->mData.mValue; } void Book::registerSelf() { std::shared_ptr instance (new Book); registerClass (typeid (ESM::Book).name(), instance); } std::string Book::getUpSoundId (const MWWorld::ConstPtr& ptr) const { return std::string("Item Book Up"); } std::string Book::getDownSoundId (const MWWorld::ConstPtr& ptr) const { return std::string("Item Book Down"); } std::string Book::getInventoryIcon (const MWWorld::ConstPtr& ptr) const { const MWWorld::LiveCellRef *ref = ptr.get(); return ref->mBase->mIcon; } MWGui::ToolTipInfo Book::getToolTipInfo (const MWWorld::ConstPtr& ptr, int count) const { const MWWorld::LiveCellRef *ref = ptr.get(); MWGui::ToolTipInfo info; info.caption = MyGUI::TextIterator::toTagsString(getName(ptr)) + MWGui::ToolTips::getCountString(count); info.icon = ref->mBase->mIcon; std::string text; text += MWGui::ToolTips::getWeightString(ref->mBase->mData.mWeight, "#{sWeight}"); text += MWGui::ToolTips::getValueString(ref->mBase->mData.mValue, "#{sValue}"); if (MWBase::Environment::get().getWindowManager()->getFullHelp()) { text += MWGui::ToolTips::getCellRefString(ptr.getCellRef()); text += MWGui::ToolTips::getMiscString(ref->mBase->mScript, "Script"); } info.enchant = ref->mBase->mEnchant; info.text = text; return info; } std::string Book::getEnchantment (const MWWorld::ConstPtr& ptr) const { const MWWorld::LiveCellRef *ref = ptr.get(); return ref->mBase->mEnchant; } std::string Book::applyEnchantment(const MWWorld::ConstPtr &ptr, const std::string& enchId, int enchCharge, const std::string& newName) const { const MWWorld::LiveCellRef *ref = ptr.get(); ESM::Book newItem = *ref->mBase; newItem.mId=""; newItem.mName=newName; newItem.mData.mIsScroll = 1; newItem.mData.mEnchant=enchCharge; newItem.mEnchant=enchId; /* Start of tes3mp addition Send the newly created record to the server and expect it to be returned with a server-set id */ mwmp::Main::get().getNetworking()->getWorldstate()->sendBookRecord(&newItem, ref->mBase->mId); /* End of tes3mp addition */ const ESM::Book *record = MWBase::Environment::get().getWorld()->createRecord (newItem); return record->mId; } std::shared_ptr Book::use (const MWWorld::Ptr& ptr, bool force) const { return std::shared_ptr(new MWWorld::ActionRead(ptr)); } MWWorld::Ptr Book::copyToCellImpl(const MWWorld::ConstPtr &ptr, MWWorld::CellStore &cell) const { const MWWorld::LiveCellRef *ref = ptr.get(); return MWWorld::Ptr(cell.insert(ref), &cell); } int Book::getEnchantmentPoints (const MWWorld::ConstPtr& ptr) const { const MWWorld::LiveCellRef *ref = ptr.get(); return ref->mBase->mData.mEnchant; } bool Book::canSell (const MWWorld::ConstPtr& item, int npcServices) const { return (npcServices & ESM::NPC::Books) || ((npcServices & ESM::NPC::MagicItems) && !getEnchantment(item).empty()); } float Book::getWeight(const MWWorld::ConstPtr &ptr) const { const MWWorld::LiveCellRef *ref = ptr.get(); return ref->mBase->mData.mWeight; } } ================================================ FILE: apps/openmw/mwclass/book.hpp ================================================ #ifndef GAME_MWCLASS_BOOK_H #define GAME_MWCLASS_BOOK_H #include "../mwworld/class.hpp" namespace MWClass { class Book : public MWWorld::Class { MWWorld::Ptr copyToCellImpl(const MWWorld::ConstPtr &ptr, MWWorld::CellStore &cell) const override; public: void insertObjectRendering (const MWWorld::Ptr& ptr, const std::string& model, MWRender::RenderingInterface& renderingInterface) const override; ///< Add reference into a cell for rendering void insertObject(const MWWorld::Ptr& ptr, const std::string& model, MWPhysics::PhysicsSystem& physics) const override; std::string getName (const MWWorld::ConstPtr& ptr) const override; ///< \return name or ID; can return an empty string. std::shared_ptr activate (const MWWorld::Ptr& ptr, const MWWorld::Ptr& actor) const override; ///< Generate action for activation std::string getScript (const MWWorld::ConstPtr& ptr) const override; ///< Return name of the script attached to ptr MWGui::ToolTipInfo getToolTipInfo (const MWWorld::ConstPtr& ptr, int count) const override; ///< @return the content of the tool tip to be displayed. raises exception if the object has no tooltip. int getValue (const MWWorld::ConstPtr& ptr) const override; ///< Return trade value of the object. Throws an exception, if the object can't be traded. static void registerSelf(); std::string getUpSoundId (const MWWorld::ConstPtr& ptr) const override; ///< Return the pick up sound Id std::string getDownSoundId (const MWWorld::ConstPtr& ptr) const override; ///< Return the put down sound Id std::string getInventoryIcon (const MWWorld::ConstPtr& ptr) const override; ///< Return name of inventory icon. std::string getEnchantment (const MWWorld::ConstPtr& ptr) const override; ///< @return the enchantment ID if the object is enchanted, otherwise an empty string std::string applyEnchantment(const MWWorld::ConstPtr &ptr, const std::string& enchId, int enchCharge, const std::string& newName) const override; ///< Creates a new record using \a ptr as template, with the given name and the given enchantment applied to it. std::shared_ptr use (const MWWorld::Ptr& ptr, bool force=false) const override; ///< Generate action for using via inventory menu std::string getModel(const MWWorld::ConstPtr &ptr) const override; int getEnchantmentPoints (const MWWorld::ConstPtr& ptr) const override; float getWeight (const MWWorld::ConstPtr& ptr) const override; bool canSell (const MWWorld::ConstPtr& item, int npcServices) const override; }; } #endif ================================================ FILE: apps/openmw/mwclass/classes.cpp ================================================ #include "classes.hpp" #include "activator.hpp" #include "creature.hpp" #include "npc.hpp" #include "weapon.hpp" #include "armor.hpp" #include "potion.hpp" #include "apparatus.hpp" #include "book.hpp" #include "clothing.hpp" #include "container.hpp" #include "door.hpp" #include "ingredient.hpp" #include "creaturelevlist.hpp" #include "itemlevlist.hpp" #include "light.hpp" #include "lockpick.hpp" #include "misc.hpp" #include "probe.hpp" #include "repair.hpp" #include "static.hpp" #include "bodypart.hpp" namespace MWClass { void registerClasses() { Activator::registerSelf(); Creature::registerSelf(); Npc::registerSelf(); Weapon::registerSelf(); Armor::registerSelf(); Potion::registerSelf(); Apparatus::registerSelf(); Book::registerSelf(); Clothing::registerSelf(); Container::registerSelf(); Door::registerSelf(); Ingredient::registerSelf(); CreatureLevList::registerSelf(); ItemLevList::registerSelf(); Light::registerSelf(); Lockpick::registerSelf(); Miscellaneous::registerSelf(); Probe::registerSelf(); Repair::registerSelf(); Static::registerSelf(); BodyPart::registerSelf(); } } ================================================ FILE: apps/openmw/mwclass/classes.hpp ================================================ #ifndef GAME_MWCLASS_CLASSES_H #define GAME_MWCLASS_CLASSES_H namespace MWClass { void registerClasses(); ///< register all known classes } #endif ================================================ FILE: apps/openmw/mwclass/clothing.cpp ================================================ #include "clothing.hpp" /* Start of tes3mp addition Include additional headers for multiplayer purposes */ #include #include "../mwmp/Main.hpp" #include "../mwmp/Networking.hpp" /* End of tes3mp addition */ #include #include "../mwbase/environment.hpp" #include "../mwbase/world.hpp" #include "../mwbase/windowmanager.hpp" #include "../mwworld/ptr.hpp" #include "../mwworld/actionequip.hpp" #include "../mwworld/inventorystore.hpp" #include "../mwworld/cellstore.hpp" #include "../mwworld/esmstore.hpp" #include "../mwphysics/physicssystem.hpp" #include "../mwworld/nullaction.hpp" #include "../mwgui/tooltips.hpp" #include "../mwrender/objects.hpp" #include "../mwrender/renderinginterface.hpp" namespace MWClass { void Clothing::insertObjectRendering (const MWWorld::Ptr& ptr, const std::string& model, MWRender::RenderingInterface& renderingInterface) const { if (!model.empty()) { renderingInterface.getObjects().insertModel(ptr, model); } } void Clothing::insertObject(const MWWorld::Ptr& ptr, const std::string& model, MWPhysics::PhysicsSystem& physics) const { // TODO: add option somewhere to enable collision for placeable objects /* Start of tes3mp addition Make it possible to enable collision for this object class from a packet */ if (!model.empty()) { mwmp::BaseWorldstate *worldstate = mwmp::Main::get().getNetworking()->getWorldstate(); if (worldstate->hasPlacedObjectCollision || Utils::vectorContains(worldstate->enforcedCollisionRefIds, ptr.getCellRef().getRefId())) { if (worldstate->useActorCollisionForPlacedObjects) physics.addObject(ptr, model, MWPhysics::CollisionType_Actor); else physics.addObject(ptr, model, MWPhysics::CollisionType_World); } } /* End of tes3mp addition */ } std::string Clothing::getModel(const MWWorld::ConstPtr &ptr) const { const MWWorld::LiveCellRef *ref = ptr.get(); const std::string &model = ref->mBase->mModel; if (!model.empty()) { return "meshes\\" + model; } return ""; } std::string Clothing::getName (const MWWorld::ConstPtr& ptr) const { const MWWorld::LiveCellRef *ref = ptr.get(); const std::string& name = ref->mBase->mName; return !name.empty() ? name : ref->mBase->mId; } std::shared_ptr Clothing::activate (const MWWorld::Ptr& ptr, const MWWorld::Ptr& actor) const { return defaultItemActivate(ptr, actor); } std::string Clothing::getScript (const MWWorld::ConstPtr& ptr) const { const MWWorld::LiveCellRef *ref = ptr.get(); return ref->mBase->mScript; } std::pair, bool> Clothing::getEquipmentSlots (const MWWorld::ConstPtr& ptr) const { const MWWorld::LiveCellRef *ref = ptr.get(); std::vector slots_; if (ref->mBase->mData.mType==ESM::Clothing::Ring) { slots_.push_back (int (MWWorld::InventoryStore::Slot_LeftRing)); slots_.push_back (int (MWWorld::InventoryStore::Slot_RightRing)); } else { const int size = 9; static const int sMapping[size][2] = { { ESM::Clothing::Shirt, MWWorld::InventoryStore::Slot_Shirt }, { ESM::Clothing::Belt, MWWorld::InventoryStore::Slot_Belt }, { ESM::Clothing::Robe, MWWorld::InventoryStore::Slot_Robe }, { ESM::Clothing::Pants, MWWorld::InventoryStore::Slot_Pants }, { ESM::Clothing::Shoes, MWWorld::InventoryStore::Slot_Boots }, { ESM::Clothing::LGlove, MWWorld::InventoryStore::Slot_LeftGauntlet }, { ESM::Clothing::RGlove, MWWorld::InventoryStore::Slot_RightGauntlet }, { ESM::Clothing::Skirt, MWWorld::InventoryStore::Slot_Skirt }, { ESM::Clothing::Amulet, MWWorld::InventoryStore::Slot_Amulet } }; for (int i=0; imBase->mData.mType) { slots_.push_back (int (sMapping[i][1])); break; } } return std::make_pair (slots_, false); } int Clothing::getEquipmentSkill (const MWWorld::ConstPtr& ptr) const { const MWWorld::LiveCellRef *ref = ptr.get(); if (ref->mBase->mData.mType==ESM::Clothing::Shoes) return ESM::Skill::Unarmored; return -1; } int Clothing::getValue (const MWWorld::ConstPtr& ptr) const { const MWWorld::LiveCellRef *ref = ptr.get(); return ref->mBase->mData.mValue; } void Clothing::registerSelf() { std::shared_ptr instance (new Clothing); registerClass (typeid (ESM::Clothing).name(), instance); } std::string Clothing::getUpSoundId (const MWWorld::ConstPtr& ptr) const { const MWWorld::LiveCellRef *ref = ptr.get(); if (ref->mBase->mData.mType == 8) { return std::string("Item Ring Up"); } return std::string("Item Clothes Up"); } std::string Clothing::getDownSoundId (const MWWorld::ConstPtr& ptr) const { const MWWorld::LiveCellRef *ref = ptr.get(); if (ref->mBase->mData.mType == 8) { return std::string("Item Ring Down"); } return std::string("Item Clothes Down"); } std::string Clothing::getInventoryIcon (const MWWorld::ConstPtr& ptr) const { const MWWorld::LiveCellRef *ref = ptr.get(); return ref->mBase->mIcon; } MWGui::ToolTipInfo Clothing::getToolTipInfo (const MWWorld::ConstPtr& ptr, int count) const { const MWWorld::LiveCellRef *ref = ptr.get(); MWGui::ToolTipInfo info; info.caption = MyGUI::TextIterator::toTagsString(getName(ptr)) + MWGui::ToolTips::getCountString(count); info.icon = ref->mBase->mIcon; std::string text; text += MWGui::ToolTips::getWeightString(ref->mBase->mData.mWeight, "#{sWeight}"); text += MWGui::ToolTips::getValueString(ref->mBase->mData.mValue, "#{sValue}"); if (MWBase::Environment::get().getWindowManager()->getFullHelp()) { text += MWGui::ToolTips::getCellRefString(ptr.getCellRef()); text += MWGui::ToolTips::getMiscString(ref->mBase->mScript, "Script"); } info.enchant = ref->mBase->mEnchant; if (!info.enchant.empty()) info.remainingEnchantCharge = static_cast(ptr.getCellRef().getEnchantmentCharge()); info.text = text; return info; } std::string Clothing::getEnchantment (const MWWorld::ConstPtr& ptr) const { const MWWorld::LiveCellRef *ref = ptr.get(); return ref->mBase->mEnchant; } std::string Clothing::applyEnchantment(const MWWorld::ConstPtr &ptr, const std::string& enchId, int enchCharge, const std::string& newName) const { const MWWorld::LiveCellRef *ref = ptr.get(); ESM::Clothing newItem = *ref->mBase; newItem.mId=""; newItem.mName=newName; newItem.mData.mEnchant=enchCharge; newItem.mEnchant=enchId; /* Start of tes3mp addition Send the newly created record to the server and expect it to be returned with a server-set id */ mwmp::Main::get().getNetworking()->getWorldstate()->sendClothingRecord(&newItem, ref->mBase->mId); /* End of tes3mp addition */ const ESM::Clothing *record = MWBase::Environment::get().getWorld()->createRecord (newItem); return record->mId; } std::pair Clothing::canBeEquipped(const MWWorld::ConstPtr &ptr, const MWWorld::Ptr &npc) const { // slots that this item can be equipped in std::pair, bool> slots_ = getEquipmentSlots(ptr); if (slots_.first.empty()) return std::make_pair(0, ""); if (npc.getClass().isNpc()) { std::string npcRace = npc.get()->mBase->mRace; // Beast races cannot equip shoes / boots, or full helms (head part vs hair part) const ESM::Race* race = MWBase::Environment::get().getWorld()->getStore().get().find(npcRace); if(race->mData.mFlags & ESM::Race::Beast) { std::vector parts = ptr.get()->mBase->mParts.mParts; for(std::vector::iterator itr = parts.begin(); itr != parts.end(); ++itr) { if((*itr).mPart == ESM::PRT_Head) return std::make_pair(0, "#{sNotifyMessage13}"); if((*itr).mPart == ESM::PRT_LFoot || (*itr).mPart == ESM::PRT_RFoot) return std::make_pair(0, "#{sNotifyMessage15}"); } } } return std::make_pair (1, ""); } std::shared_ptr Clothing::use (const MWWorld::Ptr& ptr, bool force) const { std::shared_ptr action(new MWWorld::ActionEquip(ptr, force)); action->setSound(getUpSoundId(ptr)); return action; } MWWorld::Ptr Clothing::copyToCellImpl(const MWWorld::ConstPtr &ptr, MWWorld::CellStore &cell) const { const MWWorld::LiveCellRef *ref = ptr.get(); return MWWorld::Ptr(cell.insert(ref), &cell); } int Clothing::getEnchantmentPoints (const MWWorld::ConstPtr& ptr) const { const MWWorld::LiveCellRef *ref = ptr.get(); return ref->mBase->mData.mEnchant; } bool Clothing::canSell (const MWWorld::ConstPtr& item, int npcServices) const { return (npcServices & ESM::NPC::Clothing) || ((npcServices & ESM::NPC::MagicItems) && !getEnchantment(item).empty()); } float Clothing::getWeight(const MWWorld::ConstPtr &ptr) const { const MWWorld::LiveCellRef *ref = ptr.get(); return ref->mBase->mData.mWeight; } } ================================================ FILE: apps/openmw/mwclass/clothing.hpp ================================================ #ifndef GAME_MWCLASS_CLOTHING_H #define GAME_MWCLASS_CLOTHING_H #include "../mwworld/class.hpp" namespace MWClass { class Clothing : public MWWorld::Class { MWWorld::Ptr copyToCellImpl(const MWWorld::ConstPtr &ptr, MWWorld::CellStore &cell) const override; public: void insertObjectRendering (const MWWorld::Ptr& ptr, const std::string& model, MWRender::RenderingInterface& renderingInterface) const override; ///< Add reference into a cell for rendering void insertObject(const MWWorld::Ptr& ptr, const std::string& model, MWPhysics::PhysicsSystem& physics) const override; std::string getName (const MWWorld::ConstPtr& ptr) const override; ///< \return name or ID; can return an empty string. std::shared_ptr activate (const MWWorld::Ptr& ptr, const MWWorld::Ptr& actor) const override; ///< Generate action for activation std::string getScript (const MWWorld::ConstPtr& ptr) const override; ///< Return name of the script attached to ptr std::pair, bool> getEquipmentSlots (const MWWorld::ConstPtr& ptr) const override; ///< \return first: Return IDs of the slot this object can be equipped in; second: can object /// stay stacked when equipped? int getEquipmentSkill (const MWWorld::ConstPtr& ptr) const override; /// Return the index of the skill this item corresponds to when equipped or -1, if there is /// no such skill. MWGui::ToolTipInfo getToolTipInfo (const MWWorld::ConstPtr& ptr, int count) const override; ///< @return the content of the tool tip to be displayed. raises exception if the object has no tooltip. int getValue (const MWWorld::ConstPtr& ptr) const override; ///< Return trade value of the object. Throws an exception, if the object can't be traded. static void registerSelf(); std::string getUpSoundId (const MWWorld::ConstPtr& ptr) const override; ///< Return the pick up sound Id std::string getDownSoundId (const MWWorld::ConstPtr& ptr) const override; ///< Return the put down sound Id std::string getInventoryIcon (const MWWorld::ConstPtr& ptr) const override; ///< Return name of inventory icon. std::string getEnchantment (const MWWorld::ConstPtr& ptr) const override; ///< @return the enchantment ID if the object is enchanted, otherwise an empty string std::string applyEnchantment(const MWWorld::ConstPtr &ptr, const std::string& enchId, int enchCharge, const std::string& newName) const override; ///< Creates a new record using \a ptr as template, with the given name and the given enchantment applied to it. std::pair canBeEquipped(const MWWorld::ConstPtr &ptr, const MWWorld::Ptr &npc) const override; ///< Return 0 if player cannot equip item. 1 if can equip. 2 if it's twohanded weapon. 3 if twohanded weapon conflicts with that. /// Second item in the pair specifies the error message std::shared_ptr use (const MWWorld::Ptr& ptr, bool force=false) const override; ///< Generate action for using via inventory menu std::string getModel(const MWWorld::ConstPtr &ptr) const override; int getEnchantmentPoints (const MWWorld::ConstPtr& ptr) const override; float getWeight (const MWWorld::ConstPtr& ptr) const override; bool canSell (const MWWorld::ConstPtr& item, int npcServices) const override; }; } #endif ================================================ FILE: apps/openmw/mwclass/container.cpp ================================================ #include "container.hpp" /* Start of tes3mp addition Include additional headers for multiplayer purposes */ #include "../mwmp/Main.hpp" #include "../mwmp/Networking.hpp" #include "../mwmp/ObjectList.hpp" /* End of tes3mp addition */ #include #include #include #include "../mwbase/environment.hpp" #include "../mwbase/world.hpp" #include "../mwbase/windowmanager.hpp" #include "../mwbase/soundmanager.hpp" #include "../mwworld/ptr.hpp" #include "../mwworld/failedaction.hpp" #include "../mwworld/nullaction.hpp" #include "../mwworld/containerstore.hpp" #include "../mwworld/customdata.hpp" #include "../mwworld/cellstore.hpp" #include "../mwworld/esmstore.hpp" #include "../mwworld/actionharvest.hpp" #include "../mwworld/actionopen.hpp" #include "../mwworld/actiontrap.hpp" #include "../mwphysics/physicssystem.hpp" #include "../mwworld/inventorystore.hpp" #include "../mwgui/tooltips.hpp" #include "../mwrender/animation.hpp" #include "../mwrender/objects.hpp" #include "../mwrender/renderinginterface.hpp" #include "../mwmechanics/actorutil.hpp" #include "../mwmechanics/npcstats.hpp" namespace MWClass { ContainerCustomData::ContainerCustomData(const ESM::Container& container, MWWorld::CellStore* cell) { unsigned int seed = Misc::Rng::rollDice(std::numeric_limits::max()); // setting ownership not needed, since taking items from a container inherits the // container's owner automatically mStore.fillNonRandom(container.mInventory, "", seed); } ContainerCustomData::ContainerCustomData(const ESM::InventoryState& inventory) { mStore.readState(inventory); } ContainerCustomData& ContainerCustomData::asContainerCustomData() { return *this; } const ContainerCustomData& ContainerCustomData::asContainerCustomData() const { return *this; } Container::Container() { mHarvestEnabled = Settings::Manager::getBool("graphic herbalism", "Game"); } void Container::ensureCustomData (const MWWorld::Ptr& ptr) const { if (!ptr.getRefData().getCustomData()) { MWWorld::LiveCellRef *ref = ptr.get(); // store ptr.getRefData().setCustomData (std::make_unique(*ref->mBase, ptr.getCell())); MWBase::Environment::get().getWorld()->addContainerScripts(ptr, ptr.getCell()); } } bool Container::canBeHarvested(const MWWorld::ConstPtr& ptr) const { if (!mHarvestEnabled) return false; const MWRender::Animation* animation = MWBase::Environment::get().getWorld()->getAnimation(ptr); if (animation == nullptr) return false; return animation->canBeHarvested(); } void Container::respawn(const MWWorld::Ptr &ptr) const { MWWorld::LiveCellRef *ref = ptr.get(); if (ref->mBase->mFlags & ESM::Container::Respawn) { // Container was not touched, there is no need to modify its content. if (ptr.getRefData().getCustomData() == nullptr) return; MWBase::Environment::get().getWorld()->removeContainerScripts(ptr); ptr.getRefData().setCustomData(nullptr); } } void Container::insertObjectRendering (const MWWorld::Ptr& ptr, const std::string& model, MWRender::RenderingInterface& renderingInterface) const { if (!model.empty()) { renderingInterface.getObjects().insertModel(ptr, model, true); } } void Container::insertObject(const MWWorld::Ptr& ptr, const std::string& model, MWPhysics::PhysicsSystem& physics) const { if(!model.empty()) physics.addObject(ptr, model); } std::string Container::getModel(const MWWorld::ConstPtr &ptr) const { const MWWorld::LiveCellRef *ref = ptr.get(); const std::string &model = ref->mBase->mModel; if (!model.empty()) { return "meshes\\" + model; } return ""; } bool Container::useAnim() const { return true; } std::shared_ptr Container::activate (const MWWorld::Ptr& ptr, const MWWorld::Ptr& actor) const { if (!MWBase::Environment::get().getWindowManager()->isAllowed(MWGui::GW_Inventory)) return std::shared_ptr (new MWWorld::NullAction ()); if(actor.getClass().isNpc() && actor.getClass().getNpcStats(actor).isWerewolf()) { const MWWorld::ESMStore &store = MWBase::Environment::get().getWorld()->getStore(); const ESM::Sound *sound = store.get().searchRandom("WolfContainer"); std::shared_ptr action(new MWWorld::FailedAction("#{sWerewolfRefusal}")); if(sound) action->setSound(sound->mId); return action; } const std::string lockedSound = "LockedChest"; const std::string trapActivationSound = "Disarm Trap Fail"; MWWorld::Ptr player = MWBase::Environment::get().getWorld ()->getPlayerPtr(); MWWorld::InventoryStore& invStore = player.getClass().getInventoryStore(player); bool isLocked = ptr.getCellRef().getLockLevel() > 0; bool isTrapped = !ptr.getCellRef().getTrap().empty(); bool hasKey = false; std::string keyName; const std::string keyId = ptr.getCellRef().getKey(); if (!keyId.empty()) { MWWorld::Ptr keyPtr = invStore.search(keyId); if (!keyPtr.isEmpty()) { hasKey = true; keyName = keyPtr.getClass().getName(keyPtr); } } if (isLocked && hasKey) { MWBase::Environment::get().getWindowManager ()->messageBox (keyName + " #{sKeyUsed}"); /* Start of tes3mp change (major) Disable unilateral unlocking on this client and expect the server's reply to our packet to do it instead */ //ptr.getCellRef().unlock(); /* End of tes3mp change (major) */ // using a key disarms the trap if(isTrapped) { /* Start of tes3mp change (major) Disable unilateral trap disarming on this client and expect the server's reply to our packet to do it instead */ //ptr.getCellRef().setTrap(""); //MWBase::Environment::get().getSoundManager()->playSound3D(ptr, "Disarm Trap", 1.0f, 1.0f); /* End of tes3mp change (major) */ isTrapped = false; /* Start of tes3mp addition Send an ID_OBJECT_TRAP packet every time a trap is disarmed */ mwmp::ObjectList *objectList = mwmp::Main::get().getNetworking()->getObjectList(); objectList->reset(); objectList->packetOrigin = mwmp::CLIENT_GAMEPLAY; objectList->addObjectTrap(ptr, ptr.getRefData().getPosition(), true); objectList->sendObjectTrap(); /* End of tes3mp addition */ } /* Start of tes3mp addition Send an ID_OBJECT_LOCK packet every time a container is unlocked here */ if (isLocked) { mwmp::ObjectList *objectList = mwmp::Main::get().getNetworking()->getObjectList(); objectList->reset(); objectList->packetOrigin = mwmp::CLIENT_GAMEPLAY; objectList->addObjectLock(ptr, 0); objectList->sendObjectLock(); } /* End of tes3mp addition */ } if (!isLocked || hasKey) { if(!isTrapped) { if (canBeHarvested(ptr)) { std::shared_ptr action (new MWWorld::ActionHarvest(ptr)); return action; } std::shared_ptr action (new MWWorld::ActionOpen(ptr)); return action; } else { // Activate trap std::shared_ptr action(new MWWorld::ActionTrap(ptr.getCellRef().getTrap(), ptr)); action->setSound(trapActivationSound); return action; } } else { std::shared_ptr action(new MWWorld::FailedAction(std::string(), ptr)); action->setSound(lockedSound); return action; } } std::string Container::getName (const MWWorld::ConstPtr& ptr) const { const MWWorld::LiveCellRef *ref = ptr.get(); const std::string& name = ref->mBase->mName; return !name.empty() ? name : ref->mBase->mId; } MWWorld::ContainerStore& Container::getContainerStore (const MWWorld::Ptr& ptr) const { ensureCustomData (ptr); auto& data = ptr.getRefData().getCustomData()->asContainerCustomData(); data.mStore.mPtr = ptr; return data.mStore; } std::string Container::getScript (const MWWorld::ConstPtr& ptr) const { const MWWorld::LiveCellRef *ref = ptr.get(); return ref->mBase->mScript; } void Container::registerSelf() { std::shared_ptr instance (new Container); registerClass (typeid (ESM::Container).name(), instance); } bool Container::hasToolTip (const MWWorld::ConstPtr& ptr) const { if (const MWWorld::CustomData* data = ptr.getRefData().getCustomData()) return !canBeHarvested(ptr) || data->asContainerCustomData().mStore.hasVisibleItems(); return true; } MWGui::ToolTipInfo Container::getToolTipInfo (const MWWorld::ConstPtr& ptr, int count) const { const MWWorld::LiveCellRef *ref = ptr.get(); MWGui::ToolTipInfo info; info.caption = MyGUI::TextIterator::toTagsString(getName(ptr)); std::string text; int lockLevel = ptr.getCellRef().getLockLevel(); if (lockLevel > 0 && lockLevel != ESM::UnbreakableLock) text += "\n#{sLockLevel}: " + MWGui::ToolTips::toString(lockLevel); else if (lockLevel < 0) text += "\n#{sUnlocked}"; if (ptr.getCellRef().getTrap() != "") text += "\n#{sTrapped}"; if (MWBase::Environment::get().getWindowManager()->getFullHelp()) { text += MWGui::ToolTips::getCellRefString(ptr.getCellRef()); text += MWGui::ToolTips::getMiscString(ref->mBase->mScript, "Script"); if (Misc::StringUtils::ciEqual(ptr.getCellRef().getRefId(), "stolen_goods")) text += "\nYou can not use evidence chests"; } info.text = text; return info; } float Container::getCapacity (const MWWorld::Ptr& ptr) const { MWWorld::LiveCellRef *ref = ptr.get(); return ref->mBase->mWeight; } float Container::getEncumbrance (const MWWorld::Ptr& ptr) const { return getContainerStore (ptr).getWeight(); } bool Container::canLock(const MWWorld::ConstPtr &ptr) const { const MWWorld::LiveCellRef *ref = ptr.get(); return !(ref->mBase->mFlags & ESM::Container::Organic); } void Container::modifyBaseInventory(const std::string& containerId, const std::string& itemId, int amount) const { MWMechanics::modifyBaseInventory(containerId, itemId, amount); } MWWorld::Ptr Container::copyToCellImpl(const MWWorld::ConstPtr &ptr, MWWorld::CellStore &cell) const { const MWWorld::LiveCellRef *ref = ptr.get(); return MWWorld::Ptr(cell.insert(ref), &cell); } void Container::readAdditionalState (const MWWorld::Ptr& ptr, const ESM::ObjectState& state) const { if (!state.mHasCustomState) return; const ESM::ContainerState& containerState = state.asContainerState(); ptr.getRefData().setCustomData(std::make_unique(containerState.mInventory)); } void Container::writeAdditionalState (const MWWorld::ConstPtr& ptr, ESM::ObjectState& state) const { if (!ptr.getRefData().getCustomData()) { state.mHasCustomState = false; return; } const ContainerCustomData& customData = ptr.getRefData().getCustomData()->asContainerCustomData(); if (!customData.mStore.isResolved()) { state.mHasCustomState = false; return; } ESM::ContainerState& containerState = state.asContainerState(); customData.mStore.writeState (containerState.mInventory); } } ================================================ FILE: apps/openmw/mwclass/container.hpp ================================================ #ifndef GAME_MWCLASS_CONTAINER_H #define GAME_MWCLASS_CONTAINER_H #include "../mwworld/class.hpp" #include "../mwworld/containerstore.hpp" #include "../mwworld/customdata.hpp" namespace ESM { struct Container; struct InventoryState; } namespace MWClass { class ContainerCustomData : public MWWorld::TypedCustomData { MWWorld::ContainerStore mStore; public: ContainerCustomData(const ESM::Container& container, MWWorld::CellStore* cell); ContainerCustomData(const ESM::InventoryState& inventory); ContainerCustomData& asContainerCustomData() override; const ContainerCustomData& asContainerCustomData() const override; friend class Container; }; class Container : public MWWorld::Class { bool mHarvestEnabled; void ensureCustomData (const MWWorld::Ptr& ptr) const; MWWorld::Ptr copyToCellImpl(const MWWorld::ConstPtr &ptr, MWWorld::CellStore &cell) const override; bool canBeHarvested(const MWWorld::ConstPtr& ptr) const; public: Container(); void insertObjectRendering (const MWWorld::Ptr& ptr, const std::string& model, MWRender::RenderingInterface& renderingInterface) const override; ///< Add reference into a cell for rendering void insertObject(const MWWorld::Ptr& ptr, const std::string& model, MWPhysics::PhysicsSystem& physics) const override; std::string getName (const MWWorld::ConstPtr& ptr) const override; ///< \return name or ID; can return an empty string. std::shared_ptr activate (const MWWorld::Ptr& ptr, const MWWorld::Ptr& actor) const override; ///< Generate action for activation bool hasToolTip (const MWWorld::ConstPtr& ptr) const override; ///< @return true if this object has a tooltip when focused (default implementation: true) MWGui::ToolTipInfo getToolTipInfo (const MWWorld::ConstPtr& ptr, int count) const override; ///< @return the content of the tool tip to be displayed. raises exception if the object has no tooltip. MWWorld::ContainerStore& getContainerStore (const MWWorld::Ptr& ptr) const override; ///< Return container store /* Start of tes3mp addition Make it possible to check whether a class has a container store */ virtual bool hasContainerStore(const MWWorld::Ptr &ptr) const { return true; } /* End of tes3mp addition */ std::string getScript (const MWWorld::ConstPtr& ptr) const override; ///< Return name of the script attached to ptr float getCapacity (const MWWorld::Ptr& ptr) const override; ///< Return total weight that fits into the object. Throws an exception, if the object can't /// hold other objects. float getEncumbrance (const MWWorld::Ptr& ptr) const override; ///< Returns total weight of objects inside this object (including modifications from magic /// effects). Throws an exception, if the object can't hold other objects. bool canLock(const MWWorld::ConstPtr &ptr) const override; void readAdditionalState (const MWWorld::Ptr& ptr, const ESM::ObjectState& state) const override; ///< Read additional state from \a state into \a ptr. void writeAdditionalState (const MWWorld::ConstPtr& ptr, ESM::ObjectState& state) const override; ///< Write additional state from \a ptr into \a state. static void registerSelf(); void respawn (const MWWorld::Ptr& ptr) const override; std::string getModel(const MWWorld::ConstPtr &ptr) const override; bool useAnim() const override; void modifyBaseInventory(const std::string& containerId, const std::string& itemId, int amount) const override; }; } #endif ================================================ FILE: apps/openmw/mwclass/creature.cpp ================================================ #include "creature.hpp" #include #include #include #include #include /* Start of tes3mp addition Include additional headers for multiplayer purposes */ #include #include "../mwmp/Main.hpp" #include "../mwmp/Networking.hpp" #include "../mwmp/LocalPlayer.hpp" #include "../mwmp/PlayerList.hpp" #include "../mwmp/ObjectList.hpp" #include "../mwmp/CellController.hpp" #include "../mwmp/MechanicsHelper.hpp" /* End of tes3mp addition */ #include "../mwmechanics/creaturestats.hpp" #include "../mwmechanics/magiceffects.hpp" #include "../mwmechanics/movement.hpp" #include "../mwmechanics/disease.hpp" #include "../mwmechanics/spellcasting.hpp" #include "../mwmechanics/difficultyscaling.hpp" #include "../mwbase/environment.hpp" #include "../mwbase/mechanicsmanager.hpp" #include "../mwbase/windowmanager.hpp" #include "../mwbase/world.hpp" #include "../mwbase/soundmanager.hpp" #include "../mwworld/ptr.hpp" #include "../mwworld/actiontalk.hpp" #include "../mwworld/actionopen.hpp" #include "../mwworld/failedaction.hpp" #include "../mwworld/customdata.hpp" #include "../mwworld/containerstore.hpp" #include "../mwworld/cellstore.hpp" #include "../mwworld/localscripts.hpp" #include "../mwrender/renderinginterface.hpp" #include "../mwrender/objects.hpp" #include "../mwgui/tooltips.hpp" #include "../mwworld/inventorystore.hpp" #include "../mwmechanics/npcstats.hpp" #include "../mwmechanics/combat.hpp" #include "../mwmechanics/actorutil.hpp" namespace { bool isFlagBitSet(const MWWorld::ConstPtr &ptr, ESM::Creature::Flags bitMask) { return (ptr.get()->mBase->mFlags & bitMask) != 0; } } namespace MWClass { class CreatureCustomData : public MWWorld::TypedCustomData { public: MWMechanics::CreatureStats mCreatureStats; std::unique_ptr mContainerStore; // may be InventoryStore for some creatures MWMechanics::Movement mMovement; CreatureCustomData() = default; CreatureCustomData(const CreatureCustomData& other); CreatureCustomData(CreatureCustomData&& other) = default; CreatureCustomData& asCreatureCustomData() override { return *this; } const CreatureCustomData& asCreatureCustomData() const override { return *this; } }; CreatureCustomData::CreatureCustomData(const CreatureCustomData& other) : mCreatureStats(other.mCreatureStats), mContainerStore(other.mContainerStore->clone()), mMovement(other.mMovement) { } const Creature::GMST& Creature::getGmst() { static GMST gmst; static bool inited = false; if (!inited) { const MWBase::World *world = MWBase::Environment::get().getWorld(); const MWWorld::Store &store = world->getStore().get(); gmst.fMinWalkSpeedCreature = store.find("fMinWalkSpeedCreature"); gmst.fMaxWalkSpeedCreature = store.find("fMaxWalkSpeedCreature"); gmst.fEncumberedMoveEffect = store.find("fEncumberedMoveEffect"); gmst.fSneakSpeedMultiplier = store.find("fSneakSpeedMultiplier"); gmst.fAthleticsRunBonus = store.find("fAthleticsRunBonus"); gmst.fBaseRunMultiplier = store.find("fBaseRunMultiplier"); gmst.fMinFlySpeed = store.find("fMinFlySpeed"); gmst.fMaxFlySpeed = store.find("fMaxFlySpeed"); gmst.fSwimRunBase = store.find("fSwimRunBase"); gmst.fSwimRunAthleticsMult = store.find("fSwimRunAthleticsMult"); gmst.fKnockDownMult = store.find("fKnockDownMult"); gmst.iKnockDownOddsMult = store.find("iKnockDownOddsMult"); gmst.iKnockDownOddsBase = store.find("iKnockDownOddsBase"); inited = true; } return gmst; } void Creature::ensureCustomData (const MWWorld::Ptr& ptr) const { if (!ptr.getRefData().getCustomData()) { std::unique_ptr data (new CreatureCustomData); MWWorld::LiveCellRef *ref = ptr.get(); // creature stats data->mCreatureStats.setAttribute(ESM::Attribute::Strength, ref->mBase->mData.mStrength); data->mCreatureStats.setAttribute(ESM::Attribute::Intelligence, ref->mBase->mData.mIntelligence); data->mCreatureStats.setAttribute(ESM::Attribute::Willpower, ref->mBase->mData.mWillpower); data->mCreatureStats.setAttribute(ESM::Attribute::Agility, ref->mBase->mData.mAgility); data->mCreatureStats.setAttribute(ESM::Attribute::Speed, ref->mBase->mData.mSpeed); data->mCreatureStats.setAttribute(ESM::Attribute::Endurance, ref->mBase->mData.mEndurance); data->mCreatureStats.setAttribute(ESM::Attribute::Personality, ref->mBase->mData.mPersonality); data->mCreatureStats.setAttribute(ESM::Attribute::Luck, ref->mBase->mData.mLuck); data->mCreatureStats.setHealth(static_cast(ref->mBase->mData.mHealth)); data->mCreatureStats.setMagicka(static_cast(ref->mBase->mData.mMana)); data->mCreatureStats.setFatigue(static_cast(ref->mBase->mData.mFatigue)); data->mCreatureStats.setLevel(ref->mBase->mData.mLevel); data->mCreatureStats.getAiSequence().fill(ref->mBase->mAiPackage); data->mCreatureStats.setAiSetting (MWMechanics::CreatureStats::AI_Hello, ref->mBase->mAiData.mHello); data->mCreatureStats.setAiSetting (MWMechanics::CreatureStats::AI_Fight, ref->mBase->mAiData.mFight); data->mCreatureStats.setAiSetting (MWMechanics::CreatureStats::AI_Flee, ref->mBase->mAiData.mFlee); data->mCreatureStats.setAiSetting (MWMechanics::CreatureStats::AI_Alarm, ref->mBase->mAiData.mAlarm); // Persistent actors with 0 health do not play death animation if (data->mCreatureStats.isDead()) data->mCreatureStats.setDeathAnimationFinished(isPersistent(ptr)); // spells bool spellsInitialised = data->mCreatureStats.getSpells().setSpells(ref->mBase->mId); if (!spellsInitialised) data->mCreatureStats.getSpells().addAllToInstance(ref->mBase->mSpells.mList); // inventory bool hasInventory = hasInventoryStore(ptr); if (hasInventory) data->mContainerStore = std::make_unique(); else data->mContainerStore = std::make_unique(); data->mCreatureStats.setGoldPool(ref->mBase->mData.mGold); data->mCreatureStats.setNeedRecalcDynamicStats(false); // store ptr.getRefData().setCustomData(std::move(data)); getContainerStore(ptr).fill(ref->mBase->mInventory, ptr.getCellRef().getRefId()); if (hasInventory) getInventoryStore(ptr).autoEquip(ptr); } } void Creature::insertObjectRendering (const MWWorld::Ptr& ptr, const std::string& model, MWRender::RenderingInterface& renderingInterface) const { MWRender::Objects& objects = renderingInterface.getObjects(); objects.insertCreature(ptr, model, hasInventoryStore(ptr)); } std::string Creature::getModel(const MWWorld::ConstPtr &ptr) const { const MWWorld::LiveCellRef *ref = ptr.get(); const std::string &model = ref->mBase->mModel; if (!model.empty()) { return "meshes\\" + model; } return ""; } void Creature::getModelsToPreload(const MWWorld::Ptr &ptr, std::vector &models) const { std::string model = getModel(ptr); if (!model.empty()) models.push_back(model); // FIXME: use const version of InventoryStore functions once they are available if (hasInventoryStore(ptr)) { const MWWorld::InventoryStore& invStore = getInventoryStore(ptr); for (int slot = 0; slot < MWWorld::InventoryStore::Slots; ++slot) { MWWorld::ConstContainerStoreIterator equipped = invStore.getSlot(slot); if (equipped != invStore.end()) { model = equipped->getClass().getModel(*equipped); if (!model.empty()) models.push_back(model); } } } } std::string Creature::getName (const MWWorld::ConstPtr& ptr) const { const MWWorld::LiveCellRef *ref = ptr.get(); const std::string& name = ref->mBase->mName; return !name.empty() ? name : ref->mBase->mId; } MWMechanics::CreatureStats& Creature::getCreatureStats (const MWWorld::Ptr& ptr) const { ensureCustomData (ptr); return ptr.getRefData().getCustomData()->asCreatureCustomData().mCreatureStats; } void Creature::hit(const MWWorld::Ptr& ptr, float attackStrength, int type) const { /* Start of tes3mp addition Ignore hit calculations on this client from DedicatedPlayers and DedicatedActors */ if (mwmp::PlayerList::isDedicatedPlayer(ptr) || mwmp::Main::get().getCellController()->isDedicatedActor(ptr)) { return; } /* End of tes3mp addition */ MWWorld::LiveCellRef *ref = ptr.get(); const MWWorld::Store &gmst = MWBase::Environment::get().getWorld()->getStore().get(); MWMechanics::CreatureStats &stats = getCreatureStats(ptr); if (stats.getDrawState() != MWMechanics::DrawState_Weapon) return; // Get the weapon used (if hand-to-hand, weapon = inv.end()) MWWorld::Ptr weapon; if (hasInventoryStore(ptr)) { MWWorld::InventoryStore &inv = getInventoryStore(ptr); MWWorld::ContainerStoreIterator weaponslot = inv.getSlot(MWWorld::InventoryStore::Slot_CarriedRight); if (weaponslot != inv.end() && weaponslot->getTypeName() == typeid(ESM::Weapon).name()) weapon = *weaponslot; } MWMechanics::applyFatigueLoss(ptr, weapon, attackStrength); float dist = gmst.find("fCombatDistance")->mValue.getFloat(); if (!weapon.isEmpty()) dist *= weapon.get()->mBase->mData.mReach; // For AI actors, get combat targets to use in the ray cast. Only those targets will return a positive hit result. std::vector targetActors; stats.getAiSequence().getCombatTargets(targetActors); std::pair result = MWBase::Environment::get().getWorld()->getHitContact(ptr, dist, targetActors); if (result.first.isEmpty()) return; // Didn't hit anything MWWorld::Ptr victim = result.first; /* Start of tes3mp change (major) Send an ID_OBJECT_HIT packet when hitting non-actors instead of just returning */ if (!victim.getClass().isActor()) { mwmp::ObjectList *objectList = mwmp::Main::get().getNetworking()->getObjectList(); objectList->reset(); objectList->packetOrigin = mwmp::CLIENT_GAMEPLAY; objectList->addObjectHit(victim, ptr); objectList->sendObjectHit(); return; } /* End of tes3mp change (major) */ osg::Vec3f hitPosition (result.second); float hitchance = MWMechanics::getHitChance(ptr, victim, ref->mBase->mData.mCombat); /* Start of tes3mp addition If the attacker is a LocalPlayer or LocalActor, get their Attack to assign its hit position and target */ mwmp::Attack *localAttack = MechanicsHelper::getLocalAttack(ptr); if (localAttack) { localAttack->isHit = true; localAttack->success = true; localAttack->hitPosition = MechanicsHelper::getPositionFromVector(hitPosition); MechanicsHelper::assignAttackTarget(localAttack, victim); } /* End of tes3mp addition */ if(Misc::Rng::roll0to99() >= hitchance) { /* Start of tes3mp addition If this was a failed attack by the LocalPlayer or LocalActor, send a packet about it Send an ID_OBJECT_HIT about it as well */ if (localAttack) { localAttack->pressed = false; localAttack->success = false; localAttack->shouldSend = true; mwmp::ObjectList *objectList = mwmp::Main::get().getNetworking()->getObjectList(); objectList->reset(); objectList->packetOrigin = mwmp::CLIENT_GAMEPLAY; objectList->addObjectHit(victim, ptr, *localAttack); objectList->sendObjectHit(); } /* End of tes3mp addition */ victim.getClass().onHit(victim, 0.0f, false, MWWorld::Ptr(), ptr, osg::Vec3f(), false); MWMechanics::reduceWeaponCondition(0.f, false, weapon, ptr); return; } int min,max; switch (type) { case 0: min = ref->mBase->mData.mAttack[0]; max = ref->mBase->mData.mAttack[1]; break; case 1: min = ref->mBase->mData.mAttack[2]; max = ref->mBase->mData.mAttack[3]; break; case 2: default: min = ref->mBase->mData.mAttack[4]; max = ref->mBase->mData.mAttack[5]; break; } float damage = min + (max - min) * attackStrength; bool healthdmg = true; if (!weapon.isEmpty()) { const unsigned char *attack = nullptr; if(type == ESM::Weapon::AT_Chop) attack = weapon.get()->mBase->mData.mChop; else if(type == ESM::Weapon::AT_Slash) attack = weapon.get()->mBase->mData.mSlash; else if(type == ESM::Weapon::AT_Thrust) attack = weapon.get()->mBase->mData.mThrust; if(attack) { damage = attack[0] + ((attack[1]-attack[0])*attackStrength); MWMechanics::adjustWeaponDamage(damage, weapon, ptr); MWMechanics::resistNormalWeapon(victim, ptr, weapon, damage); MWMechanics::reduceWeaponCondition(damage, true, weapon, ptr); } // Apply "On hit" enchanted weapons /* Start of tes3mp change (minor) Track whether the strike enchantment is successful for attacks by the LocalPlayer or LocalActors */ bool appliedEnchantment = MWMechanics::applyOnStrikeEnchantment(ptr, victim, weapon, hitPosition); if (localAttack) localAttack->applyWeaponEnchantment = appliedEnchantment; /* End of tes3mp change (minor) */ } else if (isBipedal(ptr)) { MWMechanics::getHandToHandDamage(ptr, victim, damage, healthdmg, attackStrength); } MWMechanics::applyElementalShields(ptr, victim); if (MWMechanics::blockMeleeAttack(ptr, victim, weapon, damage, attackStrength)) damage = 0; MWMechanics::diseaseContact(victim, ptr); victim.getClass().onHit(victim, damage, healthdmg, weapon, ptr, hitPosition, true); } void Creature::onHit(const MWWorld::Ptr &ptr, float damage, bool ishealth, const MWWorld::Ptr &object, const MWWorld::Ptr &attacker, const osg::Vec3f &hitPosition, bool successful) const { MWMechanics::CreatureStats& stats = getCreatureStats(ptr); // NOTE: 'object' and/or 'attacker' may be empty. if (!attacker.isEmpty() && attacker.getClass().isActor() && !stats.getAiSequence().isInCombat(attacker)) stats.setAttacked(true); // Self defense bool setOnPcHitMe = true; // Note OnPcHitMe is not set for friendly hits. // No retaliation for totally static creatures (they have no movement or attacks anyway) if (isMobile(ptr) && !attacker.isEmpty()) setOnPcHitMe = MWBase::Environment::get().getMechanicsManager()->actorAttacked(ptr, attacker); // Attacker and target store each other as hitattemptactor if they have no one stored yet if (!attacker.isEmpty() && attacker.getClass().isActor()) { MWMechanics::CreatureStats& statsAttacker = attacker.getClass().getCreatureStats(attacker); /* Start of tes3mp change (minor) Instead of only checking whether an attacker is the LocalPlayer, also check if they are a DedicatedPlayer Additionally, if the two players are on each other's team, don't track their hits */ // First handle the attacked actor if ((stats.getHitAttemptActorId() == -1) && (statsAttacker.getAiSequence().isInCombat(ptr) || attacker == MWMechanics::getPlayer() || mwmp::PlayerList::isDedicatedPlayer(attacker)) && !MechanicsHelper::isTeamMember(attacker, ptr)) stats.setHitAttemptActorId(statsAttacker.getActorId()); // Next handle the attacking actor if ((statsAttacker.getHitAttemptActorId() == -1) && (statsAttacker.getAiSequence().isInCombat(ptr) || attacker == MWMechanics::getPlayer() || mwmp::PlayerList::isDedicatedPlayer(attacker)) && !MechanicsHelper::isTeamMember(ptr, attacker)) statsAttacker.setHitAttemptActorId(stats.getActorId()); /* End of tes3mp change (minor) */ } if (!object.isEmpty()) stats.setLastHitAttemptObject(object.getCellRef().getRefId()); if (setOnPcHitMe && !attacker.isEmpty() && attacker == MWMechanics::getPlayer()) { const std::string &script = ptr.get()->mBase->mScript; /* Set the OnPCHitMe script variable. The script is responsible for clearing it. */ if(!script.empty()) ptr.getRefData().getLocals().setVarByInt(script, "onpchitme", 1); } if (!successful) { // Missed if (!attacker.isEmpty() && attacker == MWMechanics::getPlayer()) MWBase::Environment::get().getSoundManager()->playSound3D(ptr, "miss", 1.0f, 1.0f); return; } if (!object.isEmpty()) stats.setLastHitObject(object.getCellRef().getRefId()); if (damage < 0.001f) damage = 0; if (damage > 0.f) { if (!attacker.isEmpty()) { // Check for knockdown float agilityTerm = stats.getAttribute(ESM::Attribute::Agility).getModified() * getGmst().fKnockDownMult->mValue.getFloat(); float knockdownTerm = stats.getAttribute(ESM::Attribute::Agility).getModified() * getGmst().iKnockDownOddsMult->mValue.getInteger() * 0.01f + getGmst().iKnockDownOddsBase->mValue.getInteger(); /* Start of tes3mp change (major) If the attacker is a DedicatedPlayer or DedicatedActor with a successful knockdown, apply the knockdown If the attacker is neither of those, then it must be a LocalPlayer or a LocalActor, so calculate the knockdown probability on our client Default to hit recovery if no knockdown has taken place, like in regular OpenMW */ mwmp::Attack *dedicatedAttack = MechanicsHelper::getDedicatedAttack(attacker); if (dedicatedAttack) { if (dedicatedAttack->knockdown) stats.setKnockedDown(true); } if (ishealth && agilityTerm <= damage && knockdownTerm <= Misc::Rng::roll0to99()) stats.setKnockedDown(true); else { if (ishealth && agilityTerm <= damage && knockdownTerm <= Misc::Rng::roll0to99()) stats.setKnockedDown(true); } if (!stats.getKnockedDown()) stats.setHitRecovery(true); // Is this supposed to always occur? /* End of tes3mp change (major) */ } if(ishealth) { damage *= damage / (damage + getArmorRating(ptr)); damage = std::max(1.f, damage); if (!attacker.isEmpty()) { damage = scaleDamage(damage, attacker, ptr); MWBase::Environment::get().getWorld()->spawnBloodEffect(ptr, hitPosition); } MWBase::Environment::get().getSoundManager()->playSound3D(ptr, "Health Damage", 1.0f, 1.0f); MWMechanics::DynamicStat health(stats.getHealth()); health.setCurrent(health.getCurrent() - damage); stats.setHealth(health); } else { MWMechanics::DynamicStat fatigue(stats.getFatigue()); fatigue.setCurrent(fatigue.getCurrent() - damage, true); stats.setFatigue(fatigue); } } /* Start of tes3mp addition If the attacker was the LocalPlayer or LocalActor, record their target and send an attack packet about it Send an ID_OBJECT_HIT about it as well If the victim was a LocalActor who died, record their attacker as the killer */ mwmp::Attack *localAttack = MechanicsHelper::getLocalAttack(attacker); if (localAttack) { localAttack->pressed = false; localAttack->damage = damage; localAttack->knockdown = getCreatureStats(ptr).getKnockedDown(); MechanicsHelper::assignAttackTarget(localAttack, ptr); localAttack->shouldSend = true; mwmp::ObjectList *objectList = mwmp::Main::get().getNetworking()->getObjectList(); objectList->reset(); objectList->packetOrigin = mwmp::CLIENT_GAMEPLAY; objectList->addObjectHit(ptr, attacker, *localAttack); objectList->sendObjectHit(); } if (mwmp::Main::get().getCellController()->isLocalActor(ptr)) { if (getCreatureStats(ptr).isDead()) { mwmp::Main::get().getCellController()->getLocalActor(ptr)->killer = MechanicsHelper::getTarget(attacker); } } /* End of tes3mp addition */ } std::shared_ptr Creature::activate (const MWWorld::Ptr& ptr, const MWWorld::Ptr& actor) const { if(actor.getClass().isNpc() && actor.getClass().getNpcStats(actor).isWerewolf()) { const MWWorld::ESMStore &store = MWBase::Environment::get().getWorld()->getStore(); const ESM::Sound *sound = store.get().searchRandom("WolfCreature"); std::shared_ptr action(new MWWorld::FailedAction("#{sWerewolfRefusal}")); if(sound) action->setSound(sound->mId); return action; } const MWMechanics::CreatureStats& stats = getCreatureStats(ptr); if(stats.isDead()) { bool canLoot = Settings::Manager::getBool ("can loot during death animation", "Game"); // by default user can loot friendly actors during death animation if (canLoot && !stats.getAiSequence().isInCombat()) return std::shared_ptr(new MWWorld::ActionOpen(ptr)); // otherwise wait until death animation if(stats.isDeathAnimationFinished()) return std::shared_ptr(new MWWorld::ActionOpen(ptr)); } else if (!stats.getAiSequence().isInCombat() && !stats.getKnockedDown()) return std::shared_ptr(new MWWorld::ActionTalk(ptr)); // Tribunal and some mod companions oddly enough must use open action as fallback if (!getScript(ptr).empty() && ptr.getRefData().getLocals().getIntVar(getScript(ptr), "companion")) return std::shared_ptr(new MWWorld::ActionOpen(ptr)); return std::shared_ptr(new MWWorld::FailedAction("")); } MWWorld::ContainerStore& Creature::getContainerStore (const MWWorld::Ptr& ptr) const { ensureCustomData (ptr); return *ptr.getRefData().getCustomData()->asCreatureCustomData().mContainerStore; } MWWorld::InventoryStore& Creature::getInventoryStore(const MWWorld::Ptr &ptr) const { if (hasInventoryStore(ptr)) return dynamic_cast(getContainerStore(ptr)); else throw std::runtime_error("this creature has no inventory store"); } bool Creature::hasInventoryStore(const MWWorld::Ptr &ptr) const { return isFlagBitSet(ptr, ESM::Creature::Weapon); } std::string Creature::getScript (const MWWorld::ConstPtr& ptr) const { const MWWorld::LiveCellRef *ref = ptr.get(); return ref->mBase->mScript; } bool Creature::isEssential (const MWWorld::ConstPtr& ptr) const { return isFlagBitSet(ptr, ESM::Creature::Essential); } void Creature::registerSelf() { std::shared_ptr instance (new Creature); registerClass (typeid (ESM::Creature).name(), instance); } float Creature::getMaxSpeed(const MWWorld::Ptr &ptr) const { const MWMechanics::CreatureStats& stats = getCreatureStats(ptr); if (stats.isParalyzed() || stats.getKnockedDown() || stats.isDead()) return 0.f; const GMST& gmst = getGmst(); const MWBase::World *world = MWBase::Environment::get().getWorld(); const MWMechanics::MagicEffects &mageffects = stats.getMagicEffects(); float moveSpeed; if(getEncumbrance(ptr) > getCapacity(ptr)) moveSpeed = 0.0f; else if(canFly(ptr) || (mageffects.get(ESM::MagicEffect::Levitate).getMagnitude() > 0 && world->isLevitationEnabled())) { float flySpeed = 0.01f*(stats.getAttribute(ESM::Attribute::Speed).getModified() + mageffects.get(ESM::MagicEffect::Levitate).getMagnitude()); flySpeed = gmst.fMinFlySpeed->mValue.getFloat() + flySpeed*(gmst.fMaxFlySpeed->mValue.getFloat() - gmst.fMinFlySpeed->mValue.getFloat()); const float normalizedEncumbrance = getNormalizedEncumbrance(ptr); flySpeed *= 1.0f - gmst.fEncumberedMoveEffect->mValue.getFloat() * normalizedEncumbrance; flySpeed = std::max(0.0f, flySpeed); moveSpeed = flySpeed; } else if(world->isSwimming(ptr)) moveSpeed = getSwimSpeed(ptr); else moveSpeed = getWalkSpeed(ptr); return moveSpeed; } MWMechanics::Movement& Creature::getMovementSettings (const MWWorld::Ptr& ptr) const { ensureCustomData (ptr); return ptr.getRefData().getCustomData()->asCreatureCustomData().mMovement; } bool Creature::hasToolTip(const MWWorld::ConstPtr& ptr) const { if (!ptr.getRefData().getCustomData() || MWBase::Environment::get().getWindowManager()->isGuiMode()) return true; const CreatureCustomData& customData = ptr.getRefData().getCustomData()->asCreatureCustomData(); if (customData.mCreatureStats.isDead() && customData.mCreatureStats.isDeathAnimationFinished()) return true; return !customData.mCreatureStats.getAiSequence().isInCombat(); } MWGui::ToolTipInfo Creature::getToolTipInfo (const MWWorld::ConstPtr& ptr, int count) const { const MWWorld::LiveCellRef *ref = ptr.get(); MWGui::ToolTipInfo info; info.caption = MyGUI::TextIterator::toTagsString(getName(ptr)); std::string text; if (MWBase::Environment::get().getWindowManager()->getFullHelp()) text += MWGui::ToolTips::getMiscString(ref->mBase->mScript, "Script"); info.text = text; return info; } float Creature::getArmorRating (const MWWorld::Ptr& ptr) const { // Equipment armor rating is deliberately ignored. return getCreatureStats(ptr).getMagicEffects().get(ESM::MagicEffect::Shield).getMagnitude(); } float Creature::getCapacity (const MWWorld::Ptr& ptr) const { const MWMechanics::CreatureStats& stats = getCreatureStats (ptr); return stats.getAttribute(ESM::Attribute::Strength).getModified() * 5; } int Creature::getServices(const MWWorld::ConstPtr &actor) const { return actor.get()->mBase->mAiData.mServices; } bool Creature::isPersistent(const MWWorld::ConstPtr &actor) const { const MWWorld::LiveCellRef* ref = actor.get(); return ref->mBase->mPersistent; } std::string Creature::getSoundIdFromSndGen(const MWWorld::Ptr &ptr, const std::string &name) const { int type = getSndGenTypeFromName(ptr, name); if (type < 0) return std::string(); std::vector sounds; std::vector fallbacksounds; MWWorld::LiveCellRef* ref = ptr.get(); const std::string& ourId = (ref->mBase->mOriginal.empty()) ? ptr.getCellRef().getRefId() : ref->mBase->mOriginal; const MWWorld::ESMStore &store = MWBase::Environment::get().getWorld()->getStore(); auto sound = store.get().begin(); while (sound != store.get().end()) { if (type == sound->mType && !sound->mCreature.empty() && Misc::StringUtils::ciEqual(ourId, sound->mCreature)) sounds.push_back(&*sound); if (type == sound->mType && sound->mCreature.empty()) fallbacksounds.push_back(&*sound); ++sound; } if (sounds.empty()) { const std::string model = getModel(ptr); if (!model.empty()) { for (const ESM::Creature &creature : store.get()) { if (creature.mId != ourId && creature.mOriginal != ourId && !creature.mModel.empty() && Misc::StringUtils::ciEqual(model, "meshes\\" + creature.mModel)) { const std::string& fallbackId = !creature.mOriginal.empty() ? creature.mOriginal : creature.mId; sound = store.get().begin(); while (sound != store.get().end()) { if (type == sound->mType && !sound->mCreature.empty() && Misc::StringUtils::ciEqual(fallbackId, sound->mCreature)) sounds.push_back(&*sound); ++sound; } break; } } } } if (!sounds.empty()) return sounds[Misc::Rng::rollDice(sounds.size())]->mSound; if (!fallbacksounds.empty()) return fallbacksounds[Misc::Rng::rollDice(fallbacksounds.size())]->mSound; return std::string(); } MWWorld::Ptr Creature::copyToCellImpl(const MWWorld::ConstPtr &ptr, MWWorld::CellStore &cell) const { const MWWorld::LiveCellRef *ref = ptr.get(); return MWWorld::Ptr(cell.insert(ref), &cell); } bool Creature::isBipedal(const MWWorld::ConstPtr &ptr) const { return isFlagBitSet(ptr, ESM::Creature::Bipedal); } bool Creature::canFly(const MWWorld::ConstPtr &ptr) const { return isFlagBitSet(ptr, ESM::Creature::Flies); } bool Creature::canSwim(const MWWorld::ConstPtr &ptr) const { return isFlagBitSet(ptr, static_cast(ESM::Creature::Swims | ESM::Creature::Bipedal)); } bool Creature::canWalk(const MWWorld::ConstPtr &ptr) const { return isFlagBitSet(ptr, static_cast(ESM::Creature::Walks | ESM::Creature::Bipedal)); } int Creature::getSndGenTypeFromName(const MWWorld::Ptr &ptr, const std::string &name) { if(name == "left") { MWBase::World *world = MWBase::Environment::get().getWorld(); if(world->isFlying(ptr)) return -1; osg::Vec3f pos(ptr.getRefData().getPosition().asVec3()); if(world->isUnderwater(ptr.getCell(), pos) || world->isWalkingOnWater(ptr)) return ESM::SoundGenerator::SwimLeft; if(world->isOnGround(ptr)) return ESM::SoundGenerator::LeftFoot; return -1; } if(name == "right") { MWBase::World *world = MWBase::Environment::get().getWorld(); if(world->isFlying(ptr)) return -1; osg::Vec3f pos(ptr.getRefData().getPosition().asVec3()); if(world->isUnderwater(ptr.getCell(), pos) || world->isWalkingOnWater(ptr)) return ESM::SoundGenerator::SwimRight; if(world->isOnGround(ptr)) return ESM::SoundGenerator::RightFoot; return -1; } if(name == "swimleft") return ESM::SoundGenerator::SwimLeft; if(name == "swimright") return ESM::SoundGenerator::SwimRight; if(name == "moan") return ESM::SoundGenerator::Moan; if(name == "roar") return ESM::SoundGenerator::Roar; if(name == "scream") return ESM::SoundGenerator::Scream; if(name == "land") return ESM::SoundGenerator::Land; throw std::runtime_error(std::string("Unexpected soundgen type: ")+name); } float Creature::getSkill(const MWWorld::Ptr &ptr, int skill) const { MWWorld::LiveCellRef *ref = ptr.get(); const ESM::Skill* skillRecord = MWBase::Environment::get().getWorld()->getStore().get().find(skill); switch (skillRecord->mData.mSpecialization) { case ESM::Class::Combat: return ref->mBase->mData.mCombat; case ESM::Class::Magic: return ref->mBase->mData.mMagic; case ESM::Class::Stealth: return ref->mBase->mData.mStealth; default: throw std::runtime_error("invalid specialisation"); } } int Creature::getBloodTexture(const MWWorld::ConstPtr &ptr) const { return ptr.get()->mBase->mBloodType; } void Creature::readAdditionalState (const MWWorld::Ptr& ptr, const ESM::ObjectState& state) const { if (!state.mHasCustomState) return; if (state.mVersion > 0) { if (!ptr.getRefData().getCustomData()) { // Create a CustomData, but don't fill it from ESM records (not needed) std::unique_ptr data (new CreatureCustomData); if (hasInventoryStore(ptr)) data->mContainerStore = std::make_unique(); else data->mContainerStore = std::make_unique(); ptr.getRefData().setCustomData (std::move(data)); } } else ensureCustomData(ptr); // in openmw 0.30 savegames not all state was saved yet, so need to load it regardless. CreatureCustomData& customData = ptr.getRefData().getCustomData()->asCreatureCustomData(); const ESM::CreatureState& creatureState = state.asCreatureState(); customData.mContainerStore->readState (creatureState.mInventory); bool spellsInitialised = customData.mCreatureStats.getSpells().setSpells(ptr.get()->mBase->mId); if(spellsInitialised) customData.mCreatureStats.getSpells().clear(); customData.mCreatureStats.readState (creatureState.mCreatureStats); } void Creature::writeAdditionalState (const MWWorld::ConstPtr& ptr, ESM::ObjectState& state) const { if (!ptr.getRefData().getCustomData()) { state.mHasCustomState = false; return; } if (ptr.getRefData().getCount() <= 0) { state.mHasCustomState = false; return; } const CreatureCustomData& customData = ptr.getRefData().getCustomData()->asCreatureCustomData(); ESM::CreatureState& creatureState = state.asCreatureState(); customData.mContainerStore->writeState (creatureState.mInventory); customData.mCreatureStats.writeState (creatureState.mCreatureStats); } int Creature::getBaseGold(const MWWorld::ConstPtr& ptr) const { return ptr.get()->mBase->mData.mGold; } void Creature::respawn(const MWWorld::Ptr &ptr) const { const MWMechanics::CreatureStats& creatureStats = getCreatureStats(ptr); if (ptr.getRefData().getCount() > 0 && !creatureStats.isDead()) return; if (!creatureStats.isDeathAnimationFinished()) return; const MWWorld::Store& gmst = MWBase::Environment::get().getWorld()->getStore().get(); static const float fCorpseRespawnDelay = gmst.find("fCorpseRespawnDelay")->mValue.getFloat(); static const float fCorpseClearDelay = gmst.find("fCorpseClearDelay")->mValue.getFloat(); float delay = ptr.getRefData().getCount() == 0 ? fCorpseClearDelay : std::min(fCorpseRespawnDelay, fCorpseClearDelay); if (isFlagBitSet(ptr, ESM::Creature::Respawn) && creatureStats.getTimeOfDeath() + delay <= MWBase::Environment::get().getWorld()->getTimeStamp()) { if (ptr.getCellRef().hasContentFile()) { if (ptr.getRefData().getCount() == 0) { ptr.getRefData().setCount(1); const std::string& script = getScript(ptr); if(!script.empty()) MWBase::Environment::get().getWorld()->getLocalScripts().add(script, ptr); } MWBase::Environment::get().getWorld()->removeContainerScripts(ptr); ptr.getRefData().setCustomData(nullptr); // Reset to original position MWBase::Environment::get().getWorld()->moveObject(ptr, ptr.getCellRef().getPosition().pos[0], ptr.getCellRef().getPosition().pos[1], ptr.getCellRef().getPosition().pos[2]); } } } int Creature::getBaseFightRating(const MWWorld::ConstPtr &ptr) const { const MWWorld::LiveCellRef *ref = ptr.get(); return ref->mBase->mAiData.mFight; } void Creature::adjustScale(const MWWorld::ConstPtr &ptr, osg::Vec3f &scale, bool /* rendering */) const { const MWWorld::LiveCellRef *ref = ptr.get(); scale *= ref->mBase->mScale; } void Creature::setBaseAISetting(const std::string& id, MWMechanics::CreatureStats::AiSetting setting, int value) const { MWMechanics::setBaseAISetting(id, setting, value); } void Creature::modifyBaseInventory(const std::string& actorId, const std::string& itemId, int amount) const { MWMechanics::modifyBaseInventory(actorId, itemId, amount); } float Creature::getWalkSpeed(const MWWorld::Ptr& ptr) const { const MWMechanics::CreatureStats& stats = getCreatureStats(ptr); const GMST& gmst = getGmst(); return gmst.fMinWalkSpeedCreature->mValue.getFloat() + 0.01f * stats.getAttribute(ESM::Attribute::Speed).getModified() * (gmst.fMaxWalkSpeedCreature->mValue.getFloat() - gmst.fMinWalkSpeedCreature->mValue.getFloat()); } float Creature::getRunSpeed(const MWWorld::Ptr& ptr) const { return getWalkSpeed(ptr); } float Creature::getSwimSpeed(const MWWorld::Ptr& ptr) const { const MWMechanics::CreatureStats& stats = getCreatureStats(ptr); const GMST& gmst = getGmst(); const MWMechanics::MagicEffects& mageffects = stats.getMagicEffects(); return getWalkSpeed(ptr) * (1.0f + 0.01f * mageffects.get(ESM::MagicEffect::SwiftSwim).getMagnitude()) * (gmst.fSwimRunBase->mValue.getFloat() + 0.01f * getSkill(ptr, ESM::Skill::Athletics) * gmst.fSwimRunAthleticsMult->mValue.getFloat()); } } ================================================ FILE: apps/openmw/mwclass/creature.hpp ================================================ #ifndef GAME_MWCLASS_CREATURE_H #define GAME_MWCLASS_CREATURE_H #include "actor.hpp" namespace ESM { struct GameSetting; } namespace MWClass { class Creature : public Actor { void ensureCustomData (const MWWorld::Ptr& ptr) const; MWWorld::Ptr copyToCellImpl(const MWWorld::ConstPtr &ptr, MWWorld::CellStore &cell) const override; static int getSndGenTypeFromName(const MWWorld::Ptr &ptr, const std::string &name); // cached GMSTs struct GMST { const ESM::GameSetting *fMinWalkSpeedCreature; const ESM::GameSetting *fMaxWalkSpeedCreature; const ESM::GameSetting *fEncumberedMoveEffect; const ESM::GameSetting *fSneakSpeedMultiplier; const ESM::GameSetting *fAthleticsRunBonus; const ESM::GameSetting *fBaseRunMultiplier; const ESM::GameSetting *fMinFlySpeed; const ESM::GameSetting *fMaxFlySpeed; const ESM::GameSetting *fSwimRunBase; const ESM::GameSetting *fSwimRunAthleticsMult; const ESM::GameSetting *fKnockDownMult; const ESM::GameSetting *iKnockDownOddsMult; const ESM::GameSetting *iKnockDownOddsBase; }; static const GMST& getGmst(); public: void insertObjectRendering (const MWWorld::Ptr& ptr, const std::string& model, MWRender::RenderingInterface& renderingInterface) const override; ///< Add reference into a cell for rendering std::string getName (const MWWorld::ConstPtr& ptr) const override; ///< \return name or ID; can return an empty string. bool hasToolTip(const MWWorld::ConstPtr& ptr) const override; ///< @return true if this object has a tooltip when focused (default implementation: true) MWGui::ToolTipInfo getToolTipInfo (const MWWorld::ConstPtr& ptr, int count) const override; ///< @return the content of the tool tip to be displayed. raises exception if the object has no tooltip. MWMechanics::CreatureStats& getCreatureStats (const MWWorld::Ptr& ptr) const override; ///< Return creature stats void hit(const MWWorld::Ptr& ptr, float attackStrength, int type) const override; void onHit(const MWWorld::Ptr &ptr, float damage, bool ishealth, const MWWorld::Ptr &object, const MWWorld::Ptr &attacker, const osg::Vec3f &hitPosition, bool successful) const override; std::shared_ptr activate (const MWWorld::Ptr& ptr, const MWWorld::Ptr& actor) const override; ///< Generate action for activation MWWorld::ContainerStore& getContainerStore ( const MWWorld::Ptr& ptr) const override; ///< Return container store MWWorld::InventoryStore& getInventoryStore (const MWWorld::Ptr& ptr) const override; ///< Return inventory store bool hasInventoryStore (const MWWorld::Ptr &ptr) const override; /* Start of tes3mp addition Make it possible to check whether a class has a container store */ virtual bool hasContainerStore(const MWWorld::Ptr &ptr) const { return true; } /* End of tes3mp addition */ std::string getScript (const MWWorld::ConstPtr& ptr) const override; ///< Return name of the script attached to ptr float getCapacity (const MWWorld::Ptr& ptr) const override; ///< Return total weight that fits into the object. Throws an exception, if the object can't /// hold other objects. float getArmorRating (const MWWorld::Ptr& ptr) const override; ///< @return combined armor rating of this actor bool isEssential (const MWWorld::ConstPtr& ptr) const override; ///< Is \a ptr essential? (i.e. may losing \a ptr make the game unwinnable) int getServices (const MWWorld::ConstPtr& actor) const override; bool isPersistent (const MWWorld::ConstPtr& ptr) const override; std::string getSoundIdFromSndGen(const MWWorld::Ptr &ptr, const std::string &name) const override; MWMechanics::Movement& getMovementSettings (const MWWorld::Ptr& ptr) const override; ///< Return desired movement. float getMaxSpeed (const MWWorld::Ptr& ptr) const override; static void registerSelf(); std::string getModel(const MWWorld::ConstPtr &ptr) const override; void getModelsToPreload(const MWWorld::Ptr& ptr, std::vector& models) const override; ///< Get a list of models to preload that this object may use (directly or indirectly). default implementation: list getModel(). bool isBipedal (const MWWorld::ConstPtr &ptr) const override; bool canFly (const MWWorld::ConstPtr &ptr) const override; bool canSwim (const MWWorld::ConstPtr &ptr) const override; bool canWalk (const MWWorld::ConstPtr &ptr) const override; float getSkill(const MWWorld::Ptr &ptr, int skill) const override; /// Get a blood texture suitable for \a ptr (see Blood Texture 0-2 in Morrowind.ini) int getBloodTexture (const MWWorld::ConstPtr& ptr) const override; void readAdditionalState (const MWWorld::Ptr& ptr, const ESM::ObjectState& state) const override; ///< Read additional state from \a state into \a ptr. void writeAdditionalState (const MWWorld::ConstPtr& ptr, ESM::ObjectState& state) const override; ///< Write additional state from \a ptr into \a state. int getBaseGold(const MWWorld::ConstPtr& ptr) const override; void respawn (const MWWorld::Ptr& ptr) const override; int getBaseFightRating(const MWWorld::ConstPtr &ptr) const override; void adjustScale(const MWWorld::ConstPtr& ptr, osg::Vec3f& scale, bool rendering) const override; /// @param rendering Indicates if the scale to adjust is for the rendering mesh, or for the collision mesh void setBaseAISetting(const std::string& id, MWMechanics::CreatureStats::AiSetting setting, int value) const override; void modifyBaseInventory(const std::string& actorId, const std::string& itemId, int amount) const override; float getWalkSpeed(const MWWorld::Ptr& ptr) const override; float getRunSpeed(const MWWorld::Ptr& ptr) const override; float getSwimSpeed(const MWWorld::Ptr& ptr) const override; }; } #endif ================================================ FILE: apps/openmw/mwclass/creaturelevlist.cpp ================================================ #include "creaturelevlist.hpp" #include #include #include "../mwmechanics/levelledlist.hpp" #include "../mwworld/customdata.hpp" #include "../mwmechanics/creaturestats.hpp" namespace MWClass { class CreatureLevListCustomData : public MWWorld::TypedCustomData { public: // actorId of the creature we spawned int mSpawnActorId; bool mSpawn; // Should a new creature be spawned? CreatureLevListCustomData& asCreatureLevListCustomData() override { return *this; } const CreatureLevListCustomData& asCreatureLevListCustomData() const override { return *this; } }; std::string CreatureLevList::getName (const MWWorld::ConstPtr& ptr) const { return ""; } bool CreatureLevList::hasToolTip(const MWWorld::ConstPtr& ptr) const { return false; } void CreatureLevList::respawn(const MWWorld::Ptr &ptr) const { ensureCustomData(ptr); CreatureLevListCustomData& customData = ptr.getRefData().getCustomData()->asCreatureLevListCustomData(); if (customData.mSpawn) return; MWWorld::Ptr creature = (customData.mSpawnActorId == -1) ? MWWorld::Ptr() : MWBase::Environment::get().getWorld()->searchPtrViaActorId(customData.mSpawnActorId); if (!creature.isEmpty()) { const MWMechanics::CreatureStats& creatureStats = creature.getClass().getCreatureStats(creature); if (creature.getRefData().getCount() == 0) customData.mSpawn = true; else if (creatureStats.isDead()) { const MWWorld::Store& gmst = MWBase::Environment::get().getWorld()->getStore().get(); static const float fCorpseRespawnDelay = gmst.find("fCorpseRespawnDelay")->mValue.getFloat(); static const float fCorpseClearDelay = gmst.find("fCorpseClearDelay")->mValue.getFloat(); float delay = std::min(fCorpseRespawnDelay, fCorpseClearDelay); if (creatureStats.getTimeOfDeath() + delay <= MWBase::Environment::get().getWorld()->getTimeStamp()) customData.mSpawn = true; } } else customData.mSpawn = true; } void CreatureLevList::registerSelf() { std::shared_ptr instance (new CreatureLevList); registerClass (typeid (ESM::CreatureLevList).name(), instance); } void CreatureLevList::getModelsToPreload(const MWWorld::Ptr &ptr, std::vector &models) const { // disable for now, too many false positives /* const MWWorld::LiveCellRef *ref = ptr.get(); for (std::vector::const_iterator it = ref->mBase->mList.begin(); it != ref->mBase->mList.end(); ++it) { MWWorld::Ptr player = MWBase::Environment::get().getWorld()->getPlayerPtr(); if (it->mLevel > player.getClass().getCreatureStats(player).getLevel()) continue; const MWWorld::ESMStore& store = MWBase::Environment::get().getWorld()->getStore(); MWWorld::ManualRef ref(store, it->mId); ref.getPtr().getClass().getModelsToPreload(ref.getPtr(), models); } */ } void CreatureLevList::insertObjectRendering(const MWWorld::Ptr &ptr, const std::string& model, MWRender::RenderingInterface &renderingInterface) const { ensureCustomData(ptr); CreatureLevListCustomData& customData = ptr.getRefData().getCustomData()->asCreatureLevListCustomData(); if (!customData.mSpawn) return; MWWorld::LiveCellRef *ref = ptr.get(); std::string id = MWMechanics::getLevelledItem(ref->mBase, true); if (!id.empty()) { // Delete the previous creature if (customData.mSpawnActorId != -1) { MWWorld::Ptr creature = MWBase::Environment::get().getWorld()->searchPtrViaActorId(customData.mSpawnActorId); if (!creature.isEmpty()) MWBase::Environment::get().getWorld()->deleteObject(creature); customData.mSpawnActorId = -1; } const MWWorld::ESMStore& store = MWBase::Environment::get().getWorld()->getStore(); MWWorld::ManualRef manualRef(store, id); manualRef.getPtr().getCellRef().setPosition(ptr.getCellRef().getPosition()); /* Start of tes3mp change (major) Don't spawn leveled creatures in multiplayer; they'll be spawned when the server requests them */ /* manualRef.getPtr().getCellRef().setScale(ptr.getCellRef().getScale()); MWWorld::Ptr placed = MWBase::Environment::get().getWorld()->placeObject(manualRef.getPtr(), ptr.getCell() , ptr.getCellRef().getPosition()); customData.mSpawnActorId = placed.getClass().getCreatureStats(placed).getActorId(); */ /* End of tes3mp change (major) */ customData.mSpawn = false; } else customData.mSpawn = false; } void CreatureLevList::ensureCustomData(const MWWorld::Ptr &ptr) const { if (!ptr.getRefData().getCustomData()) { std::unique_ptr data = std::make_unique(); data->mSpawnActorId = -1; data->mSpawn = true; ptr.getRefData().setCustomData(std::move(data)); } } void CreatureLevList::readAdditionalState (const MWWorld::Ptr& ptr, const ESM::ObjectState& state) const { if (!state.mHasCustomState) return; ensureCustomData(ptr); CreatureLevListCustomData& customData = ptr.getRefData().getCustomData()->asCreatureLevListCustomData(); const ESM::CreatureLevListState& levListState = state.asCreatureLevListState(); customData.mSpawnActorId = levListState.mSpawnActorId; customData.mSpawn = levListState.mSpawn; } void CreatureLevList::writeAdditionalState (const MWWorld::ConstPtr& ptr, ESM::ObjectState& state) const { if (!ptr.getRefData().getCustomData()) { state.mHasCustomState = false; return; } const CreatureLevListCustomData& customData = ptr.getRefData().getCustomData()->asCreatureLevListCustomData(); ESM::CreatureLevListState& levListState = state.asCreatureLevListState(); levListState.mSpawnActorId = customData.mSpawnActorId; levListState.mSpawn = customData.mSpawn; } } ================================================ FILE: apps/openmw/mwclass/creaturelevlist.hpp ================================================ #ifndef GAME_MWCLASS_CREATURELEVLIST_H #define GAME_MWCLASS_CREATURELEVLIST_H #include "../mwworld/class.hpp" namespace MWClass { class CreatureLevList : public MWWorld::Class { void ensureCustomData (const MWWorld::Ptr& ptr) const; public: std::string getName (const MWWorld::ConstPtr& ptr) const override; ///< \return name or ID; can return an empty string. bool hasToolTip (const MWWorld::ConstPtr& ptr) const override; ///< @return true if this object has a tooltip when focused (default implementation: true) static void registerSelf(); void getModelsToPreload(const MWWorld::Ptr& ptr, std::vector& models) const override; ///< Get a list of models to preload that this object may use (directly or indirectly). default implementation: list getModel(). void insertObjectRendering (const MWWorld::Ptr& ptr, const std::string& model, MWRender::RenderingInterface& renderingInterface) const override; ///< Add reference into a cell for rendering void readAdditionalState (const MWWorld::Ptr& ptr, const ESM::ObjectState& state) const override; ///< Read additional state from \a state into \a ptr. void writeAdditionalState (const MWWorld::ConstPtr& ptr, ESM::ObjectState& state) const override; ///< Write additional state from \a ptr into \a state. void respawn (const MWWorld::Ptr& ptr) const override; }; } #endif ================================================ FILE: apps/openmw/mwclass/door.cpp ================================================ #include "door.hpp" /* Start of tes3mp addition Include additional headers for multiplayer purposes */ #include "../mwmp/Main.hpp" #include "../mwmp/Networking.hpp" #include "../mwmp/ObjectList.hpp" /* End of tes3mp addition */ #include #include #include #include "../mwbase/environment.hpp" #include "../mwbase/world.hpp" #include "../mwbase/windowmanager.hpp" #include "../mwbase/soundmanager.hpp" #include "../mwworld/ptr.hpp" #include "../mwworld/failedaction.hpp" #include "../mwworld/actionteleport.hpp" #include "../mwworld/actiondoor.hpp" #include "../mwworld/cellstore.hpp" #include "../mwworld/esmstore.hpp" #include "../mwphysics/physicssystem.hpp" #include "../mwworld/inventorystore.hpp" #include "../mwworld/actiontrap.hpp" #include "../mwworld/customdata.hpp" #include "../mwgui/tooltips.hpp" #include "../mwrender/objects.hpp" #include "../mwrender/renderinginterface.hpp" #include "../mwrender/animation.hpp" #include "../mwrender/vismask.hpp" #include "../mwmechanics/actorutil.hpp" namespace MWClass { class DoorCustomData : public MWWorld::TypedCustomData { public: MWWorld::DoorState mDoorState = MWWorld::DoorState::Idle; DoorCustomData& asDoorCustomData() override { return *this; } const DoorCustomData& asDoorCustomData() const override { return *this; } }; void Door::insertObjectRendering (const MWWorld::Ptr& ptr, const std::string& model, MWRender::RenderingInterface& renderingInterface) const { if (!model.empty()) { renderingInterface.getObjects().insertModel(ptr, model, true); ptr.getRefData().getBaseNode()->setNodeMask(MWRender::Mask_Static); } } void Door::insertObject(const MWWorld::Ptr& ptr, const std::string& model, MWPhysics::PhysicsSystem& physics) const { if(!model.empty()) physics.addObject(ptr, model, MWPhysics::CollisionType_Door); // Resume the door's opening/closing animation if it wasn't finished if (ptr.getRefData().getCustomData()) { const DoorCustomData& customData = ptr.getRefData().getCustomData()->asDoorCustomData(); if (customData.mDoorState != MWWorld::DoorState::Idle) { MWBase::Environment::get().getWorld()->activateDoor(ptr, customData.mDoorState); } } } bool Door::isDoor() const { return true; } bool Door::useAnim() const { return true; } std::string Door::getModel(const MWWorld::ConstPtr &ptr) const { const MWWorld::LiveCellRef *ref = ptr.get(); const std::string &model = ref->mBase->mModel; if (!model.empty()) { return "meshes\\" + model; } return ""; } std::string Door::getName (const MWWorld::ConstPtr& ptr) const { const MWWorld::LiveCellRef *ref = ptr.get(); const std::string& name = ref->mBase->mName; return !name.empty() ? name : ref->mBase->mId; } std::shared_ptr Door::activate (const MWWorld::Ptr& ptr, const MWWorld::Ptr& actor) const { MWWorld::LiveCellRef *ref = ptr.get(); const std::string &openSound = ref->mBase->mOpenSound; const std::string &closeSound = ref->mBase->mCloseSound; const std::string lockedSound = "LockedDoor"; const std::string trapActivationSound = "Disarm Trap Fail"; MWWorld::ContainerStore &invStore = actor.getClass().getContainerStore(actor); bool isLocked = ptr.getCellRef().getLockLevel() > 0; bool isTrapped = !ptr.getCellRef().getTrap().empty(); bool hasKey = false; std::string keyName; // FIXME: If NPC activate teleporting door, it can lead to crash due to iterator invalidation in the Actors update. // Make such activation a no-op for now, like how it is in the vanilla game. if (actor != MWMechanics::getPlayer() && ptr.getCellRef().getTeleport()) { std::shared_ptr action(new MWWorld::FailedAction(std::string(), ptr)); action->setSound(lockedSound); return action; } // make door glow if player activates it with telekinesis if (actor == MWMechanics::getPlayer() && MWBase::Environment::get().getWorld()->getDistanceToFacedObject() > MWBase::Environment::get().getWorld()->getMaxActivationDistance()) { MWRender::Animation* animation = MWBase::Environment::get().getWorld()->getAnimation(ptr); if(animation) { const MWWorld::ESMStore& store = MWBase::Environment::get().getWorld()->getStore(); int index = ESM::MagicEffect::effectStringToId("sEffectTelekinesis"); const ESM::MagicEffect *effect = store.get().find(index); animation->addSpellCastGlow(effect, 1); // 1 second glow to match the time taken for a door opening or closing } } const std::string keyId = ptr.getCellRef().getKey(); if (!keyId.empty()) { MWWorld::Ptr keyPtr = invStore.search(keyId); if (!keyPtr.isEmpty()) { hasKey = true; keyName = keyPtr.getClass().getName(keyPtr); } } if (isLocked && hasKey) { if(actor == MWMechanics::getPlayer()) MWBase::Environment::get().getWindowManager()->messageBox(keyName + " #{sKeyUsed}"); /* Start of tes3mp change (major) Disable unilateral unlocking on this client and expect the server's reply to our packet to do it instead */ //ptr.getCellRef().unlock(); //Call the function here. because that makes sense. /* End of tes3mp change (major) */ // using a key disarms the trap if(isTrapped) { /* Start of tes3mp change (major) Disable unilateral trap disarming on this client and expect the server's reply to our packet to do it instead */ //ptr.getCellRef().setTrap(""); //MWBase::Environment::get().getSoundManager()->playSound3D(ptr, "Disarm Trap", 1.0f, 1.0f); /* End of tes3mp change (major) */ isTrapped = false; /* Start of tes3mp addition Send an ID_OBJECT_TRAP packet every time a trap is disarmed */ mwmp::ObjectList *objectList = mwmp::Main::get().getNetworking()->getObjectList(); objectList->reset(); objectList->packetOrigin = mwmp::CLIENT_GAMEPLAY; objectList->addObjectTrap(ptr, ptr.getRefData().getPosition(), true); objectList->sendObjectTrap(); /* End of tes3mp addition */ } /* Start of tes3mp addition Send an ID_OBJECT_LOCK packet every time a door is unlocked here */ if (isLocked) { mwmp::ObjectList *objectList = mwmp::Main::get().getNetworking()->getObjectList(); objectList->reset(); objectList->packetOrigin = mwmp::CLIENT_GAMEPLAY; objectList->addObjectLock(ptr, 0); objectList->sendObjectLock(); } /* End of tes3mp addition */ } if (!isLocked || hasKey) { if(isTrapped) { // Trap activation std::shared_ptr action(new MWWorld::ActionTrap(ptr.getCellRef().getTrap(), ptr)); action->setSound(trapActivationSound); return action; } if (ptr.getCellRef().getTeleport()) { if (actor == MWMechanics::getPlayer() && MWBase::Environment::get().getWorld()->getDistanceToFacedObject() > MWBase::Environment::get().getWorld()->getMaxActivationDistance()) { // player activated teleport door with telekinesis std::shared_ptr action(new MWWorld::FailedAction); return action; } else { /* Start of tes3mp change (major) If there is a destination override in the mwmp::Worldstate for this door's original destination, use it */ std::string destinationCell = ptr.getCellRef().getDestCell(); if (mwmp::Main::get().getNetworking()->getWorldstate()->destinationOverrides.count(destinationCell) != 0) destinationCell = mwmp::Main::get().getNetworking()->getWorldstate()->destinationOverrides[destinationCell]; std::shared_ptr action(new MWWorld::ActionTeleport(destinationCell, ptr.getCellRef().getDoorDest(), true)); /* End of tes3mp change (major) */ action->setSound(openSound); return action; } } else { // animated door std::shared_ptr action(new MWWorld::ActionDoor(ptr)); const auto doorState = getDoorState(ptr); bool opening = true; float doorRot = ptr.getRefData().getPosition().rot[2] - ptr.getCellRef().getPosition().rot[2]; if (doorState == MWWorld::DoorState::Opening) opening = false; if (doorState == MWWorld::DoorState::Idle && doorRot != 0) opening = false; if (opening) { MWBase::Environment::get().getSoundManager()->fadeOutSound3D(ptr, closeSound, 0.5f); // Doors rotate at 90 degrees per second, so start the sound at // where it would be at the current rotation. float offset = doorRot/(osg::PI * 0.5f); action->setSoundOffset(offset); action->setSound(openSound); } else { MWBase::Environment::get().getSoundManager()->fadeOutSound3D(ptr, openSound, 0.5f); float offset = 1.0f - doorRot/(osg::PI * 0.5f); action->setSoundOffset(std::max(offset, 0.0f)); action->setSound(closeSound); } return action; } } else { // locked, and we can't open. std::shared_ptr action(new MWWorld::FailedAction(std::string(), ptr)); action->setSound(lockedSound); return action; } } bool Door::canLock(const MWWorld::ConstPtr &ptr) const { return true; } bool Door::allowTelekinesis(const MWWorld::ConstPtr &ptr) const { if (ptr.getCellRef().getTeleport() && ptr.getCellRef().getLockLevel() <= 0 && ptr.getCellRef().getTrap().empty()) return false; else return true; } std::string Door::getScript (const MWWorld::ConstPtr& ptr) const { const MWWorld::LiveCellRef *ref = ptr.get(); return ref->mBase->mScript; } void Door::registerSelf() { std::shared_ptr instance (new Door); registerClass (typeid (ESM::Door).name(), instance); } MWGui::ToolTipInfo Door::getToolTipInfo (const MWWorld::ConstPtr& ptr, int count) const { const MWWorld::LiveCellRef *ref = ptr.get(); MWGui::ToolTipInfo info; info.caption = MyGUI::TextIterator::toTagsString(getName(ptr)); std::string text; if (ptr.getCellRef().getTeleport()) { text += "\n#{sTo}"; text += "\n" + getDestination(*ref); } int lockLevel = ptr.getCellRef().getLockLevel(); if (lockLevel > 0 && lockLevel != ESM::UnbreakableLock) text += "\n#{sLockLevel}: " + MWGui::ToolTips::toString(ptr.getCellRef().getLockLevel()); else if (ptr.getCellRef().getLockLevel() < 0) text += "\n#{sUnlocked}"; if (ptr.getCellRef().getTrap() != "") text += "\n#{sTrapped}"; if (MWBase::Environment::get().getWindowManager()->getFullHelp()) { text += MWGui::ToolTips::getCellRefString(ptr.getCellRef()); text += MWGui::ToolTips::getMiscString(ref->mBase->mScript, "Script"); } info.text = text; return info; } std::string Door::getDestination(const MWWorld::LiveCellRef& door) { const MWWorld::ESMStore& store = MWBase::Environment::get().getWorld()->getStore(); std::string dest = door.mRef.getDestCell(); if (dest.empty()) { // door leads to exterior, use cell name (if any), otherwise translated region name int x, y; auto world = MWBase::Environment::get().getWorld(); world->positionToIndex(door.mRef.getDoorDest().pos[0], door.mRef.getDoorDest().pos[1], x, y); const ESM::Cell* cell = world->getStore().get().search(x, y); dest = world->getCellName(cell); } /* Start of tes3mp addition If there is a destination override in the mwmp::Worldstate for this door's original destination, use it */ else if (mwmp::Main::get().getNetworking()->getWorldstate()->destinationOverrides.count(dest) != 0) dest = mwmp::Main::get().getNetworking()->getWorldstate()->destinationOverrides[dest]; /* End of tes3mp addition */ return "#{sCell=" + dest + "}"; } MWWorld::Ptr Door::copyToCellImpl(const MWWorld::ConstPtr &ptr, MWWorld::CellStore &cell) const { const MWWorld::LiveCellRef *ref = ptr.get(); return MWWorld::Ptr(cell.insert(ref), &cell); } void Door::ensureCustomData(const MWWorld::Ptr &ptr) const { if (!ptr.getRefData().getCustomData()) { ptr.getRefData().setCustomData(std::make_unique()); } } MWWorld::DoorState Door::getDoorState (const MWWorld::ConstPtr &ptr) const { if (!ptr.getRefData().getCustomData()) return MWWorld::DoorState::Idle; const DoorCustomData& customData = ptr.getRefData().getCustomData()->asDoorCustomData(); return customData.mDoorState; } void Door::setDoorState (const MWWorld::Ptr &ptr, MWWorld::DoorState state) const { if (ptr.getCellRef().getTeleport()) throw std::runtime_error("load doors can't be moved"); ensureCustomData(ptr); DoorCustomData& customData = ptr.getRefData().getCustomData()->asDoorCustomData(); customData.mDoorState = state; } void Door::readAdditionalState (const MWWorld::Ptr& ptr, const ESM::ObjectState& state) const { if (!state.mHasCustomState) return; ensureCustomData(ptr); DoorCustomData& customData = ptr.getRefData().getCustomData()->asDoorCustomData(); const ESM::DoorState& doorState = state.asDoorState(); customData.mDoorState = MWWorld::DoorState(doorState.mDoorState); } void Door::writeAdditionalState (const MWWorld::ConstPtr& ptr, ESM::ObjectState& state) const { if (!ptr.getRefData().getCustomData()) { state.mHasCustomState = false; return; } const DoorCustomData& customData = ptr.getRefData().getCustomData()->asDoorCustomData(); ESM::DoorState& doorState = state.asDoorState(); doorState.mDoorState = int(customData.mDoorState); } } ================================================ FILE: apps/openmw/mwclass/door.hpp ================================================ #ifndef GAME_MWCLASS_DOOR_H #define GAME_MWCLASS_DOOR_H #include #include "../mwworld/class.hpp" namespace MWClass { class Door : public MWWorld::Class { void ensureCustomData (const MWWorld::Ptr& ptr) const; MWWorld::Ptr copyToCellImpl(const MWWorld::ConstPtr &ptr, MWWorld::CellStore &cell) const override; public: void insertObjectRendering (const MWWorld::Ptr& ptr, const std::string& model, MWRender::RenderingInterface& renderingInterface) const override; ///< Add reference into a cell for rendering void insertObject(const MWWorld::Ptr& ptr, const std::string& model, MWPhysics::PhysicsSystem& physics) const override; bool isDoor() const override; bool useAnim() const override; std::string getName (const MWWorld::ConstPtr& ptr) const override; ///< \return name or ID; can return an empty string. std::shared_ptr activate (const MWWorld::Ptr& ptr, const MWWorld::Ptr& actor) const override; ///< Generate action for activation MWGui::ToolTipInfo getToolTipInfo (const MWWorld::ConstPtr& ptr, int count) const override; ///< @return the content of the tool tip to be displayed. raises exception if the object has no tooltip. static std::string getDestination (const MWWorld::LiveCellRef& door); ///< @return destination cell name or token bool canLock(const MWWorld::ConstPtr &ptr) const override; bool allowTelekinesis(const MWWorld::ConstPtr &ptr) const override; ///< Return whether this class of object can be activated with telekinesis std::string getScript (const MWWorld::ConstPtr& ptr) const override; ///< Return name of the script attached to ptr static void registerSelf(); std::string getModel(const MWWorld::ConstPtr &ptr) const override; MWWorld::DoorState getDoorState (const MWWorld::ConstPtr &ptr) const override; /// This does not actually cause the door to move. Use World::activateDoor instead. void setDoorState (const MWWorld::Ptr &ptr, MWWorld::DoorState state) const override; void readAdditionalState (const MWWorld::Ptr& ptr, const ESM::ObjectState& state) const override; ///< Read additional state from \a state into \a ptr. void writeAdditionalState (const MWWorld::ConstPtr& ptr, ESM::ObjectState& state) const override; ///< Write additional state from \a ptr into \a state. }; } #endif ================================================ FILE: apps/openmw/mwclass/ingredient.cpp ================================================ #include "ingredient.hpp" /* Start of tes3mp addition Include additional headers for multiplayer purposes */ #include #include "../mwmp/Main.hpp" #include "../mwmp/Networking.hpp" /* End of tes3mp addition */ #include #include "../mwbase/environment.hpp" #include "../mwbase/world.hpp" #include "../mwbase/windowmanager.hpp" #include "../mwworld/ptr.hpp" #include "../mwworld/cellstore.hpp" #include "../mwworld/esmstore.hpp" #include "../mwphysics/physicssystem.hpp" #include "../mwworld/actioneat.hpp" #include "../mwworld/nullaction.hpp" #include "../mwgui/tooltips.hpp" #include "../mwrender/objects.hpp" #include "../mwrender/renderinginterface.hpp" namespace MWClass { void Ingredient::insertObjectRendering (const MWWorld::Ptr& ptr, const std::string& model, MWRender::RenderingInterface& renderingInterface) const { if (!model.empty()) { renderingInterface.getObjects().insertModel(ptr, model); } } void Ingredient::insertObject(const MWWorld::Ptr& ptr, const std::string& model, MWPhysics::PhysicsSystem& physics) const { // TODO: add option somewhere to enable collision for placeable objects /* Start of tes3mp addition Make it possible to enable collision for this object class from a packet */ if (!model.empty()) { mwmp::BaseWorldstate *worldstate = mwmp::Main::get().getNetworking()->getWorldstate(); if (worldstate->hasPlacedObjectCollision || Utils::vectorContains(worldstate->enforcedCollisionRefIds, ptr.getCellRef().getRefId())) { if (worldstate->useActorCollisionForPlacedObjects) physics.addObject(ptr, model, MWPhysics::CollisionType_Actor); else physics.addObject(ptr, model, MWPhysics::CollisionType_World); } } /* End of tes3mp addition */ } std::string Ingredient::getModel(const MWWorld::ConstPtr &ptr) const { const MWWorld::LiveCellRef *ref = ptr.get(); const std::string &model = ref->mBase->mModel; if (!model.empty()) { return "meshes\\" + model; } return ""; } std::string Ingredient::getName (const MWWorld::ConstPtr& ptr) const { const MWWorld::LiveCellRef *ref = ptr.get(); const std::string& name = ref->mBase->mName; return !name.empty() ? name : ref->mBase->mId; } std::shared_ptr Ingredient::activate (const MWWorld::Ptr& ptr, const MWWorld::Ptr& actor) const { return defaultItemActivate(ptr, actor); } std::string Ingredient::getScript (const MWWorld::ConstPtr& ptr) const { const MWWorld::LiveCellRef *ref = ptr.get(); return ref->mBase->mScript; } int Ingredient::getValue (const MWWorld::ConstPtr& ptr) const { const MWWorld::LiveCellRef *ref = ptr.get(); return ref->mBase->mData.mValue; } std::shared_ptr Ingredient::use (const MWWorld::Ptr& ptr, bool force) const { std::shared_ptr action (new MWWorld::ActionEat (ptr)); action->setSound ("Swallow"); return action; } void Ingredient::registerSelf() { std::shared_ptr instance (new Ingredient); registerClass (typeid (ESM::Ingredient).name(), instance); } std::string Ingredient::getUpSoundId (const MWWorld::ConstPtr& ptr) const { return std::string("Item Ingredient Up"); } std::string Ingredient::getDownSoundId (const MWWorld::ConstPtr& ptr) const { return std::string("Item Ingredient Down"); } std::string Ingredient::getInventoryIcon (const MWWorld::ConstPtr& ptr) const { const MWWorld::LiveCellRef *ref = ptr.get(); return ref->mBase->mIcon; } MWGui::ToolTipInfo Ingredient::getToolTipInfo (const MWWorld::ConstPtr& ptr, int count) const { const MWWorld::LiveCellRef *ref = ptr.get(); MWGui::ToolTipInfo info; info.caption = MyGUI::TextIterator::toTagsString(getName(ptr)) + MWGui::ToolTips::getCountString(count); info.icon = ref->mBase->mIcon; std::string text; text += MWGui::ToolTips::getWeightString(ref->mBase->mData.mWeight, "#{sWeight}"); text += MWGui::ToolTips::getValueString(ref->mBase->mData.mValue, "#{sValue}"); if (MWBase::Environment::get().getWindowManager()->getFullHelp()) { text += MWGui::ToolTips::getCellRefString(ptr.getCellRef()); text += MWGui::ToolTips::getMiscString(ref->mBase->mScript, "Script"); } MWWorld::Ptr player = MWBase::Environment::get().getWorld ()->getPlayerPtr(); float alchemySkill = player.getClass().getSkill(player, ESM::Skill::Alchemy); static const float fWortChanceValue = MWBase::Environment::get().getWorld()->getStore().get().find("fWortChanceValue")->mValue.getFloat(); MWGui::Widgets::SpellEffectList list; for (int i=0; i<4; ++i) { if (ref->mBase->mData.mEffectID[i] < 0) continue; MWGui::Widgets::SpellEffectParams params; params.mEffectID = ref->mBase->mData.mEffectID[i]; params.mAttribute = ref->mBase->mData.mAttributes[i]; params.mSkill = ref->mBase->mData.mSkills[i]; params.mKnown = ( (i == 0 && alchemySkill >= fWortChanceValue) || (i == 1 && alchemySkill >= fWortChanceValue*2) || (i == 2 && alchemySkill >= fWortChanceValue*3) || (i == 3 && alchemySkill >= fWortChanceValue*4)); list.push_back(params); } info.effects = list; info.text = text; info.isIngredient = true; return info; } MWWorld::Ptr Ingredient::copyToCellImpl(const MWWorld::ConstPtr &ptr, MWWorld::CellStore &cell) const { const MWWorld::LiveCellRef *ref = ptr.get(); return MWWorld::Ptr(cell.insert(ref), &cell); } bool Ingredient::canSell (const MWWorld::ConstPtr& item, int npcServices) const { return (npcServices & ESM::NPC::Ingredients) != 0; } float Ingredient::getWeight(const MWWorld::ConstPtr &ptr) const { const MWWorld::LiveCellRef *ref = ptr.get(); return ref->mBase->mData.mWeight; } } ================================================ FILE: apps/openmw/mwclass/ingredient.hpp ================================================ #ifndef GAME_MWCLASS_INGREDIENT_H #define GAME_MWCLASS_INGREDIENT_H #include "../mwworld/class.hpp" namespace MWClass { class Ingredient : public MWWorld::Class { MWWorld::Ptr copyToCellImpl(const MWWorld::ConstPtr &ptr, MWWorld::CellStore &cell) const override; public: void insertObjectRendering (const MWWorld::Ptr& ptr, const std::string& model, MWRender::RenderingInterface& renderingInterface) const override; ///< Add reference into a cell for rendering void insertObject(const MWWorld::Ptr& ptr, const std::string& model, MWPhysics::PhysicsSystem& physics) const override; std::string getName (const MWWorld::ConstPtr& ptr) const override; ///< \return name or ID; can return an empty string. std::shared_ptr activate (const MWWorld::Ptr& ptr, const MWWorld::Ptr& actor) const override; ///< Generate action for activation MWGui::ToolTipInfo getToolTipInfo (const MWWorld::ConstPtr& ptr, int count) const override; ///< @return the content of the tool tip to be displayed. raises exception if the object has no tooltip. std::string getScript (const MWWorld::ConstPtr& ptr) const override; ///< Return name of the script attached to ptr int getValue (const MWWorld::ConstPtr& ptr) const override; ///< Return trade value of the object. Throws an exception, if the object can't be traded. std::shared_ptr use (const MWWorld::Ptr& ptr, bool force=false) const override; ///< Generate action for using via inventory menu static void registerSelf(); std::string getUpSoundId (const MWWorld::ConstPtr& ptr) const override; ///< Return the pick up sound Id std::string getDownSoundId (const MWWorld::ConstPtr& ptr) const override; ///< Return the put down sound Id std::string getInventoryIcon (const MWWorld::ConstPtr& ptr) const override; ///< Return name of inventory icon. std::string getModel(const MWWorld::ConstPtr &ptr) const override; float getWeight (const MWWorld::ConstPtr& ptr) const override; bool canSell (const MWWorld::ConstPtr& item, int npcServices) const override; }; } #endif ================================================ FILE: apps/openmw/mwclass/itemlevlist.cpp ================================================ #include "itemlevlist.hpp" #include namespace MWClass { std::string ItemLevList::getName (const MWWorld::ConstPtr& ptr) const { return ""; } bool ItemLevList::hasToolTip(const MWWorld::ConstPtr& ptr) const { return false; } void ItemLevList::registerSelf() { std::shared_ptr instance (new ItemLevList); registerClass (typeid (ESM::ItemLevList).name(), instance); } } ================================================ FILE: apps/openmw/mwclass/itemlevlist.hpp ================================================ #ifndef GAME_MWCLASS_ITEMLEVLIST_H #define GAME_MWCLASS_ITEMLEVLIST_H #include "../mwworld/class.hpp" namespace MWClass { class ItemLevList : public MWWorld::Class { public: std::string getName (const MWWorld::ConstPtr& ptr) const override; ///< \return name or ID; can return an empty string. bool hasToolTip (const MWWorld::ConstPtr& ptr) const override; ///< @return true if this object has a tooltip when focused (default implementation: true) static void registerSelf(); }; } #endif ================================================ FILE: apps/openmw/mwclass/light.cpp ================================================ #include "light.hpp" #include #include #include #include "../mwbase/environment.hpp" #include "../mwbase/soundmanager.hpp" #include "../mwbase/windowmanager.hpp" #include "../mwworld/ptr.hpp" #include "../mwworld/actionequip.hpp" #include "../mwworld/nullaction.hpp" #include "../mwworld/failedaction.hpp" #include "../mwworld/inventorystore.hpp" #include "../mwworld/cellstore.hpp" #include "../mwphysics/physicssystem.hpp" #include "../mwgui/tooltips.hpp" #include "../mwrender/objects.hpp" #include "../mwrender/renderinginterface.hpp" namespace MWClass { void Light::insertObjectRendering (const MWWorld::Ptr& ptr, const std::string& model, MWRender::RenderingInterface& renderingInterface) const { MWWorld::LiveCellRef *ref = ptr.get(); // Insert even if model is empty, so that the light is added renderingInterface.getObjects().insertModel(ptr, model, true, !(ref->mBase->mData.mFlags & ESM::Light::OffDefault)); } void Light::insertObject(const MWWorld::Ptr& ptr, const std::string& model, MWPhysics::PhysicsSystem& physics) const { MWWorld::LiveCellRef *ref = ptr.get(); assert (ref->mBase != nullptr); // TODO: add option somewhere to enable collision for placeable objects if (!model.empty() && (ref->mBase->mData.mFlags & ESM::Light::Carry) == 0) physics.addObject(ptr, model); if (!ref->mBase->mSound.empty() && !(ref->mBase->mData.mFlags & ESM::Light::OffDefault)) MWBase::Environment::get().getSoundManager()->playSound3D(ptr, ref->mBase->mSound, 1.0, 1.0, MWSound::Type::Sfx, MWSound::PlayMode::Loop); } bool Light::useAnim() const { return true; } std::string Light::getModel(const MWWorld::ConstPtr &ptr) const { const MWWorld::LiveCellRef *ref = ptr.get(); const std::string &model = ref->mBase->mModel; if (!model.empty()) { return "meshes\\" + model; } return ""; } std::string Light::getName (const MWWorld::ConstPtr& ptr) const { const MWWorld::LiveCellRef *ref = ptr.get(); if (ref->mBase->mModel.empty()) return std::string(); const std::string& name = ref->mBase->mName; return !name.empty() ? name : ref->mBase->mId; } std::shared_ptr Light::activate (const MWWorld::Ptr& ptr, const MWWorld::Ptr& actor) const { if(!MWBase::Environment::get().getWindowManager()->isAllowed(MWGui::GW_Inventory)) return std::shared_ptr(new MWWorld::NullAction()); MWWorld::LiveCellRef *ref = ptr.get(); if(!(ref->mBase->mData.mFlags&ESM::Light::Carry)) return std::shared_ptr(new MWWorld::FailedAction()); return defaultItemActivate(ptr, actor); } std::string Light::getScript (const MWWorld::ConstPtr& ptr) const { const MWWorld::LiveCellRef *ref = ptr.get(); return ref->mBase->mScript; } std::pair, bool> Light::getEquipmentSlots (const MWWorld::ConstPtr& ptr) const { const MWWorld::LiveCellRef *ref = ptr.get(); std::vector slots_; if (ref->mBase->mData.mFlags & ESM::Light::Carry) slots_.push_back (int (MWWorld::InventoryStore::Slot_CarriedLeft)); return std::make_pair (slots_, false); } int Light::getValue (const MWWorld::ConstPtr& ptr) const { const MWWorld::LiveCellRef *ref = ptr.get(); return ref->mBase->mData.mValue; } void Light::registerSelf() { std::shared_ptr instance (new Light); registerClass (typeid (ESM::Light).name(), instance); } std::string Light::getUpSoundId (const MWWorld::ConstPtr& ptr) const { return std::string("Item Misc Up"); } std::string Light::getDownSoundId (const MWWorld::ConstPtr& ptr) const { return std::string("Item Misc Down"); } std::string Light::getInventoryIcon (const MWWorld::ConstPtr& ptr) const { const MWWorld::LiveCellRef *ref = ptr.get(); return ref->mBase->mIcon; } bool Light::hasToolTip (const MWWorld::ConstPtr& ptr) const { return showsInInventory(ptr); } MWGui::ToolTipInfo Light::getToolTipInfo (const MWWorld::ConstPtr& ptr, int count) const { const MWWorld::LiveCellRef *ref = ptr.get(); MWGui::ToolTipInfo info; info.caption = MyGUI::TextIterator::toTagsString(getName(ptr)) + MWGui::ToolTips::getCountString(count); info.icon = ref->mBase->mIcon; std::string text; // Don't show duration for infinite light sources. if (Settings::Manager::getBool("show effect duration","Game") && ptr.getClass().getRemainingUsageTime(ptr) != -1) text += MWGui::ToolTips::getDurationString(ptr.getClass().getRemainingUsageTime(ptr), "\n#{sDuration}"); text += MWGui::ToolTips::getWeightString(ref->mBase->mData.mWeight, "#{sWeight}"); text += MWGui::ToolTips::getValueString(ref->mBase->mData.mValue, "#{sValue}"); if (MWBase::Environment::get().getWindowManager()->getFullHelp()) { text += MWGui::ToolTips::getCellRefString(ptr.getCellRef()); text += MWGui::ToolTips::getMiscString(ref->mBase->mScript, "Script"); } info.text = text; return info; } bool Light::showsInInventory (const MWWorld::ConstPtr& ptr) const { const ESM::Light* light = ptr.get()->mBase; if (!(light->mData.mFlags & ESM::Light::Carry)) return false; return Class::showsInInventory(ptr); } std::shared_ptr Light::use (const MWWorld::Ptr& ptr, bool force) const { std::shared_ptr action(new MWWorld::ActionEquip(ptr, force)); action->setSound(getUpSoundId(ptr)); return action; } void Light::setRemainingUsageTime (const MWWorld::Ptr& ptr, float duration) const { ptr.getCellRef().setChargeFloat(duration); } float Light::getRemainingUsageTime (const MWWorld::ConstPtr& ptr) const { const MWWorld::LiveCellRef *ref = ptr.get(); if (ptr.getCellRef().getCharge() == -1) return static_cast(ref->mBase->mData.mTime); else return ptr.getCellRef().getChargeFloat(); } MWWorld::Ptr Light::copyToCellImpl(const MWWorld::ConstPtr &ptr, MWWorld::CellStore &cell) const { const MWWorld::LiveCellRef *ref = ptr.get(); return MWWorld::Ptr(cell.insert(ref), &cell); } bool Light::canSell (const MWWorld::ConstPtr& item, int npcServices) const { return (npcServices & ESM::NPC::Lights) != 0; } float Light::getWeight(const MWWorld::ConstPtr &ptr) const { const MWWorld::LiveCellRef *ref = ptr.get(); return ref->mBase->mData.mWeight; } std::pair Light::canBeEquipped(const MWWorld::ConstPtr &ptr, const MWWorld::Ptr &npc) const { const MWWorld::LiveCellRef *ref = ptr.get(); if (!(ref->mBase->mData.mFlags & ESM::Light::Carry)) return std::make_pair(0,""); return std::make_pair(1,""); } std::string Light::getSound(const MWWorld::ConstPtr& ptr) const { return ptr.get()->mBase->mSound; } } ================================================ FILE: apps/openmw/mwclass/light.hpp ================================================ #ifndef GAME_MWCLASS_LIGHT_H #define GAME_MWCLASS_LIGHT_H #include "../mwworld/class.hpp" namespace MWClass { class Light : public MWWorld::Class { MWWorld::Ptr copyToCellImpl(const MWWorld::ConstPtr &ptr, MWWorld::CellStore &cell) const override; public: void insertObjectRendering (const MWWorld::Ptr& ptr, const std::string& model, MWRender::RenderingInterface& renderingInterface) const override; ///< Add reference into a cell for rendering void insertObject(const MWWorld::Ptr& ptr, const std::string& model, MWPhysics::PhysicsSystem& physics) const override; bool useAnim() const override; std::string getName (const MWWorld::ConstPtr& ptr) const override; ///< \return name or ID; can return an empty string. bool hasToolTip (const MWWorld::ConstPtr& ptr) const override; ///< @return true if this object has a tooltip when focused (default implementation: true) MWGui::ToolTipInfo getToolTipInfo (const MWWorld::ConstPtr& ptr, int count) const override; ///< @return the content of the tool tip to be displayed. raises exception if the object has no tooltip. bool showsInInventory (const MWWorld::ConstPtr& ptr) const override; std::shared_ptr activate (const MWWorld::Ptr& ptr, const MWWorld::Ptr& actor) const override; ///< Generate action for activation std::string getScript (const MWWorld::ConstPtr& ptr) const override; ///< Return name of the script attached to ptr std::pair, bool> getEquipmentSlots (const MWWorld::ConstPtr& ptr) const override; ///< \return first: Return IDs of the slot this object can be equipped in; second: can object /// stay stacked when equipped? int getValue (const MWWorld::ConstPtr& ptr) const override; ///< Return trade value of the object. Throws an exception, if the object can't be traded. static void registerSelf(); std::string getUpSoundId (const MWWorld::ConstPtr& ptr) const override; ///< Return the pick up sound Id std::string getDownSoundId (const MWWorld::ConstPtr& ptr) const override; ///< Return the put down sound Id std::string getInventoryIcon (const MWWorld::ConstPtr& ptr) const override; ///< Return name of inventory icon. std::shared_ptr use (const MWWorld::Ptr& ptr, bool force=false) const override; ///< Generate action for using via inventory menu void setRemainingUsageTime (const MWWorld::Ptr& ptr, float duration) const override; ///< Sets the remaining duration of the object. float getRemainingUsageTime (const MWWorld::ConstPtr& ptr) const override; ///< Returns the remaining duration of the object. std::string getModel(const MWWorld::ConstPtr &ptr) const override; float getWeight (const MWWorld::ConstPtr& ptr) const override; bool canSell (const MWWorld::ConstPtr& item, int npcServices) const override; std::pair canBeEquipped(const MWWorld::ConstPtr &ptr, const MWWorld::Ptr &npc) const override; std::string getSound(const MWWorld::ConstPtr& ptr) const override; }; } #endif ================================================ FILE: apps/openmw/mwclass/lockpick.cpp ================================================ #include "lockpick.hpp" #include #include "../mwbase/environment.hpp" #include "../mwbase/mechanicsmanager.hpp" #include "../mwbase/windowmanager.hpp" #include "../mwworld/ptr.hpp" #include "../mwworld/actionequip.hpp" #include "../mwworld/inventorystore.hpp" #include "../mwworld/cellstore.hpp" #include "../mwphysics/physicssystem.hpp" #include "../mwworld/nullaction.hpp" #include "../mwgui/tooltips.hpp" #include "../mwrender/objects.hpp" #include "../mwrender/renderinginterface.hpp" namespace MWClass { void Lockpick::insertObjectRendering (const MWWorld::Ptr& ptr, const std::string& model, MWRender::RenderingInterface& renderingInterface) const { if (!model.empty()) { renderingInterface.getObjects().insertModel(ptr, model); } } void Lockpick::insertObject(const MWWorld::Ptr& ptr, const std::string& model, MWPhysics::PhysicsSystem& physics) const { // TODO: add option somewhere to enable collision for placeable objects } std::string Lockpick::getModel(const MWWorld::ConstPtr &ptr) const { const MWWorld::LiveCellRef *ref = ptr.get(); const std::string &model = ref->mBase->mModel; if (!model.empty()) { return "meshes\\" + model; } return ""; } std::string Lockpick::getName (const MWWorld::ConstPtr& ptr) const { const MWWorld::LiveCellRef *ref = ptr.get(); const std::string& name = ref->mBase->mName; return !name.empty() ? name : ref->mBase->mId; } std::shared_ptr Lockpick::activate (const MWWorld::Ptr& ptr, const MWWorld::Ptr& actor) const { return defaultItemActivate(ptr, actor); } std::string Lockpick::getScript (const MWWorld::ConstPtr& ptr) const { const MWWorld::LiveCellRef *ref = ptr.get(); return ref->mBase->mScript; } std::pair, bool> Lockpick::getEquipmentSlots (const MWWorld::ConstPtr& ptr) const { std::vector slots_; slots_.push_back (int (MWWorld::InventoryStore::Slot_CarriedRight)); return std::make_pair (slots_, false); } int Lockpick::getValue (const MWWorld::ConstPtr& ptr) const { const MWWorld::LiveCellRef *ref = ptr.get(); return ref->mBase->mData.mValue; } void Lockpick::registerSelf() { std::shared_ptr instance (new Lockpick); registerClass (typeid (ESM::Lockpick).name(), instance); } std::string Lockpick::getUpSoundId (const MWWorld::ConstPtr& ptr) const { return std::string("Item Lockpick Up"); } std::string Lockpick::getDownSoundId (const MWWorld::ConstPtr& ptr) const { return std::string("Item Lockpick Down"); } std::string Lockpick::getInventoryIcon (const MWWorld::ConstPtr& ptr) const { const MWWorld::LiveCellRef *ref = ptr.get(); return ref->mBase->mIcon; } MWGui::ToolTipInfo Lockpick::getToolTipInfo (const MWWorld::ConstPtr& ptr, int count) const { const MWWorld::LiveCellRef *ref = ptr.get(); MWGui::ToolTipInfo info; info.caption = MyGUI::TextIterator::toTagsString(getName(ptr)) + MWGui::ToolTips::getCountString(count); info.icon = ref->mBase->mIcon; std::string text; int remainingUses = getItemHealth(ptr); text += "\n#{sUses}: " + MWGui::ToolTips::toString(remainingUses); text += "\n#{sQuality}: " + MWGui::ToolTips::toString(ref->mBase->mData.mQuality); text += MWGui::ToolTips::getWeightString(ref->mBase->mData.mWeight, "#{sWeight}"); text += MWGui::ToolTips::getValueString(ref->mBase->mData.mValue, "#{sValue}"); if (MWBase::Environment::get().getWindowManager()->getFullHelp()) { text += MWGui::ToolTips::getCellRefString(ptr.getCellRef()); text += MWGui::ToolTips::getMiscString(ref->mBase->mScript, "Script"); } info.text = text; return info; } std::shared_ptr Lockpick::use (const MWWorld::Ptr& ptr, bool force) const { std::shared_ptr action(new MWWorld::ActionEquip(ptr, force)); action->setSound(getUpSoundId(ptr)); return action; } MWWorld::Ptr Lockpick::copyToCellImpl(const MWWorld::ConstPtr &ptr, MWWorld::CellStore &cell) const { const MWWorld::LiveCellRef *ref = ptr.get(); return MWWorld::Ptr(cell.insert(ref), &cell); } std::pair Lockpick::canBeEquipped(const MWWorld::ConstPtr &ptr, const MWWorld::Ptr &npc) const { // Do not allow equip tools from inventory during attack if (MWBase::Environment::get().getMechanicsManager()->isAttackingOrSpell(npc) && MWBase::Environment::get().getWindowManager()->isGuiMode()) return std::make_pair(0, "#{sCantEquipWeapWarning}"); return std::make_pair(1, ""); } bool Lockpick::canSell (const MWWorld::ConstPtr& item, int npcServices) const { return (npcServices & ESM::NPC::Picks) != 0; } int Lockpick::getItemMaxHealth (const MWWorld::ConstPtr& ptr) const { const MWWorld::LiveCellRef *ref = ptr.get(); return ref->mBase->mData.mUses; } float Lockpick::getWeight(const MWWorld::ConstPtr &ptr) const { const MWWorld::LiveCellRef *ref = ptr.get(); return ref->mBase->mData.mWeight; } } ================================================ FILE: apps/openmw/mwclass/lockpick.hpp ================================================ #ifndef GAME_MWCLASS_LOCKPICK_H #define GAME_MWCLASS_LOCKPICK_H #include "../mwworld/class.hpp" namespace MWClass { class Lockpick : public MWWorld::Class { MWWorld::Ptr copyToCellImpl(const MWWorld::ConstPtr &ptr, MWWorld::CellStore &cell) const override; public: void insertObjectRendering (const MWWorld::Ptr& ptr, const std::string& model, MWRender::RenderingInterface& renderingInterface) const override; ///< Add reference into a cell for rendering void insertObject(const MWWorld::Ptr& ptr, const std::string& model, MWPhysics::PhysicsSystem& physics) const override; std::string getName (const MWWorld::ConstPtr& ptr) const override; ///< \return name or ID; can return an empty string. std::shared_ptr activate (const MWWorld::Ptr& ptr, const MWWorld::Ptr& actor) const override; ///< Generate action for activation MWGui::ToolTipInfo getToolTipInfo (const MWWorld::ConstPtr& ptr, int count) const override; ///< @return the content of the tool tip to be displayed. raises exception if the object has no tooltip. std::string getScript (const MWWorld::ConstPtr& ptr) const override; ///< Return name of the script attached to ptr std::pair, bool> getEquipmentSlots (const MWWorld::ConstPtr& ptr) const override; ///< \return first: Return IDs of the slot this object can be equipped in; second: can object /// stay stacked when equipped? int getValue (const MWWorld::ConstPtr& ptr) const override; ///< Return trade value of the object. Throws an exception, if the object can't be traded. static void registerSelf(); std::string getUpSoundId (const MWWorld::ConstPtr& ptr) const override; ///< Return the pick up sound Id std::string getDownSoundId (const MWWorld::ConstPtr& ptr) const override; ///< Return the put down sound Id std::string getInventoryIcon (const MWWorld::ConstPtr& ptr) const override; ///< Return name of inventory icon. std::pair canBeEquipped(const MWWorld::ConstPtr &ptr, const MWWorld::Ptr &npc) const override; std::shared_ptr use (const MWWorld::Ptr& ptr, bool force=false) const override; ///< Generate action for using via inventory menu std::string getModel(const MWWorld::ConstPtr &ptr) const override; bool canSell (const MWWorld::ConstPtr& item, int npcServices) const override; float getWeight (const MWWorld::ConstPtr& ptr) const override; int getItemMaxHealth (const MWWorld::ConstPtr& ptr) const override; ///< Return item max health or throw an exception, if class does not have item health bool hasItemHealth (const MWWorld::ConstPtr& ptr) const override { return true; } ///< \return Item health data available? (default implementation: false) }; } #endif ================================================ FILE: apps/openmw/mwclass/misc.cpp ================================================ #include "misc.hpp" /* Start of tes3mp addition Include additional headers for multiplayer purposes */ #include #include "../mwmp/Main.hpp" #include "../mwmp/Networking.hpp" /* End of tes3mp addition */ #include #include #include "../mwbase/environment.hpp" #include "../mwbase/world.hpp" #include "../mwbase/windowmanager.hpp" #include "../mwworld/cellstore.hpp" #include "../mwworld/esmstore.hpp" #include "../mwphysics/physicssystem.hpp" #include "../mwworld/manualref.hpp" #include "../mwworld/nullaction.hpp" #include "../mwworld/actionsoulgem.hpp" #include "../mwgui/tooltips.hpp" #include "../mwrender/objects.hpp" #include "../mwrender/renderinginterface.hpp" namespace MWClass { bool Miscellaneous::isGold (const MWWorld::ConstPtr& ptr) const { return Misc::StringUtils::ciEqual(ptr.getCellRef().getRefId(), "gold_001") || Misc::StringUtils::ciEqual(ptr.getCellRef().getRefId(), "gold_005") || Misc::StringUtils::ciEqual(ptr.getCellRef().getRefId(), "gold_010") || Misc::StringUtils::ciEqual(ptr.getCellRef().getRefId(), "gold_025") || Misc::StringUtils::ciEqual(ptr.getCellRef().getRefId(), "gold_100"); } void Miscellaneous::insertObjectRendering (const MWWorld::Ptr& ptr, const std::string& model, MWRender::RenderingInterface& renderingInterface) const { if (!model.empty()) { renderingInterface.getObjects().insertModel(ptr, model); } } void Miscellaneous::insertObject(const MWWorld::Ptr& ptr, const std::string& model, MWPhysics::PhysicsSystem& physics) const { // TODO: add option somewhere to enable collision for placeable objects /* Start of tes3mp addition Make it possible to enable collision for this object class from a packet */ if (!model.empty()) { mwmp::BaseWorldstate *worldstate = mwmp::Main::get().getNetworking()->getWorldstate(); if (worldstate->hasPlacedObjectCollision || Utils::vectorContains(worldstate->enforcedCollisionRefIds, ptr.getCellRef().getRefId())) { if (worldstate->useActorCollisionForPlacedObjects) physics.addObject(ptr, model, MWPhysics::CollisionType_Actor); else physics.addObject(ptr, model, MWPhysics::CollisionType_World); } } /* End of tes3mp addition */ } std::string Miscellaneous::getModel(const MWWorld::ConstPtr &ptr) const { const MWWorld::LiveCellRef *ref = ptr.get(); const std::string &model = ref->mBase->mModel; if (!model.empty()) { return "meshes\\" + model; } return ""; } std::string Miscellaneous::getName (const MWWorld::ConstPtr& ptr) const { const MWWorld::LiveCellRef *ref = ptr.get(); const std::string& name = ref->mBase->mName; return !name.empty() ? name : ref->mBase->mId; } std::shared_ptr Miscellaneous::activate (const MWWorld::Ptr& ptr, const MWWorld::Ptr& actor) const { return defaultItemActivate(ptr, actor); } std::string Miscellaneous::getScript (const MWWorld::ConstPtr& ptr) const { const MWWorld::LiveCellRef *ref = ptr.get(); return ref->mBase->mScript; } int Miscellaneous::getValue (const MWWorld::ConstPtr& ptr) const { const MWWorld::LiveCellRef *ref = ptr.get(); int value = ref->mBase->mData.mValue; if (ptr.getCellRef().getGoldValue() > 1 && ptr.getRefData().getCount() == 1) value = ptr.getCellRef().getGoldValue(); if (ptr.getCellRef().getSoul() != "") { const ESM::Creature *creature = MWBase::Environment::get().getWorld()->getStore().get().search(ref->mRef.getSoul()); if (creature) { int soul = creature->mData.mSoul; if (Settings::Manager::getBool("rebalance soul gem values", "Game")) { // use the 'soul gem value rebalance' formula from the Morrowind Code Patch float soulValue = 0.0001 * pow(soul, 3) + 2 * soul; // for Azura's star add the unfilled value if (Misc::StringUtils::ciEqual(ptr.getCellRef().getRefId(), "Misc_SoulGem_Azura")) value += soulValue; else value = soulValue; } else value *= soul; } } return value; } void Miscellaneous::registerSelf() { std::shared_ptr instance (new Miscellaneous); registerClass (typeid (ESM::Miscellaneous).name(), instance); } std::string Miscellaneous::getUpSoundId (const MWWorld::ConstPtr& ptr) const { if (isGold(ptr)) return std::string("Item Gold Up"); return std::string("Item Misc Up"); } std::string Miscellaneous::getDownSoundId (const MWWorld::ConstPtr& ptr) const { if (isGold(ptr)) return std::string("Item Gold Down"); return std::string("Item Misc Down"); } std::string Miscellaneous::getInventoryIcon (const MWWorld::ConstPtr& ptr) const { const MWWorld::LiveCellRef *ref = ptr.get(); return ref->mBase->mIcon; } MWGui::ToolTipInfo Miscellaneous::getToolTipInfo (const MWWorld::ConstPtr& ptr, int count) const { const MWWorld::LiveCellRef *ref = ptr.get(); MWGui::ToolTipInfo info; bool gold = isGold(ptr); if (gold) count *= getValue(ptr); std::string countString; if (!gold) countString = MWGui::ToolTips::getCountString(count); else // gold displays its count also if it's 1. countString = " (" + std::to_string(count) + ")"; info.caption = MyGUI::TextIterator::toTagsString(getName(ptr)) + countString + MWGui::ToolTips::getSoulString(ptr.getCellRef()); info.icon = ref->mBase->mIcon; std::string text; text += MWGui::ToolTips::getWeightString(ref->mBase->mData.mWeight, "#{sWeight}"); if (!gold && !ref->mBase->mData.mIsKey) text += MWGui::ToolTips::getValueString(getValue(ptr), "#{sValue}"); if (MWBase::Environment::get().getWindowManager()->getFullHelp()) { text += MWGui::ToolTips::getCellRefString(ptr.getCellRef()); text += MWGui::ToolTips::getMiscString(ref->mBase->mScript, "Script"); } info.text = text; return info; } MWWorld::Ptr Miscellaneous::copyToCell(const MWWorld::ConstPtr &ptr, MWWorld::CellStore &cell, int count) const { MWWorld::Ptr newPtr; const MWWorld::ESMStore &store = MWBase::Environment::get().getWorld()->getStore(); if (isGold(ptr)) { int goldAmount = getValue(ptr) * count; std::string base = "Gold_001"; if (goldAmount >= 100) base = "Gold_100"; else if (goldAmount >= 25) base = "Gold_025"; else if (goldAmount >= 10) base = "Gold_010"; else if (goldAmount >= 5) base = "Gold_005"; // Really, I have no idea why moving ref out of conditional // scope causes list::push_back throwing std::bad_alloc MWWorld::ManualRef newRef(store, base); const MWWorld::LiveCellRef *ref = newRef.getPtr().get(); newPtr = MWWorld::Ptr(cell.insert(ref), &cell); newPtr.getCellRef().setGoldValue(goldAmount); newPtr.getRefData().setCount(1); } else { const MWWorld::LiveCellRef *ref = ptr.get(); newPtr = MWWorld::Ptr(cell.insert(ref), &cell); newPtr.getRefData().setCount(count); } newPtr.getCellRef().unsetRefNum(); return newPtr; } std::shared_ptr Miscellaneous::use (const MWWorld::Ptr& ptr, bool force) const { if (ptr.getCellRef().getSoul().empty() || !MWBase::Environment::get().getWorld()->getStore().get().search(ptr.getCellRef().getSoul())) return std::shared_ptr(new MWWorld::NullAction()); else return std::shared_ptr(new MWWorld::ActionSoulgem(ptr)); } bool Miscellaneous::canSell (const MWWorld::ConstPtr& item, int npcServices) const { const MWWorld::LiveCellRef *ref = item.get(); return !ref->mBase->mData.mIsKey && (npcServices & ESM::NPC::Misc) && !isGold(item); } float Miscellaneous::getWeight(const MWWorld::ConstPtr &ptr) const { const MWWorld::LiveCellRef *ref = ptr.get(); return ref->mBase->mData.mWeight; } bool Miscellaneous::isKey(const MWWorld::ConstPtr &ptr) const { const MWWorld::LiveCellRef *ref = ptr.get(); return ref->mBase->mData.mIsKey != 0; } } ================================================ FILE: apps/openmw/mwclass/misc.hpp ================================================ #ifndef GAME_MWCLASS_MISC_H #define GAME_MWCLASS_MISC_H #include "../mwworld/class.hpp" namespace MWClass { class Miscellaneous : public MWWorld::Class { public: MWWorld::Ptr copyToCell(const MWWorld::ConstPtr &ptr, MWWorld::CellStore &cell, int count) const override; void insertObjectRendering (const MWWorld::Ptr& ptr, const std::string& model, MWRender::RenderingInterface& renderingInterface) const override; ///< Add reference into a cell for rendering void insertObject(const MWWorld::Ptr& ptr, const std::string& model, MWPhysics::PhysicsSystem& physics) const override; std::string getName (const MWWorld::ConstPtr& ptr) const override; ///< \return name or ID; can return an empty string. std::shared_ptr activate (const MWWorld::Ptr& ptr, const MWWorld::Ptr& actor) const override; ///< Generate action for activation MWGui::ToolTipInfo getToolTipInfo (const MWWorld::ConstPtr& ptr, int count) const override; ///< @return the content of the tool tip to be displayed. raises exception if the object has no tooltip. std::string getScript (const MWWorld::ConstPtr& ptr) const override; ///< Return name of the script attached to ptr int getValue (const MWWorld::ConstPtr& ptr) const override; ///< Return trade value of the object. Throws an exception, if the object can't be traded. static void registerSelf(); std::string getUpSoundId (const MWWorld::ConstPtr& ptr) const override; ///< Return the pick up sound Id std::string getDownSoundId (const MWWorld::ConstPtr& ptr) const override; ///< Return the put down sound Id std::string getInventoryIcon (const MWWorld::ConstPtr& ptr) const override; ///< Return name of inventory icon. std::string getModel(const MWWorld::ConstPtr &ptr) const override; std::shared_ptr use (const MWWorld::Ptr& ptr, bool force=false) const override; ///< Generate action for using via inventory menu float getWeight (const MWWorld::ConstPtr& ptr) const override; bool canSell (const MWWorld::ConstPtr& item, int npcServices) const override; bool isKey (const MWWorld::ConstPtr &ptr) const override; bool isGold (const MWWorld::ConstPtr& ptr) const override; }; } #endif ================================================ FILE: apps/openmw/mwclass/npc.cpp ================================================ #include "npc.hpp" #include #include #include #include #include #include #include #include /* Start of tes3mp addition Include additional headers for multiplayer purposes */ #include #include "../mwmp/Main.hpp" #include "../mwmp/Networking.hpp" #include "../mwmp/LocalPlayer.hpp" #include "../mwmp/PlayerList.hpp" #include "../mwmp/ObjectList.hpp" #include "../mwmp/CellController.hpp" #include "../mwmp/MechanicsHelper.hpp" /* End of tes3mp addition */ #include "../mwbase/environment.hpp" #include "../mwbase/world.hpp" #include "../mwbase/mechanicsmanager.hpp" #include "../mwbase/windowmanager.hpp" #include "../mwbase/dialoguemanager.hpp" #include "../mwbase/soundmanager.hpp" #include "../mwmechanics/creaturestats.hpp" #include "../mwmechanics/npcstats.hpp" #include "../mwmechanics/movement.hpp" #include "../mwmechanics/spellcasting.hpp" #include "../mwmechanics/disease.hpp" #include "../mwmechanics/combat.hpp" #include "../mwmechanics/autocalcspell.hpp" #include "../mwmechanics/difficultyscaling.hpp" #include "../mwmechanics/weapontype.hpp" #include "../mwmechanics/actorutil.hpp" #include "../mwworld/ptr.hpp" #include "../mwworld/actiontalk.hpp" #include "../mwworld/actionopen.hpp" #include "../mwworld/failedaction.hpp" #include "../mwworld/inventorystore.hpp" #include "../mwworld/customdata.hpp" #include "../mwworld/cellstore.hpp" #include "../mwworld/localscripts.hpp" #include "../mwrender/objects.hpp" #include "../mwrender/renderinginterface.hpp" #include "../mwrender/npcanimation.hpp" #include "../mwgui/tooltips.hpp" namespace { int is_even(double d) { double int_part; modf(d / 2.0, &int_part); return 2.0 * int_part == d; } int round_ieee_754(double d) { double i = floor(d); d -= i; if(d < 0.5) return static_cast(i); if(d > 0.5) return static_cast(i) + 1; if(is_even(i)) return static_cast(i); return static_cast(i) + 1; } void autoCalculateAttributes (const ESM::NPC* npc, MWMechanics::CreatureStats& creatureStats) { // race bonus const ESM::Race *race = MWBase::Environment::get().getWorld()->getStore().get().find(npc->mRace); bool male = (npc->mFlags & ESM::NPC::Female) == 0; int level = creatureStats.getLevel(); for (int i=0; imData.mAttributeValues[i]; creatureStats.setAttribute(i, male ? attribute.mMale : attribute.mFemale); } // class bonus const ESM::Class *class_ = MWBase::Environment::get().getWorld()->getStore().get().find(npc->mClass); for (int i=0; i<2; ++i) { int attribute = class_->mData.mAttribute[i]; if (attribute>=0 && attribute<8) { creatureStats.setAttribute(attribute, creatureStats.getAttribute(attribute).getBase() + 10); } } // skill bonus for (int attribute=0; attribute < ESM::Attribute::Length; ++attribute) { float modifierSum = 0; for (int j=0; jgetStore().get().find(j); if (skill->mData.mAttribute != attribute) continue; // is this a minor or major skill? float add=0.2f; for (int k=0; k<5; ++k) { if (class_->mData.mSkills[k][0] == j) add=0.5; } for (int k=0; k<5; ++k) { if (class_->mData.mSkills[k][1] == j) add=1.0; } modifierSum += add; } creatureStats.setAttribute(attribute, std::min( round_ieee_754(creatureStats.getAttribute(attribute).getBase() + (level-1) * modifierSum), 100) ); } // initial health float strength = creatureStats.getAttribute(ESM::Attribute::Strength).getBase(); float endurance = creatureStats.getAttribute(ESM::Attribute::Endurance).getBase(); int multiplier = 3; if (class_->mData.mSpecialization == ESM::Class::Combat) multiplier += 2; else if (class_->mData.mSpecialization == ESM::Class::Stealth) multiplier += 1; if (class_->mData.mAttribute[0] == ESM::Attribute::Endurance || class_->mData.mAttribute[1] == ESM::Attribute::Endurance) multiplier += 1; creatureStats.setHealth(floor(0.5f * (strength + endurance)) + multiplier * (creatureStats.getLevel() - 1)); } /** * @brief autoCalculateSkills * * Skills are calculated with following formulae ( http://www.uesp.net/wiki/Morrowind:NPCs#Skills ): * * Skills: (Level - 1) × (Majority Multiplier + Specialization Multiplier) * * The Majority Multiplier is 1.0 for a Major or Minor Skill, or 0.1 for a Miscellaneous Skill. * * The Specialization Multiplier is 0.5 for a Skill in the same Specialization as the class, * zero for other Skills. * * and by adding class, race, specialization bonus. */ void autoCalculateSkills(const ESM::NPC* npc, MWMechanics::NpcStats& npcStats, const MWWorld::Ptr& ptr, bool spellsInitialised) { const ESM::Class *class_ = MWBase::Environment::get().getWorld()->getStore().get().find(npc->mClass); unsigned int level = npcStats.getLevel(); const ESM::Race *race = MWBase::Environment::get().getWorld()->getStore().get().find(npc->mRace); for (int i = 0; i < 2; ++i) { int bonus = (i==0) ? 10 : 25; for (int i2 = 0; i2 < 5; ++i2) { int index = class_->mData.mSkills[i2][i]; if (index >= 0 && index < ESM::Skill::Length) { npcStats.getSkill(index).setBase (npcStats.getSkill(index).getBase() + bonus); } } } for (int skillIndex = 0; skillIndex < ESM::Skill::Length; ++skillIndex) { float majorMultiplier = 0.1f; float specMultiplier = 0.0f; int raceBonus = 0; int specBonus = 0; for (int raceSkillIndex = 0; raceSkillIndex < 7; ++raceSkillIndex) { if (race->mData.mBonus[raceSkillIndex].mSkill == skillIndex) { raceBonus = race->mData.mBonus[raceSkillIndex].mBonus; break; } } for (int k = 0; k < 5; ++k) { // is this a minor or major skill? if ((class_->mData.mSkills[k][0] == skillIndex) || (class_->mData.mSkills[k][1] == skillIndex)) { majorMultiplier = 1.0f; break; } } // is this skill in the same Specialization as the class? const ESM::Skill* skill = MWBase::Environment::get().getWorld()->getStore().get().find(skillIndex); if (skill->mData.mSpecialization == class_->mData.mSpecialization) { specMultiplier = 0.5f; specBonus = 5; } npcStats.getSkill(skillIndex).setBase( std::min( round_ieee_754( npcStats.getSkill(skillIndex).getBase() + 5 + raceBonus + specBonus +(int(level)-1) * (majorMultiplier + specMultiplier)), 100)); // Must gracefully handle level 0 } int skills[ESM::Skill::Length]; for (int i=0; i spells = MWMechanics::autoCalcNpcSpells(skills, attributes, race); npcStats.getSpells().addAllToInstance(spells); } } } namespace MWClass { class NpcCustomData : public MWWorld::TypedCustomData { public: MWMechanics::NpcStats mNpcStats; MWMechanics::Movement mMovement; MWWorld::InventoryStore mInventoryStore; NpcCustomData& asNpcCustomData() override { return *this; } const NpcCustomData& asNpcCustomData() const override { return *this; } }; const Npc::GMST& Npc::getGmst() { static GMST gmst; static bool inited = false; if(!inited) { const MWBase::World *world = MWBase::Environment::get().getWorld(); const MWWorld::Store &store = world->getStore().get(); gmst.fMinWalkSpeed = store.find("fMinWalkSpeed"); gmst.fMaxWalkSpeed = store.find("fMaxWalkSpeed"); gmst.fEncumberedMoveEffect = store.find("fEncumberedMoveEffect"); gmst.fSneakSpeedMultiplier = store.find("fSneakSpeedMultiplier"); gmst.fAthleticsRunBonus = store.find("fAthleticsRunBonus"); gmst.fBaseRunMultiplier = store.find("fBaseRunMultiplier"); gmst.fMinFlySpeed = store.find("fMinFlySpeed"); gmst.fMaxFlySpeed = store.find("fMaxFlySpeed"); gmst.fSwimRunBase = store.find("fSwimRunBase"); gmst.fSwimRunAthleticsMult = store.find("fSwimRunAthleticsMult"); gmst.fJumpEncumbranceBase = store.find("fJumpEncumbranceBase"); gmst.fJumpEncumbranceMultiplier = store.find("fJumpEncumbranceMultiplier"); gmst.fJumpAcrobaticsBase = store.find("fJumpAcrobaticsBase"); gmst.fJumpAcroMultiplier = store.find("fJumpAcroMultiplier"); gmst.fJumpRunMultiplier = store.find("fJumpRunMultiplier"); gmst.fWereWolfRunMult = store.find("fWereWolfRunMult"); gmst.fKnockDownMult = store.find("fKnockDownMult"); gmst.iKnockDownOddsMult = store.find("iKnockDownOddsMult"); gmst.iKnockDownOddsBase = store.find("iKnockDownOddsBase"); gmst.fCombatArmorMinMult = store.find("fCombatArmorMinMult"); inited = true; } return gmst; } void Npc::ensureCustomData (const MWWorld::Ptr& ptr) const { if (!ptr.getRefData().getCustomData()) { std::unique_ptr data(new NpcCustomData); MWWorld::LiveCellRef *ref = ptr.get(); bool spellsInitialised = data->mNpcStats.getSpells().setSpells(ref->mBase->mId); // creature stats int gold=0; if(ref->mBase->mNpdtType != ESM::NPC::NPC_WITH_AUTOCALCULATED_STATS) { gold = ref->mBase->mNpdt.mGold; for (unsigned int i=0; i< ESM::Skill::Length; ++i) data->mNpcStats.getSkill (i).setBase (ref->mBase->mNpdt.mSkills[i]); data->mNpcStats.setAttribute(ESM::Attribute::Strength, ref->mBase->mNpdt.mStrength); data->mNpcStats.setAttribute(ESM::Attribute::Intelligence, ref->mBase->mNpdt.mIntelligence); data->mNpcStats.setAttribute(ESM::Attribute::Willpower, ref->mBase->mNpdt.mWillpower); data->mNpcStats.setAttribute(ESM::Attribute::Agility, ref->mBase->mNpdt.mAgility); data->mNpcStats.setAttribute(ESM::Attribute::Speed, ref->mBase->mNpdt.mSpeed); data->mNpcStats.setAttribute(ESM::Attribute::Endurance, ref->mBase->mNpdt.mEndurance); data->mNpcStats.setAttribute(ESM::Attribute::Personality, ref->mBase->mNpdt.mPersonality); data->mNpcStats.setAttribute(ESM::Attribute::Luck, ref->mBase->mNpdt.mLuck); data->mNpcStats.setHealth (ref->mBase->mNpdt.mHealth); data->mNpcStats.setMagicka (ref->mBase->mNpdt.mMana); data->mNpcStats.setFatigue (ref->mBase->mNpdt.mFatigue); data->mNpcStats.setLevel(ref->mBase->mNpdt.mLevel); data->mNpcStats.setBaseDisposition(ref->mBase->mNpdt.mDisposition); data->mNpcStats.setReputation(ref->mBase->mNpdt.mReputation); data->mNpcStats.setNeedRecalcDynamicStats(false); } else { gold = ref->mBase->mNpdt.mGold; for (int i=0; i<3; ++i) data->mNpcStats.setDynamic (i, 10); data->mNpcStats.setLevel(ref->mBase->mNpdt.mLevel); data->mNpcStats.setBaseDisposition(ref->mBase->mNpdt.mDisposition); data->mNpcStats.setReputation(ref->mBase->mNpdt.mReputation); autoCalculateAttributes(ref->mBase, data->mNpcStats); autoCalculateSkills(ref->mBase, data->mNpcStats, ptr, spellsInitialised); data->mNpcStats.setNeedRecalcDynamicStats(true); } // Persistent actors with 0 health do not play death animation if (data->mNpcStats.isDead()) data->mNpcStats.setDeathAnimationFinished(isPersistent(ptr)); // race powers const ESM::Race *race = MWBase::Environment::get().getWorld()->getStore().get().find(ref->mBase->mRace); data->mNpcStats.getSpells().addAllToInstance(race->mPowers.mList); if (!ref->mBase->mFaction.empty()) { static const int iAutoRepFacMod = MWBase::Environment::get().getWorld()->getStore().get() .find("iAutoRepFacMod")->mValue.getInteger(); static const int iAutoRepLevMod = MWBase::Environment::get().getWorld()->getStore().get() .find("iAutoRepLevMod")->mValue.getInteger(); int rank = ref->mBase->getFactionRank(); data->mNpcStats.setReputation(iAutoRepFacMod * (rank+1) + iAutoRepLevMod * (data->mNpcStats.getLevel()-1)); } data->mNpcStats.getAiSequence().fill(ref->mBase->mAiPackage); data->mNpcStats.setAiSetting (MWMechanics::CreatureStats::AI_Hello, ref->mBase->mAiData.mHello); data->mNpcStats.setAiSetting (MWMechanics::CreatureStats::AI_Fight, ref->mBase->mAiData.mFight); data->mNpcStats.setAiSetting (MWMechanics::CreatureStats::AI_Flee, ref->mBase->mAiData.mFlee); data->mNpcStats.setAiSetting (MWMechanics::CreatureStats::AI_Alarm, ref->mBase->mAiData.mAlarm); // spells if (!spellsInitialised) data->mNpcStats.getSpells().addAllToInstance(ref->mBase->mSpells.mList); // inventory // setting ownership is used to make the NPC auto-equip his initial equipment only, and not bartered items data->mInventoryStore.fill(ref->mBase->mInventory, ptr.getCellRef().getRefId()); data->mNpcStats.setGoldPool(gold); // store ptr.getRefData().setCustomData(std::move(data)); getInventoryStore(ptr).autoEquip(ptr); } } void Npc::insertObjectRendering (const MWWorld::Ptr& ptr, const std::string& model, MWRender::RenderingInterface& renderingInterface) const { renderingInterface.getObjects().insertNPC(ptr); } bool Npc::isPersistent(const MWWorld::ConstPtr &actor) const { const MWWorld::LiveCellRef* ref = actor.get(); return ref->mBase->mPersistent; } std::string Npc::getModel(const MWWorld::ConstPtr &ptr) const { const MWWorld::LiveCellRef *ref = ptr.get(); std::string model = Settings::Manager::getString("baseanim", "Models"); const ESM::Race* race = MWBase::Environment::get().getWorld()->getStore().get().find(ref->mBase->mRace); if(race->mData.mFlags & ESM::Race::Beast) model = Settings::Manager::getString("baseanimkna", "Models"); return model; } void Npc::getModelsToPreload(const MWWorld::Ptr &ptr, std::vector &models) const { const MWWorld::LiveCellRef *npc = ptr.get(); const ESM::Race* race = MWBase::Environment::get().getWorld()->getStore().get().search(npc->mBase->mRace); if(race && race->mData.mFlags & ESM::Race::Beast) models.emplace_back(Settings::Manager::getString("baseanimkna", "Models")); // keep these always loaded just in case models.emplace_back(Settings::Manager::getString("xargonianswimkna", "Models")); models.emplace_back(Settings::Manager::getString("xbaseanimfemale", "Models")); models.emplace_back(Settings::Manager::getString("xbaseanim", "Models")); if (!npc->mBase->mModel.empty()) models.push_back("meshes/"+npc->mBase->mModel); if (!npc->mBase->mHead.empty()) { const ESM::BodyPart* head = MWBase::Environment::get().getWorld()->getStore().get().search(npc->mBase->mHead); if (head) models.push_back("meshes/"+head->mModel); } if (!npc->mBase->mHair.empty()) { const ESM::BodyPart* hair = MWBase::Environment::get().getWorld()->getStore().get().search(npc->mBase->mHair); if (hair) models.push_back("meshes/"+hair->mModel); } bool female = (npc->mBase->mFlags & ESM::NPC::Female); // FIXME: use const version of InventoryStore functions once they are available // preload equipped items const MWWorld::InventoryStore& invStore = getInventoryStore(ptr); for (int slot = 0; slot < MWWorld::InventoryStore::Slots; ++slot) { MWWorld::ConstContainerStoreIterator equipped = invStore.getSlot(slot); if (equipped != invStore.end()) { std::vector parts; if(equipped->getTypeName() == typeid(ESM::Clothing).name()) { const ESM::Clothing *clothes = equipped->get()->mBase; parts = clothes->mParts.mParts; } else if(equipped->getTypeName() == typeid(ESM::Armor).name()) { const ESM::Armor *armor = equipped->get()->mBase; parts = armor->mParts.mParts; } else { std::string model = equipped->getClass().getModel(*equipped); if (!model.empty()) models.push_back(model); } for (std::vector::const_iterator it = parts.begin(); it != parts.end(); ++it) { std::string partname = female ? it->mFemale : it->mMale; if (partname.empty()) partname = female ? it->mMale : it->mFemale; const ESM::BodyPart* part = MWBase::Environment::get().getWorld()->getStore().get().search(partname); if (part && !part->mModel.empty()) models.push_back("meshes/"+part->mModel); } } } // preload body parts if (race) { const std::vector& parts = MWRender::NpcAnimation::getBodyParts(Misc::StringUtils::lowerCase(race->mId), female, false, false); for (std::vector::const_iterator it = parts.begin(); it != parts.end(); ++it) { const ESM::BodyPart* part = *it; if (part && !part->mModel.empty()) models.push_back("meshes/"+part->mModel); } } } std::string Npc::getName (const MWWorld::ConstPtr& ptr) const { if(ptr.getRefData().getCustomData() && ptr.getRefData().getCustomData()->asNpcCustomData().mNpcStats.isWerewolf()) { const MWBase::World *world = MWBase::Environment::get().getWorld(); const MWWorld::Store &store = world->getStore().get(); return store.find("sWerewolfPopup")->mValue.getString(); } const MWWorld::LiveCellRef *ref = ptr.get(); const std::string& name = ref->mBase->mName; return !name.empty() ? name : ref->mBase->mId; } MWMechanics::CreatureStats& Npc::getCreatureStats (const MWWorld::Ptr& ptr) const { ensureCustomData (ptr); return ptr.getRefData().getCustomData()->asNpcCustomData().mNpcStats; } MWMechanics::NpcStats& Npc::getNpcStats (const MWWorld::Ptr& ptr) const { ensureCustomData (ptr); return ptr.getRefData().getCustomData()->asNpcCustomData().mNpcStats; } void Npc::hit(const MWWorld::Ptr& ptr, float attackStrength, int type) const { /* Start of tes3mp addition Ignore hit calculations on this client from DedicatedPlayers and DedicatedActors */ if (mwmp::PlayerList::isDedicatedPlayer(ptr) || mwmp::Main::get().getCellController()->isDedicatedActor(ptr)) { return; } /* End of tes3mp addition */ MWBase::World *world = MWBase::Environment::get().getWorld(); const MWWorld::Store &store = world->getStore().get(); // Get the weapon used (if hand-to-hand, weapon = inv.end()) MWWorld::InventoryStore &inv = getInventoryStore(ptr); MWWorld::ContainerStoreIterator weaponslot = inv.getSlot(MWWorld::InventoryStore::Slot_CarriedRight); MWWorld::Ptr weapon = ((weaponslot != inv.end()) ? *weaponslot : MWWorld::Ptr()); if(!weapon.isEmpty() && weapon.getTypeName() != typeid(ESM::Weapon).name()) weapon = MWWorld::Ptr(); MWMechanics::applyFatigueLoss(ptr, weapon, attackStrength); const float fCombatDistance = store.find("fCombatDistance")->mValue.getFloat(); float dist = fCombatDistance * (!weapon.isEmpty() ? weapon.get()->mBase->mData.mReach : store.find("fHandToHandReach")->mValue.getFloat()); // For AI actors, get combat targets to use in the ray cast. Only those targets will return a positive hit result. std::vector targetActors; if (ptr != MWMechanics::getPlayer()) getCreatureStats(ptr).getAiSequence().getCombatTargets(targetActors); // TODO: Use second to work out the hit angle std::pair result = world->getHitContact(ptr, dist, targetActors); MWWorld::Ptr victim = result.first; osg::Vec3f hitPosition (result.second); if(victim.isEmpty()) // Didn't hit anything return; const MWWorld::Class &othercls = victim.getClass(); /* Start of tes3mp change (major) Send an ID_OBJECT_HIT packet when hitting non-actors instead of just returning */ if(!othercls.isActor()) { mwmp::ObjectList *objectList = mwmp::Main::get().getNetworking()->getObjectList(); objectList->reset(); objectList->packetOrigin = mwmp::CLIENT_GAMEPLAY; objectList->addObjectHit(victim, ptr); objectList->sendObjectHit(); return; } /* End of tes3mp change (major) */ MWMechanics::CreatureStats &otherstats = othercls.getCreatureStats(victim); if(otherstats.isDead()) // Can't hit dead actors return; if(ptr == MWMechanics::getPlayer()) MWBase::Environment::get().getWindowManager()->setEnemy(victim); int weapskill = ESM::Skill::HandToHand; if(!weapon.isEmpty()) weapskill = weapon.getClass().getEquipmentSkill(weapon); float hitchance = MWMechanics::getHitChance(ptr, victim, getSkill(ptr, weapskill)); /* Start of tes3mp addition If the attacker is a LocalPlayer or LocalActor, get their Attack to assign its hit position and target */ mwmp::Attack *localAttack = MechanicsHelper::getLocalAttack(ptr); if (localAttack) { localAttack->isHit = true; localAttack->success = true; localAttack->hitPosition = MechanicsHelper::getPositionFromVector(hitPosition); MechanicsHelper::assignAttackTarget(localAttack, victim); } /* End of tes3mp addition */ if (Misc::Rng::roll0to99() >= hitchance) { /* Start of tes3mp addition If this was a failed attack by the LocalPlayer or LocalActor, send a packet about it Send an ID_OBJECT_HIT about it as well */ if (localAttack) { localAttack->pressed = false; localAttack->success = false; localAttack->shouldSend = true; mwmp::ObjectList *objectList = mwmp::Main::get().getNetworking()->getObjectList(); objectList->reset(); objectList->packetOrigin = mwmp::CLIENT_GAMEPLAY; objectList->addObjectHit(victim, ptr, *localAttack); objectList->sendObjectHit(); } /* End of tes3mp addition */ othercls.onHit(victim, 0.0f, false, weapon, ptr, osg::Vec3f(), false); MWMechanics::reduceWeaponCondition(0.f, false, weapon, ptr); return; } bool healthdmg; float damage = 0.0f; if(!weapon.isEmpty()) { const unsigned char *attack = nullptr; if(type == ESM::Weapon::AT_Chop) attack = weapon.get()->mBase->mData.mChop; else if(type == ESM::Weapon::AT_Slash) attack = weapon.get()->mBase->mData.mSlash; else if(type == ESM::Weapon::AT_Thrust) attack = weapon.get()->mBase->mData.mThrust; if(attack) { damage = attack[0] + ((attack[1]-attack[0])*attackStrength); } MWMechanics::adjustWeaponDamage(damage, weapon, ptr); MWMechanics::resistNormalWeapon(victim, ptr, weapon, damage); MWMechanics::applyWerewolfDamageMult(victim, weapon, damage); MWMechanics::reduceWeaponCondition(damage, true, weapon, ptr); healthdmg = true; } else { MWMechanics::getHandToHandDamage(ptr, victim, damage, healthdmg, attackStrength); } if(ptr == MWMechanics::getPlayer()) { skillUsageSucceeded(ptr, weapskill, 0); const MWMechanics::AiSequence& seq = victim.getClass().getCreatureStats(victim).getAiSequence(); bool unaware = !seq.isInCombat() && !MWBase::Environment::get().getMechanicsManager()->awarenessCheck(ptr, victim); if(unaware) { damage *= store.find("fCombatCriticalStrikeMult")->mValue.getFloat(); MWBase::Environment::get().getWindowManager()->messageBox("#{sTargetCriticalStrike}"); MWBase::Environment::get().getSoundManager()->playSound3D(victim, "critical damage", 1.0f, 1.0f); } } if (othercls.getCreatureStats(victim).getKnockedDown()) damage *= store.find("fCombatKODamageMult")->mValue.getFloat(); // Apply "On hit" enchanted weapons /* Start of tes3mp change (minor) Track whether the strike enchantment is successful for attacks by the LocalPlayer or LocalActors */ bool appliedEnchantment = MWMechanics::applyOnStrikeEnchantment(ptr, victim, weapon, hitPosition); if (localAttack) localAttack->applyWeaponEnchantment = appliedEnchantment; /* End of tes3mp change (minor) */ MWMechanics::applyElementalShields(ptr, victim); if (MWMechanics::blockMeleeAttack(ptr, victim, weapon, damage, attackStrength)) damage = 0; if (victim == MWMechanics::getPlayer() && MWBase::Environment::get().getWorld()->getGodModeState()) damage = 0; MWMechanics::diseaseContact(victim, ptr); othercls.onHit(victim, damage, healthdmg, weapon, ptr, hitPosition, true); } void Npc::onHit(const MWWorld::Ptr &ptr, float damage, bool ishealth, const MWWorld::Ptr &object, const MWWorld::Ptr &attacker, const osg::Vec3f &hitPosition, bool successful) const { MWBase::SoundManager *sndMgr = MWBase::Environment::get().getSoundManager(); MWMechanics::CreatureStats& stats = getCreatureStats(ptr); bool wasDead = stats.isDead(); // Note OnPcHitMe is not set for friendly hits. bool setOnPcHitMe = true; // NOTE: 'object' and/or 'attacker' may be empty. if (!attacker.isEmpty() && attacker.getClass().isActor() && !stats.getAiSequence().isInCombat(attacker)) { stats.setAttacked(true); setOnPcHitMe = MWBase::Environment::get().getMechanicsManager()->actorAttacked(ptr, attacker); } // Attacker and target store each other as hitattemptactor if they have no one stored yet if (!attacker.isEmpty() && attacker.getClass().isActor()) { MWMechanics::CreatureStats& statsAttacker = attacker.getClass().getCreatureStats(attacker); /* Start of tes3mp change (minor) Instead of only checking whether an attacker is the LocalPlayer, also check if they are a DedicatedPlayer Additionally, if the two players are on each other's team, don't track their hits */ // First handle the attacked actor if ((stats.getHitAttemptActorId() == -1) && (statsAttacker.getAiSequence().isInCombat(ptr) || attacker == MWMechanics::getPlayer() || mwmp::PlayerList::isDedicatedPlayer(attacker)) && !MechanicsHelper::isTeamMember(attacker, ptr)) stats.setHitAttemptActorId(statsAttacker.getActorId()); // Next handle the attacking actor if ((statsAttacker.getHitAttemptActorId() == -1) && (statsAttacker.getAiSequence().isInCombat(ptr) || attacker == MWMechanics::getPlayer() || mwmp::PlayerList::isDedicatedPlayer(attacker)) && !MechanicsHelper::isTeamMember(ptr, attacker)) statsAttacker.setHitAttemptActorId(stats.getActorId()); /* End of tes3mp change (minor) */ } if (!object.isEmpty()) stats.setLastHitAttemptObject(object.getCellRef().getRefId()); if (setOnPcHitMe && !attacker.isEmpty() && attacker == MWMechanics::getPlayer()) { const std::string &script = getScript(ptr); /* Set the OnPCHitMe script variable. The script is responsible for clearing it. */ if(!script.empty()) ptr.getRefData().getLocals().setVarByInt(script, "onpchitme", 1); } if (!successful) { // Missed if (!attacker.isEmpty() && attacker == MWMechanics::getPlayer()) sndMgr->playSound3D(ptr, "miss", 1.0f, 1.0f); return; } if (!object.isEmpty()) stats.setLastHitObject(object.getCellRef().getRefId()); if (damage > 0.0f && !object.isEmpty()) MWMechanics::resistNormalWeapon(ptr, attacker, object, damage); if (damage < 0.001f) damage = 0; bool godmode = ptr == MWMechanics::getPlayer() && MWBase::Environment::get().getWorld()->getGodModeState(); if (godmode) damage = 0; if (damage > 0.0f && !attacker.isEmpty()) { // 'ptr' is losing health. Play a 'hit' voiced dialog entry if not already saying // something, alert the character controller, scripts, etc. const MWWorld::ESMStore &store = MWBase::Environment::get().getWorld()->getStore(); const GMST& gmst = getGmst(); int chance = store.get().find("iVoiceHitOdds")->mValue.getInteger(); if (Misc::Rng::roll0to99() < chance) MWBase::Environment::get().getDialogueManager()->say(ptr, "hit"); // Check for knockdown float agilityTerm = stats.getAttribute(ESM::Attribute::Agility).getModified() * gmst.fKnockDownMult->mValue.getFloat(); float knockdownTerm = stats.getAttribute(ESM::Attribute::Agility).getModified() * gmst.iKnockDownOddsMult->mValue.getInteger() * 0.01f + gmst.iKnockDownOddsBase->mValue.getInteger(); /* Start of tes3mp change (major) If the attacker is a DedicatedPlayer or DedicatedActor with a successful knockdown, apply the knockdown If the attacker is neither of those, then it must be a LocalPlayer or a LocalActor, so calculate the knockdown probability on our client Default to hit recovery if no knockdown has taken place, like in regular OpenMW */ mwmp::Attack *dedicatedAttack = MechanicsHelper::getDedicatedAttack(attacker); if (dedicatedAttack) { if (dedicatedAttack->knockdown) stats.setKnockedDown(true); } else { if (ishealth && agilityTerm <= damage && knockdownTerm <= Misc::Rng::roll0to99()) stats.setKnockedDown(true); } if (!stats.getKnockedDown()) stats.setHitRecovery(true); // Is this supposed to always occur? /* End of tes3mp change (major) */ if (damage > 0 && ishealth) { // Hit percentages: // cuirass = 30% // shield, helmet, greaves, boots, pauldrons = 10% each // guantlets = 5% each static const int hitslots[20] = { MWWorld::InventoryStore::Slot_Cuirass, MWWorld::InventoryStore::Slot_Cuirass, MWWorld::InventoryStore::Slot_Cuirass, MWWorld::InventoryStore::Slot_Cuirass, MWWorld::InventoryStore::Slot_Cuirass, MWWorld::InventoryStore::Slot_Cuirass, MWWorld::InventoryStore::Slot_CarriedLeft, MWWorld::InventoryStore::Slot_CarriedLeft, MWWorld::InventoryStore::Slot_Helmet, MWWorld::InventoryStore::Slot_Helmet, MWWorld::InventoryStore::Slot_Greaves, MWWorld::InventoryStore::Slot_Greaves, MWWorld::InventoryStore::Slot_Boots, MWWorld::InventoryStore::Slot_Boots, MWWorld::InventoryStore::Slot_LeftPauldron, MWWorld::InventoryStore::Slot_LeftPauldron, MWWorld::InventoryStore::Slot_RightPauldron, MWWorld::InventoryStore::Slot_RightPauldron, MWWorld::InventoryStore::Slot_LeftGauntlet, MWWorld::InventoryStore::Slot_RightGauntlet }; int hitslot = hitslots[Misc::Rng::rollDice(20)]; float unmitigatedDamage = damage; float x = damage / (damage + getArmorRating(ptr)); damage *= std::max(gmst.fCombatArmorMinMult->mValue.getFloat(), x); int damageDiff = static_cast(unmitigatedDamage - damage); damage = std::max(1.f, damage); damageDiff = std::max(1, damageDiff); MWWorld::InventoryStore &inv = getInventoryStore(ptr); MWWorld::ContainerStoreIterator armorslot = inv.getSlot(hitslot); MWWorld::Ptr armor = ((armorslot != inv.end()) ? *armorslot : MWWorld::Ptr()); bool hasArmor = !armor.isEmpty() && armor.getTypeName() == typeid(ESM::Armor).name(); // If there's no item in the carried left slot or if it is not a shield redistribute the hit. if (!hasArmor && hitslot == MWWorld::InventoryStore::Slot_CarriedLeft) { if (Misc::Rng::rollDice(2) == 0) hitslot = MWWorld::InventoryStore::Slot_Cuirass; else hitslot = MWWorld::InventoryStore::Slot_LeftPauldron; armorslot = inv.getSlot(hitslot); if (armorslot != inv.end()) { armor = *armorslot; hasArmor = !armor.isEmpty() && armor.getTypeName() == typeid(ESM::Armor).name(); } } if (hasArmor) { if (!object.isEmpty() || attacker.isEmpty() || attacker.getClass().isNpc()) // Unarmed creature attacks don't affect armor condition { int armorhealth = armor.getClass().getItemHealth(armor); armorhealth -= std::min(damageDiff, armorhealth); armor.getCellRef().setCharge(armorhealth); // Armor broken? unequip it if (armorhealth == 0) armor = *inv.unequipItem(armor, ptr); } if (ptr == MWMechanics::getPlayer()) skillUsageSucceeded(ptr, armor.getClass().getEquipmentSkill(armor), 0); switch(armor.getClass().getEquipmentSkill(armor)) { case ESM::Skill::LightArmor: sndMgr->playSound3D(ptr, "Light Armor Hit", 1.0f, 1.0f); break; case ESM::Skill::MediumArmor: sndMgr->playSound3D(ptr, "Medium Armor Hit", 1.0f, 1.0f); break; case ESM::Skill::HeavyArmor: sndMgr->playSound3D(ptr, "Heavy Armor Hit", 1.0f, 1.0f); break; } } else if(ptr == MWMechanics::getPlayer()) skillUsageSucceeded(ptr, ESM::Skill::Unarmored, 0); } } if (ishealth) { if (!attacker.isEmpty() && !godmode) damage = scaleDamage(damage, attacker, ptr); if (damage > 0.0f) { sndMgr->playSound3D(ptr, "Health Damage", 1.0f, 1.0f); if (ptr == MWMechanics::getPlayer()) MWBase::Environment::get().getWindowManager()->activateHitOverlay(); if (!attacker.isEmpty()) MWBase::Environment::get().getWorld()->spawnBloodEffect(ptr, hitPosition); } MWMechanics::DynamicStat health(getCreatureStats(ptr).getHealth()); health.setCurrent(health.getCurrent() - damage); stats.setHealth(health); } else { MWMechanics::DynamicStat fatigue(getCreatureStats(ptr).getFatigue()); fatigue.setCurrent(fatigue.getCurrent() - damage, true); stats.setFatigue(fatigue); } if (!wasDead && getCreatureStats(ptr).isDead()) { // NPC was killed if (!attacker.isEmpty() && attacker.getClass().isNpc() && attacker.getClass().getNpcStats(attacker).isWerewolf()) { attacker.getClass().getNpcStats(attacker).addWerewolfKill(); } MWBase::Environment::get().getMechanicsManager()->actorKilled(ptr, attacker); } /* Start of tes3mp addition If the attacker was the LocalPlayer or LocalActor, record their target and send an attack packet about it Send an ID_OBJECT_HIT about it as well If the victim was the LocalPlayer, check whether packets should be sent about their new dynamic stats and position If the victim was a LocalActor who died, record their attacker as the killer */ mwmp::Attack *localAttack = MechanicsHelper::getLocalAttack(attacker); if (localAttack) { localAttack->pressed = false; localAttack->damage = damage; localAttack->knockdown = getCreatureStats(ptr).getKnockedDown(); MechanicsHelper::assignAttackTarget(localAttack, ptr); localAttack->shouldSend = true; mwmp::ObjectList *objectList = mwmp::Main::get().getNetworking()->getObjectList(); objectList->reset(); objectList->packetOrigin = mwmp::CLIENT_GAMEPLAY; objectList->addObjectHit(ptr, attacker, *localAttack); objectList->sendObjectHit(); } if (ptr == MWMechanics::getPlayer()) { // Record the attacker as the LocalPlayer's death reason if (getCreatureStats(ptr).isDead()) { mwmp::Main::get().getLocalPlayer()->killer = MechanicsHelper::getTarget(attacker); } mwmp::Main::get().getLocalPlayer()->updateStatsDynamic(true); mwmp::Main::get().getLocalPlayer()->updatePosition(true); // fix position after getting damage; } else if (mwmp::Main::get().getCellController()->isLocalActor(ptr)) { if (getCreatureStats(ptr).isDead()) { mwmp::Main::get().getCellController()->getLocalActor(ptr)->killer = MechanicsHelper::getTarget(attacker); } } /* End of tes3mp addition */ } std::shared_ptr Npc::activate (const MWWorld::Ptr& ptr, const MWWorld::Ptr& actor) const { /* Start of tes3mp addition Don't display a dialogue screen for two players interacting with each other */ if (actor == MWMechanics::getPlayer() && mwmp::PlayerList::isDedicatedPlayer(ptr)) return std::shared_ptr(new MWWorld::FailedAction("")); /* End of tes3mp addition */ /* Start of tes3mp addition Avoid returning an ActionTalk when a non-player NPC activates another non-player NPC, because it will always pop up a dialogue screen for the local player */ if (ptr != MWMechanics::getPlayer() && actor != MWMechanics::getPlayer()) return std::shared_ptr(new MWWorld::FailedAction("")); /* End of tes3mp addition */ // player got activated by another NPC if(ptr == MWMechanics::getPlayer()) return std::shared_ptr(new MWWorld::ActionTalk(actor)); // Werewolfs can't activate NPCs if(actor.getClass().isNpc() && actor.getClass().getNpcStats(actor).isWerewolf()) { const MWWorld::ESMStore &store = MWBase::Environment::get().getWorld()->getStore(); const ESM::Sound *sound = store.get().searchRandom("WolfNPC"); std::shared_ptr action(new MWWorld::FailedAction("#{sWerewolfRefusal}")); if(sound) action->setSound(sound->mId); return action; } const MWMechanics::CreatureStats& stats = getCreatureStats(ptr); if(stats.isDead()) { bool canLoot = Settings::Manager::getBool ("can loot during death animation", "Game"); // by default user can loot friendly actors during death animation if (canLoot && !stats.getAiSequence().isInCombat()) return std::shared_ptr(new MWWorld::ActionOpen(ptr)); // otherwise wait until death animation if(stats.isDeathAnimationFinished()) return std::shared_ptr(new MWWorld::ActionOpen(ptr)); } else if (!stats.getAiSequence().isInCombat()) { if (stats.getKnockedDown() || MWBase::Environment::get().getMechanicsManager()->isSneaking(actor)) return std::shared_ptr(new MWWorld::ActionOpen(ptr)); // stealing // Can't talk to werewolves if (!getNpcStats(ptr).isWerewolf()) return std::shared_ptr(new MWWorld::ActionTalk(ptr)); } else // In combat { const bool stealingInCombat = Settings::Manager::getBool ("always allow stealing from knocked out actors", "Game"); if (stealingInCombat && stats.getKnockedDown()) return std::shared_ptr(new MWWorld::ActionOpen(ptr)); // stealing } // Tribunal and some mod companions oddly enough must use open action as fallback if (!getScript(ptr).empty() && ptr.getRefData().getLocals().getIntVar(getScript(ptr), "companion")) return std::shared_ptr(new MWWorld::ActionOpen(ptr)); return std::shared_ptr (new MWWorld::FailedAction("")); } MWWorld::ContainerStore& Npc::getContainerStore (const MWWorld::Ptr& ptr) const { ensureCustomData (ptr); return ptr.getRefData().getCustomData()->asNpcCustomData().mInventoryStore; } MWWorld::InventoryStore& Npc::getInventoryStore (const MWWorld::Ptr& ptr) const { ensureCustomData (ptr); return ptr.getRefData().getCustomData()->asNpcCustomData().mInventoryStore; } std::string Npc::getScript (const MWWorld::ConstPtr& ptr) const { const MWWorld::LiveCellRef *ref = ptr.get(); return ref->mBase->mScript; } float Npc::getMaxSpeed(const MWWorld::Ptr& ptr) const { // TODO: This function is called several times per frame for each NPC. // It would be better to calculate it only once per frame for each NPC and save the result in CreatureStats. const MWMechanics::CreatureStats& stats = getCreatureStats(ptr); bool godmode = ptr == MWMechanics::getPlayer() && MWBase::Environment::get().getWorld()->getGodModeState(); if ((!godmode && stats.isParalyzed()) || stats.getKnockedDown() || stats.isDead()) return 0.f; const MWBase::World *world = MWBase::Environment::get().getWorld(); const GMST& gmst = getGmst(); const NpcCustomData *npcdata = static_cast(ptr.getRefData().getCustomData()); const MWMechanics::MagicEffects &mageffects = npcdata->mNpcStats.getMagicEffects(); const float normalizedEncumbrance = getNormalizedEncumbrance(ptr); bool swimming = world->isSwimming(ptr); bool sneaking = MWBase::Environment::get().getMechanicsManager()->isSneaking(ptr); bool running = stats.getStance(MWMechanics::CreatureStats::Stance_Run); bool inair = !world->isOnGround(ptr) && !swimming && !world->isFlying(ptr); running = running && (inair || MWBase::Environment::get().getMechanicsManager()->isRunning(ptr)); float moveSpeed; if(getEncumbrance(ptr) > getCapacity(ptr)) moveSpeed = 0.0f; else if(mageffects.get(ESM::MagicEffect::Levitate).getMagnitude() > 0 && world->isLevitationEnabled()) { float flySpeed = 0.01f*(npcdata->mNpcStats.getAttribute(ESM::Attribute::Speed).getModified() + mageffects.get(ESM::MagicEffect::Levitate).getMagnitude()); flySpeed = gmst.fMinFlySpeed->mValue.getFloat() + flySpeed*(gmst.fMaxFlySpeed->mValue.getFloat() - gmst.fMinFlySpeed->mValue.getFloat()); flySpeed *= 1.0f - gmst.fEncumberedMoveEffect->mValue.getFloat() * normalizedEncumbrance; flySpeed = std::max(0.0f, flySpeed); moveSpeed = flySpeed; } else if (swimming) moveSpeed = getSwimSpeed(ptr); else if (running && !sneaking) moveSpeed = getRunSpeed(ptr); else moveSpeed = getWalkSpeed(ptr); if(npcdata->mNpcStats.isWerewolf() && running && npcdata->mNpcStats.getDrawState() == MWMechanics::DrawState_Nothing) moveSpeed *= gmst.fWereWolfRunMult->mValue.getFloat(); return moveSpeed; } float Npc::getJump(const MWWorld::Ptr &ptr) const { if(getEncumbrance(ptr) > getCapacity(ptr)) return 0.f; const MWMechanics::CreatureStats& stats = getCreatureStats(ptr); bool godmode = ptr == MWMechanics::getPlayer() && MWBase::Environment::get().getWorld()->getGodModeState(); if ((!godmode && stats.isParalyzed()) || stats.getKnockedDown() || stats.isDead()) return 0.f; const NpcCustomData *npcdata = static_cast(ptr.getRefData().getCustomData()); const GMST& gmst = getGmst(); const MWMechanics::MagicEffects &mageffects = npcdata->mNpcStats.getMagicEffects(); const float encumbranceTerm = gmst.fJumpEncumbranceBase->mValue.getFloat() + gmst.fJumpEncumbranceMultiplier->mValue.getFloat() * (1.0f - Npc::getNormalizedEncumbrance(ptr)); float a = getSkill(ptr, ESM::Skill::Acrobatics); float b = 0.0f; if(a > 50.0f) { b = a - 50.0f; a = 50.0f; } float x = gmst.fJumpAcrobaticsBase->mValue.getFloat() + std::pow(a / 15.0f, gmst.fJumpAcroMultiplier->mValue.getFloat()); x += 3.0f * b * gmst.fJumpAcroMultiplier->mValue.getFloat(); x += mageffects.get(ESM::MagicEffect::Jump).getMagnitude() * 64; x *= encumbranceTerm; if(stats.getStance(MWMechanics::CreatureStats::Stance_Run)) x *= gmst.fJumpRunMultiplier->mValue.getFloat(); x *= npcdata->mNpcStats.getFatigueTerm(); x -= -Constants::GravityConst * Constants::UnitsPerMeter; x /= 3.0f; return x; } MWMechanics::Movement& Npc::getMovementSettings (const MWWorld::Ptr& ptr) const { ensureCustomData (ptr); return ptr.getRefData().getCustomData()->asNpcCustomData().mMovement; } bool Npc::isEssential (const MWWorld::ConstPtr& ptr) const { const MWWorld::LiveCellRef *ref = ptr.get(); return (ref->mBase->mFlags & ESM::NPC::Essential) != 0; } void Npc::registerSelf() { std::shared_ptr instance (new Npc); registerClass (typeid (ESM::NPC).name(), instance); } bool Npc::hasToolTip(const MWWorld::ConstPtr& ptr) const { if (!ptr.getRefData().getCustomData() || MWBase::Environment::get().getWindowManager()->isGuiMode()) return true; const NpcCustomData& customData = ptr.getRefData().getCustomData()->asNpcCustomData(); if (customData.mNpcStats.isDead() && customData.mNpcStats.isDeathAnimationFinished()) return true; if (!customData.mNpcStats.getAiSequence().isInCombat()) return true; const bool stealingInCombat = Settings::Manager::getBool ("always allow stealing from knocked out actors", "Game"); if (stealingInCombat && customData.mNpcStats.getKnockedDown()) return true; return false; } MWGui::ToolTipInfo Npc::getToolTipInfo (const MWWorld::ConstPtr& ptr, int count) const { const MWWorld::LiveCellRef *ref = ptr.get(); bool fullHelp = MWBase::Environment::get().getWindowManager()->getFullHelp(); MWGui::ToolTipInfo info; info.caption = MyGUI::TextIterator::toTagsString(getName(ptr)); if(fullHelp && !ref->mBase->mName.empty() && ptr.getRefData().getCustomData() && ptr.getRefData().getCustomData()->asNpcCustomData().mNpcStats.isWerewolf()) { info.caption += " ("; info.caption += MyGUI::TextIterator::toTagsString(ref->mBase->mName); info.caption += ")"; } if(fullHelp) info.text = MWGui::ToolTips::getMiscString(ref->mBase->mScript, "Script"); return info; } float Npc::getCapacity (const MWWorld::Ptr& ptr) const { const MWMechanics::CreatureStats& stats = getCreatureStats (ptr); static const float fEncumbranceStrMult = MWBase::Environment::get().getWorld()->getStore().get().find("fEncumbranceStrMult")->mValue.getFloat(); return stats.getAttribute(ESM::Attribute::Strength).getModified()*fEncumbranceStrMult; } float Npc::getEncumbrance (const MWWorld::Ptr& ptr) const { // According to UESP, inventory weight is ignored in werewolf form. Does that include // feather and burden effects? return getNpcStats(ptr).isWerewolf() ? 0.0f : Actor::getEncumbrance(ptr); } bool Npc::apply (const MWWorld::Ptr& ptr, const std::string& id, const MWWorld::Ptr& actor) const { MWMechanics::CastSpell cast(ptr, ptr); return cast.cast(id); } void Npc::skillUsageSucceeded (const MWWorld::Ptr& ptr, int skill, int usageType, float extraFactor) const { MWMechanics::NpcStats& stats = getNpcStats (ptr); if (stats.isWerewolf()) return; MWWorld::LiveCellRef *ref = ptr.get(); const ESM::Class *class_ = MWBase::Environment::get().getWorld()->getStore().get().find ( ref->mBase->mClass ); stats.useSkill (skill, *class_, usageType, extraFactor); } float Npc::getArmorRating (const MWWorld::Ptr& ptr) const { const MWBase::World *world = MWBase::Environment::get().getWorld(); const MWWorld::Store &store = world->getStore().get(); MWMechanics::NpcStats &stats = getNpcStats(ptr); const MWWorld::InventoryStore &invStore = getInventoryStore(ptr); float fUnarmoredBase1 = store.find("fUnarmoredBase1")->mValue.getFloat(); float fUnarmoredBase2 = store.find("fUnarmoredBase2")->mValue.getFloat(); float unarmoredSkill = getSkill(ptr, ESM::Skill::Unarmored); float ratings[MWWorld::InventoryStore::Slots]; for(int i = 0;i < MWWorld::InventoryStore::Slots;i++) { MWWorld::ConstContainerStoreIterator it = invStore.getSlot(i); if (it == invStore.end() || it->getTypeName() != typeid(ESM::Armor).name()) { // unarmored ratings[i] = (fUnarmoredBase1 * unarmoredSkill) * (fUnarmoredBase2 * unarmoredSkill); } else { ratings[i] = it->getClass().getEffectiveArmorRating(*it, ptr); // Take in account armor condition const bool hasHealth = it->getClass().hasItemHealth(*it); if (hasHealth) { ratings[i] *= it->getClass().getItemNormalizedHealth(*it); } } } float shield = stats.getMagicEffects().get(ESM::MagicEffect::Shield).getMagnitude(); return ratings[MWWorld::InventoryStore::Slot_Cuirass] * 0.3f + (ratings[MWWorld::InventoryStore::Slot_CarriedLeft] + ratings[MWWorld::InventoryStore::Slot_Helmet] + ratings[MWWorld::InventoryStore::Slot_Greaves] + ratings[MWWorld::InventoryStore::Slot_Boots] + ratings[MWWorld::InventoryStore::Slot_LeftPauldron] + ratings[MWWorld::InventoryStore::Slot_RightPauldron] ) * 0.1f + (ratings[MWWorld::InventoryStore::Slot_LeftGauntlet] + ratings[MWWorld::InventoryStore::Slot_RightGauntlet]) * 0.05f + shield; } void Npc::adjustScale(const MWWorld::ConstPtr &ptr, osg::Vec3f&scale, bool rendering) const { if (!rendering) return; // collision meshes are not scaled based on race height // having the same collision extents for all races makes the environments easier to test const MWWorld::LiveCellRef *ref = ptr.get(); const ESM::Race* race = MWBase::Environment::get().getWorld()->getStore().get().find(ref->mBase->mRace); // Race weight should not affect 1st-person meshes, otherwise it will change hand proportions and can break aiming. if (ptr == MWMechanics::getPlayer() && ptr.isInCell() && MWBase::Environment::get().getWorld()->isFirstPerson()) { if (ref->mBase->isMale()) scale *= race->mData.mHeight.mMale; else scale *= race->mData.mHeight.mFemale; return; } if (ref->mBase->isMale()) { scale.x() *= race->mData.mWeight.mMale; scale.y() *= race->mData.mWeight.mMale; scale.z() *= race->mData.mHeight.mMale; } else { scale.x() *= race->mData.mWeight.mFemale; scale.y() *= race->mData.mWeight.mFemale; scale.z() *= race->mData.mHeight.mFemale; } } int Npc::getServices(const MWWorld::ConstPtr &actor) const { return actor.get()->mBase->mAiData.mServices; } std::string Npc::getSoundIdFromSndGen(const MWWorld::Ptr &ptr, const std::string &name) const { if(name == "left" || name == "right") { MWBase::World *world = MWBase::Environment::get().getWorld(); if(world->isFlying(ptr)) return std::string(); osg::Vec3f pos(ptr.getRefData().getPosition().asVec3()); if(world->isSwimming(ptr)) return (name == "left") ? "Swim Left" : "Swim Right"; if(world->isUnderwater(ptr.getCell(), pos) || world->isWalkingOnWater(ptr)) return (name == "left") ? "FootWaterLeft" : "FootWaterRight"; if(world->isOnGround(ptr)) { if (getNpcStats(ptr).isWerewolf() && getCreatureStats(ptr).getStance(MWMechanics::CreatureStats::Stance_Run)) { int weaponType = ESM::Weapon::None; MWMechanics::getActiveWeapon(ptr, &weaponType); if (weaponType == ESM::Weapon::None) return std::string(); } const MWWorld::InventoryStore &inv = Npc::getInventoryStore(ptr); MWWorld::ConstContainerStoreIterator boots = inv.getSlot(MWWorld::InventoryStore::Slot_Boots); if(boots == inv.end() || boots->getTypeName() != typeid(ESM::Armor).name()) return (name == "left") ? "FootBareLeft" : "FootBareRight"; switch(boots->getClass().getEquipmentSkill(*boots)) { case ESM::Skill::LightArmor: return (name == "left") ? "FootLightLeft" : "FootLightRight"; case ESM::Skill::MediumArmor: return (name == "left") ? "FootMedLeft" : "FootMedRight"; case ESM::Skill::HeavyArmor: return (name == "left") ? "FootHeavyLeft" : "FootHeavyRight"; } } return std::string(); } // Morrowind ignores land soundgen for NPCs if(name == "land") return std::string(); if(name == "swimleft") return "Swim Left"; if(name == "swimright") return "Swim Right"; // TODO: I have no idea what these are supposed to do for NPCs since they use // voiced dialog for various conditions like health loss and combat taunts. Maybe // only for biped creatures? if(name == "moan") return std::string(); if(name == "roar") return std::string(); if(name == "scream") return std::string(); throw std::runtime_error(std::string("Unexpected soundgen type: ")+name); } MWWorld::Ptr Npc::copyToCellImpl(const MWWorld::ConstPtr &ptr, MWWorld::CellStore &cell) const { const MWWorld::LiveCellRef *ref = ptr.get(); return MWWorld::Ptr(cell.insert(ref), &cell); } float Npc::getSkill(const MWWorld::Ptr& ptr, int skill) const { return getNpcStats(ptr).getSkill(skill).getModified(); } int Npc::getBloodTexture(const MWWorld::ConstPtr &ptr) const { return ptr.get()->mBase->mBloodType; } void Npc::readAdditionalState (const MWWorld::Ptr& ptr, const ESM::ObjectState& state) const { if (!state.mHasCustomState) return; if (state.mVersion > 0) { if (!ptr.getRefData().getCustomData()) { // Create a CustomData, but don't fill it from ESM records (not needed) ptr.getRefData().setCustomData(std::make_unique()); } } else ensureCustomData(ptr); // in openmw 0.30 savegames not all state was saved yet, so need to load it regardless. NpcCustomData& customData = ptr.getRefData().getCustomData()->asNpcCustomData(); const ESM::NpcState& npcState = state.asNpcState(); customData.mInventoryStore.readState (npcState.mInventory); customData.mNpcStats.readState (npcState.mNpcStats); bool spellsInitialised = customData.mNpcStats.getSpells().setSpells(ptr.get()->mBase->mId); if(spellsInitialised) customData.mNpcStats.getSpells().clear(); customData.mNpcStats.readState (npcState.mCreatureStats); } void Npc::writeAdditionalState (const MWWorld::ConstPtr& ptr, ESM::ObjectState& state) const { if (!ptr.getRefData().getCustomData()) { state.mHasCustomState = false; return; } if (ptr.getRefData().getCount() <= 0) { state.mHasCustomState = false; return; } const NpcCustomData& customData = ptr.getRefData().getCustomData()->asNpcCustomData(); ESM::NpcState& npcState = state.asNpcState(); customData.mInventoryStore.writeState (npcState.mInventory); customData.mNpcStats.writeState (npcState.mNpcStats); customData.mNpcStats.writeState (npcState.mCreatureStats); } int Npc::getBaseGold(const MWWorld::ConstPtr& ptr) const { const MWWorld::LiveCellRef *ref = ptr.get(); return ref->mBase->mNpdt.mGold; } bool Npc::isClass(const MWWorld::ConstPtr& ptr, const std::string &className) const { return Misc::StringUtils::ciEqual(ptr.get()->mBase->mClass, className); } bool Npc::canSwim(const MWWorld::ConstPtr &ptr) const { return true; } bool Npc::canWalk(const MWWorld::ConstPtr &ptr) const { return true; } void Npc::respawn(const MWWorld::Ptr &ptr) const { const MWMechanics::CreatureStats& creatureStats = getCreatureStats(ptr); if (ptr.getRefData().getCount() > 0 && !creatureStats.isDead()) return; if (!creatureStats.isDeathAnimationFinished()) return; const MWWorld::Store& gmst = MWBase::Environment::get().getWorld()->getStore().get(); static const float fCorpseRespawnDelay = gmst.find("fCorpseRespawnDelay")->mValue.getFloat(); static const float fCorpseClearDelay = gmst.find("fCorpseClearDelay")->mValue.getFloat(); float delay = ptr.getRefData().getCount() == 0 ? fCorpseClearDelay : std::min(fCorpseRespawnDelay, fCorpseClearDelay); if (ptr.get()->mBase->mFlags & ESM::NPC::Respawn && creatureStats.getTimeOfDeath() + delay <= MWBase::Environment::get().getWorld()->getTimeStamp()) { if (ptr.getCellRef().hasContentFile()) { if (ptr.getRefData().getCount() == 0) { ptr.getRefData().setCount(1); const std::string& script = getScript(ptr); if (!script.empty()) MWBase::Environment::get().getWorld()->getLocalScripts().add(script, ptr); } MWBase::Environment::get().getWorld()->removeContainerScripts(ptr); ptr.getRefData().setCustomData(nullptr); // Reset to original position MWBase::Environment::get().getWorld()->moveObject(ptr, ptr.getCellRef().getPosition().pos[0], ptr.getCellRef().getPosition().pos[1], ptr.getCellRef().getPosition().pos[2]); } } } int Npc::getBaseFightRating (const MWWorld::ConstPtr& ptr) const { const MWWorld::LiveCellRef *ref = ptr.get(); return ref->mBase->mAiData.mFight; } bool Npc::isBipedal(const MWWorld::ConstPtr &ptr) const { return true; } std::string Npc::getPrimaryFaction (const MWWorld::ConstPtr& ptr) const { const MWWorld::LiveCellRef *ref = ptr.get(); return ref->mBase->mFaction; } int Npc::getPrimaryFactionRank (const MWWorld::ConstPtr& ptr) const { std::string factionID = ptr.getClass().getPrimaryFaction(ptr); if(factionID.empty()) return -1; // Search in the NPC data first if (const MWWorld::CustomData* data = ptr.getRefData().getCustomData()) { int rank = data->asNpcCustomData().mNpcStats.getFactionRank(factionID); if (rank >= 0) return rank; } // Use base NPC record as a fallback const MWWorld::LiveCellRef *ref = ptr.get(); return ref->mBase->getFactionRank(); } void Npc::setBaseAISetting(const std::string& id, MWMechanics::CreatureStats::AiSetting setting, int value) const { MWMechanics::setBaseAISetting(id, setting, value); } void Npc::modifyBaseInventory(const std::string& actorId, const std::string& itemId, int amount) const { MWMechanics::modifyBaseInventory(actorId, itemId, amount); } float Npc::getWalkSpeed(const MWWorld::Ptr& ptr) const { const GMST& gmst = getGmst(); const NpcCustomData* npcdata = static_cast(ptr.getRefData().getCustomData()); const float normalizedEncumbrance = getNormalizedEncumbrance(ptr); const bool sneaking = MWBase::Environment::get().getMechanicsManager()->isSneaking(ptr); float walkSpeed = gmst.fMinWalkSpeed->mValue.getFloat() + 0.01f * npcdata->mNpcStats.getAttribute(ESM::Attribute::Speed).getModified() * (gmst.fMaxWalkSpeed->mValue.getFloat() - gmst.fMinWalkSpeed->mValue.getFloat()); walkSpeed *= 1.0f - gmst.fEncumberedMoveEffect->mValue.getFloat()*normalizedEncumbrance; walkSpeed = std::max(0.0f, walkSpeed); if(sneaking) walkSpeed *= gmst.fSneakSpeedMultiplier->mValue.getFloat(); return walkSpeed; } float Npc::getRunSpeed(const MWWorld::Ptr& ptr) const { const GMST& gmst = getGmst(); return getWalkSpeed(ptr) * (0.01f * getSkill(ptr, ESM::Skill::Athletics) * gmst.fAthleticsRunBonus->mValue.getFloat() + gmst.fBaseRunMultiplier->mValue.getFloat()); } float Npc::getSwimSpeed(const MWWorld::Ptr& ptr) const { const GMST& gmst = getGmst(); const MWBase::World* world = MWBase::Environment::get().getWorld(); const MWMechanics::CreatureStats& stats = getCreatureStats(ptr); const NpcCustomData* npcdata = static_cast(ptr.getRefData().getCustomData()); const MWMechanics::MagicEffects& mageffects = npcdata->mNpcStats.getMagicEffects(); const bool swimming = world->isSwimming(ptr); const bool inair = !world->isOnGround(ptr) && !swimming && !world->isFlying(ptr); const bool running = stats.getStance(MWMechanics::CreatureStats::Stance_Run) && (inair || MWBase::Environment::get().getMechanicsManager()->isRunning(ptr)); float swimSpeed; if (running) swimSpeed = getRunSpeed(ptr); else swimSpeed = getWalkSpeed(ptr); swimSpeed *= 1.0f + 0.01f * mageffects.get(ESM::MagicEffect::SwiftSwim).getMagnitude(); swimSpeed *= gmst.fSwimRunBase->mValue.getFloat() + 0.01f * getSkill(ptr, ESM::Skill::Athletics) * gmst.fSwimRunAthleticsMult->mValue.getFloat(); return swimSpeed; } } ================================================ FILE: apps/openmw/mwclass/npc.hpp ================================================ #ifndef GAME_MWCLASS_NPC_H #define GAME_MWCLASS_NPC_H #include "actor.hpp" namespace ESM { struct GameSetting; } namespace MWClass { class Npc : public Actor { void ensureCustomData (const MWWorld::Ptr& ptr) const; MWWorld::Ptr copyToCellImpl(const MWWorld::ConstPtr &ptr, MWWorld::CellStore &cell) const override; struct GMST { const ESM::GameSetting *fMinWalkSpeed; const ESM::GameSetting *fMaxWalkSpeed; const ESM::GameSetting *fEncumberedMoveEffect; const ESM::GameSetting *fSneakSpeedMultiplier; const ESM::GameSetting *fAthleticsRunBonus; const ESM::GameSetting *fBaseRunMultiplier; const ESM::GameSetting *fMinFlySpeed; const ESM::GameSetting *fMaxFlySpeed; const ESM::GameSetting *fSwimRunBase; const ESM::GameSetting *fSwimRunAthleticsMult; const ESM::GameSetting *fJumpEncumbranceBase; const ESM::GameSetting *fJumpEncumbranceMultiplier; const ESM::GameSetting *fJumpAcrobaticsBase; const ESM::GameSetting *fJumpAcroMultiplier; const ESM::GameSetting *fJumpRunMultiplier; const ESM::GameSetting *fWereWolfRunMult; const ESM::GameSetting *fKnockDownMult; const ESM::GameSetting *iKnockDownOddsMult; const ESM::GameSetting *iKnockDownOddsBase; const ESM::GameSetting *fCombatArmorMinMult; }; static const GMST& getGmst(); public: void insertObjectRendering (const MWWorld::Ptr& ptr, const std::string& model, MWRender::RenderingInterface& renderingInterface) const override; ///< Add reference into a cell for rendering std::string getName (const MWWorld::ConstPtr& ptr) const override; ///< \return name or ID; can return an empty string. MWMechanics::CreatureStats& getCreatureStats (const MWWorld::Ptr& ptr) const override; ///< Return creature stats MWMechanics::NpcStats& getNpcStats (const MWWorld::Ptr& ptr) const override; ///< Return NPC stats MWWorld::ContainerStore& getContainerStore (const MWWorld::Ptr& ptr) const override; ///< Return container store bool hasToolTip(const MWWorld::ConstPtr& ptr) const override; ///< @return true if this object has a tooltip when focused (default implementation: true) MWGui::ToolTipInfo getToolTipInfo (const MWWorld::ConstPtr& ptr, int count) const override; ///< @return the content of the tool tip to be displayed. raises exception if the object has no tooltip. MWWorld::InventoryStore& getInventoryStore (const MWWorld::Ptr& ptr) const override; ///< Return inventory store bool hasInventoryStore(const MWWorld::Ptr &ptr) const override { return true; } /* Start of tes3mp addition Make it possible to check whether a class has a container store */ virtual bool hasContainerStore(const MWWorld::Ptr &ptr) const { return true; } /* End of tes3mp addition */ void hit(const MWWorld::Ptr& ptr, float attackStrength, int type) const override; void onHit(const MWWorld::Ptr &ptr, float damage, bool ishealth, const MWWorld::Ptr &object, const MWWorld::Ptr &attacker, const osg::Vec3f &hitPosition, bool successful) const override; void getModelsToPreload(const MWWorld::Ptr& ptr, std::vector& models) const override; ///< Get a list of models to preload that this object may use (directly or indirectly). default implementation: list getModel(). std::shared_ptr activate (const MWWorld::Ptr& ptr, const MWWorld::Ptr& actor) const override; ///< Generate action for activation std::string getScript (const MWWorld::ConstPtr& ptr) const override; ///< Return name of the script attached to ptr float getMaxSpeed (const MWWorld::Ptr& ptr) const override; ///< Return maximal movement speed. float getJump(const MWWorld::Ptr &ptr) const override; ///< Return jump velocity (not accounting for movement) MWMechanics::Movement& getMovementSettings (const MWWorld::Ptr& ptr) const override; ///< Return desired movement. float getCapacity (const MWWorld::Ptr& ptr) const override; ///< Return total weight that fits into the object. Throws an exception, if the object can't /// hold other objects. float getEncumbrance (const MWWorld::Ptr& ptr) const override; ///< Returns total weight of objects inside this object (including modifications from magic /// effects). Throws an exception, if the object can't hold other objects. float getArmorRating (const MWWorld::Ptr& ptr) const override; ///< @return combined armor rating of this actor bool apply (const MWWorld::Ptr& ptr, const std::string& id, const MWWorld::Ptr& actor) const override; ///< Apply \a id on \a ptr. /// \param actor Actor that is resposible for the ID being applied to \a ptr. /// \return Any effect? void adjustScale (const MWWorld::ConstPtr &ptr, osg::Vec3f &scale, bool rendering) const override; /// @param rendering Indicates if the scale to adjust is for the rendering mesh, or for the collision mesh void skillUsageSucceeded (const MWWorld::Ptr& ptr, int skill, int usageType, float extraFactor=1.f) const override; ///< Inform actor \a ptr that a skill use has succeeded. bool isEssential (const MWWorld::ConstPtr& ptr) const override; ///< Is \a ptr essential? (i.e. may losing \a ptr make the game unwinnable) int getServices (const MWWorld::ConstPtr& actor) const override; bool isPersistent (const MWWorld::ConstPtr& ptr) const override; std::string getSoundIdFromSndGen(const MWWorld::Ptr &ptr, const std::string &name) const override; static void registerSelf(); std::string getModel(const MWWorld::ConstPtr &ptr) const override; float getSkill(const MWWorld::Ptr& ptr, int skill) const override; /// Get a blood texture suitable for \a ptr (see Blood Texture 0-2 in Morrowind.ini) int getBloodTexture (const MWWorld::ConstPtr& ptr) const override; bool isNpc() const override { return true; } void readAdditionalState (const MWWorld::Ptr& ptr, const ESM::ObjectState& state) const override; ///< Read additional state from \a state into \a ptr. void writeAdditionalState (const MWWorld::ConstPtr& ptr, ESM::ObjectState& state) const override; ///< Write additional state from \a ptr into \a state. int getBaseGold(const MWWorld::ConstPtr& ptr) const override; bool isClass(const MWWorld::ConstPtr& ptr, const std::string &className) const override; bool canSwim (const MWWorld::ConstPtr &ptr) const override; bool canWalk (const MWWorld::ConstPtr &ptr) const override; bool isBipedal (const MWWorld::ConstPtr &ptr) const override; void respawn (const MWWorld::Ptr& ptr) const override; int getBaseFightRating (const MWWorld::ConstPtr& ptr) const override; std::string getPrimaryFaction(const MWWorld::ConstPtr &ptr) const override; int getPrimaryFactionRank(const MWWorld::ConstPtr &ptr) const override; void setBaseAISetting(const std::string& id, MWMechanics::CreatureStats::AiSetting setting, int value) const override; void modifyBaseInventory(const std::string& actorId, const std::string& itemId, int amount) const override; float getWalkSpeed(const MWWorld::Ptr& ptr) const override; float getRunSpeed(const MWWorld::Ptr& ptr) const override; float getSwimSpeed(const MWWorld::Ptr& ptr) const override; }; } #endif ================================================ FILE: apps/openmw/mwclass/potion.cpp ================================================ #include "potion.hpp" /* Start of tes3mp addition Include additional headers for multiplayer purposes */ #include #include "../mwmp/Main.hpp" #include "../mwmp/Networking.hpp" /* End of tes3mp addition */ #include #include "../mwbase/environment.hpp" #include "../mwbase/world.hpp" #include "../mwbase/windowmanager.hpp" #include "../mwworld/ptr.hpp" #include "../mwworld/actionapply.hpp" #include "../mwworld/cellstore.hpp" #include "../mwworld/esmstore.hpp" #include "../mwphysics/physicssystem.hpp" #include "../mwworld/nullaction.hpp" #include "../mwgui/tooltips.hpp" #include "../mwrender/objects.hpp" #include "../mwrender/renderinginterface.hpp" #include "../mwmechanics/alchemy.hpp" namespace MWClass { void Potion::insertObjectRendering (const MWWorld::Ptr& ptr, const std::string& model, MWRender::RenderingInterface& renderingInterface) const { if (!model.empty()) { renderingInterface.getObjects().insertModel(ptr, model); } } void Potion::insertObject(const MWWorld::Ptr& ptr, const std::string& model, MWPhysics::PhysicsSystem& physics) const { // TODO: add option somewhere to enable collision for placeable objects /* Start of tes3mp addition Make it possible to enable collision for this object class from a packet */ if (!model.empty()) { mwmp::BaseWorldstate *worldstate = mwmp::Main::get().getNetworking()->getWorldstate(); if (worldstate->hasPlacedObjectCollision || Utils::vectorContains(worldstate->enforcedCollisionRefIds, ptr.getCellRef().getRefId())) { if (worldstate->useActorCollisionForPlacedObjects) physics.addObject(ptr, model, MWPhysics::CollisionType_Actor); else physics.addObject(ptr, model, MWPhysics::CollisionType_World); } } /* End of tes3mp addition */ } std::string Potion::getModel(const MWWorld::ConstPtr &ptr) const { const MWWorld::LiveCellRef *ref = ptr.get(); const std::string &model = ref->mBase->mModel; if (!model.empty()) { return "meshes\\" + model; } return ""; } std::string Potion::getName (const MWWorld::ConstPtr& ptr) const { const MWWorld::LiveCellRef *ref = ptr.get(); const std::string& name = ref->mBase->mName; return !name.empty() ? name : ref->mBase->mId; } std::shared_ptr Potion::activate (const MWWorld::Ptr& ptr, const MWWorld::Ptr& actor) const { return defaultItemActivate(ptr, actor); } std::string Potion::getScript (const MWWorld::ConstPtr& ptr) const { const MWWorld::LiveCellRef *ref = ptr.get(); return ref->mBase->mScript; } int Potion::getValue (const MWWorld::ConstPtr& ptr) const { const MWWorld::LiveCellRef *ref = ptr.get(); return ref->mBase->mData.mValue; } void Potion::registerSelf() { std::shared_ptr instance (new Potion); registerClass (typeid (ESM::Potion).name(), instance); } std::string Potion::getUpSoundId (const MWWorld::ConstPtr& ptr) const { return std::string("Item Potion Up"); } std::string Potion::getDownSoundId (const MWWorld::ConstPtr& ptr) const { return std::string("Item Potion Down"); } std::string Potion::getInventoryIcon (const MWWorld::ConstPtr& ptr) const { const MWWorld::LiveCellRef *ref = ptr.get(); return ref->mBase->mIcon; } MWGui::ToolTipInfo Potion::getToolTipInfo (const MWWorld::ConstPtr& ptr, int count) const { const MWWorld::LiveCellRef *ref = ptr.get(); MWGui::ToolTipInfo info; info.caption = MyGUI::TextIterator::toTagsString(getName(ptr)) + MWGui::ToolTips::getCountString(count); info.icon = ref->mBase->mIcon; std::string text; text += "\n#{sWeight}: " + MWGui::ToolTips::toString(ref->mBase->mData.mWeight); text += MWGui::ToolTips::getValueString(ref->mBase->mData.mValue, "#{sValue}"); info.effects = MWGui::Widgets::MWEffectList::effectListFromESM(&ref->mBase->mEffects); // hide effects the player doesn't know about MWWorld::Ptr player = MWBase::Environment::get().getWorld ()->getPlayerPtr(); for (unsigned int i=0; igetFullHelp()) { text += MWGui::ToolTips::getCellRefString(ptr.getCellRef()); text += MWGui::ToolTips::getMiscString(ref->mBase->mScript, "Script"); } info.text = text; return info; } std::shared_ptr Potion::use (const MWWorld::Ptr& ptr, bool force) const { MWWorld::LiveCellRef *ref = ptr.get(); std::shared_ptr action ( new MWWorld::ActionApply (ptr, ref->mBase->mId)); action->setSound ("Drink"); return action; } MWWorld::Ptr Potion::copyToCellImpl(const MWWorld::ConstPtr &ptr, MWWorld::CellStore &cell) const { const MWWorld::LiveCellRef *ref = ptr.get(); return MWWorld::Ptr(cell.insert(ref), &cell); } bool Potion::canSell (const MWWorld::ConstPtr& item, int npcServices) const { return (npcServices & ESM::NPC::Potions) != 0; } float Potion::getWeight(const MWWorld::ConstPtr &ptr) const { const MWWorld::LiveCellRef *ref = ptr.get(); return ref->mBase->mData.mWeight; } } ================================================ FILE: apps/openmw/mwclass/potion.hpp ================================================ #ifndef GAME_MWCLASS_POTION_H #define GAME_MWCLASS_POTION_H #include "../mwworld/class.hpp" namespace MWClass { class Potion : public MWWorld::Class { MWWorld::Ptr copyToCellImpl(const MWWorld::ConstPtr &ptr, MWWorld::CellStore &cell) const override; public: void insertObjectRendering (const MWWorld::Ptr& ptr, const std::string& model, MWRender::RenderingInterface& renderingInterface) const override; ///< Add reference into a cell for rendering void insertObject(const MWWorld::Ptr& ptr, const std::string& model, MWPhysics::PhysicsSystem& physics) const override; std::string getName (const MWWorld::ConstPtr& ptr) const override; ///< \return name or ID; can return an empty string. std::shared_ptr activate (const MWWorld::Ptr& ptr, const MWWorld::Ptr& actor) const override; ///< Generate action for activation MWGui::ToolTipInfo getToolTipInfo (const MWWorld::ConstPtr& ptr, int count) const override; ///< @return the content of the tool tip to be displayed. raises exception if the object has no tooltip. std::string getScript (const MWWorld::ConstPtr& ptr) const override; ///< Return name of the script attached to ptr int getValue (const MWWorld::ConstPtr& ptr) const override; ///< Return trade value of the object. Throws an exception, if the object can't be traded. std::shared_ptr use (const MWWorld::Ptr& ptr, bool force=false) const override; ///< Generate action for using via inventory menu static void registerSelf(); std::string getUpSoundId (const MWWorld::ConstPtr& ptr) const override; ///< Return the pick up sound Id std::string getDownSoundId (const MWWorld::ConstPtr& ptr) const override; ///< Return the put down sound Id std::string getInventoryIcon (const MWWorld::ConstPtr& ptr) const override; ///< Return name of inventory icon. std::string getModel(const MWWorld::ConstPtr &ptr) const override; float getWeight (const MWWorld::ConstPtr& ptr) const override; bool canSell (const MWWorld::ConstPtr& item, int npcServices) const override; }; } #endif ================================================ FILE: apps/openmw/mwclass/probe.cpp ================================================ #include "probe.hpp" #include #include "../mwbase/environment.hpp" #include "../mwbase/mechanicsmanager.hpp" #include "../mwbase/windowmanager.hpp" #include "../mwworld/ptr.hpp" #include "../mwworld/actionequip.hpp" #include "../mwworld/inventorystore.hpp" #include "../mwworld/cellstore.hpp" #include "../mwphysics/physicssystem.hpp" #include "../mwworld/nullaction.hpp" #include "../mwgui/tooltips.hpp" #include "../mwrender/objects.hpp" #include "../mwrender/renderinginterface.hpp" namespace MWClass { void Probe::insertObjectRendering (const MWWorld::Ptr& ptr, const std::string& model, MWRender::RenderingInterface& renderingInterface) const { if (!model.empty()) { renderingInterface.getObjects().insertModel(ptr, model); } } void Probe::insertObject(const MWWorld::Ptr& ptr, const std::string& model, MWPhysics::PhysicsSystem& physics) const { // TODO: add option somewhere to enable collision for placeable objects } std::string Probe::getModel(const MWWorld::ConstPtr &ptr) const { const MWWorld::LiveCellRef *ref = ptr.get(); const std::string &model = ref->mBase->mModel; if (!model.empty()) { return "meshes\\" + model; } return ""; } std::string Probe::getName (const MWWorld::ConstPtr& ptr) const { const MWWorld::LiveCellRef *ref = ptr.get(); const std::string& name = ref->mBase->mName; return !name.empty() ? name : ref->mBase->mId; } std::shared_ptr Probe::activate (const MWWorld::Ptr& ptr, const MWWorld::Ptr& actor) const { return defaultItemActivate(ptr, actor); } std::string Probe::getScript (const MWWorld::ConstPtr& ptr) const { const MWWorld::LiveCellRef *ref = ptr.get(); return ref->mBase->mScript; } std::pair, bool> Probe::getEquipmentSlots (const MWWorld::ConstPtr& ptr) const { std::vector slots_; slots_.push_back (int (MWWorld::InventoryStore::Slot_CarriedRight)); return std::make_pair (slots_, false); } int Probe::getValue (const MWWorld::ConstPtr& ptr) const { const MWWorld::LiveCellRef *ref = ptr.get(); return ref->mBase->mData.mValue; } void Probe::registerSelf() { std::shared_ptr instance (new Probe); registerClass (typeid (ESM::Probe).name(), instance); } std::string Probe::getUpSoundId (const MWWorld::ConstPtr& ptr) const { return std::string("Item Probe Up"); } std::string Probe::getDownSoundId (const MWWorld::ConstPtr& ptr) const { return std::string("Item Probe Down"); } std::string Probe::getInventoryIcon (const MWWorld::ConstPtr& ptr) const { const MWWorld::LiveCellRef *ref = ptr.get(); return ref->mBase->mIcon; } MWGui::ToolTipInfo Probe::getToolTipInfo (const MWWorld::ConstPtr& ptr, int count) const { const MWWorld::LiveCellRef *ref = ptr.get(); MWGui::ToolTipInfo info; info.caption = MyGUI::TextIterator::toTagsString(getName(ptr)) + MWGui::ToolTips::getCountString(count); info.icon = ref->mBase->mIcon; std::string text; int remainingUses = getItemHealth(ptr); text += "\n#{sUses}: " + MWGui::ToolTips::toString(remainingUses); text += "\n#{sQuality}: " + MWGui::ToolTips::toString(ref->mBase->mData.mQuality); text += MWGui::ToolTips::getWeightString(ref->mBase->mData.mWeight, "#{sWeight}"); text += MWGui::ToolTips::getValueString(ref->mBase->mData.mValue, "#{sValue}"); if (MWBase::Environment::get().getWindowManager()->getFullHelp()) { text += MWGui::ToolTips::getCellRefString(ptr.getCellRef()); text += MWGui::ToolTips::getMiscString(ref->mBase->mScript, "Script"); } info.text = text; return info; } std::shared_ptr Probe::use (const MWWorld::Ptr& ptr, bool force) const { std::shared_ptr action(new MWWorld::ActionEquip(ptr, force)); action->setSound(getUpSoundId(ptr)); return action; } MWWorld::Ptr Probe::copyToCellImpl(const MWWorld::ConstPtr &ptr, MWWorld::CellStore &cell) const { const MWWorld::LiveCellRef *ref = ptr.get(); return MWWorld::Ptr(cell.insert(ref), &cell); } std::pair Probe::canBeEquipped(const MWWorld::ConstPtr &ptr, const MWWorld::Ptr &npc) const { // Do not allow equip tools from inventory during attack if (MWBase::Environment::get().getMechanicsManager()->isAttackingOrSpell(npc) && MWBase::Environment::get().getWindowManager()->isGuiMode()) return std::make_pair(0, "#{sCantEquipWeapWarning}"); return std::make_pair(1, ""); } bool Probe::canSell (const MWWorld::ConstPtr& item, int npcServices) const { return (npcServices & ESM::NPC::Probes) != 0; } int Probe::getItemMaxHealth (const MWWorld::ConstPtr& ptr) const { const MWWorld::LiveCellRef *ref = ptr.get(); return ref->mBase->mData.mUses; } float Probe::getWeight(const MWWorld::ConstPtr &ptr) const { const MWWorld::LiveCellRef *ref = ptr.get(); return ref->mBase->mData.mWeight; } } ================================================ FILE: apps/openmw/mwclass/probe.hpp ================================================ #ifndef GAME_MWCLASS_PROBE_H #define GAME_MWCLASS_PROBE_H #include "../mwworld/class.hpp" namespace MWClass { class Probe : public MWWorld::Class { MWWorld::Ptr copyToCellImpl(const MWWorld::ConstPtr &ptr, MWWorld::CellStore &cell) const override; public: void insertObjectRendering (const MWWorld::Ptr& ptr, const std::string& model, MWRender::RenderingInterface& renderingInterface) const override; ///< Add reference into a cell for rendering void insertObject(const MWWorld::Ptr& ptr, const std::string& model, MWPhysics::PhysicsSystem& physics) const override; std::string getName (const MWWorld::ConstPtr& ptr) const override; ///< \return name or ID; can return an empty string. std::shared_ptr activate (const MWWorld::Ptr& ptr, const MWWorld::Ptr& actor) const override; ///< Generate action for activation MWGui::ToolTipInfo getToolTipInfo (const MWWorld::ConstPtr& ptr, int count) const override; ///< @return the content of the tool tip to be displayed. raises exception if the object has no tooltip. std::string getScript (const MWWorld::ConstPtr& ptr) const override; ///< Return name of the script attached to ptr std::pair, bool> getEquipmentSlots (const MWWorld::ConstPtr& ptr) const override; ///< \return first: Return IDs of the slot this object can be equipped in; second: can object /// stay stacked when equipped? int getValue (const MWWorld::ConstPtr& ptr) const override; ///< Return trade value of the object. Throws an exception, if the object can't be traded. static void registerSelf(); std::string getUpSoundId (const MWWorld::ConstPtr& ptr) const override; ///< Return the pick up sound Id std::string getDownSoundId (const MWWorld::ConstPtr& ptr) const override; ///< Return the put down sound Id std::string getInventoryIcon (const MWWorld::ConstPtr& ptr) const override; ///< Return name of inventory icon. std::pair canBeEquipped(const MWWorld::ConstPtr &ptr, const MWWorld::Ptr &npc) const override; std::shared_ptr use (const MWWorld::Ptr& ptr, bool force=false) const override; ///< Generate action for using via inventory menu std::string getModel(const MWWorld::ConstPtr &ptr) const override; bool canSell (const MWWorld::ConstPtr& item, int npcServices) const override; float getWeight (const MWWorld::ConstPtr& ptr) const override; int getItemMaxHealth (const MWWorld::ConstPtr& ptr) const override; ///< Return item max health or throw an exception, if class does not have item health bool hasItemHealth (const MWWorld::ConstPtr& ptr) const override { return true; } ///< \return Item health data available? (default implementation: false) }; } #endif ================================================ FILE: apps/openmw/mwclass/repair.cpp ================================================ #include "repair.hpp" /* Start of tes3mp addition Include additional headers for multiplayer purposes */ #include #include "../mwmp/Main.hpp" #include "../mwmp/Networking.hpp" /* End of tes3mp addition */ #include #include "../mwbase/environment.hpp" #include "../mwbase/windowmanager.hpp" #include "../mwworld/ptr.hpp" #include "../mwworld/cellstore.hpp" #include "../mwphysics/physicssystem.hpp" #include "../mwworld/actionrepair.hpp" #include "../mwgui/tooltips.hpp" #include "../mwrender/objects.hpp" #include "../mwrender/renderinginterface.hpp" namespace MWClass { void Repair::insertObjectRendering (const MWWorld::Ptr& ptr, const std::string& model, MWRender::RenderingInterface& renderingInterface) const { if (!model.empty()) { renderingInterface.getObjects().insertModel(ptr, model); } } void Repair::insertObject(const MWWorld::Ptr& ptr, const std::string& model, MWPhysics::PhysicsSystem& physics) const { // TODO: add option somewhere to enable collision for placeable objects /* Start of tes3mp addition Make it possible to enable collision for this object class from a packet */ if (!model.empty()) { mwmp::BaseWorldstate *worldstate = mwmp::Main::get().getNetworking()->getWorldstate(); if (worldstate->hasPlacedObjectCollision || Utils::vectorContains(worldstate->enforcedCollisionRefIds, ptr.getCellRef().getRefId())) { if (worldstate->useActorCollisionForPlacedObjects) physics.addObject(ptr, model, MWPhysics::CollisionType_Actor); else physics.addObject(ptr, model, MWPhysics::CollisionType_World); } } /* End of tes3mp addition */ } std::string Repair::getModel(const MWWorld::ConstPtr &ptr) const { const MWWorld::LiveCellRef *ref = ptr.get(); const std::string &model = ref->mBase->mModel; if (!model.empty()) { return "meshes\\" + model; } return ""; } std::string Repair::getName (const MWWorld::ConstPtr& ptr) const { const MWWorld::LiveCellRef *ref = ptr.get(); const std::string& name = ref->mBase->mName; return !name.empty() ? name : ref->mBase->mId; } std::shared_ptr Repair::activate (const MWWorld::Ptr& ptr, const MWWorld::Ptr& actor) const { return defaultItemActivate(ptr, actor); } std::string Repair::getScript (const MWWorld::ConstPtr& ptr) const { const MWWorld::LiveCellRef *ref = ptr.get(); return ref->mBase->mScript; } int Repair::getValue (const MWWorld::ConstPtr& ptr) const { const MWWorld::LiveCellRef *ref = ptr.get(); return ref->mBase->mData.mValue; } void Repair::registerSelf() { std::shared_ptr instance (new Repair); registerClass (typeid (ESM::Repair).name(), instance); } std::string Repair::getUpSoundId (const MWWorld::ConstPtr& ptr) const { return std::string("Item Repair Up"); } std::string Repair::getDownSoundId (const MWWorld::ConstPtr& ptr) const { return std::string("Item Repair Down"); } std::string Repair::getInventoryIcon (const MWWorld::ConstPtr& ptr) const { const MWWorld::LiveCellRef *ref = ptr.get(); return ref->mBase->mIcon; } bool Repair::hasItemHealth (const MWWorld::ConstPtr& ptr) const { return true; } int Repair::getItemMaxHealth (const MWWorld::ConstPtr& ptr) const { const MWWorld::LiveCellRef *ref = ptr.get(); return ref->mBase->mData.mUses; } MWGui::ToolTipInfo Repair::getToolTipInfo (const MWWorld::ConstPtr& ptr, int count) const { const MWWorld::LiveCellRef *ref = ptr.get(); MWGui::ToolTipInfo info; info.caption = MyGUI::TextIterator::toTagsString(getName(ptr)) + MWGui::ToolTips::getCountString(count); info.icon = ref->mBase->mIcon; std::string text; int remainingUses = getItemHealth(ptr); text += "\n#{sUses}: " + MWGui::ToolTips::toString(remainingUses); text += "\n#{sQuality}: " + MWGui::ToolTips::toString(ref->mBase->mData.mQuality); text += MWGui::ToolTips::getWeightString(ref->mBase->mData.mWeight, "#{sWeight}"); text += MWGui::ToolTips::getValueString(ref->mBase->mData.mValue, "#{sValue}"); if (MWBase::Environment::get().getWindowManager()->getFullHelp()) { text += MWGui::ToolTips::getCellRefString(ptr.getCellRef()); text += MWGui::ToolTips::getMiscString(ref->mBase->mScript, "Script"); } info.text = text; return info; } MWWorld::Ptr Repair::copyToCellImpl(const MWWorld::ConstPtr &ptr, MWWorld::CellStore &cell) const { const MWWorld::LiveCellRef *ref = ptr.get(); return MWWorld::Ptr(cell.insert(ref), &cell); } std::shared_ptr Repair::use (const MWWorld::Ptr& ptr, bool force) const { return std::shared_ptr(new MWWorld::ActionRepair(ptr, force)); } bool Repair::canSell (const MWWorld::ConstPtr& item, int npcServices) const { return (npcServices & ESM::NPC::RepairItem) != 0; } float Repair::getWeight(const MWWorld::ConstPtr &ptr) const { const MWWorld::LiveCellRef *ref = ptr.get(); return ref->mBase->mData.mWeight; } } ================================================ FILE: apps/openmw/mwclass/repair.hpp ================================================ #ifndef GAME_MWCLASS_REPAIR_H #define GAME_MWCLASS_REPAIR_H #include "../mwworld/class.hpp" namespace MWClass { class Repair : public MWWorld::Class { MWWorld::Ptr copyToCellImpl(const MWWorld::ConstPtr &ptr, MWWorld::CellStore &cell) const override; public: void insertObjectRendering (const MWWorld::Ptr& ptr, const std::string& model, MWRender::RenderingInterface& renderingInterface) const override; ///< Add reference into a cell for rendering void insertObject(const MWWorld::Ptr& ptr, const std::string& model, MWPhysics::PhysicsSystem& physics) const override; std::string getName (const MWWorld::ConstPtr& ptr) const override; ///< \return name or ID; can return an empty string. std::shared_ptr activate (const MWWorld::Ptr& ptr, const MWWorld::Ptr& actor) const override; ///< Generate action for activation MWGui::ToolTipInfo getToolTipInfo (const MWWorld::ConstPtr& ptr, int count) const override; ///< @return the content of the tool tip to be displayed. raises exception if the object has no tooltip. std::string getScript (const MWWorld::ConstPtr& ptr) const override; ///< Return name of the script attached to ptr int getValue (const MWWorld::ConstPtr& ptr) const override; ///< Return trade value of the object. Throws an exception, if the object can't be traded. static void registerSelf(); std::string getUpSoundId (const MWWorld::ConstPtr& ptr) const override; ///< Return the pick up sound Id std::string getDownSoundId (const MWWorld::ConstPtr& ptr) const override; ///< Return the put down sound Id std::string getInventoryIcon (const MWWorld::ConstPtr& ptr) const override; ///< Return name of inventory icon. std::string getModel(const MWWorld::ConstPtr &ptr) const override; std::shared_ptr use (const MWWorld::Ptr& ptr, bool force=false) const override; ///< Generate action for using via inventory menu (default implementation: return a /// null action). bool hasItemHealth (const MWWorld::ConstPtr& ptr) const override; ///< \return Item health data available? (default implementation: false) int getItemMaxHealth (const MWWorld::ConstPtr& ptr) const override; ///< Return item max health or throw an exception, if class does not have item health /// (default implementation: throw an exception) float getWeight (const MWWorld::ConstPtr& ptr) const override; bool canSell (const MWWorld::ConstPtr& item, int npcServices) const override; }; } #endif ================================================ FILE: apps/openmw/mwclass/static.cpp ================================================ #include "static.hpp" #include #include #include "../mwworld/ptr.hpp" #include "../mwphysics/physicssystem.hpp" #include "../mwworld/cellstore.hpp" #include "../mwrender/objects.hpp" #include "../mwrender/renderinginterface.hpp" #include "../mwrender/vismask.hpp" namespace MWClass { void Static::insertObjectRendering (const MWWorld::Ptr& ptr, const std::string& model, MWRender::RenderingInterface& renderingInterface) const { if (!model.empty()) { renderingInterface.getObjects().insertModel(ptr, model); ptr.getRefData().getBaseNode()->setNodeMask(MWRender::Mask_Static); } } void Static::insertObject(const MWWorld::Ptr& ptr, const std::string& model, MWPhysics::PhysicsSystem& physics) const { if(!model.empty()) physics.addObject(ptr, model); } std::string Static::getModel(const MWWorld::ConstPtr &ptr) const { const MWWorld::LiveCellRef *ref = ptr.get(); const std::string &model = ref->mBase->mModel; if (!model.empty()) { return "meshes\\" + model; } return ""; } std::string Static::getName (const MWWorld::ConstPtr& ptr) const { return ""; } bool Static::hasToolTip(const MWWorld::ConstPtr& ptr) const { return false; } void Static::registerSelf() { std::shared_ptr instance (new Static); registerClass (typeid (ESM::Static).name(), instance); } MWWorld::Ptr Static::copyToCellImpl(const MWWorld::ConstPtr &ptr, MWWorld::CellStore &cell) const { const MWWorld::LiveCellRef *ref = ptr.get(); return MWWorld::Ptr(cell.insert(ref), &cell); } } ================================================ FILE: apps/openmw/mwclass/static.hpp ================================================ #ifndef GAME_MWCLASS_STATIC_H #define GAME_MWCLASS_STATIC_H #include "../mwworld/class.hpp" namespace MWClass { class Static : public MWWorld::Class { MWWorld::Ptr copyToCellImpl(const MWWorld::ConstPtr &ptr, MWWorld::CellStore &cell) const override; public: void insertObjectRendering (const MWWorld::Ptr& ptr, const std::string& model, MWRender::RenderingInterface& renderingInterface) const override; ///< Add reference into a cell for rendering void insertObject(const MWWorld::Ptr& ptr, const std::string& model, MWPhysics::PhysicsSystem& physics) const override; std::string getName (const MWWorld::ConstPtr& ptr) const override; ///< \return name or ID; can return an empty string. bool hasToolTip (const MWWorld::ConstPtr& ptr) const override; ///< @return true if this object has a tooltip when focused (default implementation: true) static void registerSelf(); std::string getModel(const MWWorld::ConstPtr &ptr) const override; }; } #endif ================================================ FILE: apps/openmw/mwclass/weapon.cpp ================================================ #include "weapon.hpp" /* Start of tes3mp addition Include additional headers for multiplayer purposes */ #include #include "../mwmp/Main.hpp" #include "../mwmp/Networking.hpp" #include "../mwmp/LocalPlayer.hpp" /* End of tes3mp addition */ #include #include #include #include "../mwbase/environment.hpp" #include "../mwbase/mechanicsmanager.hpp" #include "../mwbase/world.hpp" #include "../mwbase/windowmanager.hpp" #include "../mwworld/ptr.hpp" #include "../mwworld/actionequip.hpp" #include "../mwworld/inventorystore.hpp" #include "../mwworld/cellstore.hpp" #include "../mwworld/esmstore.hpp" #include "../mwphysics/physicssystem.hpp" #include "../mwworld/nullaction.hpp" #include "../mwmechanics/weapontype.hpp" #include "../mwgui/tooltips.hpp" #include "../mwrender/objects.hpp" #include "../mwrender/renderinginterface.hpp" namespace MWClass { void Weapon::insertObjectRendering (const MWWorld::Ptr& ptr, const std::string& model, MWRender::RenderingInterface& renderingInterface) const { if (!model.empty()) { renderingInterface.getObjects().insertModel(ptr, model); } } void Weapon::insertObject(const MWWorld::Ptr& ptr, const std::string& model, MWPhysics::PhysicsSystem& physics) const { // TODO: add option somewhere to enable collision for placeable objects /* Start of tes3mp addition Make it possible to enable collision for this object class from a packet */ if (!model.empty()) { mwmp::BaseWorldstate *worldstate = mwmp::Main::get().getNetworking()->getWorldstate(); if (worldstate->hasPlacedObjectCollision || Utils::vectorContains(worldstate->enforcedCollisionRefIds, ptr.getCellRef().getRefId())) { if (worldstate->useActorCollisionForPlacedObjects) physics.addObject(ptr, model, MWPhysics::CollisionType_Actor); else physics.addObject(ptr, model, MWPhysics::CollisionType_World); } } /* End of tes3mp addition */ } std::string Weapon::getModel(const MWWorld::ConstPtr &ptr) const { const MWWorld::LiveCellRef *ref = ptr.get(); const std::string &model = ref->mBase->mModel; if (!model.empty()) { return "meshes\\" + model; } return ""; } std::string Weapon::getName (const MWWorld::ConstPtr& ptr) const { const MWWorld::LiveCellRef *ref = ptr.get(); const std::string& name = ref->mBase->mName; return !name.empty() ? name : ref->mBase->mId; } std::shared_ptr Weapon::activate (const MWWorld::Ptr& ptr, const MWWorld::Ptr& actor) const { return defaultItemActivate(ptr, actor); } bool Weapon::hasItemHealth (const MWWorld::ConstPtr& ptr) const { const MWWorld::LiveCellRef *ref = ptr.get(); int type = ref->mBase->mData.mType; return MWMechanics::getWeaponType(type)->mFlags & ESM::WeaponType::HasHealth; } int Weapon::getItemMaxHealth (const MWWorld::ConstPtr& ptr) const { const MWWorld::LiveCellRef *ref = ptr.get(); return ref->mBase->mData.mHealth; } std::string Weapon::getScript (const MWWorld::ConstPtr& ptr) const { const MWWorld::LiveCellRef *ref = ptr.get(); return ref->mBase->mScript; } std::pair, bool> Weapon::getEquipmentSlots (const MWWorld::ConstPtr& ptr) const { const MWWorld::LiveCellRef *ref = ptr.get(); ESM::WeaponType::Class weapClass = MWMechanics::getWeaponType(ref->mBase->mData.mType)->mWeaponClass; std::vector slots_; bool stack = false; if (weapClass == ESM::WeaponType::Ammo) { slots_.push_back (int (MWWorld::InventoryStore::Slot_Ammunition)); stack = true; } else if (weapClass == ESM::WeaponType::Thrown) { slots_.push_back (int (MWWorld::InventoryStore::Slot_CarriedRight)); stack = true; } else slots_.push_back (int (MWWorld::InventoryStore::Slot_CarriedRight)); return std::make_pair (slots_, stack); } int Weapon::getEquipmentSkill (const MWWorld::ConstPtr& ptr) const { const MWWorld::LiveCellRef *ref = ptr.get(); int type = ref->mBase->mData.mType; return MWMechanics::getWeaponType(type)->mSkill; } int Weapon::getValue (const MWWorld::ConstPtr& ptr) const { const MWWorld::LiveCellRef *ref = ptr.get(); return ref->mBase->mData.mValue; } void Weapon::registerSelf() { std::shared_ptr instance (new Weapon); registerClass (typeid (ESM::Weapon).name(), instance); } std::string Weapon::getUpSoundId (const MWWorld::ConstPtr& ptr) const { const MWWorld::LiveCellRef *ref = ptr.get(); int type = ref->mBase->mData.mType; std::string soundId = MWMechanics::getWeaponType(type)->mSoundId; return soundId + " Up"; } std::string Weapon::getDownSoundId (const MWWorld::ConstPtr& ptr) const { const MWWorld::LiveCellRef *ref = ptr.get(); int type = ref->mBase->mData.mType; std::string soundId = MWMechanics::getWeaponType(type)->mSoundId; return soundId + " Down"; } std::string Weapon::getInventoryIcon (const MWWorld::ConstPtr& ptr) const { const MWWorld::LiveCellRef *ref = ptr.get(); return ref->mBase->mIcon; } MWGui::ToolTipInfo Weapon::getToolTipInfo (const MWWorld::ConstPtr& ptr, int count) const { const MWWorld::LiveCellRef *ref = ptr.get(); const ESM::WeaponType* weaponType = MWMechanics::getWeaponType(ref->mBase->mData.mType); MWGui::ToolTipInfo info; info.caption = MyGUI::TextIterator::toTagsString(getName(ptr)) + MWGui::ToolTips::getCountString(count); info.icon = ref->mBase->mIcon; const MWWorld::ESMStore& store = MWBase::Environment::get().getWorld()->getStore(); std::string text; // weapon type & damage if (weaponType->mWeaponClass != ESM::WeaponType::Ammo || Settings::Manager::getBool("show projectile damage", "Game")) { text += "\n#{sType} "; int skill = MWMechanics::getWeaponType(ref->mBase->mData.mType)->mSkill; const std::string type = ESM::Skill::sSkillNameIds[skill]; std::string oneOrTwoHanded; if (weaponType->mWeaponClass == ESM::WeaponType::Melee) { if (weaponType->mFlags & ESM::WeaponType::TwoHanded) oneOrTwoHanded = "sTwoHanded"; else oneOrTwoHanded = "sOneHanded"; } text += store.get().find(type)->mValue.getString() + ((oneOrTwoHanded != "") ? ", " + store.get().find(oneOrTwoHanded)->mValue.getString() : ""); // weapon damage if (weaponType->mWeaponClass == ESM::WeaponType::Thrown) { // Thrown weapons have 2x real damage applied // as they're both the weapon and the ammo text += "\n#{sAttack}: " + MWGui::ToolTips::toString(static_cast(ref->mBase->mData.mChop[0] * 2)) + " - " + MWGui::ToolTips::toString(static_cast(ref->mBase->mData.mChop[1] * 2)); } else if (weaponType->mWeaponClass == ESM::WeaponType::Melee) { // Chop text += "\n#{sChop}: " + MWGui::ToolTips::toString(static_cast(ref->mBase->mData.mChop[0])) + " - " + MWGui::ToolTips::toString(static_cast(ref->mBase->mData.mChop[1])); // Slash text += "\n#{sSlash}: " + MWGui::ToolTips::toString(static_cast(ref->mBase->mData.mSlash[0])) + " - " + MWGui::ToolTips::toString(static_cast(ref->mBase->mData.mSlash[1])); // Thrust text += "\n#{sThrust}: " + MWGui::ToolTips::toString(static_cast(ref->mBase->mData.mThrust[0])) + " - " + MWGui::ToolTips::toString(static_cast(ref->mBase->mData.mThrust[1])); } else { // marksman text += "\n#{sAttack}: " + MWGui::ToolTips::toString(static_cast(ref->mBase->mData.mChop[0])) + " - " + MWGui::ToolTips::toString(static_cast(ref->mBase->mData.mChop[1])); } } if (hasItemHealth(ptr)) { int remainingHealth = getItemHealth(ptr); text += "\n#{sCondition}: " + MWGui::ToolTips::toString(remainingHealth) + "/" + MWGui::ToolTips::toString(ref->mBase->mData.mHealth); } const bool verbose = Settings::Manager::getBool("show melee info", "Game"); // add reach for melee weapon if (weaponType->mWeaponClass == ESM::WeaponType::Melee && verbose) { // display value in feet const float combatDistance = store.get().find("fCombatDistance")->mValue.getFloat() * ref->mBase->mData.mReach; text += MWGui::ToolTips::getWeightString(combatDistance / Constants::UnitsPerFoot, "#{sRange}"); text += " #{sFeet}"; } // add attack speed for any weapon excepts arrows and bolts if (weaponType->mWeaponClass != ESM::WeaponType::Ammo && verbose) { text += MWGui::ToolTips::getPercentString(ref->mBase->mData.mSpeed, "#{sAttributeSpeed}"); } text += MWGui::ToolTips::getWeightString(ref->mBase->mData.mWeight, "#{sWeight}"); text += MWGui::ToolTips::getValueString(ref->mBase->mData.mValue, "#{sValue}"); info.enchant = ref->mBase->mEnchant; if (!info.enchant.empty()) info.remainingEnchantCharge = static_cast(ptr.getCellRef().getEnchantmentCharge()); if (MWBase::Environment::get().getWindowManager()->getFullHelp()) { text += MWGui::ToolTips::getCellRefString(ptr.getCellRef()); text += MWGui::ToolTips::getMiscString(ref->mBase->mScript, "Script"); } info.text = text; return info; } std::string Weapon::getEnchantment (const MWWorld::ConstPtr& ptr) const { const MWWorld::LiveCellRef *ref = ptr.get(); return ref->mBase->mEnchant; } std::string Weapon::applyEnchantment(const MWWorld::ConstPtr &ptr, const std::string& enchId, int enchCharge, const std::string& newName) const { const MWWorld::LiveCellRef *ref = ptr.get(); ESM::Weapon newItem = *ref->mBase; newItem.mId=""; newItem.mName=newName; newItem.mData.mEnchant=enchCharge; newItem.mEnchant=enchId; newItem.mData.mFlags |= ESM::Weapon::Magical; /* Start of tes3mp addition Send the newly created record to the server and expect it to be returned with a server-set id */ unsigned int quantity = mwmp::Main::get().getLocalPlayer()->lastEnchantmentQuantity; mwmp::Main::get().getNetworking()->getWorldstate()->sendWeaponRecord(&newItem, ref->mBase->mId, quantity); /* End of tes3mp addition */ const ESM::Weapon *record = MWBase::Environment::get().getWorld()->createRecord (newItem); return record->mId; } std::pair Weapon::canBeEquipped(const MWWorld::ConstPtr &ptr, const MWWorld::Ptr &npc) const { if (hasItemHealth(ptr) && getItemHealth(ptr) == 0) return std::make_pair(0, "#{sInventoryMessage1}"); // Do not allow equip weapons from inventory during attack if (MWBase::Environment::get().getMechanicsManager()->isAttackingOrSpell(npc) && MWBase::Environment::get().getWindowManager()->isGuiMode()) return std::make_pair(0, "#{sCantEquipWeapWarning}"); std::pair, bool> slots_ = getEquipmentSlots(ptr); if (slots_.first.empty()) return std::make_pair (0, ""); int type = ptr.get()->mBase->mData.mType; if(MWMechanics::getWeaponType(type)->mFlags & ESM::WeaponType::TwoHanded) { return std::make_pair (2, ""); } return std::make_pair(1, ""); } std::shared_ptr Weapon::use (const MWWorld::Ptr& ptr, bool force) const { std::shared_ptr action(new MWWorld::ActionEquip(ptr, force)); action->setSound(getUpSoundId(ptr)); return action; } MWWorld::Ptr Weapon::copyToCellImpl(const MWWorld::ConstPtr &ptr, MWWorld::CellStore &cell) const { const MWWorld::LiveCellRef *ref = ptr.get(); return MWWorld::Ptr(cell.insert(ref), &cell); } int Weapon::getEnchantmentPoints (const MWWorld::ConstPtr& ptr) const { const MWWorld::LiveCellRef *ref = ptr.get(); return ref->mBase->mData.mEnchant; } bool Weapon::canSell (const MWWorld::ConstPtr& item, int npcServices) const { return (npcServices & ESM::NPC::Weapon) || ((npcServices & ESM::NPC::MagicItems) && !getEnchantment(item).empty()); } float Weapon::getWeight(const MWWorld::ConstPtr &ptr) const { const MWWorld::LiveCellRef *ref = ptr.get(); return ref->mBase->mData.mWeight; } } ================================================ FILE: apps/openmw/mwclass/weapon.hpp ================================================ #ifndef GAME_MWCLASS_WEAPON_H #define GAME_MWCLASS_WEAPON_H #include "../mwworld/class.hpp" namespace MWClass { class Weapon : public MWWorld::Class { MWWorld::Ptr copyToCellImpl(const MWWorld::ConstPtr &ptr, MWWorld::CellStore &cell) const override; public: void insertObjectRendering (const MWWorld::Ptr& ptr, const std::string& model, MWRender::RenderingInterface& renderingInterface) const override; ///< Add reference into a cell for rendering void insertObject(const MWWorld::Ptr& ptr, const std::string& model, MWPhysics::PhysicsSystem& physics) const override; std::string getName (const MWWorld::ConstPtr& ptr) const override; ///< \return name or ID; can return an empty string. std::shared_ptr activate (const MWWorld::Ptr& ptr, const MWWorld::Ptr& actor) const override; ///< Generate action for activation MWGui::ToolTipInfo getToolTipInfo (const MWWorld::ConstPtr& ptr, int count) const override; ///< @return the content of the tool tip to be displayed. raises exception if the object has no tooltip. bool hasItemHealth (const MWWorld::ConstPtr& ptr) const override; ///< \return Item health data available? int getItemMaxHealth (const MWWorld::ConstPtr& ptr) const override; ///< Return item max health or throw an exception, if class does not have item health std::string getScript (const MWWorld::ConstPtr& ptr) const override; ///< Return name of the script attached to ptr std::pair, bool> getEquipmentSlots (const MWWorld::ConstPtr& ptr) const override; ///< \return first: Return IDs of the slot this object can be equipped in; second: can object /// stay stacked when equipped? int getEquipmentSkill (const MWWorld::ConstPtr& ptr) const override; /// Return the index of the skill this item corresponds to when equipped or -1, if there is /// no such skill. int getValue (const MWWorld::ConstPtr& ptr) const override; ///< Return trade value of the object. Throws an exception, if the object can't be traded. static void registerSelf(); std::string getUpSoundId (const MWWorld::ConstPtr& ptr) const override; ///< Return the pick up sound Id std::string getDownSoundId (const MWWorld::ConstPtr& ptr) const override; ///< Return the put down sound Id std::string getInventoryIcon (const MWWorld::ConstPtr& ptr) const override; ///< Return name of inventory icon. std::string getEnchantment (const MWWorld::ConstPtr& ptr) const override; ///< @return the enchantment ID if the object is enchanted, otherwise an empty string std::string applyEnchantment(const MWWorld::ConstPtr &ptr, const std::string& enchId, int enchCharge, const std::string& newName) const override; ///< Creates a new record using \a ptr as template, with the given name and the given enchantment applied to it. std::pair canBeEquipped(const MWWorld::ConstPtr &ptr, const MWWorld::Ptr &npc) const override; ///< Return 0 if player cannot equip item. 1 if can equip. 2 if it's twohanded weapon. 3 if twohanded weapon conflicts with that. /// Second item in the pair specifies the error message std::shared_ptr use (const MWWorld::Ptr& ptr, bool force=false) const override; ///< Generate action for using via inventory menu std::string getModel(const MWWorld::ConstPtr &ptr) const override; bool canSell (const MWWorld::ConstPtr& item, int npcServices) const override; float getWeight (const MWWorld::ConstPtr& ptr) const override; int getEnchantmentPoints (const MWWorld::ConstPtr& ptr) const override; }; } #endif ================================================ FILE: apps/openmw/mwdialogue/dialoguemanagerimp.cpp ================================================ #include "dialoguemanagerimp.hpp" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include /* Start of tes3mp addition Include additional headers for multiplayer purposes */ #include "../mwmp/Main.hpp" #include "../mwmp/CellController.hpp" #include "../mwmp/LocalPlayer.hpp" #include "../mwmp/LocalActor.hpp" /* End of tes3mp addition */ #include "../mwbase/environment.hpp" #include "../mwbase/world.hpp" #include "../mwbase/journal.hpp" #include "../mwbase/scriptmanager.hpp" #include "../mwbase/windowmanager.hpp" #include "../mwbase/mechanicsmanager.hpp" #include "../mwbase/soundmanager.hpp" #include "../mwworld/class.hpp" #include "../mwworld/containerstore.hpp" #include "../mwworld/esmstore.hpp" #include "../mwscript/compilercontext.hpp" #include "../mwscript/interpretercontext.hpp" #include "../mwscript/extensions.hpp" #include "../mwmechanics/creaturestats.hpp" #include "../mwmechanics/npcstats.hpp" #include "../mwmechanics/actorutil.hpp" #include "filter.hpp" #include "hypertextparser.hpp" namespace MWDialogue { DialogueManager::DialogueManager (const Compiler::Extensions& extensions, Translation::Storage& translationDataStorage) : mTranslationDataStorage(translationDataStorage) , mCompilerContext (MWScript::CompilerContext::Type_Dialogue) , mErrorHandler() , mTalkedTo(false) , mTemporaryDispositionChange(0.f) , mPermanentDispositionChange(0.f) { mChoice = -1; mIsInChoice = false; mGoodbye = false; mCompilerContext.setExtensions (&extensions); } void DialogueManager::clear() { mKnownTopics.clear(); mTalkedTo = false; mTemporaryDispositionChange = 0; mPermanentDispositionChange = 0; } void DialogueManager::addTopic (const std::string& topic) { mKnownTopics.insert( Misc::StringUtils::lowerCase(topic) ); } /* Start of tes3mp addition Make it possible to check whether a topic is known by the player from elsewhere in the code */ bool DialogueManager::isNewTopic(const std::string& topic) { return (!mKnownTopics.count(topic)); } /* End of tes3mp addition */ void DialogueManager::parseText (const std::string& text) { updateActorKnownTopics(); std::vector hypertext = HyperTextParser::parseHyperText(text); for (std::vector::iterator tok = hypertext.begin(); tok != hypertext.end(); ++tok) { std::string topicId = Misc::StringUtils::lowerCase(tok->mText); if (tok->isExplicitLink()) { // calculation of standard form for all hyperlinks size_t asterisk_count = HyperTextParser::removePseudoAsterisks(topicId); for(; asterisk_count > 0; --asterisk_count) topicId.append("*"); topicId = mTranslationDataStorage.topicStandardForm(topicId); } /* Start of tes3mp addition Send an ID_PLAYER_TOPIC packet every time a new topic becomes known */ if (mActorKnownTopics.count(topicId) && isNewTopic(topicId)) mwmp::Main::get().getLocalPlayer()->sendTopic(topicId); /* End of tes3mp addition */ if (mActorKnownTopics.count( topicId )) mKnownTopics.insert( topicId ); } } bool DialogueManager::startDialogue (const MWWorld::Ptr& actor, ResponseCallback* callback) { updateGlobals(); // Dialogue with dead actor (e.g. through script) should not be allowed. if (actor.getClass().getCreatureStats(actor).isDead()) return false; mLastTopic = ""; mPermanentDispositionChange = 0; mTemporaryDispositionChange = 0; mChoice = -1; mIsInChoice = false; mGoodbye = false; mChoices.clear(); mActor = actor; MWMechanics::CreatureStats& creatureStats = actor.getClass().getCreatureStats (actor); mTalkedTo = creatureStats.hasTalkedToPlayer(); mActorKnownTopics.clear(); mActorKnownTopicsFlag.clear(); //greeting const MWWorld::Store &dialogs = MWBase::Environment::get().getWorld()->getStore().get(); Filter filter (actor, mChoice, mTalkedTo); for (MWWorld::Store::iterator it = dialogs.begin(); it != dialogs.end(); ++it) { if(it->mType == ESM::Dialogue::Greeting) { // Search a response (we do not accept a fallback to "Info refusal" here) if (const ESM::DialInfo *info = filter.search (*it, false)) { creatureStats.talkedToPlayer(); if (!info->mSound.empty()) { // TODO play sound } MWScript::InterpreterContext interpreterContext(&mActor.getRefData().getLocals(),mActor); callback->addResponse("", Interpreter::fixDefinesDialog(info->mResponse, interpreterContext)); executeScript (info->mResultScript, mActor); mLastTopic = it->mId; parseText (info->mResponse); return true; } } } return false; } bool DialogueManager::compile (const std::string& cmd, std::vector& code, const MWWorld::Ptr& actor) { bool success = true; try { mErrorHandler.reset(); mErrorHandler.setContext("[dialogue script]"); std::istringstream input (cmd + "\n"); Compiler::Scanner scanner (mErrorHandler, input, mCompilerContext.getExtensions()); Compiler::Locals locals; std::string actorScript = actor.getClass().getScript (actor); if (!actorScript.empty()) { // grab local variables from actor's script, if available. locals = MWBase::Environment::get().getScriptManager()->getLocals (actorScript); } Compiler::ScriptParser parser(mErrorHandler,mCompilerContext, locals, false); scanner.scan (parser); if (!mErrorHandler.isGood()) success = false; if (success) parser.getCode (code); } catch (const Compiler::SourceException& /* error */) { // error has already been reported via error handler success = false; } catch (const std::exception& error) { Log(Debug::Error) << std::string ("Dialogue error: An exception has been thrown: ") + error.what(); success = false; } if (!success) { Log(Debug::Error) << "Error: compiling failed (dialogue script): \n" << cmd << "\n"; } return success; } void DialogueManager::executeScript (const std::string& script, const MWWorld::Ptr& actor) { std::vector code; if(compile(script, code, actor)) { try { MWScript::InterpreterContext interpreterContext(&actor.getRefData().getLocals(), actor); /* Start of tes3mp addition Mark this InterpreterContext as having a DIALOGUE context, so that packets sent by the Interpreter can have their origin determined by serverside scripts */ interpreterContext.trackContextType(Interpreter::Context::DIALOGUE); /* End of tes3mp addition */ Interpreter::Interpreter interpreter; MWScript::installOpcodes (interpreter); interpreter.run (&code[0], code.size(), interpreterContext); } catch (const std::exception& error) { Log(Debug::Error) << std::string ("Dialogue error: An exception has been thrown: ") + error.what(); } } } bool DialogueManager::inJournal (const std::string& topicId, const std::string& infoId) { const MWDialogue::Topic *topicHistory = nullptr; MWBase::Journal *journal = MWBase::Environment::get().getJournal(); for (auto it = journal->topicBegin(); it != journal->topicEnd(); ++it) { if (it->first == topicId) { topicHistory = &it->second; break; } } if (!topicHistory) return false; for(const auto& topic : *topicHistory) { if (topic.mInfoId == infoId) return true; } return false; } void DialogueManager::executeTopic (const std::string& topic, ResponseCallback* callback) { Filter filter (mActor, mChoice, mTalkedTo); const MWWorld::Store &dialogues = MWBase::Environment::get().getWorld()->getStore().get(); const ESM::Dialogue& dialogue = *dialogues.find (topic); const ESM::DialInfo* info = filter.search(dialogue, true); if (info) { std::string title; if (dialogue.mType==ESM::Dialogue::Persuasion) { // Determine GMST from dialogue topic. GMSTs are: // sAdmireSuccess, sAdmireFail, sIntimidateSuccess, sIntimidateFail, // sTauntSuccess, sTauntFail, sBribeSuccess, sBribeFail std::string modifiedTopic = "s" + topic; modifiedTopic.erase (std::remove (modifiedTopic.begin(), modifiedTopic.end(), ' '), modifiedTopic.end()); const MWWorld::Store& gmsts = MWBase::Environment::get().getWorld()->getStore().get(); title = gmsts.find (modifiedTopic)->mValue.getString(); } else title = topic; MWScript::InterpreterContext interpreterContext(&mActor.getRefData().getLocals(),mActor); callback->addResponse(title, Interpreter::fixDefinesDialog(info->mResponse, interpreterContext)); if (dialogue.mType == ESM::Dialogue::Topic) { // Make sure the returned DialInfo is from the Dialogue we supplied. If could also be from the Info refusal group, // in which case it should not be added to the journal. for (ESM::Dialogue::InfoContainer::const_iterator iter = dialogue.mInfo.begin(); iter!=dialogue.mInfo.end(); ++iter) { if (iter->mId == info->mId) { MWBase::Environment::get().getJournal()->addTopic (Misc::StringUtils::lowerCase(topic), info->mId, mActor); break; } } } mLastTopic = topic; executeScript (info->mResultScript, mActor); parseText (info->mResponse); } } const ESM::Dialogue *DialogueManager::searchDialogue(const std::string& id) { return MWBase::Environment::get().getWorld()->getStore().get().search(id); } void DialogueManager::updateGlobals() { MWBase::Environment::get().getWorld()->updateDialogueGlobals(); } void DialogueManager::updateActorKnownTopics() { updateGlobals(); mActorKnownTopics.clear(); mActorKnownTopicsFlag.clear(); const auto& dialogs = MWBase::Environment::get().getWorld()->getStore().get(); Filter filter (mActor, -1, mTalkedTo); for (const auto& dialog : dialogs) { if (dialog.mType == ESM::Dialogue::Topic) { const auto* answer = filter.search(dialog, true); auto topicId = Misc::StringUtils::lowerCase(dialog.mId); if (answer != nullptr) { int flag = 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; } else flag |= MWBase::DialogueManager::TopicType::Exhausted; mActorKnownTopics.insert (dialog.mId); mActorKnownTopicsFlag[dialog.mId] = flag; } } } } std::list DialogueManager::getAvailableTopics() { updateActorKnownTopics(); std::list keywordList; for (const std::string& topic : mActorKnownTopics) { //does the player know the topic? if (mKnownTopics.count(topic)) keywordList.push_back(topic); } // sort again, because the previous sort was case-sensitive keywordList.sort(Misc::StringUtils::ciLess); return keywordList; } int DialogueManager::getTopicFlag(const std::string& topicId) { return mActorKnownTopicsFlag[topicId]; } void DialogueManager::keywordSelected (const std::string& keyword, ResponseCallback* callback) { if(!mIsInChoice) { const ESM::Dialogue* dialogue = searchDialogue(keyword); if (dialogue && dialogue->mType == ESM::Dialogue::Topic) { executeTopic (keyword, callback); } } } bool DialogueManager::isInChoice() const { return mIsInChoice; } void DialogueManager::goodbyeSelected() { // Apply disposition change to NPC's base disposition if (mActor.getClass().isNpc()) { // Clamp permanent disposition change so that final disposition doesn't go below 0 (could happen with intimidate) float curDisp = static_cast(MWBase::Environment::get().getMechanicsManager()->getDerivedDisposition(mActor, false)); if (curDisp + mPermanentDispositionChange < 0) mPermanentDispositionChange = -curDisp; MWMechanics::NpcStats& npcStats = mActor.getClass().getNpcStats(mActor); npcStats.setBaseDisposition(static_cast(npcStats.getBaseDisposition() + mPermanentDispositionChange)); } mPermanentDispositionChange = 0; mTemporaryDispositionChange = 0; } void DialogueManager::questionAnswered (int answer, ResponseCallback* callback) { mChoice = answer; const ESM::Dialogue* dialogue = searchDialogue(mLastTopic); if (dialogue) { Filter filter (mActor, mChoice, mTalkedTo); if (dialogue->mType == ESM::Dialogue::Topic || dialogue->mType == ESM::Dialogue::Greeting) { if (const ESM::DialInfo *info = filter.search (*dialogue, true)) { std::string text = info->mResponse; parseText (text); mChoice = -1; mIsInChoice = false; mChoices.clear(); MWScript::InterpreterContext interpreterContext(&mActor.getRefData().getLocals(),mActor); callback->addResponse("", Interpreter::fixDefinesDialog(text, interpreterContext)); if (dialogue->mType == ESM::Dialogue::Topic) { // Make sure the returned DialInfo is from the Dialogue we supplied. If could also be from the Info refusal group, // in which case it should not be added to the journal for (ESM::Dialogue::InfoContainer::const_iterator iter = dialogue->mInfo.begin(); iter!=dialogue->mInfo.end(); ++iter) { if (iter->mId == info->mId) { MWBase::Environment::get().getJournal()->addTopic (Misc::StringUtils::lowerCase(mLastTopic), info->mId, mActor); break; } } } executeScript (info->mResultScript, mActor); } else { mChoice = -1; mIsInChoice = false; mChoices.clear(); } } } updateActorKnownTopics(); } void DialogueManager::addChoice (const std::string& text, int choice) { mIsInChoice = true; mChoices.emplace_back(text, choice); } const std::vector >& DialogueManager::getChoices() { return mChoices; } bool DialogueManager::isGoodbye() { return mGoodbye; } void DialogueManager::goodbye() { mIsInChoice = false; mGoodbye = true; } void DialogueManager::persuade(int type, ResponseCallback* callback) { bool success; float temp, perm; MWBase::Environment::get().getMechanicsManager()->getPersuasionDispositionChange( mActor, MWBase::MechanicsManager::PersuasionType(type), success, temp, perm); mTemporaryDispositionChange += temp; mPermanentDispositionChange += perm; // change temp disposition so that final disposition is between 0...100 float curDisp = static_cast(MWBase::Environment::get().getMechanicsManager()->getDerivedDisposition(mActor, false)); if (curDisp + mTemporaryDispositionChange < 0) mTemporaryDispositionChange = -curDisp; else if (curDisp + mTemporaryDispositionChange > 100) mTemporaryDispositionChange = 100 - curDisp; MWWorld::Ptr player = MWMechanics::getPlayer(); player.getClass().skillUsageSucceeded(player, ESM::Skill::Speechcraft, success ? 0 : 1); if (success) { int gold=0; if (type == MWBase::MechanicsManager::PT_Bribe10) gold = 10; else if (type == MWBase::MechanicsManager::PT_Bribe100) gold = 100; else if (type == MWBase::MechanicsManager::PT_Bribe1000) gold = 1000; if (gold) { player.getClass().getContainerStore(player).remove(MWWorld::ContainerStore::sGoldId, gold, player); mActor.getClass().getContainerStore(mActor).add(MWWorld::ContainerStore::sGoldId, gold, mActor); } } std::string text; if (type == MWBase::MechanicsManager::PT_Admire) text = "Admire"; else if (type == MWBase::MechanicsManager::PT_Taunt) text = "Taunt"; else if (type == MWBase::MechanicsManager::PT_Intimidate) text = "Intimidate"; else{ text = "Bribe"; } executeTopic (text + (success ? " Success" : " Fail"), callback); } int DialogueManager::getTemporaryDispositionChange() const { return static_cast(mTemporaryDispositionChange); } void DialogueManager::applyBarterDispositionChange(int delta) { mTemporaryDispositionChange += delta; if (Settings::Manager::getBool("barter disposition change is permanent", "Game")) mPermanentDispositionChange += delta; } bool DialogueManager::checkServiceRefused(ResponseCallback* callback, ServiceType service) { Filter filter (mActor, service, mTalkedTo); const MWWorld::Store &dialogues = MWBase::Environment::get().getWorld()->getStore().get(); const ESM::Dialogue& dialogue = *dialogues.find ("Service Refusal"); std::vector infos = filter.list (dialogue, false, false, true); if (!infos.empty()) { const ESM::DialInfo* info = infos[0]; parseText (info->mResponse); const MWWorld::Store& gmsts = MWBase::Environment::get().getWorld()->getStore().get(); MWScript::InterpreterContext interpreterContext(&mActor.getRefData().getLocals(),mActor); callback->addResponse(gmsts.find ("sServiceRefusal")->mValue.getString(), Interpreter::fixDefinesDialog(info->mResponse, interpreterContext)); executeScript (info->mResultScript, mActor); return true; } return false; } void DialogueManager::say(const MWWorld::Ptr &actor, const std::string &topic) { MWBase::SoundManager *sndMgr = MWBase::Environment::get().getSoundManager(); if(sndMgr->sayActive(actor)) { // Actor is already saying something. return; } if (actor.getClass().isNpc() && MWBase::Environment::get().getWorld()->isSwimming(actor)) { // NPCs don't talk while submerged return; } if (actor.getClass().getCreatureStats(actor).getKnockedDown()) { // Unconscious actors can not speak return; } const MWWorld::ESMStore &store = MWBase::Environment::get().getWorld()->getStore(); const ESM::Dialogue *dial = store.get().find(topic); const MWMechanics::CreatureStats& creatureStats = actor.getClass().getCreatureStats(actor); Filter filter(actor, 0, creatureStats.hasTalkedToPlayer()); const ESM::DialInfo *info = filter.search(*dial, false); if(info != nullptr) { MWBase::WindowManager *winMgr = MWBase::Environment::get().getWindowManager(); if (winMgr->getSubtitlesEnabled()) /* Start of tes3mp change (minor) Prevent subtitles for NPC sounds from being added to a currently open dialogue window, which wasn't a problem in regular OpenMW because time was frozen during dialogue */ winMgr->messageBox(info->mResponse, MWGui::ShowInDialogueMode_Never); /* End of tes3mp change (minor) */ if (!info->mSound.empty()) sndMgr->say(actor, info->mSound); if (!info->mResultScript.empty()) executeScript(info->mResultScript, actor); /* Start of tes3mp addition If we are the cell authority over this actor, we need to record this new sound for it */ if (mwmp::Main::get().getCellController()->isLocalActor(actor)) { mwmp::LocalActor *localActor = mwmp::Main::get().getCellController()->getLocalActor(actor); localActor->sound = info->mSound; } /* End of tes3mp addition */ } } int DialogueManager::countSavedGameRecords() const { return 1; // known topics } void DialogueManager::write (ESM::ESMWriter& writer, Loading::Listener& progress) const { ESM::DialogueState state; for (std::set::const_iterator iter (mKnownTopics.begin()); iter!=mKnownTopics.end(); ++iter) { state.mKnownTopics.push_back (*iter); } state.mChangedFactionReaction = mChangedFactionReaction; writer.startRecord (ESM::REC_DIAS); state.save (writer); writer.endRecord (ESM::REC_DIAS); } void DialogueManager::readRecord (ESM::ESMReader& reader, uint32_t type) { if (type==ESM::REC_DIAS) { const MWWorld::ESMStore& store = MWBase::Environment::get().getWorld()->getStore(); ESM::DialogueState state; state.load (reader); for (std::vector::const_iterator iter (state.mKnownTopics.begin()); iter!=state.mKnownTopics.end(); ++iter) if (store.get().search (*iter)) mKnownTopics.insert (*iter); mChangedFactionReaction = state.mChangedFactionReaction; } } void DialogueManager::modFactionReaction(const std::string &faction1, const std::string &faction2, int diff) { std::string fact1 = Misc::StringUtils::lowerCase(faction1); std::string fact2 = Misc::StringUtils::lowerCase(faction2); // Make sure the factions exist MWBase::Environment::get().getWorld()->getStore().get().find(fact1); MWBase::Environment::get().getWorld()->getStore().get().find(fact2); int newValue = getFactionReaction(faction1, faction2) + diff; std::map& map = mChangedFactionReaction[fact1]; map[fact2] = newValue; } void DialogueManager::setFactionReaction(const std::string &faction1, const std::string &faction2, int absolute) { std::string fact1 = Misc::StringUtils::lowerCase(faction1); std::string fact2 = Misc::StringUtils::lowerCase(faction2); // Make sure the factions exist MWBase::Environment::get().getWorld()->getStore().get().find(fact1); MWBase::Environment::get().getWorld()->getStore().get().find(fact2); std::map& map = mChangedFactionReaction[fact1]; map[fact2] = absolute; } int DialogueManager::getFactionReaction(const std::string &faction1, const std::string &faction2) const { std::string fact1 = Misc::StringUtils::lowerCase(faction1); std::string fact2 = Misc::StringUtils::lowerCase(faction2); ModFactionReactionMap::const_iterator map = mChangedFactionReaction.find(fact1); if (map != mChangedFactionReaction.end() && map->second.find(fact2) != map->second.end()) return map->second.at(fact2); const ESM::Faction* faction = MWBase::Environment::get().getWorld()->getStore().get().find(fact1); std::map::const_iterator it = faction->mReactions.begin(); for (; it != faction->mReactions.end(); ++it) { if (Misc::StringUtils::ciEqual(it->first, fact2)) return it->second; } return 0; } void DialogueManager::clearInfoActor(const MWWorld::Ptr &actor) const { if (actor == mActor && !mLastTopic.empty()) { MWBase::Environment::get().getJournal()->removeLastAddedTopicResponse( Misc::StringUtils::lowerCase(mLastTopic), actor.getClass().getName(actor)); } } /* Start of tes3mp addition Make it possible to get the caption of a voice dialogue */ std::string DialogueManager::getVoiceCaption(const std::string& sound) const { const MWWorld::Store& dialogues = MWBase::Environment::get().getWorld()->getStore().get(); for (MWWorld::Store::iterator dialogueIter = dialogues.begin(); dialogueIter != dialogues.end(); ++dialogueIter) { if (dialogueIter->mType == ESM::Dialogue::Voice) { for (ESM::Dialogue::InfoContainer::const_iterator infoIter = dialogueIter->mInfo.begin(); infoIter != dialogueIter->mInfo.end(); ++infoIter) { if (!infoIter->mSound.empty() && Misc::StringUtils::ciEqual(sound, infoIter->mSound)) return infoIter->mResponse; } } } return "???"; } /* End of tes3mp addition */ } ================================================ FILE: apps/openmw/mwdialogue/dialoguemanagerimp.hpp ================================================ #ifndef GAME_MWDIALOG_DIALOGUEMANAGERIMP_H #define GAME_MWDIALOG_DIALOGUEMANAGERIMP_H #include "../mwbase/dialoguemanager.hpp" #include #include #include #include #include #include #include "../mwworld/ptr.hpp" #include "../mwscript/compilercontext.hpp" namespace ESM { struct Dialogue; } namespace MWDialogue { class DialogueManager : public MWBase::DialogueManager { std::set mKnownTopics;// Those are the topics the player knows. // Modified faction reactions. > typedef std::map > ModFactionReactionMap; ModFactionReactionMap mChangedFactionReaction; std::set mActorKnownTopics; std::unordered_map mActorKnownTopicsFlag; Translation::Storage& mTranslationDataStorage; MWScript::CompilerContext mCompilerContext; Compiler::StreamErrorHandler mErrorHandler; MWWorld::Ptr mActor; bool mTalkedTo; int mChoice; std::string mLastTopic; // last topic ID, lowercase bool mIsInChoice; bool mGoodbye; std::vector > mChoices; float mTemporaryDispositionChange; float mPermanentDispositionChange; void parseText (const std::string& text); void updateActorKnownTopics(); void updateGlobals(); bool compile (const std::string& cmd, std::vector& code, const MWWorld::Ptr& actor); void executeScript (const std::string& script, const MWWorld::Ptr& actor); void executeTopic (const std::string& topic, ResponseCallback* callback); const ESM::Dialogue* searchDialogue(const std::string& id); public: DialogueManager (const Compiler::Extensions& extensions, Translation::Storage& translationDataStorage); void clear() override; bool isInChoice() const override; bool startDialogue (const MWWorld::Ptr& actor, ResponseCallback* callback) override; std::list getAvailableTopics() override; int getTopicFlag(const std::string& topicId) override; bool inJournal (const std::string& topicId, const std::string& infoId) override; void addTopic (const std::string& topic) override; /* Start of tes3mp addition Make it possible to check whether a topic is known by the player from elsewhere in the code */ virtual bool isNewTopic(const std::string& topic); /* End of tes3mp addition */ void addChoice (const std::string& text,int choice) override; const std::vector >& getChoices() override; bool isGoodbye() override; void goodbye() override; bool checkServiceRefused (ResponseCallback* callback, ServiceType service = ServiceType::Any) override; void say(const MWWorld::Ptr &actor, const std::string &topic) override; //calbacks for the GUI void keywordSelected (const std::string& keyword, ResponseCallback* callback) override; void goodbyeSelected() override; void questionAnswered (int answer, ResponseCallback* callback) override; void persuade (int type, ResponseCallback* callback) override; int getTemporaryDispositionChange () const override; /// @note Controlled by an option, gets discarded when dialogue ends by default void applyBarterDispositionChange (int delta) override; int countSavedGameRecords() const override; void write (ESM::ESMWriter& writer, Loading::Listener& progress) const override; void readRecord (ESM::ESMReader& reader, uint32_t type) override; /// Changes faction1's opinion of faction2 by \a diff. void modFactionReaction (const std::string& faction1, const std::string& faction2, int diff) override; void setFactionReaction (const std::string& faction1, const std::string& faction2, int absolute) override; /// @return faction1's opinion of faction2 int getFactionReaction (const std::string& faction1, const std::string& faction2) const override; /// Removes the last added topic response for the given actor from the journal void clearInfoActor(const MWWorld::Ptr & actor) const override; /* Start of tes3mp addition Make it possible to get the caption of a voice dialogue */ virtual std::string getVoiceCaption(const std::string& sound) const; /* End of tes3mp addition */ }; } #endif ================================================ FILE: apps/openmw/mwdialogue/filter.cpp ================================================ #include "filter.hpp" #include #include "../mwbase/environment.hpp" #include "../mwbase/world.hpp" #include "../mwbase/journal.hpp" #include "../mwbase/mechanicsmanager.hpp" #include "../mwbase/dialoguemanager.hpp" #include "../mwbase/scriptmanager.hpp" #include "../mwworld/class.hpp" #include "../mwworld/inventorystore.hpp" #include "../mwworld/cellstore.hpp" #include "../mwworld/esmstore.hpp" #include "../mwmechanics/npcstats.hpp" #include "../mwmechanics/creaturestats.hpp" #include "../mwmechanics/magiceffects.hpp" #include "../mwmechanics/actorutil.hpp" #include "selectwrapper.hpp" bool MWDialogue::Filter::testActor (const ESM::DialInfo& info) const { bool isCreature = (mActor.getTypeName() != typeid (ESM::NPC).name()); // actor id if (!info.mActor.empty()) { if ( !Misc::StringUtils::ciEqual(info.mActor, mActor.getCellRef().getRefId())) return false; } else if (isCreature) { // Creatures must not have topics aside of those specific to their id return false; } // NPC race if (!info.mRace.empty()) { if (isCreature) return true; MWWorld::LiveCellRef *cellRef = mActor.get(); if (!Misc::StringUtils::ciEqual(info.mRace, cellRef->mBase->mRace)) return false; } // NPC class if (!info.mClass.empty()) { if (isCreature) return true; MWWorld::LiveCellRef *cellRef = mActor.get(); if ( !Misc::StringUtils::ciEqual(info.mClass, cellRef->mBase->mClass)) return false; } // NPC faction if (info.mFactionLess) { if (isCreature) return true; if (!mActor.getClass().getPrimaryFaction(mActor).empty()) return false; } else if (!info.mFaction.empty()) { if (isCreature) return true; if (!Misc::StringUtils::ciEqual(mActor.getClass().getPrimaryFaction(mActor), info.mFaction)) return false; // check rank if (mActor.getClass().getPrimaryFactionRank(mActor) < info.mData.mRank) return false; } else if (info.mData.mRank != -1) { if (isCreature) return true; // Rank requirement, but no faction given. Use the actor's faction, if there is one. // check rank if (mActor.getClass().getPrimaryFactionRank(mActor) < info.mData.mRank) return false; } // Gender if (!isCreature) { MWWorld::LiveCellRef* npc = mActor.get(); if (info.mData.mGender==(npc->mBase->mFlags & npc->mBase->Female ? 0 : 1)) return false; } return true; } bool MWDialogue::Filter::testPlayer (const ESM::DialInfo& info) const { const MWWorld::Ptr player = MWMechanics::getPlayer(); MWMechanics::NpcStats& stats = player.getClass().getNpcStats (player); // check player faction and rank if (!info.mPcFaction.empty()) { std::map::const_iterator iter = stats.getFactionRanks().find (Misc::StringUtils::lowerCase (info.mPcFaction)); if(iter==stats.getFactionRanks().end()) return false; // check rank if (iter->second < info.mData.mPCrank) return false; } else if (info.mData.mPCrank != -1) { // required PC faction is not specified but PC rank is; use speaker's faction std::map::const_iterator iter = stats.getFactionRanks().find (Misc::StringUtils::lowerCase (mActor.getClass().getPrimaryFaction(mActor))); if(iter==stats.getFactionRanks().end()) return false; // check rank if (iter->second < info.mData.mPCrank) return false; } // check cell if (!info.mCell.empty()) { // supports partial matches, just like getPcCell const std::string& playerCell = MWBase::Environment::get().getWorld()->getCellName(player.getCell()); bool match = playerCell.length()>=info.mCell.length() && Misc::StringUtils::ciEqual(playerCell.substr (0, info.mCell.length()), info.mCell); if (!match) return false; } return true; } bool MWDialogue::Filter::testSelectStructs (const ESM::DialInfo& info) const { for (std::vector::const_iterator iter (info.mSelects.begin()); iter != info.mSelects.end(); ++iter) if (!testSelectStruct (*iter)) return false; return true; } bool MWDialogue::Filter::testDisposition (const ESM::DialInfo& info, bool invert) const { bool isCreature = (mActor.getTypeName() != typeid (ESM::NPC).name()); if (isCreature) return true; int actorDisposition = MWBase::Environment::get().getMechanicsManager()->getDerivedDisposition(mActor); // For service refusal, the disposition check is inverted. However, a value of 0 still means "always succeed". return invert ? (info.mData.mDisposition == 0 || actorDisposition < info.mData.mDisposition) : (actorDisposition >= info.mData.mDisposition); } bool MWDialogue::Filter::testFunctionLocal(const MWDialogue::SelectWrapper& select) const { std::string scriptName = mActor.getClass().getScript (mActor); if (scriptName.empty()) return false; // no script std::string name = Misc::StringUtils::lowerCase (select.getName()); const Compiler::Locals& localDefs = MWBase::Environment::get().getScriptManager()->getLocals (scriptName); char type = localDefs.getType (name); if (type==' ') return false; // script does not have a variable of this name. int index = localDefs.getIndex (name); if (index < 0) return false; // shouldn't happen, we checked that variable has a type above, so must exist const MWScript::Locals& locals = mActor.getRefData().getLocals(); if (locals.isEmpty()) return select.selectCompare(0); switch (type) { case 's': return select.selectCompare (static_cast (locals.mShorts[index])); case 'l': return select.selectCompare (locals.mLongs[index]); case 'f': return select.selectCompare (locals.mFloats[index]); } throw std::logic_error ("unknown local variable type in dialogue filter"); } bool MWDialogue::Filter::testSelectStruct (const SelectWrapper& select) const { if (select.isNpcOnly() && (mActor.getTypeName() != typeid (ESM::NPC).name())) // If the actor is a creature, we pass all conditions only applicable to NPCs. return true; if (select.getFunction() == SelectWrapper::Function_Choice && mChoice == -1) // If not currently in a choice, we reject all conditions that test against choices. return false; if (select.getFunction() == SelectWrapper::Function_Weather && !(MWBase::Environment::get().getWorld()->isCellExterior() || MWBase::Environment::get().getWorld()->isCellQuasiExterior())) // Reject weather conditions in interior cells // Note that the original engine doesn't include the "|| isCellQuasiExterior()" check, which could be considered a bug. return false; switch (select.getType()) { case SelectWrapper::Type_None: return true; case SelectWrapper::Type_Integer: return select.selectCompare (getSelectStructInteger (select)); case SelectWrapper::Type_Numeric: return testSelectStructNumeric (select); case SelectWrapper::Type_Boolean: return select.selectCompare (getSelectStructBoolean (select)); // We must not do the comparison for inverted functions (eg. Function_NotClass) case SelectWrapper::Type_Inverted: return getSelectStructBoolean (select); } return true; } bool MWDialogue::Filter::testSelectStructNumeric (const SelectWrapper& select) const { switch (select.getFunction()) { case SelectWrapper::Function_Global: // internally all globals are float :( return select.selectCompare ( MWBase::Environment::get().getWorld()->getGlobalFloat (select.getName())); case SelectWrapper::Function_Local: { return testFunctionLocal(select); } case SelectWrapper::Function_NotLocal: { return !testFunctionLocal(select); } case SelectWrapper::Function_PcHealthPercent: { MWWorld::Ptr player = MWMechanics::getPlayer(); float ratio = player.getClass().getCreatureStats (player).getHealth().getCurrent() / player.getClass().getCreatureStats (player).getHealth().getModified(); return select.selectCompare (static_cast(ratio*100)); } case SelectWrapper::Function_PcDynamicStat: { MWWorld::Ptr player = MWMechanics::getPlayer(); float value = player.getClass().getCreatureStats (player). getDynamic (select.getArgument()).getCurrent(); return select.selectCompare (value); } case SelectWrapper::Function_HealthPercent: { float ratio = mActor.getClass().getCreatureStats (mActor).getHealth().getCurrent() / mActor.getClass().getCreatureStats (mActor).getHealth().getModified(); return select.selectCompare (static_cast(ratio*100)); } default: throw std::runtime_error ("unknown numeric select function"); } } int MWDialogue::Filter::getSelectStructInteger (const SelectWrapper& select) const { MWWorld::Ptr player = MWMechanics::getPlayer(); switch (select.getFunction()) { case SelectWrapper::Function_Journal: return MWBase::Environment::get().getJournal()->getJournalIndex (select.getName()); case SelectWrapper::Function_Item: { MWWorld::ContainerStore& store = player.getClass().getContainerStore (player); return store.count(select.getName()); } case SelectWrapper::Function_Dead: return MWBase::Environment::get().getMechanicsManager()->countDeaths (select.getName()); case SelectWrapper::Function_Choice: return mChoice; case SelectWrapper::Function_AiSetting: return mActor.getClass().getCreatureStats (mActor).getAiSetting ( (MWMechanics::CreatureStats::AiSetting)select.getArgument()).getModified(false); case SelectWrapper::Function_PcAttribute: return player.getClass().getCreatureStats (player). getAttribute (select.getArgument()).getModified(); case SelectWrapper::Function_PcSkill: return static_cast (player.getClass(). getNpcStats (player).getSkill (select.getArgument()).getModified()); case SelectWrapper::Function_FriendlyHit: { int hits = mActor.getClass().getCreatureStats (mActor).getFriendlyHits(); return hits>4 ? 4 : hits; } case SelectWrapper::Function_PcLevel: return player.getClass().getCreatureStats (player).getLevel(); case SelectWrapper::Function_PcGender: return player.get()->mBase->isMale() ? 0 : 1; case SelectWrapper::Function_PcClothingModifier: { const MWWorld::InventoryStore& store = player.getClass().getInventoryStore (player); int value = 0; for (int i=0; i<=15; ++i) // everything except things held in hands and ammunition { MWWorld::ConstContainerStoreIterator slot = store.getSlot (i); if (slot!=store.end()) value += slot->getClass().getValue (*slot); } return value; } case SelectWrapper::Function_PcCrimeLevel: return player.getClass().getNpcStats (player).getBounty(); case SelectWrapper::Function_RankRequirement: { std::string faction = mActor.getClass().getPrimaryFaction(mActor); if (faction.empty()) return 0; int rank = getFactionRank (player, faction); if (rank>=9) return 0; // max rank int result = 0; if (hasFactionRankSkillRequirements (player, faction, rank+1)) result += 1; if (hasFactionRankReputationRequirements (player, faction, rank+1)) result += 2; return result; } case SelectWrapper::Function_Level: return mActor.getClass().getCreatureStats (mActor).getLevel(); case SelectWrapper::Function_PCReputation: return player.getClass().getNpcStats (player).getReputation(); case SelectWrapper::Function_Weather: return MWBase::Environment::get().getWorld()->getCurrentWeather(); case SelectWrapper::Function_Reputation: return mActor.getClass().getNpcStats (mActor).getReputation(); case SelectWrapper::Function_FactionRankDiff: { std::string faction = mActor.getClass().getPrimaryFaction(mActor); if (faction.empty()) return 0; int rank = getFactionRank (player, faction); int npcRank = mActor.getClass().getPrimaryFactionRank(mActor); return rank-npcRank; } case SelectWrapper::Function_WerewolfKills: return player.getClass().getNpcStats (player).getWerewolfKills(); case SelectWrapper::Function_RankLow: case SelectWrapper::Function_RankHigh: { bool low = select.getFunction()==SelectWrapper::Function_RankLow; std::string factionId = mActor.getClass().getPrimaryFaction(mActor); if (factionId.empty()) return 0; int value = 0; MWMechanics::NpcStats& playerStats = player.getClass().getNpcStats (player); std::map::const_iterator playerFactionIt = playerStats.getFactionRanks().begin(); for (; playerFactionIt != playerStats.getFactionRanks().end(); ++playerFactionIt) { int reaction = MWBase::Environment::get().getDialogueManager()->getFactionReaction(factionId, playerFactionIt->first); if (low ? reaction < value : reaction > value) value = reaction; } return value; } case SelectWrapper::Function_CreatureTargetted: { MWWorld::Ptr target; mActor.getClass().getCreatureStats(mActor).getAiSequence().getCombatTarget(target); if (target) { if (target.getClass().isNpc() && target.getClass().getNpcStats(target).isWerewolf()) return 2; if (target.getTypeName() == typeid(ESM::Creature).name()) return 1; } } return 0; default: throw std::runtime_error ("unknown integer select function"); } } bool MWDialogue::Filter::getSelectStructBoolean (const SelectWrapper& select) const { MWWorld::Ptr player = MWMechanics::getPlayer(); switch (select.getFunction()) { case SelectWrapper::Function_False: return false; case SelectWrapper::Function_NotId: return !Misc::StringUtils::ciEqual(mActor.getCellRef().getRefId(), select.getName()); case SelectWrapper::Function_NotFaction: return !Misc::StringUtils::ciEqual(mActor.getClass().getPrimaryFaction(mActor), select.getName()); case SelectWrapper::Function_NotClass: return !Misc::StringUtils::ciEqual(mActor.get()->mBase->mClass, select.getName()); case SelectWrapper::Function_NotRace: return !Misc::StringUtils::ciEqual(mActor.get()->mBase->mRace, select.getName()); case SelectWrapper::Function_NotCell: { const std::string& actorCell = MWBase::Environment::get().getWorld()->getCellName(mActor.getCell()); return !(actorCell.length() >= select.getName().length() && Misc::StringUtils::ciEqual(actorCell.substr(0, select.getName().length()), select.getName())); } case SelectWrapper::Function_SameGender: return (player.get()->mBase->mFlags & ESM::NPC::Female)== (mActor.get()->mBase->mFlags & ESM::NPC::Female); case SelectWrapper::Function_SameRace: return Misc::StringUtils::ciEqual(mActor.get()->mBase->mRace, player.get()->mBase->mRace); case SelectWrapper::Function_SameFaction: return player.getClass().getNpcStats (player).isInFaction(mActor.getClass().getPrimaryFaction(mActor)); case SelectWrapper::Function_PcCommonDisease: return player.getClass().getCreatureStats (player).hasCommonDisease(); case SelectWrapper::Function_PcBlightDisease: return player.getClass().getCreatureStats (player).hasBlightDisease(); case SelectWrapper::Function_PcCorprus: return player.getClass().getCreatureStats (player). getMagicEffects().get (ESM::MagicEffect::Corprus).getMagnitude()!=0; case SelectWrapper::Function_PcExpelled: { std::string faction = mActor.getClass().getPrimaryFaction(mActor); if (faction.empty()) return false; return player.getClass().getNpcStats(player).getExpelled(faction); } case SelectWrapper::Function_PcVampire: return player.getClass().getCreatureStats(player).getMagicEffects(). get(ESM::MagicEffect::Vampirism).getMagnitude() > 0; case SelectWrapper::Function_TalkedToPc: return mTalkedToPlayer; case SelectWrapper::Function_Alarmed: return mActor.getClass().getCreatureStats (mActor).isAlarmed(); case SelectWrapper::Function_Detected: return MWBase::Environment::get().getMechanicsManager()->awarenessCheck(player, mActor); case SelectWrapper::Function_Attacked: return mActor.getClass().getCreatureStats (mActor).getAttacked(); case SelectWrapper::Function_ShouldAttack: return MWBase::Environment::get().getMechanicsManager()->isAggressive(mActor, MWMechanics::getPlayer()); case SelectWrapper::Function_Werewolf: return mActor.getClass().getNpcStats (mActor).isWerewolf(); default: throw std::runtime_error ("unknown boolean select function"); } } int MWDialogue::Filter::getFactionRank (const MWWorld::Ptr& actor, const std::string& factionId) const { MWMechanics::NpcStats& stats = actor.getClass().getNpcStats (actor); std::map::const_iterator iter = stats.getFactionRanks().find (Misc::StringUtils::lowerCase(factionId)); if (iter==stats.getFactionRanks().end()) return -1; return iter->second; } bool MWDialogue::Filter::hasFactionRankSkillRequirements (const MWWorld::Ptr& actor, const std::string& factionId, int rank) const { if (rank<0 || rank>=10) throw std::runtime_error ("rank index out of range"); if (!actor.getClass().getNpcStats (actor).hasSkillsForRank (factionId, rank)) return false; const ESM::Faction& faction = *MWBase::Environment::get().getWorld()->getStore().get().find (factionId); MWMechanics::CreatureStats& stats = actor.getClass().getCreatureStats (actor); return stats.getAttribute (faction.mData.mAttribute[0]).getBase()>=faction.mData.mRankData[rank].mAttribute1 && stats.getAttribute (faction.mData.mAttribute[1]).getBase()>=faction.mData.mRankData[rank].mAttribute2; } bool MWDialogue::Filter::hasFactionRankReputationRequirements (const MWWorld::Ptr& actor, const std::string& factionId, int rank) const { if (rank<0 || rank>=10) throw std::runtime_error ("rank index out of range"); MWMechanics::NpcStats& stats = actor.getClass().getNpcStats (actor); const ESM::Faction& faction = *MWBase::Environment::get().getWorld()->getStore().get().find (factionId); return stats.getFactionReputation (factionId)>=faction.mData.mRankData[rank].mFactReaction; } MWDialogue::Filter::Filter (const MWWorld::Ptr& actor, int choice, bool talkedToPlayer) : mActor (actor), mChoice (choice), mTalkedToPlayer (talkedToPlayer) {} const ESM::DialInfo* MWDialogue::Filter::search (const ESM::Dialogue& dialogue, const bool fallbackToInfoRefusal) const { std::vector suitableInfos = list (dialogue, fallbackToInfoRefusal, false); if (suitableInfos.empty()) return nullptr; else return suitableInfos[0]; } std::vector MWDialogue::Filter::listAll (const ESM::Dialogue& dialogue) const { std::vector infos; for (ESM::Dialogue::InfoContainer::const_iterator iter = dialogue.mInfo.begin(); iter!=dialogue.mInfo.end(); ++iter) { if (testActor (*iter)) infos.push_back(&*iter); } return infos; } std::vector MWDialogue::Filter::list (const ESM::Dialogue& dialogue, bool fallbackToInfoRefusal, bool searchAll, bool invertDisposition) const { std::vector infos; bool infoRefusal = false; // Iterate over topic responses to find a matching one for (ESM::Dialogue::InfoContainer::const_iterator iter = dialogue.mInfo.begin(); iter!=dialogue.mInfo.end(); ++iter) { if (testActor (*iter) && testPlayer (*iter) && testSelectStructs (*iter)) { if (testDisposition (*iter, invertDisposition)) { infos.push_back(&*iter); if (!searchAll) break; } else infoRefusal = true; } } if (infos.empty() && infoRefusal && fallbackToInfoRefusal) { // No response is valid because of low NPC disposition, // search a response in the topic "Info Refusal" const MWWorld::Store &dialogues = MWBase::Environment::get().getWorld()->getStore().get(); const ESM::Dialogue& infoRefusalDialogue = *dialogues.find ("Info Refusal"); for (ESM::Dialogue::InfoContainer::const_iterator iter = infoRefusalDialogue.mInfo.begin(); iter!=infoRefusalDialogue.mInfo.end(); ++iter) if (testActor (*iter) && testPlayer (*iter) && testSelectStructs (*iter) && testDisposition(*iter, invertDisposition)) { infos.push_back(&*iter); if (!searchAll) break; } } return infos; } ================================================ FILE: apps/openmw/mwdialogue/filter.hpp ================================================ #ifndef GAME_MWDIALOGUE_FILTER_H #define GAME_MWDIALOGUE_FILTER_H #include #include "../mwworld/ptr.hpp" namespace ESM { struct DialInfo; struct Dialogue; } namespace MWDialogue { class SelectWrapper; class Filter { MWWorld::Ptr mActor; int mChoice; bool mTalkedToPlayer; bool testActor (const ESM::DialInfo& info) const; ///< Is this the right actor for this \a info? bool testPlayer (const ESM::DialInfo& info) const; ///< Do the player and the cell the player is currently in match \a info? bool testSelectStructs (const ESM::DialInfo& info) const; ///< Are all select structs matching? bool testDisposition (const ESM::DialInfo& info, bool invert=false) const; ///< Is the actor disposition toward the player high enough (or low enough, if \a invert is true)? bool testFunctionLocal(const SelectWrapper& select) const; bool testSelectStruct (const SelectWrapper& select) const; bool testSelectStructNumeric (const SelectWrapper& select) const; int getSelectStructInteger (const SelectWrapper& select) const; bool getSelectStructBoolean (const SelectWrapper& select) const; int getFactionRank (const MWWorld::Ptr& actor, const std::string& factionId) const; bool hasFactionRankSkillRequirements (const MWWorld::Ptr& actor, const std::string& factionId, int rank) const; bool hasFactionRankReputationRequirements (const MWWorld::Ptr& actor, const std::string& factionId, int rank) const; public: Filter (const MWWorld::Ptr& actor, int choice, bool talkedToPlayer); std::vector list (const ESM::Dialogue& dialogue, bool fallbackToInfoRefusal, bool searchAll, bool invertDisposition=false) const; ///< List all infos that could be used on the given actor, using the current runtime state of the actor. /// \note If fallbackToInfoRefusal is used, the returned DialInfo might not be from the supplied ESM::Dialogue. std::vector listAll (const ESM::Dialogue& dialogue) const; ///< List all infos that could possibly be used on the given actor, ignoring runtime state filters and ignoring player filters. const ESM::DialInfo* search (const ESM::Dialogue& dialogue, const bool fallbackToInfoRefusal) const; ///< Get a matching response for the requested dialogue. /// Redirect to "Info Refusal" topic if a response fulfills all conditions but disposition. }; } #endif ================================================ FILE: apps/openmw/mwdialogue/hypertextparser.cpp ================================================ #include #include "../mwbase/environment.hpp" #include "../mwbase/world.hpp" #include "../mwworld/store.hpp" #include "../mwworld/esmstore.hpp" #include "keywordsearch.hpp" #include "hypertextparser.hpp" namespace MWDialogue { namespace HyperTextParser { std::vector parseHyperText(const std::string & text) { std::vector result; size_t pos_end = std::string::npos, iteration_pos = 0; for(;;) { size_t pos_begin = text.find('@', iteration_pos); if (pos_begin != std::string::npos) pos_end = text.find('#', pos_begin); if (pos_begin != std::string::npos && pos_end != std::string::npos) { if (pos_begin != iteration_pos) tokenizeKeywords(text.substr(iteration_pos, pos_begin - iteration_pos), result); std::string link = text.substr(pos_begin + 1, pos_end - pos_begin - 1); result.emplace_back(link, Token::ExplicitLink); iteration_pos = pos_end + 1; } else { if (iteration_pos != text.size()) tokenizeKeywords(text.substr(iteration_pos), result); break; } } return result; } void tokenizeKeywords(const std::string & text, std::vector & tokens) { const MWWorld::Store & dialogs = MWBase::Environment::get().getWorld()->getStore().get(); std::list keywordList; for (MWWorld::Store::iterator it = dialogs.begin(); it != dialogs.end(); ++it) keywordList.push_back(Misc::StringUtils::lowerCase(it->mId)); keywordList.sort(Misc::StringUtils::ciLess); KeywordSearch keywordSearch; for (std::list::const_iterator it = keywordList.begin(); it != keywordList.end(); ++it) keywordSearch.seed(*it, 0 /*unused*/); std::vector::Match> matches; keywordSearch.highlightKeywords(text.begin(), text.end(), matches); for (std::vector::Match>::const_iterator it = matches.begin(); it != matches.end(); ++it) { tokens.emplace_back(std::string(it->mBeg, it->mEnd), Token::ImplicitKeyword); } } size_t removePseudoAsterisks(std::string & phrase) { size_t pseudoAsterisksCount = 0; if( !phrase.empty() ) { std::string::reverse_iterator rit = phrase.rbegin(); const char specialPseudoAsteriskCharacter = 127; while( rit != phrase.rend() && *rit == specialPseudoAsteriskCharacter ) { pseudoAsterisksCount++; ++rit; } } phrase = phrase.substr(0, phrase.length() - pseudoAsterisksCount); return pseudoAsterisksCount; } } } ================================================ FILE: apps/openmw/mwdialogue/hypertextparser.hpp ================================================ #ifndef GAME_MWDIALOGUE_HYPERTEXTPARSER_H #define GAME_MWDIALOGUE_HYPERTEXTPARSER_H #include #include namespace MWDialogue { namespace HyperTextParser { struct Token { enum Type { ExplicitLink, // enclosed in @# ImplicitKeyword }; Token(const std::string & text, Type type) : mText(text), mType(type) {} bool isExplicitLink() { return mType == ExplicitLink; } std::string mText; Type mType; }; // In translations (at least Russian) the links are marked with @#, so // it should be a function to parse it std::vector parseHyperText(const std::string & text); void tokenizeKeywords(const std::string & text, std::vector & tokens); size_t removePseudoAsterisks(std::string & phrase); } } #endif ================================================ FILE: apps/openmw/mwdialogue/journalentry.cpp ================================================ #include "journalentry.hpp" #include #include #include #include "../mwbase/environment.hpp" #include "../mwbase/world.hpp" #include "../mwworld/esmstore.hpp" #include "../mwscript/interpretercontext.hpp" namespace MWDialogue { Entry::Entry() {} Entry::Entry (const std::string& topic, const std::string& infoId, const MWWorld::Ptr& actor) : mInfoId (infoId) { const ESM::Dialogue *dialogue = MWBase::Environment::get().getWorld()->getStore().get().find (topic); for (ESM::Dialogue::InfoContainer::const_iterator iter (dialogue->mInfo.begin()); iter!=dialogue->mInfo.end(); ++iter) if (iter->mId == mInfoId) { if (actor.isEmpty()) { MWScript::InterpreterContext interpreterContext(nullptr, MWWorld::Ptr()); mText = Interpreter::fixDefinesDialog(iter->mResponse, interpreterContext); } else { MWScript::InterpreterContext interpreterContext(&actor.getRefData().getLocals(),actor); mText = Interpreter::fixDefinesDialog(iter->mResponse, interpreterContext); } return; } throw std::runtime_error ("unknown info ID " + mInfoId + " for topic " + topic); } Entry::Entry (const ESM::JournalEntry& record) : mInfoId (record.mInfo), mText (record.mText), mActorName(record.mActorName) {} std::string Entry::getText() const { return mText; } void Entry::write (ESM::JournalEntry& entry) const { entry.mInfo = mInfoId; entry.mText = mText; entry.mActorName = mActorName; } JournalEntry::JournalEntry() {} JournalEntry::JournalEntry (const std::string& topic, const std::string& infoId, const MWWorld::Ptr& actor) : Entry (topic, infoId, actor), mTopic (topic) {} JournalEntry::JournalEntry (const ESM::JournalEntry& record) : Entry (record), mTopic (record.mTopic) {} void JournalEntry::write (ESM::JournalEntry& entry) const { Entry::write (entry); entry.mTopic = mTopic; } JournalEntry JournalEntry::makeFromQuest (const std::string& topic, int index) { return JournalEntry (topic, idFromIndex (topic, index), MWWorld::Ptr()); } std::string JournalEntry::idFromIndex (const std::string& topic, int index) { const ESM::Dialogue *dialogue = MWBase::Environment::get().getWorld()->getStore().get().find (topic); for (ESM::Dialogue::InfoContainer::const_iterator iter (dialogue->mInfo.begin()); iter!=dialogue->mInfo.end(); ++iter) if (iter->mData.mJournalIndex==index) { return iter->mId; } throw std::runtime_error ("unknown journal index for topic " + topic); } StampedJournalEntry::StampedJournalEntry() : mDay (0), mMonth (0), mDayOfMonth (0) {} StampedJournalEntry::StampedJournalEntry (const std::string& topic, const std::string& infoId, int day, int month, int dayOfMonth, const MWWorld::Ptr& actor) : JournalEntry (topic, infoId, actor), mDay (day), mMonth (month), mDayOfMonth (dayOfMonth) {} StampedJournalEntry::StampedJournalEntry (const ESM::JournalEntry& record) : JournalEntry (record), mDay (record.mDay), mMonth (record.mMonth), mDayOfMonth (record.mDayOfMonth) {} void StampedJournalEntry::write (ESM::JournalEntry& entry) const { JournalEntry::write (entry); entry.mDay = mDay; entry.mMonth = mMonth; entry.mDayOfMonth = mDayOfMonth; } StampedJournalEntry StampedJournalEntry::makeFromQuest (const std::string& topic, int index, const MWWorld::Ptr& actor) { int day = MWBase::Environment::get().getWorld()->getGlobalInt ("dayspassed"); int month = MWBase::Environment::get().getWorld()->getGlobalInt ("month"); int dayOfMonth = MWBase::Environment::get().getWorld()->getGlobalInt ("day"); return StampedJournalEntry (topic, idFromIndex (topic, index), day, month, dayOfMonth, actor); } } ================================================ FILE: apps/openmw/mwdialogue/journalentry.hpp ================================================ #ifndef GAME_MWDIALOGUE_JOURNALENTRY_H #define GAME_MWDIALOGUE_JOURNALENTRY_H #include namespace ESM { struct JournalEntry; } namespace MWWorld { class Ptr; } namespace MWDialogue { /// \brief Basic quest/dialogue/topic entry struct Entry { std::string mInfoId; std::string mText; std::string mActorName; // optional Entry(); /// actor is optional Entry (const std::string& topic, const std::string& infoId, const MWWorld::Ptr& actor); Entry (const ESM::JournalEntry& record); std::string getText() const; void write (ESM::JournalEntry& entry) const; }; /// \brief A dialogue entry /// /// Same as entry, but store TopicID struct JournalEntry : public Entry { std::string mTopic; JournalEntry(); JournalEntry (const std::string& topic, const std::string& infoId, const MWWorld::Ptr& actor); JournalEntry (const ESM::JournalEntry& record); void write (ESM::JournalEntry& entry) const; static JournalEntry makeFromQuest (const std::string& topic, int index); static std::string idFromIndex (const std::string& topic, int index); }; /// \brief A quest entry with a timestamp. struct StampedJournalEntry : public JournalEntry { int mDay; int mMonth; int mDayOfMonth; StampedJournalEntry(); StampedJournalEntry (const std::string& topic, const std::string& infoId, int day, int month, int dayOfMonth, const MWWorld::Ptr& actor); StampedJournalEntry (const ESM::JournalEntry& record); void write (ESM::JournalEntry& entry) const; static StampedJournalEntry makeFromQuest (const std::string& topic, int index, const MWWorld::Ptr& actor); }; } #endif ================================================ FILE: apps/openmw/mwdialogue/journalimp.cpp ================================================ #include "journalimp.hpp" #include #include #include #include #include #include "../mwworld/esmstore.hpp" #include "../mwworld/class.hpp" #include "../mwbase/environment.hpp" #include "../mwbase/world.hpp" #include "../mwbase/windowmanager.hpp" #include "../mwgui/messagebox.hpp" namespace MWDialogue { Quest& Journal::getQuest (const std::string& id) { TQuestContainer::iterator iter = mQuests.find (id); if (iter==mQuests.end()) { std::pair result = mQuests.insert (std::make_pair (id, Quest (id))); iter = result.first; } return iter->second; } Topic& Journal::getTopic (const std::string& id) { TTopicContainer::iterator iter = mTopics.find (id); if (iter==mTopics.end()) { std::pair result = mTopics.insert (std::make_pair (id, Topic (id))); iter = result.first; } return iter->second; } bool Journal::isThere (const std::string& topicId, const std::string& infoId) const { if (const ESM::Dialogue *dialogue = MWBase::Environment::get().getWorld()->getStore().get().search (topicId)) { if (infoId.empty()) return true; for (ESM::Dialogue::InfoContainer::const_iterator iter (dialogue->mInfo.begin()); iter!=dialogue->mInfo.end(); ++iter) if (iter->mId == infoId) return true; } return false; } Journal::Journal() {} void Journal::clear() { mJournal.clear(); mQuests.clear(); mTopics.clear(); } /* Start of tes3mp addition Make it possible to check whether a journal entry already exists from elsewhere in the code */ bool Journal::hasEntry(const std::string& id, int index) { std::string infoId = JournalEntry::idFromIndex(id, index); for (TEntryIter i = mJournal.begin(); i != mJournal.end(); ++i) if (i->mTopic == id && i->mInfoId == infoId) return true; return false; } /* End of tes3mp addition */ /* Start of tes3mp change (minor) Make it possible to override current time when adding journal entries, by adding optional timestamp override arguments */ void Journal::addEntry (const std::string& id, int index, const MWWorld::Ptr& actor, int daysPassed, int month, int day) /* End of tes3mp change (major) */ { // bail out if we already have heard this... std::string infoId = JournalEntry::idFromIndex (id, index); for (TEntryIter i = mJournal.begin (); i != mJournal.end (); ++i) if (i->mTopic == id && i->mInfoId == infoId) { if (getJournalIndex(id) < index) { setJournalIndex(id, index); MWBase::Environment::get().getWindowManager()->messageBox ("#{sJournalEntry}"); } return; } StampedJournalEntry entry = StampedJournalEntry::makeFromQuest (id, index, actor); /* Start of tes3mp addition Override the entry's timestamp if provided with valid time arguments */ if (daysPassed != -1 && month != -1 && day != -1) { entry.mDay = daysPassed; entry.mMonth = month; entry.mDayOfMonth = day; } /* End of tes3mp addition */ Quest& quest = getQuest (id); quest.addEntry (entry); // we are doing slicing on purpose here // there is no need to show empty entries in journal if (!entry.getText().empty()) { mJournal.push_back (entry); MWBase::Environment::get().getWindowManager()->messageBox ("#{sJournalEntry}"); } } void Journal::setJournalIndex (const std::string& id, int index) { Quest& quest = getQuest (id); quest.setIndex (index); } void Journal::addTopic (const std::string& topicId, const std::string& infoId, const MWWorld::Ptr& actor) { Topic& topic = getTopic (topicId); JournalEntry entry(topicId, infoId, actor); entry.mActorName = actor.getClass().getName(actor); topic.addEntry (entry); } void Journal::removeLastAddedTopicResponse(const std::string &topicId, const std::string &actorName) { Topic& topic = getTopic (topicId); topic.removeLastAddedResponse(actorName); if (topic.begin() == topic.end()) mTopics.erase(mTopics.find(topicId)); // All responses removed -> remove topic } int Journal::getJournalIndex (const std::string& id) const { TQuestContainer::const_iterator iter = mQuests.find (id); if (iter==mQuests.end()) return 0; return iter->second.getIndex(); } Journal::TEntryIter Journal::begin() const { return mJournal.begin(); } Journal::TEntryIter Journal::end() const { return mJournal.end(); } Journal::TQuestIter Journal::questBegin() const { return mQuests.begin(); } Journal::TQuestIter Journal::questEnd() const { return mQuests.end(); } Journal::TTopicIter Journal::topicBegin() const { return mTopics.begin(); } Journal::TTopicIter Journal::topicEnd() const { return mTopics.end(); } int Journal::countSavedGameRecords() const { int count = static_cast (mQuests.size()); for (TQuestIter iter (mQuests.begin()); iter!=mQuests.end(); ++iter) count += std::distance (iter->second.begin(), iter->second.end()); count += std::distance (mJournal.begin(), mJournal.end()); for (TTopicIter iter (mTopics.begin()); iter!=mTopics.end(); ++iter) count += std::distance (iter->second.begin(), iter->second.end()); return count; } void Journal::write (ESM::ESMWriter& writer, Loading::Listener& progress) const { for (TQuestIter iter (mQuests.begin()); iter!=mQuests.end(); ++iter) { const Quest& quest = iter->second; ESM::QuestState state; quest.write (state); writer.startRecord (ESM::REC_QUES); state.save (writer); writer.endRecord (ESM::REC_QUES); for (Topic::TEntryIter entryIter (quest.begin()); entryIter!=quest.end(); ++entryIter) { ESM::JournalEntry entry; entry.mType = ESM::JournalEntry::Type_Quest; entry.mTopic = quest.getTopic(); entryIter->write (entry); writer.startRecord (ESM::REC_JOUR); entry.save (writer); writer.endRecord (ESM::REC_JOUR); } } for (TEntryIter iter (mJournal.begin()); iter!=mJournal.end(); ++iter) { ESM::JournalEntry entry; entry.mType = ESM::JournalEntry::Type_Journal; iter->write (entry); writer.startRecord (ESM::REC_JOUR); entry.save (writer); writer.endRecord (ESM::REC_JOUR); } for (TTopicIter iter (mTopics.begin()); iter!=mTopics.end(); ++iter) { const Topic& topic = iter->second; for (Topic::TEntryIter entryIter (topic.begin()); entryIter!=topic.end(); ++entryIter) { ESM::JournalEntry entry; entry.mType = ESM::JournalEntry::Type_Topic; entry.mTopic = topic.getTopic(); entryIter->write (entry); writer.startRecord (ESM::REC_JOUR); entry.save (writer); writer.endRecord (ESM::REC_JOUR); } } } void Journal::readRecord (ESM::ESMReader& reader, uint32_t type) { if (type==ESM::REC_JOUR || type==ESM::REC_JOUR_LEGACY) { ESM::JournalEntry record; record.load (reader); if (isThere (record.mTopic, record.mInfo)) switch (record.mType) { case ESM::JournalEntry::Type_Quest: getQuest (record.mTopic).insertEntry (record); break; case ESM::JournalEntry::Type_Journal: mJournal.push_back (record); break; case ESM::JournalEntry::Type_Topic: getTopic (record.mTopic).insertEntry (record); break; } } else if (type==ESM::REC_QUES) { ESM::QuestState record; record.load (reader); if (isThere (record.mTopic)) { std::pair result = mQuests.insert (std::make_pair (record.mTopic, record)); // reapply quest index, this is to handle users upgrading from only // Morrowind.esm (no quest states) to Morrowind.esm + Tribunal.esm result.first->second.setIndex(record.mState); } } } } ================================================ FILE: apps/openmw/mwdialogue/journalimp.hpp ================================================ #ifndef GAME_MWDIALOG_JOURNAL_H #define GAME_MWDIALOG_JOURNAL_H #include "../mwbase/journal.hpp" #include "quest.hpp" namespace MWDialogue { /// \brief The player's journal class Journal : public MWBase::Journal { TEntryContainer mJournal; TQuestContainer mQuests; TTopicContainer mTopics; private: Quest& getQuest (const std::string& id); Topic& getTopic (const std::string& id); bool isThere (const std::string& topicId, const std::string& infoId = "") const; public: Journal(); void clear() override; /* Start of tes3mp addition Make it possible to check whether a journal entry already exists from elsewhere in the code */ virtual bool hasEntry(const std::string& id, int index); /* End of tes3mp addition */ /* Start of tes3mp change (minor) Make it possible to override current time when adding journal entries, by adding optional timestamp override arguments */ void addEntry (const std::string& id, int index, const MWWorld::Ptr& actor, int daysPassed = -1, int month = -1, int day = -1) override; ///< Add a journal entry. /// @param actor Used as context for replacing of escape sequences (%name, etc). /* End of tes3mp change (major) */ void setJournalIndex (const std::string& id, int index) override; ///< Set the journal index without adding an entry. int getJournalIndex (const std::string& id) const override; ///< Get the journal index. void addTopic (const std::string& topicId, const std::string& infoId, const MWWorld::Ptr& actor) override; /// \note topicId must be lowercase void removeLastAddedTopicResponse (const std::string& topicId, const std::string& actorName) override; ///< Removes the last topic response added for the given topicId and actor name. /// \note topicId must be lowercase TEntryIter begin() const override; ///< Iterator pointing to the begin of the main journal. /// /// \note Iterators to main journal entries will never become invalid. TEntryIter end() const override; ///< Iterator pointing past the end of the main journal. TQuestIter questBegin() const override; ///< Iterator pointing to the first quest (sorted by topic ID) TQuestIter questEnd() const override; ///< Iterator pointing past the last quest. TTopicIter topicBegin() const override; ///< Iterator pointing to the first topic (sorted by topic ID) /// /// \note The topic ID is identical with the user-visible topic string. TTopicIter topicEnd() const override; ///< Iterator pointing past the last topic. int countSavedGameRecords() const override; void write (ESM::ESMWriter& writer, Loading::Listener& progress) const override; void readRecord (ESM::ESMReader& reader, uint32_t type) override; }; } #endif ================================================ FILE: apps/openmw/mwdialogue/keywordsearch.cpp ================================================ ================================================ FILE: apps/openmw/mwdialogue/keywordsearch.hpp ================================================ #ifndef GAME_MWDIALOGUE_KEYWORDSEARCH_H #define GAME_MWDIALOGUE_KEYWORDSEARCH_H #include #include #include #include #include // std::reverse #include namespace MWDialogue { template class KeywordSearch { public: typedef typename string_t::const_iterator Point; struct Match { Point mBeg; Point mEnd; value_t mValue; }; void seed (string_t keyword, value_t value) { if (keyword.empty()) return; seed_impl (/*std::move*/ (keyword), /*std::move*/ (value), 0, mRoot); } void clear () { mRoot.mChildren.clear (); mRoot.mKeyword.clear (); } bool containsKeyword (string_t keyword, value_t& value) { typename Entry::childen_t::iterator current; typename Entry::childen_t::iterator next; current = mRoot.mChildren.find (Misc::StringUtils::toLower (*keyword.begin())); if (current == mRoot.mChildren.end()) return false; else if (current->second.mKeyword.size() && Misc::StringUtils::ciEqual(current->second.mKeyword, keyword)) { value = current->second.mValue; return true; } for (Point i = ++keyword.begin(); i != keyword.end(); ++i) { next = current->second.mChildren.find(Misc::StringUtils::toLower (*i)); if (next == current->second.mChildren.end()) return false; if (Misc::StringUtils::ciEqual(next->second.mKeyword, keyword)) { value = next->second.mValue; return true; } current = next; } return false; } static bool sortMatches(const Match& left, const Match& right) { return left.mBeg < right.mBeg; } void highlightKeywords (Point beg, Point end, std::vector& out) { std::vector matches; for (Point i = beg; i != end; ++i) { // check if previous character marked start of new word if (i != beg) { Point prev = i; --prev; if(isalpha(*prev)) continue; } // check first character typename Entry::childen_t::iterator candidate = mRoot.mChildren.find (Misc::StringUtils::toLower (*i)); // no match, on to next character if (candidate == mRoot.mChildren.end ()) continue; // see how far the match goes Point j = i; // 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 > candidates; while ((j + 1) != end) { typename Entry::childen_t::iterator next = candidate->second.mChildren.find (Misc::StringUtils::toLower (*++j)); if (next == candidate->second.mChildren.end ()) { if (candidate->second.mKeyword.size() > 0) candidates.push_back(std::make_pair((j-i), candidate)); break; } candidate = next; if (candidate->second.mKeyword.size() > 0) candidates.push_back(std::make_pair((j-i), candidate)); } if (candidates.empty()) continue; // didn't match enough to disambiguate, on to next character // 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 >::iterator it = candidates.begin(); it != candidates.end(); ++it) { candidate = it->second; // try to match the rest of the keyword Point k = i + it->first; typename string_t::const_iterator t = candidate->second.mKeyword.begin () + (k - i); while (k != end && t != candidate->second.mKeyword.end ()) { if (Misc::StringUtils::toLower (*k) != Misc::StringUtils::toLower (*t)) break; ++k, ++t; } // didn't match full keyword, try the next candidate if (t != candidate->second.mKeyword.end ()) continue; // found a keyword, but there might still be longer keywords that start somewhere _within_ this keyword // we will resolve these overlapping keywords later, choosing the longest one in case of conflict Match match; match.mValue = candidate->second.mValue; match.mBeg = i; match.mEnd = k; matches.push_back(match); break; } } // resolve overlapping keywords while (!matches.empty()) { int longestKeywordSize = 0; typename std::vector::iterator longestKeyword = matches.begin(); for (typename std::vector::iterator it = matches.begin(); it != matches.end(); ++it) { int size = it->mEnd - it->mBeg; if (size > longestKeywordSize) { longestKeywordSize = size; longestKeyword = it; } typename std::vector::iterator next = it; ++next; if (next == matches.end()) break; if (it->mEnd <= next->mBeg) { break; // no overlap } } Match keyword = *longestKeyword; matches.erase(longestKeyword); out.push_back(keyword); // erase anything that overlaps with the keyword we just added to the output for (typename std::vector::iterator it = matches.begin(); it != matches.end();) { if (it->mBeg < keyword.mEnd && it->mEnd > keyword.mBeg) it = matches.erase(it); else ++it; } } std::sort(out.begin(), out.end(), sortMatches); } private: struct Entry { typedef std::map childen_t; string_t mKeyword; value_t mValue; childen_t mChildren; }; void seed_impl (string_t keyword, value_t value, size_t depth, Entry & entry) { int ch = Misc::StringUtils::toLower (keyword.at (depth)); typename Entry::childen_t::iterator j = entry.mChildren.find (ch); if (j == entry.mChildren.end ()) { entry.mChildren [ch].mValue = /*std::move*/ (value); entry.mChildren [ch].mKeyword = /*std::move*/ (keyword); } else { if (j->second.mKeyword.size () > 0) { if (keyword == j->second.mKeyword) throw std::runtime_error ("duplicate keyword inserted"); value_t pushValue = /*std::move*/ (j->second.mValue); string_t pushKeyword = /*std::move*/ (j->second.mKeyword); if (depth >= pushKeyword.size ()) throw std::runtime_error ("unexpected"); if (depth+1 < pushKeyword.size()) { seed_impl (/*std::move*/ (pushKeyword), /*std::move*/ (pushValue), depth+1, j->second); j->second.mKeyword.clear (); } } if (depth+1 == keyword.size()) j->second.mKeyword = value; else // depth+1 < keyword.size() seed_impl (/*std::move*/ (keyword), /*std::move*/ (value), depth+1, j->second); } } Entry mRoot; }; } #endif ================================================ FILE: apps/openmw/mwdialogue/quest.cpp ================================================ #include "quest.hpp" #include #include "../mwworld/esmstore.hpp" #include "../mwbase/environment.hpp" #include "../mwbase/world.hpp" namespace MWDialogue { Quest::Quest() : Topic(), mIndex (0), mFinished (false) {} Quest::Quest (const std::string& topic) : Topic (topic), mIndex (0), mFinished (false) {} Quest::Quest (const ESM::QuestState& state) : Topic (state.mTopic), mIndex (state.mState), mFinished (state.mFinished!=0) {} std::string Quest::getName() const { const ESM::Dialogue *dialogue = MWBase::Environment::get().getWorld()->getStore().get().find (mTopic); for (ESM::Dialogue::InfoContainer::const_iterator iter (dialogue->mInfo.begin()); iter!=dialogue->mInfo.end(); ++iter) if (iter->mQuestStatus==ESM::DialInfo::QS_Name) return iter->mResponse; return ""; } int Quest::getIndex() const { return mIndex; } void Quest::setIndex (int index) { // The index must be set even if no related journal entry was found mIndex = index; } bool Quest::isFinished() const { return mFinished; } void Quest::addEntry (const JournalEntry& entry) { int index = -1; const ESM::Dialogue *dialogue = MWBase::Environment::get().getWorld()->getStore().get().find (entry.mTopic); for (ESM::Dialogue::InfoContainer::const_iterator iter (dialogue->mInfo.begin()); iter!=dialogue->mInfo.end(); ++iter) if (iter->mId == entry.mInfoId) { index = iter->mData.mJournalIndex; break; } if (index==-1) throw std::runtime_error ("unknown journal entry for topic " + mTopic); for (auto &info : dialogue->mInfo) { if (info.mData.mJournalIndex == index && (info.mQuestStatus == ESM::DialInfo::QS_Finished || info.mQuestStatus == ESM::DialInfo::QS_Restart)) { mFinished = (info.mQuestStatus == ESM::DialInfo::QS_Finished); break; } } if (index > mIndex) mIndex = index; for (TEntryIter iter (mEntries.begin()); iter!=mEntries.end(); ++iter) if (iter->mInfoId==entry.mInfoId) return; mEntries.push_back (entry); // we want slicing here } void Quest::write (ESM::QuestState& state) const { state.mTopic = getTopic(); state.mState = mIndex; state.mFinished = mFinished; } } ================================================ FILE: apps/openmw/mwdialogue/quest.hpp ================================================ #ifndef GAME_MWDIALOG_QUEST_H #define GAME_MWDIALOG_QUEST_H #include "topic.hpp" namespace ESM { struct QuestState; } namespace MWDialogue { /// \brief A quest in progress or a completed quest class Quest : public Topic { int mIndex; bool mFinished; public: Quest(); Quest (const std::string& topic); Quest (const ESM::QuestState& state); std::string getName() const override; ///< May be an empty string int getIndex() const; void setIndex (int index); ///< Calling this function with a non-existent index will throw an exception. bool isFinished() const; void addEntry (const JournalEntry& entry) override; ///< Add entry and adjust index accordingly. /// /// \note Redundant entries are ignored, but the index is still adjusted. void write (ESM::QuestState& state) const; }; } #endif ================================================ FILE: apps/openmw/mwdialogue/scripttest.cpp ================================================ #include "scripttest.hpp" #include "../mwworld/manualref.hpp" #include "../mwworld/esmstore.hpp" #include "../mwworld/class.hpp" #include "../mwbase/environment.hpp" #include "../mwbase/world.hpp" #include "../mwbase/scriptmanager.hpp" #include "../mwscript/compilercontext.hpp" #include #include #include #include #include #include #include "filter.hpp" namespace { void test(const MWWorld::Ptr& actor, int &compiled, int &total, const Compiler::Extensions* extensions, int warningsMode) { MWDialogue::Filter filter(actor, 0, false); MWScript::CompilerContext compilerContext(MWScript::CompilerContext::Type_Dialogue); compilerContext.setExtensions(extensions); Compiler::StreamErrorHandler errorHandler; errorHandler.setWarningsMode (warningsMode); const MWWorld::Store& dialogues = MWBase::Environment::get().getWorld()->getStore().get(); for (MWWorld::Store::iterator it = dialogues.begin(); it != dialogues.end(); ++it) { std::vector infos = filter.listAll(*it); for (std::vector::iterator iter = infos.begin(); iter != infos.end(); ++iter) { const ESM::DialInfo* info = *iter; if (!info->mResultScript.empty()) { bool success = true; ++total; try { errorHandler.reset(); std::istringstream input (info->mResultScript + "\n"); Compiler::Scanner scanner (errorHandler, input, extensions); Compiler::Locals locals; std::string actorScript = actor.getClass().getScript(actor); if (!actorScript.empty()) { // grab local variables from actor's script, if available. locals = MWBase::Environment::get().getScriptManager()->getLocals (actorScript); } Compiler::ScriptParser parser(errorHandler, compilerContext, locals, false); scanner.scan (parser); if (!errorHandler.isGood()) success = false; ++compiled; } catch (const Compiler::SourceException& /* error */) { // error has already been reported via error handler success = false; } catch (const std::exception& error) { Log(Debug::Error) << std::string ("Dialogue error: An exception has been thrown: ") + error.what(); success = false; } if (!success) { Log(Debug::Error) << "Error: compiling failed (dialogue script): \n" << info->mResultScript << "\n"; } } } } } } namespace MWDialogue { namespace ScriptTest { std::pair compileAll(const Compiler::Extensions *extensions, int warningsMode) { int compiled = 0, total = 0; const MWWorld::Store& npcs = MWBase::Environment::get().getWorld()->getStore().get(); for (MWWorld::Store::iterator it = npcs.begin(); it != npcs.end(); ++it) { MWWorld::ManualRef ref(MWBase::Environment::get().getWorld()->getStore(), it->mId); test(ref.getPtr(), compiled, total, extensions, warningsMode); } const MWWorld::Store& creatures = MWBase::Environment::get().getWorld()->getStore().get(); for (MWWorld::Store::iterator it = creatures.begin(); it != creatures.end(); ++it) { MWWorld::ManualRef ref(MWBase::Environment::get().getWorld()->getStore(), it->mId); test(ref.getPtr(), compiled, total, extensions, warningsMode); } return std::make_pair(total, compiled); } } } ================================================ FILE: apps/openmw/mwdialogue/scripttest.hpp ================================================ #ifndef OPENMW_MWDIALOGUE_SCRIPTTEST_H #define OPENMW_MWDIALOGUE_SCRIPTTEST_H #include namespace MWDialogue { namespace ScriptTest { /// Attempt to compile all dialogue scripts, use for verification purposes /// @return A pair containing std::pair compileAll(const Compiler::Extensions* extensions, int warningsMode); } } #endif ================================================ FILE: apps/openmw/mwdialogue/selectwrapper.cpp ================================================ #include "selectwrapper.hpp" #include #include #include #include namespace { template bool selectCompareImp (char comp, T1 value1, T2 value2) { switch (comp) { case '0': return value1==value2; case '1': return value1!=value2; case '2': return value1>value2; case '3': return value1>=value2; case '4': return value1 bool selectCompareImp (const ESM::DialInfo::SelectStruct& select, T value1) { if (select.mValue.getType()==ESM::VT_Int) { return selectCompareImp (select.mSelectRule[4], value1, select.mValue.getInteger()); } else if (select.mValue.getType()==ESM::VT_Float) { return selectCompareImp (select.mSelectRule[4], value1, select.mValue.getFloat()); } else throw std::runtime_error ( "unsupported variable type in dialogue info select"); } } MWDialogue::SelectWrapper::Function MWDialogue::SelectWrapper::decodeFunction() const { int index = 0; std::istringstream (mSelect.mSelectRule.substr(2,2)) >> index; switch (index) { case 0: return Function_RankLow; case 1: return Function_RankHigh; case 2: return Function_RankRequirement; case 3: return Function_Reputation; case 4: return Function_HealthPercent; case 5: return Function_PCReputation; case 6: return Function_PcLevel; case 7: return Function_PcHealthPercent; case 8: case 9: return Function_PcDynamicStat; case 10: return Function_PcAttribute; case 11: case 12: case 13: case 14: case 15: case 16: case 17: case 18: case 19: case 20: case 21: case 22: case 23: case 24: case 25: case 26: case 27: case 28: case 29: case 30: case 31: case 32: case 33: case 34: case 35: case 36: case 37: return Function_PcSkill; case 38: return Function_PcGender; case 39: return Function_PcExpelled; case 40: return Function_PcCommonDisease; case 41: return Function_PcBlightDisease; case 42: return Function_PcClothingModifier; case 43: return Function_PcCrimeLevel; case 44: return Function_SameGender; case 45: return Function_SameRace; case 46: return Function_SameFaction; case 47: return Function_FactionRankDiff; case 48: return Function_Detected; case 49: return Function_Alarmed; case 50: return Function_Choice; case 51: case 52: case 53: case 54: case 55: case 56: case 57: return Function_PcAttribute; case 58: return Function_PcCorprus; case 59: return Function_Weather; case 60: return Function_PcVampire; case 61: return Function_Level; case 62: return Function_Attacked; case 63: return Function_TalkedToPc; case 64: return Function_PcDynamicStat; case 65: return Function_CreatureTargetted; case 66: return Function_FriendlyHit; case 67: case 68: case 69: case 70: return Function_AiSetting; case 71: return Function_ShouldAttack; case 72: return Function_Werewolf; case 73: return Function_WerewolfKills; } return Function_False; } MWDialogue::SelectWrapper::SelectWrapper (const ESM::DialInfo::SelectStruct& select) : mSelect (select) {} MWDialogue::SelectWrapper::Function MWDialogue::SelectWrapper::getFunction() const { char type = mSelect.mSelectRule[1]; switch (type) { case '1': return decodeFunction(); case '2': return Function_Global; case '3': return Function_Local; case '4': return Function_Journal; case '5': return Function_Item; case '6': return Function_Dead; case '7': return Function_NotId; case '8': return Function_NotFaction; case '9': return Function_NotClass; case 'A': return Function_NotRace; case 'B': return Function_NotCell; case 'C': return Function_NotLocal; } return Function_None; } int MWDialogue::SelectWrapper::getArgument() const { if (mSelect.mSelectRule[1]!='1') return 0; int index = 0; std::istringstream (mSelect.mSelectRule.substr(2,2)) >> index; switch (index) { // AI settings case 67: return 1; case 68: return 0; case 69: return 3; case 70: return 2; // attributes case 10: return 0; case 51: return 1; case 52: return 2; case 53: return 3; case 54: return 4; case 55: return 5; case 56: return 6; case 57: return 7; // skills case 11: return 0; case 12: return 1; case 13: return 2; case 14: return 3; case 15: return 4; case 16: return 5; case 17: return 6; case 18: return 7; case 19: return 8; case 20: return 9; case 21: return 10; case 22: return 11; case 23: return 12; case 24: return 13; case 25: return 14; case 26: return 15; case 27: return 16; case 28: return 17; case 29: return 18; case 30: return 19; case 31: return 20; case 32: return 21; case 33: return 22; case 34: return 23; case 35: return 24; case 36: return 25; case 37: return 26; // dynamic stats case 8: return 1; case 9: return 2; case 64: return 0; } return 0; } MWDialogue::SelectWrapper::Type MWDialogue::SelectWrapper::getType() const { static const Function integerFunctions[] = { Function_Journal, Function_Item, Function_Dead, Function_Choice, Function_AiSetting, Function_PcAttribute, Function_PcSkill, Function_FriendlyHit, Function_PcLevel, Function_PcGender, Function_PcClothingModifier, Function_PcCrimeLevel, Function_RankRequirement, Function_Level, Function_PCReputation, Function_Weather, Function_Reputation, Function_FactionRankDiff, Function_WerewolfKills, Function_RankLow, Function_RankHigh, Function_CreatureTargetted, Function_None // end marker }; static const Function numericFunctions[] = { Function_Global, Function_Local, Function_NotLocal, Function_PcDynamicStat, Function_PcHealthPercent, Function_HealthPercent, Function_None // end marker }; static const Function booleanFunctions[] = { Function_False, Function_SameGender, Function_SameRace, Function_SameFaction, Function_PcCommonDisease, Function_PcBlightDisease, Function_PcCorprus, Function_PcExpelled, Function_PcVampire, Function_TalkedToPc, Function_Alarmed, Function_Detected, Function_Attacked, Function_ShouldAttack, Function_Werewolf, Function_None // end marker }; static const Function invertedBooleanFunctions[] = { Function_NotId, Function_NotFaction, Function_NotClass, Function_NotRace, Function_NotCell, Function_None // end marker }; Function function = getFunction(); for (int i=0; integerFunctions[i]!=Function_None; ++i) if (integerFunctions[i]==function) return Type_Integer; for (int i=0; numericFunctions[i]!=Function_None; ++i) if (numericFunctions[i]==function) return Type_Numeric; for (int i=0; booleanFunctions[i]!=Function_None; ++i) if (booleanFunctions[i]==function) return Type_Boolean; for (int i=0; invertedBooleanFunctions[i]!=Function_None; ++i) if (invertedBooleanFunctions[i]==function) return Type_Inverted; return Type_None; } bool MWDialogue::SelectWrapper::isNpcOnly() const { static const Function functions[] = { Function_NotFaction, Function_NotClass, Function_NotRace, Function_SameGender, Function_SameRace, Function_SameFaction, Function_RankRequirement, Function_Reputation, Function_FactionRankDiff, Function_Werewolf, Function_WerewolfKills, Function_RankLow, Function_RankHigh, Function_None // end marker }; Function function = getFunction(); for (int i=0; functions[i]!=Function_None; ++i) if (functions[i]==function) return true; return false; } bool MWDialogue::SelectWrapper::selectCompare (int value) const { return selectCompareImp (mSelect, value); } bool MWDialogue::SelectWrapper::selectCompare (float value) const { return selectCompareImp (mSelect, value); } bool MWDialogue::SelectWrapper::selectCompare (bool value) const { return selectCompareImp (mSelect, static_cast (value)); } std::string MWDialogue::SelectWrapper::getName() const { return Misc::StringUtils::lowerCase (mSelect.mSelectRule.substr (5)); } ================================================ FILE: apps/openmw/mwdialogue/selectwrapper.hpp ================================================ #ifndef GAME_MWDIALOGUE_SELECTWRAPPER_H #define GAME_MWDIALOGUE_SELECTWRAPPER_H #include namespace MWDialogue { class SelectWrapper { const ESM::DialInfo::SelectStruct& mSelect; public: enum Function { Function_None, Function_False, Function_Journal, Function_Item, Function_Dead, Function_NotId, Function_NotFaction, Function_NotClass, Function_NotRace, Function_NotCell, Function_NotLocal, Function_Local, Function_Global, Function_SameGender, Function_SameRace, Function_SameFaction, Function_Choice, Function_PcCommonDisease, Function_PcBlightDisease, Function_PcCorprus, Function_AiSetting, Function_PcAttribute, Function_PcSkill, Function_PcExpelled, Function_PcVampire, Function_FriendlyHit, Function_TalkedToPc, Function_PcLevel, Function_PcHealthPercent, Function_PcDynamicStat, Function_PcGender, Function_PcClothingModifier, Function_PcCrimeLevel, Function_RankRequirement, Function_HealthPercent, Function_Level, Function_PCReputation, Function_Weather, Function_Reputation, Function_Alarmed, Function_FactionRankDiff, Function_Detected, Function_Attacked, Function_ShouldAttack, Function_CreatureTargetted, Function_Werewolf, Function_WerewolfKills, Function_RankLow, Function_RankHigh }; enum Type { Type_None, Type_Integer, Type_Numeric, Type_Boolean, Type_Inverted }; private: Function decodeFunction() const; public: SelectWrapper (const ESM::DialInfo::SelectStruct& select); Function getFunction() const; int getArgument() const; Type getType() const; bool isNpcOnly() const; ///< \attention Do not call any of the select functions for this select struct! bool selectCompare (int value) const; bool selectCompare (float value) const; bool selectCompare (bool value) const; std::string getName() const; ///< Return case-smashed name. }; } #endif ================================================ FILE: apps/openmw/mwdialogue/topic.cpp ================================================ #include "topic.hpp" #include "../mwbase/environment.hpp" #include "../mwbase/world.hpp" #include "../mwworld/esmstore.hpp" namespace MWDialogue { Topic::Topic() {} Topic::Topic (const std::string& topic) : mTopic (topic), mName ( MWBase::Environment::get().getWorld()->getStore().get().find (topic)->mId) {} Topic::~Topic() {} void Topic::addEntry (const JournalEntry& entry) { if (entry.mTopic!=mTopic) throw std::runtime_error ("topic does not match: " + mTopic); // bail out if we already have heard this for (Topic::TEntryIter it = mEntries.begin(); it != mEntries.end(); ++it) { if (it->mInfoId == entry.mInfoId) return; } mEntries.push_back (entry); // we want slicing here } void Topic::insertEntry (const ESM::JournalEntry& entry) { mEntries.push_back (entry); } std::string Topic::getTopic() const { return mTopic; } std::string Topic::getName() const { return mName; } Topic::TEntryIter Topic::begin() const { return mEntries.begin(); } Topic::TEntryIter Topic::end() const { return mEntries.end(); } void Topic::removeLastAddedResponse (const std::string& actorName) { for (std::vector::reverse_iterator it = mEntries.rbegin(); it != mEntries.rend(); ++it) { if (it->mActorName == actorName) { mEntries.erase( (++it).base() ); // erase doesn't take a reverse_iterator return; } } } } ================================================ FILE: apps/openmw/mwdialogue/topic.hpp ================================================ #ifndef GAME_MWDIALOG_TOPIC_H #define GAME_MWDIALOG_TOPIC_H #include #include #include "journalentry.hpp" namespace ESM { struct JournalEntry; } namespace MWDialogue { /// \brief Collection of seen responses for a topic class Topic { public: typedef std::vector TEntryContainer; typedef TEntryContainer::const_iterator TEntryIter; protected: std::string mTopic; std::string mName; TEntryContainer mEntries; public: Topic(); Topic (const std::string& topic); virtual ~Topic(); virtual void addEntry (const JournalEntry& entry); ///< Add entry /// /// \note Redundant entries are ignored. void insertEntry (const ESM::JournalEntry& entry); ///< Add entry without checking for redundant entries or modifying the state of the /// topic otherwise std::string getTopic() const; virtual std::string getName() const; void removeLastAddedResponse (const std::string& actorName); TEntryIter begin() const; ///< Iterator pointing to the begin of the journal for this topic. TEntryIter end() const; ///< Iterator pointing past the end of the journal for this topic. }; } #endif ================================================ FILE: apps/openmw/mwgui/alchemywindow.cpp ================================================ #include "alchemywindow.hpp" #include #include #include #include #include #include /* Start of tes3mp addition Include additional headers for multiplayer purposes */ #include "../mwmp/Main.hpp" #include "../mwmp/Networking.hpp" #include "../mwmp/ObjectList.hpp" /* End of tes3mp addition */ #include "../mwbase/environment.hpp" #include "../mwbase/world.hpp" #include "../mwbase/windowmanager.hpp" #include "../mwmechanics/magiceffects.hpp" #include "../mwmechanics/alchemy.hpp" #include "../mwmechanics/actorutil.hpp" #include "../mwworld/class.hpp" #include "../mwworld/esmstore.hpp" #include #include #include "inventoryitemmodel.hpp" #include "sortfilteritemmodel.hpp" #include "itemview.hpp" #include "itemwidget.hpp" #include "widgets.hpp" namespace MWGui { AlchemyWindow::AlchemyWindow() : WindowBase("openmw_alchemy_window.layout") , mCurrentFilter(FilterType::ByName) , mModel(nullptr) , mSortModel(nullptr) , mAlchemy(new MWMechanics::Alchemy()) , mApparatus (4) , mIngredients (4) { getWidget(mCreateButton, "CreateButton"); getWidget(mCancelButton, "CancelButton"); getWidget(mIngredients[0], "Ingredient1"); getWidget(mIngredients[1], "Ingredient2"); getWidget(mIngredients[2], "Ingredient3"); getWidget(mIngredients[3], "Ingredient4"); getWidget(mApparatus[0], "Apparatus1"); getWidget(mApparatus[1], "Apparatus2"); getWidget(mApparatus[2], "Apparatus3"); getWidget(mApparatus[3], "Apparatus4"); getWidget(mEffectsBox, "CreatedEffects"); getWidget(mBrewCountEdit, "BrewCount"); getWidget(mIncreaseButton, "IncreaseButton"); getWidget(mDecreaseButton, "DecreaseButton"); getWidget(mNameEdit, "NameEdit"); getWidget(mItemView, "ItemView"); getWidget(mFilterValue, "FilterValue"); getWidget(mFilterType, "FilterType"); mBrewCountEdit->eventValueChanged += MyGUI::newDelegate(this, &AlchemyWindow::onCountValueChanged); mBrewCountEdit->eventEditSelectAccept += MyGUI::newDelegate(this, &AlchemyWindow::onAccept); mBrewCountEdit->setMinValue(1); mBrewCountEdit->setValue(1); mIncreaseButton->eventMouseButtonPressed += MyGUI::newDelegate(this, &AlchemyWindow::onIncreaseButtonPressed); mIncreaseButton->eventMouseButtonReleased += MyGUI::newDelegate(this, &AlchemyWindow::onCountButtonReleased); mDecreaseButton->eventMouseButtonPressed += MyGUI::newDelegate(this, &AlchemyWindow::onDecreaseButtonPressed); mDecreaseButton->eventMouseButtonReleased += MyGUI::newDelegate(this, &AlchemyWindow::onCountButtonReleased); mItemView->eventItemClicked += MyGUI::newDelegate(this, &AlchemyWindow::onSelectedItem); mIngredients[0]->eventMouseButtonClick += MyGUI::newDelegate(this, &AlchemyWindow::onIngredientSelected); mIngredients[1]->eventMouseButtonClick += MyGUI::newDelegate(this, &AlchemyWindow::onIngredientSelected); mIngredients[2]->eventMouseButtonClick += MyGUI::newDelegate(this, &AlchemyWindow::onIngredientSelected); mIngredients[3]->eventMouseButtonClick += MyGUI::newDelegate(this, &AlchemyWindow::onIngredientSelected); mCreateButton->eventMouseButtonClick += MyGUI::newDelegate(this, &AlchemyWindow::onCreateButtonClicked); mCancelButton->eventMouseButtonClick += MyGUI::newDelegate(this, &AlchemyWindow::onCancelButtonClicked); mNameEdit->eventEditSelectAccept += MyGUI::newDelegate(this, &AlchemyWindow::onAccept); mFilterValue->eventComboChangePosition += MyGUI::newDelegate(this, &AlchemyWindow::onFilterChanged); mFilterValue->eventEditTextChange += MyGUI::newDelegate(this, &AlchemyWindow::onFilterEdited); mFilterType->eventMouseButtonClick += MyGUI::newDelegate(this, &AlchemyWindow::switchFilterType); center(); } void AlchemyWindow::onAccept(MyGUI::EditBox* sender) { onCreateButtonClicked(sender); // To do not spam onAccept() again and again MWBase::Environment::get().getWindowManager()->injectKeyRelease(MyGUI::KeyCode::None); } void AlchemyWindow::onCancelButtonClicked(MyGUI::Widget* _sender) { MWBase::Environment::get().getWindowManager()->removeGuiMode(GM_Alchemy); } void AlchemyWindow::onCreateButtonClicked(MyGUI::Widget* _sender) { mAlchemy->setPotionName(mNameEdit->getCaption()); int count = mAlchemy->countPotionsToBrew(); count = std::min(count, mBrewCountEdit->getValue()); createPotions(count); } void AlchemyWindow::createPotions(int count) { MWMechanics::Alchemy::Result result = mAlchemy->create(mNameEdit->getCaption(), count); MWBase::WindowManager *winMgr = MWBase::Environment::get().getWindowManager(); /* Start of tes3mp addition Declare objectList here so we can use it below */ mwmp::ObjectList *objectList = mwmp::Main::get().getNetworking()->getObjectList(); /* End of tes3mp addition */ switch (result) { case MWMechanics::Alchemy::Result_NoName: winMgr->messageBox("#{sNotifyMessage37}"); break; case MWMechanics::Alchemy::Result_NoMortarAndPestle: winMgr->messageBox("#{sNotifyMessage45}"); break; case MWMechanics::Alchemy::Result_LessThanTwoIngredients: winMgr->messageBox("#{sNotifyMessage6a}"); break; case MWMechanics::Alchemy::Result_Success: winMgr->playSound("potion success"); /* Start of tes3mp addition Send an ID_OBJECT_SOUND packet every time the player makes a sound here */ objectList->reset(); objectList->packetOrigin = mwmp::CLIENT_GAMEPLAY; objectList->addObjectSound(MWMechanics::getPlayer(), "potion success", 1.0, 1.0); objectList->sendObjectSound(); /* End of tes3mp addition */ if (count == 1) winMgr->messageBox("#{sPotionSuccess}"); else winMgr->messageBox("#{sPotionSuccess} "+mNameEdit->getCaption()+" ("+std::to_string(count)+")"); break; case MWMechanics::Alchemy::Result_NoEffects: case MWMechanics::Alchemy::Result_RandomFailure: winMgr->messageBox("#{sNotifyMessage8}"); winMgr->playSound("potion fail"); /* Start of tes3mp addition Send an ID_OBJECT_SOUND packet every time the player makes a sound here */ objectList->reset(); objectList->packetOrigin = mwmp::CLIENT_GAMEPLAY; objectList->addObjectSound(MWMechanics::getPlayer(), "potion fail", 1.0, 1.0); objectList->sendObjectSound(); /* End of tes3mp addition */ break; } // remove ingredient slots that have been fully used up for (int i=0; i<4; ++i) if (mIngredients[i]->isUserString("ToolTipType")) { MWWorld::Ptr ingred = *mIngredients[i]->getUserData(); if (ingred.getRefData().getCount() == 0) removeIngredient(mIngredients[i]); } updateFilters(); update(); } void AlchemyWindow::initFilter() { auto const& wm = MWBase::Environment::get().getWindowManager(); auto const ingredient = wm->getGameSettingString("sIngredients", "Ingredients"); auto const effect = wm->getGameSettingString("sMagicEffects", "Magic Effects"); if (mFilterType->getCaption() == ingredient) mCurrentFilter = FilterType::ByName; else mCurrentFilter = FilterType::ByEffect; updateFilters(); mFilterValue->clearIndexSelected(); updateFilters(); } void AlchemyWindow::switchFilterType(MyGUI::Widget* _sender) { auto const& wm = MWBase::Environment::get().getWindowManager(); auto const ingredient = wm->getGameSettingString("sIngredients", "Ingredients"); auto const effect = wm->getGameSettingString("sMagicEffects", "Magic Effects"); auto *button = _sender->castType(); if (button->getCaption() == ingredient) { button->setCaption(effect); mCurrentFilter = FilterType::ByEffect; } else { button->setCaption(ingredient); mCurrentFilter = FilterType::ByName; } mSortModel->setNameFilter({}); mSortModel->setEffectFilter({}); mFilterValue->clearIndexSelected(); updateFilters(); mItemView->update(); } void AlchemyWindow::updateFilters() { std::set itemNames, itemEffects; for (size_t i = 0; i < mModel->getItemCount(); ++i) { MWWorld::Ptr item = mModel->getItem(i).mBase; if (item.getTypeName() != typeid(ESM::Ingredient).name()) continue; itemNames.insert(item.getClass().getName(item)); MWWorld::Ptr player = MWBase::Environment::get().getWorld ()->getPlayerPtr(); auto const alchemySkill = player.getClass().getSkill(player, ESM::Skill::Alchemy); auto const effects = MWMechanics::Alchemy::effectsDescription(item, alchemySkill); itemEffects.insert(effects.begin(), effects.end()); } mFilterValue->removeAllItems(); auto const addItems = [&](auto const& container) { for (auto const& item : container) mFilterValue->addItem(item); }; switch (mCurrentFilter) { case FilterType::ByName: addItems(itemNames); break; case FilterType::ByEffect: addItems(itemEffects); break; } } void AlchemyWindow::applyFilter(const std::string& filter) { switch (mCurrentFilter) { case FilterType::ByName: mSortModel->setNameFilter(filter); break; case FilterType::ByEffect: mSortModel->setEffectFilter(filter); break; } mItemView->update(); } void AlchemyWindow::onFilterChanged(MyGUI::ComboBox* _sender, size_t _index) { // ignore spurious event fired when one edit the content after selection. // onFilterEdited will handle it. if (_index != MyGUI::ITEM_NONE) applyFilter(_sender->getItemNameAt(_index)); } void AlchemyWindow::onFilterEdited(MyGUI::EditBox* _sender) { applyFilter(_sender->getCaption()); } void AlchemyWindow::onOpen() { mAlchemy->clear(); mAlchemy->setAlchemist (MWMechanics::getPlayer()); mModel = new InventoryItemModel(MWMechanics::getPlayer()); mSortModel = new SortFilterItemModel(mModel); mSortModel->setFilter(SortFilterItemModel::Filter_OnlyIngredients); mItemView->setModel (mSortModel); mItemView->resetScrollBars(); mNameEdit->setCaption(""); mBrewCountEdit->setValue(1); int index = 0; for (MWMechanics::Alchemy::TToolsIterator iter (mAlchemy->beginTools()); iter!=mAlchemy->endTools() && index (mApparatus.size()); ++iter, ++index) { mApparatus.at (index)->setItem(*iter); mApparatus.at (index)->clearUserStrings(); if (!iter->isEmpty()) { mApparatus.at (index)->setUserString ("ToolTipType", "ItemPtr"); mApparatus.at (index)->setUserData (MWWorld::Ptr(*iter)); } } update(); initFilter(); MWBase::Environment::get().getWindowManager()->setKeyFocusWidget(mNameEdit); } void AlchemyWindow::onIngredientSelected(MyGUI::Widget* _sender) { removeIngredient(_sender); update(); } void AlchemyWindow::onSelectedItem(int index) { MWWorld::Ptr item = mSortModel->getItem(index).mBase; int res = mAlchemy->addIngredient(item); if (res != -1) { update(); std::string sound = item.getClass().getUpSoundId(item); MWBase::Environment::get().getWindowManager()->playSound(sound); } } void AlchemyWindow::update() { std::string suggestedName = mAlchemy->suggestPotionName(); if (suggestedName != mSuggestedPotionName) mNameEdit->setCaptionWithReplacing(suggestedName); mSuggestedPotionName = suggestedName; mSortModel->clearDragItems(); MWMechanics::Alchemy::TIngredientsIterator it = mAlchemy->beginIngredients (); for (int i=0; i<4; ++i) { ItemWidget* ingredient = mIngredients[i]; MWWorld::Ptr item; if (it != mAlchemy->endIngredients ()) { item = *it; ++it; } if (!item.isEmpty()) mSortModel->addDragItem(item, item.getRefData().getCount()); if (ingredient->getChildCount()) MyGUI::Gui::getInstance().destroyWidget(ingredient->getChildAt(0)); ingredient->clearUserStrings (); ingredient->setItem(item); if (item.isEmpty ()) continue; ingredient->setUserString("ToolTipType", "ItemPtr"); ingredient->setUserData(MWWorld::Ptr(item)); ingredient->setCount(item.getRefData().getCount()); } mItemView->update(); std::set effectIds = mAlchemy->listEffects(); Widgets::SpellEffectList list; unsigned int effectIndex=0; for (const MWMechanics::EffectKey& effectKey : effectIds) { Widgets::SpellEffectParams params; params.mEffectID = effectKey.mId; const ESM::MagicEffect* magicEffect = MWBase::Environment::get().getWorld()->getStore().get().find(effectKey.mId); if (magicEffect->mData.mFlags & ESM::MagicEffect::TargetSkill) params.mSkill = effectKey.mArg; else if (magicEffect->mData.mFlags & ESM::MagicEffect::TargetAttribute) params.mAttribute = effectKey.mArg; params.mIsConstant = true; params.mNoTarget = true; params.mNoMagnitude = true; params.mKnown = mAlchemy->knownEffect(effectIndex, MWBase::Environment::get().getWorld()->getPlayerPtr()); list.push_back(params); ++effectIndex; } while (mEffectsBox->getChildCount()) MyGUI::Gui::getInstance().destroyWidget(mEffectsBox->getChildAt(0)); MyGUI::IntCoord coord(0, 0, mEffectsBox->getWidth(), 24); Widgets::MWEffectListPtr effectsWidget = mEffectsBox->createWidget ("MW_StatName", coord, MyGUI::Align::Left | MyGUI::Align::Top); effectsWidget->setEffectList(list); std::vector effectItems; effectsWidget->createEffectWidgets(effectItems, mEffectsBox, coord, false, 0); effectsWidget->setCoord(coord); } void AlchemyWindow::removeIngredient(MyGUI::Widget* ingredient) { for (int i=0; i<4; ++i) if (mIngredients[i] == ingredient) mAlchemy->removeIngredient (i); update(); } void AlchemyWindow::addRepeatController(MyGUI::Widget *widget) { MyGUI::ControllerItem* item = MyGUI::ControllerManager::getInstance().createItem(MyGUI::ControllerRepeatClick::getClassTypeName()); MyGUI::ControllerRepeatClick* controller = static_cast(item); controller->eventRepeatClick += newDelegate(this, &AlchemyWindow::onRepeatClick); MyGUI::ControllerManager::getInstance().addItem(widget, controller); } void AlchemyWindow::onIncreaseButtonPressed(MyGUI::Widget* _sender, int _left, int _top, MyGUI::MouseButton _id) { addRepeatController(_sender); onIncreaseButtonTriggered(); } void AlchemyWindow::onDecreaseButtonPressed(MyGUI::Widget* _sender, int _left, int _top, MyGUI::MouseButton _id) { addRepeatController(_sender); onDecreaseButtonTriggered(); } void AlchemyWindow::onRepeatClick(MyGUI::Widget* widget, MyGUI::ControllerItem* controller) { if (widget == mIncreaseButton) onIncreaseButtonTriggered(); else if (widget == mDecreaseButton) onDecreaseButtonTriggered(); } void AlchemyWindow::onCountButtonReleased(MyGUI::Widget *_sender, int _left, int _top, MyGUI::MouseButton _id) { MyGUI::ControllerManager::getInstance().removeItem(_sender); } void AlchemyWindow::onCountValueChanged(int value) { mBrewCountEdit->setValue(std::abs(value)); } void AlchemyWindow::onIncreaseButtonTriggered() { int currentCount = mBrewCountEdit->getValue(); // prevent overflows if (currentCount == std::numeric_limits::max()) return; mBrewCountEdit->setValue(currentCount+1); } void AlchemyWindow::onDecreaseButtonTriggered() { int currentCount = mBrewCountEdit->getValue(); if (currentCount > 1) mBrewCountEdit->setValue(currentCount-1); } } ================================================ FILE: apps/openmw/mwgui/alchemywindow.hpp ================================================ #ifndef MWGUI_ALCHEMY_H #define MWGUI_ALCHEMY_H #include #include #include #include #include #include #include "windowbase.hpp" namespace MWMechanics { class Alchemy; } namespace MWGui { class ItemView; class ItemWidget; class InventoryItemModel; class SortFilterItemModel; class AlchemyWindow : public WindowBase { public: AlchemyWindow(); void onOpen() override; void onResChange(int, int) override { center(); } private: static const float sCountChangeInitialPause; // in seconds static const float sCountChangeInterval; // in seconds std::string mSuggestedPotionName; enum class FilterType { ByName, ByEffect }; FilterType mCurrentFilter; ItemView* mItemView; InventoryItemModel* mModel; SortFilterItemModel* mSortModel; MyGUI::Button* mCreateButton; MyGUI::Button* mCancelButton; MyGUI::Widget* mEffectsBox; MyGUI::Button* mIncreaseButton; MyGUI::Button* mDecreaseButton; Gui::AutoSizedButton* mFilterType; MyGUI::ComboBox* mFilterValue; MyGUI::EditBox* mNameEdit; Gui::NumericEditBox* mBrewCountEdit; void onCancelButtonClicked(MyGUI::Widget* _sender); void onCreateButtonClicked(MyGUI::Widget* _sender); void onIngredientSelected(MyGUI::Widget* _sender); void onAccept(MyGUI::EditBox*); void onIncreaseButtonPressed(MyGUI::Widget* _sender, int _left, int _top, MyGUI::MouseButton _id); void onDecreaseButtonPressed(MyGUI::Widget* _sender, int _left, int _top, MyGUI::MouseButton _id); void onCountButtonReleased(MyGUI::Widget* _sender, int _left, int _top, MyGUI::MouseButton _id); void onCountValueChanged(int value); void onRepeatClick(MyGUI::Widget* widget, MyGUI::ControllerItem* controller); void applyFilter(const std::string& filter); void initFilter(); void onFilterChanged(MyGUI::ComboBox* _sender, size_t _index); void onFilterEdited(MyGUI::EditBox* _sender); void switchFilterType(MyGUI::Widget* _sender); void updateFilters(); void addRepeatController(MyGUI::Widget* widget); void onIncreaseButtonTriggered(); void onDecreaseButtonTriggered(); void onSelectedItem(int index); void removeIngredient(MyGUI::Widget* ingredient); void createPotions(int count); void update(); std::unique_ptr mAlchemy; std::vector mApparatus; std::vector mIngredients; }; } #endif ================================================ FILE: apps/openmw/mwgui/backgroundimage.cpp ================================================ #include "backgroundimage.hpp" #include namespace MWGui { void BackgroundImage::setBackgroundImage (const std::string& image, bool fixedRatio, bool stretch) { if (mChild) { MyGUI::Gui::getInstance().destroyWidget(mChild); mChild = nullptr; } if (!stretch) { setImageTexture("black"); if (fixedRatio) mAspect = 4.0/3.0; else mAspect = 0; // TODO mChild = createWidgetReal("ImageBox", MyGUI::FloatCoord(0,0,1,1), MyGUI::Align::Default); mChild->setImageTexture(image); adjustSize(); } else { mAspect = 0; setImageTexture(image); } } void BackgroundImage::adjustSize() { if (mAspect == 0) return; MyGUI::IntSize screenSize = getSize(); int leftPadding = std::max(0, static_cast(screenSize.width - screenSize.height * mAspect) / 2); int topPadding = std::max(0, static_cast(screenSize.height - screenSize.width / mAspect) / 2); mChild->setCoord(leftPadding, topPadding, screenSize.width - leftPadding*2, screenSize.height - topPadding*2); } void BackgroundImage::setSize (const MyGUI::IntSize& _value) { MyGUI::Widget::setSize (_value); adjustSize(); } void BackgroundImage::setCoord (const MyGUI::IntCoord& _value) { MyGUI::Widget::setCoord (_value); adjustSize(); } } ================================================ FILE: apps/openmw/mwgui/backgroundimage.hpp ================================================ #ifndef OPENMW_MWGUI_BACKGROUNDIMAGE_H #define OPENMW_MWGUI_BACKGROUNDIMAGE_H #include namespace MWGui { /** * @brief A variant of MyGUI::ImageBox with aspect ratio correction using black bars */ class BackgroundImage final : public MyGUI::ImageBox { MYGUI_RTTI_DERIVED(BackgroundImage) public: BackgroundImage() : mChild(nullptr), mAspect(0) {} /** * @param fixedRatio Use a fixed ratio of 4:3, regardless of the image dimensions * @param stretch Stretch to fill the whole screen, or add black bars? */ void setBackgroundImage (const std::string& image, bool fixedRatio=true, bool stretch=true); void setSize (const MyGUI::IntSize &_value) override; void setCoord (const MyGUI::IntCoord &_value) override; private: MyGUI::ImageBox* mChild; double mAspect; void adjustSize(); }; } #endif ================================================ FILE: apps/openmw/mwgui/birth.cpp ================================================ #include "birth.hpp" #include #include #include #include #include "../mwbase/environment.hpp" #include "../mwbase/world.hpp" #include "../mwbase/windowmanager.hpp" #include "../mwworld/esmstore.hpp" #include "../mwworld/player.hpp" #include "widgets.hpp" namespace { bool sortBirthSigns(const std::pair& left, const std::pair& right) { return left.second->mName.compare (right.second->mName) < 0; } } namespace MWGui { BirthDialog::BirthDialog() : WindowModal("openmw_chargen_birth.layout") { // Centre dialog center(); getWidget(mSpellArea, "SpellArea"); getWidget(mBirthImage, "BirthsignImage"); getWidget(mBirthList, "BirthsignList"); mBirthList->setScrollVisible(true); mBirthList->eventListSelectAccept += MyGUI::newDelegate(this, &BirthDialog::onAccept); mBirthList->eventListChangePosition += MyGUI::newDelegate(this, &BirthDialog::onSelectBirth); MyGUI::Button* backButton; getWidget(backButton, "BackButton"); backButton->eventMouseButtonClick += MyGUI::newDelegate(this, &BirthDialog::onBackClicked); MyGUI::Button* okButton; getWidget(okButton, "OKButton"); okButton->setCaption(MWBase::Environment::get().getWindowManager()->getGameSettingString("sOK", "")); okButton->eventMouseButtonClick += MyGUI::newDelegate(this, &BirthDialog::onOkClicked); updateBirths(); updateSpells(); } void BirthDialog::setNextButtonShow(bool shown) { MyGUI::Button* okButton; getWidget(okButton, "OKButton"); if (shown) okButton->setCaption(MWBase::Environment::get().getWindowManager()->getGameSettingString("sNext", "")); else okButton->setCaption(MWBase::Environment::get().getWindowManager()->getGameSettingString("sOK", "")); } void BirthDialog::onOpen() { WindowModal::onOpen(); updateBirths(); updateSpells(); MWBase::Environment::get().getWindowManager()->setKeyFocusWidget(mBirthList); // Show the current birthsign by default const std::string &signId = MWBase::Environment::get().getWorld()->getPlayer().getBirthSign(); if (!signId.empty()) setBirthId(signId); } void BirthDialog::setBirthId(const std::string &birthId) { mCurrentBirthId = birthId; mBirthList->setIndexSelected(MyGUI::ITEM_NONE); size_t count = mBirthList->getItemCount(); for (size_t i = 0; i < count; ++i) { if (Misc::StringUtils::ciEqual(*mBirthList->getItemDataAt(i), birthId)) { mBirthList->setIndexSelected(i); break; } } updateSpells(); } // widget controls void BirthDialog::onOkClicked(MyGUI::Widget* _sender) { if(mBirthList->getIndexSelected() == MyGUI::ITEM_NONE) return; eventDone(this); } void BirthDialog::onAccept(MyGUI::ListBox *_sender, size_t _index) { onSelectBirth(_sender, _index); if(mBirthList->getIndexSelected() == MyGUI::ITEM_NONE) return; eventDone(this); } void BirthDialog::onBackClicked(MyGUI::Widget* _sender) { eventBack(); } void BirthDialog::onSelectBirth(MyGUI::ListBox* _sender, size_t _index) { if (_index == MyGUI::ITEM_NONE) return; const std::string *birthId = mBirthList->getItemDataAt(_index); if (Misc::StringUtils::ciEqual(mCurrentBirthId, *birthId)) return; mCurrentBirthId = *birthId; updateSpells(); } // update widget content void BirthDialog::updateBirths() { mBirthList->removeAllItems(); const MWWorld::Store &signs = MWBase::Environment::get().getWorld()->getStore().get(); // sort by name std::vector < std::pair > birthSigns; for (const ESM::BirthSign& sign : signs) { birthSigns.emplace_back(sign.mId, &sign); } std::sort(birthSigns.begin(), birthSigns.end(), sortBirthSigns); int index = 0; for (auto& birthsignPair : birthSigns) { mBirthList->addItem(birthsignPair.second->mName, birthsignPair.first); if (mCurrentBirthId.empty()) { mBirthList->setIndexSelected(index); mCurrentBirthId = birthsignPair.first; } else if (Misc::StringUtils::ciEqual(birthsignPair.first, mCurrentBirthId)) { mBirthList->setIndexSelected(index); } index++; } } void BirthDialog::updateSpells() { for (MyGUI::Widget* widget : mSpellItems) { MyGUI::Gui::getInstance().destroyWidget(widget); } mSpellItems.clear(); if (mCurrentBirthId.empty()) return; Widgets::MWSpellPtr spellWidget; const int lineHeight = 18; MyGUI::IntCoord coord(0, 0, mSpellArea->getWidth(), 18); const MWWorld::ESMStore &store = MWBase::Environment::get().getWorld()->getStore(); const ESM::BirthSign *birth = store.get().find(mCurrentBirthId); mBirthImage->setImageTexture(MWBase::Environment::get().getWindowManager()->correctTexturePath(birth->mTexture)); std::vector abilities, powers, spells; std::vector::const_iterator it = birth->mPowers.mList.begin(); std::vector::const_iterator end = birth->mPowers.mList.end(); for (; it != end; ++it) { const std::string &spellId = *it; const ESM::Spell *spell = store.get().search(spellId); if (!spell) continue; // Skip spells which cannot be found ESM::Spell::SpellType type = static_cast(spell->mData.mType); if (type != ESM::Spell::ST_Spell && type != ESM::Spell::ST_Ability && type != ESM::Spell::ST_Power) continue; // We only want spell, ability and powers. if (type == ESM::Spell::ST_Ability) abilities.push_back(spellId); else if (type == ESM::Spell::ST_Power) powers.push_back(spellId); else if (type == ESM::Spell::ST_Spell) spells.push_back(spellId); } int i = 0; struct { const std::vector &spells; const char *label; } categories[3] = { {abilities, "sBirthsignmenu1"}, {powers, "sPowers"}, {spells, "sBirthsignmenu2"} }; for (int category = 0; category < 3; ++category) { if (!categories[category].spells.empty()) { MyGUI::TextBox* label = mSpellArea->createWidget("SandBrightText", coord, MyGUI::Align::Default, std::string("Label")); label->setCaption(MWBase::Environment::get().getWindowManager()->getGameSettingString(categories[category].label, "")); mSpellItems.push_back(label); coord.top += lineHeight; end = categories[category].spells.end(); for (it = categories[category].spells.begin(); it != end; ++it) { const std::string &spellId = *it; spellWidget = mSpellArea->createWidget("MW_StatName", coord, MyGUI::Align::Default, std::string("Spell") + MyGUI::utility::toString(i)); spellWidget->setSpellId(spellId); mSpellItems.push_back(spellWidget); coord.top += lineHeight; MyGUI::IntCoord spellCoord = coord; spellCoord.height = 24; // TODO: This should be fetched from the skin somehow, or perhaps a widget in the layout as a template? spellWidget->createEffectWidgets(mSpellItems, mSpellArea, spellCoord, (category == 0) ? Widgets::MWEffectList::EF_Constant : 0); coord.top = spellCoord.top; ++i; } } } // Canvas size must be expressed with VScroll disabled, otherwise MyGUI would expand the scroll area when the scrollbar is hidden mSpellArea->setVisibleVScroll(false); mSpellArea->setCanvasSize(MyGUI::IntSize(mSpellArea->getWidth(), std::max(mSpellArea->getHeight(), coord.top))); mSpellArea->setVisibleVScroll(true); mSpellArea->setViewOffset(MyGUI::IntPoint(0, 0)); } } ================================================ FILE: apps/openmw/mwgui/birth.hpp ================================================ #ifndef MWGUI_BIRTH_H #define MWGUI_BIRTH_H #include "windowbase.hpp" namespace MWGui { class BirthDialog : public WindowModal { public: BirthDialog(); enum Gender { GM_Male, GM_Female }; const std::string &getBirthId() const { return mCurrentBirthId; } void setBirthId(const std::string &raceId); void setNextButtonShow(bool shown); void onOpen() override; bool exit() override { return false; } // Events typedef MyGUI::delegates::CMultiDelegate0 EventHandle_Void; /** Event : Back button clicked.\n signature : void method()\n */ EventHandle_Void eventBack; /** Event : Dialog finished, OK button clicked.\n signature : void method()\n */ EventHandle_WindowBase eventDone; protected: void onSelectBirth(MyGUI::ListBox* _sender, size_t _index); void onAccept(MyGUI::ListBox* _sender, size_t index); void onOkClicked(MyGUI::Widget* _sender); void onBackClicked(MyGUI::Widget* _sender); private: void updateBirths(); void updateSpells(); MyGUI::ListBox* mBirthList; MyGUI::ScrollView* mSpellArea; MyGUI::ImageBox* mBirthImage; std::vector mSpellItems; std::string mCurrentBirthId; }; } #endif ================================================ FILE: apps/openmw/mwgui/bookpage.cpp ================================================ #include "bookpage.hpp" #include #include "MyGUI_RenderItem.h" #include "MyGUI_RenderManager.h" #include "MyGUI_TextureUtility.h" #include "MyGUI_FactoryManager.h" #include #include "../mwbase/environment.hpp" #include "../mwbase/windowmanager.hpp" namespace MWGui { struct TypesetBookImpl; class PageDisplay; class BookPageImpl; static bool ucsSpace (int codePoint); static bool ucsLineBreak (int codePoint); static bool ucsCarriageReturn (int codePoint); static bool ucsBreakingSpace (int codePoint); struct BookTypesetter::Style { virtual ~Style () {} }; struct TypesetBookImpl : TypesetBook { typedef std::vector Content; typedef std::list Contents; typedef Utf8Stream::Point Utf8Point; typedef std::pair Range; struct StyleImpl : BookTypesetter::Style { MyGUI::IFont* mFont; MyGUI::Colour mHotColour; MyGUI::Colour mActiveColour; MyGUI::Colour mNormalColour; InteractiveId mInteractiveId; bool match (MyGUI::IFont* tstFont, const MyGUI::Colour& tstHotColour, const MyGUI::Colour& tstActiveColour, const MyGUI::Colour& tstNormalColour, intptr_t tstInteractiveId) { return (mFont == tstFont) && partal_match (tstHotColour, tstActiveColour, tstNormalColour, tstInteractiveId); } bool match (char const * tstFont, const MyGUI::Colour& tstHotColour, const MyGUI::Colour& tstActiveColour, const MyGUI::Colour& tstNormalColour, intptr_t tstInteractiveId) { return (mFont->getResourceName () == tstFont) && partal_match (tstHotColour, tstActiveColour, tstNormalColour, tstInteractiveId); } bool partal_match (const MyGUI::Colour& tstHotColour, const MyGUI::Colour& tstActiveColour, const MyGUI::Colour& tstNormalColour, intptr_t tstInteractiveId) { return (mHotColour == tstHotColour ) && (mActiveColour == tstActiveColour ) && (mNormalColour == tstNormalColour ) && (mInteractiveId == tstInteractiveId ) ; } }; typedef std::list Styles; struct Run { StyleImpl* mStyle; Range mRange; int mLeft, mRight; int mPrintableChars; }; typedef std::vector Runs; struct Line { Runs mRuns; MyGUI::IntRect mRect; }; typedef std::vector Lines; struct Section { Lines mLines; MyGUI::IntRect mRect; }; typedef std::vector

Sections; // Holds "top" and "bottom" vertical coordinates in the source text. // A page is basically a "window" into a portion of the source text, similar to a ScrollView. typedef std::pair Page; typedef std::vector Pages; Pages mPages; Sections mSections; Contents mContents; Styles mStyles; MyGUI::IntRect mRect; virtual ~TypesetBookImpl () {} Range addContent (BookTypesetter::Utf8Span text) { Contents::iterator i = mContents.insert (mContents.end (), Content (text.first, text.second)); if (i->empty()) return Range (Utf8Point (nullptr), Utf8Point (nullptr)); return Range (i->data(), i->data() + i->size()); } size_t pageCount () const override { return mPages.size (); } std::pair getSize () const override { return std::make_pair (mRect.width (), mRect.height ()); } template void visitRuns (int top, int bottom, MyGUI::IFont* Font, Visitor const & visitor) const { for (Sections::const_iterator i = mSections.begin (); i != mSections.end (); ++i) { if (top >= mRect.bottom || bottom <= i->mRect.top) continue; for (Lines::const_iterator j = i->mLines.begin (); j != i->mLines.end (); ++j) { if (top >= j->mRect.bottom || bottom <= j->mRect.top) continue; for (Runs::const_iterator k = j->mRuns.begin (); k != j->mRuns.end (); ++k) if (!Font || k->mStyle->mFont == Font) visitor (*i, *j, *k); } } } template void visitRuns (int top, int bottom, Visitor const & visitor) const { visitRuns (top, bottom, nullptr, visitor); } /// hit test with a margin for error. only hits on interactive text fragments are reported. StyleImpl * hitTestWithMargin (int left, int top) { StyleImpl * hit = hitTest(left, top); if (hit && hit->mInteractiveId != 0) return hit; const int maxMargin = 10; for (int margin=1; margin < maxMargin; ++margin) { for (int i=0; i<4; ++i) { if (i==0) hit = hitTest(left, top-margin); else if (i==1) hit = hitTest(left, top+margin); else if (i==2) hit = hitTest(left-margin, top); else hit = hitTest(left+margin, top); if (hit && hit->mInteractiveId != 0) return hit; } } return nullptr; } StyleImpl * hitTest (int left, int top) const { for (Sections::const_iterator i = mSections.begin (); i != mSections.end (); ++i) { if (top < i->mRect.top || top >= i->mRect.bottom) continue; int left1 = left - i->mRect.left; for (Lines::const_iterator j = i->mLines.begin (); j != i->mLines.end (); ++j) { if (top < j->mRect.top || top >= j->mRect.bottom) continue; int left2 = left1 - j->mRect.left; for (Runs::const_iterator k = j->mRuns.begin (); k != j->mRuns.end (); ++k) { if (left2 < k->mLeft || left2 >= k->mRight) continue; return k->mStyle; } } } return nullptr; } MyGUI::IFont* affectedFont (StyleImpl* style) { for (Styles::iterator i = mStyles.begin (); i != mStyles.end (); ++i) if (&*i == style) return i->mFont; return nullptr; } struct Typesetter; }; struct TypesetBookImpl::Typesetter : BookTypesetter { struct PartialText { StyleImpl *mStyle; Utf8Stream::Point mBegin; Utf8Stream::Point mEnd; int mWidth; PartialText( StyleImpl *style, Utf8Stream::Point begin, Utf8Stream::Point end, int width) : mStyle(style), mBegin(begin), mEnd(end), mWidth(width) {} }; typedef TypesetBookImpl Book; typedef std::shared_ptr BookPtr; typedef std::vector::const_iterator PartialTextConstIterator; int mPageWidth; int mPageHeight; BookPtr mBook; Section * mSection; Line * mLine; Run * mRun; std::vector mSectionAlignment; std::vector mPartialWhitespace; std::vector mPartialWord; Book::Content const * mCurrentContent; Alignment mCurrentAlignment; Typesetter (size_t width, size_t height) : mPageWidth (width), mPageHeight(height), mSection (nullptr), mLine (nullptr), mRun (nullptr), mCurrentContent (nullptr), mCurrentAlignment (AlignLeft) { mBook = std::make_shared (); } virtual ~Typesetter () { } Style * createStyle (const std::string& fontName, const Colour& fontColour, bool useBookFont) override { std::string fullFontName; if (fontName.empty()) fullFontName = MyGUI::FontManager::getInstance().getDefaultFont(); else fullFontName = fontName; if (useBookFont) fullFontName = "Journalbook " + fullFontName; for (Styles::iterator i = mBook->mStyles.begin (); i != mBook->mStyles.end (); ++i) if (i->match (fullFontName.c_str(), fontColour, fontColour, fontColour, 0)) return &*i; MyGUI::IFont* font = MyGUI::FontManager::getInstance().getByName(fullFontName); if (!font) throw std::runtime_error(std::string("can't find font ") + fullFontName); StyleImpl & style = *mBook->mStyles.insert (mBook->mStyles.end (), StyleImpl ()); style.mFont = font; style.mHotColour = fontColour; style.mActiveColour = fontColour; style.mNormalColour = fontColour; style.mInteractiveId = 0; return &style; } Style* createHotStyle (Style* baseStyle, const Colour& normalColour, const Colour& hoverColour, const Colour& activeColour, InteractiveId id, bool unique) override { StyleImpl* BaseStyle = static_cast (baseStyle); if (!unique) for (Styles::iterator i = mBook->mStyles.begin (); i != mBook->mStyles.end (); ++i) if (i->match (BaseStyle->mFont, hoverColour, activeColour, normalColour, id)) return &*i; StyleImpl & style = *mBook->mStyles.insert (mBook->mStyles.end (), StyleImpl ()); style.mFont = BaseStyle->mFont; style.mHotColour = hoverColour; style.mActiveColour = activeColour; style.mNormalColour = normalColour; style.mInteractiveId = id; return &style; } void write (Style * style, Utf8Span text) override { Range range = mBook->addContent (text); writeImpl (static_cast (style), range.first, range.second); } intptr_t addContent (Utf8Span text, bool select) override { add_partial_text(); Contents::iterator i = mBook->mContents.insert (mBook->mContents.end (), Content (text.first, text.second)); if (select) mCurrentContent = &(*i); return reinterpret_cast (&(*i)); } void selectContent (intptr_t contentHandle) override { add_partial_text(); mCurrentContent = reinterpret_cast (contentHandle); } void write (Style * style, size_t begin, size_t end) override { assert (mCurrentContent != nullptr); assert (end <= mCurrentContent->size ()); assert (begin <= mCurrentContent->size ()); Utf8Point begin_ = mCurrentContent->data() + begin; Utf8Point end_ = mCurrentContent->data() + end; writeImpl (static_cast (style), begin_, end_); } void lineBreak (float margin) override { assert (margin == 0); //TODO: figure out proper behavior here... add_partial_text(); mRun = nullptr; mLine = nullptr; } void sectionBreak (int margin) override { add_partial_text(); if (mBook->mSections.size () > 0) { mRun = nullptr; mLine = nullptr; mSection = nullptr; if (mBook->mRect.bottom < (mBook->mSections.back ().mRect.bottom + margin)) mBook->mRect.bottom = (mBook->mSections.back ().mRect.bottom + margin); } } void setSectionAlignment (Alignment sectionAlignment) override { add_partial_text(); if (mSection != nullptr) mSectionAlignment.back () = sectionAlignment; mCurrentAlignment = sectionAlignment; } TypesetBook::Ptr complete () override { int curPageStart = 0; int curPageStop = 0; add_partial_text(); std::vector ::iterator sa = mSectionAlignment.begin (); for (Sections::iterator i = mBook->mSections.begin (); i != mBook->mSections.end (); ++i, ++sa) { // apply alignment to individual lines... for (Lines::iterator j = i->mLines.begin (); j != i->mLines.end (); ++j) { int width = j->mRect.width (); int excess = mPageWidth - width; switch (*sa) { default: case AlignLeft: j->mRect.left = 0; break; case AlignCenter: j->mRect.left = excess/2; break; case AlignRight: j->mRect.left = excess; break; } j->mRect.right = j->mRect.left + width; } if (curPageStop == curPageStart) { curPageStart = i->mRect.top; curPageStop = i->mRect.top; } int spaceLeft = mPageHeight - (curPageStop - curPageStart); int sectionHeight = i->mRect.height (); // This is NOT equal to i->mRect.height(), which doesn't account for section breaks. int spaceRequired = (i->mRect.bottom - curPageStop); if (curPageStart == curPageStop) // If this is a new page, the section break is not needed spaceRequired = i->mRect.height(); if (spaceRequired <= mPageHeight) { if (spaceRequired > spaceLeft) { // The section won't completely fit on the current page. Finish the current page and start a new one. assert (curPageStart != curPageStop); mBook->mPages.push_back (Page (curPageStart, curPageStop)); curPageStart = i->mRect.top; curPageStop = i->mRect.bottom; } else curPageStop = i->mRect.bottom; } else { // The section won't completely fit on the current page. Finish the current page and start a new one. mBook->mPages.push_back (Page (curPageStart, curPageStop)); curPageStart = i->mRect.top; curPageStop = i->mRect.bottom; //split section int sectionHeightLeft = sectionHeight; while (sectionHeightLeft >= mPageHeight) { // Adjust to the top of the first line that does not fit on the current page anymore int splitPos = curPageStop; for (Lines::iterator j = i->mLines.begin (); j != i->mLines.end (); ++j) { if (j->mRect.bottom > curPageStart + mPageHeight) { splitPos = j->mRect.top; break; } } mBook->mPages.push_back (Page (curPageStart, splitPos)); curPageStart = splitPos; curPageStop = splitPos; sectionHeightLeft = (i->mRect.bottom - splitPos); } curPageStop = i->mRect.bottom; } } if (curPageStart != curPageStop) mBook->mPages.push_back (Page (curPageStart, curPageStop)); return mBook; } void writeImpl (StyleImpl * style, Utf8Stream::Point _begin, Utf8Stream::Point _end) { Utf8Stream stream (_begin, _end); while (!stream.eof ()) { if (ucsLineBreak (stream.peek ())) { add_partial_text(); stream.consume (); mLine = nullptr, mRun = nullptr; continue; } if (ucsBreakingSpace (stream.peek ()) && !mPartialWord.empty()) add_partial_text(); int word_width = 0; int space_width = 0; Utf8Stream::Point lead = stream.current (); while (!stream.eof () && !ucsLineBreak (stream.peek ()) && ucsBreakingSpace (stream.peek ())) { MWGui::GlyphInfo info = GlyphInfo(style->mFont, stream.peek()); if (info.charFound) space_width += static_cast(info.advance + info.bearingX); stream.consume (); } Utf8Stream::Point origin = stream.current (); while (!stream.eof () && !ucsLineBreak (stream.peek ()) && !ucsBreakingSpace (stream.peek ())) { MWGui::GlyphInfo info = GlyphInfo(style->mFont, stream.peek()); if (info.charFound) word_width += static_cast(info.advance + info.bearingX); stream.consume (); } Utf8Stream::Point extent = stream.current (); if (lead == extent) break; if ( lead != origin ) mPartialWhitespace.emplace_back(style, lead, origin, space_width); if ( origin != extent ) mPartialWord.emplace_back(style, origin, extent, word_width); } } void add_partial_text () { if (mPartialWhitespace.empty() && mPartialWord.empty()) return; int fontHeight = MWBase::Environment::get().getWindowManager()->getFontHeight(); int space_width = 0; int word_width = 0; for (PartialTextConstIterator i = mPartialWhitespace.begin (); i != mPartialWhitespace.end (); ++i) space_width += i->mWidth; for (PartialTextConstIterator i = mPartialWord.begin (); i != mPartialWord.end (); ++i) word_width += i->mWidth; int left = mLine ? mLine->mRect.right : 0; if (left + space_width + word_width > mPageWidth) { mLine = nullptr, mRun = nullptr, left = 0; } else { for (PartialTextConstIterator i = mPartialWhitespace.begin (); i != mPartialWhitespace.end (); ++i) { int top = mLine ? mLine->mRect.top : mBook->mRect.bottom; append_run ( i->mStyle, i->mBegin, i->mEnd, 0, left + i->mWidth, top + fontHeight); left = mLine->mRect.right; } } for (PartialTextConstIterator i = mPartialWord.begin (); i != mPartialWord.end (); ++i) { int top = mLine ? mLine->mRect.top : mBook->mRect.bottom; append_run (i->mStyle, i->mBegin, i->mEnd, i->mEnd - i->mBegin, left + i->mWidth, top + fontHeight); left = mLine->mRect.right; } mPartialWhitespace.clear(); mPartialWord.clear(); } void append_run (StyleImpl * style, Utf8Stream::Point begin, Utf8Stream::Point end, int pc, int right, int bottom) { if (mSection == nullptr) { mBook->mSections.push_back (Section ()); mSection = &mBook->mSections.back (); mSection->mRect = MyGUI::IntRect (0, mBook->mRect.bottom, 0, mBook->mRect.bottom); mSectionAlignment.push_back (mCurrentAlignment); } if (mLine == nullptr) { mSection->mLines.push_back (Line ()); mLine = &mSection->mLines.back (); mLine->mRect = MyGUI::IntRect (0, mSection->mRect.bottom, 0, mBook->mRect.bottom); } if (mBook->mRect.right < right) mBook->mRect.right = right; if (mBook->mRect.bottom < bottom) mBook->mRect.bottom = bottom; if (mSection->mRect.right < right) mSection->mRect.right = right; if (mSection->mRect.bottom < bottom) mSection->mRect.bottom = bottom; if (mLine->mRect.right < right) mLine->mRect.right = right; if (mLine->mRect.bottom < bottom) mLine->mRect.bottom = bottom; if (mRun == nullptr || mRun->mStyle != style || mRun->mRange.second != begin) { int left = mRun ? mRun->mRight : mLine->mRect.left; mLine->mRuns.push_back (Run ()); mRun = &mLine->mRuns.back (); mRun->mStyle = style; mRun->mLeft = left; mRun->mRight = right; mRun->mRange.first = begin; mRun->mRange.second = end; mRun->mPrintableChars = pc; //Run->Locale = Locale; } else { mRun->mRight = right; mRun->mRange.second = end; mRun->mPrintableChars += pc; } } }; BookTypesetter::Ptr BookTypesetter::create (int pageWidth, int pageHeight) { return std::make_shared (pageWidth, pageHeight); } namespace { struct RenderXform { public: float clipTop; float clipLeft; float clipRight; float clipBottom; float absoluteLeft; float absoluteTop; float leftOffset; float topOffset; float pixScaleX; float pixScaleY; float hOffset; float vOffset; RenderXform (MyGUI::ICroppedRectangle* croppedParent, MyGUI::RenderTargetInfo const & renderTargetInfo) { clipTop = static_cast(croppedParent->_getMarginTop()); clipLeft = static_cast(croppedParent->_getMarginLeft ()); clipRight = static_cast(croppedParent->getWidth () - croppedParent->_getMarginRight ()); clipBottom = static_cast(croppedParent->getHeight() - croppedParent->_getMarginBottom()); absoluteLeft = static_cast(croppedParent->getAbsoluteLeft()); absoluteTop = static_cast(croppedParent->getAbsoluteTop()); leftOffset = static_cast(renderTargetInfo.leftOffset); topOffset = static_cast(renderTargetInfo.topOffset); pixScaleX = renderTargetInfo.pixScaleX; pixScaleY = renderTargetInfo.pixScaleY; hOffset = renderTargetInfo.hOffset; vOffset = renderTargetInfo.vOffset; } bool clip (MyGUI::FloatRect & vr, MyGUI::FloatRect & tr) { if (vr.bottom <= clipTop || vr.right <= clipLeft || vr.left >= clipRight || vr.top >= clipBottom ) return false; if (vr.top < clipTop) { tr.top += tr.height () * (clipTop - vr.top) / vr.height (); vr.top = clipTop; } if (vr.left < clipLeft) { tr.left += tr.width () * (clipLeft - vr.left) / vr.width (); vr.left = clipLeft; } if (vr.right > clipRight) { tr.right -= tr.width () * (vr.right - clipRight) / vr.width (); vr.right = clipRight; } if (vr.bottom > clipBottom) { tr.bottom -= tr.height () * (vr.bottom - clipBottom) / vr.height (); vr.bottom = clipBottom; } return true; } MyGUI::FloatPoint operator () (MyGUI::FloatPoint pt) { pt.left = absoluteLeft - leftOffset + pt.left; pt.top = absoluteTop - topOffset + pt.top; pt.left = +(((pixScaleX * pt.left + hOffset) * 2.0f) - 1.0f); pt.top = -(((pixScaleY * pt.top + vOffset) * 2.0f) - 1.0f); return pt; } }; struct GlyphStream { float mZ; uint32_t mC; MyGUI::IFont* mFont; MyGUI::FloatPoint mOrigin; MyGUI::FloatPoint mCursor; MyGUI::Vertex* mVertices; RenderXform mRenderXform; MyGUI::VertexColourType mVertexColourType; GlyphStream (MyGUI::IFont* font, float left, float top, float Z, MyGUI::Vertex* vertices, RenderXform const & renderXform) : mZ(Z), mC(0), mFont (font), mOrigin (left, top), mVertices (vertices), mRenderXform (renderXform) { assert(font != nullptr); mVertexColourType = MyGUI::RenderManager::getInstance().getVertexFormat(); } ~GlyphStream () { } MyGUI::Vertex* end () const { return mVertices; } void reset (float left, float top, MyGUI::Colour colour) { mC = MyGUI::texture_utility::toColourARGB(colour) | 0xFF000000; MyGUI::texture_utility::convertColour(mC, mVertexColourType); mCursor.left = mOrigin.left + left; mCursor.top = mOrigin.top + top; } void emitGlyph (wchar_t ch) { MWGui::GlyphInfo info = GlyphInfo(mFont, ch); if (!info.charFound) return; MyGUI::FloatRect vr; vr.left = mCursor.left + info.bearingX; vr.top = mCursor.top + info.bearingY; vr.right = vr.left + info.width; vr.bottom = vr.top + info.height; MyGUI::FloatRect tr = info.uvRect; if (mRenderXform.clip (vr, tr)) quad (vr, tr); mCursor.left += static_cast(info.bearingX + info.advance); } void emitSpace (wchar_t ch) { MWGui::GlyphInfo info = GlyphInfo(mFont, ch); if (info.charFound) mCursor.left += static_cast(info.bearingX + info.advance); } private: void quad (const MyGUI::FloatRect& vr, const MyGUI::FloatRect& tr) { vertex (vr.left, vr.top, tr.left, tr.top); vertex (vr.right, vr.top, tr.right, tr.top); vertex (vr.left, vr.bottom, tr.left, tr.bottom); vertex (vr.right, vr.top, tr.right, tr.top); vertex (vr.left, vr.bottom, tr.left, tr.bottom); vertex (vr.right, vr.bottom, tr.right, tr.bottom); } void vertex (float x, float y, float u, float v) { MyGUI::FloatPoint pt = mRenderXform (MyGUI::FloatPoint (x, y)); mVertices->x = pt.left; mVertices->y = pt.top ; mVertices->z = mZ; mVertices->u = u; mVertices->v = v; mVertices->colour = mC; ++mVertices; } }; } class PageDisplay final : public MyGUI::ISubWidgetText { MYGUI_RTTI_DERIVED(PageDisplay) protected: typedef TypesetBookImpl::Section Section; typedef TypesetBookImpl::Line Line; typedef TypesetBookImpl::Run Run; bool mIsPageReset; size_t mPage; struct TextFormat : ISubWidget { typedef MyGUI::IFont* Id; Id mFont; int mCountVertex; MyGUI::ITexture* mTexture; MyGUI::RenderItem* mRenderItem; PageDisplay * mDisplay; TextFormat (MyGUI::IFont* id, PageDisplay * display) : mFont (id), mCountVertex (0), mTexture (nullptr), mRenderItem (nullptr), mDisplay (display) { } void createDrawItem (MyGUI::ILayerNode* node) { assert (mRenderItem == nullptr); if (mTexture != nullptr) { mRenderItem = node->addToRenderItem(mTexture, false, false); mRenderItem->addDrawItem(this, mCountVertex); } } void destroyDrawItem (MyGUI::ILayerNode* node) { assert (mTexture != nullptr ? mRenderItem != nullptr : mRenderItem == nullptr); if (mTexture != nullptr) { mRenderItem->removeDrawItem (this); mRenderItem = nullptr; } } void doRender() override { mDisplay->doRender (*this); } // this isn't really a sub-widget, its just a "drawitem" which // should have its own interface void createDrawItem(MyGUI::ITexture* _texture, MyGUI::ILayerNode* _node) override {} void destroyDrawItem() override {} }; void resetPage() { mIsPageReset = true; mPage = 0; } void setPage(size_t page) { mIsPageReset = false; mPage = page; } bool isPageDifferent(size_t page) { return mIsPageReset || (mPage != page); } std::optional getAdjustedPos(int left, int top, bool move = false) { if (!mBook) return {}; if (mPage >= mBook->mPages.size()) 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; return pos; } public: typedef TypesetBookImpl::StyleImpl Style; typedef std::map > ActiveTextFormats; int mViewTop; int mViewBottom; Style* mFocusItem; bool mItemActive; MyGUI::MouseButton mLastDown; std::function mLinkClicked; std::shared_ptr mBook; MyGUI::ILayerNode* mNode; ActiveTextFormats mActiveTextFormats; PageDisplay () { resetPage (); mViewTop = 0; mViewBottom = 0; mFocusItem = nullptr; mItemActive = false; mNode = nullptr; } void dirtyFocusItem () { if (mFocusItem != nullptr) { MyGUI::IFont* Font = mBook->affectedFont (mFocusItem); ActiveTextFormats::iterator i = mActiveTextFormats.find (Font); if (mNode) mNode->outOfDate (i->second->mRenderItem); } } void onMouseLostFocus () { if (!mBook) return; if (mPage >= mBook->mPages.size()) return; dirtyFocusItem (); mFocusItem = nullptr; mItemActive = false; } void onMouseMove (int left, int top) { Style * hit = nullptr; if(auto pos = getAdjustedPos(left, top, true)) if(pos->top <= mViewBottom) hit = mBook->hitTestWithMargin (pos->left, pos->top); if (mLastDown == MyGUI::MouseButton::None) { if (hit != mFocusItem) { dirtyFocusItem (); mFocusItem = hit; mItemActive = false; dirtyFocusItem (); } } else if (mFocusItem != nullptr) { bool newItemActive = hit == mFocusItem; if (newItemActive != mItemActive) { mItemActive = newItemActive; dirtyFocusItem (); } } } void onMouseButtonPressed (int left, int top, MyGUI::MouseButton id) { auto pos = getAdjustedPos(left, top); if (pos && mLastDown == MyGUI::MouseButton::None) { mFocusItem = pos->top <= mViewBottom ? mBook->hitTestWithMargin (pos->left, pos->top) : nullptr; mItemActive = true; dirtyFocusItem (); mLastDown = id; } } void onMouseButtonReleased(int left, int top, MyGUI::MouseButton id) { auto pos = getAdjustedPos(left, top); if (pos && mLastDown == id) { Style * item = pos->top <= mViewBottom ? mBook->hitTestWithMargin (pos->left, pos->top) : nullptr; bool clicked = mFocusItem == item; mItemActive = false; dirtyFocusItem (); mLastDown = MyGUI::MouseButton::None; if (clicked && mLinkClicked && item && item->mInteractiveId != 0) mLinkClicked (item->mInteractiveId); } } void showPage (TypesetBook::Ptr book, size_t newPage) { std::shared_ptr newBook = std::dynamic_pointer_cast (book); if (mBook != newBook) { mFocusItem = nullptr; mItemActive = 0; for (ActiveTextFormats::iterator i = mActiveTextFormats.begin (); i != mActiveTextFormats.end (); ++i) { if (mNode != nullptr) i->second->destroyDrawItem (mNode); i->second.reset(); } mActiveTextFormats.clear (); if (newBook != nullptr) { createActiveFormats (newBook); mBook = newBook; setPage (newPage); if (newPage < mBook->mPages.size ()) { mViewTop = mBook->mPages [newPage].first; mViewBottom = mBook->mPages [newPage].second; } else { mViewTop = 0; mViewBottom = 0; } } else { mBook.reset (); resetPage (); mViewTop = 0; mViewBottom = 0; } } else if (mBook && isPageDifferent (newPage)) { if (mNode != nullptr) for (ActiveTextFormats::iterator i = mActiveTextFormats.begin (); i != mActiveTextFormats.end (); ++i) mNode->outOfDate(i->second->mRenderItem); setPage (newPage); if (newPage < mBook->mPages.size ()) { mViewTop = mBook->mPages [newPage].first; mViewBottom = mBook->mPages [newPage].second; } else { mViewTop = 0; mViewBottom = 0; } } } struct CreateActiveFormat { PageDisplay * this_; CreateActiveFormat (PageDisplay * this_) : this_ (this_) {} void operator () (Section const & section, Line const & line, Run const & run) const { MyGUI::IFont* Font = run.mStyle->mFont; ActiveTextFormats::iterator j = this_->mActiveTextFormats.find (Font); if (j == this_->mActiveTextFormats.end ()) { std::unique_ptr textFormat(new TextFormat (Font, this_)); textFormat->mTexture = Font->getTextureFont (); j = this_->mActiveTextFormats.insert (std::make_pair (Font, std::move(textFormat))).first; } j->second->mCountVertex += run.mPrintableChars * 6; } }; void createActiveFormats (std::shared_ptr newBook) { newBook->visitRuns (0, 0x7FFFFFFF, CreateActiveFormat (this)); if (mNode != nullptr) for (ActiveTextFormats::iterator i = mActiveTextFormats.begin (); i != mActiveTextFormats.end (); ++i) i->second->createDrawItem (mNode); } void setVisible (bool newVisible) override { if (mVisible == newVisible) return; mVisible = newVisible; if (mVisible) { // reset input state mLastDown = MyGUI::MouseButton::None; mFocusItem = nullptr; mItemActive = 0; } if (nullptr != mNode) { for (ActiveTextFormats::iterator i = mActiveTextFormats.begin (); i != mActiveTextFormats.end (); ++i) mNode->outOfDate(i->second->mRenderItem); } } void createDrawItem(MyGUI::ITexture* texture, MyGUI::ILayerNode* node) override { mNode = node; for (ActiveTextFormats::iterator i = mActiveTextFormats.begin (); i != mActiveTextFormats.end (); ++i) i->second->createDrawItem (node); } struct RenderRun { PageDisplay * this_; GlyphStream &glyphStream; RenderRun (PageDisplay * this_, GlyphStream &glyphStream) : this_(this_), glyphStream (glyphStream) { } void operator () (Section const & section, Line const & line, Run const & run) const { bool isActive = run.mStyle->mInteractiveId && (run.mStyle == this_->mFocusItem); MyGUI::Colour colour = isActive ? (this_->mItemActive ? run.mStyle->mActiveColour: run.mStyle->mHotColour) : run.mStyle->mNormalColour; glyphStream.reset(static_cast(section.mRect.left + line.mRect.left + run.mLeft), static_cast(line.mRect.top), colour); Utf8Stream stream (run.mRange); while (!stream.eof ()) { Utf8Stream::UnicodeChar code_point = stream.consume (); if (ucsCarriageReturn (code_point)) continue; if (!ucsSpace (code_point)) glyphStream.emitGlyph (code_point); else glyphStream.emitSpace (code_point); } } }; /* queue up rendering operations for this text format */ void doRender(TextFormat & textFormat) { if (!mVisible) return; MyGUI::Vertex* vertices = textFormat.mRenderItem->getCurrentVertexBuffer(); RenderXform renderXform (mCroppedParent, textFormat.mRenderItem->getRenderTarget()->getInfo()); GlyphStream glyphStream(textFormat.mFont, static_cast(mCoord.left), static_cast(mCoord.top - mViewTop), -1 /*mNode->getNodeDepth()*/, vertices, renderXform); int visit_top = (std::max) (mViewTop, mViewTop + int (renderXform.clipTop )); int visit_bottom = (std::min) (mViewBottom, mViewTop + int (renderXform.clipBottom)); mBook->visitRuns (visit_top, visit_bottom, textFormat.mFont, RenderRun (this, glyphStream)); textFormat.mRenderItem->setLastVertexCount(glyphStream.end () - vertices); } // ISubWidget should not necessarily be a drawitem // in this case, it is not... void doRender() override { } void _updateView () override { _checkMargin(); if (mNode != nullptr) for (ActiveTextFormats::iterator i = mActiveTextFormats.begin (); i != mActiveTextFormats.end (); ++i) mNode->outOfDate (i->second->mRenderItem); } void _correctView() override { _checkMargin (); if (mNode != nullptr) for (ActiveTextFormats::iterator i = mActiveTextFormats.begin (); i != mActiveTextFormats.end (); ++i) mNode->outOfDate (i->second->mRenderItem); } void destroyDrawItem() override { for (ActiveTextFormats::iterator i = mActiveTextFormats.begin (); i != mActiveTextFormats.end (); ++i) i->second->destroyDrawItem (mNode); mNode = nullptr; } }; class BookPageImpl final : public BookPage { MYGUI_RTTI_DERIVED(BookPage) public: BookPageImpl() : mPageDisplay(nullptr) { } void showPage (TypesetBook::Ptr book, size_t page) override { mPageDisplay->showPage (book, page); } void adviseLinkClicked (std::function linkClicked) override { mPageDisplay->mLinkClicked = linkClicked; } void unadviseLinkClicked () override { mPageDisplay->mLinkClicked = std::function (); } protected: void initialiseOverride() override { Base::initialiseOverride(); if (getSubWidgetText()) { mPageDisplay = getSubWidgetText()->castType(); } else { throw std::runtime_error("BookPage unable to find page display sub widget"); } } void onMouseLostFocus(Widget* _new) override { // NOTE: MyGUI also fires eventMouseLostFocus for widgets that are about to be destroyed (if they had focus). // Child widgets may already be destroyed! So be careful. mPageDisplay->onMouseLostFocus (); } void onMouseMove(int left, int top) override { mPageDisplay->onMouseMove (left, top); } void onMouseButtonPressed (int left, int top, MyGUI::MouseButton id) override { mPageDisplay->onMouseButtonPressed (left, top, id); } void onMouseButtonReleased(int left, int top, MyGUI::MouseButton id) override { mPageDisplay->onMouseButtonReleased (left, top, id); } PageDisplay* mPageDisplay; }; void BookPage::registerMyGUIComponents () { MyGUI::FactoryManager & factory = MyGUI::FactoryManager::getInstance(); factory.registerFactory("Widget"); factory.registerFactory("BasisSkin"); } static bool ucsLineBreak (int codePoint) { return codePoint == '\n'; } static bool ucsCarriageReturn (int codePoint) { return codePoint == '\r'; } static bool ucsSpace (int codePoint) { switch (codePoint) { case 0x0020: // SPACE case 0x00A0: // NO-BREAK SPACE case 0x1680: // OGHAM SPACE MARK case 0x180E: // MONGOLIAN VOWEL SEPARATOR case 0x2000: // EN QUAD case 0x2001: // EM QUAD case 0x2002: // EN SPACE case 0x2003: // EM SPACE case 0x2004: // THREE-PER-EM SPACE case 0x2005: // FOUR-PER-EM SPACE case 0x2006: // SIX-PER-EM SPACE case 0x2007: // FIGURE SPACE case 0x2008: // PUNCTUATION SPACE case 0x2009: // THIN SPACE case 0x200A: // HAIR SPACE case 0x200B: // ZERO WIDTH SPACE case 0x202F: // NARROW NO-BREAK SPACE case 0x205F: // MEDIUM MATHEMATICAL SPACE case 0x3000: // IDEOGRAPHIC SPACE case 0xFEFF: // ZERO WIDTH NO-BREAK SPACE return true; default: return false; } } static bool ucsBreakingSpace (int codePoint) { switch (codePoint) { case 0x0020: // SPACE //case 0x00A0: // NO-BREAK SPACE case 0x1680: // OGHAM SPACE MARK case 0x180E: // MONGOLIAN VOWEL SEPARATOR case 0x2000: // EN QUAD case 0x2001: // EM QUAD case 0x2002: // EN SPACE case 0x2003: // EM SPACE case 0x2004: // THREE-PER-EM SPACE case 0x2005: // FOUR-PER-EM SPACE case 0x2006: // SIX-PER-EM SPACE case 0x2007: // FIGURE SPACE case 0x2008: // PUNCTUATION SPACE case 0x2009: // THIN SPACE case 0x200A: // HAIR SPACE case 0x200B: // ZERO WIDTH SPACE case 0x202F: // NARROW NO-BREAK SPACE case 0x205F: // MEDIUM MATHEMATICAL SPACE case 0x3000: // IDEOGRAPHIC SPACE //case 0xFEFF: // ZERO WIDTH NO-BREAK SPACE return true; default: return false; } } } ================================================ FILE: apps/openmw/mwgui/bookpage.hpp ================================================ #ifndef MWGUI_BOOKPAGE_HPP #define MWGUI_BOOKPAGE_HPP #include "MyGUI_Colour.h" #include "MyGUI_Widget.h" #include "MyGUI_FontManager.h" #include #include #include #include #include "../mwbase/environment.hpp" #include "../mwbase/windowmanager.hpp" namespace MWGui { /// A formatted and paginated document to be used with /// the book page widget. struct TypesetBook { typedef std::shared_ptr Ptr; typedef intptr_t InteractiveId; /// Returns the number of pages in the document. virtual size_t pageCount () const = 0; /// Return the area covered by the document. The first /// integer is the maximum with of any line. This is not /// the largest coordinate of the right edge of any line, /// it is the largest distance from the left edge to the /// right edge. The second integer is the height of all /// text combined prior to pagination. virtual std::pair getSize () const = 0; virtual ~TypesetBook() = default; }; struct GlyphInfo { char codePoint; float width; float height; float advance; float bearingX; float bearingY; bool charFound; MyGUI::FloatRect uvRect; GlyphInfo(MyGUI::IFont* font, MyGUI::Char ch) { static const int fontHeight = MWBase::Environment::get().getWindowManager()->getFontHeight(); const MyGUI::GlyphInfo* gi = font->getGlyphInfo(ch); if (gi) { const float scale = font->getDefaultHeight() / (float) fontHeight; codePoint = gi->codePoint; bearingX = (int) gi->bearingX / scale; bearingY = (int) gi->bearingY / scale; width = (int) gi->width / scale; height = (int) gi->height / scale; advance = (int) gi->advance / scale; uvRect = gi->uvRect; charFound = true; } else { codePoint = 0; bearingX = 0; bearingY = 0; width = 0; height = 0; advance = 0; charFound = false; } } }; /// A factory class for creating a typeset book instance. struct BookTypesetter { typedef std::shared_ptr Ptr; typedef TypesetBook::InteractiveId InteractiveId; typedef MyGUI::Colour Colour; typedef uint8_t const * Utf8Point; typedef std::pair Utf8Span; virtual ~BookTypesetter() = default; enum Alignment { AlignLeft = -1, AlignCenter = 0, AlignRight = +1 }; /// Styles are used to control the character level formatting /// of text added to a typeset book. Their lifetime is equal /// to the lifetime of the book-typesetter instance that created /// them. struct Style; /// A factory function for creating the default implementation of a book typesetter static Ptr create (int pageWidth, int pageHeight); /// Create a simple text style consisting of a font and a text color. virtual Style* createStyle (const std::string& fontName, const Colour& colour, bool useBookFont=true) = 0; /// Create a hyper-link style with a user-defined identifier based on an /// existing style. The unique flag forces a new instance of this style /// to be created even if an existing instance is present. virtual Style* createHotStyle (Style * BaseStyle, const Colour& NormalColour, const Colour& HoverColour, const Colour& ActiveColour, InteractiveId Id, bool Unique = true) = 0; /// Insert a line break into the document. Newline characters in the input /// text have the same affect. The margin parameter adds additional space /// before the next line of text. virtual void lineBreak (float margin = 0) = 0; /// Insert a section break into the document. This causes a new section /// to begin when additional text is inserted. Pagination attempts to keep /// sections together on a single page. The margin parameter adds additional space /// before the next line of text. virtual void sectionBreak (int margin = 0) = 0; /// Changes the alignment for the current section of text. virtual void setSectionAlignment (Alignment sectionAlignment) = 0; // Layout a block of text with the specified style into the document. virtual void write (Style * Style, Utf8Span Text) = 0; /// Adds a content block to the document without laying it out. An /// identifier is returned that can be used to refer to it. If select /// is true, the block is activated to be references by future writes. virtual intptr_t addContent (Utf8Span Text, bool Select = true) = 0; /// Select a previously created content block for future writes. virtual void selectContent (intptr_t contentHandle) = 0; /// Layout a span of the selected content block into the document /// using the specified style. virtual void write (Style * Style, size_t Begin, size_t End) = 0; /// Finalize the document layout, and return a pointer to it. virtual TypesetBook::Ptr complete () = 0; }; /// An interface to the BookPage widget. class BookPage : public MyGUI::Widget { MYGUI_RTTI_DERIVED(BookPage) public: typedef TypesetBook::InteractiveId InteractiveId; typedef std::function ClickCallback; /// Make the widget display the specified page from the specified book. virtual void showPage (TypesetBook::Ptr Book, size_t Page) = 0; /// Set the callback for a clicking a hyper-link in the document. virtual void adviseLinkClicked (ClickCallback callback) = 0; /// Clear the hyper-link click callback. virtual void unadviseLinkClicked () = 0; /// Register the widget and associated sub-widget with MyGUI. Should be /// called once near the beginning of the program. static void registerMyGUIComponents (); }; } #endif // MWGUI_BOOKPAGE_HPP ================================================ FILE: apps/openmw/mwgui/bookwindow.cpp ================================================ #include "bookwindow.hpp" #include #include #include #include "../mwbase/environment.hpp" #include "../mwbase/world.hpp" #include "../mwbase/windowmanager.hpp" #include "../mwmechanics/actorutil.hpp" #include "../mwworld/actiontake.hpp" #include "../mwworld/class.hpp" #include "formatting.hpp" namespace MWGui { BookWindow::BookWindow () : BookWindowBase("openmw_book.layout") , mCurrentPage(0) , mTakeButtonShow(true) , mTakeButtonAllowed(true) { getWidget(mCloseButton, "CloseButton"); mCloseButton->eventMouseButtonClick += MyGUI::newDelegate(this, &BookWindow::onCloseButtonClicked); getWidget(mTakeButton, "TakeButton"); mTakeButton->eventMouseButtonClick += MyGUI::newDelegate(this, &BookWindow::onTakeButtonClicked); getWidget(mNextPageButton, "NextPageBTN"); mNextPageButton->eventMouseButtonClick += MyGUI::newDelegate(this, &BookWindow::onNextPageButtonClicked); getWidget(mPrevPageButton, "PrevPageBTN"); mPrevPageButton->eventMouseButtonClick += MyGUI::newDelegate(this, &BookWindow::onPrevPageButtonClicked); getWidget(mLeftPageNumber, "LeftPageNumber"); getWidget(mRightPageNumber, "RightPageNumber"); getWidget(mLeftPage, "LeftPage"); getWidget(mRightPage, "RightPage"); adjustButton("CloseButton"); adjustButton("TakeButton"); adjustButton("PrevPageBTN"); float scale = adjustButton("NextPageBTN"); mLeftPage->setNeedMouseFocus(true); mLeftPage->eventMouseWheel += MyGUI::newDelegate(this, &BookWindow::onMouseWheel); mRightPage->setNeedMouseFocus(true); mRightPage->eventMouseWheel += MyGUI::newDelegate(this, &BookWindow::onMouseWheel); mNextPageButton->eventKeyButtonPressed += MyGUI::newDelegate(this, &BookWindow::onKeyButtonPressed); mPrevPageButton->eventKeyButtonPressed += MyGUI::newDelegate(this, &BookWindow::onKeyButtonPressed); mTakeButton->eventKeyButtonPressed += MyGUI::newDelegate(this, &BookWindow::onKeyButtonPressed); mCloseButton->eventKeyButtonPressed += MyGUI::newDelegate(this, &BookWindow::onKeyButtonPressed); if (mNextPageButton->getSize().width == 64) { // english button has a 7 pixel wide strip of garbage on its right edge mNextPageButton->setSize(64-7, mNextPageButton->getSize().height); mNextPageButton->setImageCoord(MyGUI::IntCoord(0,0,(64-7)*scale,mNextPageButton->getSize().height*scale)); } center(); } void BookWindow::onMouseWheel(MyGUI::Widget *_sender, int _rel) { if (_rel < 0) nextPage(); else prevPage(); } void BookWindow::clearPages() { mPages.clear(); } void BookWindow::setPtr (const MWWorld::Ptr& book) { mBook = book; MWWorld::Ptr player = MWMechanics::getPlayer(); bool showTakeButton = book.getContainerStore() != &player.getClass().getContainerStore(player); clearPages(); mCurrentPage = 0; MWWorld::LiveCellRef *ref = mBook.get(); Formatting::BookFormatter formatter; mPages = formatter.markupToWidget(mLeftPage, ref->mBase->mText); formatter.markupToWidget(mRightPage, ref->mBase->mText); updatePages(); setTakeButtonShow(showTakeButton); MWBase::Environment::get().getWindowManager()->setKeyFocusWidget(mCloseButton); } void BookWindow::setTakeButtonShow(bool show) { mTakeButtonShow = show; mTakeButton->setVisible(mTakeButtonShow && mTakeButtonAllowed); } void BookWindow::onKeyButtonPressed(MyGUI::Widget *sender, MyGUI::KeyCode key, MyGUI::Char character) { if (key == MyGUI::KeyCode::ArrowUp) prevPage(); else if (key == MyGUI::KeyCode::ArrowDown) nextPage(); } void BookWindow::setInventoryAllowed(bool allowed) { mTakeButtonAllowed = allowed; mTakeButton->setVisible(mTakeButtonShow && mTakeButtonAllowed); } void BookWindow::onCloseButtonClicked (MyGUI::Widget* sender) { MWBase::Environment::get().getWindowManager()->removeGuiMode(GM_Book); } void BookWindow::onTakeButtonClicked (MyGUI::Widget* sender) { MWBase::Environment::get().getWindowManager()->playSound("Item Book Up"); MWWorld::ActionTake take(mBook); take.execute (MWMechanics::getPlayer()); MWBase::Environment::get().getWindowManager()->removeGuiMode(GM_Book); } void BookWindow::onNextPageButtonClicked (MyGUI::Widget* sender) { nextPage(); } void BookWindow::onPrevPageButtonClicked (MyGUI::Widget* sender) { prevPage(); } void BookWindow::updatePages() { mLeftPageNumber->setCaption( MyGUI::utility::toString(mCurrentPage*2 + 1) ); mRightPageNumber->setCaption( MyGUI::utility::toString(mCurrentPage*2 + 2) ); MyGUI::Widget* focus = MyGUI::InputManager::getInstance().getKeyFocusWidget(); bool nextPageVisible = (mCurrentPage+1)*2 < mPages.size(); mNextPageButton->setVisible(nextPageVisible); bool prevPageVisible = mCurrentPage != 0; mPrevPageButton->setVisible(prevPageVisible); if (focus == mNextPageButton && !nextPageVisible && prevPageVisible) MWBase::Environment::get().getWindowManager()->setKeyFocusWidget(mPrevPageButton); else if (focus == mPrevPageButton && !prevPageVisible && nextPageVisible) MWBase::Environment::get().getWindowManager()->setKeyFocusWidget(mNextPageButton); if (mPages.empty()) return; MyGUI::Widget * paper; paper = mLeftPage->getChildAt(0); paper->setCoord(paper->getPosition().left, -mPages[mCurrentPage*2].first, paper->getWidth(), mPages[mCurrentPage*2].second); paper = mRightPage->getChildAt(0); if ((mCurrentPage+1)*2 <= mPages.size()) { paper->setCoord(paper->getPosition().left, -mPages[mCurrentPage*2+1].first, paper->getWidth(), mPages[mCurrentPage*2+1].second); paper->setVisible(true); } else { paper->setVisible(false); } } void BookWindow::nextPage() { if ((mCurrentPage+1)*2 < mPages.size()) { MWBase::Environment::get().getWindowManager()->playSound("book page2"); ++mCurrentPage; updatePages(); } } void BookWindow::prevPage() { if (mCurrentPage > 0) { MWBase::Environment::get().getWindowManager()->playSound("book page"); --mCurrentPage; updatePages(); } } } ================================================ FILE: apps/openmw/mwgui/bookwindow.hpp ================================================ #ifndef MWGUI_BOOKWINDOW_H #define MWGUI_BOOKWINDOW_H #include "windowbase.hpp" #include "../mwworld/ptr.hpp" #include namespace MWGui { class BookWindow : public BookWindowBase { public: BookWindow(); void setPtr(const MWWorld::Ptr& book) override; void setInventoryAllowed(bool allowed); void onResChange(int, int) override { center(); } protected: void onNextPageButtonClicked (MyGUI::Widget* sender); void onPrevPageButtonClicked (MyGUI::Widget* sender); void onCloseButtonClicked (MyGUI::Widget* sender); void onTakeButtonClicked (MyGUI::Widget* sender); void onMouseWheel(MyGUI::Widget* _sender, int _rel); void setTakeButtonShow(bool show); void onKeyButtonPressed(MyGUI::Widget* sender, MyGUI::KeyCode key, MyGUI::Char character); void nextPage(); void prevPage(); void updatePages(); void clearPages(); private: typedef std::pair Page; typedef std::vector Pages; Gui::ImageButton* mCloseButton; Gui::ImageButton* mTakeButton; Gui::ImageButton* mNextPageButton; Gui::ImageButton* mPrevPageButton; MyGUI::TextBox* mLeftPageNumber; MyGUI::TextBox* mRightPageNumber; MyGUI::Widget* mLeftPage; MyGUI::Widget* mRightPage; unsigned int mCurrentPage; // 0 is first page Pages mPages; MWWorld::Ptr mBook; bool mTakeButtonShow; bool mTakeButtonAllowed; }; } #endif ================================================ FILE: apps/openmw/mwgui/charactercreation.cpp ================================================ #include "charactercreation.hpp" #include #include #include #include "../mwbase/environment.hpp" #include "../mwbase/soundmanager.hpp" #include "../mwbase/mechanicsmanager.hpp" #include "../mwbase/world.hpp" #include "../mwbase/windowmanager.hpp" #include "../mwmechanics/npcstats.hpp" #include "../mwmechanics/actorutil.hpp" #include "../mwworld/class.hpp" #include "../mwworld/esmstore.hpp" #include "../mwworld/player.hpp" /* Start of tes3mp addition Include additional headers for multiplayer purposes */ #include "../mwmp/Main.hpp" #include "../mwmp/LocalPlayer.hpp" /* End of tes3mp addition */ #include "textinput.hpp" #include "race.hpp" #include "class.hpp" #include "birth.hpp" #include "review.hpp" #include "inventorywindow.hpp" namespace { struct Response { const std::string mText; const ESM::Class::Specialization mSpecialization; }; struct Step { const std::string mText; const Response mResponses[3]; const std::string mSound; }; Step sGenerateClassSteps(int number) { number++; std::string question = Fallback::Map::getString("Question_" + MyGUI::utility::toString(number) + "_Question"); std::string answer0 = Fallback::Map::getString("Question_" + MyGUI::utility::toString(number) + "_AnswerOne"); std::string answer1 = Fallback::Map::getString("Question_" + MyGUI::utility::toString(number) + "_AnswerTwo"); std::string answer2 = Fallback::Map::getString("Question_" + MyGUI::utility::toString(number) + "_AnswerThree"); std::string sound = "vo\\misc\\chargen qa" + MyGUI::utility::toString(number) + ".wav"; Response r0 = {answer0, ESM::Class::Combat}; Response r1 = {answer1, ESM::Class::Magic}; Response r2 = {answer2, ESM::Class::Stealth}; // randomize order in which responses are displayed int order = Misc::Rng::rollDice(6); switch (order) { case 0: return {question, {r0, r1, r2}, sound}; case 1: return {question, {r0, r2, r1}, sound}; case 2: return {question, {r1, r0, r2}, sound}; case 3: return {question, {r1, r2, r0}, sound}; case 4: return {question, {r2, r0, r1}, sound}; default: return {question, {r2, r1, r0}, sound}; } } void updatePlayerHealth() { MWWorld::Ptr player = MWMechanics::getPlayer(); MWMechanics::NpcStats& npcStats = player.getClass().getNpcStats(player); npcStats.updateHealth(); } } namespace MWGui { CharacterCreation::CharacterCreation(osg::Group* parent, Resource::ResourceSystem* resourceSystem) : mParent(parent) , mResourceSystem(resourceSystem) , mNameDialog(nullptr) , mRaceDialog(nullptr) , mClassChoiceDialog(nullptr) , mGenerateClassQuestionDialog(nullptr) , mGenerateClassResultDialog(nullptr) , mPickClassDialog(nullptr) , mCreateClassDialog(nullptr) , mBirthSignDialog(nullptr) , mReviewDialog(nullptr) , mGenerateClassStep(0) { mCreationStage = CSE_NotStarted; mGenerateClassResponses[0] = ESM::Class::Combat; mGenerateClassResponses[1] = ESM::Class::Magic; mGenerateClassResponses[2] = ESM::Class::Stealth; mGenerateClassSpecializations[0] = 0; mGenerateClassSpecializations[1] = 0; mGenerateClassSpecializations[2] = 0; // Setup player stats for (int i = 0; i < ESM::Attribute::Length; ++i) mPlayerAttributes.emplace(ESM::Attribute::sAttributeIds[i], MWMechanics::AttributeValue()); for (int i = 0; i < ESM::Skill::Length; ++i) mPlayerSkillValues.emplace(ESM::Skill::sSkillIds[i], MWMechanics::SkillValue()); } void CharacterCreation::setValue (const std::string& id, const MWMechanics::AttributeValue& value) { static const char *ids[] = { "AttribVal1", "AttribVal2", "AttribVal3", "AttribVal4", "AttribVal5", "AttribVal6", "AttribVal7", "AttribVal8", 0 }; for (int i=0; ids[i]; ++i) { if (ids[i]==id) { mPlayerAttributes[static_cast(i)] = value; if (mReviewDialog) mReviewDialog->setAttribute(static_cast(i), value); break; } } } void CharacterCreation::setValue (const std::string& id, const MWMechanics::DynamicStat& value) { if (mReviewDialog) { if (id == "HBar") { mReviewDialog->setHealth (value); } else if (id == "MBar") { mReviewDialog->setMagicka (value); } else if (id == "FBar") { mReviewDialog->setFatigue (value); } } } void CharacterCreation::setValue(const ESM::Skill::SkillEnum parSkill, const MWMechanics::SkillValue& value) { mPlayerSkillValues[parSkill] = value; if (mReviewDialog) mReviewDialog->setSkillValue(parSkill, value); } void CharacterCreation::configureSkills (const SkillList& major, const SkillList& minor) { if (mReviewDialog) mReviewDialog->configureSkills(major, minor); mPlayerMajorSkills = major; mPlayerMinorSkills = minor; } void CharacterCreation::onFrame(float duration) { if (mReviewDialog) mReviewDialog->onFrame(duration); } void CharacterCreation::spawnDialog(const char id) { try { switch (id) { case GM_Name: MWBase::Environment::get().getWindowManager()->removeDialog(mNameDialog); mNameDialog = nullptr; mNameDialog = new TextInputDialog(); mNameDialog->setTextLabel(MWBase::Environment::get().getWindowManager()->getGameSettingString("sName", "Name")); mNameDialog->setTextInput(mPlayerName); mNameDialog->setNextButtonShow(mCreationStage >= CSE_NameChosen); mNameDialog->eventDone += MyGUI::newDelegate(this, &CharacterCreation::onNameDialogDone); mNameDialog->setVisible(true); break; case GM_Race: MWBase::Environment::get().getWindowManager()->removeDialog(mRaceDialog); mRaceDialog = nullptr; mRaceDialog = new RaceDialog(mParent, mResourceSystem); mRaceDialog->setNextButtonShow(mCreationStage >= CSE_RaceChosen); mRaceDialog->setRaceId(mPlayerRaceId); mRaceDialog->eventDone += MyGUI::newDelegate(this, &CharacterCreation::onRaceDialogDone); mRaceDialog->eventBack += MyGUI::newDelegate(this, &CharacterCreation::onRaceDialogBack); mRaceDialog->setVisible(true); if (mCreationStage < CSE_NameChosen) mCreationStage = CSE_NameChosen; break; case GM_Class: MWBase::Environment::get().getWindowManager()->removeDialog(mClassChoiceDialog); mClassChoiceDialog = nullptr; mClassChoiceDialog = new ClassChoiceDialog(); mClassChoiceDialog->eventButtonSelected += MyGUI::newDelegate(this, &CharacterCreation::onClassChoice); mClassChoiceDialog->setVisible(true); if (mCreationStage < CSE_RaceChosen) mCreationStage = CSE_RaceChosen; break; case GM_ClassPick: MWBase::Environment::get().getWindowManager()->removeDialog(mPickClassDialog); mPickClassDialog = nullptr; mPickClassDialog = new PickClassDialog(); mPickClassDialog->setNextButtonShow(mCreationStage >= CSE_ClassChosen); mPickClassDialog->setClassId(mPlayerClass.mId); mPickClassDialog->eventDone += MyGUI::newDelegate(this, &CharacterCreation::onPickClassDialogDone); mPickClassDialog->eventBack += MyGUI::newDelegate(this, &CharacterCreation::onPickClassDialogBack); mPickClassDialog->setVisible(true); if (mCreationStage < CSE_RaceChosen) mCreationStage = CSE_RaceChosen; break; case GM_Birth: MWBase::Environment::get().getWindowManager()->removeDialog(mBirthSignDialog); mBirthSignDialog = nullptr; mBirthSignDialog = new BirthDialog(); mBirthSignDialog->setNextButtonShow(mCreationStage >= CSE_BirthSignChosen); mBirthSignDialog->setBirthId(mPlayerBirthSignId); mBirthSignDialog->eventDone += MyGUI::newDelegate(this, &CharacterCreation::onBirthSignDialogDone); mBirthSignDialog->eventBack += MyGUI::newDelegate(this, &CharacterCreation::onBirthSignDialogBack); mBirthSignDialog->setVisible(true); if (mCreationStage < CSE_ClassChosen) mCreationStage = CSE_ClassChosen; break; case GM_ClassCreate: if (!mCreateClassDialog) { mCreateClassDialog = new CreateClassDialog(); mCreateClassDialog->eventDone += MyGUI::newDelegate(this, &CharacterCreation::onCreateClassDialogDone); mCreateClassDialog->eventBack += MyGUI::newDelegate(this, &CharacterCreation::onCreateClassDialogBack); } mCreateClassDialog->setNextButtonShow(mCreationStage >= CSE_ClassChosen); mCreateClassDialog->setVisible(true); if (mCreationStage < CSE_RaceChosen) mCreationStage = CSE_RaceChosen; break; case GM_ClassGenerate: mGenerateClassStep = 0; mGenerateClass = ""; mGenerateClassSpecializations[0] = 0; mGenerateClassSpecializations[1] = 0; mGenerateClassSpecializations[2] = 0; showClassQuestionDialog(); if (mCreationStage < CSE_RaceChosen) mCreationStage = CSE_RaceChosen; break; case GM_Review: MWBase::Environment::get().getWindowManager()->removeDialog(mReviewDialog); mReviewDialog = nullptr; mReviewDialog = new ReviewDialog(); MWBase::World *world = MWBase::Environment::get().getWorld(); const ESM::NPC *playerNpc = world->getPlayerPtr().get()->mBase; const MWWorld::Player player = world->getPlayer(); const ESM::Class *playerClass = world->getStore().get().find(playerNpc->mClass); mReviewDialog->setPlayerName(playerNpc->mName); mReviewDialog->setRace(playerNpc->mRace); mReviewDialog->setClass(*playerClass); mReviewDialog->setBirthSign(player.getBirthSign()); MWWorld::Ptr playerPtr = MWMechanics::getPlayer(); const MWMechanics::CreatureStats& stats = playerPtr.getClass().getCreatureStats(playerPtr); mReviewDialog->setHealth(stats.getHealth()); mReviewDialog->setMagicka(stats.getMagicka()); mReviewDialog->setFatigue(stats.getFatigue()); for (auto& attributePair : mPlayerAttributes) { mReviewDialog->setAttribute(static_cast (attributePair.first), attributePair.second); } for (auto& skillPair : mPlayerSkillValues) { mReviewDialog->setSkillValue(static_cast (skillPair.first), skillPair.second); } mReviewDialog->configureSkills(mPlayerMajorSkills, mPlayerMinorSkills); mReviewDialog->eventDone += MyGUI::newDelegate(this, &CharacterCreation::onReviewDialogDone); mReviewDialog->eventBack += MyGUI::newDelegate(this, &CharacterCreation::onReviewDialogBack); mReviewDialog->eventActivateDialog += MyGUI::newDelegate(this, &CharacterCreation::onReviewActivateDialog); mReviewDialog->setVisible(true); if (mCreationStage < CSE_BirthSignChosen) mCreationStage = CSE_BirthSignChosen; break; } } catch (std::exception& e) { Log(Debug::Error) << "Error: Failed to create chargen window: " << e.what(); } } void CharacterCreation::onReviewDialogDone(WindowBase* parWindow) { MWBase::Environment::get().getWindowManager()->removeDialog(mReviewDialog); mReviewDialog = nullptr; MWBase::Environment::get().getWindowManager()->popGuiMode(); } void CharacterCreation::onReviewDialogBack() { MWBase::Environment::get().getWindowManager()->removeDialog(mReviewDialog); mReviewDialog = nullptr; mCreationStage = CSE_ReviewBack; MWBase::Environment::get().getWindowManager()->popGuiMode(); MWBase::Environment::get().getWindowManager()->pushGuiMode(GM_Birth); /* Start of tes3mp addition Decrease the character generation stage tracked for the LocalPlayer */ mwmp::Main::get().getLocalPlayer()->charGenState.currentStage--; /* End of tes3mp addition */ } void CharacterCreation::onReviewActivateDialog(int parDialog) { MWBase::Environment::get().getWindowManager()->removeDialog(mReviewDialog); mReviewDialog = nullptr; mCreationStage = CSE_ReviewNext; MWBase::Environment::get().getWindowManager()->popGuiMode(); switch(parDialog) { case ReviewDialog::NAME_DIALOG: MWBase::Environment::get().getWindowManager()->pushGuiMode(GM_Name); break; case ReviewDialog::RACE_DIALOG: MWBase::Environment::get().getWindowManager()->pushGuiMode(GM_Race); break; case ReviewDialog::CLASS_DIALOG: MWBase::Environment::get().getWindowManager()->pushGuiMode(GM_Class); break; case ReviewDialog::BIRTHSIGN_DIALOG: MWBase::Environment::get().getWindowManager()->pushGuiMode(GM_Birth); }; } void CharacterCreation::selectPickedClass() { if (mPickClassDialog) { const std::string &classId = mPickClassDialog->getClassId(); if (!classId.empty()) MWBase::Environment::get().getMechanicsManager()->setPlayerClass(classId); const ESM::Class *klass = MWBase::Environment::get().getWorld()->getStore().get().find(classId); if (klass) { mPlayerClass = *klass; } MWBase::Environment::get().getWindowManager()->removeDialog(mPickClassDialog); mPickClassDialog = nullptr; } updatePlayerHealth(); } void CharacterCreation::onPickClassDialogDone(WindowBase* parWindow) { selectPickedClass(); handleDialogDone(CSE_ClassChosen, GM_Birth); /* Start of tes3mp addition Increase the character generation stage tracked for the LocalPlayer */ mwmp::Main::get().getLocalPlayer()->charGenState.currentStage++; /* End of tes3mp addition */ } void CharacterCreation::onPickClassDialogBack() { selectPickedClass(); MWBase::Environment::get().getWindowManager()->popGuiMode(); MWBase::Environment::get().getWindowManager()->pushGuiMode(GM_Class); } void CharacterCreation::onClassChoice(int _index) { MWBase::Environment::get().getWindowManager()->removeDialog(mClassChoiceDialog); mClassChoiceDialog = nullptr; MWBase::Environment::get().getWindowManager()->popGuiMode(); switch(_index) { case ClassChoiceDialog::Class_Generate: MWBase::Environment::get().getWindowManager()->pushGuiMode(GM_ClassGenerate); break; case ClassChoiceDialog::Class_Pick: MWBase::Environment::get().getWindowManager()->pushGuiMode(GM_ClassPick); break; case ClassChoiceDialog::Class_Create: MWBase::Environment::get().getWindowManager()->pushGuiMode(GM_ClassCreate); break; case ClassChoiceDialog::Class_Back: MWBase::Environment::get().getWindowManager()->pushGuiMode(GM_Race); /* Start of tes3mp addition Decrease the character generation stage tracked for the LocalPlayer */ mwmp::Main::get().getLocalPlayer()->charGenState.currentStage--; /* End of tes3mp addition */ break; }; } void CharacterCreation::onNameDialogDone(WindowBase* parWindow) { if (mNameDialog) { mPlayerName = mNameDialog->getTextInput(); /* Start of tes3mp change (major) Ensure names are not longer than the original game's 31 character maximum */ if (mPlayerName.length() > 31) mPlayerName = mPlayerName.substr(0, 31); /* End of tes3mp change (major) */ MWBase::Environment::get().getMechanicsManager()->setPlayerName(mPlayerName); MWBase::Environment::get().getWindowManager()->removeDialog(mNameDialog); mNameDialog = nullptr; } handleDialogDone(CSE_NameChosen, GM_Race); /* Start of tes3mp addition Increase the character generation stage tracked for the LocalPlayer */ mwmp::Main::get().getLocalPlayer()->charGenState.currentStage++; /* End of tes3mp addition */ } void CharacterCreation::selectRace() { if (mRaceDialog) { const ESM::NPC &data = mRaceDialog->getResult(); mPlayerRaceId = data.mRace; if (!mPlayerRaceId.empty()) { MWBase::Environment::get().getMechanicsManager()->setPlayerRace( data.mRace, data.isMale(), data.mHead, data.mHair ); } MWBase::Environment::get().getWindowManager()->getInventoryWindow()->rebuildAvatar(); MWBase::Environment::get().getWindowManager()->removeDialog(mRaceDialog); mRaceDialog = nullptr; } updatePlayerHealth(); } void CharacterCreation::onRaceDialogBack() { selectRace(); MWBase::Environment::get().getWindowManager()->popGuiMode(); MWBase::Environment::get().getWindowManager()->pushGuiMode(GM_Name); } void CharacterCreation::onRaceDialogDone(WindowBase* parWindow) { selectRace(); handleDialogDone(CSE_RaceChosen, GM_Class); /* Start of tes3mp addition Increase the character generation stage tracked for the LocalPlayer */ mwmp::Main::get().getLocalPlayer()->charGenState.currentStage++; /* End of tes3mp addition */ } void CharacterCreation::selectBirthSign() { if (mBirthSignDialog) { mPlayerBirthSignId = mBirthSignDialog->getBirthId(); if (!mPlayerBirthSignId.empty()) MWBase::Environment::get().getMechanicsManager()->setPlayerBirthsign(mPlayerBirthSignId); MWBase::Environment::get().getWindowManager()->removeDialog(mBirthSignDialog); mBirthSignDialog = nullptr; } updatePlayerHealth(); } void CharacterCreation::onBirthSignDialogDone(WindowBase* parWindow) { selectBirthSign(); handleDialogDone(CSE_BirthSignChosen, GM_Review); /* Start of tes3mp addition Increase the character generation stage tracked for the LocalPlayer */ mwmp::Main::get().getLocalPlayer()->charGenState.currentStage++; /* End of tes3mp addition */ } void CharacterCreation::onBirthSignDialogBack() { selectBirthSign(); MWBase::Environment::get().getWindowManager()->popGuiMode(); MWBase::Environment::get().getWindowManager()->pushGuiMode(GM_Class); /* Start of tes3mp addition Decrease the character generation stage tracked for the LocalPlayer */ mwmp::Main::get().getLocalPlayer()->charGenState.currentStage--; /* End of tes3mp addition */ } void CharacterCreation::selectCreatedClass() { if (mCreateClassDialog) { ESM::Class klass; klass.mName = mCreateClassDialog->getName(); klass.mDescription = mCreateClassDialog->getDescription(); klass.mData.mSpecialization = mCreateClassDialog->getSpecializationId(); klass.mData.mIsPlayable = 0x1; std::vector attributes = mCreateClassDialog->getFavoriteAttributes(); assert(attributes.size() == 2); klass.mData.mAttribute[0] = attributes[0]; klass.mData.mAttribute[1] = attributes[1]; std::vector majorSkills = mCreateClassDialog->getMajorSkills(); std::vector minorSkills = mCreateClassDialog->getMinorSkills(); assert(majorSkills.size() >= sizeof(klass.mData.mSkills)/sizeof(klass.mData.mSkills[0])); assert(minorSkills.size() >= sizeof(klass.mData.mSkills)/sizeof(klass.mData.mSkills[0])); for (size_t i = 0; i < sizeof(klass.mData.mSkills)/sizeof(klass.mData.mSkills[0]); ++i) { klass.mData.mSkills[i][1] = majorSkills[i]; klass.mData.mSkills[i][0] = minorSkills[i]; } MWBase::Environment::get().getMechanicsManager()->setPlayerClass(klass); mPlayerClass = klass; // Do not delete dialog, so that choices are remembered in case we want to go back and adjust them later mCreateClassDialog->setVisible(false); } updatePlayerHealth(); } void CharacterCreation::onCreateClassDialogDone(WindowBase* parWindow) { selectCreatedClass(); handleDialogDone(CSE_ClassChosen, GM_Birth); /* Start of tes3mp addition Increase the character generation stage tracked for the LocalPlayer */ mwmp::Main::get().getLocalPlayer()->charGenState.currentStage++; /* End of tes3mp addition */ } void CharacterCreation::onCreateClassDialogBack() { // not done in MW, but we do it for consistency with the other dialogs selectCreatedClass(); MWBase::Environment::get().getWindowManager()->popGuiMode(); MWBase::Environment::get().getWindowManager()->pushGuiMode(GM_Class); /* Start of tes3mp addition Decrease the character generation stage tracked for the LocalPlayer */ mwmp::Main::get().getLocalPlayer()->charGenState.currentStage--; /* End of tes3mp addition */ } void CharacterCreation::onClassQuestionChosen(int _index) { MWBase::Environment::get().getSoundManager()->stopSay(); MWBase::Environment::get().getWindowManager()->removeDialog(mGenerateClassQuestionDialog); mGenerateClassQuestionDialog = nullptr; if (_index < 0 || _index >= 3) { MWBase::Environment::get().getWindowManager()->popGuiMode(); MWBase::Environment::get().getWindowManager()->pushGuiMode(GM_Class); return; } ESM::Class::Specialization specialization = mGenerateClassResponses[_index]; if (specialization == ESM::Class::Combat) ++mGenerateClassSpecializations[0]; else if (specialization == ESM::Class::Magic) ++mGenerateClassSpecializations[1]; else if (specialization == ESM::Class::Stealth) ++mGenerateClassSpecializations[2]; ++mGenerateClassStep; showClassQuestionDialog(); } void CharacterCreation::showClassQuestionDialog() { if (mGenerateClassStep == 10) { unsigned combat = mGenerateClassSpecializations[0]; unsigned magic = mGenerateClassSpecializations[1]; unsigned stealth = mGenerateClassSpecializations[2]; if (combat > 7) { mGenerateClass = "Warrior"; } else if (magic > 7) { mGenerateClass = "Mage"; } else if (stealth > 7) { mGenerateClass = "Thief"; } else { switch (combat) { case 4: mGenerateClass = "Rogue"; break; case 5: if (stealth == 3) mGenerateClass = "Scout"; else mGenerateClass = "Archer"; break; case 6: if (stealth == 1) mGenerateClass = "Barbarian"; else if (stealth == 3) mGenerateClass = "Crusader"; else mGenerateClass = "Knight"; break; case 7: mGenerateClass = "Warrior"; break; default: switch (magic) { case 4: mGenerateClass = "Spellsword"; break; case 5: mGenerateClass = "Witchhunter"; break; case 6: if (combat == 2) mGenerateClass = "Sorcerer"; else if (combat == 3) mGenerateClass = "Healer"; else mGenerateClass = "Battlemage"; break; case 7: mGenerateClass = "Mage"; break; default: switch (stealth) { case 3: if (magic == 3) mGenerateClass = "Bard"; // unreachable else mGenerateClass = "Warrior"; break; case 5: if (magic == 3) mGenerateClass = "Monk"; else mGenerateClass = "Pilgrim"; break; case 6: if (magic == 1) mGenerateClass = "Agent"; else if (magic == 3) mGenerateClass = "Assassin"; else mGenerateClass = "Acrobat"; break; case 7: mGenerateClass = "Thief"; break; default: mGenerateClass = "Warrior"; } } } } MWBase::Environment::get().getWindowManager()->removeDialog(mGenerateClassResultDialog); mGenerateClassResultDialog = nullptr; mGenerateClassResultDialog = new GenerateClassResultDialog(); mGenerateClassResultDialog->setClassId(mGenerateClass); mGenerateClassResultDialog->eventBack += MyGUI::newDelegate(this, &CharacterCreation::onGenerateClassBack); mGenerateClassResultDialog->eventDone += MyGUI::newDelegate(this, &CharacterCreation::onGenerateClassDone); mGenerateClassResultDialog->setVisible(true); return; } if (mGenerateClassStep > 10) { MWBase::Environment::get().getWindowManager()->popGuiMode(); MWBase::Environment::get().getWindowManager()->pushGuiMode(GM_Class); return; } MWBase::Environment::get().getWindowManager()->removeDialog(mGenerateClassQuestionDialog); mGenerateClassQuestionDialog = nullptr; mGenerateClassQuestionDialog = new InfoBoxDialog(); Step step = sGenerateClassSteps(mGenerateClassStep); mGenerateClassResponses[0] = step.mResponses[0].mSpecialization; mGenerateClassResponses[1] = step.mResponses[1].mSpecialization; mGenerateClassResponses[2] = step.mResponses[2].mSpecialization; InfoBoxDialog::ButtonList buttons; mGenerateClassQuestionDialog->setText(step.mText); buttons.push_back(step.mResponses[0].mText); buttons.push_back(step.mResponses[1].mText); buttons.push_back(step.mResponses[2].mText); mGenerateClassQuestionDialog->setButtons(buttons); mGenerateClassQuestionDialog->eventButtonSelected += MyGUI::newDelegate(this, &CharacterCreation::onClassQuestionChosen); mGenerateClassQuestionDialog->setVisible(true); MWBase::Environment::get().getSoundManager()->say(step.mSound); } void CharacterCreation::selectGeneratedClass() { MWBase::Environment::get().getWindowManager()->removeDialog(mGenerateClassResultDialog); mGenerateClassResultDialog = nullptr; MWBase::Environment::get().getMechanicsManager()->setPlayerClass(mGenerateClass); const ESM::Class *klass = MWBase::Environment::get().getWorld()->getStore().get().find(mGenerateClass); mPlayerClass = *klass; updatePlayerHealth(); } void CharacterCreation::onGenerateClassBack() { selectGeneratedClass(); MWBase::Environment::get().getWindowManager()->popGuiMode(); MWBase::Environment::get().getWindowManager()->pushGuiMode(GM_Class); } void CharacterCreation::onGenerateClassDone(WindowBase* parWindow) { selectGeneratedClass(); handleDialogDone(CSE_ClassChosen, GM_Birth); /* Start of tes3mp addition Increase the character generation stage tracked for the LocalPlayer */ mwmp::Main::get().getLocalPlayer()->charGenState.currentStage++; /* End of tes3mp addition */ } CharacterCreation::~CharacterCreation() { delete mNameDialog; delete mRaceDialog; delete mClassChoiceDialog; delete mGenerateClassQuestionDialog; delete mGenerateClassResultDialog; delete mPickClassDialog; delete mCreateClassDialog; delete mBirthSignDialog; delete mReviewDialog; } void CharacterCreation::handleDialogDone(CSE currentStage, int nextMode) { MWBase::Environment::get().getWindowManager()->popGuiMode(); if (mCreationStage == CSE_ReviewNext) { MWBase::Environment::get().getWindowManager()->pushGuiMode(GM_Review); } /* Start of tes3mp change (major) Servers have control over character generation in multiplayer, which is why the automatic transition to the next character generation menu has been commented out here */ /* else if (mCreationStage >= currentStage) { MWBase::Environment::get().getWindowManager()->pushGuiMode((GuiMode)nextMode); } */ /* End of tes3mp change (major) */ else { mCreationStage = currentStage; } } } ================================================ FILE: apps/openmw/mwgui/charactercreation.hpp ================================================ #ifndef CHARACTER_CREATION_HPP #define CHARACTER_CREATION_HPP #include #include #include #include "statswatcher.hpp" namespace osg { class Group; } namespace Resource { class ResourceSystem; } namespace MWGui { class WindowBase; class TextInputDialog; class InfoBoxDialog; class RaceDialog; class DialogueWindow; class ClassChoiceDialog; class GenerateClassResultDialog; class PickClassDialog; class CreateClassDialog; class BirthDialog; class ReviewDialog; class MessageBoxManager; class CharacterCreation : public StatsListener { public: typedef std::vector SkillList; CharacterCreation(osg::Group* parent, Resource::ResourceSystem* resourceSystem); virtual ~CharacterCreation(); //Show a dialog void spawnDialog(const char id); void setValue (const std::string& id, const MWMechanics::AttributeValue& value) override; void setValue (const std::string& id, const MWMechanics::DynamicStat& value) override; void setValue(const ESM::Skill::SkillEnum parSkill, const MWMechanics::SkillValue& value) override; void configureSkills(const SkillList& major, const SkillList& minor) override; void onFrame(float duration); private: osg::Group* mParent; Resource::ResourceSystem* mResourceSystem; SkillList mPlayerMajorSkills, mPlayerMinorSkills; std::map mPlayerAttributes; std::map mPlayerSkillValues; //Dialogs TextInputDialog* mNameDialog; RaceDialog* mRaceDialog; ClassChoiceDialog* mClassChoiceDialog; InfoBoxDialog* mGenerateClassQuestionDialog; GenerateClassResultDialog* mGenerateClassResultDialog; PickClassDialog* mPickClassDialog; CreateClassDialog* mCreateClassDialog; BirthDialog* mBirthSignDialog; ReviewDialog* mReviewDialog; //Player data std::string mPlayerName; std::string mPlayerRaceId; std::string mPlayerBirthSignId; ESM::Class mPlayerClass; //Class generation vars unsigned mGenerateClassStep; // Keeps track of current step in Generate Class dialog ESM::Class::Specialization mGenerateClassResponses[3]; unsigned mGenerateClassSpecializations[3]; // A counter for each specialization which is increased when an answer is chosen std::string mGenerateClass; // In order: Combat, Magic, Stealth ////Dialog events //Name dialog void onNameDialogDone(WindowBase* parWindow); //Race dialog void onRaceDialogDone(WindowBase* parWindow); void onRaceDialogBack(); void selectRace(); //Class dialogs void onClassChoice(int _index); void onPickClassDialogDone(WindowBase* parWindow); void onPickClassDialogBack(); void onCreateClassDialogDone(WindowBase* parWindow); void onCreateClassDialogBack(); void showClassQuestionDialog(); void onClassQuestionChosen(int _index); void onGenerateClassBack(); void onGenerateClassDone(WindowBase* parWindow); void selectGeneratedClass(); void selectCreatedClass(); void selectPickedClass(); //Birthsign dialog void onBirthSignDialogDone(WindowBase* parWindow); void onBirthSignDialogBack(); void selectBirthSign(); //Review dialog void onReviewDialogDone(WindowBase* parWindow); void onReviewDialogBack(); void onReviewActivateDialog(int parDialog); enum CSE //Creation Stage Enum { CSE_NotStarted, CSE_NameChosen, CSE_RaceChosen, CSE_ClassChosen, CSE_BirthSignChosen, CSE_ReviewBack, CSE_ReviewNext }; CSE mCreationStage; // Which state the character creating is in, controls back/next/ok buttons void handleDialogDone(CSE currentStage, int nextMode); }; } #endif ================================================ FILE: apps/openmw/mwgui/class.cpp ================================================ #include "class.hpp" #include #include #include #include "../mwbase/environment.hpp" #include "../mwbase/world.hpp" #include "../mwbase/windowmanager.hpp" #include "../mwmechanics/actorutil.hpp" #include "../mwworld/esmstore.hpp" #include #include "tooltips.hpp" namespace { bool sortClasses(const std::pair& left, const std::pair& right) { return left.second.compare(right.second) < 0; } } namespace MWGui { /* GenerateClassResultDialog */ GenerateClassResultDialog::GenerateClassResultDialog() : WindowModal("openmw_chargen_generate_class_result.layout") { setText("ReflectT", MWBase::Environment::get().getWindowManager()->getGameSettingString("sMessageQuestionAnswer1", "")); getWidget(mClassImage, "ClassImage"); getWidget(mClassName, "ClassName"); MyGUI::Button* backButton; getWidget(backButton, "BackButton"); backButton->setCaptionWithReplacing("#{sMessageQuestionAnswer3}"); backButton->eventMouseButtonClick += MyGUI::newDelegate(this, &GenerateClassResultDialog::onBackClicked); MyGUI::Button* okButton; getWidget(okButton, "OKButton"); okButton->setCaptionWithReplacing("#{sMessageQuestionAnswer2}"); okButton->eventMouseButtonClick += MyGUI::newDelegate(this, &GenerateClassResultDialog::onOkClicked); center(); } std::string GenerateClassResultDialog::getClassId() const { return mClassName->getCaption(); } void GenerateClassResultDialog::setClassId(const std::string &classId) { mCurrentClassId = classId; setClassImage(mClassImage, mCurrentClassId); mClassName->setCaption(MWBase::Environment::get().getWorld()->getStore().get().find(mCurrentClassId)->mName); center(); } // widget controls void GenerateClassResultDialog::onOkClicked(MyGUI::Widget* _sender) { eventDone(this); } void GenerateClassResultDialog::onBackClicked(MyGUI::Widget* _sender) { eventBack(); } /* PickClassDialog */ PickClassDialog::PickClassDialog() : WindowModal("openmw_chargen_class.layout") { // Centre dialog center(); getWidget(mSpecializationName, "SpecializationName"); getWidget(mFavoriteAttribute[0], "FavoriteAttribute0"); getWidget(mFavoriteAttribute[1], "FavoriteAttribute1"); for(int i = 0; i < 5; i++) { char theIndex = '0'+i; getWidget(mMajorSkill[i], std::string("MajorSkill").append(1, theIndex)); getWidget(mMinorSkill[i], std::string("MinorSkill").append(1, theIndex)); } getWidget(mClassList, "ClassList"); mClassList->setScrollVisible(true); mClassList->eventListSelectAccept += MyGUI::newDelegate(this, &PickClassDialog::onAccept); mClassList->eventListChangePosition += MyGUI::newDelegate(this, &PickClassDialog::onSelectClass); getWidget(mClassImage, "ClassImage"); MyGUI::Button* backButton; getWidget(backButton, "BackButton"); backButton->eventMouseButtonClick += MyGUI::newDelegate(this, &PickClassDialog::onBackClicked); MyGUI::Button* okButton; getWidget(okButton, "OKButton"); okButton->eventMouseButtonClick += MyGUI::newDelegate(this, &PickClassDialog::onOkClicked); updateClasses(); updateStats(); } void PickClassDialog::setNextButtonShow(bool shown) { MyGUI::Button* okButton; getWidget(okButton, "OKButton"); if (shown) okButton->setCaption(MWBase::Environment::get().getWindowManager()->getGameSettingString("sNext", "")); else okButton->setCaption(MWBase::Environment::get().getWindowManager()->getGameSettingString("sOK", "")); } void PickClassDialog::onOpen() { WindowModal::onOpen (); updateClasses(); updateStats(); MWBase::Environment::get().getWindowManager()->setKeyFocusWidget(mClassList); // Show the current class by default MWWorld::Ptr player = MWMechanics::getPlayer(); const std::string &classId = player.get()->mBase->mClass; if (!classId.empty()) setClassId(classId); } void PickClassDialog::setClassId(const std::string &classId) { mCurrentClassId = classId; mClassList->setIndexSelected(MyGUI::ITEM_NONE); size_t count = mClassList->getItemCount(); for (size_t i = 0; i < count; ++i) { if (Misc::StringUtils::ciEqual(*mClassList->getItemDataAt(i), classId)) { mClassList->setIndexSelected(i); break; } } updateStats(); } // widget controls void PickClassDialog::onOkClicked(MyGUI::Widget* _sender) { if(mClassList->getIndexSelected() == MyGUI::ITEM_NONE) return; eventDone(this); } void PickClassDialog::onBackClicked(MyGUI::Widget* _sender) { eventBack(); } void PickClassDialog::onAccept(MyGUI::ListBox* _sender, size_t _index) { onSelectClass(_sender, _index); if(mClassList->getIndexSelected() == MyGUI::ITEM_NONE) return; eventDone(this); } void PickClassDialog::onSelectClass(MyGUI::ListBox* _sender, size_t _index) { if (_index == MyGUI::ITEM_NONE) return; const std::string *classId = mClassList->getItemDataAt(_index); if (Misc::StringUtils::ciEqual(mCurrentClassId, *classId)) return; mCurrentClassId = *classId; updateStats(); } // update widget content void PickClassDialog::updateClasses() { mClassList->removeAllItems(); const MWWorld::ESMStore &store = MWBase::Environment::get().getWorld()->getStore(); std::vector > items; // class id, class name for (const ESM::Class& classInfo : store.get()) { bool playable = (classInfo.mData.mIsPlayable != 0); if (!playable) // Only display playable classes continue; if (store.get().isDynamic(classInfo.mId)) continue; // custom-made class not relevant for this dialog items.emplace_back(classInfo.mId, classInfo.mName); } std::sort(items.begin(), items.end(), sortClasses); int index = 0; for (auto& itemPair : items) { const std::string &id = itemPair.first; mClassList->addItem(itemPair.second, id); if (mCurrentClassId.empty()) { mCurrentClassId = id; mClassList->setIndexSelected(index); } else if (Misc::StringUtils::ciEqual(id, mCurrentClassId)) { mClassList->setIndexSelected(index); } ++index; } } void PickClassDialog::updateStats() { if (mCurrentClassId.empty()) return; const MWWorld::ESMStore &store = MWBase::Environment::get().getWorld()->getStore(); const ESM::Class *klass = store.get().search(mCurrentClassId); if (!klass) return; ESM::Class::Specialization specialization = static_cast(klass->mData.mSpecialization); static const char *specIds[3] = { "sSpecializationCombat", "sSpecializationMagic", "sSpecializationStealth" }; std::string specName = MWBase::Environment::get().getWindowManager()->getGameSettingString(specIds[specialization], specIds[specialization]); mSpecializationName->setCaption(specName); ToolTips::createSpecializationToolTip(mSpecializationName, specName, specialization); mFavoriteAttribute[0]->setAttributeId(klass->mData.mAttribute[0]); mFavoriteAttribute[1]->setAttributeId(klass->mData.mAttribute[1]); ToolTips::createAttributeToolTip(mFavoriteAttribute[0], mFavoriteAttribute[0]->getAttributeId()); ToolTips::createAttributeToolTip(mFavoriteAttribute[1], mFavoriteAttribute[1]->getAttributeId()); for (int i = 0; i < 5; ++i) { mMinorSkill[i]->setSkillNumber(klass->mData.mSkills[i][0]); mMajorSkill[i]->setSkillNumber(klass->mData.mSkills[i][1]); ToolTips::createSkillToolTip(mMinorSkill[i], klass->mData.mSkills[i][0]); ToolTips::createSkillToolTip(mMajorSkill[i], klass->mData.mSkills[i][1]); } setClassImage(mClassImage, mCurrentClassId); } /* InfoBoxDialog */ void InfoBoxDialog::fitToText(MyGUI::TextBox* widget) { MyGUI::IntCoord inner = widget->getTextRegion(); MyGUI::IntCoord outer = widget->getCoord(); MyGUI::IntSize size = widget->getTextSize(); size.width += outer.width - inner.width; size.height += outer.height - inner.height; widget->setSize(size); } void InfoBoxDialog::layoutVertically(MyGUI::Widget* widget, int margin) { size_t count = widget->getChildCount(); int pos = 0; pos += margin; int width = 0; for (unsigned i = 0; i < count; ++i) { MyGUI::Widget* child = widget->getChildAt(i); if (!child->getVisible()) continue; child->setPosition(child->getLeft(), pos); width = std::max(width, child->getWidth()); pos += child->getHeight() + margin; } width += margin*2; widget->setSize(width, pos); } InfoBoxDialog::InfoBoxDialog() : WindowModal("openmw_infobox.layout") { getWidget(mTextBox, "TextBox"); getWidget(mText, "Text"); mText->getSubWidgetText()->setWordWrap(true); getWidget(mButtonBar, "ButtonBar"); center(); } void InfoBoxDialog::setText(const std::string &str) { mText->setCaption(str); mTextBox->setVisible(!str.empty()); fitToText(mText); } std::string InfoBoxDialog::getText() const { return mText->getCaption(); } void InfoBoxDialog::setButtons(ButtonList &buttons) { for (MyGUI::Button* button : this->mButtons) { MyGUI::Gui::getInstance().destroyWidget(button); } this->mButtons.clear(); // TODO: The buttons should be generated from a template in the layout file, ie. cloning an existing widget MyGUI::Button* button; MyGUI::IntCoord coord = MyGUI::IntCoord(0, 0, mButtonBar->getWidth(), 10); for (const std::string &text : buttons) { button = mButtonBar->createWidget("MW_Button", coord, MyGUI::Align::Top | MyGUI::Align::HCenter, ""); button->getSubWidgetText()->setWordWrap(true); button->setCaption(text); fitToText(button); button->eventMouseButtonClick += MyGUI::newDelegate(this, &InfoBoxDialog::onButtonClicked); coord.top += button->getHeight(); this->mButtons.push_back(button); } } void InfoBoxDialog::onOpen() { WindowModal::onOpen(); // Fix layout layoutVertically(mTextBox, 4); layoutVertically(mButtonBar, 6); layoutVertically(mMainWidget, 4 + 6); center(); } void InfoBoxDialog::onButtonClicked(MyGUI::Widget* _sender) { int i = 0; for (MyGUI::Button* button : mButtons) { if (button == _sender) { eventButtonSelected(i); return; } ++i; } } /* ClassChoiceDialog */ ClassChoiceDialog::ClassChoiceDialog() : InfoBoxDialog() { setText(""); ButtonList buttons; buttons.push_back(MWBase::Environment::get().getWindowManager()->getGameSettingString("sClassChoiceMenu1", "")); buttons.push_back(MWBase::Environment::get().getWindowManager()->getGameSettingString("sClassChoiceMenu2", "")); buttons.push_back(MWBase::Environment::get().getWindowManager()->getGameSettingString("sClassChoiceMenu3", "")); buttons.push_back(MWBase::Environment::get().getWindowManager()->getGameSettingString("sBack", "")); setButtons(buttons); } /* CreateClassDialog */ CreateClassDialog::CreateClassDialog() : WindowModal("openmw_chargen_create_class.layout") , mSpecDialog(nullptr) , mAttribDialog(nullptr) , mSkillDialog(nullptr) , mDescDialog(nullptr) , mAffectedAttribute(nullptr) , mAffectedSkill(nullptr) { // Centre dialog center(); setText("SpecializationT", MWBase::Environment::get().getWindowManager()->getGameSettingString("sChooseClassMenu1", "Specialization")); getWidget(mSpecializationName, "SpecializationName"); mSpecializationName->eventMouseButtonClick += MyGUI::newDelegate(this, &CreateClassDialog::onSpecializationClicked); setText("FavoriteAttributesT", MWBase::Environment::get().getWindowManager()->getGameSettingString("sChooseClassMenu2", "Favorite Attributes:")); getWidget(mFavoriteAttribute0, "FavoriteAttribute0"); getWidget(mFavoriteAttribute1, "FavoriteAttribute1"); mFavoriteAttribute0->eventClicked += MyGUI::newDelegate(this, &CreateClassDialog::onAttributeClicked); mFavoriteAttribute1->eventClicked += MyGUI::newDelegate(this, &CreateClassDialog::onAttributeClicked); setText("MajorSkillT", MWBase::Environment::get().getWindowManager()->getGameSettingString("sSkillClassMajor", "")); setText("MinorSkillT", MWBase::Environment::get().getWindowManager()->getGameSettingString("sSkillClassMinor", "")); for(int i = 0; i < 5; i++) { char theIndex = '0'+i; getWidget(mMajorSkill[i], std::string("MajorSkill").append(1, theIndex)); getWidget(mMinorSkill[i], std::string("MinorSkill").append(1, theIndex)); mSkills.push_back(mMajorSkill[i]); mSkills.push_back(mMinorSkill[i]); } for (Widgets::MWSkillPtr& skill : mSkills) { skill->eventClicked += MyGUI::newDelegate(this, &CreateClassDialog::onSkillClicked); } setText("LabelT", MWBase::Environment::get().getWindowManager()->getGameSettingString("sName", "")); getWidget(mEditName, "EditName"); // Make sure the edit box has focus MWBase::Environment::get().getWindowManager()->setKeyFocusWidget(mEditName); MyGUI::Button* descriptionButton; getWidget(descriptionButton, "DescriptionButton"); descriptionButton->eventMouseButtonClick += MyGUI::newDelegate(this, &CreateClassDialog::onDescriptionClicked); MyGUI::Button* backButton; getWidget(backButton, "BackButton"); backButton->eventMouseButtonClick += MyGUI::newDelegate(this, &CreateClassDialog::onBackClicked); MyGUI::Button* okButton; getWidget(okButton, "OKButton"); okButton->eventMouseButtonClick += MyGUI::newDelegate(this, &CreateClassDialog::onOkClicked); // Set default skills, attributes mFavoriteAttribute0->setAttributeId(ESM::Attribute::Strength); mFavoriteAttribute1->setAttributeId(ESM::Attribute::Agility); mMajorSkill[0]->setSkillId(ESM::Skill::Block); mMajorSkill[1]->setSkillId(ESM::Skill::Armorer); mMajorSkill[2]->setSkillId(ESM::Skill::MediumArmor); mMajorSkill[3]->setSkillId(ESM::Skill::HeavyArmor); mMajorSkill[4]->setSkillId(ESM::Skill::BluntWeapon); mMinorSkill[0]->setSkillId(ESM::Skill::LongBlade); mMinorSkill[1]->setSkillId(ESM::Skill::Axe); mMinorSkill[2]->setSkillId(ESM::Skill::Spear); mMinorSkill[3]->setSkillId(ESM::Skill::Athletics); mMinorSkill[4]->setSkillId(ESM::Skill::Enchant); setSpecialization(0); update(); } CreateClassDialog::~CreateClassDialog() { delete mSpecDialog; delete mAttribDialog; delete mSkillDialog; delete mDescDialog; } void CreateClassDialog::update() { for (int i = 0; i < 5; ++i) { ToolTips::createSkillToolTip(mMajorSkill[i], mMajorSkill[i]->getSkillId()); ToolTips::createSkillToolTip(mMinorSkill[i], mMinorSkill[i]->getSkillId()); } ToolTips::createAttributeToolTip(mFavoriteAttribute0, mFavoriteAttribute0->getAttributeId()); ToolTips::createAttributeToolTip(mFavoriteAttribute1, mFavoriteAttribute1->getAttributeId()); } std::string CreateClassDialog::getName() const { return mEditName->getCaption(); } std::string CreateClassDialog::getDescription() const { return mDescription; } ESM::Class::Specialization CreateClassDialog::getSpecializationId() const { return mSpecializationId; } std::vector CreateClassDialog::getFavoriteAttributes() const { std::vector v; v.push_back(mFavoriteAttribute0->getAttributeId()); v.push_back(mFavoriteAttribute1->getAttributeId()); return v; } std::vector CreateClassDialog::getMajorSkills() const { std::vector v; for(int i = 0; i < 5; i++) { v.push_back(mMajorSkill[i]->getSkillId()); } return v; } std::vector CreateClassDialog::getMinorSkills() const { std::vector v; for(int i=0; i < 5; i++) { v.push_back(mMinorSkill[i]->getSkillId()); } return v; } void CreateClassDialog::setNextButtonShow(bool shown) { MyGUI::Button* okButton; getWidget(okButton, "OKButton"); if (shown) okButton->setCaption(MWBase::Environment::get().getWindowManager()->getGameSettingString("sNext", "")); else okButton->setCaption(MWBase::Environment::get().getWindowManager()->getGameSettingString("sOK", "")); } // widget controls void CreateClassDialog::onDialogCancel() { MWBase::Environment::get().getWindowManager()->removeDialog(mSpecDialog); mSpecDialog = nullptr; MWBase::Environment::get().getWindowManager()->removeDialog(mAttribDialog); mAttribDialog = nullptr; MWBase::Environment::get().getWindowManager()->removeDialog(mSkillDialog); mSkillDialog = nullptr; MWBase::Environment::get().getWindowManager()->removeDialog(mDescDialog); mDescDialog = nullptr; } void CreateClassDialog::onSpecializationClicked(MyGUI::Widget* _sender) { delete mSpecDialog; mSpecDialog = new SelectSpecializationDialog(); mSpecDialog->eventCancel += MyGUI::newDelegate(this, &CreateClassDialog::onDialogCancel); mSpecDialog->eventItemSelected += MyGUI::newDelegate(this, &CreateClassDialog::onSpecializationSelected); mSpecDialog->setVisible(true); } void CreateClassDialog::onSpecializationSelected() { mSpecializationId = mSpecDialog->getSpecializationId(); setSpecialization(mSpecializationId); MWBase::Environment::get().getWindowManager()->removeDialog(mSpecDialog); mSpecDialog = nullptr; } void CreateClassDialog::setSpecialization(int id) { mSpecializationId = (ESM::Class::Specialization) id; static const char *specIds[3] = { "sSpecializationCombat", "sSpecializationMagic", "sSpecializationStealth" }; std::string specName = MWBase::Environment::get().getWindowManager()->getGameSettingString(specIds[mSpecializationId], specIds[mSpecializationId]); mSpecializationName->setCaption(specName); ToolTips::createSpecializationToolTip(mSpecializationName, specName, mSpecializationId); } void CreateClassDialog::onAttributeClicked(Widgets::MWAttributePtr _sender) { delete mAttribDialog; mAttribDialog = new SelectAttributeDialog(); mAffectedAttribute = _sender; mAttribDialog->eventCancel += MyGUI::newDelegate(this, &CreateClassDialog::onDialogCancel); mAttribDialog->eventItemSelected += MyGUI::newDelegate(this, &CreateClassDialog::onAttributeSelected); mAttribDialog->setVisible(true); } void CreateClassDialog::onAttributeSelected() { ESM::Attribute::AttributeID id = mAttribDialog->getAttributeId(); if (mAffectedAttribute == mFavoriteAttribute0) { if (mFavoriteAttribute1->getAttributeId() == id) mFavoriteAttribute1->setAttributeId(mFavoriteAttribute0->getAttributeId()); } else if (mAffectedAttribute == mFavoriteAttribute1) { if (mFavoriteAttribute0->getAttributeId() == id) mFavoriteAttribute0->setAttributeId(mFavoriteAttribute1->getAttributeId()); } mAffectedAttribute->setAttributeId(id); MWBase::Environment::get().getWindowManager()->removeDialog(mAttribDialog); mAttribDialog = nullptr; update(); } void CreateClassDialog::onSkillClicked(Widgets::MWSkillPtr _sender) { delete mSkillDialog; mSkillDialog = new SelectSkillDialog(); mAffectedSkill = _sender; mSkillDialog->eventCancel += MyGUI::newDelegate(this, &CreateClassDialog::onDialogCancel); mSkillDialog->eventItemSelected += MyGUI::newDelegate(this, &CreateClassDialog::onSkillSelected); mSkillDialog->setVisible(true); } void CreateClassDialog::onSkillSelected() { ESM::Skill::SkillEnum id = mSkillDialog->getSkillId(); // Avoid duplicate skills by swapping any skill field that matches the selected one for (Widgets::MWSkillPtr& skill : mSkills) { if (skill == mAffectedSkill) continue; if (skill->getSkillId() == id) { skill->setSkillId(mAffectedSkill->getSkillId()); break; } } mAffectedSkill->setSkillId(mSkillDialog->getSkillId()); MWBase::Environment::get().getWindowManager()->removeDialog(mSkillDialog); mSkillDialog = nullptr; update(); } void CreateClassDialog::onDescriptionClicked(MyGUI::Widget* _sender) { mDescDialog = new DescriptionDialog(); mDescDialog->setTextInput(mDescription); mDescDialog->eventDone += MyGUI::newDelegate(this, &CreateClassDialog::onDescriptionEntered); mDescDialog->setVisible(true); } void CreateClassDialog::onDescriptionEntered(WindowBase* parWindow) { mDescription = mDescDialog->getTextInput(); MWBase::Environment::get().getWindowManager()->removeDialog(mDescDialog); mDescDialog = nullptr; } void CreateClassDialog::onOkClicked(MyGUI::Widget* _sender) { if(getName().size() <= 0) return; eventDone(this); } void CreateClassDialog::onBackClicked(MyGUI::Widget* _sender) { eventBack(); } /* SelectSpecializationDialog */ SelectSpecializationDialog::SelectSpecializationDialog() : WindowModal("openmw_chargen_select_specialization.layout") { // Centre dialog center(); getWidget(mSpecialization0, "Specialization0"); getWidget(mSpecialization1, "Specialization1"); getWidget(mSpecialization2, "Specialization2"); std::string combat = MWBase::Environment::get().getWindowManager()->getGameSettingString(ESM::Class::sGmstSpecializationIds[ESM::Class::Combat], ""); std::string magic = MWBase::Environment::get().getWindowManager()->getGameSettingString(ESM::Class::sGmstSpecializationIds[ESM::Class::Magic], ""); std::string stealth = MWBase::Environment::get().getWindowManager()->getGameSettingString(ESM::Class::sGmstSpecializationIds[ESM::Class::Stealth], ""); mSpecialization0->setCaption(combat); mSpecialization0->eventMouseButtonClick += MyGUI::newDelegate(this, &SelectSpecializationDialog::onSpecializationClicked); mSpecialization1->setCaption(magic); mSpecialization1->eventMouseButtonClick += MyGUI::newDelegate(this, &SelectSpecializationDialog::onSpecializationClicked); mSpecialization2->setCaption(stealth); mSpecialization2->eventMouseButtonClick += MyGUI::newDelegate(this, &SelectSpecializationDialog::onSpecializationClicked); mSpecializationId = ESM::Class::Combat; ToolTips::createSpecializationToolTip(mSpecialization0, combat, ESM::Class::Combat); ToolTips::createSpecializationToolTip(mSpecialization1, magic, ESM::Class::Magic); ToolTips::createSpecializationToolTip(mSpecialization2, stealth, ESM::Class::Stealth); MyGUI::Button* cancelButton; getWidget(cancelButton, "CancelButton"); cancelButton->eventMouseButtonClick += MyGUI::newDelegate(this, &SelectSpecializationDialog::onCancelClicked); } SelectSpecializationDialog::~SelectSpecializationDialog() { } // widget controls void SelectSpecializationDialog::onSpecializationClicked(MyGUI::Widget* _sender) { if (_sender == mSpecialization0) mSpecializationId = ESM::Class::Combat; else if (_sender == mSpecialization1) mSpecializationId = ESM::Class::Magic; else if (_sender == mSpecialization2) mSpecializationId = ESM::Class::Stealth; else return; eventItemSelected(); } void SelectSpecializationDialog::onCancelClicked(MyGUI::Widget* _sender) { exit(); } bool SelectSpecializationDialog::exit() { eventCancel(); return true; } /* SelectAttributeDialog */ SelectAttributeDialog::SelectAttributeDialog() : WindowModal("openmw_chargen_select_attribute.layout") , mAttributeId(ESM::Attribute::Strength) { // Centre dialog center(); for (int i = 0; i < 8; ++i) { Widgets::MWAttributePtr attribute; char theIndex = '0'+i; getWidget(attribute, std::string("Attribute").append(1, theIndex)); attribute->setAttributeId(ESM::Attribute::sAttributeIds[i]); attribute->eventClicked += MyGUI::newDelegate(this, &SelectAttributeDialog::onAttributeClicked); ToolTips::createAttributeToolTip(attribute, attribute->getAttributeId()); } MyGUI::Button* cancelButton; getWidget(cancelButton, "CancelButton"); cancelButton->eventMouseButtonClick += MyGUI::newDelegate(this, &SelectAttributeDialog::onCancelClicked); } SelectAttributeDialog::~SelectAttributeDialog() { } // widget controls void SelectAttributeDialog::onAttributeClicked(Widgets::MWAttributePtr _sender) { // TODO: Change MWAttribute to set and get AttributeID enum instead of int mAttributeId = static_cast(_sender->getAttributeId()); eventItemSelected(); } void SelectAttributeDialog::onCancelClicked(MyGUI::Widget* _sender) { exit(); } bool SelectAttributeDialog::exit() { eventCancel(); return true; } /* SelectSkillDialog */ SelectSkillDialog::SelectSkillDialog() : WindowModal("openmw_chargen_select_skill.layout") , mSkillId(ESM::Skill::Block) { // Centre dialog center(); for(int i = 0; i < 9; i++) { char theIndex = '0'+i; getWidget(mCombatSkill[i], std::string("CombatSkill").append(1, theIndex)); getWidget(mMagicSkill[i], std::string("MagicSkill").append(1, theIndex)); getWidget(mStealthSkill[i], std::string("StealthSkill").append(1, theIndex)); } struct {Widgets::MWSkillPtr widget; ESM::Skill::SkillEnum skillId;} mSkills[3][9] = { { {mCombatSkill[0], ESM::Skill::Block}, {mCombatSkill[1], ESM::Skill::Armorer}, {mCombatSkill[2], ESM::Skill::MediumArmor}, {mCombatSkill[3], ESM::Skill::HeavyArmor}, {mCombatSkill[4], ESM::Skill::BluntWeapon}, {mCombatSkill[5], ESM::Skill::LongBlade}, {mCombatSkill[6], ESM::Skill::Axe}, {mCombatSkill[7], ESM::Skill::Spear}, {mCombatSkill[8], ESM::Skill::Athletics} }, { {mMagicSkill[0], ESM::Skill::Enchant}, {mMagicSkill[1], ESM::Skill::Destruction}, {mMagicSkill[2], ESM::Skill::Alteration}, {mMagicSkill[3], ESM::Skill::Illusion}, {mMagicSkill[4], ESM::Skill::Conjuration}, {mMagicSkill[5], ESM::Skill::Mysticism}, {mMagicSkill[6], ESM::Skill::Restoration}, {mMagicSkill[7], ESM::Skill::Alchemy}, {mMagicSkill[8], ESM::Skill::Unarmored} }, { {mStealthSkill[0], ESM::Skill::Security}, {mStealthSkill[1], ESM::Skill::Sneak}, {mStealthSkill[2], ESM::Skill::Acrobatics}, {mStealthSkill[3], ESM::Skill::LightArmor}, {mStealthSkill[4], ESM::Skill::ShortBlade}, {mStealthSkill[5] ,ESM::Skill::Marksman}, {mStealthSkill[6] ,ESM::Skill::Mercantile}, {mStealthSkill[7] ,ESM::Skill::Speechcraft}, {mStealthSkill[8] ,ESM::Skill::HandToHand} } }; for (int spec = 0; spec < 3; ++spec) { for (int i = 0; i < 9; ++i) { mSkills[spec][i].widget->setSkillId(mSkills[spec][i].skillId); mSkills[spec][i].widget->eventClicked += MyGUI::newDelegate(this, &SelectSkillDialog::onSkillClicked); ToolTips::createSkillToolTip(mSkills[spec][i].widget, mSkills[spec][i].widget->getSkillId()); } } MyGUI::Button* cancelButton; getWidget(cancelButton, "CancelButton"); cancelButton->eventMouseButtonClick += MyGUI::newDelegate(this, &SelectSkillDialog::onCancelClicked); } SelectSkillDialog::~SelectSkillDialog() { } // widget controls void SelectSkillDialog::onSkillClicked(Widgets::MWSkillPtr _sender) { mSkillId = _sender->getSkillId(); eventItemSelected(); } void SelectSkillDialog::onCancelClicked(MyGUI::Widget* _sender) { exit(); } bool SelectSkillDialog::exit() { eventCancel(); return true; } /* DescriptionDialog */ DescriptionDialog::DescriptionDialog() : WindowModal("openmw_chargen_class_description.layout") { // Centre dialog center(); getWidget(mTextEdit, "TextEdit"); MyGUI::Button* okButton; getWidget(okButton, "OKButton"); okButton->eventMouseButtonClick += MyGUI::newDelegate(this, &DescriptionDialog::onOkClicked); okButton->setCaption(MWBase::Environment::get().getWindowManager()->getGameSettingString("sInputMenu1", "")); // Make sure the edit box has focus MWBase::Environment::get().getWindowManager()->setKeyFocusWidget(mTextEdit); } DescriptionDialog::~DescriptionDialog() { } // widget controls void DescriptionDialog::onOkClicked(MyGUI::Widget* _sender) { eventDone(this); } void setClassImage(MyGUI::ImageBox* imageBox, const std::string &classId) { std::string classImage = std::string("textures\\levelup\\") + classId + ".dds"; if (!MWBase::Environment::get().getWindowManager()->textureExists(classImage)) { Log(Debug::Warning) << "No class image for " << classId << ", falling back to default"; classImage = "textures\\levelup\\warrior.dds"; } imageBox->setImageTexture(classImage); } } ================================================ FILE: apps/openmw/mwgui/class.hpp ================================================ #ifndef MWGUI_CLASS_H #define MWGUI_CLASS_H #include #include #include "widgets.hpp" #include "windowbase.hpp" namespace MWGui { void setClassImage(MyGUI::ImageBox* imageBox, const std::string& classId); class InfoBoxDialog : public WindowModal { public: InfoBoxDialog(); typedef std::vector ButtonList; void setText(const std::string &str); std::string getText() const; void setButtons(ButtonList &buttons); void onOpen() override; bool exit() override { return false; } // Events typedef MyGUI::delegates::CMultiDelegate1 EventHandle_Int; /** Event : Button was clicked.\n signature : void method(int index)\n */ EventHandle_Int eventButtonSelected; protected: void onButtonClicked(MyGUI::Widget* _sender); private: void fitToText(MyGUI::TextBox* widget); void layoutVertically(MyGUI::Widget* widget, int margin); MyGUI::Widget* mTextBox; MyGUI::TextBox* mText; MyGUI::Widget* mButtonBar; std::vector mButtons; }; // Lets the player choose between 3 ways of creating a class class ClassChoiceDialog : public InfoBoxDialog { public: // Corresponds to the buttons that can be clicked enum ClassChoice { Class_Generate = 0, Class_Pick = 1, Class_Create = 2, Class_Back = 3 }; ClassChoiceDialog(); }; class GenerateClassResultDialog : public WindowModal { public: GenerateClassResultDialog(); std::string getClassId() const; void setClassId(const std::string &classId); bool exit() override { return false; } // Events typedef MyGUI::delegates::CMultiDelegate0 EventHandle_Void; /** Event : Back button clicked.\n signature : void method()\n */ EventHandle_Void eventBack; /** Event : Dialog finished, OK button clicked.\n signature : void method()\n */ EventHandle_WindowBase eventDone; protected: void onOkClicked(MyGUI::Widget* _sender); void onBackClicked(MyGUI::Widget* _sender); private: MyGUI::ImageBox* mClassImage; MyGUI::TextBox* mClassName; std::string mCurrentClassId; }; class PickClassDialog : public WindowModal { public: PickClassDialog(); const std::string &getClassId() const { return mCurrentClassId; } void setClassId(const std::string &classId); void setNextButtonShow(bool shown); void onOpen() override; bool exit() override { return false; } // Events typedef MyGUI::delegates::CMultiDelegate0 EventHandle_Void; /** Event : Back button clicked.\n signature : void method()\n */ EventHandle_Void eventBack; /** Event : Dialog finished, OK button clicked.\n signature : void method()\n */ EventHandle_WindowBase eventDone; protected: void onSelectClass(MyGUI::ListBox* _sender, size_t _index); void onAccept(MyGUI::ListBox* _sender, size_t _index); void onOkClicked(MyGUI::Widget* _sender); void onBackClicked(MyGUI::Widget* _sender); private: void updateClasses(); void updateStats(); MyGUI::ImageBox* mClassImage; MyGUI::ListBox* mClassList; MyGUI::TextBox* mSpecializationName; Widgets::MWAttributePtr mFavoriteAttribute[2]; Widgets::MWSkillPtr mMajorSkill[5]; Widgets::MWSkillPtr mMinorSkill[5]; std::string mCurrentClassId; }; class SelectSpecializationDialog : public WindowModal { public: SelectSpecializationDialog(); ~SelectSpecializationDialog(); bool exit() override; ESM::Class::Specialization getSpecializationId() const { return mSpecializationId; } // Events typedef MyGUI::delegates::CMultiDelegate0 EventHandle_Void; /** Event : Cancel button clicked.\n signature : void method()\n */ EventHandle_Void eventCancel; /** Event : Dialog finished, specialization selected.\n signature : void method()\n */ EventHandle_Void eventItemSelected; protected: void onSpecializationClicked(MyGUI::Widget* _sender); void onCancelClicked(MyGUI::Widget* _sender); private: MyGUI::TextBox *mSpecialization0, *mSpecialization1, *mSpecialization2; ESM::Class::Specialization mSpecializationId; }; class SelectAttributeDialog : public WindowModal { public: SelectAttributeDialog(); ~SelectAttributeDialog(); bool exit() override; ESM::Attribute::AttributeID getAttributeId() const { return mAttributeId; } // Events typedef MyGUI::delegates::CMultiDelegate0 EventHandle_Void; /** Event : Cancel button clicked.\n signature : void method()\n */ EventHandle_Void eventCancel; /** Event : Dialog finished, attribute selected.\n signature : void method()\n */ EventHandle_Void eventItemSelected; protected: void onAttributeClicked(Widgets::MWAttributePtr _sender); void onCancelClicked(MyGUI::Widget* _sender); private: ESM::Attribute::AttributeID mAttributeId; }; class SelectSkillDialog : public WindowModal { public: SelectSkillDialog(); ~SelectSkillDialog(); bool exit() override; ESM::Skill::SkillEnum getSkillId() const { return mSkillId; } // Events typedef MyGUI::delegates::CMultiDelegate0 EventHandle_Void; /** Event : Cancel button clicked.\n signature : void method()\n */ EventHandle_Void eventCancel; /** Event : Dialog finished, skill selected.\n signature : void method()\n */ EventHandle_Void eventItemSelected; protected: void onSkillClicked(Widgets::MWSkillPtr _sender); void onCancelClicked(MyGUI::Widget* _sender); private: Widgets::MWSkillPtr mCombatSkill[9]; Widgets::MWSkillPtr mMagicSkill[9]; Widgets::MWSkillPtr mStealthSkill[9]; ESM::Skill::SkillEnum mSkillId; }; class DescriptionDialog : public WindowModal { public: DescriptionDialog(); ~DescriptionDialog(); std::string getTextInput() const { return mTextEdit->getCaption(); } void setTextInput(const std::string &text) { mTextEdit->setCaption(text); } /** Event : Dialog finished, OK button clicked.\n signature : void method()\n */ EventHandle_WindowBase eventDone; protected: void onOkClicked(MyGUI::Widget* _sender); private: MyGUI::EditBox* mTextEdit; }; class CreateClassDialog : public WindowModal { public: CreateClassDialog(); virtual ~CreateClassDialog(); bool exit() override { return false; } std::string getName() const; std::string getDescription() const; ESM::Class::Specialization getSpecializationId() const; std::vector getFavoriteAttributes() const; std::vector getMajorSkills() const; std::vector getMinorSkills() const; void setNextButtonShow(bool shown); // Events typedef MyGUI::delegates::CMultiDelegate0 EventHandle_Void; /** Event : Back button clicked.\n signature : void method()\n */ EventHandle_Void eventBack; /** Event : Dialog finished, OK button clicked.\n signature : void method()\n */ EventHandle_WindowBase eventDone; protected: void onOkClicked(MyGUI::Widget* _sender); void onBackClicked(MyGUI::Widget* _sender); void onSpecializationClicked(MyGUI::Widget* _sender); void onSpecializationSelected(); void onAttributeClicked(Widgets::MWAttributePtr _sender); void onAttributeSelected(); void onSkillClicked(Widgets::MWSkillPtr _sender); void onSkillSelected(); void onDescriptionClicked(MyGUI::Widget* _sender); void onDescriptionEntered(WindowBase* parWindow); void onDialogCancel(); void setSpecialization(int id); void update(); private: MyGUI::EditBox* mEditName; MyGUI::TextBox* mSpecializationName; Widgets::MWAttributePtr mFavoriteAttribute0, mFavoriteAttribute1; Widgets::MWSkillPtr mMajorSkill[5]; Widgets::MWSkillPtr mMinorSkill[5]; std::vector mSkills; std::string mDescription; SelectSpecializationDialog *mSpecDialog; SelectAttributeDialog *mAttribDialog; SelectSkillDialog *mSkillDialog; DescriptionDialog *mDescDialog; ESM::Class::Specialization mSpecializationId; Widgets::MWAttributePtr mAffectedAttribute; Widgets::MWSkillPtr mAffectedSkill; }; } #endif ================================================ FILE: apps/openmw/mwgui/companionitemmodel.cpp ================================================ #include "companionitemmodel.hpp" #include "../mwworld/class.hpp" namespace { void modifyProfit(const MWWorld::Ptr& actor, int diff) { std::string script = actor.getClass().getScript(actor); if (!script.empty()) { int profit = actor.getRefData().getLocals().getIntVar(script, "minimumprofit"); profit += diff; actor.getRefData().getLocals().setVarByInt(script, "minimumprofit", profit); } } } namespace MWGui { CompanionItemModel::CompanionItemModel(const MWWorld::Ptr &actor) : InventoryItemModel(actor) { } MWWorld::Ptr CompanionItemModel::copyItem (const ItemStack& item, size_t count, bool allowAutoEquip) { if (hasProfit(mActor)) modifyProfit(mActor, item.mBase.getClass().getValue(item.mBase) * count); return InventoryItemModel::copyItem(item, count, allowAutoEquip); } void CompanionItemModel::removeItem (const ItemStack& item, size_t count) { if (hasProfit(mActor)) modifyProfit(mActor, -item.mBase.getClass().getValue(item.mBase) * count); InventoryItemModel::removeItem(item, count); } bool CompanionItemModel::hasProfit(const MWWorld::Ptr &actor) { std::string script = actor.getClass().getScript(actor); if (script.empty()) return false; return actor.getRefData().getLocals().hasVar(script, "minimumprofit"); } } ================================================ FILE: apps/openmw/mwgui/companionitemmodel.hpp ================================================ #ifndef MWGUI_COMPANION_ITEM_MODEL_H #define MWGUI_COMPANION_ITEM_MODEL_H #include "inventoryitemmodel.hpp" namespace MWGui { /// @brief The companion item model keeps track of the companion's profit by /// monitoring which items are being added to and removed from the model. class CompanionItemModel : public InventoryItemModel { public: CompanionItemModel (const MWWorld::Ptr& actor); MWWorld::Ptr copyItem (const ItemStack& item, size_t count, bool allowAutoEquip = true) override; void removeItem (const ItemStack& item, size_t count) override; bool hasProfit(const MWWorld::Ptr& actor); }; } #endif ================================================ FILE: apps/openmw/mwgui/companionwindow.cpp ================================================ #include "companionwindow.hpp" #include #include #include "../mwbase/environment.hpp" #include "../mwbase/windowmanager.hpp" #include "../mwworld/class.hpp" #include "messagebox.hpp" #include "itemview.hpp" #include "sortfilteritemmodel.hpp" #include "companionitemmodel.hpp" #include "draganddrop.hpp" #include "countdialog.hpp" #include "widgets.hpp" #include "tooltips.hpp" namespace { int getProfit(const MWWorld::Ptr& actor) { std::string script = actor.getClass().getScript(actor); if (!script.empty()) { return actor.getRefData().getLocals().getIntVar(script, "minimumprofit"); } return 0; } } namespace MWGui { CompanionWindow::CompanionWindow(DragAndDrop *dragAndDrop, MessageBoxManager* manager) : WindowBase("openmw_companion_window.layout") , mSortModel(nullptr) , mModel(nullptr) , mSelectedItem(-1) , mDragAndDrop(dragAndDrop) , mMessageBoxManager(manager) { getWidget(mCloseButton, "CloseButton"); getWidget(mProfitLabel, "ProfitLabel"); getWidget(mEncumbranceBar, "EncumbranceBar"); getWidget(mFilterEdit, "FilterEdit"); getWidget(mItemView, "ItemView"); mItemView->eventBackgroundClicked += MyGUI::newDelegate(this, &CompanionWindow::onBackgroundSelected); mItemView->eventItemClicked += MyGUI::newDelegate(this, &CompanionWindow::onItemSelected); mFilterEdit->eventEditTextChange += MyGUI::newDelegate(this, &CompanionWindow::onNameFilterChanged); mCloseButton->eventMouseButtonClick += MyGUI::newDelegate(this, &CompanionWindow::onCloseButtonClicked); setCoord(200,0,600,300); } void CompanionWindow::onItemSelected(int index) { if (mDragAndDrop->mIsOnDragAndDrop) { mDragAndDrop->drop(mModel, mItemView); updateEncumbranceBar(); return; } const ItemStack& item = mSortModel->getItem(index); // We can't take conjured items from a companion NPC if (item.mFlags & ItemStack::Flag_Bound) { MWBase::Environment::get().getWindowManager()->messageBox("#{sBarterDialog12}"); return; } MWWorld::Ptr object = item.mBase; int count = item.mCount; bool shift = MyGUI::InputManager::getInstance().isShiftPressed(); if (MyGUI::InputManager::getInstance().isControlPressed()) count = 1; mSelectedItem = mSortModel->mapToSource(index); if (count > 1 && !shift) { CountDialog* dialog = MWBase::Environment::get().getWindowManager()->getCountDialog(); std::string name = object.getClass().getName(object) + MWGui::ToolTips::getSoulString(object.getCellRef()); dialog->openCountDialog(name, "#{sTake}", count); dialog->eventOkClicked.clear(); dialog->eventOkClicked += MyGUI::newDelegate(this, &CompanionWindow::dragItem); } else dragItem (nullptr, count); } void CompanionWindow::onNameFilterChanged(MyGUI::EditBox* _sender) { mSortModel->setNameFilter(_sender->getCaption()); mItemView->update(); } void CompanionWindow::dragItem(MyGUI::Widget* sender, int count) { mDragAndDrop->startDrag(mSelectedItem, mSortModel, mModel, mItemView, count); } void CompanionWindow::onBackgroundSelected() { if (mDragAndDrop->mIsOnDragAndDrop) { mDragAndDrop->drop(mModel, mItemView); updateEncumbranceBar(); } } void CompanionWindow::setPtr(const MWWorld::Ptr& npc) { mPtr = npc; updateEncumbranceBar(); mModel = new CompanionItemModel(npc); mSortModel = new SortFilterItemModel(mModel); mFilterEdit->setCaption(std::string()); mItemView->setModel(mSortModel); mItemView->resetScrollBars(); setTitle(npc.getClass().getName(npc)); } void CompanionWindow::onFrame(float dt) { checkReferenceAvailable(); updateEncumbranceBar(); } void CompanionWindow::updateEncumbranceBar() { if (mPtr.isEmpty()) return; float capacity = mPtr.getClass().getCapacity(mPtr); float encumbrance = mPtr.getClass().getEncumbrance(mPtr); mEncumbranceBar->setValue(std::ceil(encumbrance), static_cast(capacity)); if (mModel && mModel->hasProfit(mPtr)) { mProfitLabel->setCaptionWithReplacing("#{sProfitValue} " + MyGUI::utility::toString(getProfit(mPtr))); } else mProfitLabel->setCaption(""); } void CompanionWindow::onCloseButtonClicked(MyGUI::Widget* _sender) { if (exit()) MWBase::Environment::get().getWindowManager()->removeGuiMode(GM_Companion); } bool CompanionWindow::exit() { if (mModel && mModel->hasProfit(mPtr) && getProfit(mPtr) < 0) { std::vector buttons; buttons.emplace_back("#{sCompanionWarningButtonOne}"); buttons.emplace_back("#{sCompanionWarningButtonTwo}"); mMessageBoxManager->createInteractiveMessageBox("#{sCompanionWarningMessage}", buttons); mMessageBoxManager->eventButtonPressed += MyGUI::newDelegate(this, &CompanionWindow::onMessageBoxButtonClicked); return false; } return true; } void CompanionWindow::onMessageBoxButtonClicked(int button) { if (button == 0) { MWBase::Environment::get().getWindowManager()->removeGuiMode(GM_Companion); // Important for Calvus' contract script to work properly MWBase::Environment::get().getWindowManager()->exitCurrentGuiMode(); } } void CompanionWindow::onReferenceUnavailable() { MWBase::Environment::get().getWindowManager()->removeGuiMode(GM_Companion); } void CompanionWindow::resetReference() { ReferenceInterface::resetReference(); mItemView->setModel(nullptr); mModel = nullptr; mSortModel = nullptr; } } ================================================ FILE: apps/openmw/mwgui/companionwindow.hpp ================================================ #ifndef OPENMW_MWGUI_COMPANIONWINDOW_H #define OPENMW_MWGUI_COMPANIONWINDOW_H #include "windowbase.hpp" #include "referenceinterface.hpp" namespace MWGui { namespace Widgets { class MWDynamicStat; } class MessageBoxManager; class ItemView; class DragAndDrop; class SortFilterItemModel; class CompanionItemModel; class CompanionWindow : public WindowBase, public ReferenceInterface { public: CompanionWindow(DragAndDrop* dragAndDrop, MessageBoxManager* manager); bool exit() override; void resetReference() override; void setPtr(const MWWorld::Ptr& npc) override; void onFrame (float dt) override; void clear() override { resetReference(); } private: ItemView* mItemView; SortFilterItemModel* mSortModel; CompanionItemModel* mModel; int mSelectedItem; DragAndDrop* mDragAndDrop; MyGUI::Button* mCloseButton; MyGUI::EditBox* mFilterEdit; MyGUI::TextBox* mProfitLabel; Widgets::MWDynamicStat* mEncumbranceBar; MessageBoxManager* mMessageBoxManager; void onItemSelected(int index); void onNameFilterChanged(MyGUI::EditBox* _sender); void onBackgroundSelected(); void dragItem(MyGUI::Widget* sender, int count); void onMessageBoxButtonClicked(int button); void updateEncumbranceBar(); void onCloseButtonClicked(MyGUI::Widget* _sender); void onReferenceUnavailable() override; }; } #endif ================================================ FILE: apps/openmw/mwgui/confirmationdialog.cpp ================================================ #include "confirmationdialog.hpp" #include #include #include "../mwbase/environment.hpp" #include "../mwbase/windowmanager.hpp" namespace MWGui { ConfirmationDialog::ConfirmationDialog() : WindowModal("openmw_confirmation_dialog.layout") { getWidget(mMessage, "Message"); getWidget(mOkButton, "OkButton"); getWidget(mCancelButton, "CancelButton"); mCancelButton->eventMouseButtonClick += MyGUI::newDelegate(this, &ConfirmationDialog::onCancelButtonClicked); mOkButton->eventMouseButtonClick += MyGUI::newDelegate(this, &ConfirmationDialog::onOkButtonClicked); } void ConfirmationDialog::askForConfirmation(const std::string& message) { setVisible(true); mMessage->setCaptionWithReplacing(message); int height = mMessage->getTextSize().height + 60; int width = mMessage->getTextSize().width + 24; mMainWidget->setSize(width, height); mMessage->setSize(mMessage->getWidth(), mMessage->getTextSize().height + 24); MWBase::Environment::get().getWindowManager()->setKeyFocusWidget(mOkButton); center(); } bool ConfirmationDialog::exit() { setVisible(false); eventCancelClicked(); return true; } void ConfirmationDialog::onCancelButtonClicked(MyGUI::Widget* _sender) { exit(); } void ConfirmationDialog::onOkButtonClicked(MyGUI::Widget* _sender) { setVisible(false); eventOkClicked(); } } ================================================ FILE: apps/openmw/mwgui/confirmationdialog.hpp ================================================ #ifndef MWGUI_CONFIRMATIONDIALOG_H #define MWGUI_CONFIRMATIONDIALOG_H #include "windowbase.hpp" namespace MWGui { class ConfirmationDialog : public WindowModal { public: ConfirmationDialog(); void askForConfirmation(const std::string& message); bool exit() override; typedef MyGUI::delegates::CMultiDelegate0 EventHandle_Void; /** Event : Ok button was clicked.\n signature : void method()\n */ EventHandle_Void eventOkClicked; EventHandle_Void eventCancelClicked; private: MyGUI::EditBox* mMessage; MyGUI::Button* mOkButton; MyGUI::Button* mCancelButton; void onCancelButtonClicked(MyGUI::Widget* _sender); void onOkButtonClicked(MyGUI::Widget* _sender); }; } #endif ================================================ FILE: apps/openmw/mwgui/console.cpp ================================================ #include "console.hpp" #include #include #include #include #include /* Start of tes3mp addition Include additional headers for multiplayer purposes */ #include #include "../mwmp/Main.hpp" #include "../mwmp/Networking.hpp" #include "../mwmp/LocalPlayer.hpp" #include "../mwmp/ObjectList.hpp" /* End of tes3mp addition */ #include #include #include #include #include #include #include "../mwscript/extensions.hpp" #include "../mwbase/environment.hpp" #include "../mwbase/scriptmanager.hpp" #include "../mwbase/windowmanager.hpp" #include "../mwbase/world.hpp" #include "../mwworld/esmstore.hpp" #include "../mwworld/class.hpp" namespace MWGui { class ConsoleInterpreterContext : public MWScript::InterpreterContext { Console& mConsole; public: ConsoleInterpreterContext (Console& console, MWWorld::Ptr reference); void report (const std::string& message) override; }; ConsoleInterpreterContext::ConsoleInterpreterContext (Console& console, MWWorld::Ptr reference) : MWScript::InterpreterContext ( reference.isEmpty() ? nullptr : &reference.getRefData().getLocals(), reference), mConsole (console) {} void ConsoleInterpreterContext::report (const std::string& message) { mConsole.printOK (message); } bool Console::compile (const std::string& cmd, Compiler::Output& output) { try { ErrorHandler::reset(); std::istringstream input (cmd + '\n'); Compiler::Scanner scanner (*this, input, mCompilerContext.getExtensions()); Compiler::LineParser parser (*this, mCompilerContext, output.getLocals(), output.getLiterals(), output.getCode(), true); scanner.scan (parser); return isGood(); } catch (const Compiler::SourceException&) { // error has already been reported via error handler } catch (const std::exception& error) { printError (std::string ("Error: ") + error.what()); } return false; } void Console::report (const std::string& message, const Compiler::TokenLoc& loc, Type type) { std::ostringstream error; error << "column " << loc.mColumn << " (" << loc.mLiteral << "):"; printError (error.str()); printError ((type==ErrorMessage ? "error: " : "warning: ") + message); } void Console::report (const std::string& message, Type type) { printError ((type==ErrorMessage ? "error: " : "warning: ") + message); } void Console::listNames() { if (mNames.empty()) { // keywords std::istringstream input (""); Compiler::Scanner scanner (*this, input, mCompilerContext.getExtensions()); scanner.listKeywords (mNames); // identifier const MWWorld::ESMStore& store = MWBase::Environment::get().getWorld()->getStore(); for (MWWorld::ESMStore::iterator it = store.begin(); it != store.end(); ++it) { it->second->listIdentifier (mNames); } // exterior cell names aren't technically identifiers, but since the COC function accepts them, // we should list them too for (MWWorld::Store::iterator it = store.get().extBegin(); it != store.get().extEnd(); ++it) { if (!it->mName.empty()) mNames.push_back(it->mName); } // sort std::sort (mNames.begin(), mNames.end()); // remove duplicates mNames.erase( std::unique( mNames.begin(), mNames.end() ), mNames.end() ); } } Console::Console(int w, int h, bool consoleOnlyScripts) : WindowBase("openmw_console.layout"), mCompilerContext (MWScript::CompilerContext::Type_Console), mConsoleOnlyScripts (consoleOnlyScripts) { setCoord(10,10, w-10, h/2); getWidget(mCommandLine, "edit_Command"); getWidget(mHistory, "list_History"); // Set up the command line box mCommandLine->eventEditSelectAccept += newDelegate(this, &Console::acceptCommand); mCommandLine->eventKeyButtonPressed += newDelegate(this, &Console::keyPress); // Set up the log window mHistory->setOverflowToTheLeft(true); // compiler Compiler::registerExtensions (mExtensions, mConsoleOnlyScripts); mCompilerContext.setExtensions (&mExtensions); } void Console::onOpen() { // Give keyboard focus to the combo box whenever the console is // turned on and place it over other widgets MWBase::Environment::get().getWindowManager()->setKeyFocusWidget(mCommandLine); MyGUI::LayerManager::getInstance().upLayerItem(mMainWidget); } void Console::print(const std::string &msg, const std::string& color) { mHistory->addText(color + MyGUI::TextIterator::toTagsString(msg)); } void Console::printOK(const std::string &msg) { print(msg + "\n", "#FF00FF"); } void Console::printError(const std::string &msg) { print(msg + "\n", "#FF2222"); } void Console::execute (const std::string& command) { // Log the command print("> " + command + "\n"); Compiler::Locals locals; if (!mPtr.isEmpty()) { std::string script = mPtr.getClass().getScript(mPtr); if (!script.empty()) locals = MWBase::Environment::get().getScriptManager()->getLocals(script); } Compiler::Output output (locals); if (compile (command + "\n", output)) { try { ConsoleInterpreterContext interpreterContext (*this, mPtr); /* Start of tes3mp addition Send an ID_CONSOLE_COMMAND packet to the server with the command and target used Mark this InterpreterContext as having a CONSOLE context, so that packets sent by the Interpreter can have their origin determined by serverside scripts */ interpreterContext.trackContextType(Interpreter::Context::CONSOLE); mwmp::ObjectList *objectList = mwmp::Main::get().getNetworking()->getObjectList(); objectList->reset(); objectList->packetOrigin = mwmp::CLIENT_CONSOLE; objectList->consoleCommand = command; if (mPtr.isEmpty()) objectList->cell = mwmp::Main::get().getLocalPlayer()->cell; else { objectList->addObjectGeneric(mPtr); } objectList->sendConsoleCommand(); /* End of tes3mp addition */ Interpreter::Interpreter interpreter; MWScript::installOpcodes (interpreter, mConsoleOnlyScripts); std::vector code; output.getCode (code); interpreter.run (&code[0], code.size(), interpreterContext); } catch (const std::exception& error) { printError (std::string ("Error: ") + error.what()); } } } void Console::executeFile (const std::string& path) { namespace bfs = boost::filesystem; bfs::ifstream stream ((bfs::path(path))); if (!stream.is_open()) printError ("failed to open file: " + path); else { std::string line; while (std::getline (stream, line)) execute (line); } } void Console::clear() { resetReference(); } bool isWhitespace(char c) { return c == ' ' || c == '\t'; } void Console::keyPress(MyGUI::Widget* _sender, MyGUI::KeyCode key, MyGUI::Char _char) { if(MyGUI::InputManager::getInstance().isControlPressed()) { if(key == MyGUI::KeyCode::W) { const auto& caption = mCommandLine->getCaption(); if(caption.empty()) return; size_t max = mCommandLine->getTextCursor(); while(max > 0 && (isWhitespace(caption[max - 1]) || caption[max - 1] == '>')) max--; while(max > 0 && !isWhitespace(caption[max - 1]) && caption[max - 1] != '>') max--; size_t length = mCommandLine->getTextCursor() - max; if(length > 0) { auto text = caption; text.erase(max, length); mCommandLine->setCaption(text); mCommandLine->setTextCursor(max); } } else if(key == MyGUI::KeyCode::U) { if(mCommandLine->getTextCursor() > 0) { auto text = mCommandLine->getCaption(); text.erase(0, mCommandLine->getTextCursor()); mCommandLine->setCaption(text); mCommandLine->setTextCursor(0); } } } else if(key == MyGUI::KeyCode::Tab) { std::vector matches; listNames(); std::string oldCaption = mCommandLine->getCaption(); std::string newCaption = complete( mCommandLine->getOnlyText(), matches ); mCommandLine->setCaption(newCaption); // List candidates if repeatedly pressing tab if (oldCaption == newCaption && !matches.empty()) { int i = 0; printOK(""); for(std::string& match : matches) { if(i == 50) break; printOK(match); i++; } } } if(mCommandHistory.empty()) return; // Traverse history with up and down arrows if(key == MyGUI::KeyCode::ArrowUp) { // If the user was editing a string, store it for later if(mCurrent == mCommandHistory.end()) mEditString = mCommandLine->getOnlyText(); if(mCurrent != mCommandHistory.begin()) { --mCurrent; mCommandLine->setCaption(*mCurrent); } } else if(key == MyGUI::KeyCode::ArrowDown) { if(mCurrent != mCommandHistory.end()) { ++mCurrent; if(mCurrent != mCommandHistory.end()) mCommandLine->setCaption(*mCurrent); else // Restore the edit string mCommandLine->setCaption(mEditString); } } } void Console::acceptCommand(MyGUI::EditBox* _sender) { const std::string &cm = mCommandLine->getOnlyText(); if(cm.empty()) return; // Add the command to the history, and set the current pointer to // the end of the list if (mCommandHistory.empty() || mCommandHistory.back() != cm) mCommandHistory.push_back(cm); mCurrent = mCommandHistory.end(); mEditString.clear(); // Reset the command line before the command execution. // It prevents the re-triggering of the acceptCommand() event for the same command // during the actual command execution mCommandLine->setCaption(""); execute (cm); } std::string Console::complete( std::string input, std::vector &matches ) { std::string output = input; std::string tmp = input; bool has_front_quote = false; /* Does the input string contain things that don't have to be completed? If yes erase them. */ /* Erase a possible call to an explicit reference. */ size_t explicitPos = tmp.find("->"); if (explicitPos != std::string::npos) { tmp.erase(0, explicitPos+2); } /* Are there quotation marks? */ if( tmp.find('"') != std::string::npos ) { int numquotes=0; for(std::string::iterator it=tmp.begin(); it < tmp.end(); ++it) { if( *it == '"' ) numquotes++; } /* Is it terminated?*/ if( numquotes % 2 ) { tmp.erase( 0, tmp.rfind('"')+1 ); has_front_quote = true; } else { size_t pos; if( ( ((pos = tmp.rfind(' ')) != std::string::npos ) ) && ( pos > tmp.rfind('"') ) ) { tmp.erase( 0, tmp.rfind(' ')+1); } else { tmp.clear(); } has_front_quote = false; } } /* No quotation marks. Are there spaces?*/ else { size_t rpos; if( (rpos=tmp.rfind(' ')) != std::string::npos ) { if( rpos == 0 ) { tmp.clear(); } else { tmp.erase(0, rpos+1); } } } /* Erase the input from the output string so we can easily append the completed form later. */ output.erase(output.end()-tmp.length(), output.end()); /* Is there still something in the input string? If not just display all commands and return the unchanged input. */ if( tmp.length() == 0 ) { matches=mNames; return input; } /* Iterate through the vector. */ for(std::string& name : mNames) { bool string_different=false; /* Is the string shorter than the input string? If yes skip it. */ if(name.length() < tmp.length()) continue; /* Is the beginning of the string different from the input string? If yes skip it. */ for( std::string::iterator iter=tmp.begin(), iter2=name.begin(); iter < tmp.end();++iter, ++iter2) { if( Misc::StringUtils::toLower(*iter) != Misc::StringUtils::toLower(*iter2) ) { string_different=true; break; } } if( string_different ) continue; /* The beginning of the string matches the input string, save it for the next test. */ matches.push_back(name); } /* There are no matches. Return the unchanged input. */ if( matches.empty() ) { return input; } /* Only one match. We're done. */ if( matches.size() == 1 ) { /* Adding quotation marks when the input string started with a quotation mark or has spaces in it*/ if( ( matches.front().find(' ') != std::string::npos ) ) { if( !has_front_quote ) output.append(std::string("\"")); return output.append(matches.front() + std::string("\" ")); } else if( has_front_quote ) { return output.append(matches.front() + std::string("\" ")); } else { return output.append(matches.front() + std::string(" ")); } } /* Check if all matching strings match further than input. If yes complete to this match. */ int i = tmp.length(); for(std::string::iterator iter=matches.front().begin()+tmp.length(); iter < matches.front().end(); ++iter, ++i) { for(std::string& match : matches) { if(Misc::StringUtils::toLower(match[i]) != Misc::StringUtils::toLower(*iter)) { /* Append the longest match to the end of the output string*/ output.append(matches.front().substr(0, i)); return output; } } } /* All keywords match with the shortest. Append it to the output string and return it. */ return output.append(matches.front()); } void Console::onResChange(int width, int height) { setCoord(10,10, width-10, height/2); } void Console::updateSelectedObjectPtr(const MWWorld::Ptr& currentPtr, const MWWorld::Ptr& newPtr) { if (mPtr == currentPtr) mPtr = newPtr; } void Console::setSelectedObject(const MWWorld::Ptr& object) { if (!object.isEmpty()) { if (object == mPtr) { setTitle("#{sConsoleTitle}"); mPtr=MWWorld::Ptr(); } else { /* Start of tes3mp change (major) Display the selected object's refNum and mpNum alongside its refId in the title of the console window, for easier debugging of almost everything */ setTitle("#{sConsoleTitle} (" + object.getCellRef().getRefId() + ", " + std::to_string(object.getCellRef().getRefNum().mIndex) + "-" + std::to_string(object.getCellRef().getMpNum()) + ")"); /* End of tes3mp change (major) */ mPtr = object; } // User clicked on an object. Restore focus to the console command line. MWBase::Environment::get().getWindowManager()->setKeyFocusWidget(mCommandLine); } else { setTitle("#{sConsoleTitle}"); mPtr = MWWorld::Ptr(); } } /* Start of tes3mp addition Allow the direct setting of a console's Ptr, without the assumption that an object was clicked and that key focus should be restored to the console window, for console commands executed via server scripts */ void Console::setPtr(const MWWorld::Ptr& object) { mPtr = object; } /* End of tes3mp addition */ void Console::onReferenceUnavailable() { setSelectedObject(MWWorld::Ptr()); } void Console::resetReference() { ReferenceInterface::resetReference(); setSelectedObject(MWWorld::Ptr()); } } ================================================ FILE: apps/openmw/mwgui/console.hpp ================================================ #ifndef MWGUI_CONSOLE_H #define MWGUI_CONSOLE_H #include #include #include #include #include #include #include "../mwscript/compilercontext.hpp" #include "../mwscript/interpretercontext.hpp" #include "referenceinterface.hpp" #include "windowbase.hpp" namespace MWGui { class Console : public WindowBase, private Compiler::ErrorHandler, public ReferenceInterface { public: /// Set the implicit object for script execution void setSelectedObject(const MWWorld::Ptr& object); /* Start of tes3mp addition Allow the direct setting of a console's Ptr, without the assumption that an object was clicked and that key focus should be restored to the console window, for console commands executed via server scripts */ void setPtr(const MWWorld::Ptr& object); /* End of tes3mp addition */ MyGUI::EditBox* mCommandLine; MyGUI::EditBox* mHistory; typedef std::list StringList; // History of previous entered commands StringList mCommandHistory; StringList::iterator mCurrent; std::string mEditString; Console(int w, int h, bool consoleOnlyScripts); void onOpen() override; void onResChange(int width, int height) override; // Print a message to the console, in specified color. void print(const std::string &msg, const std::string& color = "#FFFFFF"); // These are pre-colored versions that you should use. /// Output from successful console command void printOK(const std::string &msg); /// Error message void printError(const std::string &msg); void execute (const std::string& command); void executeFile (const std::string& path); void updateSelectedObjectPtr(const MWWorld::Ptr& currentPtr, const MWWorld::Ptr& newPtr); void clear() override; void resetReference () override; protected: void onReferenceUnavailable() override; private: void keyPress(MyGUI::Widget* _sender, MyGUI::KeyCode key, MyGUI::Char _char); void acceptCommand(MyGUI::EditBox* _sender); std::string complete( std::string input, std::vector &matches ); Compiler::Extensions mExtensions; MWScript::CompilerContext mCompilerContext; std::vector mNames; bool mConsoleOnlyScripts; bool compile (const std::string& cmd, Compiler::Output& output); /// Report error to the user. void report (const std::string& message, const Compiler::TokenLoc& loc, Type type) override; /// Report a file related error void report (const std::string& message, Type type) override; /// Write all valid identifiers and keywords into mNames and sort them. /// \note If mNames is not empty, this function is a no-op. /// \note The list may contain duplicates (if a name is a keyword and an identifier at the same /// time). void listNames(); }; } #endif ================================================ FILE: apps/openmw/mwgui/container.cpp ================================================ #include "container.hpp" #include #include /* Start of tes3mp addition Include additional headers for multiplayer purposes */ #include #include "../mwmp/Main.hpp" #include "../mwmp/Networking.hpp" #include "../mwmp/LocalPlayer.hpp" #include "../mwmp/ObjectList.hpp" #include "../mwmp/CellController.hpp" /* End of tes3mp addition */ #include "../mwbase/environment.hpp" #include "../mwbase/world.hpp" #include "../mwbase/windowmanager.hpp" #include "../mwbase/mechanicsmanager.hpp" #include "../mwbase/scriptmanager.hpp" #include "../mwworld/class.hpp" #include "../mwworld/inventorystore.hpp" #include "../mwmechanics/aipackage.hpp" #include "../mwmechanics/creaturestats.hpp" #include "../mwmechanics/summoning.hpp" #include "../mwscript/interpretercontext.hpp" #include "countdialog.hpp" #include "inventorywindow.hpp" #include "itemview.hpp" #include "inventoryitemmodel.hpp" #include "containeritemmodel.hpp" #include "sortfilteritemmodel.hpp" #include "pickpocketitemmodel.hpp" #include "draganddrop.hpp" #include "tooltips.hpp" namespace MWGui { ContainerWindow::ContainerWindow(DragAndDrop* dragAndDrop) : WindowBase("openmw_container_window.layout") , mDragAndDrop(dragAndDrop) , mSortModel(nullptr) , mModel(nullptr) , mSelectedItem(-1) { getWidget(mDisposeCorpseButton, "DisposeCorpseButton"); getWidget(mTakeButton, "TakeButton"); getWidget(mCloseButton, "CloseButton"); getWidget(mItemView, "ItemView"); mItemView->eventBackgroundClicked += MyGUI::newDelegate(this, &ContainerWindow::onBackgroundSelected); mItemView->eventItemClicked += MyGUI::newDelegate(this, &ContainerWindow::onItemSelected); mDisposeCorpseButton->eventMouseButtonClick += MyGUI::newDelegate(this, &ContainerWindow::onDisposeCorpseButtonClicked); mCloseButton->eventMouseButtonClick += MyGUI::newDelegate(this, &ContainerWindow::onCloseButtonClicked); mTakeButton->eventMouseButtonClick += MyGUI::newDelegate(this, &ContainerWindow::onTakeAllButtonClicked); setCoord(200,0,600,300); } void ContainerWindow::onItemSelected(int index) { if (mDragAndDrop->mIsOnDragAndDrop) { dropItem(); return; } const ItemStack& item = mSortModel->getItem(index); // We can't take a conjured item from a container (some NPC we're pickpocketing, a box, etc) if (item.mFlags & ItemStack::Flag_Bound) { MWBase::Environment::get().getWindowManager()->messageBox("#{sContentsMessage1}"); return; } MWWorld::Ptr object = item.mBase; int count = item.mCount; bool shift = MyGUI::InputManager::getInstance().isShiftPressed(); if (MyGUI::InputManager::getInstance().isControlPressed()) count = 1; mSelectedItem = mSortModel->mapToSource(index); if (count > 1 && !shift) { CountDialog* dialog = MWBase::Environment::get().getWindowManager()->getCountDialog(); std::string name = object.getClass().getName(object) + MWGui::ToolTips::getSoulString(object.getCellRef()); dialog->openCountDialog(name, "#{sTake}", count); dialog->eventOkClicked.clear(); dialog->eventOkClicked += MyGUI::newDelegate(this, &ContainerWindow::dragItem); } else dragItem (nullptr, count); } void ContainerWindow::dragItem(MyGUI::Widget* sender, int count) { if (!mModel) return; if (!onTakeItem(mModel->getItem(mSelectedItem), count)) return; /* Start of tes3mp addition Send an ID_CONTAINER packet every time an item starts being dragged from a container */ mwmp::ObjectList *objectList = mwmp::Main::get().getNetworking()->getObjectList(); objectList->reset(); objectList->packetOrigin = mwmp::CLIENT_GAMEPLAY; objectList->cell = *mPtr.getCell()->getCell(); objectList->action = mwmp::BaseObjectList::REMOVE; objectList->containerSubAction = mwmp::BaseObjectList::DRAG; mwmp::BaseObject baseObject = objectList->getBaseObjectFromPtr(mPtr); MWWorld::Ptr itemPtr = mModel->getItem(mSelectedItem).mBase; objectList->addContainerItem(baseObject, itemPtr, itemPtr.getRefData().getCount(), count); objectList->addBaseObject(baseObject); objectList->sendContainer(); /* End of tes3mp addition */ /* Start of tes3mp change (major) Avoid running any of the original code for dragging items, to prevent possibilities for item duping or interaction with restricted containers */ return; /* End of tes3mp change (major) */ mDragAndDrop->startDrag(mSelectedItem, mSortModel, mModel, mItemView, count); } void ContainerWindow::dropItem() { if (!mModel) return; bool success = mModel->onDropItem(mDragAndDrop->mItem.mBase, mDragAndDrop->mDraggedCount); /* Start of tes3mp addition Send an ID_CONTAINER packet every time an item is dropped in a container */ if (success) { mwmp::ObjectList *objectList = mwmp::Main::get().getNetworking()->getObjectList(); objectList->reset(); objectList->packetOrigin = mwmp::CLIENT_GAMEPLAY; objectList->cell = *mPtr.getCell()->getCell(); objectList->action = mwmp::BaseObjectList::ADD; objectList->containerSubAction = mwmp::BaseObjectList::DROP; mwmp::BaseObject baseObject = objectList->getBaseObjectFromPtr(mPtr); MWWorld::Ptr itemPtr = mDragAndDrop->mItem.mBase; objectList->addContainerItem(baseObject, itemPtr, mDragAndDrop->mDraggedCount, 0); objectList->addBaseObject(baseObject); objectList->sendContainer(); } /* End of tes3mp addition */ /* Start of tes3mp change (major) For valid drops, avoid running the original code for the item transfer, to prevent unilateral item duping or interaction on this client Instead, finish the drag in a way that removes the items in it, and let the server's reply handle the rest */ if (success) // mDragAndDrop->drop(mModel, mItemView); mDragAndDrop->finish(true); /* End of tes3mp change (major) */ } void ContainerWindow::onBackgroundSelected() { if (mDragAndDrop->mIsOnDragAndDrop) dropItem(); } void ContainerWindow::setPtr(const MWWorld::Ptr& container) { /* Start of tes3mp addition Mark this container as open for multiplayer logic purposes */ mwmp::Main::get().getLocalPlayer()->storeCurrentContainer(container); /* End of tes3mp addition */ mPtr = container; bool loot = mPtr.getClass().isActor() && mPtr.getClass().getCreatureStats(mPtr).isDead(); if (mPtr.getClass().hasInventoryStore(mPtr)) { if (mPtr.getClass().isNpc() && !loot) { // we are stealing stuff mModel = new PickpocketItemModel(mPtr, new InventoryItemModel(container), !mPtr.getClass().getCreatureStats(mPtr).getKnockedDown()); } else mModel = new InventoryItemModel(container); } else { mModel = new ContainerItemModel(container); } mDisposeCorpseButton->setVisible(loot); mSortModel = new SortFilterItemModel(mModel); mItemView->setModel (mSortModel); mItemView->resetScrollBars(); MWBase::Environment::get().getWindowManager()->setKeyFocusWidget(mCloseButton); setTitle(container.getClass().getName(container)); } void ContainerWindow::resetReference() { ReferenceInterface::resetReference(); mItemView->setModel(nullptr); mModel = nullptr; mSortModel = nullptr; } void ContainerWindow::onClose() { /* Start of tes3mp addition Mark this container as closed for multiplayer logic purposes */ mwmp::Main::get().getLocalPlayer()->clearCurrentContainer(); /* End of tes3mp addition */ WindowBase::onClose(); // Make sure the window was actually closed and not temporarily hidden. if (MWBase::Environment::get().getWindowManager()->containsMode(GM_Container)) return; if (mModel) mModel->onClose(); if (!mPtr.isEmpty()) MWBase::Environment::get().getMechanicsManager()->onClose(mPtr); resetReference(); } void ContainerWindow::onCloseButtonClicked(MyGUI::Widget* _sender) { MWBase::Environment::get().getWindowManager()->removeGuiMode(GM_Container); } void ContainerWindow::onTakeAllButtonClicked(MyGUI::Widget* _sender) { if(mDragAndDrop != nullptr && mDragAndDrop->mIsOnDragAndDrop) return; MWBase::Environment::get().getWindowManager()->setKeyFocusWidget(mCloseButton); /* Start of tes3mp addition Send an ID_CONTAINER packet every time the Take All button is used on a container */ mwmp::ObjectList *objectList = mwmp::Main::get().getNetworking()->getObjectList(); objectList->reset(); objectList->packetOrigin = mwmp::CLIENT_GAMEPLAY; objectList->cell = *mPtr.getCell()->getCell(); objectList->action = mwmp::BaseObjectList::REMOVE; objectList->containerSubAction = mwmp::BaseObjectList::TAKE_ALL; mwmp::BaseObject baseObject = objectList->getBaseObjectFromPtr(mPtr); for (size_t i = 0; i < mModel->getItemCount(); ++i) { const ItemStack& item = mModel->getItem(i); // Trigger crimes related to the attempted taking of these items, if applicable if (!onTakeItem(item, item.mCount)) break; objectList->addContainerItem(baseObject, item, item.mCount, item.mCount); } if (baseObject.containerItems.size() > 0) { objectList->addBaseObject(baseObject); objectList->sendContainer(); } /* End of tes3mp addition */ /* Start of tes3mp change (major) Avoid running any of the original code for taking all items, to prevent possibilities for item duping or interaction with restricted containers */ return; /* End of tes3mp change (major) */ // transfer everything into the player's inventory ItemModel* playerModel = MWBase::Environment::get().getWindowManager()->getInventoryWindow()->getModel(); assert(mModel); mModel->update(); // unequip all items to avoid unequipping/reequipping if (mPtr.getClass().hasInventoryStore(mPtr)) { MWWorld::InventoryStore& invStore = mPtr.getClass().getInventoryStore(mPtr); for (size_t i=0; igetItemCount(); ++i) { const ItemStack& item = mModel->getItem(i); if (invStore.isEquipped(item.mBase) == false) continue; invStore.unequipItem(item.mBase, mPtr); } } mModel->update(); for (size_t i=0; igetItemCount(); ++i) { if (i==0) { // play the sound of the first object MWWorld::Ptr item = mModel->getItem(i).mBase; std::string sound = item.getClass().getUpSoundId(item); MWBase::Environment::get().getWindowManager()->playSound(sound); } const ItemStack& item = mModel->getItem(i); if (!onTakeItem(item, item.mCount)) break; mModel->moveItem(item, item.mCount, playerModel); } MWBase::Environment::get().getWindowManager()->removeGuiMode(GM_Container); } void ContainerWindow::onDisposeCorpseButtonClicked(MyGUI::Widget *sender) { if(mDragAndDrop == nullptr || !mDragAndDrop->mIsOnDragAndDrop) { MWBase::Environment::get().getWindowManager()->setKeyFocusWidget(mCloseButton); // Copy mPtr because onTakeAllButtonClicked closes the window which resets the reference MWWorld::Ptr ptr = mPtr; onTakeAllButtonClicked(mTakeButton); if (ptr.getClass().isPersistent(ptr)) MWBase::Environment::get().getWindowManager()->messageBox("#{sDisposeCorpseFail}"); else { /* Start of tes3mp change (major) Instead of deleting the corpse on this client, increasing the death count and running the dead actor's script, simply send an ID_OBJECT_DELETE packet to the server as a request for the deletion */ /* MWMechanics::CreatureStats& creatureStats = ptr.getClass().getCreatureStats(ptr); // If we dispose corpse before end of death animation, we should update death counter counter manually. // Also we should run actor's script - it may react on actor's death. if (creatureStats.isDead() && !creatureStats.isDeathAnimationFinished()) { creatureStats.setDeathAnimationFinished(true); MWBase::Environment::get().getMechanicsManager()->notifyDied(ptr); const std::string script = ptr.getClass().getScript(ptr); if (!script.empty() && MWBase::Environment::get().getWorld()->getScriptsEnabled()) { MWScript::InterpreterContext interpreterContext (&ptr.getRefData().getLocals(), ptr); MWBase::Environment::get().getScriptManager()->run (script, interpreterContext); } // Clean up summoned creatures as well std::map& creatureMap = creatureStats.getSummonedCreatureMap(); for (const auto& creature : creatureMap) MWBase::Environment::get().getMechanicsManager()->cleanupSummonedCreature(ptr, creature.second); creatureMap.clear(); // Check if we are a summon and inform our master we've bit the dust for(const auto& package : creatureStats.getAiSequence()) { if(package->followTargetThroughDoors() && !package->getTarget().isEmpty()) { const auto& summoner = package->getTarget(); auto& summons = summoner.getClass().getCreatureStats(summoner).getSummonedCreatureMap(); auto it = std::find_if(summons.begin(), summons.end(), [&] (const auto& entry) { return entry.second == creatureStats.getActorId(); }); if(it != summons.end()) { MWMechanics::purgeSummonEffect(summoner, *it); summons.erase(it); break; } } } } MWBase::Environment::get().getWorld()->deleteObject(ptr); */ mwmp::ObjectList *objectList = mwmp::Main::get().getNetworking()->getObjectList(); objectList->reset(); objectList->packetOrigin = mwmp::CLIENT_GAMEPLAY; objectList->addObjectGeneric(ptr); objectList->sendObjectDelete(); /* End of tes3mp change (major) */ } } } void ContainerWindow::onReferenceUnavailable() { MWBase::Environment::get().getWindowManager()->removeGuiMode(GM_Container); } bool ContainerWindow::onTakeItem(const ItemStack &item, int count) { return mModel->onTakeItem(item.mBase, count); } /* Start of tes3mp addition Make it possible to check from elsewhere whether there is currently an item being dragged in the container window */ bool ContainerWindow::isOnDragAndDrop() { return mDragAndDrop->mIsOnDragAndDrop; } /* End of tes3mp addition */ /* Start of tes3mp addition Make it possible to drag a specific item Ptr instead of having to rely on an index that may have changed in the meantime, for drags that require approval from the server */ bool ContainerWindow::dragItemByPtr(const MWWorld::Ptr& itemPtr, int dragCount) { ItemModel::ModelIndex newIndex = -1; for (unsigned int i = 0; i < mModel->getItemCount(); ++i) { if (mModel->getItem(i).mBase == itemPtr) { newIndex = i; break; } } if (newIndex != -1) { mDragAndDrop->startDrag(newIndex, mSortModel, mModel, mItemView, dragCount); return true; } return false; } /* End of tes3mp addition */ } ================================================ FILE: apps/openmw/mwgui/container.hpp ================================================ #ifndef MGUI_CONTAINER_H #define MGUI_CONTAINER_H #include "windowbase.hpp" #include "referenceinterface.hpp" #include "itemmodel.hpp" namespace MyGUI { class Gui; class Widget; } namespace MWGui { class ContainerWindow; class ItemView; class SortFilterItemModel; } namespace MWGui { class ContainerWindow : public WindowBase, public ReferenceInterface { public: ContainerWindow(DragAndDrop* dragAndDrop); void setPtr(const MWWorld::Ptr& container) override; void onClose() override; void clear() override { resetReference(); } void onFrame(float dt) override { checkReferenceAvailable(); } void resetReference() override; /* Start of tes3mp addition Make it possible to check from elsewhere whether there is currently an item being dragged in the container window */ bool isOnDragAndDrop(); /* End of tes3mp addition */ /* Start of tes3mp addition Make it possible to drag a specific item Ptr instead of having to rely on an index that may have changed in the meantime, for drags that require approval from the server */ bool dragItemByPtr(const MWWorld::Ptr& itemPtr, int dragCount); /* End of tes3mp addition */ private: DragAndDrop* mDragAndDrop; MWGui::ItemView* mItemView; SortFilterItemModel* mSortModel; ItemModel* mModel; int mSelectedItem; MyGUI::Button* mDisposeCorpseButton; MyGUI::Button* mTakeButton; MyGUI::Button* mCloseButton; void onItemSelected(int index); void onBackgroundSelected(); void dragItem(MyGUI::Widget* sender, int count); void dropItem(); void onCloseButtonClicked(MyGUI::Widget* _sender); void onTakeAllButtonClicked(MyGUI::Widget* _sender); void onDisposeCorpseButtonClicked(MyGUI::Widget* sender); /// @return is taking the item allowed? bool onTakeItem(const ItemStack& item, int count); void onReferenceUnavailable() override; }; } #endif // CONTAINER_H ================================================ FILE: apps/openmw/mwgui/containeritemmodel.cpp ================================================ #include "containeritemmodel.hpp" #include /* Start of tes3mp addition Include additional headers for multiplayer purposes */ #include "../mwmp/Main.hpp" #include "../mwmp/Networking.hpp" #include "../mwmp/LocalPlayer.hpp" #include "../mwmp/ObjectList.hpp" /* End of tes3mp addition */ #include "../mwmechanics/creaturestats.hpp" #include "../mwmechanics/actorutil.hpp" #include "../mwworld/containerstore.hpp" #include "../mwworld/class.hpp" #include "../mwbase/environment.hpp" #include "../mwbase/mechanicsmanager.hpp" #include "../mwbase/windowmanager.hpp" #include "../mwbase/world.hpp" namespace { bool stacks (const MWWorld::Ptr& left, const MWWorld::Ptr& right) { if (left == right) return true; // If one of the items is in an inventory and currently equipped, we need to check stacking both ways to be sure if (left.getContainerStore() && right.getContainerStore()) return left.getContainerStore()->stacks(left, right) && right.getContainerStore()->stacks(left, right); if (left.getContainerStore()) return left.getContainerStore()->stacks(left, right); if (right.getContainerStore()) return right.getContainerStore()->stacks(left, right); MWWorld::ContainerStore store; return store.stacks(left, right); } } namespace MWGui { ContainerItemModel::ContainerItemModel(const std::vector& itemSources, const std::vector& worldItems) : mWorldItems(worldItems) , mTrading(true) { assert (!itemSources.empty()); // Tie resolution lifetimes to the ItemModel mItemSources.reserve(itemSources.size()); for(const MWWorld::Ptr& source : itemSources) { MWWorld::ContainerStore& store = source.getClass().getContainerStore(source); mItemSources.emplace_back(source, store.resolveTemporarily()); } } ContainerItemModel::ContainerItemModel (const MWWorld::Ptr& source) : mTrading(false) { MWWorld::ContainerStore& store = source.getClass().getContainerStore(source); mItemSources.emplace_back(source, store.resolveTemporarily()); } bool ContainerItemModel::allowedToUseItems() const { if (mItemSources.empty()) return true; MWWorld::Ptr ptr = MWMechanics::getPlayer(); MWWorld::Ptr victim; // Check if the player is allowed to use items from opened container MWBase::MechanicsManager* mm = MWBase::Environment::get().getMechanicsManager(); return mm->isAllowedToUse(ptr, mItemSources[0].first, victim); } ItemStack ContainerItemModel::getItem (ModelIndex index) { if (index < 0) throw std::runtime_error("Invalid index supplied"); if (mItems.size() <= static_cast(index)) throw std::runtime_error("Item index out of range"); return mItems[index]; } size_t ContainerItemModel::getItemCount() { return mItems.size(); } ItemModel::ModelIndex ContainerItemModel::getIndex (ItemStack item) { size_t i = 0; for (ItemStack& itemStack : mItems) { if (itemStack == item) return i; ++i; } return -1; } MWWorld::Ptr ContainerItemModel::copyItem (const ItemStack& item, size_t count, bool allowAutoEquip) { auto& source = mItemSources[0]; MWWorld::ContainerStore& store = source.first.getClass().getContainerStore(source.first); if (item.mBase.getContainerStore() == &store) throw std::runtime_error("Item to copy needs to be from a different container!"); /* Start of tes3mp addition Send an ID_CONTAINER packet every time an item is added to a container here */ mwmp::ObjectList *objectList = mwmp::Main::get().getNetworking()->getObjectList(); objectList->reset(); objectList->packetOrigin = mwmp::PACKET_ORIGIN::CLIENT_GAMEPLAY; objectList->cell = *source.first.getCell()->getCell(); objectList->action = mwmp::BaseObjectList::ADD; objectList->containerSubAction = mwmp::BaseObjectList::NONE; mwmp::BaseObject baseObject = objectList->getBaseObjectFromPtr(source.first); objectList->addContainerItem(baseObject, item.mBase, count, 0); objectList->addBaseObject(baseObject); objectList->sendContainer(); /* End of tes3mp addition */ /* Start of tes3mp change (major) Instead of unilaterally adding the item to this source's ContainerStore on this client and returning the resulting Ptr, rely on the server to handle the item transfer and just return the original item Ptr as a placeholder return value */ return item.mBase; /* End of tes3mp change (major) */ } void ContainerItemModel::removeItem (const ItemStack& item, size_t count) { int toRemove = count; for (auto& source : mItemSources) { MWWorld::ContainerStore& store = source.first.getClass().getContainerStore(source.first); for (MWWorld::ContainerStoreIterator it = store.begin(); it != store.end(); ++it) { if (stacks(*it, item.mBase)) { /* Start of tes3mp change (major) Send an ID_CONTAINER packet every time an item is removed here and prevent any unilateral item removal on this client, as long as this isn't the player's currently open container and doesn't require the drag and drop logic dealt with in MWGui::ContainerWindow instead */ mwmp::CurrentContainer *currentContainer = &mwmp::Main::get().getLocalPlayer()->currentContainer; if (currentContainer->refNum != source.first.getCellRef().getRefNum().mIndex || currentContainer->mpNum != source.first.getCellRef().getMpNum()) { mwmp::ObjectList *objectList = mwmp::Main::get().getNetworking()->getObjectList(); objectList->reset(); objectList->packetOrigin = mwmp::PACKET_ORIGIN::CLIENT_GAMEPLAY; objectList->cell = *source.first.getCell()->getCell(); objectList->action = mwmp::BaseObjectList::REMOVE; objectList->containerSubAction = mwmp::BaseObjectList::NONE; mwmp::BaseObject baseObject = objectList->getBaseObjectFromPtr(source.first); objectList->addContainerItem(baseObject, *it, it->getRefData().getCount(), toRemove); objectList->addBaseObject(baseObject); objectList->sendContainer(); toRemove -= it->getRefData().getCount(); } else { int quantity = it->mRef->mData.getCount(false); // If this is a restocking quantity, just don't remove it if (quantity < 0 && mTrading) toRemove += quantity; else toRemove -= store.remove(*it, toRemove, source.first); } /* End of tes3mp change (major) */ if (toRemove <= 0) return; } } } for (MWWorld::Ptr& source : mWorldItems) { if (stacks(source, item.mBase)) { int refCount = source.getRefData().getCount(); if (refCount - toRemove <= 0) { /* Start of tes3mp addition Send an ID_OBJECT_DELETE packet every time an item is removed from the world because it has been purchased from its owner */ mwmp::ObjectList *objectList = mwmp::Main::get().getNetworking()->getObjectList(); objectList->reset(); objectList->packetOrigin = mwmp::CLIENT_GAMEPLAY; objectList->addObjectGeneric(source); objectList->sendObjectDelete(); /* End of tes3mp addition */ MWBase::Environment::get().getWorld()->deleteObject(source); } else source.getRefData().setCount(std::max(0, refCount - toRemove)); toRemove -= refCount; if (toRemove <= 0) return; } } throw std::runtime_error("Not enough items to remove could be found"); } void ContainerItemModel::update() { mItems.clear(); for (auto& source : mItemSources) { MWWorld::ContainerStore& store = source.first.getClass().getContainerStore(source.first); for (MWWorld::ContainerStoreIterator it = store.begin(); it != store.end(); ++it) { if (!(*it).getClass().showsInInventory(*it)) continue; bool found = false; for (ItemStack& itemStack : mItems) { if (stacks(*it, itemStack.mBase)) { // we already have an item stack of this kind, add to it itemStack.mCount += it->getRefData().getCount(); found = true; break; } } if (!found) { // no stack yet, create one ItemStack newItem (*it, this, it->getRefData().getCount()); mItems.push_back(newItem); } } } for (MWWorld::Ptr& source : mWorldItems) { bool found = false; for (ItemStack& itemStack : mItems) { if (stacks(source, itemStack.mBase)) { // we already have an item stack of this kind, add to it itemStack.mCount += source.getRefData().getCount(); found = true; break; } } if (!found) { // no stack yet, create one ItemStack newItem (source, this, source.getRefData().getCount()); mItems.push_back(newItem); } } } bool ContainerItemModel::onDropItem(const MWWorld::Ptr &item, int count) { if (mItemSources.empty()) return false; MWWorld::Ptr target = mItemSources[0].first; if (target.getTypeName() != typeid(ESM::Container).name()) return true; // check container organic flag MWWorld::LiveCellRef* ref = target.get(); if (ref->mBase->mFlags & ESM::Container::Organic) { MWBase::Environment::get().getWindowManager()-> messageBox("#{sContentsMessage2}"); return false; } // check that we don't exceed container capacity float weight = item.getClass().getWeight(item) * count; if (target.getClass().getCapacity(target) < target.getClass().getEncumbrance(target) + weight) { MWBase::Environment::get().getWindowManager()->messageBox("#{sContentsMessage3}"); return false; } return true; } bool ContainerItemModel::onTakeItem(const MWWorld::Ptr &item, int count) { if (mItemSources.empty()) return false; MWWorld::Ptr target = mItemSources[0].first; // Looting a dead corpse is considered OK if (target.getClass().isActor() && target.getClass().getCreatureStats(target).isDead()) return true; MWWorld::Ptr player = MWMechanics::getPlayer(); MWBase::Environment::get().getMechanicsManager()->itemTaken(player, item, target, count); return true; } } ================================================ FILE: apps/openmw/mwgui/containeritemmodel.hpp ================================================ #ifndef MWGUI_CONTAINER_ITEM_MODEL_H #define MWGUI_CONTAINER_ITEM_MODEL_H #include #include #include "itemmodel.hpp" #include "../mwworld/containerstore.hpp" namespace MWGui { /// @brief The container item model supports multiple item sources, which are needed for /// making NPCs sell items from containers owned by them class ContainerItemModel : public ItemModel { public: ContainerItemModel (const std::vector& itemSources, const std::vector& worldItems); ///< @note The order of elements \a itemSources matters here. The first element has the highest priority for removal, /// while the last element will be used to add new items to. ContainerItemModel (const MWWorld::Ptr& source); bool allowedToUseItems() const override; bool onDropItem(const MWWorld::Ptr &item, int count) override; bool onTakeItem(const MWWorld::Ptr &item, int count) override; ItemStack getItem (ModelIndex index) override; ModelIndex getIndex (ItemStack item) override; size_t getItemCount() override; MWWorld::Ptr copyItem (const ItemStack& item, size_t count, bool allowAutoEquip = true) override; void removeItem (const ItemStack& item, size_t count) override; void update() override; private: std::vector> mItemSources; std::vector mWorldItems; const bool mTrading; std::vector mItems; }; } #endif ================================================ FILE: apps/openmw/mwgui/controllers.cpp ================================================ #include "controllers.hpp" #include #include namespace MWGui { namespace Controllers { void ControllerFollowMouse::prepareItem(MyGUI::Widget *_widget) { } bool ControllerFollowMouse::addTime(MyGUI::Widget *_widget, float _time) { _widget->setPosition(MyGUI::InputManager::getInstance().getMousePosition()); return true; } } } ================================================ FILE: apps/openmw/mwgui/controllers.hpp ================================================ #ifndef MWGUI_CONTROLLERS_H #define MWGUI_CONTROLLERS_H #include #include namespace MyGUI { class Widget; } namespace MWGui::Controllers { /// Automatically positions a widget below the mouse cursor. class ControllerFollowMouse final : public MyGUI::ControllerItem { MYGUI_RTTI_DERIVED( ControllerFollowMouse ) private: bool addTime(MyGUI::Widget* _widget, float _time) override; void prepareItem(MyGUI::Widget* _widget) override; }; } #endif ================================================ FILE: apps/openmw/mwgui/countdialog.cpp ================================================ #include "countdialog.hpp" #include #include #include #include #include "../mwbase/environment.hpp" #include "../mwbase/windowmanager.hpp" namespace MWGui { CountDialog::CountDialog() : WindowModal("openmw_count_window.layout") { getWidget(mSlider, "CountSlider"); getWidget(mItemEdit, "ItemEdit"); getWidget(mItemText, "ItemText"); getWidget(mLabelText, "LabelText"); getWidget(mOkButton, "OkButton"); getWidget(mCancelButton, "CancelButton"); mCancelButton->eventMouseButtonClick += MyGUI::newDelegate(this, &CountDialog::onCancelButtonClicked); mOkButton->eventMouseButtonClick += MyGUI::newDelegate(this, &CountDialog::onOkButtonClicked); mItemEdit->eventValueChanged += MyGUI::newDelegate(this, &CountDialog::onEditValueChanged); mSlider->eventScrollChangePosition += MyGUI::newDelegate(this, &CountDialog::onSliderMoved); // make sure we read the enter key being pressed to accept multiple items mItemEdit->eventEditSelectAccept += MyGUI::newDelegate(this, &CountDialog::onEnterKeyPressed); } void CountDialog::openCountDialog(const std::string& item, const std::string& message, const int maxCount) { setVisible(true); mLabelText->setCaptionWithReplacing(message); MyGUI::IntSize viewSize = MyGUI::RenderManager::getInstance().getViewSize(); mSlider->setScrollRange(maxCount); mItemText->setCaption(item); int width = std::max(mItemText->getTextSize().width + 128, 320); setCoord(viewSize.width/2 - width/2, viewSize.height/2 - mMainWidget->getHeight()/2, width, mMainWidget->getHeight()); // by default, the text edit field has the focus of the keyboard MWBase::Environment::get().getWindowManager()->setKeyFocusWidget(mItemEdit); mSlider->setScrollPosition(maxCount-1); mItemEdit->setMinValue(1); mItemEdit->setMaxValue(maxCount); mItemEdit->setValue(maxCount); } void CountDialog::onCancelButtonClicked(MyGUI::Widget* _sender) { setVisible(false); } void CountDialog::onOkButtonClicked(MyGUI::Widget* _sender) { eventOkClicked(nullptr, mSlider->getScrollPosition()+1); setVisible(false); } // essentially duplicating what the OK button does if user presses // Enter key void CountDialog::onEnterKeyPressed(MyGUI::EditBox* _sender) { eventOkClicked(nullptr, mSlider->getScrollPosition()+1); setVisible(false); // To do not spam onEnterKeyPressed() again and again MWBase::Environment::get().getWindowManager()->injectKeyRelease(MyGUI::KeyCode::None); } void CountDialog::onEditValueChanged(int value) { mSlider->setScrollPosition(value-1); } void CountDialog::onSliderMoved(MyGUI::ScrollBar* _sender, size_t _position) { mItemEdit->setValue(_position+1); } } ================================================ FILE: apps/openmw/mwgui/countdialog.hpp ================================================ #ifndef MWGUI_COUNTDIALOG_H #define MWGUI_COUNTDIALOG_H #include "windowbase.hpp" namespace Gui { class NumericEditBox; } namespace MWGui { class CountDialog : public WindowModal { public: CountDialog(); void openCountDialog(const std::string& item, const std::string& message, const int maxCount); typedef MyGUI::delegates::CMultiDelegate2 EventHandle_WidgetInt; /** Event : Ok button was clicked.\n signature : void method(MyGUI::Widget* _sender, int _count)\n */ EventHandle_WidgetInt eventOkClicked; private: MyGUI::ScrollBar* mSlider; Gui::NumericEditBox* mItemEdit; MyGUI::TextBox* mItemText; MyGUI::TextBox* mLabelText; MyGUI::Button* mOkButton; MyGUI::Button* mCancelButton; void onCancelButtonClicked(MyGUI::Widget* _sender); void onOkButtonClicked(MyGUI::Widget* _sender); void onEditValueChanged(int value); void onSliderMoved(MyGUI::ScrollBar* _sender, size_t _position); void onEnterKeyPressed(MyGUI::EditBox* _sender); }; } #endif ================================================ FILE: apps/openmw/mwgui/cursor.cpp ================================================ #include "cursor.hpp" #include #include #include #include namespace MWGui { ResourceImageSetPointerFix::ResourceImageSetPointerFix() : mImageSet(nullptr) , mRotation(0) { } ResourceImageSetPointerFix::~ResourceImageSetPointerFix() { } void ResourceImageSetPointerFix::deserialization(MyGUI::xml::ElementPtr _node, MyGUI::Version _version) { Base::deserialization(_node, _version); MyGUI::xml::ElementEnumerator info = _node->getElementEnumerator(); while (info.next("Property")) { const std::string& key = info->findAttribute("key"); const std::string& value = info->findAttribute("value"); if (key == "Point") mPoint = MyGUI::IntPoint::parse(value); else if (key == "Size") mSize = MyGUI::IntSize::parse(value); else if (key == "Rotation") mRotation = MyGUI::utility::parseInt(value); else if (key == "Resource") mImageSet = MyGUI::ResourceManager::getInstance().getByName(value)->castType(); } } int ResourceImageSetPointerFix::getRotation() { return mRotation; } void ResourceImageSetPointerFix::setImage(MyGUI::ImageBox* _image) { if (mImageSet != nullptr) _image->setItemResourceInfo(mImageSet->getIndexInfo(0, 0)); } void ResourceImageSetPointerFix::setPosition(MyGUI::ImageBox* _image, const MyGUI::IntPoint& _point) { _image->setCoord(_point.left - mPoint.left, _point.top - mPoint.top, mSize.width, mSize.height); } MyGUI::ResourceImageSetPtr ResourceImageSetPointerFix:: getImageSet() { return mImageSet; } MyGUI::IntPoint ResourceImageSetPointerFix::getHotSpot() { return mPoint; } MyGUI::IntSize ResourceImageSetPointerFix::getSize() { return mSize; } } ================================================ FILE: apps/openmw/mwgui/cursor.hpp ================================================ #ifndef MWGUI_CURSOR_H #define MWGUI_CURSOR_H #include #include namespace MWGui { /// \brief Allows us to get the members of /// ResourceImageSetPointer that we need. /// \example MyGUI::FactoryManager::getInstance().registerFactory("Resource", "ResourceImageSetPointer"); /// MyGUI::ResourceManager::getInstance().load("core.xml"); class ResourceImageSetPointerFix final : public MyGUI::IPointer { MYGUI_RTTI_DERIVED( ResourceImageSetPointerFix ) public: ResourceImageSetPointerFix(); virtual ~ResourceImageSetPointerFix(); void deserialization(MyGUI::xml::ElementPtr _node, MyGUI::Version _version) override; void setImage(MyGUI::ImageBox* _image) override; void setPosition(MyGUI::ImageBox* _image, const MyGUI::IntPoint& _point) override; //and now for the whole point of this class, allow us to get //the hot spot, the image and the size of the cursor. MyGUI::ResourceImageSetPtr getImageSet(); MyGUI::IntPoint getHotSpot(); MyGUI::IntSize getSize(); int getRotation(); private: MyGUI::IntPoint mPoint; MyGUI::IntSize mSize; MyGUI::ResourceImageSetPtr mImageSet; int mRotation; // rotation in degrees }; } #endif ================================================ FILE: apps/openmw/mwgui/debugwindow.cpp ================================================ #include "debugwindow.hpp" #include #include #include #include #include #ifndef BT_NO_PROFILE namespace { void bulletDumpRecursive(CProfileIterator* pit, int spacing, std::stringstream& os) { pit->First(); if (pit->Is_Done()) return; float accumulated_time=0,parent_time = pit->Is_Root() ? CProfileManager::Get_Time_Since_Reset() : pit->Get_Current_Parent_Total_Time(); int i,j; int frames_since_reset = CProfileManager::Get_Frame_Count_Since_Reset(); for (i=0;iGet_Current_Parent_Name())+" (total running time: "+MyGUI::utility::toString(parent_time,3)+" ms) ---\n"; os << s; //float totalTime = 0.f; int numChildren = 0; for (i = 0; !pit->Is_Done(); i++,pit->Next()) { numChildren++; float current_total_time = pit->Get_Current_Total_Time(); accumulated_time += current_total_time; float fraction = parent_time > SIMD_EPSILON ? (current_total_time / parent_time) * 100 : 0.f; for (j=0;jGet_Current_Name()+" ("+MyGUI::utility::toString(fraction,2)+" %) :: "+MyGUI::utility::toString(ms,3)+" ms / frame ("+MyGUI::utility::toString(pit->Get_Current_Total_Calls())+" calls)\n"; os << s; //totalTime += current_total_time; //recurse into children } if (parent_time < accumulated_time) { os << "what's wrong\n"; } for (i=0;i SIMD_EPSILON ? ((parent_time - accumulated_time) / parent_time) * 100 : 0.f; s = "Unaccounted: ("+MyGUI::utility::toString(unaccounted,3)+" %) :: "+MyGUI::utility::toString(parent_time - accumulated_time,3)+" ms\n"; os << s; for (i=0;iEnter_Child(i); bulletDumpRecursive(pit, spacing+3, os); pit->Enter_Parent(); } } void bulletDumpAll(std::stringstream& os) { CProfileIterator* profileIterator = 0; profileIterator = CProfileManager::Get_Iterator(); bulletDumpRecursive(profileIterator, 0, os); CProfileManager::Release_Iterator(profileIterator); } } #endif // BT_NO_PROFILE namespace MWGui { DebugWindow::DebugWindow() : WindowBase("openmw_debug_window.layout") { getWidget(mTabControl, "TabControl"); // Ideas for other tabs: // - Texture / compositor texture viewer // - Log viewer // - Material editor // - Shader editor MyGUI::TabItem* item = mTabControl->addItem("Physics Profiler"); mBulletProfilerEdit = item->createWidgetReal ("LogEdit", MyGUI::FloatCoord(0,0,1,1), MyGUI::Align::Stretch); MyGUI::IntSize viewSize = MyGUI::RenderManager::getInstance().getViewSize(); mMainWidget->setSize(viewSize); } void DebugWindow::onFrame(float dt) { #ifndef BT_NO_PROFILE if (!isVisible()) return; static float timer = 0; timer -= dt; if (timer > 0) return; timer = 1; std::stringstream stream; bulletDumpAll(stream); if (mBulletProfilerEdit->isTextSelection()) // pause updating while user is trying to copy text return; size_t previousPos = mBulletProfilerEdit->getVScrollPosition(); mBulletProfilerEdit->setCaption(stream.str()); mBulletProfilerEdit->setVScrollPosition(std::min(previousPos, mBulletProfilerEdit->getVScrollRange()-1)); #endif } } ================================================ FILE: apps/openmw/mwgui/debugwindow.hpp ================================================ #ifndef OPENMW_MWGUI_DEBUGWINDOW_H #define OPENMW_MWGUI_DEBUGWINDOW_H #include "windowbase.hpp" namespace MWGui { class DebugWindow : public WindowBase { public: DebugWindow(); void onFrame(float dt) override; private: MyGUI::TabControl* mTabControl; MyGUI::EditBox* mBulletProfilerEdit; }; } #endif ================================================ FILE: apps/openmw/mwgui/dialogue.cpp ================================================ #include "dialogue.hpp" #include #include #include #include #include #include #include #include /* Start of tes3mp addition Include additional headers for multiplayer purposes */ #include "../mwmp/Main.hpp" #include "../mwmp/Networking.hpp" #include "../mwmp/ObjectList.hpp" #include /* End of tes3mp addition */ #include "../mwbase/environment.hpp" #include "../mwbase/windowmanager.hpp" #include "../mwbase/mechanicsmanager.hpp" #include "../mwbase/world.hpp" #include "../mwbase/dialoguemanager.hpp" #include "../mwworld/class.hpp" #include "../mwworld/containerstore.hpp" #include "../mwworld/esmstore.hpp" #include "../mwmechanics/creaturestats.hpp" #include "../mwmechanics/actorutil.hpp" #include "bookpage.hpp" #include "textcolours.hpp" #include "journalbooks.hpp" // to_utf8_span namespace MWGui { class ResponseCallback : public MWBase::DialogueManager::ResponseCallback { public: ResponseCallback(DialogueWindow* win, bool needMargin=true) : mWindow(win) , mNeedMargin(needMargin) { } void addResponse(const std::string& title, const std::string& text) override { mWindow->addResponse(title, text, mNeedMargin); } void updateTopics() { mWindow->updateTopics(); } private: DialogueWindow* mWindow; bool mNeedMargin; }; PersuasionDialog::PersuasionDialog(ResponseCallback* callback) : WindowModal("openmw_persuasion_dialog.layout") , mCallback(callback) { getWidget(mCancelButton, "CancelButton"); getWidget(mAdmireButton, "AdmireButton"); getWidget(mIntimidateButton, "IntimidateButton"); getWidget(mTauntButton, "TauntButton"); getWidget(mBribe10Button, "Bribe10Button"); getWidget(mBribe100Button, "Bribe100Button"); getWidget(mBribe1000Button, "Bribe1000Button"); getWidget(mGoldLabel, "GoldLabel"); mCancelButton->eventMouseButtonClick += MyGUI::newDelegate(this, &PersuasionDialog::onCancel); mAdmireButton->eventMouseButtonClick += MyGUI::newDelegate(this, &PersuasionDialog::onPersuade); mIntimidateButton->eventMouseButtonClick += MyGUI::newDelegate(this, &PersuasionDialog::onPersuade); mTauntButton->eventMouseButtonClick += MyGUI::newDelegate(this, &PersuasionDialog::onPersuade); mBribe10Button->eventMouseButtonClick += MyGUI::newDelegate(this, &PersuasionDialog::onPersuade); mBribe100Button->eventMouseButtonClick += MyGUI::newDelegate(this, &PersuasionDialog::onPersuade); mBribe1000Button->eventMouseButtonClick += MyGUI::newDelegate(this, &PersuasionDialog::onPersuade); } void PersuasionDialog::onCancel(MyGUI::Widget *sender) { setVisible(false); } void PersuasionDialog::onPersuade(MyGUI::Widget *sender) { MWBase::MechanicsManager::PersuasionType type; if (sender == mAdmireButton) type = MWBase::MechanicsManager::PT_Admire; else if (sender == mIntimidateButton) type = MWBase::MechanicsManager::PT_Intimidate; else if (sender == mTauntButton) type = MWBase::MechanicsManager::PT_Taunt; else if (sender == mBribe10Button) type = MWBase::MechanicsManager::PT_Bribe10; else if (sender == mBribe100Button) type = MWBase::MechanicsManager::PT_Bribe100; else /*if (sender == mBribe1000Button)*/ type = MWBase::MechanicsManager::PT_Bribe1000; MWBase::Environment::get().getDialogueManager()->persuade(type, mCallback.get()); mCallback->updateTopics(); setVisible(false); } void PersuasionDialog::onOpen() { center(); MWWorld::Ptr player = MWMechanics::getPlayer(); int playerGold = player.getClass().getContainerStore(player).count(MWWorld::ContainerStore::sGoldId); mBribe10Button->setEnabled (playerGold >= 10); mBribe100Button->setEnabled (playerGold >= 100); mBribe1000Button->setEnabled (playerGold >= 1000); mGoldLabel->setCaptionWithReplacing("#{sGold}: " + MyGUI::utility::toString(playerGold)); WindowModal::onOpen(); } MyGUI::Widget* PersuasionDialog::getDefaultKeyFocus() { return mAdmireButton; } // -------------------------------------------------------------------------------------------------- Response::Response(const std::string &text, const std::string &title, bool needMargin) : mTitle(title), mNeedMargin(needMargin) { mText = text; } void Response::write(BookTypesetter::Ptr typesetter, KeywordSearchT* keywordSearch, std::map& topicLinks) const { typesetter->sectionBreak(mNeedMargin ? 9 : 0); if (mTitle != "") { const MyGUI::Colour& headerColour = MWBase::Environment::get().getWindowManager()->getTextColours().header; BookTypesetter::Style* title = typesetter->createStyle("", headerColour, false); typesetter->write(title, to_utf8_span(mTitle.c_str())); typesetter->sectionBreak(); } typedef std::pair Range; std::map hyperLinks; // We need this copy for when @# hyperlinks are replaced std::string text = mText; size_t pos_end = std::string::npos; for(;;) { size_t pos_begin = text.find('@'); if (pos_begin != std::string::npos) pos_end = text.find('#', pos_begin); if (pos_begin != std::string::npos && pos_end != std::string::npos) { std::string link = text.substr(pos_begin + 1, pos_end - pos_begin - 1); const char specialPseudoAsteriskCharacter = 127; std::replace(link.begin(), link.end(), specialPseudoAsteriskCharacter, '*'); std::string topicName = MWBase::Environment::get().getWindowManager()-> getTranslationDataStorage().topicStandardForm(link); std::string displayName = link; while (displayName[displayName.size()-1] == '*') displayName.erase(displayName.size()-1, 1); text.replace(pos_begin, pos_end+1-pos_begin, displayName); if (topicLinks.find(Misc::StringUtils::lowerCase(topicName)) != topicLinks.end()) hyperLinks[std::make_pair(pos_begin, pos_begin+displayName.size())] = intptr_t(topicLinks[Misc::StringUtils::lowerCase(topicName)]); } else break; } typesetter->addContent(to_utf8_span(text.c_str())); if (hyperLinks.size() && MWBase::Environment::get().getWindowManager()->getTranslationDataStorage().hasTranslation()) { const TextColours& textColours = MWBase::Environment::get().getWindowManager()->getTextColours(); BookTypesetter::Style* style = typesetter->createStyle("", textColours.normal, false); size_t formatted = 0; // points to the first character that is not laid out yet for (auto& hyperLink : hyperLinks) { intptr_t topicId = hyperLink.second; BookTypesetter::Style* hotStyle = typesetter->createHotStyle (style, textColours.link, textColours.linkOver, textColours.linkPressed, topicId); if (formatted < hyperLink.first.first) typesetter->write(style, formatted, hyperLink.first.first); typesetter->write(hotStyle, hyperLink.first.first, hyperLink.first.second); formatted = hyperLink.first.second; } if (formatted < text.size()) typesetter->write(style, formatted, text.size()); } else { std::vector matches; keywordSearch->highlightKeywords(text.begin(), text.end(), matches); std::string::const_iterator i = text.begin (); for (KeywordSearchT::Match& match : matches) { if (i != match.mBeg) addTopicLink (typesetter, 0, i - text.begin (), match.mBeg - text.begin ()); addTopicLink (typesetter, match.mValue, match.mBeg - text.begin (), match.mEnd - text.begin ()); i = match.mEnd; } if (i != text.end ()) addTopicLink (typesetter, 0, i - text.begin (), text.size ()); } } void Response::addTopicLink(BookTypesetter::Ptr typesetter, intptr_t topicId, size_t begin, size_t end) const { const TextColours& textColours = MWBase::Environment::get().getWindowManager()->getTextColours(); BookTypesetter::Style* style = typesetter->createStyle("", textColours.normal, false); if (topicId) style = typesetter->createHotStyle (style, textColours.link, textColours.linkOver, textColours.linkPressed, topicId); typesetter->write (style, begin, end); } Message::Message(const std::string& text) { mText = text; } void Message::write(BookTypesetter::Ptr typesetter, KeywordSearchT* keywordSearch, std::map& topicLinks) const { const MyGUI::Colour& textColour = MWBase::Environment::get().getWindowManager()->getTextColours().notify; BookTypesetter::Style* title = typesetter->createStyle("", textColour, false); typesetter->sectionBreak(9); typesetter->write(title, to_utf8_span(mText.c_str())); } // -------------------------------------------------------------------------------------------------- void Choice::activated() { MWBase::Environment::get().getWindowManager()->playSound("Menu Click"); eventChoiceActivated(mChoiceId); } void Topic::activated() { MWBase::Environment::get().getWindowManager()->playSound("Menu Click"); eventTopicActivated(mTopicId); } void Goodbye::activated() { MWBase::Environment::get().getWindowManager()->playSound("Menu Click"); eventActivated(); } // -------------------------------------------------------------------------------------------------- DialogueWindow::DialogueWindow() : WindowBase("openmw_dialogue_window.layout") , mIsCompanion(false) , mGoodbye(false) , mPersuasionDialog(new ResponseCallback(this)) , mCallback(new ResponseCallback(this)) , mGreetingCallback(new ResponseCallback(this, false)) { // Centre dialog center(); mPersuasionDialog.setVisible(false); //History view getWidget(mHistory, "History"); //Topics list getWidget(mTopicsList, "TopicsList"); mTopicsList->eventItemSelected += MyGUI::newDelegate(this, &DialogueWindow::onSelectListItem); getWidget(mGoodbyeButton, "ByeButton"); mGoodbyeButton->eventMouseButtonClick += MyGUI::newDelegate(this, &DialogueWindow::onByeClicked); getWidget(mDispositionBar, "Disposition"); getWidget(mDispositionText,"DispositionText"); getWidget(mScrollBar, "VScroll"); mScrollBar->eventScrollChangePosition += MyGUI::newDelegate(this, &DialogueWindow::onScrollbarMoved); mHistory->eventMouseWheel += MyGUI::newDelegate(this, &DialogueWindow::onMouseWheel); BookPage::ClickCallback callback = std::bind (&DialogueWindow::notifyLinkClicked, this, std::placeholders::_1); mHistory->adviseLinkClicked(callback); mMainWidget->castType()->eventWindowChangeCoord += MyGUI::newDelegate(this, &DialogueWindow::onWindowResize); } DialogueWindow::~DialogueWindow() { deleteLater(); for (Link* link : mLinks) delete link; for (const auto& link : mTopicLinks) delete link.second; for (auto history : mHistoryContents) delete history; } void DialogueWindow::onTradeComplete() { addResponse("", MyGUI::LanguageManager::getInstance().replaceTags("#{sBarterDialog5}")); } bool DialogueWindow::exit() { if ((MWBase::Environment::get().getDialogueManager()->isInChoice())) { return false; } else { resetReference(); MWBase::Environment::get().getDialogueManager()->goodbyeSelected(); mTopicsList->scrollToTop(); return true; } } void DialogueWindow::onWindowResize(MyGUI::Window* _sender) { // if the window has only been moved, not resized, we don't need to update if (mCurrentWindowSize == _sender->getSize()) return; mTopicsList->adjustSize(); updateHistory(); updateTopicFormat(); mCurrentWindowSize = _sender->getSize(); } void DialogueWindow::onMouseWheel(MyGUI::Widget* _sender, int _rel) { if (!mScrollBar->getVisible()) return; mScrollBar->setScrollPosition(std::min(static_cast(mScrollBar->getScrollRange()-1), std::max(0, static_cast(mScrollBar->getScrollPosition() - _rel*0.3)))); onScrollbarMoved(mScrollBar, mScrollBar->getScrollPosition()); } void DialogueWindow::onByeClicked(MyGUI::Widget* _sender) { if (exit()) { MWBase::Environment::get().getWindowManager()->removeGuiMode(GM_Dialogue); } } void DialogueWindow::onSelectListItem(const std::string& topic, int id) { /* Start of tes3mp change (major) Instead of activating a list item here, send an ObjectDialogueChoice packet to the server and let it decide whether the list item gets activated */ sendDialogueChoicePacket(topic); return; /* End of tes3mp change (major) */ MWBase::DialogueManager* dialogueManager = MWBase::Environment::get().getDialogueManager(); if (mGoodbye || dialogueManager->isInChoice()) return; const MWWorld::Store &gmst = MWBase::Environment::get().getWorld()->getStore().get(); const std::string sPersuasion = gmst.find("sPersuasion")->mValue.getString(); const std::string sCompanionShare = gmst.find("sCompanionShare")->mValue.getString(); const std::string sBarter = gmst.find("sBarter")->mValue.getString(); const std::string sSpells = gmst.find("sSpells")->mValue.getString(); const std::string sTravel = gmst.find("sTravel")->mValue.getString(); const std::string sSpellMakingMenuTitle = gmst.find("sSpellMakingMenuTitle")->mValue.getString(); const std::string sEnchanting = gmst.find("sEnchanting")->mValue.getString(); const std::string sServiceTrainingTitle = gmst.find("sServiceTrainingTitle")->mValue.getString(); const std::string sRepair = gmst.find("sRepair")->mValue.getString(); if (topic != sPersuasion && topic != sCompanionShare && topic != sBarter && topic != sSpells && topic != sTravel && topic != sSpellMakingMenuTitle && topic != sEnchanting && topic != sServiceTrainingTitle && topic != sRepair) { onTopicActivated(topic); if (mGoodbyeButton->getEnabled()) MWBase::Environment::get().getWindowManager()->setKeyFocusWidget(mGoodbyeButton); } else if (topic == sPersuasion) mPersuasionDialog.setVisible(true); else if (topic == sCompanionShare) MWBase::Environment::get().getWindowManager()->pushGuiMode(GM_Companion, mPtr); else if (!dialogueManager->checkServiceRefused(mCallback.get())) { if (topic == sBarter && !dialogueManager->checkServiceRefused(mCallback.get(), MWBase::DialogueManager::Barter)) MWBase::Environment::get().getWindowManager()->pushGuiMode(GM_Barter, mPtr); else if (topic == sSpells && !dialogueManager->checkServiceRefused(mCallback.get(), MWBase::DialogueManager::Spells)) MWBase::Environment::get().getWindowManager()->pushGuiMode(GM_SpellBuying, mPtr); else if (topic == sTravel && !dialogueManager->checkServiceRefused(mCallback.get(), MWBase::DialogueManager::Travel)) MWBase::Environment::get().getWindowManager()->pushGuiMode(GM_Travel, mPtr); else if (topic == sSpellMakingMenuTitle && !dialogueManager->checkServiceRefused(mCallback.get(), MWBase::DialogueManager::Spellmaking)) MWBase::Environment::get().getWindowManager()->pushGuiMode(GM_SpellCreation, mPtr); else if (topic == sEnchanting && !dialogueManager->checkServiceRefused(mCallback.get(), MWBase::DialogueManager::Enchanting)) MWBase::Environment::get().getWindowManager()->pushGuiMode(GM_Enchanting, mPtr); else if (topic == sServiceTrainingTitle && !dialogueManager->checkServiceRefused(mCallback.get(), MWBase::DialogueManager::Training)) MWBase::Environment::get().getWindowManager()->pushGuiMode(GM_Training, mPtr); else if (topic == sRepair && !dialogueManager->checkServiceRefused(mCallback.get(), MWBase::DialogueManager::Repair)) MWBase::Environment::get().getWindowManager()->pushGuiMode(GM_MerchantRepair, mPtr); } else updateTopics(); } /* Start of tes3mp addition A different event that should be used in multiplayer when clicking on choices in the dialogue screen, sending DialogueChoice packets to the server so they can be approved or denied */ void DialogueWindow::sendDialogueChoicePacket(const std::string& topic) { mwmp::ObjectList* objectList = mwmp::Main::get().getNetworking()->getObjectList(); objectList->reset(); objectList->packetOrigin = mwmp::CLIENT_GAMEPLAY; objectList->addObjectDialogueChoice(mPtr, topic); objectList->sendObjectDialogueChoice(); } /* End of tes3mp addition */ /* Start of tes3mp addition Make it possible to activate any dialogue choice from elsewhere in the code */ void DialogueWindow::activateDialogueChoice(unsigned char dialogueChoiceType, std::string topic) { if (dialogueChoiceType == mwmp::DialogueChoiceType::TOPIC) { onTopicActivated(topic); } else if (dialogueChoiceType == mwmp::DialogueChoiceType::PERSUASION) mPersuasionDialog.setVisible(true); else if (dialogueChoiceType == mwmp::DialogueChoiceType::COMPANION_SHARE) MWBase::Environment::get().getWindowManager()->pushGuiMode(GM_Companion, mPtr); else { MWBase::DialogueManager* dialogueManager = MWBase::Environment::get().getDialogueManager(); if (dialogueChoiceType == mwmp::DialogueChoiceType::BARTER && !dialogueManager->checkServiceRefused(mCallback.get(), MWBase::DialogueManager::Barter)) MWBase::Environment::get().getWindowManager()->pushGuiMode(GM_Barter, mPtr); else if (dialogueChoiceType == mwmp::DialogueChoiceType::SPELLS && !dialogueManager->checkServiceRefused(mCallback.get(), MWBase::DialogueManager::Spells)) MWBase::Environment::get().getWindowManager()->pushGuiMode(GM_SpellBuying, mPtr); else if (dialogueChoiceType == mwmp::DialogueChoiceType::TRAVEL && !dialogueManager->checkServiceRefused(mCallback.get(), MWBase::DialogueManager::Travel)) MWBase::Environment::get().getWindowManager()->pushGuiMode(GM_Travel, mPtr); else if (dialogueChoiceType == mwmp::DialogueChoiceType::SPELLMAKING && !dialogueManager->checkServiceRefused(mCallback.get(), MWBase::DialogueManager::Spellmaking)) MWBase::Environment::get().getWindowManager()->pushGuiMode(GM_SpellCreation, mPtr); else if (dialogueChoiceType == mwmp::DialogueChoiceType::ENCHANTING && !dialogueManager->checkServiceRefused(mCallback.get(), MWBase::DialogueManager::Enchanting)) MWBase::Environment::get().getWindowManager()->pushGuiMode(GM_Enchanting, mPtr); else if (dialogueChoiceType == mwmp::DialogueChoiceType::TRAINING && !dialogueManager->checkServiceRefused(mCallback.get(), MWBase::DialogueManager::Training)) MWBase::Environment::get().getWindowManager()->pushGuiMode(GM_Training, mPtr); else if (dialogueChoiceType == mwmp::DialogueChoiceType::REPAIR && !dialogueManager->checkServiceRefused(mCallback.get(), MWBase::DialogueManager::Repair)) MWBase::Environment::get().getWindowManager()->pushGuiMode(GM_MerchantRepair, mPtr); } } /* End of tes3mp addition */ /* Start of tes3mp addition Make it possible to get the Ptr of the actor involved in the dialogue */ MWWorld::Ptr DialogueWindow::getPtr() { return mPtr; } /* End of tes3mp addition */ void DialogueWindow::setPtr(const MWWorld::Ptr& actor) { if (!actor.getClass().isActor()) { Log(Debug::Warning) << "Warning: can not talk with non-actor object."; return; } bool sameActor = (mPtr == actor); if (!sameActor) { // The history is not reset here mKeywords.clear(); mTopicsList->clear(); for (Link* link : mLinks) mDeleteLater.push_back(link); // Links are not deleted right away to prevent issues with event handlers mLinks.clear(); } mPtr = actor; mGoodbye = false; mTopicsList->setEnabled(true); if (!MWBase::Environment::get().getDialogueManager()->startDialogue(actor, mGreetingCallback.get())) { // No greetings found. The dialogue window should not be shown. // If this is a companion, we must show the companion window directly (used by BM_bear_be_unique). MWBase::Environment::get().getWindowManager()->removeGuiMode(MWGui::GM_Dialogue); mPtr = MWWorld::Ptr(); if (isCompanion(actor)) MWBase::Environment::get().getWindowManager()->pushGuiMode(MWGui::GM_Companion, actor); return; } MWBase::Environment::get().getWindowManager()->setKeyFocusWidget(mGoodbyeButton); setTitle(mPtr.getClass().getName(mPtr)); updateTopics(); updateTopicsPane(); // force update for new services updateDisposition(); restock(); } void DialogueWindow::restock() { MWMechanics::CreatureStats &sellerStats = mPtr.getClass().getCreatureStats(mPtr); float delay = MWBase::Environment::get().getWorld()->getStore().get().find("fBarterGoldResetDelay")->mValue.getFloat(); // Gold is restocked every 24h if (MWBase::Environment::get().getWorld()->getTimeStamp() >= sellerStats.getLastRestockTime() + delay) { /* Start of tes3mp change (major) Instead of restocking the NPC's gold pool or last restock time here, send a packet about them to the server */ /* sellerStats.setGoldPool(mPtr.getClass().getBaseGold(mPtr)); sellerStats.setLastRestockTime(MWBase::Environment::get().getWorld()->getTimeStamp()); */ mwmp::ObjectList* objectList = mwmp::Main::get().getNetworking()->getObjectList(); objectList->reset(); objectList->packetOrigin = mwmp::CLIENT_GAMEPLAY; objectList->addObjectMiscellaneous(mPtr, mPtr.getClass().getBaseGold(mPtr), MWBase::Environment::get().getWorld()->getTimeStamp().getHour(), MWBase::Environment::get().getWorld()->getTimeStamp().getDay()); objectList->sendObjectMiscellaneous(); /* End of tes3mp change (major) */ } } void DialogueWindow::deleteLater() { for (Link* link : mDeleteLater) delete link; mDeleteLater.clear(); } void DialogueWindow::onClose() { if (MWBase::Environment::get().getWindowManager()->containsMode(GM_Dialogue)) return; // Reset history for (DialogueText* text : mHistoryContents) delete text; mHistoryContents.clear(); } bool DialogueWindow::setKeywords(std::list keyWords) { if (mKeywords == keyWords && isCompanion() == mIsCompanion) return false; mIsCompanion = isCompanion(); mKeywords = keyWords; updateTopicsPane(); return true; } void DialogueWindow::updateTopicsPane() { mTopicsList->clear(); for (auto& linkPair : mTopicLinks) mDeleteLater.push_back(linkPair.second); mTopicLinks.clear(); mKeywordSearch.clear(); int services = mPtr.getClass().getServices(mPtr); bool travel = (mPtr.getTypeName() == typeid(ESM::NPC).name() && !mPtr.get()->mBase->getTransport().empty()) || (mPtr.getTypeName() == typeid(ESM::Creature).name() && !mPtr.get()->mBase->getTransport().empty()); const MWWorld::Store &gmst = MWBase::Environment::get().getWorld()->getStore().get(); if (mPtr.getTypeName() == typeid(ESM::NPC).name()) mTopicsList->addItem(gmst.find("sPersuasion")->mValue.getString()); if (services & ESM::NPC::AllItems) mTopicsList->addItem(gmst.find("sBarter")->mValue.getString()); if (services & ESM::NPC::Spells) mTopicsList->addItem(gmst.find("sSpells")->mValue.getString()); if (travel) mTopicsList->addItem(gmst.find("sTravel")->mValue.getString()); if (services & ESM::NPC::Spellmaking) mTopicsList->addItem(gmst.find("sSpellmakingMenuTitle")->mValue.getString()); if (services & ESM::NPC::Enchanting) mTopicsList->addItem(gmst.find("sEnchanting")->mValue.getString()); if (services & ESM::NPC::Training) mTopicsList->addItem(gmst.find("sServiceTrainingTitle")->mValue.getString()); if (services & ESM::NPC::Repair) mTopicsList->addItem(gmst.find("sRepair")->mValue.getString()); if (isCompanion()) mTopicsList->addItem(gmst.find("sCompanionShare")->mValue.getString()); if (mTopicsList->getItemCount() > 0) mTopicsList->addSeparator(); for(const auto& keyword : mKeywords) { std::string topicId = Misc::StringUtils::lowerCase(keyword); mTopicsList->addItem(keyword); Topic* t = new Topic(keyword); /* Start of tes3mp change (major) Instead of running DialogueWindow::onSelectListItem() when clicking a highlighted topic, run onSendDialoguePacket() so the server can approve or deny a dialogue choice */ //t->eventTopicActivated += MyGUI::newDelegate(this, &DialogueWindow::onTopicActivated); t->eventTopicActivated += MyGUI::newDelegate(this, &DialogueWindow::sendDialogueChoicePacket); /* End of tes3mp change (major) */ mTopicLinks[topicId] = t; mKeywordSearch.seed(topicId, intptr_t(t)); } mTopicsList->adjustSize(); updateHistory(); // The topics list has been regenerated so topic formatting needs to be updated updateTopicFormat(); } void DialogueWindow::updateHistory(bool scrollbar) { if (!scrollbar && mScrollBar->getVisible()) { mHistory->setSize(mHistory->getSize()+MyGUI::IntSize(mScrollBar->getWidth(),0)); mScrollBar->setVisible(false); } if (scrollbar && !mScrollBar->getVisible()) { mHistory->setSize(mHistory->getSize()-MyGUI::IntSize(mScrollBar->getWidth(),0)); mScrollBar->setVisible(true); } BookTypesetter::Ptr typesetter = BookTypesetter::create (mHistory->getWidth(), std::numeric_limits::max()); for (DialogueText* text : mHistoryContents) text->write(typesetter, &mKeywordSearch, mTopicLinks); BookTypesetter::Style* body = typesetter->createStyle("", MyGUI::Colour::White, false); typesetter->sectionBreak(9); // choices const TextColours& textColours = MWBase::Environment::get().getWindowManager()->getTextColours(); mChoices = MWBase::Environment::get().getDialogueManager()->getChoices(); for (std::pair& choice : mChoices) { Choice* link = new Choice(choice.second); link->eventChoiceActivated += MyGUI::newDelegate(this, &DialogueWindow::onChoiceActivated); mLinks.push_back(link); typesetter->lineBreak(); BookTypesetter::Style* questionStyle = typesetter->createHotStyle(body, textColours.answer, textColours.answerOver, textColours.answerPressed, TypesetBook::InteractiveId(link)); typesetter->write(questionStyle, to_utf8_span(choice.first.c_str())); } mGoodbye = MWBase::Environment::get().getDialogueManager()->isGoodbye(); if (mGoodbye) { Goodbye* link = new Goodbye(); link->eventActivated += MyGUI::newDelegate(this, &DialogueWindow::onGoodbyeActivated); mLinks.push_back(link); std::string goodbye = MWBase::Environment::get().getWorld()->getStore().get().find("sGoodbye")->mValue.getString(); BookTypesetter::Style* questionStyle = typesetter->createHotStyle(body, textColours.answer, textColours.answerOver, textColours.answerPressed, TypesetBook::InteractiveId(link)); typesetter->lineBreak(); typesetter->write(questionStyle, to_utf8_span(goodbye.c_str())); } TypesetBook::Ptr book = typesetter->complete(); mHistory->showPage(book, 0); size_t viewHeight = mHistory->getParent()->getHeight(); if (!scrollbar && book->getSize().second > viewHeight) updateHistory(true); else if (scrollbar) { mHistory->setSize(MyGUI::IntSize(mHistory->getWidth(), book->getSize().second)); size_t range = book->getSize().second - viewHeight; mScrollBar->setScrollRange(range); mScrollBar->setScrollPosition(range-1); mScrollBar->setTrackSize(static_cast(viewHeight / static_cast(book->getSize().second) * mScrollBar->getLineSize())); onScrollbarMoved(mScrollBar, range-1); } else { // no scrollbar onScrollbarMoved(mScrollBar, 0); } bool goodbyeEnabled = !MWBase::Environment::get().getDialogueManager()->isInChoice() || mGoodbye; bool goodbyeWasEnabled = mGoodbyeButton->getEnabled(); mGoodbyeButton->setEnabled(goodbyeEnabled); if (goodbyeEnabled && !goodbyeWasEnabled) MWBase::Environment::get().getWindowManager()->setKeyFocusWidget(mGoodbyeButton); bool topicsEnabled = !MWBase::Environment::get().getDialogueManager()->isInChoice() && !mGoodbye; mTopicsList->setEnabled(topicsEnabled); } void DialogueWindow::notifyLinkClicked (TypesetBook::InteractiveId link) { reinterpret_cast(link)->activated(); } void DialogueWindow::onTopicActivated(const std::string &topicId) { if (mGoodbye) return; MWBase::Environment::get().getDialogueManager()->keywordSelected(topicId, mCallback.get()); updateTopics(); } void DialogueWindow::onChoiceActivated(int id) { if (mGoodbye) { onGoodbyeActivated(); return; } MWBase::Environment::get().getDialogueManager()->questionAnswered(id, mCallback.get()); updateTopics(); } void DialogueWindow::onGoodbyeActivated() { MWBase::Environment::get().getDialogueManager()->goodbyeSelected(); MWBase::Environment::get().getWindowManager()->removeGuiMode(MWGui::GM_Dialogue); resetReference(); } void DialogueWindow::onScrollbarMoved(MyGUI::ScrollBar *sender, size_t pos) { mHistory->setPosition(0, static_cast(pos) * -1); } void DialogueWindow::addResponse(const std::string &title, const std::string &text, bool needMargin) { mHistoryContents.push_back(new Response(text, title, needMargin)); updateHistory(); } void DialogueWindow::addMessageBox(const std::string& text) { mHistoryContents.push_back(new Message(text)); updateHistory(); } void DialogueWindow::updateDisposition() { bool dispositionVisible = false; if (!mPtr.isEmpty() && mPtr.getClass().isNpc()) { dispositionVisible = true; mDispositionBar->setProgressRange(100); mDispositionBar->setProgressPosition(MWBase::Environment::get().getMechanicsManager()->getDerivedDisposition(mPtr)); mDispositionText->setCaption(MyGUI::utility::toString(MWBase::Environment::get().getMechanicsManager()->getDerivedDisposition(mPtr))+std::string("/100")); } bool dispositionWasVisible = mDispositionBar->getVisible(); if (dispositionVisible && !dispositionWasVisible) { mDispositionBar->setVisible(true); int offset = mDispositionBar->getHeight()+5; mTopicsList->setCoord(mTopicsList->getCoord() + MyGUI::IntCoord(0,offset,0,-offset)); mTopicsList->adjustSize(); } else if (!dispositionVisible && dispositionWasVisible) { mDispositionBar->setVisible(false); int offset = mDispositionBar->getHeight()+5; mTopicsList->setCoord(mTopicsList->getCoord() - MyGUI::IntCoord(0,offset,0,-offset)); mTopicsList->adjustSize(); } } void DialogueWindow::onReferenceUnavailable() { MWBase::Environment::get().getWindowManager()->removeGuiMode(GM_Dialogue); } void DialogueWindow::onFrame(float dt) { checkReferenceAvailable(); if (mPtr.isEmpty()) return; updateDisposition(); deleteLater(); if (mChoices != MWBase::Environment::get().getDialogueManager()->getChoices() || mGoodbye != MWBase::Environment::get().getDialogueManager()->isGoodbye()) updateHistory(); } void DialogueWindow::updateTopicFormat() { if (!Settings::Manager::getBool("color topic enable", "GUI")) return; std::string specialColour = Settings::Manager::getString("color topic specific", "GUI"); std::string oldColour = Settings::Manager::getString("color topic exhausted", "GUI"); for (const std::string& keyword : mKeywords) { int flag = MWBase::Environment::get().getDialogueManager()->getTopicFlag(keyword); MyGUI::Button* button = mTopicsList->getItemWidget(keyword); if (!specialColour.empty() && flag & MWBase::DialogueManager::TopicType::Specific) button->getSubWidgetText()->setTextColour(MyGUI::Colour::parse(specialColour)); else if (!oldColour.empty() && flag & MWBase::DialogueManager::TopicType::Exhausted) button->getSubWidgetText()->setTextColour(MyGUI::Colour::parse(oldColour)); } } void DialogueWindow::updateTopics() { // Topic formatting needs to be updated regardless of whether the topic list has changed if (!setKeywords(MWBase::Environment::get().getDialogueManager()->getAvailableTopics())) updateTopicFormat(); } bool DialogueWindow::isCompanion() { return isCompanion(mPtr); } bool DialogueWindow::isCompanion(const MWWorld::Ptr& actor) { if (actor.isEmpty()) return false; return !actor.getClass().getScript(actor).empty() && actor.getRefData().getLocals().getIntVar(actor.getClass().getScript(actor), "companion"); } } ================================================ FILE: apps/openmw/mwgui/dialogue.hpp ================================================ #ifndef MWGUI_DIALOGE_H #define MWGUI_DIALOGE_H #include "windowbase.hpp" #include "referenceinterface.hpp" #include "bookpage.hpp" #include "../mwdialogue/keywordsearch.hpp" #include namespace Gui { class MWList; } namespace MWGui { class ResponseCallback; class PersuasionDialog : public WindowModal { public: PersuasionDialog(ResponseCallback* callback); void onOpen() override; MyGUI::Widget* getDefaultKeyFocus() override; private: std::unique_ptr mCallback; MyGUI::Button* mCancelButton; MyGUI::Button* mAdmireButton; MyGUI::Button* mIntimidateButton; MyGUI::Button* mTauntButton; MyGUI::Button* mBribe10Button; MyGUI::Button* mBribe100Button; MyGUI::Button* mBribe1000Button; MyGUI::TextBox* mGoldLabel; void onCancel (MyGUI::Widget* sender); void onPersuade (MyGUI::Widget* sender); }; struct Link { virtual ~Link() {} virtual void activated () = 0; }; struct Topic : Link { typedef MyGUI::delegates::CMultiDelegate1 EventHandle_TopicId; EventHandle_TopicId eventTopicActivated; Topic(const std::string& id) : mTopicId(id) {} std::string mTopicId; void activated () override; }; struct Choice : Link { typedef MyGUI::delegates::CMultiDelegate1 EventHandle_ChoiceId; EventHandle_ChoiceId eventChoiceActivated; Choice(int id) : mChoiceId(id) {} int mChoiceId; void activated () override; }; struct Goodbye : Link { typedef MyGUI::delegates::CMultiDelegate0 Event_Activated; Event_Activated eventActivated; void activated () override; }; typedef MWDialogue::KeywordSearch KeywordSearchT; struct DialogueText { virtual ~DialogueText() {} virtual void write (BookTypesetter::Ptr typesetter, KeywordSearchT* keywordSearch, std::map& topicLinks) const = 0; std::string mText; }; struct Response : DialogueText { Response(const std::string& text, const std::string& title = "", bool needMargin = true); void write (BookTypesetter::Ptr typesetter, KeywordSearchT* keywordSearch, std::map& topicLinks) const override; void addTopicLink (BookTypesetter::Ptr typesetter, intptr_t topicId, size_t begin, size_t end) const; std::string mTitle; bool mNeedMargin; }; struct Message : DialogueText { Message(const std::string& text); void write (BookTypesetter::Ptr typesetter, KeywordSearchT* keywordSearch, std::map& topicLinks) const override; }; class DialogueWindow: public WindowBase, public ReferenceInterface { public: DialogueWindow(); ~DialogueWindow(); void onTradeComplete(); bool exit() override; // Events typedef MyGUI::delegates::CMultiDelegate0 EventHandle_Void; void notifyLinkClicked (TypesetBook::InteractiveId link); /* Start of tes3mp addition Make it possible to activate any dialogue choice from elsewhere in the code */ void activateDialogueChoice(unsigned char dialogueChoiceType, std::string topic = ""); /* End of tes3mp addition */ /* Start of tes3mp addition Make it possible to get the Ptr of the actor involved in the dialogue */ MWWorld::Ptr getPtr(); /* End of tes3mp addition */ void setPtr(const MWWorld::Ptr& actor) override; /// @return true if stale keywords were updated successfully bool setKeywords(std::list keyWord); void addResponse (const std::string& title, const std::string& text, bool needMargin = true); void addMessageBox(const std::string& text); void onFrame(float dt) override; void clear() override { resetReference(); } void updateTopics(); void onClose() override; protected: void updateTopicsPane(); bool isCompanion(const MWWorld::Ptr& actor); bool isCompanion(); /* Start of tes3mp addition A different event that should be used in multiplayer when clicking on choices in the dialogue screen, sending DialogueChoice packets to the server so they can be approved or denied */ void sendDialogueChoicePacket(const std::string& topic); /* End of tes3mp addition */ void onSelectListItem(const std::string& topic, int id); void onByeClicked(MyGUI::Widget* _sender); void onMouseWheel(MyGUI::Widget* _sender, int _rel); void onWindowResize(MyGUI::Window* _sender); void onTopicActivated(const std::string& topicId); void onChoiceActivated(int id); void onGoodbyeActivated(); void onScrollbarMoved (MyGUI::ScrollBar* sender, size_t pos); void updateHistory(bool scrollbar=false); void onReferenceUnavailable() override; private: void updateDisposition(); void restock(); void deleteLater(); bool mIsCompanion; std::list mKeywords; std::vector mHistoryContents; std::vector > mChoices; bool mGoodbye; std::vector mLinks; std::map mTopicLinks; std::vector mDeleteLater; KeywordSearchT mKeywordSearch; BookPage* mHistory; Gui::MWList* mTopicsList; MyGUI::ScrollBar* mScrollBar; MyGUI::ProgressBar* mDispositionBar; MyGUI::TextBox* mDispositionText; MyGUI::Button* mGoodbyeButton; PersuasionDialog mPersuasionDialog; MyGUI::IntSize mCurrentWindowSize; std::unique_ptr mCallback; std::unique_ptr mGreetingCallback; void updateTopicFormat(); }; } #endif ================================================ FILE: apps/openmw/mwgui/draganddrop.cpp ================================================ #include "draganddrop.hpp" #include #include #include "../mwbase/windowmanager.hpp" #include "../mwbase/environment.hpp" #include "../mwworld/class.hpp" #include "sortfilteritemmodel.hpp" #include "inventorywindow.hpp" #include "itemwidget.hpp" #include "itemview.hpp" #include "controllers.hpp" namespace MWGui { DragAndDrop::DragAndDrop() : mIsOnDragAndDrop(false) , mDraggedWidget(nullptr) , mSourceModel(nullptr) , mSourceView(nullptr) , mSourceSortModel(nullptr) , mDraggedCount(0) { } void DragAndDrop::startDrag (int index, SortFilterItemModel* sortModel, ItemModel* sourceModel, ItemView* sourceView, int count) { mItem = sourceModel->getItem(index); mDraggedCount = count; mSourceModel = sourceModel; mSourceView = sourceView; mSourceSortModel = sortModel; // If picking up an item that isn't from the player's inventory, the item gets added to player inventory backend // immediately, even though it's still floating beneath the mouse cursor. A bit counterintuitive, // but this is how it works in vanilla, and not doing so would break quests (BM_beasts for instance). ItemModel* playerModel = MWBase::Environment::get().getWindowManager()->getInventoryWindow()->getModel(); if (mSourceModel != playerModel) { MWWorld::Ptr item = mSourceModel->moveItem(mItem, mDraggedCount, playerModel); playerModel->update(); ItemModel::ModelIndex newIndex = -1; for (unsigned int i=0; igetItemCount(); ++i) { if (playerModel->getItem(i).mBase == item) { newIndex = i; break; } } mItem = playerModel->getItem(newIndex); mSourceModel = playerModel; SortFilterItemModel* playerFilterModel = MWBase::Environment::get().getWindowManager()->getInventoryWindow()->getSortFilterModel(); mSourceSortModel = playerFilterModel; } std::string sound = mItem.mBase.getClass().getUpSoundId(mItem.mBase); MWBase::Environment::get().getWindowManager()->playSound (sound); if (mSourceSortModel) { mSourceSortModel->clearDragItems(); mSourceSortModel->addDragItem(mItem.mBase, count); } ItemWidget* baseWidget = MyGUI::Gui::getInstance().createWidget("MW_ItemIcon", 0, 0, 42, 42, MyGUI::Align::Default, "DragAndDrop"); Controllers::ControllerFollowMouse* controller = MyGUI::ControllerManager::getInstance().createItem(Controllers::ControllerFollowMouse::getClassTypeName()) ->castType(); MyGUI::ControllerManager::getInstance().addItem(baseWidget, controller); mDraggedWidget = baseWidget; baseWidget->setItem(mItem.mBase); baseWidget->setNeedMouseFocus(false); baseWidget->setCount(count); sourceView->update(); MWBase::Environment::get().getWindowManager()->setDragDrop(true); mIsOnDragAndDrop = true; } void DragAndDrop::drop(ItemModel *targetModel, ItemView *targetView) { std::string sound = mItem.mBase.getClass().getDownSoundId(mItem.mBase); MWBase::Environment::get().getWindowManager()->playSound(sound); // We can't drop a conjured item to the ground; the target container should always be the source container if (mItem.mFlags & ItemStack::Flag_Bound && targetModel != mSourceModel) { MWBase::Environment::get().getWindowManager()->messageBox("#{sBarterDialog12}"); return; } // If item is dropped where it was taken from, we don't need to do anything - // otherwise, do the transfer if (targetModel != mSourceModel) { mSourceModel->moveItem(mItem, mDraggedCount, targetModel); } mSourceModel->update(); finish(); if (targetView) targetView->update(); MWBase::Environment::get().getWindowManager()->getInventoryWindow()->updateItemView(); // We need to update the view since an other item could be auto-equipped. mSourceView->update(); } void DragAndDrop::onFrame() { if (mIsOnDragAndDrop && mItem.mBase.getRefData().getCount() == 0) finish(); } /* Start of tes3mp change (minor) Add a deleteDragItems argument that allows the deletion of the items in the drag as oppposed to the regular behavior of returning them to their source model This is required to reduce unpredictable behavior for drags approved or rejected by the server */ void DragAndDrop::finish(bool deleteDragItems) /* End of tes3mp change (minor) */ { mIsOnDragAndDrop = false; mSourceSortModel->clearDragItems(); /* Start of tes3mp addition Make it possible to entirely delete the items in the drag */ if (deleteDragItems) mSourceModel->removeItem(mItem, mDraggedCount); /* End of tes3mp addition */ // since mSourceView doesn't get updated in drag() MWBase::Environment::get().getWindowManager()->getInventoryWindow()->updateItemView(); MyGUI::Gui::getInstance().destroyWidget(mDraggedWidget); mDraggedWidget = nullptr; MWBase::Environment::get().getWindowManager()->setDragDrop(false); } } ================================================ FILE: apps/openmw/mwgui/draganddrop.hpp ================================================ #ifndef OPENMW_MWGUI_DRAGANDDROP_H #define OPENMW_MWGUI_DRAGANDDROP_H #include "itemmodel.hpp" namespace MyGUI { class Widget; } namespace MWGui { class ItemView; class SortFilterItemModel; class DragAndDrop { public: bool mIsOnDragAndDrop; MyGUI::Widget* mDraggedWidget; ItemModel* mSourceModel; ItemView* mSourceView; SortFilterItemModel* mSourceSortModel; ItemStack mItem; int mDraggedCount; DragAndDrop(); void startDrag (int index, SortFilterItemModel* sortModel, ItemModel* sourceModel, ItemView* sourceView, int count); void drop (ItemModel* targetModel, ItemView* targetView); void onFrame(); /* Start of tes3mp change (minor) Add a deleteDragItems argument that allows the deletion of the items in the drag as oppposed to the regular behavior of returning them to their source model This is required to reduce unpredictable behavior for drags approved or rejected by the server */ void finish(bool deleteDragItems = false); /* End of tes3mp change (minor) */ }; } #endif ================================================ FILE: apps/openmw/mwgui/enchantingdialog.cpp ================================================ #include "enchantingdialog.hpp" #include #include #include #include #include #include /* Start of tes3mp addition Include additional headers for multiplayer purposes */ #include "../mwmp/Main.hpp" #include "../mwmp/Networking.hpp" #include "../mwmp/ObjectList.hpp" /* End of tes3mp addition */ #include "../mwbase/environment.hpp" #include "../mwbase/mechanicsmanager.hpp" #include "../mwbase/world.hpp" #include "../mwworld/class.hpp" #include "../mwworld/containerstore.hpp" #include "../mwworld/esmstore.hpp" #include "../mwmechanics/actorutil.hpp" #include "itemselection.hpp" #include "itemwidget.hpp" #include "sortfilteritemmodel.hpp" namespace MWGui { EnchantingDialog::EnchantingDialog() : WindowBase("openmw_enchanting_dialog.layout") , EffectEditorBase(EffectEditorBase::Enchanting) , mItemSelectionDialog(nullptr) { getWidget(mName, "NameEdit"); getWidget(mCancelButton, "CancelButton"); getWidget(mAvailableEffectsList, "AvailableEffects"); getWidget(mUsedEffectsView, "UsedEffects"); getWidget(mItemBox, "ItemBox"); getWidget(mSoulBox, "SoulBox"); getWidget(mEnchantmentPoints, "Enchantment"); getWidget(mCastCost, "CastCost"); getWidget(mCharge, "Charge"); getWidget(mSuccessChance, "SuccessChance"); getWidget(mChanceLayout, "ChanceLayout"); getWidget(mTypeButton, "TypeButton"); getWidget(mBuyButton, "BuyButton"); getWidget(mPrice, "PriceLabel"); getWidget(mPriceText, "PriceTextLabel"); setWidgets(mAvailableEffectsList, mUsedEffectsView); mCancelButton->eventMouseButtonClick += MyGUI::newDelegate(this, &EnchantingDialog::onCancelButtonClicked); mItemBox->eventMouseButtonClick += MyGUI::newDelegate(this, &EnchantingDialog::onSelectItem); mSoulBox->eventMouseButtonClick += MyGUI::newDelegate(this, &EnchantingDialog::onSelectSoul); mBuyButton->eventMouseButtonClick += MyGUI::newDelegate(this, &EnchantingDialog::onBuyButtonClicked); mTypeButton->eventMouseButtonClick += MyGUI::newDelegate(this, &EnchantingDialog::onTypeButtonClicked); mName->eventEditSelectAccept += MyGUI::newDelegate(this, &EnchantingDialog::onAccept); } EnchantingDialog::~EnchantingDialog() { delete mItemSelectionDialog; } void EnchantingDialog::onOpen() { center(); MWBase::Environment::get().getWindowManager()->setKeyFocusWidget(mName); } void EnchantingDialog::setSoulGem(const MWWorld::Ptr &gem) { if (gem.isEmpty()) { mSoulBox->setItem(MWWorld::Ptr()); mSoulBox->clearUserStrings(); mEnchanting.setSoulGem(MWWorld::Ptr()); } else { mSoulBox->setItem(gem); mSoulBox->setUserString ("ToolTipType", "ItemPtr"); mSoulBox->setUserData(MWWorld::Ptr(gem)); mEnchanting.setSoulGem(gem); } } void EnchantingDialog::setItem(const MWWorld::Ptr &item) { if (item.isEmpty()) { mItemBox->setItem(MWWorld::Ptr()); mItemBox->clearUserStrings(); mEnchanting.setOldItem(MWWorld::Ptr()); } else { mName->setCaption(item.getClass().getName(item)); mItemBox->setItem(item); mItemBox->setUserString ("ToolTipType", "ItemPtr"); mItemBox->setUserData(MWWorld::Ptr(item)); mEnchanting.setOldItem(item); } } void EnchantingDialog::updateLabels() { mEnchantmentPoints->setCaption(std::to_string(static_cast(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())))); mCastCost->setCaption(std::to_string(mEnchanting.getEffectiveCastCost())); mPrice->setCaption(std::to_string(mEnchanting.getEnchantPrice())); switch(mEnchanting.getCastStyle()) { case ESM::Enchantment::CastOnce: mTypeButton->setCaption(MWBase::Environment::get().getWindowManager()->getGameSettingString("sItemCastOnce","Cast Once")); setConstantEffect(false); break; case ESM::Enchantment::WhenStrikes: mTypeButton->setCaption(MWBase::Environment::get().getWindowManager()->getGameSettingString("sItemCastWhenStrikes", "When Strikes")); setConstantEffect(false); break; case ESM::Enchantment::WhenUsed: mTypeButton->setCaption(MWBase::Environment::get().getWindowManager()->getGameSettingString("sItemCastWhenUsed", "When Used")); setConstantEffect(false); break; case ESM::Enchantment::ConstantEffect: mTypeButton->setCaption(MWBase::Environment::get().getWindowManager()->getGameSettingString("sItemCastConstant", "Cast Constant")); setConstantEffect(true); break; } } void EnchantingDialog::setPtr (const MWWorld::Ptr& ptr) { mName->setCaption(""); if (ptr.getClass().isActor()) { mEnchanting.setSelfEnchanting(false); mEnchanting.setEnchanter(ptr); mBuyButton->setCaptionWithReplacing("#{sBuy}"); mChanceLayout->setVisible(false); mPtr = ptr; setSoulGem(MWWorld::Ptr()); mPrice->setVisible(true); mPriceText->setVisible(true); } else { mEnchanting.setSelfEnchanting(true); mEnchanting.setEnchanter(MWMechanics::getPlayer()); mBuyButton->setCaptionWithReplacing("#{sCreate}"); bool enabled = Settings::Manager::getBool("show enchant chance","Game"); mChanceLayout->setVisible(enabled); mPtr = MWMechanics::getPlayer(); setSoulGem(ptr); mPrice->setVisible(false); mPriceText->setVisible(false); } setItem(MWWorld::Ptr()); startEditing (); updateLabels(); } void EnchantingDialog::onReferenceUnavailable () { MWBase::Environment::get().getWindowManager()->removeGuiMode (GM_Enchanting); resetReference(); } void EnchantingDialog::resetReference() { ReferenceInterface::resetReference(); setItem(MWWorld::Ptr()); setSoulGem(MWWorld::Ptr()); mPtr = MWWorld::Ptr(); mEnchanting.setEnchanter(MWWorld::Ptr()); } void EnchantingDialog::onCancelButtonClicked(MyGUI::Widget* sender) { MWBase::Environment::get().getWindowManager()->removeGuiMode(GM_Enchanting); } void EnchantingDialog::onSelectItem(MyGUI::Widget *sender) { if (mEnchanting.getOldItem().isEmpty()) { delete mItemSelectionDialog; mItemSelectionDialog = new ItemSelectionDialog("#{sEnchantItems}"); mItemSelectionDialog->eventItemSelected += MyGUI::newDelegate(this, &EnchantingDialog::onItemSelected); mItemSelectionDialog->eventDialogCanceled += MyGUI::newDelegate(this, &EnchantingDialog::onItemCancel); mItemSelectionDialog->setVisible(true); mItemSelectionDialog->openContainer(MWMechanics::getPlayer()); mItemSelectionDialog->setFilter(SortFilterItemModel::Filter_OnlyEnchantable); } else { setItem(MWWorld::Ptr()); updateLabels(); } } void EnchantingDialog::onItemSelected(MWWorld::Ptr item) { mItemSelectionDialog->setVisible(false); setItem(item); MWBase::Environment::get().getWindowManager()->playSound(item.getClass().getDownSoundId(item)); mEnchanting.nextCastStyle(); updateLabels(); } void EnchantingDialog::onItemCancel() { mItemSelectionDialog->setVisible(false); } void EnchantingDialog::onSoulSelected(MWWorld::Ptr item) { mItemSelectionDialog->setVisible(false); mEnchanting.setSoulGem(item); if(mEnchanting.getGemCharge()==0) { MWBase::Environment::get().getWindowManager()->messageBox ("#{sNotifyMessage32}"); return; } setSoulGem(item); MWBase::Environment::get().getWindowManager()->playSound(item.getClass().getDownSoundId(item)); updateLabels(); } void EnchantingDialog::onSoulCancel() { mItemSelectionDialog->setVisible(false); } void EnchantingDialog::onSelectSoul(MyGUI::Widget *sender) { if (mEnchanting.getGem().isEmpty()) { delete mItemSelectionDialog; mItemSelectionDialog = new ItemSelectionDialog("#{sSoulGemsWithSouls}"); mItemSelectionDialog->eventItemSelected += MyGUI::newDelegate(this, &EnchantingDialog::onSoulSelected); mItemSelectionDialog->eventDialogCanceled += MyGUI::newDelegate(this, &EnchantingDialog::onSoulCancel); mItemSelectionDialog->setVisible(true); mItemSelectionDialog->openContainer(MWMechanics::getPlayer()); mItemSelectionDialog->setFilter(SortFilterItemModel::Filter_OnlyChargedSoulstones); //MWBase::Environment::get().getWindowManager()->messageBox("#{sInventorySelectNoSoul}"); } else { setSoulGem(MWWorld::Ptr()); mEnchanting.nextCastStyle(); updateLabels(); updateEffectsView(); } } void EnchantingDialog::notifyEffectsChanged () { mEffectList.mList = mEffects; mEnchanting.setEffect(mEffectList); updateLabels(); } void EnchantingDialog::onTypeButtonClicked(MyGUI::Widget* sender) { mEnchanting.nextCastStyle(); updateLabels(); updateEffectsView(); } void EnchantingDialog::onAccept(MyGUI::EditBox *sender) { onBuyButtonClicked(sender); // To do not spam onAccept() again and again MWBase::Environment::get().getWindowManager()->injectKeyRelease(MyGUI::KeyCode::None); } void EnchantingDialog::onBuyButtonClicked(MyGUI::Widget* sender) { if (mEffects.size() <= 0) { MWBase::Environment::get().getWindowManager()->messageBox ("#{sEnchantmentMenu11}"); return; } if (mName->getCaption ().empty()) { MWBase::Environment::get().getWindowManager()->messageBox ("#{sNotifyMessage10}"); return; } if (mEnchanting.soulEmpty()) { MWBase::Environment::get().getWindowManager()->messageBox ("#{sNotifyMessage52}"); return; } if (mEnchanting.itemEmpty()) { MWBase::Environment::get().getWindowManager()->messageBox ("#{sNotifyMessage11}"); return; } if (static_cast(mEnchanting.getEnchantPoints(false)) > mEnchanting.getMaxEnchantValue()) { MWBase::Environment::get().getWindowManager()->messageBox ("#{sNotifyMessage29}"); return; } mEnchanting.setNewItemName(mName->getCaption()); mEnchanting.setEffect(mEffectList); MWWorld::Ptr player = MWMechanics::getPlayer(); int playerGold = player.getClass().getContainerStore(player).count(MWWorld::ContainerStore::sGoldId); if (mPtr != player && mEnchanting.getEnchantPrice() > playerGold) { MWBase::Environment::get().getWindowManager()->messageBox ("#{sNotifyMessage18}"); return; } // check if the player is attempting to use a soulstone or item that was stolen from this actor if (mPtr != player) { for (int i=0; i<2; ++i) { MWWorld::Ptr item = (i == 0) ? mEnchanting.getOldItem() : mEnchanting.getGem(); if (MWBase::Environment::get().getMechanicsManager()->isItemStolenFrom(item.getCellRef().getRefId(), mPtr)) { std::string msg = MWBase::Environment::get().getWorld()->getStore().get().find("sNotifyMessage49")->mValue.getString(); msg = Misc::StringUtils::format(msg, item.getClass().getName(item)); MWBase::Environment::get().getWindowManager()->messageBox(msg); MWBase::Environment::get().getMechanicsManager()->confiscateStolenItemToOwner(player, item, mPtr, 1); MWBase::Environment::get().getWindowManager()->removeGuiMode (GM_Enchanting); MWBase::Environment::get().getWindowManager()->exitCurrentGuiMode(); return; } } } int result = mEnchanting.create(); if(result==1) { MWBase::Environment::get().getWindowManager()->playSound("enchant success"); MWBase::Environment::get().getWindowManager()->messageBox ("#{sEnchantmentMenu12}"); MWBase::Environment::get().getWindowManager()->removeGuiMode (GM_Enchanting); /* Start of tes3mp addition Send an ID_OBJECT_SOUND packet every time the player makes a sound here */ mwmp::ObjectList *objectList = mwmp::Main::get().getNetworking()->getObjectList(); objectList->reset(); objectList->packetOrigin = mwmp::CLIENT_GAMEPLAY; objectList->addObjectSound(MWMechanics::getPlayer(), "enchant success", 1.0, 1.0); objectList->sendObjectSound(); /* End of tes3mp addition */ } else { MWBase::Environment::get().getWindowManager()->playSound("enchant fail"); MWBase::Environment::get().getWindowManager()->messageBox ("#{sNotifyMessage34}"); /* Start of tes3mp addition Send an ID_OBJECT_SOUND packet every time the player makes a sound here */ mwmp::ObjectList *objectList = mwmp::Main::get().getNetworking()->getObjectList(); objectList->reset(); objectList->packetOrigin = mwmp::CLIENT_GAMEPLAY; objectList->addObjectSound(MWMechanics::getPlayer(), "enchant fail", 1.0, 1.0); objectList->sendObjectSound(); /* End of tes3mp addition */ if (!mEnchanting.getGem().isEmpty() && !mEnchanting.getGem().getRefData().getCount()) { setSoulGem(MWWorld::Ptr()); mEnchanting.nextCastStyle(); updateLabels(); updateEffectsView(); } } } } ================================================ FILE: apps/openmw/mwgui/enchantingdialog.hpp ================================================ #ifndef MWGUI_ENCHANTINGDIALOG_H #define MWGUI_ENCHANTINGDIALOG_H #include "spellcreationdialog.hpp" #include "../mwbase/windowmanager.hpp" #include "../mwmechanics/enchanting.hpp" namespace MWGui { class ItemSelectionDialog; class ItemWidget; class EnchantingDialog : public WindowBase, public ReferenceInterface, public EffectEditorBase { public: EnchantingDialog(); virtual ~EnchantingDialog(); void onOpen() override; void onFrame(float dt) override { checkReferenceAvailable(); } void clear() override { resetReference(); } void setSoulGem (const MWWorld::Ptr& gem); void setItem (const MWWorld::Ptr& item); /// Actor Ptr: buy enchantment from this actor /// Soulgem Ptr: player self-enchant void setPtr(const MWWorld::Ptr& ptr) override; void resetReference() override; protected: void onReferenceUnavailable() override; void notifyEffectsChanged() override; void onCancelButtonClicked(MyGUI::Widget* sender); void onSelectItem (MyGUI::Widget* sender); void onSelectSoul (MyGUI::Widget* sender); void onItemSelected(MWWorld::Ptr item); void onItemCancel(); void onSoulSelected(MWWorld::Ptr item); void onSoulCancel(); void onBuyButtonClicked(MyGUI::Widget* sender); void updateLabels(); void onTypeButtonClicked(MyGUI::Widget* sender); void onAccept(MyGUI::EditBox* sender); ItemSelectionDialog* mItemSelectionDialog; MyGUI::Widget* mChanceLayout; MyGUI::Button* mCancelButton; ItemWidget* mItemBox; ItemWidget* mSoulBox; MyGUI::Button* mTypeButton; MyGUI::Button* mBuyButton; MyGUI::EditBox* mName; MyGUI::TextBox* mEnchantmentPoints; MyGUI::TextBox* mCastCost; MyGUI::TextBox* mCharge; MyGUI::TextBox* mSuccessChance; MyGUI::TextBox* mPrice; MyGUI::TextBox* mPriceText; MWMechanics::Enchanting mEnchanting; ESM::EffectList mEffectList; }; } #endif ================================================ FILE: apps/openmw/mwgui/exposedwindow.cpp ================================================ #include "exposedwindow.hpp" namespace MWGui { MyGUI::VectorWidgetPtr Window::getSkinWidgetsByName (const std::string &name) { return MyGUI::Widget::getSkinWidgetsByName (name); } MyGUI::Widget* Window::getSkinWidget(const std::string & _name, bool _throw) { MyGUI::VectorWidgetPtr widgets = getSkinWidgetsByName (_name); if (widgets.empty()) { MYGUI_ASSERT( ! _throw, "widget name '" << _name << "' not found in skin of layout '" << getName() << "'"); return nullptr; } else { return widgets[0]; } } } ================================================ FILE: apps/openmw/mwgui/exposedwindow.hpp ================================================ #ifndef MWGUI_EXPOSEDWINDOW_H #define MWGUI_EXPOSEDWINDOW_H #include namespace MWGui { /** * @brief subclass to provide access to some Widget internals. */ class Window : public MyGUI::Window { MYGUI_RTTI_DERIVED(Window) public: MyGUI::VectorWidgetPtr getSkinWidgetsByName (const std::string &name); MyGUI::Widget* getSkinWidget(const std::string & _name, bool _throw = true); ///< Get a widget defined in the inner skin of this window. }; } #endif ================================================ FILE: apps/openmw/mwgui/formatting.cpp ================================================ #include "formatting.hpp" #include #include #include #include // correctBookartPath #include "../mwbase/environment.hpp" #include "../mwbase/windowmanager.hpp" #include #include #include #include "../mwscript/interpretercontext.hpp" namespace MWGui { namespace Formatting { /* BookTextParser */ BookTextParser::BookTextParser(const std::string & text) : mIndex(0), mText(text), mIgnoreNewlineTags(true), mIgnoreLineEndings(true), mClosingTag(false) { MWScript::InterpreterContext interpreterContext(nullptr, MWWorld::Ptr()); // empty arguments, because there is no locals or actor mText = Interpreter::fixDefinesBook(mText, interpreterContext); Misc::StringUtils::replaceAll(mText, "\r", ""); // vanilla game does not show any text after the last EOL tag. const std::string lowerText = Misc::StringUtils::lowerCase(mText); size_t brIndex = lowerText.rfind("
"); size_t pIndex = lowerText.rfind("

"); mPlainTextEnd = 0; if (brIndex != pIndex) { if (brIndex != std::string::npos && pIndex != std::string::npos) mPlainTextEnd = std::max(brIndex, pIndex); else if (brIndex != std::string::npos) mPlainTextEnd = brIndex; else mPlainTextEnd = pIndex; } registerTag("br", Event_BrTag); registerTag("p", Event_PTag); registerTag("img", Event_ImgTag); registerTag("div", Event_DivTag); registerTag("font", Event_FontTag); } void BookTextParser::registerTag(const std::string & tag, BookTextParser::Events type) { mTagTypes[tag] = type; } std::string BookTextParser::getReadyText() const { return mReadyText; } BookTextParser::Events BookTextParser::next() { while (mIndex < mText.size()) { char ch = mText[mIndex]; if (ch == '<') { const size_t tagStart = mIndex + 1; const size_t tagEnd = mText.find('>', tagStart); if (tagEnd == std::string::npos) throw std::runtime_error("BookTextParser Error: Tag is not terminated"); parseTag(mText.substr(tagStart, tagEnd - tagStart)); mIndex = tagEnd; if (mTagTypes.find(mTag) != mTagTypes.end()) { Events type = mTagTypes.at(mTag); if (type == Event_BrTag || type == Event_PTag) { if (!mIgnoreNewlineTags) { if (type == Event_BrTag) mBuffer.push_back('\n'); else { mBuffer.append("\n\n"); } } mIgnoreLineEndings = true; } else flushBuffer(); if (type == Event_ImgTag) { mIgnoreNewlineTags = false; } ++mIndex; return type; } } else { if (!mIgnoreLineEndings || ch != '\n') { if (mIndex < mPlainTextEnd) mBuffer.push_back(ch); mIgnoreLineEndings = false; mIgnoreNewlineTags = false; } } ++mIndex; } flushBuffer(); return Event_EOF; } void BookTextParser::flushBuffer() { mReadyText = mBuffer; mBuffer.clear(); } const BookTextParser::Attributes & BookTextParser::getAttributes() const { return mAttributes; } bool BookTextParser::isClosingTag() const { return mClosingTag; } void BookTextParser::parseTag(std::string tag) { size_t tagNameEndPos = tag.find(' '); mAttributes.clear(); mTag = tag.substr(0, tagNameEndPos); Misc::StringUtils::lowerCaseInPlace(mTag); if (mTag.empty()) return; mClosingTag = (mTag[0] == '/'); if (mClosingTag) { mTag.erase(mTag.begin()); return; } if (tagNameEndPos == std::string::npos) return; tag.erase(0, tagNameEndPos+1); while (!tag.empty()) { size_t sepPos = tag.find('='); if (sepPos == std::string::npos) return; std::string key = tag.substr(0, sepPos); Misc::StringUtils::lowerCaseInPlace(key); tag.erase(0, sepPos+1); std::string value; if (tag.empty()) return; if (tag[0] == '"') { size_t quoteEndPos = tag.find('"', 1); if (quoteEndPos == std::string::npos) throw std::runtime_error("BookTextParser Error: Missing end quote in tag"); value = tag.substr(1, quoteEndPos-1); tag.erase(0, quoteEndPos+2); } else { size_t valEndPos = tag.find(' '); if (valEndPos == std::string::npos) { value = tag; tag.erase(); } else { value = tag.substr(0, valEndPos); tag.erase(0, valEndPos+1); } } mAttributes[key] = value; } } /* BookFormatter */ Paginator::Pages BookFormatter::markupToWidget(MyGUI::Widget * parent, const std::string & markup, const int pageWidth, const int pageHeight) { Paginator pag(pageWidth, pageHeight); while (parent->getChildCount()) { MyGUI::Gui::getInstance().destroyWidget(parent->getChildAt(0)); } mTextStyle = TextStyle(); mBlockStyle = BlockStyle(); MyGUI::Widget * paper = parent->createWidget("Widget", MyGUI::IntCoord(0, 0, pag.getPageWidth(), pag.getPageHeight()), MyGUI::Align::Left | MyGUI::Align::Top); paper->setNeedMouseFocus(false); BookTextParser parser(markup); bool brBeforeLastTag = false; bool isPrevImg = false; for (;;) { BookTextParser::Events event = parser.next(); if (event == BookTextParser::Event_BrTag || event == BookTextParser::Event_PTag) continue; std::string plainText = parser.getReadyText(); // for cases when linebreaks are used to cause a shift to the next page // if the split text block ends in an empty line, proceeding text block(s) should have leading empty lines removed if (pag.getIgnoreLeadingEmptyLines()) { while (!plainText.empty()) { if (plainText[0] == '\n') plainText.erase(plainText.begin()); else { pag.setIgnoreLeadingEmptyLines(false); break; } } } if (plainText.empty()) brBeforeLastTag = true; else { // Each block of text (between two tags / boundary and tag) will be displayed in a separate editbox widget, // which means an additional linebreak will be created between them. // ^ This is not what vanilla MW assumes, so we must deal with line breaks around tags appropriately. bool brAtStart = (plainText[0] == '\n'); bool brAtEnd = (plainText[plainText.size()-1] == '\n'); if (brAtStart && !brBeforeLastTag && !isPrevImg) plainText.erase(plainText.begin()); if (plainText.size() && brAtEnd) plainText.erase(plainText.end()-1); if (!plainText.empty() || brBeforeLastTag || isPrevImg) { TextElement elem(paper, pag, mBlockStyle, mTextStyle, plainText); elem.paginate(); } brBeforeLastTag = brAtEnd; } if (event == BookTextParser::Event_EOF) break; isPrevImg = (event == BookTextParser::Event_ImgTag); switch (event) { case BookTextParser::Event_ImgTag: { const BookTextParser::Attributes & attr = parser.getAttributes(); if (attr.find("src") == attr.end() || attr.find("width") == attr.end() || attr.find("height") == attr.end()) continue; std::string src = attr.at("src"); int width = MyGUI::utility::parseInt(attr.at("width")); int height = MyGUI::utility::parseInt(attr.at("height")); bool exists; std::string correctedSrc = MWBase::Environment::get().getWindowManager()->correctBookartPath(src, width, height, &exists); if (!exists) { Log(Debug::Warning) << "Warning: Could not find \"" << src << "\" referenced by an tag."; break; } pag.setIgnoreLeadingEmptyLines(false); ImageElement elem(paper, pag, mBlockStyle, correctedSrc, width, height); elem.paginate(); break; } case BookTextParser::Event_FontTag: if (parser.isClosingTag()) resetFontProperties(); else handleFont(parser.getAttributes()); break; case BookTextParser::Event_DivTag: handleDiv(parser.getAttributes()); break; default: break; } } // insert last page if (pag.getStartTop() != pag.getCurrentTop()) pag << Paginator::Page(pag.getStartTop(), pag.getStartTop() + pag.getPageHeight()); paper->setSize(paper->getWidth(), pag.getCurrentTop()); return pag.getPages(); } Paginator::Pages BookFormatter::markupToWidget(MyGUI::Widget * parent, const std::string & markup) { return markupToWidget(parent, markup, parent->getWidth(), parent->getHeight()); } void BookFormatter::resetFontProperties() { mTextStyle = TextStyle(); } void BookFormatter::handleDiv(const BookTextParser::Attributes & attr) { if (attr.find("align") == attr.end()) return; std::string align = attr.at("align"); if (Misc::StringUtils::ciEqual(align, "center")) mBlockStyle.mAlign = MyGUI::Align::HCenter; else if (Misc::StringUtils::ciEqual(align, "left")) mBlockStyle.mAlign = MyGUI::Align::Left; else if (Misc::StringUtils::ciEqual(align, "right")) mBlockStyle.mAlign = MyGUI::Align::Right; } void BookFormatter::handleFont(const BookTextParser::Attributes & attr) { if (attr.find("color") != attr.end()) { unsigned int color; std::stringstream ss; ss << attr.at("color"); ss >> std::hex >> color; mTextStyle.mColour = MyGUI::Colour( (color>>16 & 0xFF) / 255.f, (color>>8 & 0xFF) / 255.f, (color & 0xFF) / 255.f); } if (attr.find("face") != attr.end()) { std::string face = attr.at("face"); mTextStyle.mFont = "Journalbook "+face; } if (attr.find("size") != attr.end()) { /// \todo } } /* GraphicElement */ GraphicElement::GraphicElement(MyGUI::Widget * parent, Paginator & pag, const BlockStyle & blockStyle) : mParent(parent), mPaginator(pag), mBlockStyle(blockStyle) { } void GraphicElement::paginate() { int newTop = mPaginator.getCurrentTop() + getHeight(); while (newTop-mPaginator.getStartTop() > mPaginator.getPageHeight()) { int newStartTop = pageSplit(); mPaginator << Paginator::Page(mPaginator.getStartTop(), newStartTop); mPaginator.setStartTop(newStartTop); } mPaginator.setCurrentTop(newTop); } int GraphicElement::pageSplit() { return mPaginator.getStartTop() + mPaginator.getPageHeight(); } /* TextElement */ TextElement::TextElement(MyGUI::Widget * parent, Paginator & pag, const BlockStyle & blockStyle, const TextStyle & textStyle, const std::string & text) : GraphicElement(parent, pag, blockStyle), mTextStyle(textStyle) { Gui::EditBox* box = parent->createWidget("NormalText", MyGUI::IntCoord(0, pag.getCurrentTop(), pag.getPageWidth(), 0), MyGUI::Align::Left | MyGUI::Align::Top, parent->getName() + MyGUI::utility::toString(parent->getChildCount())); box->setEditStatic(true); box->setEditMultiLine(true); box->setEditWordWrap(true); box->setNeedMouseFocus(false); box->setNeedKeyFocus(false); box->setMaxTextLength(text.size()); box->setTextAlign(mBlockStyle.mAlign); box->setTextColour(mTextStyle.mColour); box->setFontName(mTextStyle.mFont); box->setCaption(MyGUI::TextIterator::toTagsString(text)); box->setSize(box->getSize().width, box->getTextSize().height); mEditBox = box; } int TextElement::getHeight() { return mEditBox->getTextSize().height; } int TextElement::pageSplit() { // split lines const int lineHeight = MWBase::Environment::get().getWindowManager()->getFontHeight(); unsigned int lastLine = (mPaginator.getStartTop() + mPaginator.getPageHeight() - mPaginator.getCurrentTop()); if (lineHeight > 0) lastLine /= lineHeight; int ret = mPaginator.getCurrentTop() + lastLine * lineHeight; // first empty lines that would go to the next page should be ignored mPaginator.setIgnoreLeadingEmptyLines(true); const MyGUI::VectorLineInfo & lines = mEditBox->getSubWidgetText()->castType()->getLineInfo(); for (unsigned int i = lastLine; i < lines.size(); ++i) { if (lines[i].width == 0) ret += lineHeight; else { mPaginator.setIgnoreLeadingEmptyLines(false); break; } } return ret; } /* ImageElement */ ImageElement::ImageElement(MyGUI::Widget * parent, Paginator & pag, const BlockStyle & blockStyle, const std::string & src, int width, int height) : GraphicElement(parent, pag, blockStyle), mImageHeight(height) { int left = 0; if (mBlockStyle.mAlign.isHCenter()) left += (pag.getPageWidth() - width) / 2; else if (mBlockStyle.mAlign.isLeft()) left = 0; else if (mBlockStyle.mAlign.isRight()) left += pag.getPageWidth() - width; mImageBox = parent->createWidget ("ImageBox", MyGUI::IntCoord(left, pag.getCurrentTop(), width, mImageHeight), MyGUI::Align::Left | MyGUI::Align::Top, parent->getName() + MyGUI::utility::toString(parent->getChildCount())); mImageBox->setImageTexture(src); mImageBox->setProperty("NeedMouse", "false"); } int ImageElement::getHeight() { return mImageHeight; } int ImageElement::pageSplit() { // if the image is larger than the page, fall back to the default pageSplit implementation if (mImageHeight > mPaginator.getPageHeight()) return GraphicElement::pageSplit(); return mPaginator.getCurrentTop(); } } } ================================================ FILE: apps/openmw/mwgui/formatting.hpp ================================================ #ifndef MWGUI_FORMATTING_H #define MWGUI_FORMATTING_H #include #include #include namespace MWGui { namespace Formatting { struct TextStyle { TextStyle() : mColour(0,0,0) , mFont("Journalbook Magic Cards") , mTextSize(16) { } MyGUI::Colour mColour; std::string mFont; int mTextSize; }; struct BlockStyle { BlockStyle() : mAlign(MyGUI::Align::Left | MyGUI::Align::Top) { } MyGUI::Align mAlign; }; class BookTextParser { public: typedef std::map Attributes; enum Events { Event_None = -2, Event_EOF = -1, Event_BrTag, Event_PTag, Event_ImgTag, Event_DivTag, Event_FontTag }; BookTextParser(const std::string & text); Events next(); const Attributes & getAttributes() const; std::string getReadyText() const; bool isClosingTag() const; private: void registerTag(const std::string & tag, Events type); void flushBuffer(); void parseTag(std::string tag); size_t mIndex; std::string mText; std::string mReadyText; bool mIgnoreNewlineTags; bool mIgnoreLineEndings; Attributes mAttributes; std::string mTag; bool mClosingTag; std::map mTagTypes; std::string mBuffer; size_t mPlainTextEnd; }; class Paginator { public: typedef std::pair Page; typedef std::vector Pages; Paginator(int pageWidth, int pageHeight) : mStartTop(0), mCurrentTop(0), mPageWidth(pageWidth), mPageHeight(pageHeight), mIgnoreLeadingEmptyLines(false) { } int getStartTop() const { return mStartTop; } int getCurrentTop() const { return mCurrentTop; } int getPageWidth() const { return mPageWidth; } int getPageHeight() const { return mPageHeight; } bool getIgnoreLeadingEmptyLines() const { return mIgnoreLeadingEmptyLines; } Pages getPages() const { return mPages; } void setStartTop(int top) { mStartTop = top; } void setCurrentTop(int top) { mCurrentTop = top; } void setIgnoreLeadingEmptyLines(bool ignore) { mIgnoreLeadingEmptyLines = ignore; } Paginator & operator<<(const Page & page) { mPages.push_back(page); return *this; } private: int mStartTop, mCurrentTop; int mPageWidth, mPageHeight; bool mIgnoreLeadingEmptyLines; Pages mPages; }; /// \brief utilities for parsing book/scroll text as mygui widgets class BookFormatter { public: Paginator::Pages markupToWidget(MyGUI::Widget * parent, const std::string & markup, const int pageWidth, const int pageHeight); Paginator::Pages markupToWidget(MyGUI::Widget * parent, const std::string & markup); private: void resetFontProperties(); void handleDiv(const BookTextParser::Attributes & attr); void handleFont(const BookTextParser::Attributes & attr); TextStyle mTextStyle; BlockStyle mBlockStyle; }; class GraphicElement { public: GraphicElement(MyGUI::Widget * parent, Paginator & pag, const BlockStyle & blockStyle); virtual int getHeight() = 0; virtual void paginate(); virtual int pageSplit(); protected: virtual ~GraphicElement() {} MyGUI::Widget * mParent; Paginator & mPaginator; BlockStyle mBlockStyle; }; class TextElement : public GraphicElement { public: TextElement(MyGUI::Widget * parent, Paginator & pag, const BlockStyle & blockStyle, const TextStyle & textStyle, const std::string & text); int getHeight() override; int pageSplit() override; private: int currentFontHeight() const; TextStyle mTextStyle; Gui::EditBox * mEditBox; }; class ImageElement : public GraphicElement { public: ImageElement(MyGUI::Widget * parent, Paginator & pag, const BlockStyle & blockStyle, const std::string & src, int width, int height); int getHeight() override; int pageSplit() override; private: int mImageHeight; MyGUI::ImageBox * mImageBox; }; } } #endif ================================================ FILE: apps/openmw/mwgui/hud.cpp ================================================ #include "hud.hpp" #include #include #include #include #include #include /* Start of tes3mp addition Include additional headers for multiplayer purposes */ #include "../mwmp/Main.hpp" #include "../mwmp/Networking.hpp" #include "../mwmp/ObjectList.hpp" #include "../mwworld/cellstore.hpp" /* End of tes3mp addition */ #include #include #include "../mwbase/environment.hpp" #include "../mwbase/windowmanager.hpp" #include "../mwbase/world.hpp" #include "../mwworld/class.hpp" #include "../mwworld/esmstore.hpp" #include "../mwmechanics/creaturestats.hpp" #include "../mwmechanics/npcstats.hpp" #include "../mwmechanics/actorutil.hpp" #include "inventorywindow.hpp" #include "spellicons.hpp" #include "itemmodel.hpp" #include "draganddrop.hpp" #include "itemwidget.hpp" namespace MWGui { /** * Makes it possible to use ItemModel::moveItem to move an item from an inventory to the world. */ class WorldItemModel : public ItemModel { public: WorldItemModel(float left, float top) : mLeft(left), mTop(top) {} virtual ~WorldItemModel() override {} MWWorld::Ptr copyItem (const ItemStack& item, size_t count, bool /*allowAutoEquip*/) override { MWBase::World* world = MWBase::Environment::get().getWorld(); MWWorld::Ptr dropped; if (world->canPlaceObject(mLeft, mTop)) dropped = world->placeObject(item.mBase, mLeft, mTop, count); else dropped = world->dropObjectOnGround(world->getPlayerPtr(), item.mBase, count); dropped.getCellRef().setOwner(""); /* Start of tes3mp addition Send an ID_OBJECT_PLACE packet every time an object is dropped into the world from the inventory screen */ mwmp::ObjectList *objectList = mwmp::Main::get().getNetworking()->getObjectList(); objectList->reset(); objectList->packetOrigin = mwmp::CLIENT_GAMEPLAY; objectList->addObjectPlace(dropped, true); objectList->sendObjectPlace(); /* End of tes3mp addition */ /* Start of tes3mp change (major) Instead of actually keeping this object as is, delete it after sending the packet and wait for the server to send it back with a unique mpNum of its own */ MWBase::Environment::get().getWorld()->deleteObject(dropped); /* End of tes3mp change (major) */ return dropped; } void removeItem (const ItemStack& item, size_t count) override { throw std::runtime_error("removeItem not implemented"); } ModelIndex getIndex (ItemStack item) override { throw std::runtime_error("getIndex not implemented"); } void update() override {} size_t getItemCount() override { return 0; } ItemStack getItem (ModelIndex index) override { throw std::runtime_error("getItem not implemented"); } private: // Where to drop the item float mLeft; float mTop; }; HUD::HUD(CustomMarkerCollection &customMarkers, DragAndDrop* dragAndDrop, MWRender::LocalMap* localMapRender) : WindowBase("openmw_hud.layout") , LocalMapBase(customMarkers, localMapRender, Settings::Manager::getBool("local map hud fog of war", "Map")) , mHealth(nullptr) , mMagicka(nullptr) , mStamina(nullptr) , mDrowning(nullptr) , mWeapImage(nullptr) , mSpellImage(nullptr) , mWeapStatus(nullptr) , mSpellStatus(nullptr) , mEffectBox(nullptr) , mMinimap(nullptr) , mCrosshair(nullptr) , mCellNameBox(nullptr) , mDrowningFrame(nullptr) , mDrowningFlash(nullptr) , mHealthManaStaminaBaseLeft(0) , mWeapBoxBaseLeft(0) , mSpellBoxBaseLeft(0) , mMinimapBoxBaseRight(0) , mEffectBoxBaseRight(0) , mDragAndDrop(dragAndDrop) , mCellNameTimer(0.0f) , mWeaponSpellTimer(0.f) , mMapVisible(true) , mWeaponVisible(true) , mSpellVisible(true) , mWorldMouseOver(false) , mEnemyActorId(-1) , mEnemyHealthTimer(-1) , mIsDrowning(false) , mDrowningFlashTheta(0.f) { mMainWidget->setSize(MyGUI::RenderManager::getInstance().getViewSize()); // Energy bars getWidget(mHealthFrame, "HealthFrame"); getWidget(mHealth, "Health"); getWidget(mMagicka, "Magicka"); getWidget(mStamina, "Stamina"); getWidget(mEnemyHealth, "EnemyHealth"); mHealthManaStaminaBaseLeft = mHealthFrame->getLeft(); MyGUI::Widget *healthFrame, *magickaFrame, *fatigueFrame; getWidget(healthFrame, "HealthFrame"); getWidget(magickaFrame, "MagickaFrame"); getWidget(fatigueFrame, "FatigueFrame"); healthFrame->eventMouseButtonClick += MyGUI::newDelegate(this, &HUD::onHMSClicked); magickaFrame->eventMouseButtonClick += MyGUI::newDelegate(this, &HUD::onHMSClicked); fatigueFrame->eventMouseButtonClick += MyGUI::newDelegate(this, &HUD::onHMSClicked); //Drowning bar getWidget(mDrowningFrame, "DrowningFrame"); getWidget(mDrowning, "Drowning"); getWidget(mDrowningFlash, "Flash"); mDrowning->setProgressRange(200); const MyGUI::IntSize& viewSize = MyGUI::RenderManager::getInstance().getViewSize(); // Item and spell images and status bars getWidget(mWeapBox, "WeapBox"); getWidget(mWeapImage, "WeapImage"); getWidget(mWeapStatus, "WeapStatus"); mWeapBoxBaseLeft = mWeapBox->getLeft(); mWeapBox->eventMouseButtonClick += MyGUI::newDelegate(this, &HUD::onWeaponClicked); getWidget(mSpellBox, "SpellBox"); getWidget(mSpellImage, "SpellImage"); getWidget(mSpellStatus, "SpellStatus"); mSpellBoxBaseLeft = mSpellBox->getLeft(); mSpellBox->eventMouseButtonClick += MyGUI::newDelegate(this, &HUD::onMagicClicked); getWidget(mSneakBox, "SneakBox"); mSneakBoxBaseLeft = mSneakBox->getLeft(); getWidget(mEffectBox, "EffectBox"); mEffectBoxBaseRight = viewSize.width - mEffectBox->getRight(); getWidget(mMinimapBox, "MiniMapBox"); mMinimapBoxBaseRight = viewSize.width - mMinimapBox->getRight(); getWidget(mMinimap, "MiniMap"); getWidget(mCompass, "Compass"); getWidget(mMinimapButton, "MiniMapButton"); mMinimapButton->eventMouseButtonClick += MyGUI::newDelegate(this, &HUD::onMapClicked); getWidget(mCellNameBox, "CellName"); getWidget(mWeaponSpellBox, "WeaponSpellName"); getWidget(mCrosshair, "Crosshair"); LocalMapBase::init(mMinimap, mCompass); mMainWidget->eventMouseButtonClick += MyGUI::newDelegate(this, &HUD::onWorldClicked); mMainWidget->eventMouseMove += MyGUI::newDelegate(this, &HUD::onWorldMouseOver); mMainWidget->eventMouseLostFocus += MyGUI::newDelegate(this, &HUD::onWorldMouseLostFocus); mSpellIcons = new SpellIcons(); } HUD::~HUD() { mMainWidget->eventMouseLostFocus.clear(); mMainWidget->eventMouseMove.clear(); mMainWidget->eventMouseButtonClick.clear(); delete mSpellIcons; } void HUD::setValue(const std::string& id, const MWMechanics::DynamicStat& value) { int current = static_cast(value.getCurrent()); int modified = static_cast(value.getModified()); // Fatigue can be negative if (id != "FBar") current = std::max(0, current); MyGUI::Widget* w; std::string valStr = MyGUI::utility::toString(current) + " / " + MyGUI::utility::toString(modified); if (id == "HBar") { mHealth->setProgressRange(std::max(0, modified)); mHealth->setProgressPosition(std::max(0, current)); getWidget(w, "HealthFrame"); w->setUserString("Caption_HealthDescription", "#{sHealthDesc}\n" + valStr); } else if (id == "MBar") { mMagicka->setProgressRange(std::max(0, modified)); mMagicka->setProgressPosition(std::max(0, current)); getWidget(w, "MagickaFrame"); w->setUserString("Caption_HealthDescription", "#{sMagDesc}\n" + valStr); } else if (id == "FBar") { mStamina->setProgressRange(std::max(0, modified)); mStamina->setProgressPosition(std::max(0, current)); getWidget(w, "FatigueFrame"); w->setUserString("Caption_HealthDescription", "#{sFatDesc}\n" + valStr); } } void HUD::setDrowningTimeLeft(float time, float maxTime) { size_t progress = static_cast(time / maxTime * 200); mDrowning->setProgressPosition(progress); bool isDrowning = (progress == 0); if (isDrowning && !mIsDrowning) // Just started drowning mDrowningFlashTheta = 0.0f; // Start out on bright red every time. mDrowningFlash->setVisible(isDrowning); mIsDrowning = isDrowning; } void HUD::setDrowningBarVisible(bool visible) { mDrowningFrame->setVisible(visible); } void HUD::onWorldClicked(MyGUI::Widget* _sender) { if (!MWBase::Environment::get().getWindowManager ()->isGuiMode ()) return; MWBase::WindowManager *winMgr = MWBase::Environment::get().getWindowManager(); if (mDragAndDrop->mIsOnDragAndDrop) { // drop item into the gameworld MWBase::Environment::get().getWorld()->breakInvisibility( MWMechanics::getPlayer()); MyGUI::IntSize viewSize = MyGUI::RenderManager::getInstance().getViewSize(); MyGUI::IntPoint cursorPosition = MyGUI::InputManager::getInstance().getMousePosition(); float mouseX = cursorPosition.left / float(viewSize.width); float mouseY = cursorPosition.top / float(viewSize.height); WorldItemModel drop (mouseX, mouseY); mDragAndDrop->drop(&drop, nullptr); winMgr->changePointer("arrow"); } else { GuiMode mode = winMgr->getMode(); if (!winMgr->isConsoleMode() && (mode != GM_Container) && (mode != GM_Inventory)) return; MWWorld::Ptr object = MWBase::Environment::get().getWorld()->getFacedObject(); if (winMgr->isConsoleMode()) winMgr->setConsoleSelectedObject(object); else //if ((mode == GM_Container) || (mode == GM_Inventory)) { // pick up object if (!object.isEmpty()) /* Start of tes3mp change (major) Disable unilateral picking up of objects on this client Instead, send an ID_OBJECT_ACTIVATE packet every time an item is made to pick up an item here, and expect the server's reply to our packet to cause the actual picking up of items */ //winMgr->getInventoryWindow()->pickUpObject(object); { mwmp::ObjectList *objectList = mwmp::Main::get().getNetworking()->getObjectList(); objectList->reset(); objectList->packetOrigin = mwmp::CLIENT_GAMEPLAY; objectList->addObjectActivate(object, MWMechanics::getPlayer()); objectList->sendObjectActivate(); } /* End of tes3mp change (major) */ } } } void HUD::onWorldMouseOver(MyGUI::Widget* _sender, int x, int y) { if (mDragAndDrop->mIsOnDragAndDrop) { mWorldMouseOver = false; MyGUI::IntSize viewSize = MyGUI::RenderManager::getInstance().getViewSize(); MyGUI::IntPoint cursorPosition = MyGUI::InputManager::getInstance().getMousePosition(); float mouseX = cursorPosition.left / float(viewSize.width); float mouseY = cursorPosition.top / float(viewSize.height); MWBase::World* world = MWBase::Environment::get().getWorld(); // if we can't drop the object at the wanted position, show the "drop on ground" cursor. bool canDrop = world->canPlaceObject(mouseX, mouseY); if (!canDrop) MWBase::Environment::get().getWindowManager()->changePointer("drop_ground"); else MWBase::Environment::get().getWindowManager()->changePointer("arrow"); } else { MWBase::Environment::get().getWindowManager()->changePointer("arrow"); mWorldMouseOver = true; } } void HUD::onWorldMouseLostFocus(MyGUI::Widget* _sender, MyGUI::Widget* _new) { MWBase::Environment::get().getWindowManager()->changePointer("arrow"); mWorldMouseOver = false; } void HUD::onHMSClicked(MyGUI::Widget* _sender) { MWBase::Environment::get().getWindowManager()->toggleVisible(GW_Stats); } void HUD::onMapClicked(MyGUI::Widget* _sender) { MWBase::Environment::get().getWindowManager()->toggleVisible(GW_Map); } void HUD::onWeaponClicked(MyGUI::Widget* _sender) { const MWWorld::Ptr& player = MWMechanics::getPlayer(); if (player.getClass().getNpcStats(player).isWerewolf()) { MWBase::Environment::get().getWindowManager()->messageBox("#{sWerewolfRefusal}"); return; } MWBase::Environment::get().getWindowManager()->toggleVisible(GW_Inventory); } void HUD::onMagicClicked(MyGUI::Widget* _sender) { const MWWorld::Ptr& player = MWMechanics::getPlayer(); if (player.getClass().getNpcStats(player).isWerewolf()) { MWBase::Environment::get().getWindowManager()->messageBox("#{sWerewolfRefusal}"); return; } MWBase::Environment::get().getWindowManager()->toggleVisible(GW_Magic); } void HUD::setCellName(const std::string& cellName) { if (mCellName != cellName) { mCellNameTimer = 5.0f; mCellName = cellName; mCellNameBox->setCaptionWithReplacing("#{sCell=" + mCellName + "}"); mCellNameBox->setVisible(mMapVisible); } } void HUD::onFrame(float dt) { LocalMapBase::onFrame(dt); mCellNameTimer -= dt; mWeaponSpellTimer -= dt; if (mCellNameTimer < 0) mCellNameBox->setVisible(false); if (mWeaponSpellTimer < 0) mWeaponSpellBox->setVisible(false); mEnemyHealthTimer -= dt; if (mEnemyHealth->getVisible() && mEnemyHealthTimer < 0) { mEnemyHealth->setVisible(false); mWeaponSpellBox->setPosition(mWeaponSpellBox->getPosition() + MyGUI::IntPoint(0,20)); } if (mIsDrowning) mDrowningFlashTheta += dt * osg::PI*2; mSpellIcons->updateWidgets(mEffectBox, true); if (mEnemyActorId != -1 && mEnemyHealth->getVisible()) { updateEnemyHealthBar(); } if (mIsDrowning) { float intensity = (cos(mDrowningFlashTheta) + 2.0f) / 3.0f; mDrowningFlash->setAlpha(intensity); } } void HUD::setSelectedSpell(const std::string& spellId, int successChancePercent) { const ESM::Spell* spell = MWBase::Environment::get().getWorld()->getStore().get().find(spellId); std::string spellName = spell->mName; if (spellName != mSpellName && mSpellVisible) { mWeaponSpellTimer = 5.0f; mSpellName = spellName; mWeaponSpellBox->setCaption(mSpellName); mWeaponSpellBox->setVisible(true); } mSpellStatus->setProgressRange(100); mSpellStatus->setProgressPosition(successChancePercent); mSpellBox->setUserString("ToolTipType", "Spell"); mSpellBox->setUserString("Spell", spellId); // use the icon of the first effect const ESM::MagicEffect* effect = MWBase::Environment::get().getWorld()->getStore().get().find(spell->mEffects.mList.front().mEffectID); std::string icon = effect->mIcon; int slashPos = icon.rfind('\\'); icon.insert(slashPos+1, "b_"); icon = MWBase::Environment::get().getWindowManager()->correctIconPath(icon); mSpellImage->setSpellIcon(icon); } void HUD::setSelectedEnchantItem(const MWWorld::Ptr& item, int chargePercent) { std::string itemName = item.getClass().getName(item); if (itemName != mSpellName && mSpellVisible) { mWeaponSpellTimer = 5.0f; mSpellName = itemName; mWeaponSpellBox->setCaption(mSpellName); mWeaponSpellBox->setVisible(true); } mSpellStatus->setProgressRange(100); mSpellStatus->setProgressPosition(chargePercent); mSpellBox->setUserString("ToolTipType", "ItemPtr"); mSpellBox->setUserData(MWWorld::Ptr(item)); mSpellImage->setItem(item); } void HUD::setSelectedWeapon(const MWWorld::Ptr& item, int durabilityPercent) { std::string itemName = item.getClass().getName(item); if (itemName != mWeaponName && mWeaponVisible) { mWeaponSpellTimer = 5.0f; mWeaponName = itemName; mWeaponSpellBox->setCaption(mWeaponName); mWeaponSpellBox->setVisible(true); } mWeapBox->clearUserStrings(); mWeapBox->setUserString("ToolTipType", "ItemPtr"); mWeapBox->setUserData(MWWorld::Ptr(item)); mWeapStatus->setProgressRange(100); mWeapStatus->setProgressPosition(durabilityPercent); mWeapImage->setItem(item); } void HUD::unsetSelectedSpell() { std::string spellName = "#{sNone}"; if (spellName != mSpellName && mSpellVisible) { mWeaponSpellTimer = 5.0f; mSpellName = spellName; mWeaponSpellBox->setCaptionWithReplacing(mSpellName); mWeaponSpellBox->setVisible(true); } mSpellStatus->setProgressRange(100); mSpellStatus->setProgressPosition(0); mSpellImage->setItem(MWWorld::Ptr()); mSpellBox->clearUserStrings(); } void HUD::unsetSelectedWeapon() { std::string itemName = "#{sSkillHandtohand}"; if (itemName != mWeaponName && mWeaponVisible) { mWeaponSpellTimer = 5.0f; mWeaponName = itemName; mWeaponSpellBox->setCaptionWithReplacing(mWeaponName); mWeaponSpellBox->setVisible(true); } mWeapStatus->setProgressRange(100); mWeapStatus->setProgressPosition(0); MWBase::World *world = MWBase::Environment::get().getWorld(); MWWorld::Ptr player = world->getPlayerPtr(); mWeapImage->setItem(MWWorld::Ptr()); std::string icon = (player.getClass().getNpcStats(player).isWerewolf()) ? "icons\\k\\tx_werewolf_hand.dds" : "icons\\k\\stealth_handtohand.dds"; mWeapImage->setIcon(icon); mWeapBox->clearUserStrings(); mWeapBox->setUserString("ToolTipType", "Layout"); mWeapBox->setUserString("ToolTipLayout", "HandToHandToolTip"); mWeapBox->setUserString("Caption_HandToHandText", itemName); mWeapBox->setUserString("ImageTexture_HandToHandImage", icon); } void HUD::setCrosshairVisible(bool visible) { mCrosshair->setVisible (visible); } void HUD::setCrosshairOwned(bool owned) { if(owned) { mCrosshair->changeWidgetSkin("HUD_Crosshair_Owned"); } else { mCrosshair->changeWidgetSkin("HUD_Crosshair"); } } void HUD::setHmsVisible(bool visible) { mHealth->setVisible(visible); mMagicka->setVisible(visible); mStamina->setVisible(visible); updatePositions(); } void HUD::setWeapVisible(bool visible) { mWeapBox->setVisible(visible); updatePositions(); } void HUD::setSpellVisible(bool visible) { mSpellBox->setVisible(visible); updatePositions(); } void HUD::setSneakVisible(bool visible) { mSneakBox->setVisible(visible); updatePositions(); } void HUD::setEffectVisible(bool visible) { mEffectBox->setVisible (visible); updatePositions(); } void HUD::setMinimapVisible(bool visible) { mMinimapBox->setVisible (visible); updatePositions(); } void HUD::updatePositions() { int weapDx = 0, spellDx = 0, sneakDx = 0; if (!mHealth->getVisible()) sneakDx = spellDx = weapDx = mWeapBoxBaseLeft - mHealthManaStaminaBaseLeft; if (!mWeapBox->getVisible()) { spellDx += mSpellBoxBaseLeft - mWeapBoxBaseLeft; sneakDx = spellDx; } if (!mSpellBox->getVisible()) sneakDx += mSneakBoxBaseLeft - mSpellBoxBaseLeft; mWeaponVisible = mWeapBox->getVisible(); mSpellVisible = mSpellBox->getVisible(); if (!mWeaponVisible && !mSpellVisible) mWeaponSpellBox->setVisible(false); mWeapBox->setPosition(mWeapBoxBaseLeft - weapDx, mWeapBox->getTop()); mSpellBox->setPosition(mSpellBoxBaseLeft - spellDx, mSpellBox->getTop()); mSneakBox->setPosition(mSneakBoxBaseLeft - sneakDx, mSneakBox->getTop()); const MyGUI::IntSize& viewSize = MyGUI::RenderManager::getInstance().getViewSize(); // effect box can have variable width -> variable left coordinate int effectsDx = 0; if (!mMinimapBox->getVisible ()) effectsDx = mEffectBoxBaseRight - mMinimapBoxBaseRight; mMapVisible = mMinimapBox->getVisible (); if (!mMapVisible) mCellNameBox->setVisible(false); mEffectBox->setPosition((viewSize.width - mEffectBoxBaseRight) - mEffectBox->getWidth() + effectsDx, mEffectBox->getTop()); } void HUD::updateEnemyHealthBar() { MWWorld::Ptr enemy = MWBase::Environment::get().getWorld()->searchPtrViaActorId(mEnemyActorId); if (enemy.isEmpty()) return; MWMechanics::CreatureStats& stats = enemy.getClass().getCreatureStats(enemy); mEnemyHealth->setProgressRange(100); // Health is usually cast to int before displaying. Actors die whenever they are < 1 health. // Therefore any value < 1 should show as an empty health bar. We do the same in statswindow :) mEnemyHealth->setProgressPosition(static_cast(stats.getHealth().getCurrent() / stats.getHealth().getModified() * 100)); static const float fNPCHealthBarFade = MWBase::Environment::get().getWorld()->getStore().get().find("fNPCHealthBarFade")->mValue.getFloat(); if (fNPCHealthBarFade > 0.f) mEnemyHealth->setAlpha(std::max(0.f, std::min(1.f, mEnemyHealthTimer/fNPCHealthBarFade))); } void HUD::setEnemy(const MWWorld::Ptr &enemy) { mEnemyActorId = enemy.getClass().getCreatureStats(enemy).getActorId(); mEnemyHealthTimer = MWBase::Environment::get().getWorld()->getStore().get().find("fNPCHealthBarTime")->mValue.getFloat(); if (!mEnemyHealth->getVisible()) mWeaponSpellBox->setPosition(mWeaponSpellBox->getPosition() - MyGUI::IntPoint(0,20)); mEnemyHealth->setVisible(true); updateEnemyHealthBar(); } void HUD::resetEnemy() { mEnemyActorId = -1; mEnemyHealthTimer = -1; } void HUD::clear() { unsetSelectedSpell(); unsetSelectedWeapon(); resetEnemy(); } void HUD::customMarkerCreated(MyGUI::Widget *marker) { marker->eventMouseButtonClick += MyGUI::newDelegate(this, &HUD::onMapClicked); } void HUD::doorMarkerCreated(MyGUI::Widget *marker) { marker->eventMouseButtonClick += MyGUI::newDelegate(this, &HUD::onMapClicked); } } ================================================ FILE: apps/openmw/mwgui/hud.hpp ================================================ #ifndef OPENMW_GAME_MWGUI_HUD_H #define OPENMW_GAME_MWGUI_HUD_H #include "mapwindow.hpp" #include "statswatcher.hpp" namespace MWWorld { class Ptr; } namespace MWGui { class DragAndDrop; class SpellIcons; class ItemWidget; class SpellWidget; class HUD : public WindowBase, public LocalMapBase, public StatsListener { public: HUD(CustomMarkerCollection& customMarkers, DragAndDrop* dragAndDrop, MWRender::LocalMap* localMapRender); virtual ~HUD(); void setValue (const std::string& id, const MWMechanics::DynamicStat& value) override; /// Set time left for the player to start drowning /// @param time time left to start drowning /// @param maxTime how long we can be underwater (in total) until drowning starts void setDrowningTimeLeft(float time, float maxTime); void setDrowningBarVisible(bool visible); void setHmsVisible(bool visible); void setWeapVisible(bool visible); void setSpellVisible(bool visible); void setSneakVisible(bool visible); void setEffectVisible(bool visible); void setMinimapVisible(bool visible); void setSelectedSpell(const std::string& spellId, int successChancePercent); void setSelectedEnchantItem(const MWWorld::Ptr& item, int chargePercent); const MWWorld::Ptr& getSelectedEnchantItem(); void setSelectedWeapon(const MWWorld::Ptr& item, int durabilityPercent); void unsetSelectedSpell(); void unsetSelectedWeapon(); void setCrosshairVisible(bool visible); void setCrosshairOwned(bool owned); void onFrame(float dt) override; void setCellName(const std::string& cellName); bool getWorldMouseOver() { return mWorldMouseOver; } MyGUI::Widget* getEffectBox() { return mEffectBox; } void setEnemy(const MWWorld::Ptr& enemy); void resetEnemy(); void clear() override; private: MyGUI::ProgressBar *mHealth, *mMagicka, *mStamina, *mEnemyHealth, *mDrowning; MyGUI::Widget* mHealthFrame; MyGUI::Widget *mWeapBox, *mSpellBox, *mSneakBox; ItemWidget *mWeapImage; SpellWidget *mSpellImage; MyGUI::ProgressBar *mWeapStatus, *mSpellStatus; MyGUI::Widget *mEffectBox, *mMinimapBox; MyGUI::Button* mMinimapButton; MyGUI::ScrollView* mMinimap; MyGUI::ImageBox* mCrosshair; MyGUI::TextBox* mCellNameBox; MyGUI::TextBox* mWeaponSpellBox; MyGUI::Widget *mDrowningFrame, *mDrowningFlash; // bottom left elements int mHealthManaStaminaBaseLeft, mWeapBoxBaseLeft, mSpellBoxBaseLeft, mSneakBoxBaseLeft; // bottom right elements int mMinimapBoxBaseRight, mEffectBoxBaseRight; DragAndDrop* mDragAndDrop; std::string mCellName; float mCellNameTimer; std::string mWeaponName; std::string mSpellName; float mWeaponSpellTimer; bool mMapVisible; bool mWeaponVisible; bool mSpellVisible; bool mWorldMouseOver; SpellIcons* mSpellIcons; int mEnemyActorId; float mEnemyHealthTimer; bool mIsDrowning; float mDrowningFlashTheta; void onWorldClicked(MyGUI::Widget* _sender); void onWorldMouseOver(MyGUI::Widget* _sender, int x, int y); void onWorldMouseLostFocus(MyGUI::Widget* _sender, MyGUI::Widget* _new); void onHMSClicked(MyGUI::Widget* _sender); void onWeaponClicked(MyGUI::Widget* _sender); void onMagicClicked(MyGUI::Widget* _sender); void onMapClicked(MyGUI::Widget* _sender); // LocalMapBase void customMarkerCreated(MyGUI::Widget* marker) override; void doorMarkerCreated(MyGUI::Widget* marker) override; void updateEnemyHealthBar(); void updatePositions(); }; } #endif ================================================ FILE: apps/openmw/mwgui/inventoryitemmodel.cpp ================================================ #include "inventoryitemmodel.hpp" #include #include "../mwmechanics/actorutil.hpp" #include "../mwmechanics/npcstats.hpp" #include "../mwworld/containerstore.hpp" #include "../mwworld/class.hpp" #include "../mwworld/inventorystore.hpp" #include "../mwbase/environment.hpp" #include "../mwbase/mechanicsmanager.hpp" namespace MWGui { InventoryItemModel::InventoryItemModel(const MWWorld::Ptr &actor) : mActor(actor) { } ItemStack InventoryItemModel::getItem (ModelIndex index) { if (index < 0) throw std::runtime_error("Invalid index supplied"); if (mItems.size() <= static_cast(index)) throw std::runtime_error("Item index out of range"); return mItems[index]; } size_t InventoryItemModel::getItemCount() { return mItems.size(); } ItemModel::ModelIndex InventoryItemModel::getIndex (ItemStack item) { size_t i = 0; for (ItemStack& itemStack : mItems) { if (itemStack == item) return i; ++i; } return -1; } MWWorld::Ptr InventoryItemModel::copyItem (const ItemStack& item, size_t count, bool allowAutoEquip) { if (item.mBase.getContainerStore() == &mActor.getClass().getContainerStore(mActor)) throw std::runtime_error("Item to copy needs to be from a different container!"); return *mActor.getClass().getContainerStore(mActor).add(item.mBase, count, mActor, allowAutoEquip); } void InventoryItemModel::removeItem (const ItemStack& item, size_t count) { int removed = 0; // Re-equipping makes sense only if a target has inventory if (mActor.getClass().hasInventoryStore(mActor)) { MWWorld::InventoryStore& store = mActor.getClass().getInventoryStore(mActor); removed = store.remove(item.mBase, count, mActor, true); } else { MWWorld::ContainerStore& store = mActor.getClass().getContainerStore(mActor); removed = store.remove(item.mBase, count, mActor); } std::stringstream error; if (removed == 0) { error << "Item '" << item.mBase.getCellRef().getRefId() << "' was not found in container store to remove"; throw std::runtime_error(error.str()); } else if (removed < static_cast(count)) { error << "Not enough items '" << item.mBase.getCellRef().getRefId() << "' in the stack to remove (" << static_cast(count) << " requested, " << removed << " found)"; throw std::runtime_error(error.str()); } } MWWorld::Ptr InventoryItemModel::moveItem(const ItemStack &item, size_t count, ItemModel *otherModel) { // Can't move conjured items: This is a general fix that also takes care of issues with taking conjured items via the 'Take All' button. if (item.mFlags & ItemStack::Flag_Bound) return MWWorld::Ptr(); MWWorld::Ptr ret = otherModel->copyItem(item, count); removeItem(item, count); return ret; } void InventoryItemModel::update() { MWWorld::ContainerStore& store = mActor.getClass().getContainerStore(mActor); mItems.clear(); for (MWWorld::ContainerStoreIterator it = store.begin(); it != store.end(); ++it) { MWWorld::Ptr item = *it; if (!item.getClass().showsInInventory(item)) continue; ItemStack newItem (item, this, item.getRefData().getCount()); if (mActor.getClass().hasInventoryStore(mActor)) { MWWorld::InventoryStore& invStore = mActor.getClass().getInventoryStore(mActor); if (invStore.isEquipped(newItem.mBase)) newItem.mType = ItemStack::Type_Equipped; } mItems.push_back(newItem); } } bool InventoryItemModel::onTakeItem(const MWWorld::Ptr &item, int count) { // Looting a dead corpse is considered OK if (mActor.getClass().isActor() && mActor.getClass().getCreatureStats(mActor).isDead()) return true; MWWorld::Ptr player = MWMechanics::getPlayer(); MWBase::Environment::get().getMechanicsManager()->itemTaken(player, item, mActor, count); return true; } } ================================================ FILE: apps/openmw/mwgui/inventoryitemmodel.hpp ================================================ #ifndef MWGUI_INVENTORY_ITEM_MODEL_H #define MWGUI_INVENTORY_ITEM_MODEL_H #include "itemmodel.hpp" namespace MWGui { class InventoryItemModel : public ItemModel { public: InventoryItemModel (const MWWorld::Ptr& actor); ItemStack getItem (ModelIndex index) override; ModelIndex getIndex (ItemStack item) override; size_t getItemCount() override; bool onTakeItem(const MWWorld::Ptr &item, int count) override; MWWorld::Ptr copyItem (const ItemStack& item, size_t count, bool allowAutoEquip = true) override; void removeItem (const ItemStack& item, size_t count) override; /// Move items from this model to \a otherModel. MWWorld::Ptr moveItem (const ItemStack& item, size_t count, ItemModel* otherModel) override; void update() override; protected: MWWorld::Ptr mActor; private: std::vector mItems; }; } #endif ================================================ FILE: apps/openmw/mwgui/inventorywindow.cpp ================================================ #include "inventorywindow.hpp" #include #include #include #include #include #include #include #include #include #include #include #include /* Start of tes3mp addition Include additional headers for multiplayer purposes */ #include "../mwmp/Main.hpp" #include "../mwmp/Networking.hpp" #include "../mwmp/ObjectList.hpp" #include "../mwmp/LocalPlayer.hpp" #include "../mwworld/cellstore.hpp" /* End of tes3mp addition */ #include "../mwbase/world.hpp" #include "../mwbase/environment.hpp" #include "../mwbase/windowmanager.hpp" #include "../mwbase/mechanicsmanager.hpp" #include "../mwworld/inventorystore.hpp" #include "../mwworld/class.hpp" #include "../mwworld/actionequip.hpp" #include "../mwmechanics/actorutil.hpp" #include "../mwmechanics/creaturestats.hpp" #include "itemview.hpp" #include "inventoryitemmodel.hpp" #include "sortfilteritemmodel.hpp" #include "tradeitemmodel.hpp" #include "countdialog.hpp" #include "tradewindow.hpp" #include "draganddrop.hpp" #include "widgets.hpp" #include "tooltips.hpp" namespace { bool isRightHandWeapon(const MWWorld::Ptr& item) { if (item.getClass().getTypeName() != typeid(ESM::Weapon).name()) return false; std::vector equipmentSlots = item.getClass().getEquipmentSlots(item).first; return (!equipmentSlots.empty() && equipmentSlots.front() == MWWorld::InventoryStore::Slot_CarriedRight); } } namespace MWGui { InventoryWindow::InventoryWindow(DragAndDrop* dragAndDrop, osg::Group* parent, Resource::ResourceSystem* resourceSystem) : WindowPinnableBase("openmw_inventory_window.layout") , mDragAndDrop(dragAndDrop) , mSelectedItem(-1) , mSortModel(nullptr) , mTradeModel(nullptr) , mGuiMode(GM_Inventory) , mLastXSize(0) , mLastYSize(0) , mPreview(new MWRender::InventoryPreview(parent, resourceSystem, MWMechanics::getPlayer())) , mTrading(false) , mUpdateTimer(0.f) { mPreviewTexture.reset(new osgMyGUI::OSGTexture(mPreview->getTexture())); mPreview->rebuild(); mMainWidget->castType()->eventWindowChangeCoord += MyGUI::newDelegate(this, &InventoryWindow::onWindowResize); getWidget(mAvatar, "Avatar"); getWidget(mAvatarImage, "AvatarImage"); getWidget(mEncumbranceBar, "EncumbranceBar"); getWidget(mFilterAll, "AllButton"); getWidget(mFilterWeapon, "WeaponButton"); getWidget(mFilterApparel, "ApparelButton"); getWidget(mFilterMagic, "MagicButton"); getWidget(mFilterMisc, "MiscButton"); getWidget(mLeftPane, "LeftPane"); getWidget(mRightPane, "RightPane"); getWidget(mArmorRating, "ArmorRating"); getWidget(mFilterEdit, "FilterEdit"); mAvatarImage->eventMouseButtonClick += MyGUI::newDelegate(this, &InventoryWindow::onAvatarClicked); mAvatarImage->setRenderItemTexture(mPreviewTexture.get()); mAvatarImage->getSubWidgetMain()->_setUVSet(MyGUI::FloatRect(0.f, 0.f, 1.f, 1.f)); getWidget(mItemView, "ItemView"); mItemView->eventItemClicked += MyGUI::newDelegate(this, &InventoryWindow::onItemSelected); mItemView->eventBackgroundClicked += MyGUI::newDelegate(this, &InventoryWindow::onBackgroundSelected); mFilterAll->eventMouseButtonClick += MyGUI::newDelegate(this, &InventoryWindow::onFilterChanged); mFilterWeapon->eventMouseButtonClick += MyGUI::newDelegate(this, &InventoryWindow::onFilterChanged); mFilterApparel->eventMouseButtonClick += MyGUI::newDelegate(this, &InventoryWindow::onFilterChanged); mFilterMagic->eventMouseButtonClick += MyGUI::newDelegate(this, &InventoryWindow::onFilterChanged); mFilterMisc->eventMouseButtonClick += MyGUI::newDelegate(this, &InventoryWindow::onFilterChanged); mFilterEdit->eventEditTextChange += MyGUI::newDelegate(this, &InventoryWindow::onNameFilterChanged); mFilterAll->setStateSelected(true); setGuiMode(mGuiMode); adjustPanes(); } void InventoryWindow::adjustPanes() { const float aspect = 0.5; // fixed aspect ratio for the avatar image int leftPaneWidth = static_cast((mMainWidget->getSize().height - 44 - mArmorRating->getHeight()) * aspect); mLeftPane->setSize( leftPaneWidth, mMainWidget->getSize().height-44 ); mRightPane->setCoord( mLeftPane->getPosition().left + leftPaneWidth + 4, mRightPane->getPosition().top, mMainWidget->getSize().width - 12 - leftPaneWidth - 15, mMainWidget->getSize().height-44 ); } void InventoryWindow::updatePlayer() { mPtr = MWBase::Environment::get().getWorld ()->getPlayerPtr(); mTradeModel = new TradeItemModel(new InventoryItemModel(mPtr), MWWorld::Ptr()); if (mSortModel) // reuse existing SortModel when possible to keep previous category/filter settings mSortModel->setSourceModel(mTradeModel); else mSortModel = new SortFilterItemModel(mTradeModel); mSortModel->setNameFilter(mFilterEdit->getCaption()); mItemView->setModel(mSortModel); mFilterAll->setStateSelected(true); mFilterWeapon->setStateSelected(false); mFilterApparel->setStateSelected(false); mFilterMagic->setStateSelected(false); mFilterMisc->setStateSelected(false); mPreview->updatePtr(mPtr); mPreview->rebuild(); mPreview->update(); dirtyPreview(); updatePreviewSize(); updateEncumbranceBar(); mItemView->update(); notifyContentChanged(); } void InventoryWindow::clear() { mPtr = MWWorld::Ptr(); mTradeModel = nullptr; mSortModel = nullptr; mItemView->setModel(nullptr); } void InventoryWindow::toggleMaximized() { std::string setting = getModeSetting(); bool maximized = !Settings::Manager::getBool(setting + " maximized", "Windows"); if (maximized) setting += " maximized"; MyGUI::IntSize viewSize = MyGUI::RenderManager::getInstance().getViewSize(); float x = Settings::Manager::getFloat(setting + " x", "Windows") * float(viewSize.width); float y = Settings::Manager::getFloat(setting + " y", "Windows") * float(viewSize.height); float w = Settings::Manager::getFloat(setting + " w", "Windows") * float(viewSize.width); float h = Settings::Manager::getFloat(setting + " h", "Windows") * float(viewSize.height); MyGUI::Window* window = mMainWidget->castType(); window->setCoord(x, y, w, h); if (maximized) Settings::Manager::setBool(setting, "Windows", maximized); else Settings::Manager::setBool(setting + " maximized", "Windows", maximized); adjustPanes(); updatePreviewSize(); } void InventoryWindow::setGuiMode(GuiMode mode) { mGuiMode = mode; std::string setting = getModeSetting(); setPinButtonVisible(mode == GM_Inventory); if (Settings::Manager::getBool(setting + " maximized", "Windows")) setting += " maximized"; MyGUI::IntSize viewSize = MyGUI::RenderManager::getInstance().getViewSize(); MyGUI::IntPoint pos(static_cast(Settings::Manager::getFloat(setting + " x", "Windows") * viewSize.width), static_cast(Settings::Manager::getFloat(setting + " y", "Windows") * viewSize.height)); MyGUI::IntSize size(static_cast(Settings::Manager::getFloat(setting + " w", "Windows") * viewSize.width), static_cast(Settings::Manager::getFloat(setting + " h", "Windows") * viewSize.height)); bool needUpdate = (size.width != mMainWidget->getWidth() || size.height != mMainWidget->getHeight()); mMainWidget->setPosition(pos); mMainWidget->setSize(size); adjustPanes(); if (needUpdate) updatePreviewSize(); } SortFilterItemModel* InventoryWindow::getSortFilterModel() { return mSortModel; } TradeItemModel* InventoryWindow::getTradeModel() { return mTradeModel; } ItemModel* InventoryWindow::getModel() { return mTradeModel; } void InventoryWindow::onBackgroundSelected() { if (mDragAndDrop->mIsOnDragAndDrop) mDragAndDrop->drop(mTradeModel, mItemView); } void InventoryWindow::onItemSelected (int index) { onItemSelectedFromSourceModel (mSortModel->mapToSource(index)); } void InventoryWindow::onItemSelectedFromSourceModel (int index) { if (mDragAndDrop->mIsOnDragAndDrop) { mDragAndDrop->drop(mTradeModel, mItemView); return; } const ItemStack& item = mTradeModel->getItem(index); std::string sound = item.mBase.getClass().getDownSoundId(item.mBase); MWWorld::Ptr object = item.mBase; int count = item.mCount; bool shift = MyGUI::InputManager::getInstance().isShiftPressed(); if (MyGUI::InputManager::getInstance().isControlPressed()) count = 1; if (mTrading) { // Can't give conjured items to a merchant if (item.mFlags & ItemStack::Flag_Bound) { MWBase::Environment::get().getWindowManager()->playSound(sound); MWBase::Environment::get().getWindowManager()->messageBox("#{sBarterDialog9}"); return; } // check if merchant accepts item int services = MWBase::Environment::get().getWindowManager()->getTradeWindow()->getMerchantServices(); if (!object.getClass().canSell(object, services)) { MWBase::Environment::get().getWindowManager()->playSound(sound); MWBase::Environment::get().getWindowManager()-> messageBox("#{sBarterDialog4}"); return; } } // If we unequip weapon during attack, it can lead to unexpected behaviour if (MWBase::Environment::get().getMechanicsManager()->isAttackingOrSpell(mPtr)) { bool isWeapon = item.mBase.getTypeName() == typeid(ESM::Weapon).name(); MWWorld::InventoryStore& invStore = mPtr.getClass().getInventoryStore(mPtr); if (isWeapon && invStore.isEquipped(item.mBase)) { MWBase::Environment::get().getWindowManager()->messageBox("#{sCantEquipWeapWarning}"); return; } } if (count > 1 && !shift) { CountDialog* dialog = MWBase::Environment::get().getWindowManager()->getCountDialog(); std::string message = mTrading ? "#{sQuanityMenuMessage01}" : "#{sTake}"; std::string name = object.getClass().getName(object) + MWGui::ToolTips::getSoulString(object.getCellRef()); dialog->openCountDialog(name, message, count); dialog->eventOkClicked.clear(); if (mTrading) dialog->eventOkClicked += MyGUI::newDelegate(this, &InventoryWindow::sellItem); else dialog->eventOkClicked += MyGUI::newDelegate(this, &InventoryWindow::dragItem); mSelectedItem = index; } else { mSelectedItem = index; if (mTrading) sellItem (nullptr, count); else dragItem (nullptr, count); } } void InventoryWindow::ensureSelectedItemUnequipped(int count) { const ItemStack& item = mTradeModel->getItem(mSelectedItem); if (item.mType == ItemStack::Type_Equipped) { MWWorld::InventoryStore& invStore = mPtr.getClass().getInventoryStore(mPtr); MWWorld::Ptr newStack = *invStore.unequipItemQuantity(item.mBase, mPtr, count); // The unequipped item was re-stacked. We have to update the index // since the item pointed does not exist anymore. if (item.mBase != newStack) { updateItemView(); // Unequipping can produce a new stack, not yet in the window... // newIndex will store the index of the ItemStack the item was stacked on int newIndex = -1; for (size_t i=0; i < mTradeModel->getItemCount(); ++i) { if (mTradeModel->getItem(i).mBase == newStack) { newIndex = i; break; } } if (newIndex == -1) throw std::runtime_error("Can't find restacked item"); mSelectedItem = newIndex; } } } void InventoryWindow::dragItem(MyGUI::Widget* sender, int count) { ensureSelectedItemUnequipped(count); mDragAndDrop->startDrag(mSelectedItem, mSortModel, mTradeModel, mItemView, count); notifyContentChanged(); } void InventoryWindow::sellItem(MyGUI::Widget* sender, int count) { ensureSelectedItemUnequipped(count); const ItemStack& item = mTradeModel->getItem(mSelectedItem); std::string sound = item.mBase.getClass().getUpSoundId(item.mBase); MWBase::Environment::get().getWindowManager()->playSound(sound); if (item.mType == ItemStack::Type_Barter) { // this was an item borrowed to us by the merchant mTradeModel->returnItemBorrowedToUs(mSelectedItem, count); MWBase::Environment::get().getWindowManager()->getTradeWindow()->returnItem(mSelectedItem, count); } else { // borrow item to the merchant mTradeModel->borrowItemFromUs(mSelectedItem, count); MWBase::Environment::get().getWindowManager()->getTradeWindow()->borrowItem(mSelectedItem, count); } mItemView->update(); notifyContentChanged(); } void InventoryWindow::updateItemView() { MWBase::Environment::get().getWindowManager()->updateSpellWindow(); mItemView->update(); dirtyPreview(); } void InventoryWindow::onOpen() { // Reset the filter focus when opening the window MyGUI::Widget* focus = MyGUI::InputManager::getInstance().getKeyFocusWidget(); if (focus == mFilterEdit) MWBase::Environment::get().getWindowManager()->setKeyFocusWidget(nullptr); if (!mPtr.isEmpty()) { updateEncumbranceBar(); mItemView->update(); notifyContentChanged(); } adjustPanes(); } std::string InventoryWindow::getModeSetting() const { std::string setting = "inventory"; switch(mGuiMode) { case GM_Container: setting += " container"; break; case GM_Companion: setting += " companion"; break; case GM_Barter: setting += " barter"; break; default: break; } return setting; } void InventoryWindow::onWindowResize(MyGUI::Window* _sender) { adjustPanes(); std::string setting = getModeSetting(); MyGUI::IntSize viewSize = MyGUI::RenderManager::getInstance().getViewSize(); float x = _sender->getPosition().left / float(viewSize.width); float y = _sender->getPosition().top / float(viewSize.height); float w = _sender->getSize().width / float(viewSize.width); float h = _sender->getSize().height / float(viewSize.height); Settings::Manager::setFloat(setting + " x", "Windows", x); Settings::Manager::setFloat(setting + " y", "Windows", y); Settings::Manager::setFloat(setting + " w", "Windows", w); Settings::Manager::setFloat(setting + " h", "Windows", h); bool maximized = Settings::Manager::getBool(setting + " maximized", "Windows"); if (maximized) Settings::Manager::setBool(setting + " maximized", "Windows", false); if (mMainWidget->getSize().width != mLastXSize || mMainWidget->getSize().height != mLastYSize) { mLastXSize = mMainWidget->getSize().width; mLastYSize = mMainWidget->getSize().height; updatePreviewSize(); updateArmorRating(); } } void InventoryWindow::updateArmorRating() { mArmorRating->setCaptionWithReplacing ("#{sArmor}: " + MyGUI::utility::toString(static_cast(mPtr.getClass().getArmorRating(mPtr)))); if (mArmorRating->getTextSize().width > mArmorRating->getSize().width) mArmorRating->setCaptionWithReplacing (MyGUI::utility::toString(static_cast(mPtr.getClass().getArmorRating(mPtr)))); } void InventoryWindow::updatePreviewSize() { MyGUI::IntSize size = mAvatarImage->getSize(); int width = std::min(mPreview->getTextureWidth(), size.width); int height = std::min(mPreview->getTextureHeight(), size.height); float scalingFactor = MWBase::Environment::get().getWindowManager()->getScalingFactor(); mPreview->setViewport(int(width*scalingFactor), int(height*scalingFactor)); mAvatarImage->getSubWidgetMain()->_setUVSet(MyGUI::FloatRect(0.f, 0.f, width*scalingFactor/float(mPreview->getTextureWidth()), height*scalingFactor/float(mPreview->getTextureHeight()))); } void InventoryWindow::onNameFilterChanged(MyGUI::EditBox* _sender) { mSortModel->setNameFilter(_sender->getCaption()); mItemView->update(); } void InventoryWindow::onFilterChanged(MyGUI::Widget* _sender) { if (_sender == mFilterAll) mSortModel->setCategory(SortFilterItemModel::Category_All); else if (_sender == mFilterWeapon) mSortModel->setCategory(SortFilterItemModel::Category_Weapon); else if (_sender == mFilterApparel) mSortModel->setCategory(SortFilterItemModel::Category_Apparel); else if (_sender == mFilterMagic) mSortModel->setCategory(SortFilterItemModel::Category_Magic); else if (_sender == mFilterMisc) mSortModel->setCategory(SortFilterItemModel::Category_Misc); mFilterAll->setStateSelected(false); mFilterWeapon->setStateSelected(false); mFilterApparel->setStateSelected(false); mFilterMagic->setStateSelected(false); mFilterMisc->setStateSelected(false); mItemView->update(); _sender->castType()->setStateSelected(true); } void InventoryWindow::onPinToggled() { Settings::Manager::setBool("inventory pin", "Windows", mPinned); MWBase::Environment::get().getWindowManager()->setWeaponVisibility(!mPinned); } void InventoryWindow::onTitleDoubleClicked() { if (MyGUI::InputManager::getInstance().isShiftPressed()) toggleMaximized(); else if (!mPinned) MWBase::Environment::get().getWindowManager()->toggleVisible(GW_Inventory); } void InventoryWindow::useItem(const MWWorld::Ptr &ptr, bool force) { const std::string& script = ptr.getClass().getScript(ptr); if (!script.empty()) { // Don't try to equip the item if PCSkipEquip is set to 1 if (ptr.getRefData().getLocals().getIntVar(script, "pcskipequip") == 1) { ptr.getRefData().getLocals().setVarByInt(script, "onpcequip", 1); return; } ptr.getRefData().getLocals().setVarByInt(script, "onpcequip", 0); } MWWorld::Ptr player = MWMechanics::getPlayer(); // early-out for items that need to be equipped, but can't be equipped: we don't want to set OnPcEquip in that case if (!ptr.getClass().getEquipmentSlots(ptr).first.empty()) { if (ptr.getClass().hasItemHealth(ptr) && ptr.getCellRef().getCharge() == 0) { MWBase::Environment::get().getWindowManager()->messageBox("#{sInventoryMessage1}"); updateItemView(); return; } if (!force) { std::pair canEquip = ptr.getClass().canBeEquipped(ptr, player); if (canEquip.first == 0) { MWBase::Environment::get().getWindowManager()->messageBox(canEquip.second); updateItemView(); return; } } } // If the item has a script, set OnPCEquip or PCSkipEquip to 1 if (!script.empty()) { // Ingredients, books and repair hammers must not have OnPCEquip set to 1 here const std::string& type = ptr.getTypeName(); bool isBook = type == typeid(ESM::Book).name(); if (!isBook && type != typeid(ESM::Ingredient).name() && type != typeid(ESM::Repair).name()) ptr.getRefData().getLocals().setVarByInt(script, "onpcequip", 1); // Books must have PCSkipEquip set to 1 instead else if (isBook) ptr.getRefData().getLocals().setVarByInt(script, "pcskipequip", 1); } std::shared_ptr action = ptr.getClass().use(ptr, force); action->execute(player); if (isVisible()) { mItemView->update(); notifyContentChanged(); } // else: will be updated in open() } void InventoryWindow::onAvatarClicked(MyGUI::Widget* _sender) { if (mDragAndDrop->mIsOnDragAndDrop) { MWWorld::Ptr ptr = mDragAndDrop->mItem.mBase; mDragAndDrop->finish(); if (mDragAndDrop->mSourceModel != mTradeModel) { // Move item to the player's inventory ptr = mDragAndDrop->mSourceModel->moveItem(mDragAndDrop->mItem, mDragAndDrop->mDraggedCount, mTradeModel); } /* Start of tes3mp change (major) Instead of unilaterally using an item, send an ID_PLAYER_ITEM_USE packet and let the server decide if the item actually gets used */ //useItem(ptr); mwmp::Main::get().getLocalPlayer()->sendItemUse(ptr); /* End of tes3mp change (major) */ // If item is ingredient or potion don't stop drag and drop to simplify action of taking more than one 1 item if ((ptr.getTypeName() == typeid(ESM::Potion).name() || ptr.getTypeName() == typeid(ESM::Ingredient).name()) && mDragAndDrop->mDraggedCount > 1) { // Item can be provided from other window for example container. // But after DragAndDrop::startDrag item automaticly always gets to player inventory. mSelectedItem = getModel()->getIndex(mDragAndDrop->mItem); dragItem(nullptr, mDragAndDrop->mDraggedCount - 1); } } else { MyGUI::IntPoint mousePos = MyGUI::InputManager::getInstance ().getLastPressedPosition (MyGUI::MouseButton::Left); MyGUI::IntPoint relPos = mousePos - mAvatarImage->getAbsolutePosition (); MWWorld::Ptr itemSelected = getAvatarSelectedItem (relPos.left, relPos.top); if (itemSelected.isEmpty ()) return; for (size_t i=0; i < mTradeModel->getItemCount (); ++i) { if (mTradeModel->getItem(i).mBase == itemSelected) { onItemSelectedFromSourceModel(i); return; } } throw std::runtime_error("Can't find clicked item"); } } MWWorld::Ptr InventoryWindow::getAvatarSelectedItem(int x, int y) { // convert to OpenGL lower-left origin y = (mAvatarImage->getHeight()-1) - y; // Scale coordinates float scalingFactor = MWBase::Environment::get().getWindowManager()->getScalingFactor(); x = static_cast(x*scalingFactor); y = static_cast(y*scalingFactor); int slot = mPreview->getSlotSelected (x, y); if (slot == -1) return MWWorld::Ptr(); MWWorld::InventoryStore& invStore = mPtr.getClass().getInventoryStore(mPtr); if(invStore.getSlot(slot) != invStore.end()) { MWWorld::Ptr item = *invStore.getSlot(slot); if (!item.getClass().showsInInventory(item)) return MWWorld::Ptr(); return item; } return MWWorld::Ptr(); } void InventoryWindow::updateEncumbranceBar() { MWWorld::Ptr player = MWMechanics::getPlayer(); float capacity = player.getClass().getCapacity(player); float encumbrance = player.getClass().getEncumbrance(player); mTradeModel->adjustEncumbrance(encumbrance); mEncumbranceBar->setValue(std::ceil(encumbrance), static_cast(capacity)); } void InventoryWindow::onFrame(float dt) { updateEncumbranceBar(); if (mPinned) { mUpdateTimer += dt; if (0.1f < mUpdateTimer) { mUpdateTimer = 0; // Update pinned inventory in-game if (!MWBase::Environment::get().getWindowManager()->isGuiMode()) { mItemView->update(); notifyContentChanged(); } } } } void InventoryWindow::setTrading(bool trading) { mTrading = trading; } void InventoryWindow::dirtyPreview() { mPreview->update(); updateArmorRating(); } void InventoryWindow::notifyContentChanged() { // update the spell window just in case new enchanted items were added to inventory MWBase::Environment::get().getWindowManager()->updateSpellWindow(); MWBase::Environment::get().getMechanicsManager()->updateMagicEffects( MWMechanics::getPlayer()); dirtyPreview(); } void InventoryWindow::pickUpObject (MWWorld::Ptr object) { // If the inventory is not yet enabled, don't pick anything up if (!MWBase::Environment::get().getWindowManager()->isAllowed(GW_Inventory)) return; // make sure the object is of a type that can be picked up std::string type = object.getTypeName(); if ( (type != typeid(ESM::Apparatus).name()) && (type != typeid(ESM::Armor).name()) && (type != typeid(ESM::Book).name()) && (type != typeid(ESM::Clothing).name()) && (type != typeid(ESM::Ingredient).name()) && (type != typeid(ESM::Light).name()) && (type != typeid(ESM::Miscellaneous).name()) && (type != typeid(ESM::Lockpick).name()) && (type != typeid(ESM::Probe).name()) && (type != typeid(ESM::Repair).name()) && (type != typeid(ESM::Weapon).name()) && (type != typeid(ESM::Potion).name())) return; // An object that can be picked up must have a tooltip. if (!object.getClass().hasToolTip(object)) return; int count = object.getRefData().getCount(); if (object.getClass().isGold(object)) count *= object.getClass().getValue(object); MWWorld::Ptr player = MWMechanics::getPlayer(); MWBase::Environment::get().getWorld()->breakInvisibility(player); if (!object.getRefData().activate()) return; MWBase::Environment::get().getMechanicsManager()->itemTaken(player, object, MWWorld::Ptr(), count); // add to player inventory // can't use ActionTake here because we need an MWWorld::Ptr to the newly inserted object MWWorld::Ptr newObject = *player.getClass().getContainerStore (player).add (object, object.getRefData().getCount(), player); /* Start of tes3mp addition Send an ID_OBJECT_DELETE packet every time an item from the world is picked up by the player through the inventory HUD */ mwmp::ObjectList *objectList = mwmp::Main::get().getNetworking()->getObjectList(); objectList->reset(); objectList->packetOrigin = mwmp::CLIENT_GAMEPLAY; objectList->addObjectGeneric(object); objectList->sendObjectDelete(); /* End of tes3mp addition */ // remove from world MWBase::Environment::get().getWorld()->deleteObject (object); // get ModelIndex to the item mTradeModel->update(); size_t i=0; for (; igetItemCount(); ++i) { if (mTradeModel->getItem(i).mBase == newObject) break; } if (i == mTradeModel->getItemCount()) throw std::runtime_error("Added item not found"); mDragAndDrop->startDrag(i, mSortModel, mTradeModel, mItemView, count); MWBase::Environment::get().getWindowManager()->updateSpellWindow(); } void InventoryWindow::cycle(bool next) { MWWorld::Ptr player = MWMechanics::getPlayer(); if (MWBase::Environment::get().getMechanicsManager()->isAttackingOrSpell(player)) return; const MWMechanics::CreatureStats &stats = player.getClass().getCreatureStats(player); bool godmode = MWBase::Environment::get().getWorld()->getGodModeState(); if ((!godmode && stats.isParalyzed()) || stats.getKnockedDown() || stats.isDead() || stats.getHitRecovery()) return; ItemModel::ModelIndex selected = -1; // not using mSortFilterModel as we only need sorting, not filtering SortFilterItemModel model(new InventoryItemModel(player)); model.setSortByType(false); model.update(); if (model.getItemCount() == 0) return; for (ItemModel::ModelIndex i=0; isendItemUse(model.getItem(cycled).mBase); /* End of tes3mp change (major) */ } void InventoryWindow::rebuildAvatar() { mPreview->rebuild(); } } ================================================ FILE: apps/openmw/mwgui/inventorywindow.hpp ================================================ #ifndef MGUI_Inventory_H #define MGUI_Inventory_H #include "windowpinnablebase.hpp" #include "mode.hpp" #include "../mwworld/ptr.hpp" #include "../mwrender/characterpreview.hpp" namespace osg { class Group; } namespace Resource { class ResourceSystem; } namespace MWGui { namespace Widgets { class MWDynamicStat; } class ItemView; class SortFilterItemModel; class TradeItemModel; class DragAndDrop; class ItemModel; class InventoryWindow : public WindowPinnableBase { public: InventoryWindow(DragAndDrop* dragAndDrop, osg::Group* parent, Resource::ResourceSystem* resourceSystem); void onOpen() override; /// start trading, disables item drag&drop void setTrading(bool trading); void onFrame(float dt) override; void pickUpObject (MWWorld::Ptr object); MWWorld::Ptr getAvatarSelectedItem(int x, int y); void rebuildAvatar(); SortFilterItemModel* getSortFilterModel(); TradeItemModel* getTradeModel(); ItemModel* getModel(); void updateItemView(); void updatePlayer(); void clear() override; void useItem(const MWWorld::Ptr& ptr, bool force=false); void setGuiMode(GuiMode mode); /// Cycle to previous/next weapon void cycle(bool next); protected: void onTitleDoubleClicked() override; private: DragAndDrop* mDragAndDrop; int mSelectedItem; MWWorld::Ptr mPtr; MWGui::ItemView* mItemView; SortFilterItemModel* mSortModel; TradeItemModel* mTradeModel; MyGUI::Widget* mAvatar; MyGUI::ImageBox* mAvatarImage; MyGUI::TextBox* mArmorRating; Widgets::MWDynamicStat* mEncumbranceBar; MyGUI::Widget* mLeftPane; MyGUI::Widget* mRightPane; MyGUI::Button* mFilterAll; MyGUI::Button* mFilterWeapon; MyGUI::Button* mFilterApparel; MyGUI::Button* mFilterMagic; MyGUI::Button* mFilterMisc; MyGUI::EditBox* mFilterEdit; GuiMode mGuiMode; int mLastXSize; int mLastYSize; std::unique_ptr mPreviewTexture; std::unique_ptr mPreview; bool mTrading; float mUpdateTimer; void toggleMaximized(); void onItemSelected(int index); void onItemSelectedFromSourceModel(int index); void onBackgroundSelected(); std::string getModeSetting() const; void sellItem(MyGUI::Widget* sender, int count); void dragItem(MyGUI::Widget* sender, int count); void onWindowResize(MyGUI::Window* _sender); void onFilterChanged(MyGUI::Widget* _sender); void onNameFilterChanged(MyGUI::EditBox* _sender); void onAvatarClicked(MyGUI::Widget* _sender); void onPinToggled() override; void updateEncumbranceBar(); void notifyContentChanged(); void dirtyPreview(); void updatePreviewSize(); void updateArmorRating(); void adjustPanes(); /// Unequips count items from mSelectedItem, if it is equipped, and then updates mSelectedItem in case the items were re-stacked void ensureSelectedItemUnequipped(int count); }; } #endif // Inventory_H ================================================ FILE: apps/openmw/mwgui/itemchargeview.cpp ================================================ #include "itemchargeview.hpp" #include #include #include #include #include #include #include "../mwbase/environment.hpp" #include "../mwbase/world.hpp" #include "../mwworld/class.hpp" #include "../mwworld/esmstore.hpp" #include "itemmodel.hpp" #include "itemwidget.hpp" namespace MWGui { ItemChargeView::ItemChargeView() : mScrollView(nullptr), mDisplayMode(DisplayMode_Health) { } void ItemChargeView::registerComponents() { MyGUI::FactoryManager::getInstance().registerFactory("Widget"); } void ItemChargeView::initialiseOverride() { Base::initialiseOverride(); assignWidget(mScrollView, "ScrollView"); if (mScrollView == nullptr) throw std::runtime_error("Item charge view needs a scroll view"); mScrollView->setCanvasAlign(MyGUI::Align::Left | MyGUI::Align::Top); } void ItemChargeView::setModel(ItemModel* model) { mModel.reset(model); } void ItemChargeView::setDisplayMode(ItemChargeView::DisplayMode type) { mDisplayMode = type; update(); } void ItemChargeView::update() { if (!mModel.get()) { layoutWidgets(); return; } mModel->update(); Lines lines; std::set visitedLines; for (size_t i = 0; i < mModel->getItemCount(); ++i) { ItemStack stack = mModel->getItem(static_cast(i)); bool found = false; for (Lines::const_iterator iter = mLines.begin(); iter != mLines.end(); ++iter) { if (iter->mItemPtr == stack.mBase) { found = true; visitedLines.insert(iter); // update line widgets updateLine(*iter); lines.push_back(*iter); break; } } if (!found) { // add line widgets Line line; line.mItemPtr = stack.mBase; line.mText = mScrollView->createWidget("SandText", MyGUI::IntCoord(), MyGUI::Align::Default); line.mText->setNeedMouseFocus(false); line.mIcon = mScrollView->createWidget("MW_ItemIconSmall", MyGUI::IntCoord(), MyGUI::Align::Default); line.mIcon->setItem(line.mItemPtr); line.mIcon->setUserString("ToolTipType", "ItemPtr"); line.mIcon->setUserData(MWWorld::Ptr(line.mItemPtr)); line.mIcon->eventMouseButtonClick += MyGUI::newDelegate(this, &ItemChargeView::onIconClicked); line.mIcon->eventMouseWheel += MyGUI::newDelegate(this, &ItemChargeView::onMouseWheelMoved); line.mCharge = mScrollView->createWidget("MW_ChargeBar", MyGUI::IntCoord(), MyGUI::Align::Default); line.mCharge->setNeedMouseFocus(false); updateLine(line); lines.push_back(line); } } for (Lines::iterator iter = mLines.begin(); iter != mLines.end(); ++iter) { if (visitedLines.count(iter)) continue; // remove line widgets MyGUI::Gui::getInstance().destroyWidget(iter->mText); MyGUI::Gui::getInstance().destroyWidget(iter->mIcon); MyGUI::Gui::getInstance().destroyWidget(iter->mCharge); } mLines.swap(lines); layoutWidgets(); } void ItemChargeView::layoutWidgets() { int currentY = 0; for (Line& line : mLines) { line.mText->setCoord(8, currentY, mScrollView->getWidth()-8, 18); currentY += 19; line.mIcon->setCoord(16, currentY, 32, 32); line.mCharge->setCoord(72, currentY+2, std::max(199, mScrollView->getWidth()-72-38), 20); currentY += 32 + 4; } // Canvas size must be expressed with VScroll disabled, otherwise MyGUI would expand the scroll area when the scrollbar is hidden mScrollView->setVisibleVScroll(false); mScrollView->setCanvasSize(MyGUI::IntSize(mScrollView->getWidth(), std::max(mScrollView->getHeight(), currentY))); mScrollView->setVisibleVScroll(true); } void ItemChargeView::resetScrollbars() { mScrollView->setViewOffset(MyGUI::IntPoint(0, 0)); } void ItemChargeView::setSize(const MyGUI::IntSize& value) { bool changed = (value.width != getWidth() || value.height != getHeight()); Base::setSize(value); if (changed) layoutWidgets(); } void ItemChargeView::setCoord(const MyGUI::IntCoord& value) { bool changed = (value.width != getWidth() || value.height != getHeight()); Base::setCoord(value); if (changed) layoutWidgets(); } void ItemChargeView::updateLine(const ItemChargeView::Line& line) { line.mText->setCaption(line.mItemPtr.getClass().getName(line.mItemPtr)); line.mCharge->setVisible(false); switch (mDisplayMode) { case DisplayMode_Health: if (!line.mItemPtr.getClass().hasItemHealth(line.mItemPtr)) break; line.mCharge->setVisible(true); line.mCharge->setValue(line.mItemPtr.getClass().getItemHealth(line.mItemPtr), line.mItemPtr.getClass().getItemMaxHealth(line.mItemPtr)); break; case DisplayMode_EnchantmentCharge: std::string enchId = line.mItemPtr.getClass().getEnchantment(line.mItemPtr); if (enchId.empty()) break; const ESM::Enchantment* ench = MWBase::Environment::get().getWorld()->getStore().get().search(enchId); if (!ench) break; line.mCharge->setVisible(true); line.mCharge->setValue(static_cast(line.mItemPtr.getCellRef().getEnchantmentCharge()), ench->mData.mCharge); break; } } void ItemChargeView::onIconClicked(MyGUI::Widget* sender) { eventItemClicked(this, *sender->getUserData()); } void ItemChargeView::onMouseWheelMoved(MyGUI::Widget* /*sender*/, int rel) { if (mScrollView->getViewOffset().top + rel*0.3f > 0) mScrollView->setViewOffset(MyGUI::IntPoint(0, 0)); else mScrollView->setViewOffset(MyGUI::IntPoint(0, static_cast(mScrollView->getViewOffset().top + rel*0.3f))); } } ================================================ FILE: apps/openmw/mwgui/itemchargeview.hpp ================================================ #ifndef MWGUI_ITEMCHARGEVIEW_H #define MWGUI_ITEMCHARGEVIEW_H #include #include #include #include "../mwworld/ptr.hpp" #include "widgets.hpp" namespace MyGUI { class TextBox; class ScrollView; } namespace MWGui { class ItemModel; class ItemWidget; class ItemChargeView final : public MyGUI::Widget { MYGUI_RTTI_DERIVED(ItemChargeView) public: enum DisplayMode { DisplayMode_Health, DisplayMode_EnchantmentCharge }; ItemChargeView(); /// Register needed components with MyGUI's factory manager static void registerComponents(); void initialiseOverride() override; /// Takes ownership of \a model void setModel(ItemModel* model); void setDisplayMode(DisplayMode type); void update(); void layoutWidgets(); void resetScrollbars(); void setSize(const MyGUI::IntSize& value) override; void setCoord(const MyGUI::IntCoord& value) override; MyGUI::delegates::CMultiDelegate2 eventItemClicked; private: struct Line { MWWorld::Ptr mItemPtr; MyGUI::TextBox* mText; ItemWidget* mIcon; Widgets::MWDynamicStatPtr mCharge; }; void updateLine(const Line& line); void onIconClicked(MyGUI::Widget* sender); void onMouseWheelMoved(MyGUI::Widget* sender, int rel); typedef std::vector Lines; Lines mLines; std::unique_ptr mModel; MyGUI::ScrollView* mScrollView; DisplayMode mDisplayMode; }; } #endif ================================================ FILE: apps/openmw/mwgui/itemmodel.cpp ================================================ #include "itemmodel.hpp" #include "../mwworld/class.hpp" #include "../mwworld/containerstore.hpp" #include "../mwbase/environment.hpp" #include "../mwbase/mechanicsmanager.hpp" namespace MWGui { ItemStack::ItemStack(const MWWorld::Ptr &base, ItemModel *creator, size_t count) : mType(Type_Normal) , mFlags(0) , mCreator(creator) , mCount(count) , mBase(base) { if (base.getClass().getEnchantment(base) != "") mFlags |= Flag_Enchanted; if (MWBase::Environment::get().getMechanicsManager()->isBoundItem(base)) mFlags |= Flag_Bound; } ItemStack::ItemStack() : mType(Type_Normal) , mFlags(0) , mCreator(nullptr) , mCount(0) { } bool operator == (const ItemStack& left, const ItemStack& right) { if (left.mType != right.mType) return false; if(left.mBase == right.mBase) return true; // If one of the items is in an inventory and currently equipped, we need to check stacking both ways to be sure if (left.mBase.getContainerStore() && right.mBase.getContainerStore()) return left.mBase.getContainerStore()->stacks(left.mBase, right.mBase) && right.mBase.getContainerStore()->stacks(left.mBase, right.mBase); if (left.mBase.getContainerStore()) return left.mBase.getContainerStore()->stacks(left.mBase, right.mBase); if (right.mBase.getContainerStore()) return right.mBase.getContainerStore()->stacks(left.mBase, right.mBase); MWWorld::ContainerStore store; return store.stacks(left.mBase, right.mBase); } ItemModel::ItemModel() { } MWWorld::Ptr ItemModel::moveItem(const ItemStack &item, size_t count, ItemModel *otherModel) { MWWorld::Ptr ret = otherModel->copyItem(item, count); removeItem(item, count); return ret; } bool ItemModel::allowedToUseItems() const { return true; } bool ItemModel::onDropItem(const MWWorld::Ptr &item, int count) { return true; } bool ItemModel::onTakeItem(const MWWorld::Ptr &item, int count) { return true; } ProxyItemModel::ProxyItemModel() : mSourceModel(nullptr) { } ProxyItemModel::~ProxyItemModel() { delete mSourceModel; } bool ProxyItemModel::allowedToUseItems() const { return mSourceModel->allowedToUseItems(); } MWWorld::Ptr ProxyItemModel::copyItem (const ItemStack& item, size_t count, bool allowAutoEquip) { return mSourceModel->copyItem (item, count, allowAutoEquip); } void ProxyItemModel::removeItem (const ItemStack& item, size_t count) { mSourceModel->removeItem (item, count); } ItemModel::ModelIndex ProxyItemModel::mapToSource (ModelIndex index) { const ItemStack& itemToSearch = getItem(index); for (size_t i=0; igetItemCount(); ++i) { const ItemStack& item = mSourceModel->getItem(i); if (item.mBase == itemToSearch.mBase) return i; } return -1; } ItemModel::ModelIndex ProxyItemModel::mapFromSource (ModelIndex index) { const ItemStack& itemToSearch = mSourceModel->getItem(index); for (size_t i=0; igetIndex(item); } void ProxyItemModel::setSourceModel(ItemModel *sourceModel) { if (mSourceModel == sourceModel) return; if (mSourceModel) { delete mSourceModel; mSourceModel = nullptr; } mSourceModel = sourceModel; } void ProxyItemModel::onClose() { mSourceModel->onClose(); } bool ProxyItemModel::onDropItem(const MWWorld::Ptr &item, int count) { return mSourceModel->onDropItem(item, count); } bool ProxyItemModel::onTakeItem(const MWWorld::Ptr &item, int count) { return mSourceModel->onTakeItem(item, count); } } ================================================ FILE: apps/openmw/mwgui/itemmodel.hpp ================================================ #ifndef MWGUI_ITEM_MODEL_H #define MWGUI_ITEM_MODEL_H #include "../mwworld/ptr.hpp" namespace MWGui { class ItemModel; /// @brief A single item stack managed by an item model struct ItemStack { ItemStack (const MWWorld::Ptr& base, ItemModel* creator, size_t count); ItemStack(); ///< like operator==, only without checking mType enum Type { Type_Barter, Type_Equipped, Type_Normal }; Type mType; enum Flags { Flag_Enchanted = (1<<0), Flag_Bound = (1<<1) }; int mFlags; ItemModel* mCreator; size_t mCount; MWWorld::Ptr mBase; }; bool operator == (const ItemStack& left, const ItemStack& right); /// @brief The base class that all item models should derive from. class ItemModel { public: ItemModel(); virtual ~ItemModel() {} typedef int ModelIndex; // -1 means invalid index /// Throws for invalid index or out of range index virtual ItemStack getItem (ModelIndex index) = 0; /// The number of items in the model, this specifies the range of indices you can pass to /// the getItem function (but this range is only valid until the next call to update()) virtual size_t getItemCount() = 0; /// Returns an invalid index if the item was not found virtual ModelIndex getIndex (ItemStack item) = 0; /// Rebuild the item model, this will invalidate existing model indices virtual void update() = 0; /// Move items from this model to \a otherModel. /// @note Derived implementations may return an empty Ptr if the move was unsuccessful. virtual MWWorld::Ptr moveItem (const ItemStack& item, size_t count, ItemModel* otherModel); virtual MWWorld::Ptr copyItem (const ItemStack& item, size_t count, bool allowAutoEquip = true) = 0; virtual void removeItem (const ItemStack& item, size_t count) = 0; /// Is the player allowed to use items from this item model? (default true) virtual bool allowedToUseItems() const; virtual void onClose() { } virtual bool onDropItem(const MWWorld::Ptr &item, int count); virtual bool onTakeItem(const MWWorld::Ptr &item, int count); private: ItemModel(const ItemModel&); ItemModel& operator=(const ItemModel&); }; /// @brief A proxy item model can be used to filter or rearrange items from a source model (or even add new items to it). /// The neat thing is that this does not actually alter the source model. class ProxyItemModel : public ItemModel { public: ProxyItemModel(); virtual ~ProxyItemModel(); bool allowedToUseItems() const override; void onClose() override; bool onDropItem(const MWWorld::Ptr &item, int count) override; bool onTakeItem(const MWWorld::Ptr &item, int count) override; MWWorld::Ptr copyItem (const ItemStack& item, size_t count, bool allowAutoEquip = true) override; void removeItem (const ItemStack& item, size_t count) override; ModelIndex getIndex (ItemStack item) override; /// @note Takes ownership of the passed pointer. void setSourceModel(ItemModel* sourceModel); ModelIndex mapToSource (ModelIndex index); ModelIndex mapFromSource (ModelIndex index); protected: ItemModel* mSourceModel; }; } #endif ================================================ FILE: apps/openmw/mwgui/itemselection.cpp ================================================ #include "itemselection.hpp" #include #include #include "itemview.hpp" #include "inventoryitemmodel.hpp" #include "sortfilteritemmodel.hpp" namespace MWGui { ItemSelectionDialog::ItemSelectionDialog(const std::string &label) : WindowModal("openmw_itemselection_dialog.layout") , mSortModel(nullptr) , mModel(nullptr) { getWidget(mItemView, "ItemView"); mItemView->eventItemClicked += MyGUI::newDelegate(this, &ItemSelectionDialog::onSelectedItem); MyGUI::TextBox* l; getWidget(l, "Label"); l->setCaptionWithReplacing (label); MyGUI::Button* cancelButton; getWidget(cancelButton, "CancelButton"); cancelButton->eventMouseButtonClick += MyGUI::newDelegate(this, &ItemSelectionDialog::onCancelButtonClicked); center(); } bool ItemSelectionDialog::exit() { eventDialogCanceled(); return true; } void ItemSelectionDialog::openContainer(const MWWorld::Ptr& container) { mModel = new InventoryItemModel(container); mSortModel = new SortFilterItemModel(mModel); mItemView->setModel(mSortModel); mItemView->resetScrollBars(); } void ItemSelectionDialog::setCategory(int category) { mSortModel->setCategory(category); mItemView->update(); } void ItemSelectionDialog::setFilter(int filter) { mSortModel->setFilter(filter); mItemView->update(); } void ItemSelectionDialog::onSelectedItem(int index) { ItemStack item = mSortModel->getItem(index); eventItemSelected(item.mBase); } void ItemSelectionDialog::onCancelButtonClicked(MyGUI::Widget* sender) { exit(); } } ================================================ FILE: apps/openmw/mwgui/itemselection.hpp ================================================ #ifndef OPENMW_GAME_MWGUI_ITEMSELECTION_H #define OPENMW_GAME_MWGUI_ITEMSELECTION_H #include #include "windowbase.hpp" namespace MWWorld { class Ptr; } namespace MWGui { class ItemView; class SortFilterItemModel; class InventoryItemModel; class ItemSelectionDialog : public WindowModal { public: ItemSelectionDialog(const std::string& label); bool exit() override; typedef MyGUI::delegates::CMultiDelegate0 EventHandle_Void; typedef MyGUI::delegates::CMultiDelegate1 EventHandle_Item; EventHandle_Item eventItemSelected; EventHandle_Void eventDialogCanceled; void openContainer (const MWWorld::Ptr& container); void setCategory(int category); void setFilter(int filter); private: ItemView* mItemView; SortFilterItemModel* mSortModel; InventoryItemModel* mModel; void onSelectedItem(int index); void onCancelButtonClicked(MyGUI::Widget* sender); }; } #endif ================================================ FILE: apps/openmw/mwgui/itemview.cpp ================================================ #include "itemview.hpp" #include #include #include #include #include #include "../mwworld/class.hpp" #include "itemmodel.hpp" #include "itemwidget.hpp" namespace MWGui { ItemView::ItemView() : mModel(nullptr) , mScrollView(nullptr) { } ItemView::~ItemView() { delete mModel; } void ItemView::setModel(ItemModel *model) { if (mModel == model) return; delete mModel; mModel = model; update(); } void ItemView::initialiseOverride() { Base::initialiseOverride(); assignWidget(mScrollView, "ScrollView"); if (mScrollView == nullptr) throw std::runtime_error("Item view needs a scroll view"); mScrollView->setCanvasAlign(MyGUI::Align::Left | MyGUI::Align::Top); } void ItemView::layoutWidgets() { if (!mScrollView->getChildCount()) return; int x = 0; int y = 0; MyGUI::Widget* dragArea = mScrollView->getChildAt(0); int maxHeight = mScrollView->getHeight(); int rows = maxHeight/42; rows = std::max(rows, 1); bool showScrollbar = int(std::ceil(dragArea->getChildCount()/float(rows))) > mScrollView->getWidth()/42; if (showScrollbar) maxHeight -= 18; for (unsigned int i=0; igetChildCount(); ++i) { MyGUI::Widget* w = dragArea->getChildAt(i); w->setPosition(x, y); y += 42; if (y > maxHeight-42 && i < dragArea->getChildCount()-1) { x += 42; y = 0; } } x += 42; MyGUI::IntSize size = MyGUI::IntSize(std::max(mScrollView->getSize().width, x), mScrollView->getSize().height); // Canvas size must be expressed with VScroll disabled, otherwise MyGUI would expand the scroll area when the scrollbar is hidden mScrollView->setVisibleVScroll(false); mScrollView->setVisibleHScroll(false); mScrollView->setCanvasSize(size); mScrollView->setVisibleVScroll(true); mScrollView->setVisibleHScroll(true); dragArea->setSize(size); } void ItemView::update() { while (mScrollView->getChildCount()) MyGUI::Gui::getInstance().destroyWidget(mScrollView->getChildAt(0)); if (!mModel) return; mModel->update(); MyGUI::Widget* dragArea = mScrollView->createWidget("",0,0,mScrollView->getWidth(),mScrollView->getHeight(), MyGUI::Align::Stretch); dragArea->setNeedMouseFocus(true); dragArea->eventMouseButtonClick += MyGUI::newDelegate(this, &ItemView::onSelectedBackground); dragArea->eventMouseWheel += MyGUI::newDelegate(this, &ItemView::onMouseWheelMoved); for (ItemModel::ModelIndex i=0; i(mModel->getItemCount()); ++i) { const ItemStack& item = mModel->getItem(i); ItemWidget* itemWidget = dragArea->createWidget("MW_ItemIcon", MyGUI::IntCoord(0, 0, 42, 42), MyGUI::Align::Default); itemWidget->setUserString("ToolTipType", "ItemModelIndex"); itemWidget->setUserData(std::make_pair(i, mModel)); ItemWidget::ItemState state = ItemWidget::None; if (item.mType == ItemStack::Type_Barter) state = ItemWidget::Barter; if (item.mType == ItemStack::Type_Equipped) state = ItemWidget::Equip; itemWidget->setItem(item.mBase, state); itemWidget->setCount(item.mCount); itemWidget->eventMouseButtonClick += MyGUI::newDelegate(this, &ItemView::onSelectedItem); itemWidget->eventMouseWheel += MyGUI::newDelegate(this, &ItemView::onMouseWheelMoved); } layoutWidgets(); } void ItemView::resetScrollBars() { mScrollView->setViewOffset(MyGUI::IntPoint(0, 0)); } void ItemView::onSelectedItem(MyGUI::Widget *sender) { ItemModel::ModelIndex index = (*sender->getUserData >()).first; eventItemClicked(index); } void ItemView::onSelectedBackground(MyGUI::Widget *sender) { eventBackgroundClicked(); } void ItemView::onMouseWheelMoved(MyGUI::Widget *_sender, int _rel) { if (mScrollView->getViewOffset().left + _rel*0.3f > 0) mScrollView->setViewOffset(MyGUI::IntPoint(0, 0)); else mScrollView->setViewOffset(MyGUI::IntPoint(static_cast(mScrollView->getViewOffset().left + _rel*0.3f), 0)); } void ItemView::setSize(const MyGUI::IntSize &_value) { bool changed = (_value.width != getWidth() || _value.height != getHeight()); Base::setSize(_value); if (changed) layoutWidgets(); } void ItemView::setCoord(const MyGUI::IntCoord &_value) { bool changed = (_value.width != getWidth() || _value.height != getHeight()); Base::setCoord(_value); if (changed) layoutWidgets(); } void ItemView::registerComponents() { MyGUI::FactoryManager::getInstance().registerFactory("Widget"); } } ================================================ FILE: apps/openmw/mwgui/itemview.hpp ================================================ #ifndef MWGUI_ITEMVIEW_H #define MWGUI_ITEMVIEW_H #include #include "itemmodel.hpp" namespace MWGui { class ItemView final : public MyGUI::Widget { MYGUI_RTTI_DERIVED(ItemView) public: ItemView(); ~ItemView() override; /// Register needed components with MyGUI's factory manager static void registerComponents (); /// Takes ownership of \a model void setModel (ItemModel* model); typedef MyGUI::delegates::CMultiDelegate1 EventHandle_ModelIndex; typedef MyGUI::delegates::CMultiDelegate0 EventHandle_Void; /// Fired when an item was clicked EventHandle_ModelIndex eventItemClicked; /// Fired when the background was clicked (useful for drag and drop) EventHandle_Void eventBackgroundClicked; void update(); void resetScrollBars(); private: void initialiseOverride() override; void layoutWidgets(); void setSize(const MyGUI::IntSize& _value) override; void setCoord(const MyGUI::IntCoord& _value) override; void onSelectedItem (MyGUI::Widget* sender); void onSelectedBackground (MyGUI::Widget* sender); void onMouseWheelMoved(MyGUI::Widget* _sender, int _rel); ItemModel* mModel; MyGUI::ScrollView* mScrollView; }; } #endif ================================================ FILE: apps/openmw/mwgui/itemwidget.cpp ================================================ #include "itemwidget.hpp" #include #include #include #include #include // correctIconPath #include #include #include "../mwbase/environment.hpp" #include "../mwbase/windowmanager.hpp" #include "../mwworld/class.hpp" namespace { std::string getCountString(int count) { if (count == 1) return ""; if (count > 999999999) return MyGUI::utility::toString(count/1000000000) + "b"; else if (count > 999999) return MyGUI::utility::toString(count/1000000) + "m"; else if (count > 9999) return MyGUI::utility::toString(count/1000) + "k"; else return MyGUI::utility::toString(count); } } namespace MWGui { std::map ItemWidget::mScales; ItemWidget::ItemWidget() : mItem(nullptr) , mItemShadow(nullptr) , mFrame(nullptr) , mText(nullptr) { } void ItemWidget::registerComponents() { MyGUI::FactoryManager::getInstance().registerFactory("Widget"); MyGUI::FactoryManager::getInstance().registerFactory("Widget"); } void ItemWidget::initialiseOverride() { assignWidget(mItem, "Item"); if (mItem) mItem->setNeedMouseFocus(false); assignWidget(mItemShadow, "ItemShadow"); if (mItemShadow) mItemShadow->setNeedMouseFocus(false); assignWidget(mFrame, "Frame"); if (mFrame) mFrame->setNeedMouseFocus(false); assignWidget(mText, "Text"); if (mText) mText->setNeedMouseFocus(false); Base::initialiseOverride(); } void ItemWidget::setCount(int count) { if (!mText) return; mText->setCaption(getCountString(count)); } void ItemWidget::setIcon(const std::string &icon) { if (mCurrentIcon != icon) { mCurrentIcon = icon; if (mItemShadow) mItemShadow->setImageTexture(icon); if (mItem) mItem->setImageTexture(icon); } } void ItemWidget::setFrame(const std::string &frame, const MyGUI::IntCoord &coord) { if (mFrame) { mFrame->setImageTile(MyGUI::IntSize(coord.width, coord.height)); // Why is this needed? MyGUI bug? mFrame->setImageCoord(coord); } if (mCurrentFrame != frame) { mCurrentFrame = frame; mFrame->setImageTexture(frame); } } void ItemWidget::setIcon(const MWWorld::Ptr &ptr) { std::string invIcon = ptr.getClass().getInventoryIcon(ptr); if (invIcon.empty()) invIcon = "default icon.tga"; invIcon = MWBase::Environment::get().getWindowManager()->correctIconPath(invIcon); if (!MWBase::Environment::get().getResourceSystem()->getVFS()->exists(invIcon)) { Log(Debug::Error) << "Failed to open image: '" << invIcon << "' not found, falling back to 'default-icon.tga'"; invIcon = MWBase::Environment::get().getWindowManager()->correctIconPath("default icon.tga"); } setIcon(invIcon); } void ItemWidget::setItem(const MWWorld::Ptr &ptr, ItemState state) { if (!mItem) return; if (ptr.isEmpty()) { if (mFrame) mFrame->setImageTexture(""); if (mItemShadow) mItemShadow->setImageTexture(""); mItem->setImageTexture(""); mText->setCaption(""); mCurrentIcon.clear(); mCurrentFrame.clear(); return; } bool isMagic = !ptr.getClass().getEnchantment(ptr).empty(); std::string backgroundTex = "textures\\menu_icon"; if (isMagic) backgroundTex += "_magic"; if (state == None) { if (!isMagic) backgroundTex = ""; } else if (state == Equip) { backgroundTex += "_equip"; } else if (state == Barter) backgroundTex += "_barter"; if (backgroundTex != "") backgroundTex += ".dds"; float scale = 1.f; if (!backgroundTex.empty()) { auto found = mScales.find(backgroundTex); if (found == mScales.end()) { // By default, background icons are supposed to use the 42x42 part of 64x64 image. // If the image has a different size, we should use a different chunk size // Cache result to do not retrieve background texture every frame. MyGUI::ITexture* texture = MyGUI::RenderManager::getInstance().getTexture(backgroundTex); if (texture) scale = texture->getHeight() / 64.f; mScales[backgroundTex] = scale; } else scale = found->second; } if (state == Barter && !isMagic) setFrame(backgroundTex, MyGUI::IntCoord(2*scale,2*scale,44*scale,44*scale)); else setFrame(backgroundTex, MyGUI::IntCoord(0,0,44*scale,44*scale)); setIcon(ptr); } void SpellWidget::setSpellIcon(const std::string& icon) { if (mFrame && !mCurrentFrame.empty()) { mCurrentFrame.clear(); mFrame->setImageTexture(""); } if (mCurrentIcon != icon) { mCurrentIcon = icon; if (mItemShadow) mItemShadow->setImageTexture(icon); if (mItem) mItem->setImageTexture(icon); } } } ================================================ FILE: apps/openmw/mwgui/itemwidget.hpp ================================================ #ifndef OPENMW_MWGUI_ITEMWIDGET_H #define OPENMW_MWGUI_ITEMWIDGET_H #include namespace MWWorld { class Ptr; } namespace MWGui { /// @brief A widget that shows an icon for an MWWorld::Ptr class ItemWidget : public MyGUI::Widget { MYGUI_RTTI_DERIVED(ItemWidget) public: ItemWidget(); /// Register needed components with MyGUI's factory manager static void registerComponents (); enum ItemState { None, Equip, Barter, Magic }; /// Set count to be displayed in a textbox over the item void setCount(int count); /// \a ptr may be empty void setItem (const MWWorld::Ptr& ptr, ItemState state = None); // Set icon and frame manually void setIcon (const std::string& icon); void setIcon (const MWWorld::Ptr& ptr); void setFrame (const std::string& frame, const MyGUI::IntCoord& coord); protected: void initialiseOverride() override; MyGUI::ImageBox* mItem; MyGUI::ImageBox* mItemShadow; MyGUI::ImageBox* mFrame; MyGUI::TextBox* mText; std::string mCurrentIcon; std::string mCurrentFrame; static std::map mScales; }; class SpellWidget : public ItemWidget { MYGUI_RTTI_DERIVED(SpellWidget) public: void setSpellIcon (const std::string& icon); }; } #endif ================================================ FILE: apps/openmw/mwgui/jailscreen.cpp ================================================ #include /* Start of tes3mp addition Include additional headers for multiplayer purposes */ #include "../mwmp/Main.hpp" #include "../mwmp/LocalPlayer.hpp" /* End of tes3mp addition */ #include #include "../mwbase/windowmanager.hpp" #include "../mwbase/mechanicsmanager.hpp" #include "../mwbase/world.hpp" #include "../mwbase/environment.hpp" #include "../mwmechanics/npcstats.hpp" #include "../mwmechanics/actorutil.hpp" #include "../mwworld/esmstore.hpp" #include "../mwworld/store.hpp" #include "../mwworld/class.hpp" #include "jailscreen.hpp" namespace MWGui { JailScreen::JailScreen() : WindowBase("openmw_jail_screen.layout"), mDays(1), mFadeTimeRemaining(0), mTimeAdvancer(0.01f) { getWidget(mProgressBar, "ProgressBar"); mTimeAdvancer.eventProgressChanged += MyGUI::newDelegate(this, &JailScreen::onJailProgressChanged); mTimeAdvancer.eventFinished += MyGUI::newDelegate(this, &JailScreen::onJailFinished); center(); } void JailScreen::goToJail(int days) { mDays = days; MWBase::Environment::get().getWindowManager()->fadeScreenOut(0.5); mFadeTimeRemaining = 0.5; setVisible(false); mProgressBar->setScrollRange(100+1); mProgressBar->setScrollPosition(0); mProgressBar->setTrackSize(0); /* Start of tes3mp addition If we've received a packet overriding the default jail progress text, use the new text */ if (!mwmp::Main::get().getLocalPlayer()->jailProgressText.empty()) setText("LoadingText", mwmp::Main::get().getLocalPlayer()->jailProgressText); /* End of tes3mp addition */ } void JailScreen::onFrame(float dt) { mTimeAdvancer.onFrame(dt); if (mFadeTimeRemaining <= 0) return; mFadeTimeRemaining -= dt; if (mFadeTimeRemaining <= 0) { MWWorld::Ptr player = MWMechanics::getPlayer(); /* Start of tes3mp change (minor) Prevent teleportation to jail if specified */ if (!mwmp::Main::get().getLocalPlayer()->ignoreJailTeleportation) { MWBase::Environment::get().getWorld()->teleportToClosestMarker(player, "prisonmarker"); MWBase::Environment::get().getWindowManager()->fadeScreenOut(0.f); // override fade-in caused by cell transition } /* End of tes3mp change (minor) */ setVisible(true); mTimeAdvancer.run(100); } } void JailScreen::onJailProgressChanged(int cur, int /*total*/) { mProgressBar->setScrollPosition(0); mProgressBar->setTrackSize(static_cast(cur / (float)(mProgressBar->getScrollRange()) * mProgressBar->getLineSize())); } void JailScreen::onJailFinished() { MWBase::Environment::get().getWindowManager()->removeGuiMode(MWGui::GM_Jail); MWBase::Environment::get().getWindowManager()->fadeScreenIn(0.5); MWWorld::Ptr player = MWMechanics::getPlayer(); /* Start of tes3mp addition Declare pointer to LocalPlayer for use in other additions */ mwmp::LocalPlayer* localPlayer = mwmp::Main::get().getLocalPlayer(); /* End of tes3mp addition */ MWBase::Environment::get().getMechanicsManager()->rest(mDays * 24, true); /* Start of tes3mp change (major) Multiplayer requires that time not get advanced here */ //MWBase::Environment::get().getWorld()->advanceTime(mDays * 24); /* End of tes3mp change (major) */ // We should not worsen corprus when in prison for (auto& spell : player.getClass().getCreatureStats(player).getCorprusSpells()) { spell.second.mNextWorsening += mDays * 24; } std::set skills; for (int day=0; dayignoreJailSkillIncreases) value.setBase(std::max(0.f, value.getBase()-1)); else if (skill == ESM::Skill::Security || skill == ESM::Skill::Sneak) /* End of tes3mp change (minor) */ value.setBase(std::min(100.f, value.getBase() + 1)); else value.setBase(std::max(0.f, value.getBase()-1)); } const MWWorld::Store& gmst = MWBase::Environment::get().getWorld()->getStore().get(); std::string message; if (mDays == 1) message = gmst.find("sNotifyMessage42")->mValue.getString(); else message = gmst.find("sNotifyMessage43")->mValue.getString(); /* Start of tes3mp addition If we've received a packet overriding the default jail end text, use the new text */ if (!localPlayer->jailEndText.empty()) message = mwmp::Main::get().getLocalPlayer()->jailEndText; /* End of tes3mp addition */ message = Misc::StringUtils::format(message, mDays); for (const int& skill : skills) { std::string skillName = gmst.find(ESM::Skill::sSkillNameIds[skill])->mValue.getString(); int skillValue = player.getClass().getNpcStats(player).getSkill(skill).getBase(); std::string skillMsg = gmst.find("sNotifyMessage44")->mValue.getString(); /* Start of tes3mp change (minor) Account for usage of ignoreJailSkillIncreases */ if (!localPlayer->ignoreJailSkillIncreases && (skill == ESM::Skill::Sneak || skill == ESM::Skill::Security)) /* End of tes3mp change (minor) */ skillMsg = gmst.find("sNotifyMessage39")->mValue.getString(); skillMsg = Misc::StringUtils::format(skillMsg, skillName, skillValue); message += "\n" + skillMsg; } /* Start of tes3mp addition Reset all PlayerJail-related overrides */ localPlayer->ignoreJailTeleportation = false; localPlayer->ignoreJailSkillIncreases = false; localPlayer->jailProgressText = ""; localPlayer->jailEndText = ""; /* End of tes3mp addition */ std::vector buttons; buttons.emplace_back("#{sOk}"); MWBase::Environment::get().getWindowManager()->interactiveMessageBox(message, buttons); } } ================================================ FILE: apps/openmw/mwgui/jailscreen.hpp ================================================ #ifndef MWGUI_JAILSCREEN_H #define MWGUI_JAILSCREEN_H #include "windowbase.hpp" #include "timeadvancer.hpp" namespace MWGui { class JailScreen : public WindowBase { public: JailScreen(); void goToJail(int days); void onFrame(float dt) override; bool exit() override { return false; } private: int mDays; float mFadeTimeRemaining; MyGUI::ScrollBar* mProgressBar; void onJailProgressChanged(int cur, int total); void onJailFinished(); TimeAdvancer mTimeAdvancer; }; } #endif ================================================ FILE: apps/openmw/mwgui/journalbooks.cpp ================================================ #include "journalbooks.hpp" #include "../mwbase/environment.hpp" #include "../mwbase/windowmanager.hpp" #include #include #include "textcolours.hpp" namespace { struct AddContent { MWGui::BookTypesetter::Ptr mTypesetter; MWGui::BookTypesetter::Style* mBodyStyle; AddContent (MWGui::BookTypesetter::Ptr typesetter, MWGui::BookTypesetter::Style* body_style) : mTypesetter (typesetter), mBodyStyle (body_style) { } }; struct AddSpan : AddContent { AddSpan (MWGui::BookTypesetter::Ptr typesetter, MWGui::BookTypesetter::Style* body_style) : AddContent (typesetter, body_style) { } void operator () (intptr_t topicId, size_t begin, size_t end) { MWGui::BookTypesetter::Style* style = mBodyStyle; const MWGui::TextColours& textColours = MWBase::Environment::get().getWindowManager()->getTextColours(); if (topicId) style = mTypesetter->createHotStyle (mBodyStyle, textColours.journalLink, textColours.journalLinkOver, textColours.journalLinkPressed, topicId); mTypesetter->write (style, begin, end); } }; struct AddEntry { MWGui::BookTypesetter::Ptr mTypesetter; MWGui::BookTypesetter::Style* mBodyStyle; AddEntry (MWGui::BookTypesetter::Ptr typesetter, MWGui::BookTypesetter::Style* body_style) : mTypesetter (typesetter), mBodyStyle (body_style) { } void operator () (MWGui::JournalViewModel::Entry const & entry) { mTypesetter->addContent (entry.body ()); entry.visitSpans (AddSpan (mTypesetter, mBodyStyle)); } }; struct AddJournalEntry : AddEntry { bool mAddHeader; MWGui::BookTypesetter::Style* mHeaderStyle; AddJournalEntry (MWGui::BookTypesetter::Ptr typesetter, MWGui::BookTypesetter::Style* body_style, MWGui::BookTypesetter::Style* header_style, bool add_header) : AddEntry (typesetter, body_style), mAddHeader (add_header), mHeaderStyle (header_style) { } void operator () (MWGui::JournalViewModel::JournalEntry const & entry) { if (mAddHeader) { mTypesetter->write (mHeaderStyle, entry.timestamp ()); mTypesetter->lineBreak (); } AddEntry::operator () (entry); mTypesetter->sectionBreak (30); } }; struct AddTopicEntry : AddEntry { intptr_t mContentId; MWGui::BookTypesetter::Style* mHeaderStyle; AddTopicEntry (MWGui::BookTypesetter::Ptr typesetter, MWGui::BookTypesetter::Style* body_style, MWGui::BookTypesetter::Style* header_style, intptr_t contentId) : AddEntry (typesetter, body_style), mContentId (contentId), mHeaderStyle (header_style) { } void operator () (MWGui::JournalViewModel::TopicEntry const & entry) { mTypesetter->write (mBodyStyle, entry.source ()); mTypesetter->write (mBodyStyle, 0, 3);// begin AddEntry::operator() (entry); mTypesetter->selectContent (mContentId); mTypesetter->write (mBodyStyle, 2, 3);// end quote mTypesetter->sectionBreak (30); } }; struct AddTopicName : AddContent { AddTopicName (MWGui::BookTypesetter::Ptr typesetter, MWGui::BookTypesetter::Style* style) : AddContent (typesetter, style) { } void operator () (MWGui::JournalViewModel::Utf8Span topicName) { mTypesetter->write (mBodyStyle, topicName); mTypesetter->sectionBreak (); } }; struct AddQuestName : AddContent { AddQuestName (MWGui::BookTypesetter::Ptr typesetter, MWGui::BookTypesetter::Style* style) : AddContent (typesetter, style) { } void operator () (MWGui::JournalViewModel::Utf8Span topicName) { mTypesetter->write (mBodyStyle, topicName); mTypesetter->sectionBreak (); } }; } namespace MWGui { MWGui::BookTypesetter::Utf8Span to_utf8_span (char const * text) { typedef MWGui::BookTypesetter::Utf8Point point; point begin = reinterpret_cast (text); return MWGui::BookTypesetter::Utf8Span (begin, begin + strlen (text)); } typedef TypesetBook::Ptr book; JournalBooks::JournalBooks (JournalViewModel::Ptr model, ToUTF8::FromType encoding) : mModel (model), mEncoding(encoding), mIndexPagesCount(0) { } book JournalBooks::createEmptyJournalBook () { BookTypesetter::Ptr typesetter = createTypesetter (); BookTypesetter::Style* header = typesetter->createStyle ("", MyGUI::Colour (0.60f, 0.00f, 0.00f)); BookTypesetter::Style* body = typesetter->createStyle ("", MyGUI::Colour::Black); typesetter->write (header, to_utf8_span ("You have no journal entries!")); typesetter->lineBreak (); typesetter->write (body, to_utf8_span ("You should have gone though the starting quest and got an initial quest.")); return typesetter->complete (); } book JournalBooks::createJournalBook () { BookTypesetter::Ptr typesetter = createTypesetter (); BookTypesetter::Style* header = typesetter->createStyle ("", MyGUI::Colour (0.60f, 0.00f, 0.00f)); BookTypesetter::Style* body = typesetter->createStyle ("", MyGUI::Colour::Black); mModel->visitJournalEntries ("", AddJournalEntry (typesetter, body, header, true)); return typesetter->complete (); } book JournalBooks::createTopicBook (uintptr_t topicId) { BookTypesetter::Ptr typesetter = createTypesetter (); BookTypesetter::Style* header = typesetter->createStyle ("", MyGUI::Colour (0.60f, 0.00f, 0.00f)); BookTypesetter::Style* body = typesetter->createStyle ("", MyGUI::Colour::Black); mModel->visitTopicName (topicId, AddTopicName (typesetter, header)); intptr_t contentId = typesetter->addContent (to_utf8_span (", \"")); mModel->visitTopicEntries (topicId, AddTopicEntry (typesetter, body, header, contentId)); return typesetter->complete (); } book JournalBooks::createQuestBook (const std::string& questName) { BookTypesetter::Ptr typesetter = createTypesetter (); BookTypesetter::Style* header = typesetter->createStyle ("", MyGUI::Colour (0.60f, 0.00f, 0.00f)); BookTypesetter::Style* body = typesetter->createStyle ("", MyGUI::Colour::Black); AddQuestName addName (typesetter, header); addName(to_utf8_span(questName.c_str())); mModel->visitJournalEntries (questName, AddJournalEntry (typesetter, body, header, false)); return typesetter->complete (); } book JournalBooks::createTopicIndexBook () { bool isRussian = (mEncoding == ToUTF8::WINDOWS_1251); BookTypesetter::Ptr typesetter = isRussian ? createCyrillicJournalIndex() : createLatinJournalIndex(); return typesetter->complete (); } BookTypesetter::Ptr JournalBooks::createLatinJournalIndex () { BookTypesetter::Ptr typesetter = BookTypesetter::create (92, 260); typesetter->setSectionAlignment (BookTypesetter::AlignCenter); // Latin journal index always has two columns for now. mIndexPagesCount = 2; char ch = 'A'; BookTypesetter::Style* body = typesetter->createStyle ("", MyGUI::Colour::Black); for (int i = 0; i < 26; ++i) { char buffer [32]; sprintf (buffer, "( %c )", ch); const MWGui::TextColours& textColours = MWBase::Environment::get().getWindowManager()->getTextColours(); BookTypesetter::Style* style = typesetter->createHotStyle (body, textColours.journalTopic, textColours.journalTopicOver, textColours.journalTopicPressed, (Utf8Stream::UnicodeChar) ch); if (i == 13) typesetter->sectionBreak (); typesetter->write (style, to_utf8_span (buffer)); typesetter->lineBreak (); ch++; } return typesetter; } BookTypesetter::Ptr JournalBooks::createCyrillicJournalIndex () { BookTypesetter::Ptr typesetter = BookTypesetter::create (92, 260); typesetter->setSectionAlignment (BookTypesetter::AlignCenter); BookTypesetter::Style* body = typesetter->createStyle ("", MyGUI::Colour::Black); int fontHeight = MWBase::Environment::get().getWindowManager()->getFontHeight(); // for small font size split alphabet to two columns (2x15 characers), for big font size split it to three colums (3x10 characters). int sectionBreak = 10; mIndexPagesCount = 3; if (fontHeight < 18) { sectionBreak = 15; mIndexPagesCount = 2; } unsigned char ch[3] = {0xd0, 0x90, 0x00}; // CYRILLIC CAPITAL A is a 0xd090 in UTF-8 for (int i = 0; i < 32; ++i) { char buffer [32]; sprintf(buffer, "( %c%c )", ch[0], ch[1]); Utf8Stream stream ((char*) ch); Utf8Stream::UnicodeChar first = stream.peek(); const MWGui::TextColours& textColours = MWBase::Environment::get().getWindowManager()->getTextColours(); BookTypesetter::Style* style = typesetter->createHotStyle (body, textColours.journalTopic, textColours.journalTopicOver, textColours.journalTopicPressed, first); ch[1]++; // Words can not be started with these characters if (i == 26 || i == 28) continue; if (i % sectionBreak == 0) typesetter->sectionBreak (); typesetter->write (style, to_utf8_span (buffer)); typesetter->lineBreak (); } return typesetter; } BookTypesetter::Ptr JournalBooks::createTypesetter () { //TODO: determine page size from layout... return BookTypesetter::create (240, 320); } } ================================================ FILE: apps/openmw/mwgui/journalbooks.hpp ================================================ #ifndef MWGUI_JOURNALBOOKS_HPP #define MWGUI_JOURNALBOOKS_HPP #include "bookpage.hpp" #include "journalviewmodel.hpp" #include namespace MWGui { MWGui::BookTypesetter::Utf8Span to_utf8_span (char const * text); struct JournalBooks { typedef TypesetBook::Ptr Book; JournalViewModel::Ptr mModel; JournalBooks (JournalViewModel::Ptr model, ToUTF8::FromType encoding); Book createEmptyJournalBook (); Book createJournalBook (); Book createTopicBook (uintptr_t topicId); Book createTopicBook (const std::string& topicId); Book createQuestBook (const std::string& questName); Book createTopicIndexBook (); ToUTF8::FromType mEncoding; int mIndexPagesCount; private: BookTypesetter::Ptr createTypesetter (); BookTypesetter::Ptr createLatinJournalIndex (); BookTypesetter::Ptr createCyrillicJournalIndex (); }; } #endif // MWGUI_JOURNALBOOKS_HPP ================================================ FILE: apps/openmw/mwgui/journalviewmodel.cpp ================================================ #include "journalviewmodel.hpp" #include #include #include #include #include #include "../mwbase/world.hpp" #include "../mwbase/journal.hpp" #include "../mwbase/environment.hpp" #include "../mwbase/windowmanager.hpp" #include "../mwdialogue/keywordsearch.hpp" namespace MWGui { struct JournalViewModelImpl; struct JournalViewModelImpl : JournalViewModel { typedef MWDialogue::KeywordSearch KeywordSearchT; mutable bool mKeywordSearchLoaded; mutable KeywordSearchT mKeywordSearch; JournalViewModelImpl () { mKeywordSearchLoaded = false; } virtual ~JournalViewModelImpl () { } /// \todo replace this nasty BS static Utf8Span toUtf8Span (std::string const & str) { if (str.size () == 0) return Utf8Span (Utf8Point (nullptr), Utf8Point (nullptr)); Utf8Point point = reinterpret_cast (str.c_str ()); return Utf8Span (point, point + str.size ()); } void load () override { } void unload () override { mKeywordSearch.clear (); mKeywordSearchLoaded = false; } void ensureKeyWordSearchLoaded () const { if (!mKeywordSearchLoaded) { MWBase::Journal * journal = MWBase::Environment::get().getJournal(); for(MWBase::Journal::TTopicIter i = journal->topicBegin(); i != journal->topicEnd (); ++i) mKeywordSearch.seed (i->first, intptr_t (&i->second)); mKeywordSearchLoaded = true; } } bool isEmpty () const override { MWBase::Journal * journal = MWBase::Environment::get().getJournal(); return journal->begin () == journal->end (); } template struct BaseEntry : Interface { typedef t_iterator iterator_t; iterator_t itr; JournalViewModelImpl const * mModel; BaseEntry (JournalViewModelImpl const * model, iterator_t itr) : itr (itr), mModel (model), loaded (false) {} virtual ~BaseEntry () {} mutable bool loaded; mutable std::string utf8text; typedef std::pair Range; // hyperlinks in @link# notation mutable std::map mHyperLinks; virtual std::string getText () const = 0; void ensureLoaded () const { if (!loaded) { mModel->ensureKeyWordSearchLoaded (); utf8text = getText (); size_t pos_end = 0; for(;;) { size_t pos_begin = utf8text.find('@'); if (pos_begin != std::string::npos) pos_end = utf8text.find('#', pos_begin); if (pos_begin != std::string::npos && pos_end != std::string::npos) { std::string link = utf8text.substr(pos_begin + 1, pos_end - pos_begin - 1); const char specialPseudoAsteriskCharacter = 127; std::replace(link.begin(), link.end(), specialPseudoAsteriskCharacter, '*'); std::string topicName = MWBase::Environment::get().getWindowManager()-> getTranslationDataStorage().topicStandardForm(link); std::string displayName = link; while (displayName[displayName.size()-1] == '*') displayName.erase(displayName.size()-1, 1); utf8text.replace(pos_begin, pos_end+1-pos_begin, displayName); intptr_t value = 0; if (mModel->mKeywordSearch.containsKeyword(topicName, value)) mHyperLinks[std::make_pair(pos_begin, pos_begin+displayName.size())] = value; } else break; } loaded = true; } } Utf8Span body () const override { ensureLoaded (); return toUtf8Span (utf8text); } void visitSpans (std::function < void (TopicId, size_t, size_t)> visitor) const override { ensureLoaded (); mModel->ensureKeyWordSearchLoaded (); if (mHyperLinks.size() && MWBase::Environment::get().getWindowManager()->getTranslationDataStorage().hasTranslation()) { size_t formatted = 0; // points to the first character that is not laid out yet for (std::map::const_iterator it = mHyperLinks.begin(); it != mHyperLinks.end(); ++it) { intptr_t topicId = it->second; if (formatted < it->first.first) visitor (0, formatted, it->first.first); visitor (topicId, it->first.first, it->first.second); formatted = it->first.second; } if (formatted < utf8text.size()) visitor (0, formatted, utf8text.size()); } else { std::vector matches; mModel->mKeywordSearch.highlightKeywords(utf8text.begin(), utf8text.end(), matches); std::string::const_iterator i = utf8text.begin (); for (std::vector::const_iterator it = matches.begin(); it != matches.end(); ++it) { const KeywordSearchT::Match& match = *it; if (i != match.mBeg) visitor (0, i - utf8text.begin (), match.mBeg - utf8text.begin ()); visitor (match.mValue, match.mBeg - utf8text.begin (), match.mEnd - utf8text.begin ()); i = match.mEnd; } if (i != utf8text.end ()) visitor (0, i - utf8text.begin (), utf8text.size ()); } } }; void visitQuestNames (bool active_only, std::function visitor) const override { MWBase::Journal * journal = MWBase::Environment::get ().getJournal (); std::set visitedQuests; // Note that for purposes of the journal GUI, quests are identified by the name, not the ID, so several // different quest IDs can end up in the same quest log. A quest log should be considered finished // when any quest ID in that log is finished. for (MWBase::Journal::TQuestIter i = journal->questBegin (); i != journal->questEnd (); ++i) { const MWDialogue::Quest& quest = i->second; bool isFinished = false; for (MWBase::Journal::TQuestIter j = journal->questBegin (); j != journal->questEnd (); ++j) { if (quest.getName() == j->second.getName() && j->second.isFinished()) isFinished = true; } if (active_only && isFinished) continue; // Unfortunately Morrowind.esm has no quest names, since the quest book was added with tribunal. // Note that even with Tribunal, some quests still don't have quest names. I'm assuming those are not supposed // to appear in the quest book. if (!quest.getName().empty()) { // Don't list the same quest name twice if (visitedQuests.find(quest.getName()) != visitedQuests.end()) continue; visitor (quest.getName(), isFinished); visitedQuests.insert(quest.getName()); } } } template struct JournalEntryImpl : BaseEntry { using BaseEntry ::itr; mutable std::string timestamp_buffer; JournalEntryImpl (JournalViewModelImpl const * model, iterator_t itr) : BaseEntry (model, itr) {} std::string getText () const override { return itr->getText(); } Utf8Span timestamp () const override { if (timestamp_buffer.empty ()) { std::string dayStr = MyGUI::LanguageManager::getInstance().replaceTags("#{sDay}"); std::ostringstream os; os << itr->mDayOfMonth << ' ' << MWBase::Environment::get().getWorld()->getMonthName (itr->mMonth) << " (" << dayStr << " " << (itr->mDay) << ')'; timestamp_buffer = os.str (); } return toUtf8Span (timestamp_buffer); } }; void visitJournalEntries (const std::string& questName, std::function visitor) const override { MWBase::Journal * journal = MWBase::Environment::get().getJournal(); if (!questName.empty()) { std::vector quests; for (MWBase::Journal::TQuestIter questIt = journal->questBegin(); questIt != journal->questEnd(); ++questIt) { if (Misc::StringUtils::ciEqual(questIt->second.getName(), questName)) quests.push_back(&questIt->second); } for(MWBase::Journal::TEntryIter i = journal->begin(); i != journal->end (); ++i) { for (std::vector::iterator questIt = quests.begin(); questIt != quests.end(); ++questIt) { MWDialogue::Quest const* quest = *questIt; for (MWDialogue::Topic::TEntryIter j = quest->begin (); j != quest->end (); ++j) { if (i->mInfoId == j->mInfoId) visitor (JournalEntryImpl (this, i)); } } } } else { for(MWBase::Journal::TEntryIter i = journal->begin(); i != journal->end (); ++i) visitor (JournalEntryImpl (this, i)); } } void visitTopicName (TopicId topicId, std::function visitor) const override { MWDialogue::Topic const & topic = * reinterpret_cast (topicId); visitor (toUtf8Span (topic.getName())); } void visitTopicNamesStartingWith (Utf8Stream::UnicodeChar character, std::function < void (const std::string&) > visitor) const override { MWBase::Journal * journal = MWBase::Environment::get().getJournal(); for (MWBase::Journal::TTopicIter i = journal->topicBegin (); i != journal->topicEnd (); ++i) { Utf8Stream stream (i->first.c_str()); Utf8Stream::UnicodeChar first = Misc::StringUtils::toLowerUtf8(stream.peek()); if (first != Misc::StringUtils::toLowerUtf8(character)) continue; visitor (i->second.getName()); } } struct TopicEntryImpl : BaseEntry { MWDialogue::Topic const & mTopic; TopicEntryImpl (JournalViewModelImpl const * model, MWDialogue::Topic const & topic, iterator_t itr) : BaseEntry (model, itr), mTopic (topic) {} std::string getText () const override { return itr->getText(); } Utf8Span source () const override { return toUtf8Span (itr->mActorName); } }; void visitTopicEntries (TopicId topicId, std::function visitor) const override { typedef MWDialogue::Topic::TEntryIter iterator_t; MWDialogue::Topic const & topic = * reinterpret_cast (topicId); for (iterator_t i = topic.begin (); i != topic.end (); ++i) visitor (TopicEntryImpl (this, topic, i)); } }; JournalViewModel::Ptr JournalViewModel::create () { return std::make_shared (); } } ================================================ FILE: apps/openmw/mwgui/journalviewmodel.hpp ================================================ #ifndef MWGUI_JOURNALVIEWMODEL_HPP #define MWGUI_JOURNALVIEWMODEL_HPP #include #include #include #include #include namespace MWGui { /// View-Model for the journal GUI /// /// This interface defines an abstract data model suited /// specifically to the needs of the journal GUI. It isolates /// the journal GUI from the implementation details of the /// game data store. struct JournalViewModel { typedef std::shared_ptr Ptr; typedef intptr_t QuestId; typedef intptr_t TopicId; typedef uint8_t const * Utf8Point; typedef std::pair Utf8Span; /// The base interface for both journal entries and topics. struct Entry { /// returns the body text for the journal entry /// /// This function returns a borrowed reference to the body of the /// journal entry. The returned reference becomes invalid when the /// entry is destroyed. virtual Utf8Span body () const = 0; /// Visits each subset of text in the body, delivering the beginning /// and end of the span relative to the body, and a valid topic ID if /// the span represents a keyword, or zero if not. virtual void visitSpans (std::function visitor) const = 0; virtual ~Entry() = default; }; /// An interface to topic data. struct TopicEntry : Entry { /// Returns a pre-formatted span of UTF8 encoded text representing /// the name of the NPC this portion of dialog was heard from. virtual Utf8Span source () const = 0; virtual ~TopicEntry() = default; }; /// An interface to journal data. struct JournalEntry : Entry { /// Returns a pre-formatted span of UTF8 encoded text representing /// the in-game date this entry was added to the journal. virtual Utf8Span timestamp () const = 0; virtual ~JournalEntry() = default; }; /// called prior to journal opening virtual void load () = 0; /// called prior to journal closing virtual void unload () = 0; /// returns true if their are no journal entries to display virtual bool isEmpty () const = 0; /// walks the active and optionally completed, quests providing the name and completed status virtual void visitQuestNames (bool active_only, std::function visitor) const = 0; /// walks over the journal entries related to all quests with the given name /// If \a questName is empty, simply visits all journal entries virtual void visitJournalEntries (const std::string& questName, std::function visitor) const = 0; /// provides the name of the topic specified by its id virtual void visitTopicName (TopicId topicId, std::function visitor) const = 0; /// walks over the topics whose names start with the character virtual void visitTopicNamesStartingWith (Utf8Stream::UnicodeChar character, std::function < void (const std::string&) > visitor) const = 0; /// walks over the topic entries for the topic specified by its identifier virtual void visitTopicEntries (TopicId topicId, std::function visitor) const = 0; // create an instance of the default journal view model implementation static Ptr create (); virtual ~JournalViewModel() = default; }; } #endif // MWGUI_JOURNALVIEWMODEL_HPP ================================================ FILE: apps/openmw/mwgui/journalwindow.cpp ================================================ #include "journalwindow.hpp" #include #include #include #include #include #include #include #include #include #include #include "../mwbase/environment.hpp" #include "../mwbase/windowmanager.hpp" #include "../mwbase/journal.hpp" #include "bookpage.hpp" #include "windowbase.hpp" #include "journalviewmodel.hpp" #include "journalbooks.hpp" namespace { static char const OptionsOverlay [] = "OptionsOverlay"; static char const OptionsBTN [] = "OptionsBTN"; static char const PrevPageBTN [] = "PrevPageBTN"; static char const NextPageBTN [] = "NextPageBTN"; static char const CloseBTN [] = "CloseBTN"; static char const JournalBTN [] = "JournalBTN"; static char const TopicsBTN [] = "TopicsBTN"; static char const QuestsBTN [] = "QuestsBTN"; static char const CancelBTN [] = "CancelBTN"; static char const ShowAllBTN [] = "ShowAllBTN"; static char const ShowActiveBTN [] = "ShowActiveBTN"; static char const PageOneNum [] = "PageOneNum"; static char const PageTwoNum [] = "PageTwoNum"; static char const TopicsList [] = "TopicsList"; static char const QuestsList [] = "QuestsList"; static char const LeftBookPage [] = "LeftBookPage"; static char const RightBookPage [] = "RightBookPage"; static char const LeftTopicIndex [] = "LeftTopicIndex"; static char const CenterTopicIndex [] = "CenterTopicIndex"; static char const RightTopicIndex [] = "RightTopicIndex"; struct JournalWindowImpl : MWGui::JournalBooks, MWGui::JournalWindow { struct DisplayState { unsigned int mPage; Book mBook; }; typedef std::stack DisplayStateStack; DisplayStateStack mStates; Book mTopicIndexBook; bool mQuestMode; bool mOptionsMode; bool mTopicsMode; bool mAllQuests; template T * getWidget (char const * name) { T * widget; WindowBase::getWidget (widget, name); return widget; } template void setText (char const * name, value_type const & value) { getWidget (name) -> setCaption (MyGUI::utility::toString (value)); } void setVisible (char const * name, bool visible) { getWidget (name) -> setVisible (visible); } void adviseButtonClick (char const * name, void (JournalWindowImpl::*Handler) (MyGUI::Widget* _sender)) { getWidget (name) -> eventMouseButtonClick += newDelegate(this, Handler); } void adviseKeyPress (char const * name, void (JournalWindowImpl::*Handler) (MyGUI::Widget* _sender, MyGUI::KeyCode key, MyGUI::Char character)) { getWidget (name) -> eventKeyButtonPressed += newDelegate(this, Handler); } MWGui::BookPage* getPage (char const * name) { return getWidget (name); } JournalWindowImpl (MWGui::JournalViewModel::Ptr Model, bool questList, ToUTF8::FromType encoding) : JournalBooks (Model, encoding), JournalWindow() { center(); adviseButtonClick (OptionsBTN, &JournalWindowImpl::notifyOptions ); adviseButtonClick (PrevPageBTN, &JournalWindowImpl::notifyPrevPage ); adviseButtonClick (NextPageBTN, &JournalWindowImpl::notifyNextPage ); adviseButtonClick (CloseBTN, &JournalWindowImpl::notifyClose ); adviseButtonClick (JournalBTN, &JournalWindowImpl::notifyJournal ); adviseButtonClick (TopicsBTN, &JournalWindowImpl::notifyTopics ); adviseButtonClick (QuestsBTN, &JournalWindowImpl::notifyQuests ); adviseButtonClick (CancelBTN, &JournalWindowImpl::notifyCancel ); adviseButtonClick (ShowAllBTN, &JournalWindowImpl::notifyShowAll ); adviseButtonClick (ShowActiveBTN, &JournalWindowImpl::notifyShowActive); adviseKeyPress (OptionsBTN, &JournalWindowImpl::notifyKeyPress); adviseKeyPress (PrevPageBTN, &JournalWindowImpl::notifyKeyPress); adviseKeyPress (NextPageBTN, &JournalWindowImpl::notifyKeyPress); adviseKeyPress (CloseBTN, &JournalWindowImpl::notifyKeyPress); adviseKeyPress (JournalBTN, &JournalWindowImpl::notifyKeyPress); Gui::MWList* list = getWidget(QuestsList); list->eventItemSelected += MyGUI::newDelegate(this, &JournalWindowImpl::notifyQuestClicked); Gui::MWList* topicsList = getWidget(TopicsList); topicsList->eventItemSelected += MyGUI::newDelegate(this, &JournalWindowImpl::notifyTopicSelected); { MWGui::BookPage::ClickCallback callback; callback = std::bind (&JournalWindowImpl::notifyTopicClicked, this, std::placeholders::_1); getPage (LeftBookPage)->adviseLinkClicked (callback); getPage (RightBookPage)->adviseLinkClicked (callback); getPage (LeftBookPage)->eventMouseWheel += MyGUI::newDelegate(this, &JournalWindowImpl::notifyMouseWheel); getPage (RightBookPage)->eventMouseWheel += MyGUI::newDelegate(this, &JournalWindowImpl::notifyMouseWheel); } { MWGui::BookPage::ClickCallback callback; callback = std::bind(&JournalWindowImpl::notifyIndexLinkClicked, this, std::placeholders::_1); getPage (LeftTopicIndex)->adviseLinkClicked (callback); getPage (CenterTopicIndex)->adviseLinkClicked (callback); getPage (RightTopicIndex)->adviseLinkClicked (callback); } adjustButton(PrevPageBTN); float nextButtonScale = adjustButton(NextPageBTN); adjustButton(CloseBTN); adjustButton(CancelBTN); adjustButton(JournalBTN); Gui::ImageButton* optionsButton = getWidget(OptionsBTN); Gui::ImageButton* showActiveButton = getWidget(ShowActiveBTN); Gui::ImageButton* showAllButton = getWidget(ShowAllBTN); Gui::ImageButton* questsButton = getWidget(QuestsBTN); Gui::ImageButton* nextButton = getWidget(NextPageBTN); if (nextButton->getSize().width == 64) { // english button has a 7 pixel wide strip of garbage on its right edge nextButton->setSize(64-7, nextButton->getSize().height); nextButton->setImageCoord(MyGUI::IntCoord(0,0,(64-7)*nextButtonScale,nextButton->getSize().height*nextButtonScale)); } if (!questList) { // If tribunal is not installed (-> no options button), we still want the Topics button available, // so place it where the options button would have been Gui::ImageButton* topicsButton = getWidget(TopicsBTN); topicsButton->detachFromWidget(); topicsButton->attachToWidget(optionsButton->getParent()); topicsButton->setPosition(optionsButton->getPosition()); topicsButton->eventMouseButtonClick.clear(); topicsButton->eventMouseButtonClick += MyGUI::newDelegate(this, &JournalWindowImpl::notifyOptions); optionsButton->setVisible(false); showActiveButton->setVisible(false); showAllButton->setVisible(false); questsButton->setVisible(false); adjustButton(TopicsBTN); } else { optionsButton->setImage("textures/tx_menubook_options.dds"); showActiveButton->setImage("textures/tx_menubook_quests_active.dds"); showAllButton->setImage("textures/tx_menubook_quests_all.dds"); questsButton->setImage("textures/tx_menubook_quests.dds"); adjustButton(ShowAllBTN); adjustButton(ShowActiveBTN); adjustButton(OptionsBTN); adjustButton(QuestsBTN); adjustButton(TopicsBTN); int topicsWidth = getWidget(TopicsBTN)->getSize().width; int cancelLeft = getWidget(CancelBTN)->getPosition().left; int cancelRight = getWidget(CancelBTN)->getPosition().left + getWidget(CancelBTN)->getSize().width; getWidget(QuestsBTN)->setPosition(cancelRight, getWidget(QuestsBTN)->getPosition().top); // Usually Topics, Quests, and Cancel buttons have the 64px width, so we can place the Topics left-up from the Cancel button, and the Quests right-up from the Cancel button. // But in some installations, e.g. German one, the Topics button has the 128px width, so we should place it exactly left from the Quests button. if (topicsWidth == 64) { getWidget(TopicsBTN)->setPosition(cancelLeft - topicsWidth, getWidget(TopicsBTN)->getPosition().top); } else { int questLeft = getWidget(QuestsBTN)->getPosition().left; getWidget(TopicsBTN)->setPosition(questLeft - topicsWidth, getWidget(TopicsBTN)->getPosition().top); } } mQuestMode = false; mAllQuests = false; mOptionsMode = false; mTopicsMode = false; } void onOpen() override { if (!MWBase::Environment::get().getWindowManager ()->getJournalAllowed ()) { MWBase::Environment::get().getWindowManager()->popGuiMode (); } mModel->load (); setBookMode (); Book journalBook; if (mModel->isEmpty ()) journalBook = createEmptyJournalBook (); else journalBook = createJournalBook (); pushBook (journalBook, 0); // fast forward to the last page if (!mStates.empty ()) { unsigned int & page = mStates.top ().mPage; page = mStates.top().mBook->pageCount()-1; if (page%2) --page; } updateShowingPages(); MWBase::Environment::get().getWindowManager()->setKeyFocusWidget(getWidget(CloseBTN)); } void onClose() override { mModel->unload (); getPage (LeftBookPage)->showPage (Book (), 0); getPage (RightBookPage)->showPage (Book (), 0); while (!mStates.empty ()) mStates.pop (); mTopicIndexBook.reset (); } void setVisible (bool newValue) override { WindowBase::setVisible (newValue); } void setBookMode () { mOptionsMode = false; mTopicsMode = false; setVisible (OptionsBTN, true); setVisible (OptionsOverlay, false); updateShowingPages (); updateCloseJournalButton (); } void setOptionsMode () { mOptionsMode = true; mTopicsMode = false; setVisible (OptionsBTN, false); setVisible (OptionsOverlay, true); setVisible (PrevPageBTN, false); setVisible (NextPageBTN, false); setVisible (CloseBTN, false); setVisible (JournalBTN, false); setVisible (TopicsList, false); setVisible (QuestsList, mQuestMode); setVisible (LeftTopicIndex, !mQuestMode); setVisible (CenterTopicIndex, !mQuestMode); setVisible (RightTopicIndex, !mQuestMode); setVisible (ShowAllBTN, mQuestMode && !mAllQuests); setVisible (ShowActiveBTN, mQuestMode && mAllQuests); //TODO: figure out how to make "options" page overlay book page // correctly, so that text may show underneath getPage (RightBookPage)->showPage (Book (), 0); // If in quest mode, ensure the quest list is updated if (mQuestMode) notifyQuests(getWidget(QuestsList)); else notifyTopics(getWidget(TopicsList)); } void pushBook (Book book, unsigned int page) { DisplayState bs; bs.mPage = page; bs.mBook = book; mStates.push (bs); updateShowingPages (); updateCloseJournalButton (); } void replaceBook (Book book, unsigned int page) { assert (!mStates.empty ()); mStates.top ().mBook = book; mStates.top ().mPage = page; updateShowingPages (); } void popBook () { mStates.pop (); updateShowingPages (); updateCloseJournalButton (); } void updateCloseJournalButton () { setVisible (CloseBTN, mStates.size () < 2); setVisible (JournalBTN, mStates.size () >= 2); } void updateShowingPages () { Book book; unsigned int page; unsigned int relPages; if (!mStates.empty ()) { book = mStates.top ().mBook; page = mStates.top ().mPage; relPages = book->pageCount () - page; } else { page = 0; relPages = 0; } MyGUI::Widget* nextPageBtn = getWidget(NextPageBTN); MyGUI::Widget* prevPageBtn = getWidget(PrevPageBTN); MyGUI::Widget* focus = MyGUI::InputManager::getInstance().getKeyFocusWidget(); bool nextPageVisible = relPages > 2; nextPageBtn->setVisible(nextPageVisible); bool prevPageVisible = page > 0; prevPageBtn->setVisible(prevPageVisible); if (focus == nextPageBtn && !nextPageVisible && prevPageVisible) MWBase::Environment::get().getWindowManager()->setKeyFocusWidget(prevPageBtn); else if (focus == prevPageBtn && !prevPageVisible && nextPageVisible) MWBase::Environment::get().getWindowManager()->setKeyFocusWidget(nextPageBtn); setVisible (PageOneNum, relPages > 0); setVisible (PageTwoNum, relPages > 1); getPage (LeftBookPage)->showPage ((relPages > 0) ? book : Book (), page+0); getPage (RightBookPage)->showPage ((relPages > 0) ? book : Book (), page+1); setText (PageOneNum, page + 1); setText (PageTwoNum, page + 2); } void notifyKeyPress(MyGUI::Widget* sender, MyGUI::KeyCode key, MyGUI::Char character) { if (key == MyGUI::KeyCode::ArrowUp) notifyPrevPage(sender); else if (key == MyGUI::KeyCode::ArrowDown) notifyNextPage(sender); } void notifyTopicClicked (intptr_t linkId) { Book topicBook = createTopicBook (linkId); if (mStates.size () > 1) replaceBook (topicBook, 0); else pushBook (topicBook, 0); setVisible (OptionsOverlay, false); setVisible (OptionsBTN, true); setVisible (JournalBTN, true); mOptionsMode = false; mTopicsMode = false; MWBase::Environment::get().getWindowManager()->playSound("book page"); } void notifyTopicSelected (const std::string& topic, int id) { const MWBase::Journal* journal = MWBase::Environment::get().getJournal(); intptr_t topicId = 0; /// \todo get rid of intptr ids for(MWBase::Journal::TTopicIter i = journal->topicBegin(); i != journal->topicEnd (); ++i) { if (Misc::StringUtils::ciEqual(i->first, topic)) topicId = intptr_t (&i->second); } notifyTopicClicked(topicId); } void notifyQuestClicked (const std::string& name, int id) { Book book = createQuestBook (name); if (mStates.size () > 1) replaceBook (book, 0); else pushBook (book, 0); setVisible (OptionsOverlay, false); setVisible (OptionsBTN, true); setVisible (JournalBTN, true); mOptionsMode = false; MWBase::Environment::get().getWindowManager()->playSound("book page"); } void notifyOptions(MyGUI::Widget* _sender) { setOptionsMode (); if (!mTopicIndexBook) mTopicIndexBook = createTopicIndexBook (); if (mIndexPagesCount == 3) { getPage (LeftTopicIndex)->showPage (mTopicIndexBook, 0); getPage (CenterTopicIndex)->showPage (mTopicIndexBook, 1); getPage (RightTopicIndex)->showPage (mTopicIndexBook, 2); } else { getPage (LeftTopicIndex)->showPage (mTopicIndexBook, 0); getPage (RightTopicIndex)->showPage (mTopicIndexBook, 1); } } void notifyJournal(MyGUI::Widget* _sender) { assert (mStates.size () > 1); popBook (); MWBase::Environment::get().getWindowManager()->playSound("book page"); } void notifyIndexLinkClicked (MWGui::TypesetBook::InteractiveId index) { setVisible (LeftTopicIndex, false); setVisible (CenterTopicIndex, false); setVisible (RightTopicIndex, false); setVisible (TopicsList, true); mTopicsMode = true; Gui::MWList* list = getWidget(TopicsList); list->clear(); AddNamesToList add(list); mModel->visitTopicNamesStartingWith(index, add); list->adjustSize(); MWBase::Environment::get().getWindowManager()->playSound("book page"); } void notifyTopics(MyGUI::Widget* _sender) { mQuestMode = false; mTopicsMode = false; setVisible (LeftTopicIndex, true); setVisible (CenterTopicIndex, true); setVisible (RightTopicIndex, true); setVisible (TopicsList, false); setVisible (QuestsList, false); setVisible (ShowAllBTN, false); setVisible (ShowActiveBTN, false); MWBase::Environment::get().getWindowManager()->playSound("book page"); } struct AddNamesToList { AddNamesToList(Gui::MWList* list) : mList(list) {} Gui::MWList* mList; void operator () (const std::string& name, bool finished=false) { mList->addItem(name); } }; struct SetNamesInactive { SetNamesInactive(Gui::MWList* list) : mList(list) {} Gui::MWList* mList; void operator () (const std::string& name, bool finished) { if (finished) { mList->getItemWidget(name)->setStateSelected(true); } } }; void notifyQuests(MyGUI::Widget* _sender) { mQuestMode = true; setVisible (LeftTopicIndex, false); setVisible (CenterTopicIndex, false); setVisible (RightTopicIndex, false); setVisible (TopicsList, false); setVisible (QuestsList, true); setVisible (ShowAllBTN, !mAllQuests); setVisible (ShowActiveBTN, mAllQuests); Gui::MWList* list = getWidget(QuestsList); list->clear(); AddNamesToList add(list); mModel->visitQuestNames(!mAllQuests, add); list->adjustSize(); if (mAllQuests) { SetNamesInactive setInactive(list); mModel->visitQuestNames(false, setInactive); } MWBase::Environment::get().getWindowManager()->playSound("book page"); } void notifyShowAll(MyGUI::Widget* _sender) { mAllQuests = true; notifyQuests(_sender); } void notifyShowActive(MyGUI::Widget* _sender) { mAllQuests = false; notifyQuests(_sender); } void notifyCancel(MyGUI::Widget* _sender) { if (mTopicsMode) { notifyTopics(_sender); } else { setBookMode(); MWBase::Environment::get().getWindowManager()->playSound("book page"); } } void notifyClose(MyGUI::Widget* _sender) { MWBase::WindowManager *winMgr = MWBase::Environment::get().getWindowManager(); winMgr->playSound("book close"); winMgr->popGuiMode(); } void notifyMouseWheel(MyGUI::Widget* sender, int rel) { if (rel < 0) notifyNextPage(sender); else notifyPrevPage(sender); } void notifyNextPage(MyGUI::Widget* _sender) { if (mOptionsMode) return; if (!mStates.empty ()) { unsigned int & page = mStates.top ().mPage; Book book = mStates.top ().mBook; if (page+2 < book->pageCount()) { MWBase::Environment::get().getWindowManager()->playSound("book page"); page += 2; updateShowingPages (); } } } void notifyPrevPage(MyGUI::Widget* _sender) { if (mOptionsMode) return; if (!mStates.empty ()) { unsigned int & page = mStates.top ().mPage; if(page >= 2) { MWBase::Environment::get().getWindowManager()->playSound("book page"); page -= 2; updateShowingPages (); } } } }; } // glue the implementation to the interface MWGui::JournalWindow * MWGui::JournalWindow::create (JournalViewModel::Ptr Model, bool questList, ToUTF8::FromType encoding) { return new JournalWindowImpl (Model, questList, encoding); } MWGui::JournalWindow::JournalWindow() : BookWindowBase("openmw_journal.layout") { } ================================================ FILE: apps/openmw/mwgui/journalwindow.hpp ================================================ #ifndef MWGUI_JOURNAL_H #define MWGUI_JOURNAL_H #include "windowbase.hpp" #include #include namespace MWBase { class WindowManager; } namespace MWGui { struct JournalViewModel; struct JournalWindow : public BookWindowBase { JournalWindow(); /// construct a new instance of the one JournalWindow implementation static JournalWindow * create (std::shared_ptr Model, bool questList, ToUTF8::FromType encoding); /// destroy this instance of the JournalWindow implementation virtual ~JournalWindow () {} /// show/hide the journal window void setVisible (bool newValue) override = 0; }; } #endif ================================================ FILE: apps/openmw/mwgui/keyboardnavigation.cpp ================================================ #include "keyboardnavigation.hpp" #include #include #include #include #include #include /* Start of tes3mp addition Include additional headers for multiplayer purposes */ #include "../mwmp/Main.hpp" #include "../mwmp/GUIController.hpp" /* End of tes3mp addition */ #include "../mwbase/windowmanager.hpp" #include "../mwbase/environment.hpp" namespace MWGui { bool shouldAcceptKeyFocus(MyGUI::Widget* w) { return w && !w->castType(false) && w->getInheritedEnabled() && w->getInheritedVisible() && w->getVisible() && w->getEnabled(); } /// Recursively get all child widgets that accept keyboard input void getKeyFocusWidgets(MyGUI::Widget* parent, std::vector& results) { assert(parent != nullptr); if (!parent->getVisible() || !parent->getEnabled()) return; MyGUI::EnumeratorWidgetPtr enumerator = parent->getEnumerator(); while (enumerator.next()) { MyGUI::Widget* w = enumerator.current(); if (!w->getVisible() || !w->getEnabled()) continue; if (w->getNeedKeyFocus() && shouldAcceptKeyFocus(w)) results.push_back(w); else getKeyFocusWidgets(w, results); } } KeyboardNavigation::KeyboardNavigation() : mCurrentFocus(nullptr) , mModalWindow(nullptr) , mEnabled(true) { MyGUI::WidgetManager::getInstance().registerUnlinker(this); } KeyboardNavigation::~KeyboardNavigation() { try { MyGUI::WidgetManager::getInstance().unregisterUnlinker(this); } catch(const MyGUI::Exception& e) { Log(Debug::Error) << "Error in the destructor: " << e.what(); } } void KeyboardNavigation::saveFocus(int mode) { MyGUI::Widget* focus = MyGUI::InputManager::getInstance().getKeyFocusWidget(); if (shouldAcceptKeyFocus(focus)) { mKeyFocus[mode] = focus; } else if(shouldAcceptKeyFocus(mCurrentFocus)) { mKeyFocus[mode] = mCurrentFocus; } } void KeyboardNavigation::restoreFocus(int mode) { std::map::const_iterator found = mKeyFocus.find(mode); if (found != mKeyFocus.end()) { MyGUI::Widget* w = found->second; if (w && w->getVisible() && w->getEnabled()) MWBase::Environment::get().getWindowManager()->setKeyFocusWidget(found->second); } } void KeyboardNavigation::_unlinkWidget(MyGUI::Widget *widget) { for (std::pair& w : mKeyFocus) if (w.second == widget) w.second = nullptr; if (widget == mCurrentFocus) mCurrentFocus = nullptr; } #if MYGUI_VERSION < MYGUI_DEFINE_VERSION(3,2,3) void styleFocusedButton(MyGUI::Widget* w) { if (w) { if (MyGUI::Button* b = w->castType(false)) { b->_setWidgetState("highlighted"); } } } #endif bool isRootParent(MyGUI::Widget* widget, MyGUI::Widget* root) { while (widget && widget->getParent()) widget = widget->getParent(); return widget == root; } void KeyboardNavigation::onFrame() { if (!mEnabled) return; /* Start of tes3mp change (major) Don't clear key focus widget when not in menus if the chat is currently focused */ if (!MWBase::Environment::get().getWindowManager()->isGuiMode() && !mwmp::Main::get().getGUIController()->getChatEditState()) /* End of tes3mp change (major) */ { MWBase::Environment::get().getWindowManager()->setKeyFocusWidget(nullptr); return; } MyGUI::Widget* focus = MyGUI::InputManager::getInstance().getKeyFocusWidget(); if (focus == mCurrentFocus) { #if MYGUI_VERSION < MYGUI_DEFINE_VERSION(3,2,3) styleFocusedButton(mCurrentFocus); #endif return; } // workaround incorrect key focus resets (fix in MyGUI TBD) if (!shouldAcceptKeyFocus(focus) && shouldAcceptKeyFocus(mCurrentFocus) && (!mModalWindow || isRootParent(mCurrentFocus, mModalWindow))) { MWBase::Environment::get().getWindowManager()->setKeyFocusWidget(mCurrentFocus); focus = mCurrentFocus; } if (focus != mCurrentFocus) { #if MYGUI_VERSION < MYGUI_DEFINE_VERSION(3,2,3) if (mCurrentFocus) { if (MyGUI::Button* b = mCurrentFocus->castType(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) { MyGUI::Widget* focus = MyGUI::InputManager::getInstance().getKeyFocusWidget(); if (!focus || !shouldAcceptKeyFocus(focus)) { MWBase::Environment::get().getWindowManager()->setKeyFocusWidget(defaultFocus); } else { if (!isRootParent(focus, window)) MWBase::Environment::get().getWindowManager()->setKeyFocusWidget(defaultFocus); } } void KeyboardNavigation::setModalWindow(MyGUI::Widget *window) { mModalWindow = window; } void KeyboardNavigation::setEnabled(bool enabled) { mEnabled = enabled; } enum Direction { D_Left, D_Up, D_Right, D_Down, D_Next, D_Prev }; bool KeyboardNavigation::injectKeyPress(MyGUI::KeyCode key, unsigned int text, bool repeat) { if (!mEnabled) return false; switch (key.getValue()) { case MyGUI::KeyCode::ArrowLeft: return switchFocus(D_Left, false); case MyGUI::KeyCode::ArrowRight: return switchFocus(D_Right, false); case MyGUI::KeyCode::ArrowUp: return switchFocus(D_Up, false); case MyGUI::KeyCode::ArrowDown: return switchFocus(D_Down, false); case MyGUI::KeyCode::Tab: return switchFocus(MyGUI::InputManager::getInstance().isShiftPressed() ? D_Prev : D_Next, true); case MyGUI::KeyCode::Return: case MyGUI::KeyCode::NumpadEnter: case MyGUI::KeyCode::Space: { // We should disable repeating for activation keys MyGUI::InputManager::getInstance().injectKeyRelease(MyGUI::KeyCode::None); if (repeat) return true; return accept(); } default: return false; } } bool KeyboardNavigation::switchFocus(int direction, bool wrap) { if (!MWBase::Environment::get().getWindowManager()->isGuiMode()) return false; MyGUI::Widget* focus = MyGUI::InputManager::getInstance().getKeyFocusWidget(); bool isCycle = (direction == D_Prev || direction == D_Next); if ((focus && focus->getTypeName().find("Button") == std::string::npos) && !isCycle) return false; if (focus && isCycle && focus->getUserString("AcceptTab") == "true") return false; if ((!focus || !focus->getNeedKeyFocus()) && isCycle) { // if nothing is selected, select the first widget return selectFirstWidget(); } if (!focus) return false; MyGUI::Widget* window = focus; while (window && window->getParent()) window = window->getParent(); MyGUI::VectorWidgetPtr keyFocusList; getKeyFocusWidgets(window, keyFocusList); if (keyFocusList.empty()) return false; MyGUI::VectorWidgetPtr::iterator found = std::find(keyFocusList.begin(), keyFocusList.end(), focus); if (found == keyFocusList.end()) { if (isCycle) return selectFirstWidget(); else return false; } bool forward = (direction == D_Next || direction == D_Right || direction == D_Down); int index = found - keyFocusList.begin(); index = forward ? (index+1) : (index-1); if (wrap) index = (index + keyFocusList.size())%keyFocusList.size(); else index = std::min(std::max(0, index), static_cast(keyFocusList.size())-1); MyGUI::Widget* next = keyFocusList[index]; int vertdiff = next->getTop() - focus->getTop(); int horizdiff = next->getLeft() - focus->getLeft(); bool isVertical = std::abs(vertdiff) > std::abs(horizdiff); if (direction == D_Right && (horizdiff <= 0 || isVertical)) return false; else if (direction == D_Left && (horizdiff >= 0 || isVertical)) return false; else if (direction == D_Down && (vertdiff <= 0 || !isVertical)) return false; else if (direction == D_Up && (vertdiff >= 0 || !isVertical)) return false; MWBase::Environment::get().getWindowManager()->setKeyFocusWidget(keyFocusList[index]); return true; } bool KeyboardNavigation::selectFirstWidget() { MyGUI::VectorWidgetPtr keyFocusList; MyGUI::EnumeratorWidgetPtr enumerator = MyGUI::Gui::getInstance().getEnumerator(); if (mModalWindow) enumerator = mModalWindow->getEnumerator(); while (enumerator.next()) getKeyFocusWidgets(enumerator.current(), keyFocusList); if (!keyFocusList.empty()) { MWBase::Environment::get().getWindowManager()->setKeyFocusWidget(keyFocusList[0]); return true; } return false; } bool KeyboardNavigation::accept() { MyGUI::Widget* focus = MyGUI::InputManager::getInstance().getKeyFocusWidget(); if (!focus) return false; //MyGUI::Button* button = focus->castType(false); //if (button && button->getEnabled()) if (focus->getTypeName().find("Button") != std::string::npos && focus->getEnabled()) { focus->eventMouseButtonClick(focus); return true; } return false; } } ================================================ FILE: apps/openmw/mwgui/keyboardnavigation.hpp ================================================ #ifndef OPENMW_MWGUI_KEYBOARDNAVIGATION_H #define OPENMW_MWGUI_KEYBOARDNAVIGATION_H #include #include namespace MWGui { class KeyboardNavigation : public MyGUI::IUnlinkWidget { public: KeyboardNavigation(); ~KeyboardNavigation(); /// @return Was the key handled by this class? bool injectKeyPress(MyGUI::KeyCode key, unsigned int text, bool repeat); void saveFocus(int mode); void restoreFocus(int mode); void _unlinkWidget(MyGUI::Widget* widget) override; void onFrame(); /// Set a key focus widget for this window, if one isn't already set. void setDefaultFocus(MyGUI::Widget* window, MyGUI::Widget* defaultFocus); void setModalWindow(MyGUI::Widget* window); void setEnabled(bool enabled); private: bool switchFocus(int direction, bool wrap); bool selectFirstWidget(); /// Send button press event to focused button bool accept(); std::map mKeyFocus; MyGUI::Widget* mCurrentFocus; MyGUI::Widget* mModalWindow; bool mEnabled; }; } #endif ================================================ FILE: apps/openmw/mwgui/layout.cpp ================================================ #include "layout.hpp" #include #include #include #include #include namespace MWGui { void Layout::initialise(const std::string& _layout, MyGUI::Widget* _parent) { const std::string MAIN_WINDOW = "_Main"; mLayoutName = _layout; if (mLayoutName.empty()) mMainWidget = _parent; else { mPrefix = MyGUI::utility::toString(this, "_"); mListWindowRoot = MyGUI::LayoutManager::getInstance().loadLayout(mLayoutName, mPrefix, _parent); const std::string main_name = mPrefix + MAIN_WINDOW; for (MyGUI::Widget* widget : mListWindowRoot) { if (widget->getName() == main_name) { mMainWidget = widget; break; } } MYGUI_ASSERT(mMainWidget, "root widget name '" << MAIN_WINDOW << "' in layout '" << mLayoutName << "' not found."); } } void Layout::shutdown() { setVisible(false); MyGUI::Gui::getInstance().destroyWidget(mMainWidget); mListWindowRoot.clear(); } void Layout::setCoord(int x, int y, int w, int h) { mMainWidget->setCoord(x,y,w,h); } void Layout::setVisible(bool b) { mMainWidget->setVisible(b); } void Layout::setText(const std::string &name, const std::string &caption) { MyGUI::Widget* pt; getWidget(pt, name); static_cast(pt)->setCaption(caption); } void Layout::setTitle(const std::string& title) { MyGUI::Window* window = static_cast(mMainWidget); if (window->getCaption() != title) window->setCaptionWithReplacing(title); } MyGUI::Widget* Layout::getWidget(const std::string &_name) { for (MyGUI::Widget* widget : mListWindowRoot) { MyGUI::Widget* find = widget->findWidget(mPrefix + _name); if (nullptr != find) { return find; } } MYGUI_EXCEPT("widget name '" << _name << "' in layout '" << mLayoutName << "' not found."); } } ================================================ FILE: apps/openmw/mwgui/layout.hpp ================================================ #ifndef OPENMW_MWGUI_LAYOUT_H #define OPENMW_MWGUI_LAYOUT_H #include #include #include #include namespace MWGui { /** The Layout class is an utility class used to load MyGUI layouts from xml files, and to manipulate member widgets. */ class Layout { public: Layout(const std::string & _layout, MyGUI::Widget* _parent = nullptr) : mMainWidget(nullptr) { initialise(_layout, _parent); } virtual ~Layout() { try { shutdown(); } catch(const MyGUI::Exception& e) { Log(Debug::Error) << "Error in the destructor: " << e.what(); } } MyGUI::Widget* getWidget(const std::string& _name); template void getWidget(T * & _widget, const std::string & _name) { MyGUI::Widget* w = getWidget(_name); T* cast = w->castType(false); if (!cast) { MYGUI_EXCEPT("Error cast : dest type = '" << T::getClassTypeName() << "' source name = '" << w->getName() << "' source type = '" << w->getTypeName() << "' in layout '" << mLayoutName << "'"); } else _widget = cast; } private: void initialise(const std::string & _layout, MyGUI::Widget* _parent = nullptr); void shutdown(); public: void setCoord(int x, int y, int w, int h); virtual void setVisible(bool b); void setText(const std::string& name, const std::string& caption); // NOTE: this assume that mMainWidget is of type Window. void setTitle(const std::string& title); MyGUI::Widget* mMainWidget; protected: std::string mPrefix; std::string mLayoutName; MyGUI::VectorWidgetPtr mListWindowRoot; }; } #endif ================================================ FILE: apps/openmw/mwgui/levelupdialog.cpp ================================================ #include "levelupdialog.hpp" #include #include #include #include #include "../mwbase/windowmanager.hpp" #include "../mwbase/environment.hpp" #include "../mwbase/world.hpp" #include "../mwbase/soundmanager.hpp" #include "../mwworld/class.hpp" #include "../mwmechanics/creaturestats.hpp" #include "../mwmechanics/npcstats.hpp" #include "../mwmechanics/actorutil.hpp" #include "class.hpp" namespace MWGui { const unsigned int LevelupDialog::sMaxCoins = 3; LevelupDialog::LevelupDialog() : WindowBase("openmw_levelup_dialog.layout"), mCoinCount(sMaxCoins) { getWidget(mOkButton, "OkButton"); getWidget(mClassImage, "ClassImage"); getWidget(mLevelText, "LevelText"); getWidget(mLevelDescription, "LevelDescription"); getWidget(mCoinBox, "Coins"); getWidget(mAssignWidget, "AssignWidget"); mOkButton->eventMouseButtonClick += MyGUI::newDelegate(this, &LevelupDialog::onOkButtonClicked); for (int i=1; i<9; ++i) { MyGUI::TextBox* t; getWidget(t, "AttribVal" + MyGUI::utility::toString(i)); mAttributeValues.push_back(t); MyGUI::Button* b; getWidget(b, "Attrib" + MyGUI::utility::toString(i)); b->setUserData (i-1); b->eventMouseButtonClick += MyGUI::newDelegate(this, &LevelupDialog::onAttributeClicked); mAttributes.push_back(b); getWidget(t, "AttribMultiplier" + MyGUI::utility::toString(i)); mAttributeMultipliers.push_back(t); } for (unsigned int i = 0; i < mCoinCount; ++i) { MyGUI::ImageBox* image = mCoinBox->createWidget("ImageBox", MyGUI::IntCoord(0,0,16,16), MyGUI::Align::Default); image->setImageTexture ("icons\\tx_goldicon.dds"); mCoins.push_back(image); } center(); } void LevelupDialog::setAttributeValues() { MWWorld::Ptr player = MWBase::Environment::get().getWorld ()->getPlayerPtr(); MWMechanics::CreatureStats& creatureStats = player.getClass().getCreatureStats(player); MWMechanics::NpcStats& pcStats = player.getClass().getNpcStats (player); for (int i = 0; i < 8; ++i) { int val = creatureStats.getAttribute(i).getBase(); if (std::find(mSpentAttributes.begin(), mSpentAttributes.end(), i) != mSpentAttributes.end()) { val += pcStats.getLevelupAttributeMultiplier(i); } if (val >= 100) val = 100; mAttributeValues[i]->setCaption(MyGUI::utility::toString(val)); } } void LevelupDialog::resetCoins() { const int coinSpacing = 33; int curX = mCoinBox->getWidth()/2 - (coinSpacing*(mCoinCount - 1) + 16*mCoinCount)/2; for (unsigned int i=0; idetachFromWidget(); image->attachToWidget(mCoinBox); if (i < mCoinCount) { mCoins[i]->setVisible(true); image->setCoord(MyGUI::IntCoord(curX,0,16,16)); curX += 16+coinSpacing; } else mCoins[i]->setVisible(false); } } void LevelupDialog::assignCoins() { resetCoins(); for (unsigned int i=0; idetachFromWidget(); image->attachToWidget(mAssignWidget); int attribute = mSpentAttributes[i]; int xdiff = mAttributeMultipliers[attribute]->getCaption() == "" ? 0 : 20; MyGUI::IntPoint pos = mAttributes[attribute]->getAbsolutePosition() - mAssignWidget->getAbsolutePosition() - MyGUI::IntPoint(22+xdiff,0); pos.top += (mAttributes[attribute]->getHeight() - image->getHeight())/2; image->setPosition(pos); } setAttributeValues(); } void LevelupDialog::onOpen() { MWBase::World *world = MWBase::Environment::get().getWorld(); MWWorld::Ptr player = world->getPlayerPtr(); MWMechanics::CreatureStats& creatureStats = player.getClass().getCreatureStats(player); MWMechanics::NpcStats& pcStats = player.getClass().getNpcStats(player); setClassImage(mClassImage, getLevelupClassImage(pcStats.getSkillIncreasesForSpecialization(0), pcStats.getSkillIncreasesForSpecialization(1), pcStats.getSkillIncreasesForSpecialization(2))); int level = creatureStats.getLevel ()+1; mLevelText->setCaptionWithReplacing("#{sLevelUpMenu1} " + MyGUI::utility::toString(level)); std::string levelupdescription; levelupdescription = Fallback::Map::getString("Level_Up_Level"+MyGUI::utility::toString(level)); if (levelupdescription == "") levelupdescription = Fallback::Map::getString("Level_Up_Default"); mLevelDescription->setCaption (levelupdescription); unsigned int availableAttributes = 0; for (int i = 0; i < 8; ++i) { MyGUI::TextBox* text = mAttributeMultipliers[i]; if (pcStats.getAttribute(i).getBase() < 100) { mAttributes[i]->setEnabled(true); mAttributeValues[i]->setEnabled(true); availableAttributes++; float mult = pcStats.getLevelupAttributeMultiplier (i); mult = std::min(mult, 100-pcStats.getAttribute(i).getBase()); text->setCaption(mult <= 1 ? "" : "x" + MyGUI::utility::toString(mult)); } else { mAttributes[i]->setEnabled(false); mAttributeValues[i]->setEnabled(false); text->setCaption(""); } } mCoinCount = std::min(sMaxCoins, availableAttributes); mSpentAttributes.clear(); resetCoins(); setAttributeValues(); center(); // Play LevelUp Music MWBase::Environment::get().getSoundManager()->streamMusic("Special/MW_Triumph.mp3"); } void LevelupDialog::onOkButtonClicked(MyGUI::Widget* sender) { MWWorld::Ptr player = MWMechanics::getPlayer(); MWMechanics::NpcStats& pcStats = player.getClass().getNpcStats (player); if (mSpentAttributes.size() < mCoinCount) MWBase::Environment::get().getWindowManager()->messageBox("#{sNotifyMessage36}"); else { // increase attributes for (unsigned int i = 0; i < mCoinCount; ++i) { MWMechanics::AttributeValue attribute = pcStats.getAttribute(mSpentAttributes[i]); attribute.setBase(attribute.getBase() + pcStats.getLevelupAttributeMultiplier(mSpentAttributes[i])); if (attribute.getBase() >= 100) attribute.setBase(100); pcStats.setAttribute(mSpentAttributes[i], attribute); } pcStats.levelUp(); MWBase::Environment::get().getWindowManager()->removeGuiMode(GM_Levelup); } } void LevelupDialog::onAttributeClicked(MyGUI::Widget *sender) { int attribute = *sender->getUserData(); std::vector::iterator found = std::find(mSpentAttributes.begin(), mSpentAttributes.end(), attribute); if (found != mSpentAttributes.end()) mSpentAttributes.erase(found); else { if (mSpentAttributes.size() == mCoinCount) mSpentAttributes[mCoinCount - 1] = attribute; else mSpentAttributes.push_back(attribute); } assignCoins(); } std::string LevelupDialog::getLevelupClassImage(const int combatIncreases, const int magicIncreases, const int stealthIncreases) { std::string ret = "acrobat"; int total = combatIncreases + magicIncreases + stealthIncreases; if (total == 0) return ret; int combatFraction = static_cast(static_cast(combatIncreases) / total * 10.f); int magicFraction = static_cast(static_cast(magicIncreases) / total * 10.f); int stealthFraction = static_cast(static_cast(stealthIncreases) / total * 10.f); if (combatFraction > 7) ret = "warrior"; else if (magicFraction > 7) ret = "mage"; else if (stealthFraction > 7) ret = "thief"; switch (combatFraction) { case 7: ret = "warrior"; break; case 6: if (stealthFraction == 1) ret = "barbarian"; else if (stealthFraction == 3) ret = "crusader"; else ret = "knight"; break; case 5: if (stealthFraction == 3) ret = "scout"; else ret = "archer"; break; case 4: ret = "rogue"; break; default: break; } switch (magicFraction) { case 7: ret = "mage"; break; case 6: if (combatFraction == 2) ret = "sorcerer"; else if (combatIncreases == 3) ret = "healer"; else ret = "battlemage"; break; case 5: ret = "witchhunter"; break; case 4: ret = "spellsword"; // In vanilla there's also code for "nightblade", however it seems to be unreachable. break; default: break; } switch (stealthFraction) { case 7: ret = "thief"; break; case 6: if (magicFraction == 1) ret = "agent"; else if (magicIncreases == 3) ret = "assassin"; else ret = "acrobat"; break; case 5: if (magicIncreases == 3) ret = "monk"; else ret = "pilgrim"; break; case 3: if (magicFraction == 3) ret = "bard"; break; default: break; } return ret; } } ================================================ FILE: apps/openmw/mwgui/levelupdialog.hpp ================================================ #ifndef MWGUI_LEVELUPDIALOG_H #define MWGUI_LEVELUPDIALOG_H #include "windowbase.hpp" namespace MWGui { class LevelupDialog : public WindowBase { public: LevelupDialog(); void onOpen() override; private: MyGUI::Button* mOkButton; MyGUI::ImageBox* mClassImage; MyGUI::TextBox* mLevelText; MyGUI::EditBox* mLevelDescription; MyGUI::Widget* mCoinBox; MyGUI::Widget* mAssignWidget; std::vector mAttributes; std::vector mAttributeValues; std::vector mAttributeMultipliers; std::vector mCoins; std::vector mSpentAttributes; unsigned int mCoinCount; static const unsigned int sMaxCoins; void onOkButtonClicked(MyGUI::Widget* sender); void onAttributeClicked(MyGUI::Widget* sender); void assignCoins(); void resetCoins(); void setAttributeValues(); std::string getLevelupClassImage(const int combatIncreases, const int magicIncreases, const int stealthIncreases); }; } #endif ================================================ FILE: apps/openmw/mwgui/loadingscreen.cpp ================================================ #include "loadingscreen.hpp" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "../mwbase/environment.hpp" #include "../mwbase/statemanager.hpp" #include "../mwbase/windowmanager.hpp" #include "../mwbase/inputmanager.hpp" #include "backgroundimage.hpp" namespace MWGui { LoadingScreen::LoadingScreen(Resource::ResourceSystem* resourceSystem, osgViewer::Viewer* viewer) : WindowBase("openmw_loading_screen.layout") , mResourceSystem(resourceSystem) , mViewer(viewer) , mTargetFrameRate(120.0) , mLastWallpaperChangeTime(0.0) , mLastRenderTime(0.0) , mLoadingOnTime(0.0) , mImportantLabel(false) , mVisible(false) , mNestedLoadingCount(0) , mProgress(0) , mShowWallpaper(true) { mMainWidget->setSize(MyGUI::RenderManager::getInstance().getViewSize()); getWidget(mLoadingText, "LoadingText"); getWidget(mProgressBar, "ProgressBar"); getWidget(mLoadingBox, "LoadingBox"); mProgressBar->setScrollViewPage(1); mBackgroundImage = MyGUI::Gui::getInstance().createWidgetReal("ImageBox", 0,0,1,1, MyGUI::Align::Stretch, "Menu"); mSceneImage = MyGUI::Gui::getInstance().createWidgetReal("ImageBox", 0,0,1,1, MyGUI::Align::Stretch, "Scene"); findSplashScreens(); } LoadingScreen::~LoadingScreen() { } void LoadingScreen::findSplashScreens() { const std::map& index = mResourceSystem->getVFS()->getIndex(); std::string pattern = "Splash/"; mResourceSystem->getVFS()->normalizeFilename(pattern); /* priority given to the left */ const std::array supported_extensions {{".tga", ".dds", ".ktx", ".png", ".bmp", ".jpeg", ".jpg"}}; auto found = index.lower_bound(pattern); while (found != index.end()) { const std::string& name = found->first; if (name.size() >= pattern.size() && name.substr(0, pattern.size()) == pattern) { size_t pos = name.find_last_of('.'); if (pos != std::string::npos) { for(auto const& extension: supported_extensions) { if (name.compare(pos, name.size() - pos, extension) == 0) { mSplashScreens.push_back(found->first); break; /* based on priority */ } } } } else break; ++found; } if (mSplashScreens.empty()) Log(Debug::Warning) << "Warning: no splash screens found!"; } void LoadingScreen::setLabel(const std::string &label, bool important) { mImportantLabel = important; mLoadingText->setCaptionWithReplacing(label); int padding = mLoadingBox->getWidth() - mLoadingText->getWidth(); MyGUI::IntSize size(mLoadingText->getTextSize().width+padding, mLoadingBox->getHeight()); size.width = std::max(300, size.width); mLoadingBox->setSize(size); if (MWBase::Environment::get().getWindowManager()->getMessagesCount() > 0) mLoadingBox->setPosition(mMainWidget->getWidth()/2 - mLoadingBox->getWidth()/2, mMainWidget->getHeight()/2 - mLoadingBox->getHeight()/2); else mLoadingBox->setPosition(mMainWidget->getWidth()/2 - mLoadingBox->getWidth()/2, mMainWidget->getHeight() - mLoadingBox->getHeight() - 8); } void LoadingScreen::setVisible(bool visible) { WindowBase::setVisible(visible); mBackgroundImage->setVisible(visible); mSceneImage->setVisible(visible); } double LoadingScreen::getTargetFrameRate() const { double frameRateLimit = MWBase::Environment::get().getFrameRateLimit(); if (frameRateLimit > 0) return std::min(frameRateLimit, mTargetFrameRate); else return mTargetFrameRate; } class CopyFramebufferToTextureCallback : public osg::Camera::DrawCallback { public: CopyFramebufferToTextureCallback(osg::Texture2D* texture) : mOneshot(true) , mTexture(texture) { } void operator () (osg::RenderInfo& renderInfo) const override { int w = renderInfo.getCurrentCamera()->getViewport()->width(); int h = renderInfo.getCurrentCamera()->getViewport()->height(); mTexture->copyTexImage2D(*renderInfo.getState(), 0, 0, w, h); mOneshot = false; } void reset() { mOneshot = true; } private: mutable bool mOneshot; osg::ref_ptr mTexture; }; class DontComputeBoundCallback : public osg::Node::ComputeBoundingSphereCallback { public: osg::BoundingSphere computeBound(const osg::Node&) const override { return osg::BoundingSphere(); } }; void LoadingScreen::loadingOn(bool visible) { // Early-out if already on if (mNestedLoadingCount++ > 0 && mMainWidget->getVisible()) return; mLoadingOnTime = mTimer.time_m(); // Assign dummy bounding sphere callback to avoid the bounding sphere of the entire scene being recomputed after each frame of loading // We are already using node masks to avoid the scene from being updated/rendered, but node masks don't work for computeBound() mViewer->getSceneData()->setComputeBoundingSphereCallback(new DontComputeBoundCallback); if (const osgUtil::IncrementalCompileOperation* ico = mViewer->getIncrementalCompileOperation()) { mOldIcoMin = ico->getMinimumTimeAvailableForGLCompileAndDeletePerFrame(); mOldIcoMax = ico->getMaximumNumOfObjectsToCompilePerFrame(); } mVisible = visible; mLoadingBox->setVisible(mVisible); setVisible(true); if (!mVisible) { mShowWallpaper = false; draw(); return; } mShowWallpaper = MWBase::Environment::get().getStateManager()->getState() == MWBase::StateManager::State_NoGame; if (mShowWallpaper) { changeWallpaper(); } MWBase::Environment::get().getWindowManager()->pushGuiMode(mShowWallpaper ? GM_LoadingWallpaper : GM_Loading); } void LoadingScreen::loadingOff() { if (--mNestedLoadingCount > 0) return; mLoadingBox->setVisible(true); // restore if (mLastRenderTime < mLoadingOnTime) { // the loading was so fast that we didn't show loading screen at all // we may still want to show the label if the caller requested it if (mImportantLabel) { MWBase::Environment::get().getWindowManager()->messageBox(mLoadingText->getCaption()); mImportantLabel = false; } } else mImportantLabel = false; // label was already shown on loading screen mViewer->getSceneData()->setComputeBoundingSphereCallback(nullptr); mViewer->getSceneData()->dirtyBound(); //std::cout << "loading took " << mTimer.time_m() - mLoadingOnTime << std::endl; setVisible(false); if (osgUtil::IncrementalCompileOperation* ico = mViewer->getIncrementalCompileOperation()) { ico->setMinimumTimeAvailableForGLCompileAndDeletePerFrame(mOldIcoMin); ico->setMaximumNumOfObjectsToCompilePerFrame(mOldIcoMax); } MWBase::Environment::get().getWindowManager()->removeGuiMode(GM_Loading); MWBase::Environment::get().getWindowManager()->removeGuiMode(GM_LoadingWallpaper); } void LoadingScreen::changeWallpaper () { if (!mSplashScreens.empty()) { std::string const & randomSplash = mSplashScreens.at(Misc::Rng::rollDice(mSplashScreens.size())); // TODO: add option (filename pattern?) to use image aspect ratio instead of 4:3 // we can't do this by default, because the Morrowind splash screens are 1024x1024, but should be displayed as 4:3 bool stretch = Settings::Manager::getBool("stretch menu background", "GUI"); mBackgroundImage->setVisible(true); mBackgroundImage->setBackgroundImage(randomSplash, true, stretch); } mSceneImage->setBackgroundImage(""); mSceneImage->setVisible(false); } void LoadingScreen::setProgressRange (size_t range) { mProgressBar->setScrollRange(range+1); mProgressBar->setScrollPosition(0); mProgressBar->setTrackSize(0); mProgress = 0; } void LoadingScreen::setProgress (size_t value) { // skip expensive update if there isn't enough visible progress if (mProgressBar->getWidth() <= 0 || value - mProgress < mProgressBar->getScrollRange()/mProgressBar->getWidth()) return; value = std::min(value, mProgressBar->getScrollRange()-1); mProgress = value; mProgressBar->setScrollPosition(0); mProgressBar->setTrackSize(static_cast(value / (float)(mProgressBar->getScrollRange()) * mProgressBar->getLineSize())); draw(); } void LoadingScreen::increaseProgress (size_t increase) { mProgressBar->setScrollPosition(0); size_t value = mProgress + increase; value = std::min(value, mProgressBar->getScrollRange()-1); mProgress = value; mProgressBar->setTrackSize(static_cast(value / (float)(mProgressBar->getScrollRange()) * mProgressBar->getLineSize())); draw(); } bool LoadingScreen::needToDrawLoadingScreen() { if ( mTimer.time_m() <= mLastRenderTime + (1.0/getTargetFrameRate()) * 1000.0) return false; // the minimal delay before a loading screen shows const float initialDelay = 0.05; bool alreadyShown = (mLastRenderTime > mLoadingOnTime); float diff = (mTimer.time_m() - mLoadingOnTime); if (!alreadyShown) { // bump the delay by the current progress - i.e. if during the initial delay the loading // has almost finished, no point showing the loading screen now diff -= mProgress / static_cast(mProgressBar->getScrollRange()) * 100.f; } if (!mShowWallpaper && diff < initialDelay*1000) return false; return true; } void LoadingScreen::setupCopyFramebufferToTextureCallback() { // Copy the current framebuffer onto a texture and display that texture as the background image // Note, we could also set the camera to disable clearing and have the background image transparent, // but then we get shaking effects on buffer swaps. if (!mTexture) { mTexture = new osg::Texture2D; mTexture->setInternalFormat(GL_RGB); mTexture->setResizeNonPowerOfTwoHint(false); } if (!mGuiTexture.get()) { mGuiTexture.reset(new osgMyGUI::OSGTexture(mTexture)); } if (!mCopyFramebufferToTextureCallback) { mCopyFramebufferToTextureCallback = new CopyFramebufferToTextureCallback(mTexture); } #if OSG_VERSION_GREATER_OR_EQUAL(3, 5, 10) mViewer->getCamera()->removeInitialDrawCallback(mCopyFramebufferToTextureCallback); mViewer->getCamera()->addInitialDrawCallback(mCopyFramebufferToTextureCallback); #else mViewer->getCamera()->setInitialDrawCallback(mCopyFramebufferToTextureCallback); #endif mCopyFramebufferToTextureCallback->reset(); mBackgroundImage->setBackgroundImage(""); mBackgroundImage->setVisible(false); mSceneImage->setRenderItemTexture(mGuiTexture.get()); mSceneImage->getSubWidgetMain()->_setUVSet(MyGUI::FloatRect(0.f, 0.f, 1.f, 1.f)); mSceneImage->setVisible(true); } void LoadingScreen::draw() { if (mVisible && !needToDrawLoadingScreen()) return; if (mShowWallpaper && mTimer.time_m() > mLastWallpaperChangeTime + 5000*1) { mLastWallpaperChangeTime = mTimer.time_m(); changeWallpaper(); } if (!mShowWallpaper && mLastRenderTime < mLoadingOnTime) { setupCopyFramebufferToTextureCallback(); } MWBase::Environment::get().getInputManager()->update(0, true, true); mResourceSystem->reportStats(mViewer->getFrameStamp()->getFrameNumber(), mViewer->getViewerStats()); if (osgUtil::IncrementalCompileOperation* ico = mViewer->getIncrementalCompileOperation()) { ico->setMinimumTimeAvailableForGLCompileAndDeletePerFrame(1.f/getTargetFrameRate()); ico->setMaximumNumOfObjectsToCompilePerFrame(1000); } // at the time this function is called we are in the middle of a frame, // so out of order calls are necessary to get a correct frameNumber for the next frame. // refer to the advance() and frame() order in Engine::go() mViewer->eventTraversal(); mViewer->updateTraversal(); mViewer->renderingTraversals(); mViewer->advance(mViewer->getFrameStamp()->getSimulationTime()); mLastRenderTime = mTimer.time_m(); } } ================================================ FILE: apps/openmw/mwgui/loadingscreen.hpp ================================================ #ifndef MWGUI_LOADINGSCREEN_H #define MWGUI_LOADINGSCREEN_H #include #include #include #include "windowbase.hpp" #include namespace osgViewer { class Viewer; } namespace osg { class Texture2D; } namespace Resource { class ResourceSystem; } namespace MWGui { class BackgroundImage; class CopyFramebufferToTextureCallback; class LoadingScreen : public WindowBase, public Loading::Listener { public: LoadingScreen(Resource::ResourceSystem* resourceSystem, osgViewer::Viewer* viewer); virtual ~LoadingScreen(); /// Overridden from Loading::Listener, see the Loading::Listener documentation for usage details void setLabel (const std::string& label, bool important) override; void loadingOn(bool visible=true) override; void loadingOff() override; void setProgressRange (size_t range) override; void setProgress (size_t value) override; void increaseProgress (size_t increase=1) override; void setVisible(bool visible) override; double getTargetFrameRate() const; private: void findSplashScreens(); bool needToDrawLoadingScreen(); void setupCopyFramebufferToTextureCallback(); Resource::ResourceSystem* mResourceSystem; osg::ref_ptr mViewer; double mTargetFrameRate; double mLastWallpaperChangeTime; double mLastRenderTime; osg::Timer mTimer; double mLoadingOnTime; bool mImportantLabel; bool mVisible; int mNestedLoadingCount; size_t mProgress; bool mShowWallpaper; float mOldIcoMin = 0.f; unsigned int mOldIcoMax = 0; MyGUI::Widget* mLoadingBox; MyGUI::TextBox* mLoadingText; MyGUI::ScrollBar* mProgressBar; BackgroundImage* mBackgroundImage; BackgroundImage* mSceneImage; std::vector mSplashScreens; osg::ref_ptr mTexture; osg::ref_ptr mCopyFramebufferToTextureCallback; std::unique_ptr mGuiTexture; void changeWallpaper(); void draw(); }; } #endif ================================================ FILE: apps/openmw/mwgui/mainmenu.cpp ================================================ #include "mainmenu.hpp" #include #include #include #include #include #include #include "../mwbase/environment.hpp" #include "../mwbase/windowmanager.hpp" #include "../mwbase/world.hpp" #include "../mwbase/statemanager.hpp" #include "savegamedialog.hpp" #include "confirmationdialog.hpp" #include "backgroundimage.hpp" #include "videowidget.hpp" namespace MWGui { MainMenu::MainMenu(int w, int h, const VFS::Manager* vfs, const std::string& versionDescription) : WindowBase("openmw_mainmenu.layout") , mWidth (w), mHeight (h) , mVFS(vfs), mButtonBox(nullptr) , mBackground(nullptr) , mVideoBackground(nullptr) , mVideo(nullptr) , mSaveGameDialog(nullptr) { getWidget(mVersionText, "VersionText"); mVersionText->setCaption(versionDescription); mHasAnimatedMenu = mVFS->exists("video/menu_background.bik"); updateMenu(); } MainMenu::~MainMenu() { delete mSaveGameDialog; } void MainMenu::onResChange(int w, int h) { mWidth = w; mHeight = h; updateMenu(); } void MainMenu::setVisible (bool visible) { if (visible) updateMenu(); bool isMainMenu = MWBase::Environment::get().getWindowManager()->containsMode(MWGui::GM_MainMenu) && MWBase::Environment::get().getStateManager()->getState() == MWBase::StateManager::State_NoGame; showBackground(isMainMenu); if (visible) { if (isMainMenu) { if (mButtons["loadgame"]->getVisible()) MWBase::Environment::get().getWindowManager()->setKeyFocusWidget(mButtons["loadgame"]); else MWBase::Environment::get().getWindowManager()->setKeyFocusWidget(mButtons["newgame"]); } else MWBase::Environment::get().getWindowManager()->setKeyFocusWidget(mButtons["return"]); } Layout::setVisible (visible); } void MainMenu::onNewGameConfirmed() { MWBase::Environment::get().getWindowManager()->removeGuiMode (MWGui::GM_MainMenu); MWBase::Environment::get().getStateManager()->newGame(); } void MainMenu::onExitConfirmed() { MWBase::Environment::get().getStateManager()->requestQuit(); } void MainMenu::onButtonClicked(MyGUI::Widget *sender) { MWBase::WindowManager *winMgr = MWBase::Environment::get().getWindowManager(); std::string name = *sender->getUserData(); winMgr->playSound("Menu Click"); if (name == "return") { winMgr->removeGuiMode (GM_MainMenu); } else if (name == "options") winMgr->pushGuiMode (GM_Settings); else if (name == "credits") winMgr->playVideo("mw_credits.bik", true); else if (name == "exitgame") { if (MWBase::Environment::get().getStateManager()->getState() == MWBase::StateManager::State_NoGame) onExitConfirmed(); else { ConfirmationDialog* dialog = winMgr->getConfirmationDialog(); dialog->askForConfirmation("#{sMessage2}"); dialog->eventOkClicked.clear(); dialog->eventOkClicked += MyGUI::newDelegate(this, &MainMenu::onExitConfirmed); dialog->eventCancelClicked.clear(); } } else if (name == "newgame") { if (MWBase::Environment::get().getStateManager()->getState() == MWBase::StateManager::State_NoGame) onNewGameConfirmed(); else { ConfirmationDialog* dialog = winMgr->getConfirmationDialog(); dialog->askForConfirmation("#{sNotifyMessage54}"); dialog->eventOkClicked.clear(); dialog->eventOkClicked += MyGUI::newDelegate(this, &MainMenu::onNewGameConfirmed); dialog->eventCancelClicked.clear(); } } else { if (!mSaveGameDialog) mSaveGameDialog = new SaveGameDialog(); if (name == "loadgame") mSaveGameDialog->setLoadOrSave(true); else if (name == "savegame") mSaveGameDialog->setLoadOrSave(false); mSaveGameDialog->setVisible(true); } } void MainMenu::showBackground(bool show) { if (mVideo && !show) { MyGUI::Gui::getInstance().destroyWidget(mVideoBackground); mVideoBackground = nullptr; mVideo = nullptr; } if (mBackground && !show) { MyGUI::Gui::getInstance().destroyWidget(mBackground); mBackground = nullptr; } if (!show) return; bool stretch = Settings::Manager::getBool("stretch menu background", "GUI"); if (mHasAnimatedMenu) { if (!mVideo) { // Use black background to correct aspect ratio mVideoBackground = MyGUI::Gui::getInstance().createWidgetReal("ImageBox", 0,0,1,1, MyGUI::Align::Default, "Menu"); mVideoBackground->setImageTexture("black"); mVideo = mVideoBackground->createWidget("ImageBox", 0,0,1,1, MyGUI::Align::Stretch, "Menu"); mVideo->setVFS(mVFS); mVideo->playVideo("video\\menu_background.bik"); } MyGUI::IntSize viewSize = MyGUI::RenderManager::getInstance().getViewSize(); int screenWidth = viewSize.width; int screenHeight = viewSize.height; mVideoBackground->setSize(screenWidth, screenHeight); mVideo->autoResize(stretch); mVideo->setVisible(true); } else { if (!mBackground) { mBackground = MyGUI::Gui::getInstance().createWidgetReal("ImageBox", 0,0,1,1, MyGUI::Align::Stretch, "Menu"); mBackground->setBackgroundImage("textures\\menu_morrowind.dds", true, stretch); } mBackground->setVisible(true); } } void MainMenu::onFrame(float dt) { if (mVideo) { if (!mVideo->update()) { // If finished playing, start again mVideo->playVideo("video\\menu_background.bik"); } } } bool MainMenu::exit() { return MWBase::Environment::get().getStateManager()->getState() == MWBase::StateManager::State_Running; } void MainMenu::updateMenu() { setCoord(0,0, mWidth, mHeight); if (!mButtonBox) mButtonBox = mMainWidget->createWidget("", MyGUI::IntCoord(0, 0, 0, 0), MyGUI::Align::Default); int curH = 0; MWBase::StateManager::State state = MWBase::Environment::get().getStateManager()->getState(); mVersionText->setVisible(state == MWBase::StateManager::State_NoGame); std::vector buttons; if (state==MWBase::StateManager::State_Running) buttons.emplace_back("return"); /* Start of tes3mp change (major) In multiplayer, the main menu should not have options for starting or loading the game, so they have been removed Saving the game should still be possible, as long as it's clear that the resulting save is singleplayer-only; this will prevent players from completely losing their characters and houses on servers if those servers ever go down */ //buttons.emplace_back("newgame"); if (state==MWBase::StateManager::State_Running && MWBase::Environment::get().getWorld()->getGlobalInt ("chargenstate")==-1 && MWBase::Environment::get().getWindowManager()->isSavingAllowed()) buttons.emplace_back("savegame"); /* if (MWBase::Environment::get().getStateManager()->characterBegin()!= MWBase::Environment::get().getStateManager()->characterEnd()) buttons.emplace_back("loadgame"); */ /* End of tes3mp change (major) */ buttons.emplace_back("options"); if (state==MWBase::StateManager::State_NoGame) buttons.emplace_back("credits"); buttons.emplace_back("exitgame"); // Create new buttons if needed std::vector allButtons { "return", "newgame", "savegame", "loadgame", "options", "credits", "exitgame"}; for (std::string& buttonId : allButtons) { if (mButtons.find(buttonId) == mButtons.end()) { Gui::ImageButton* button = mButtonBox->createWidget ("ImageBox", MyGUI::IntCoord(0, curH, 0, 0), MyGUI::Align::Default); button->setProperty("ImageHighlighted", "textures\\menu_" + buttonId + "_over.dds"); button->setProperty("ImageNormal", "textures\\menu_" + buttonId + ".dds"); button->setProperty("ImagePushed", "textures\\menu_" + buttonId + "_pressed.dds"); button->eventMouseButtonClick += MyGUI::newDelegate(this, &MainMenu::onButtonClicked); button->setUserData(std::string(buttonId)); mButtons[buttonId] = button; } } // Start by hiding all buttons int maxwidth = 0; for (auto& buttonPair : mButtons) { buttonPair.second->setVisible(false); MyGUI::IntSize requested = buttonPair.second->getRequestedSize(); if (requested.width > maxwidth) maxwidth = requested.width; } // Now show and position the ones we want for (std::string& buttonId : buttons) { assert(mButtons.find(buttonId) != mButtons.end()); Gui::ImageButton* button = mButtons[buttonId]; button->setVisible(true); // By default, assume that all menu buttons textures should have 64 height. // If they have a different resolution, scale them. MyGUI::IntSize requested = button->getRequestedSize(); float scale = requested.height / 64.f; button->setImageCoord(MyGUI::IntCoord(0, 0, requested.width, requested.height)); // Trim off some of the excessive padding // TODO: perhaps do this within ImageButton? int height = requested.height; button->setImageTile(MyGUI::IntSize(requested.width, requested.height-16*scale)); button->setCoord((maxwidth-requested.width/scale) / 2, curH, requested.width/scale, height/scale-16); curH += height/scale-16; } if (state == MWBase::StateManager::State_NoGame) { // Align with the background image int bottomPadding=24; mButtonBox->setCoord (mWidth/2 - maxwidth/2, mHeight - curH - bottomPadding, maxwidth, curH); } else mButtonBox->setCoord (mWidth/2 - maxwidth/2, mHeight/2 - curH/2, maxwidth, curH); } } ================================================ FILE: apps/openmw/mwgui/mainmenu.hpp ================================================ #ifndef OPENMW_GAME_MWGUI_MAINMENU_H #define OPENMW_GAME_MWGUI_MAINMENU_H #include "windowbase.hpp" namespace Gui { class ImageButton; } namespace VFS { class Manager; } namespace MWGui { class BackgroundImage; class SaveGameDialog; class VideoWidget; class MainMenu : public WindowBase { int mWidth; int mHeight; bool mHasAnimatedMenu; public: MainMenu(int w, int h, const VFS::Manager* vfs, const std::string& versionDescription); ~MainMenu(); void onResChange(int w, int h) override; void setVisible (bool visible) override; void onFrame(float dt) override; bool exit() override; private: const VFS::Manager* mVFS; MyGUI::Widget* mButtonBox; MyGUI::TextBox* mVersionText; BackgroundImage* mBackground; MyGUI::ImageBox* mVideoBackground; VideoWidget* mVideo; // For animated main menus std::map mButtons; void onButtonClicked (MyGUI::Widget* sender); void onNewGameConfirmed(); void onExitConfirmed(); void showBackground(bool show); void updateMenu(); SaveGameDialog* mSaveGameDialog; }; } #endif ================================================ FILE: apps/openmw/mwgui/mapwindow.cpp ================================================ #include "mapwindow.hpp" #include #include #include #include #include #include #include #include #include /* Start of tes3mp addition Include additional headers for multiplayer purposes */ #include "../mwmp/Main.hpp" #include "../mwmp/GUIController.hpp" /* End of tes3mp addition */ #include #include #include #include #include "../mwbase/windowmanager.hpp" #include "../mwbase/world.hpp" #include "../mwbase/environment.hpp" #include "../mwworld/player.hpp" #include "../mwworld/cellstore.hpp" #include "../mwworld/esmstore.hpp" #include "../mwrender/globalmap.hpp" #include "../mwrender/localmap.hpp" #include "confirmationdialog.hpp" #include "tooltips.hpp" namespace { const int cellSize = Constants::CellSizeInUnits; enum LocalMapWidgetDepth { Local_MarkerAboveFogLayer = 0, Local_CompassLayer = 1, Local_FogLayer = 2, Local_MarkerLayer = 3, Local_MapLayer = 4 }; enum GlobalMapWidgetDepth { Global_CompassLayer = 0, Global_MarkerLayer = 1, Global_ExploreOverlayLayer = 2, Global_MapLayer = 3 }; /// @brief A widget that changes its color when hovered. class MarkerWidget final : public MyGUI::Widget { MYGUI_RTTI_DERIVED(MarkerWidget) public: void setNormalColour(const MyGUI::Colour& colour) { mNormalColour = colour; setColour(colour); } void setHoverColour(const MyGUI::Colour& colour) { mHoverColour = colour; } private: MyGUI::Colour mNormalColour; MyGUI::Colour mHoverColour; void onMouseLostFocus(MyGUI::Widget* _new) override { setColour(mNormalColour); } void onMouseSetFocus(MyGUI::Widget* _old) override { setColour(mHoverColour); } }; } namespace MWGui { void CustomMarkerCollection::addMarker(const ESM::CustomMarker &marker, bool triggerEvent) { mMarkers.insert(std::make_pair(marker.mCell, marker)); if (triggerEvent) eventMarkersChanged(); } void CustomMarkerCollection::deleteMarker(const ESM::CustomMarker &marker) { std::pair range = mMarkers.equal_range(marker.mCell); for (ContainerType::iterator it = range.first; it != range.second; ++it) { if (it->second == marker) { mMarkers.erase(it); eventMarkersChanged(); return; } } throw std::runtime_error("can't find marker to delete"); } void CustomMarkerCollection::updateMarker(const ESM::CustomMarker &marker, const std::string &newNote) { std::pair range = mMarkers.equal_range(marker.mCell); for (ContainerType::iterator it = range.first; it != range.second; ++it) { if (it->second == marker) { it->second.mNote = newNote; eventMarkersChanged(); return; } } throw std::runtime_error("can't find marker to update"); } void CustomMarkerCollection::clear() { mMarkers.clear(); eventMarkersChanged(); } CustomMarkerCollection::ContainerType::const_iterator CustomMarkerCollection::begin() const { return mMarkers.begin(); } CustomMarkerCollection::ContainerType::const_iterator CustomMarkerCollection::end() const { return mMarkers.end(); } CustomMarkerCollection::RangeType CustomMarkerCollection::getMarkers(const ESM::CellId &cellId) const { return mMarkers.equal_range(cellId); } size_t CustomMarkerCollection::size() const { return mMarkers.size(); } // ------------------------------------------------------ LocalMapBase::LocalMapBase(CustomMarkerCollection &markers, MWRender::LocalMap* localMapRender, bool fogOfWarEnabled) : mLocalMapRender(localMapRender) , mCurX(0) , mCurY(0) , mInterior(false) , mLocalMap(nullptr) , mCompass(nullptr) , mChanged(true) , mFogOfWarToggled(true) , mFogOfWarEnabled(fogOfWarEnabled) , mMapWidgetSize(0) , mNumCells(0) , mCellDistance(0) , mCustomMarkers(markers) , mMarkerUpdateTimer(0.0f) , mLastDirectionX(0.0f) , mLastDirectionY(0.0f) , mNeedDoorMarkersUpdate(false) { mCustomMarkers.eventMarkersChanged += MyGUI::newDelegate(this, &LocalMapBase::updateCustomMarkers); /* Start of tes3mp addition Add a MyGUI delegate for updating player markers */ mwmp::Main::get().getGUIController()->mPlayerMarkers.eventMarkersChanged += MyGUI::newDelegate(this, &LocalMapBase::updatePlayerMarkers); /* End of tes3mp addition */ } LocalMapBase::~LocalMapBase() { mCustomMarkers.eventMarkersChanged -= MyGUI::newDelegate(this, &LocalMapBase::updateCustomMarkers); /* Start of tes3mp addition Remove a MyGUI delegate for updating player markers */ mwmp::Main::get().getGUIController()->mPlayerMarkers.eventMarkersChanged -= MyGUI::newDelegate(this, &LocalMapBase::updatePlayerMarkers); /* End of tes3mp addition */ } void LocalMapBase::init(MyGUI::ScrollView* widget, MyGUI::ImageBox* compass) { mLocalMap = widget; mCompass = compass; mMapWidgetSize = std::max(1, Settings::Manager::getInt("local map widget size", "Map")); mCellDistance = Constants::CellGridRadius; mNumCells = mCellDistance * 2 + 1; mLocalMap->setCanvasSize(mMapWidgetSize*mNumCells, mMapWidgetSize*mNumCells); mCompass->setDepth(Local_CompassLayer); mCompass->setNeedMouseFocus(false); for (int mx=0; mxcreateWidget("ImageBox", MyGUI::IntCoord(mx*mMapWidgetSize, my*mMapWidgetSize, mMapWidgetSize, mMapWidgetSize), MyGUI::Align::Top | MyGUI::Align::Left); map->setDepth(Local_MapLayer); MyGUI::ImageBox* fog = mLocalMap->createWidget("ImageBox", MyGUI::IntCoord(mx*mMapWidgetSize, my*mMapWidgetSize, mMapWidgetSize, mMapWidgetSize), MyGUI::Align::Top | MyGUI::Align::Left); fog->setDepth(Local_FogLayer); fog->setColour(MyGUI::Colour(0, 0, 0)); map->setNeedMouseFocus(false); fog->setNeedMouseFocus(false); mMaps.emplace_back(map, fog); } } } void LocalMapBase::setCellPrefix(const std::string& prefix) { mPrefix = prefix; mChanged = true; } bool LocalMapBase::toggleFogOfWar() { mFogOfWarToggled = !mFogOfWarToggled; applyFogOfWar(); return mFogOfWarToggled; } void LocalMapBase::applyFogOfWar() { for (int mx=0; mxsetImageTexture(""); entry.mFogTexture.reset(); continue; } } } redraw(); } MyGUI::IntPoint LocalMapBase::getMarkerPosition(float worldX, float worldY, MarkerUserData& markerPos) { MyGUI::IntPoint widgetPos; // normalized cell coordinates float nX,nY; if (!mInterior) { int cellX, cellY; MWBase::Environment::get().getWorld()->positionToIndex(worldX, worldY, cellX, cellY); nX = (worldX - cellSize * cellX) / cellSize; // Image space is -Y up, cells are Y up nY = 1 - (worldY - cellSize * cellY) / cellSize; float cellDx = static_cast(cellX - mCurX); float cellDy = static_cast(cellY - mCurY); markerPos.cellX = cellX; markerPos.cellY = cellY; widgetPos = MyGUI::IntPoint(static_cast(nX * mMapWidgetSize + (mCellDistance + cellDx) * mMapWidgetSize), static_cast(nY * mMapWidgetSize + (mCellDistance - cellDy) * mMapWidgetSize)); } else { int cellX, cellY; osg::Vec2f worldPos (worldX, worldY); mLocalMapRender->worldToInteriorMapPosition(worldPos, nX, nY, cellX, cellY); markerPos.cellX = cellX; markerPos.cellY = cellY; // Image space is -Y up, cells are Y up widgetPos = MyGUI::IntPoint(static_cast(nX * mMapWidgetSize + (mCellDistance + (cellX - mCurX)) * mMapWidgetSize), static_cast(nY * mMapWidgetSize + (mCellDistance - (cellY - mCurY)) * mMapWidgetSize)); } markerPos.nX = nX; markerPos.nY = nY; return widgetPos; } void LocalMapBase::updateCustomMarkers() { for (MyGUI::Widget* widget : mCustomMarkerWidgets) MyGUI::Gui::getInstance().destroyWidget(widget); mCustomMarkerWidgets.clear(); for (int dX = -mCellDistance; dX <= mCellDistance; ++dX) { for (int dY =-mCellDistance; dY <= mCellDistance; ++dY) { ESM::CellId cellId; cellId.mPaged = !mInterior; cellId.mWorldspace = (mInterior ? mPrefix : ESM::CellId::sDefaultWorldspace); cellId.mIndex.mX = mCurX+dX; cellId.mIndex.mY = mCurY+dY; CustomMarkerCollection::RangeType markers = mCustomMarkers.getMarkers(cellId); for (CustomMarkerCollection::ContainerType::const_iterator it = markers.first; it != markers.second; ++it) { const ESM::CustomMarker& marker = it->second; MarkerUserData markerPos (mLocalMapRender); MyGUI::IntPoint widgetPos = getMarkerPosition(marker.mWorldX, marker.mWorldY, markerPos); MyGUI::IntCoord widgetCoord(widgetPos.left - 8, widgetPos.top - 8, 16, 16); MarkerWidget* markerWidget = mLocalMap->createWidget("CustomMarkerButton", widgetCoord, MyGUI::Align::Default); markerWidget->setDepth(Local_MarkerAboveFogLayer); markerWidget->setUserString("ToolTipType", "Layout"); markerWidget->setUserString("ToolTipLayout", "TextToolTipOneLine"); markerWidget->setUserString("Caption_TextOneLine", MyGUI::TextIterator::toTagsString(marker.mNote)); markerWidget->setNormalColour(MyGUI::Colour(0.6f, 0.6f, 0.6f)); markerWidget->setHoverColour(MyGUI::Colour(1.0f, 1.0f, 1.0f)); markerWidget->setUserData(marker); markerWidget->setNeedMouseFocus(true); customMarkerCreated(markerWidget); mCustomMarkerWidgets.push_back(markerWidget); } } } redraw(); } /* Start of tes3mp addition Send the LocalMapBase to our GUIController when updating player markers */ void LocalMapBase::updatePlayerMarkers() { mwmp::Main::get().getGUIController()->updatePlayersMarkers(this); } /* End of tes3mp addition */ /* Start of tes3mp addition Send the MapWindow to our GUIController when updating player markers */ void MapWindow::updatePlayerMarkers() { LocalMapBase::updatePlayerMarkers(); mwmp::Main::get().getGUIController()->updateGlobalMapMarkerTooltips(this); } /* End of tes3mp addition */ void LocalMapBase::setActiveCell(const int x, const int y, bool interior) { if (x==mCurX && y==mCurY && mInterior==interior && !mChanged) return; // don't do anything if we're still in the same cell mCurX = x; mCurY = y; mInterior = interior; mChanged = false; for (int mx=0; mxsetRenderItemTexture(nullptr); entry.mFogWidget->setRenderItemTexture(nullptr); entry.mMapTexture.reset(); entry.mFogTexture.reset(); entry.mCellX = x + (mx - mCellDistance); entry.mCellY = y - (my - mCellDistance); } } // Delay the door markers update until scripts have been given a chance to run. // If we don't do this, door markers that should be disabled will still appear on the map. mNeedDoorMarkersUpdate = true; updateMagicMarkers(); updateCustomMarkers(); } void LocalMapBase::requestMapRender(const MWWorld::CellStore *cell) { mLocalMapRender->requestMap(cell); } void LocalMapBase::redraw() { // Redraw children in proper order mLocalMap->getParent()->_updateChilds(); } void LocalMapBase::setPlayerPos(int cellX, int cellY, const float nx, const float ny) { MyGUI::IntPoint pos(static_cast(mMapWidgetSize * mCellDistance + nx*mMapWidgetSize - 16), static_cast(mMapWidgetSize * mCellDistance + ny*mMapWidgetSize - 16)); pos.left += (cellX - mCurX) * mMapWidgetSize; pos.top -= (cellY - mCurY) * mMapWidgetSize; if (pos != mCompass->getPosition()) { notifyPlayerUpdate (); mCompass->setPosition(pos); MyGUI::IntPoint middle (pos.left+16, pos.top+16); MyGUI::IntCoord viewsize = mLocalMap->getCoord(); MyGUI::IntPoint viewOffset((viewsize.width / 2) - middle.left, (viewsize.height / 2) - middle.top); mLocalMap->setViewOffset(viewOffset); } } void LocalMapBase::setPlayerDir(const float x, const float y) { if (x == mLastDirectionX && y == mLastDirectionY) return; notifyPlayerUpdate (); MyGUI::ISubWidget* main = mCompass->getSubWidgetMain(); MyGUI::RotatingSkin* rotatingSubskin = main->castType(); rotatingSubskin->setCenter(MyGUI::IntPoint(16,16)); float angle = std::atan2(x,y); rotatingSubskin->setAngle(angle); mLastDirectionX = x; mLastDirectionY = y; } void LocalMapBase::addDetectionMarkers(int type) { std::vector markers; MWBase::World* world = MWBase::Environment::get().getWorld(); world->listDetectedReferences( world->getPlayerPtr(), markers, MWBase::World::DetectionType(type)); if (markers.empty()) return; std::string markerTexture; if (type == MWBase::World::Detect_Creature) { markerTexture = "textures\\detect_animal_icon.dds"; } if (type == MWBase::World::Detect_Key) { markerTexture = "textures\\detect_key_icon.dds"; } if (type == MWBase::World::Detect_Enchantment) { markerTexture = "textures\\detect_enchantment_icon.dds"; } int counter = 0; for (const MWWorld::Ptr& ptr : markers) { const ESM::Position& worldPos = ptr.getRefData().getPosition(); MarkerUserData markerPos (mLocalMapRender); MyGUI::IntPoint widgetPos = getMarkerPosition(worldPos.pos[0], worldPos.pos[1], markerPos); MyGUI::IntCoord widgetCoord(widgetPos.left - 4, widgetPos.top - 4, 8, 8); ++counter; MyGUI::ImageBox* markerWidget = mLocalMap->createWidget("ImageBox", widgetCoord, MyGUI::Align::Default); markerWidget->setDepth(Local_MarkerAboveFogLayer); markerWidget->setImageTexture(markerTexture); markerWidget->setImageCoord(MyGUI::IntCoord(0,0,8,8)); markerWidget->setNeedMouseFocus(false); mMagicMarkerWidgets.push_back(markerWidget); } } void LocalMapBase::onFrame(float dt) { if (mNeedDoorMarkersUpdate) { updateDoorMarkers(); mNeedDoorMarkersUpdate = false; } mMarkerUpdateTimer += dt; if (mMarkerUpdateTimer >= 0.25) { mMarkerUpdateTimer = 0; updateMagicMarkers(); } updateRequiredMaps(); } bool widgetCropped(MyGUI::Widget* widget, MyGUI::Widget* cropTo) { MyGUI::IntRect coord = widget->getAbsoluteRect(); MyGUI::IntRect croppedCoord = cropTo->getAbsoluteRect(); if (coord.left < croppedCoord.left && coord.right < croppedCoord.left) return true; if (coord.left > croppedCoord.right && coord.right > croppedCoord.right) return true; if (coord.top < croppedCoord.top && coord.bottom < croppedCoord.top) return true; if (coord.top > croppedCoord.bottom && coord.bottom > croppedCoord.bottom) return true; return false; } void LocalMapBase::updateRequiredMaps() { bool needRedraw = false; for (MapEntry& entry : mMaps) { if (widgetCropped(entry.mMapWidget, mLocalMap)) continue; if (!entry.mMapTexture) { if (!mInterior) requestMapRender(MWBase::Environment::get().getWorld()->getExterior (entry.mCellX, entry.mCellY)); osg::ref_ptr texture = mLocalMapRender->getMapTexture(entry.mCellX, entry.mCellY); if (texture) { entry.mMapTexture.reset(new osgMyGUI::OSGTexture(texture)); entry.mMapWidget->setRenderItemTexture(entry.mMapTexture.get()); entry.mMapWidget->getSubWidgetMain()->_setUVSet(MyGUI::FloatRect(0.f, 0.f, 1.f, 1.f)); needRedraw = true; } else entry.mMapTexture.reset(new osgMyGUI::OSGTexture("", nullptr)); } if (!entry.mFogTexture && mFogOfWarToggled && mFogOfWarEnabled) { osg::ref_ptr tex = mLocalMapRender->getFogOfWarTexture(entry.mCellX, entry.mCellY); if (tex) { entry.mFogTexture.reset(new osgMyGUI::OSGTexture(tex)); entry.mFogWidget->setRenderItemTexture(entry.mFogTexture.get()); entry.mFogWidget->getSubWidgetMain()->_setUVSet(MyGUI::FloatRect(0.f, 1.f, 1.f, 0.f)); } else { entry.mFogWidget->setImageTexture("black"); entry.mFogTexture.reset(new osgMyGUI::OSGTexture("", nullptr)); } needRedraw = true; } } if (needRedraw) redraw(); } void LocalMapBase::updateDoorMarkers() { // clear all previous door markers for (MyGUI::Widget* widget : mDoorMarkerWidgets) MyGUI::Gui::getInstance().destroyWidget(widget); mDoorMarkerWidgets.clear(); MWBase::World* world = MWBase::Environment::get().getWorld(); // Retrieve the door markers we want to show std::vector doors; if (mInterior) { MWWorld::CellStore* cell = world->getInterior (mPrefix); world->getDoorMarkers(cell, doors); } else { for (int dX=-mCellDistance; dX<=mCellDistance; ++dX) { for (int dY=-mCellDistance; dY<=mCellDistance; ++dY) { MWWorld::CellStore* cell = world->getExterior (mCurX+dX, mCurY+dY); world->getDoorMarkers(cell, doors); } } } // Create a widget for each marker int counter = 0; for (MWBase::World::DoorMarker& marker : doors) { std::vector destNotes; CustomMarkerCollection::RangeType markers = mCustomMarkers.getMarkers(marker.dest); for (CustomMarkerCollection::ContainerType::const_iterator iter = markers.first; iter != markers.second; ++iter) destNotes.push_back(iter->second.mNote); MarkerUserData data (mLocalMapRender); data.notes = destNotes; data.caption = marker.name; MyGUI::IntPoint widgetPos = getMarkerPosition(marker.x, marker.y, data); MyGUI::IntCoord widgetCoord(widgetPos.left - 4, widgetPos.top - 4, 8, 8); ++counter; MarkerWidget* markerWidget = mLocalMap->createWidget("MarkerButton", widgetCoord, MyGUI::Align::Default); markerWidget->setNormalColour(MyGUI::Colour::parse(MyGUI::LanguageManager::getInstance().replaceTags("#{fontcolour=normal}"))); markerWidget->setHoverColour(MyGUI::Colour::parse(MyGUI::LanguageManager::getInstance().replaceTags("#{fontcolour=normal_over}"))); markerWidget->setDepth(Local_MarkerLayer); markerWidget->setNeedMouseFocus(true); // Used by tooltips to not show the tooltip if marker is hidden by fog of war markerWidget->setUserString("ToolTipType", "MapMarker"); markerWidget->setUserData(data); doorMarkerCreated(markerWidget); mDoorMarkerWidgets.push_back(markerWidget); } } void LocalMapBase::updateMagicMarkers() { // clear all previous markers for (MyGUI::Widget* widget : mMagicMarkerWidgets) MyGUI::Gui::getInstance().destroyWidget(widget); mMagicMarkerWidgets.clear(); addDetectionMarkers(MWBase::World::Detect_Creature); addDetectionMarkers(MWBase::World::Detect_Key); addDetectionMarkers(MWBase::World::Detect_Enchantment); // Add marker for the spot marked with Mark magic effect MWWorld::CellStore* markedCell = nullptr; ESM::Position markedPosition; MWBase::Environment::get().getWorld()->getPlayer().getMarkedPosition(markedCell, markedPosition); if (markedCell && markedCell->isExterior() == !mInterior && (!mInterior || Misc::StringUtils::ciEqual(markedCell->getCell()->mName, mPrefix))) { MarkerUserData markerPos (mLocalMapRender); MyGUI::IntPoint widgetPos = getMarkerPosition(markedPosition.pos[0], markedPosition.pos[1], markerPos); MyGUI::IntCoord widgetCoord(widgetPos.left - 4, widgetPos.top - 4, 8, 8); MyGUI::ImageBox* markerWidget = mLocalMap->createWidget("ImageBox", widgetCoord, MyGUI::Align::Default); markerWidget->setDepth(Local_MarkerAboveFogLayer); markerWidget->setImageTexture("textures\\menu_map_smark.dds"); markerWidget->setNeedMouseFocus(false); mMagicMarkerWidgets.push_back(markerWidget); } redraw(); } // ------------------------------------------------------------------------------------------ MapWindow::MapWindow(CustomMarkerCollection &customMarkers, DragAndDrop* drag, MWRender::LocalMap* localMapRender, SceneUtil::WorkQueue* workQueue) : WindowPinnableBase("openmw_map_window.layout") , LocalMapBase(customMarkers, localMapRender) , NoDrop(drag, mMainWidget) , mGlobalMap(nullptr) , mGlobalMapImage(nullptr) , mGlobalMapOverlay(nullptr) , mGlobal(Settings::Manager::getBool("global", "Map")) , mEventBoxGlobal(nullptr) , mEventBoxLocal(nullptr) , mGlobalMapRender(new MWRender::GlobalMap(localMapRender->getRoot(), workQueue)) , mEditNoteDialog() { static bool registered = false; if (!registered) { MyGUI::FactoryManager::getInstance().registerFactory("Widget"); registered = true; } mEditNoteDialog.setVisible(false); mEditNoteDialog.eventOkClicked += MyGUI::newDelegate(this, &MapWindow::onNoteEditOk); mEditNoteDialog.eventDeleteClicked += MyGUI::newDelegate(this, &MapWindow::onNoteEditDelete); setCoord(500,0,320,300); getWidget(mLocalMap, "LocalMap"); getWidget(mGlobalMap, "GlobalMap"); getWidget(mGlobalMapImage, "GlobalMapImage"); getWidget(mGlobalMapOverlay, "GlobalMapOverlay"); getWidget(mPlayerArrowLocal, "CompassLocal"); getWidget(mPlayerArrowGlobal, "CompassGlobal"); mPlayerArrowGlobal->setDepth(Global_CompassLayer); mPlayerArrowGlobal->setNeedMouseFocus(false); mGlobalMapImage->setDepth(Global_MapLayer); mGlobalMapOverlay->setDepth(Global_ExploreOverlayLayer); mLastScrollWindowCoordinates = mLocalMap->getCoord(); mLocalMap->eventChangeCoord += MyGUI::newDelegate(this, &MapWindow::onChangeScrollWindowCoord); mGlobalMap->setVisible (false); getWidget(mButton, "WorldButton"); mButton->eventMouseButtonClick += MyGUI::newDelegate(this, &MapWindow::onWorldButtonClicked); mButton->setCaptionWithReplacing( mGlobal ? "#{sLocal}" : "#{sWorld}"); getWidget(mEventBoxGlobal, "EventBoxGlobal"); mEventBoxGlobal->eventMouseDrag += MyGUI::newDelegate(this, &MapWindow::onMouseDrag); mEventBoxGlobal->eventMouseButtonPressed += MyGUI::newDelegate(this, &MapWindow::onDragStart); mEventBoxGlobal->setDepth(Global_ExploreOverlayLayer); getWidget(mEventBoxLocal, "EventBoxLocal"); mEventBoxLocal->eventMouseDrag += MyGUI::newDelegate(this, &MapWindow::onMouseDrag); mEventBoxLocal->eventMouseButtonPressed += MyGUI::newDelegate(this, &MapWindow::onDragStart); mEventBoxLocal->eventMouseButtonDoubleClick += MyGUI::newDelegate(this, &MapWindow::onMapDoubleClicked); LocalMapBase::init(mLocalMap, mPlayerArrowLocal); mGlobalMap->setVisible(mGlobal); mLocalMap->setVisible(!mGlobal); } void MapWindow::onNoteEditOk() { if (mEditNoteDialog.getDeleteButtonShown()) mCustomMarkers.updateMarker(mEditingMarker, mEditNoteDialog.getText()); else { mEditingMarker.mNote = mEditNoteDialog.getText(); mCustomMarkers.addMarker(mEditingMarker); } mEditNoteDialog.setVisible(false); } void MapWindow::onNoteEditDelete() { ConfirmationDialog* confirmation = MWBase::Environment::get().getWindowManager()->getConfirmationDialog(); confirmation->askForConfirmation("#{sDeleteNote}"); confirmation->eventCancelClicked.clear(); confirmation->eventOkClicked.clear(); confirmation->eventOkClicked += MyGUI::newDelegate(this, &MapWindow::onNoteEditDeleteConfirm); } void MapWindow::onNoteEditDeleteConfirm() { mCustomMarkers.deleteMarker(mEditingMarker); mEditNoteDialog.setVisible(false); } void MapWindow::onCustomMarkerDoubleClicked(MyGUI::Widget *sender) { mEditingMarker = *sender->getUserData(); mEditNoteDialog.setText(mEditingMarker.mNote); mEditNoteDialog.showDeleteButton(true); mEditNoteDialog.setVisible(true); } void MapWindow::onMapDoubleClicked(MyGUI::Widget *sender) { MyGUI::IntPoint clickedPos = MyGUI::InputManager::getInstance().getMousePosition(); MyGUI::IntPoint widgetPos = clickedPos - mEventBoxLocal->getAbsolutePosition(); int x = int(widgetPos.left/float(mMapWidgetSize))-mCellDistance; int y = (int(widgetPos.top/float(mMapWidgetSize))-mCellDistance)*-1; float nX = widgetPos.left/float(mMapWidgetSize) - int(widgetPos.left/float(mMapWidgetSize)); float nY = widgetPos.top/float(mMapWidgetSize) - int(widgetPos.top/float(mMapWidgetSize)); x += mCurX; y += mCurY; osg::Vec2f worldPos; if (mInterior) { worldPos = mLocalMapRender->interiorMapToWorldPosition(nX, nY, x, y); } else { worldPos.x() = (x + nX) * cellSize; worldPos.y() = (y + (1.0f-nY)) * cellSize; } mEditingMarker.mWorldX = worldPos.x(); mEditingMarker.mWorldY = worldPos.y(); mEditingMarker.mCell.mPaged = !mInterior; if (mInterior) mEditingMarker.mCell.mWorldspace = LocalMapBase::mPrefix; else { mEditingMarker.mCell.mWorldspace = ESM::CellId::sDefaultWorldspace; mEditingMarker.mCell.mIndex.mX = x; mEditingMarker.mCell.mIndex.mY = y; } mEditNoteDialog.setVisible(true); mEditNoteDialog.showDeleteButton(false); mEditNoteDialog.setText(""); } void MapWindow::onChangeScrollWindowCoord(MyGUI::Widget* sender) { MyGUI::IntCoord currentCoordinates = sender->getCoord(); MyGUI::IntPoint currentViewPortCenter = MyGUI::IntPoint(currentCoordinates.width / 2, currentCoordinates.height / 2); MyGUI::IntPoint lastViewPortCenter = MyGUI::IntPoint(mLastScrollWindowCoordinates.width / 2, mLastScrollWindowCoordinates.height / 2); MyGUI::IntPoint viewPortCenterDiff = currentViewPortCenter - lastViewPortCenter; mLocalMap->setViewOffset(mLocalMap->getViewOffset() + viewPortCenterDiff); mGlobalMap->setViewOffset(mGlobalMap->getViewOffset() + viewPortCenterDiff); mLastScrollWindowCoordinates = currentCoordinates; } void MapWindow::setVisible(bool visible) { WindowBase::setVisible(visible); mButton->setVisible(visible && MWBase::Environment::get().getWindowManager()->getMode() != MWGui::GM_None); } void MapWindow::renderGlobalMap() { mGlobalMapRender->render(); mGlobalMap->setCanvasSize (mGlobalMapRender->getWidth(), mGlobalMapRender->getHeight()); mGlobalMapImage->setSize(mGlobalMapRender->getWidth(), mGlobalMapRender->getHeight()); } MapWindow::~MapWindow() { delete mGlobalMapRender; } void MapWindow::setCellName(const std::string& cellName) { setTitle("#{sCell=" + cellName + "}"); } void MapWindow::addVisitedLocation(const std::string& name, int x, int y) { CellId cell; cell.first = x; cell.second = y; if (mMarkers.insert(cell).second) { float worldX, worldY; mGlobalMapRender->cellTopLeftCornerToImageSpace (x, y, worldX, worldY); int markerSize = 12; int offset = mGlobalMapRender->getCellSize()/2 - markerSize/2; MyGUI::IntCoord widgetCoord( static_cast(worldX * mGlobalMapRender->getWidth()+offset), static_cast(worldY * mGlobalMapRender->getHeight() + offset), markerSize, markerSize); MyGUI::Widget* markerWidget = mGlobalMap->createWidget("MarkerButton", widgetCoord, MyGUI::Align::Default); markerWidget->setUserString("Caption_TextOneLine", "#{sCell=" + name + "}"); setGlobalMapMarkerTooltip(markerWidget, x, y); markerWidget->setUserString("ToolTipLayout", "TextToolTipOneLine"); markerWidget->setNeedMouseFocus(true); markerWidget->setColour(MyGUI::Colour::parse(MyGUI::LanguageManager::getInstance().replaceTags("#{fontcolour=normal}"))); markerWidget->setDepth(Global_MarkerLayer); markerWidget->eventMouseDrag += MyGUI::newDelegate(this, &MapWindow::onMouseDrag); markerWidget->eventMouseButtonPressed += MyGUI::newDelegate(this, &MapWindow::onDragStart); mGlobalMapMarkers[std::make_pair(x,y)] = markerWidget; } } void MapWindow::cellExplored(int x, int y) { mGlobalMapRender->cleanupCameras(); mGlobalMapRender->exploreCell(x, y, mLocalMapRender->getMapTexture(x, y)); } void MapWindow::onFrame(float dt) { LocalMapBase::onFrame(dt); NoDrop::onFrame(dt); } void MapWindow::setGlobalMapMarkerTooltip(MyGUI::Widget* markerWidget, int x, int y) { ESM::CellId cellId; cellId.mIndex.mX = x; cellId.mIndex.mY = y; cellId.mWorldspace = ESM::CellId::sDefaultWorldspace; cellId.mPaged = true; CustomMarkerCollection::RangeType markers = mCustomMarkers.getMarkers(cellId); std::vector destNotes; for (CustomMarkerCollection::ContainerType::const_iterator it = markers.first; it != markers.second; ++it) destNotes.push_back(it->second.mNote); if (!destNotes.empty()) { MarkerUserData data (nullptr); data.notes = destNotes; data.caption = markerWidget->getUserString("Caption_TextOneLine"); markerWidget->setUserData(data); markerWidget->setUserString("ToolTipType", "MapMarker"); } else { markerWidget->setUserString("ToolTipType", "Layout"); } } /* Start of tes3mp addition Allow the setting of the image data for a global map tile from elsewhere in the code */ void MapWindow::setGlobalMapImage(int cellX, int cellY, const std::vector& imageData) { mGlobalMapRender->setImage(cellX, cellY, imageData); } /* End of tes3mp addition */ void MapWindow::updateCustomMarkers() { LocalMapBase::updateCustomMarkers(); for (auto& widgetPair : mGlobalMapMarkers) { int x = widgetPair.first.first; int y = widgetPair.first.second; MyGUI::Widget* markerWidget = widgetPair.second; setGlobalMapMarkerTooltip(markerWidget, x, y); } } void MapWindow::onDragStart(MyGUI::Widget* _sender, int _left, int _top, MyGUI::MouseButton _id) { if (_id!=MyGUI::MouseButton::Left) return; mLastDragPos = MyGUI::IntPoint(_left, _top); } void MapWindow::onMouseDrag(MyGUI::Widget* _sender, int _left, int _top, MyGUI::MouseButton _id) { if (_id!=MyGUI::MouseButton::Left) return; MyGUI::IntPoint diff = MyGUI::IntPoint(_left, _top) - mLastDragPos; if (!mGlobal) mLocalMap->setViewOffset( mLocalMap->getViewOffset() + diff ); else mGlobalMap->setViewOffset( mGlobalMap->getViewOffset() + diff ); mLastDragPos = MyGUI::IntPoint(_left, _top); } void MapWindow::onWorldButtonClicked(MyGUI::Widget* _sender) { mGlobal = !mGlobal; mGlobalMap->setVisible(mGlobal); mLocalMap->setVisible(!mGlobal); Settings::Manager::setBool("global", "Map", mGlobal); mButton->setCaptionWithReplacing( mGlobal ? "#{sLocal}" : "#{sWorld}"); if (mGlobal) globalMapUpdatePlayer (); } void MapWindow::onPinToggled() { Settings::Manager::setBool("map pin", "Windows", mPinned); MWBase::Environment::get().getWindowManager()->setMinimapVisibility(!mPinned); } void MapWindow::onTitleDoubleClicked() { if (MyGUI::InputManager::getInstance().isShiftPressed()) MWBase::Environment::get().getWindowManager()->toggleMaximized(this); else if (!mPinned) MWBase::Environment::get().getWindowManager()->toggleVisible(GW_Map); } void MapWindow::onOpen() { ensureGlobalMapLoaded(); globalMapUpdatePlayer(); } void MapWindow::globalMapUpdatePlayer () { // For interiors, position is set by WindowManager via setGlobalMapPlayerPosition if (MWBase::Environment::get().getWorld ()->isCellExterior ()) { osg::Vec3f pos = MWBase::Environment::get().getWorld ()->getPlayerPtr().getRefData().getPosition().asVec3(); setGlobalMapPlayerPosition(pos.x(), pos.y()); } } void MapWindow::notifyPlayerUpdate () { globalMapUpdatePlayer (); setGlobalMapPlayerDir(mLastDirectionX, mLastDirectionY); } void MapWindow::setGlobalMapPlayerPosition(float worldX, float worldY) { float x, y; mGlobalMapRender->worldPosToImageSpace (worldX, worldY, x, y); x *= mGlobalMapRender->getWidth(); y *= mGlobalMapRender->getHeight(); mPlayerArrowGlobal->setPosition(MyGUI::IntPoint(static_cast(x - 16), static_cast(y - 16))); // set the view offset so that player is in the center MyGUI::IntSize viewsize = mGlobalMap->getSize(); MyGUI::IntPoint viewoffs(static_cast(viewsize.width * 0.5f - x), static_cast(viewsize.height *0.5 - y)); mGlobalMap->setViewOffset(viewoffs); } void MapWindow::setGlobalMapPlayerDir(const float x, const float y) { MyGUI::ISubWidget* main = mPlayerArrowGlobal->getSubWidgetMain(); MyGUI::RotatingSkin* rotatingSubskin = main->castType(); rotatingSubskin->setCenter(MyGUI::IntPoint(16,16)); float angle = std::atan2(x,y); rotatingSubskin->setAngle(angle); } void MapWindow::ensureGlobalMapLoaded() { if (!mGlobalMapTexture.get()) { mGlobalMapTexture.reset(new osgMyGUI::OSGTexture(mGlobalMapRender->getBaseTexture())); mGlobalMapImage->setRenderItemTexture(mGlobalMapTexture.get()); mGlobalMapImage->getSubWidgetMain()->_setUVSet(MyGUI::FloatRect(0.f, 0.f, 1.f, 1.f)); mGlobalMapOverlayTexture.reset(new osgMyGUI::OSGTexture(mGlobalMapRender->getOverlayTexture())); mGlobalMapOverlay->setRenderItemTexture(mGlobalMapOverlayTexture.get()); mGlobalMapOverlay->getSubWidgetMain()->_setUVSet(MyGUI::FloatRect(0.f, 0.f, 1.f, 1.f)); // Redraw children in proper order mGlobalMap->getParent()->_updateChilds(); } } void MapWindow::clear() { mMarkers.clear(); mGlobalMapRender->clear(); mChanged = true; for (auto& widgetPair : mGlobalMapMarkers) MyGUI::Gui::getInstance().destroyWidget(widgetPair.second); mGlobalMapMarkers.clear(); } void MapWindow::write(ESM::ESMWriter &writer, Loading::Listener& progress) { ESM::GlobalMap map; mGlobalMapRender->write(map); map.mMarkers = mMarkers; writer.startRecord(ESM::REC_GMAP); map.save(writer); writer.endRecord(ESM::REC_GMAP); } void MapWindow::readRecord(ESM::ESMReader &reader, uint32_t type) { if (type == ESM::REC_GMAP) { ESM::GlobalMap map; map.load(reader); mGlobalMapRender->read(map); for (const ESM::GlobalMap::CellId& cellId : map.mMarkers) { const ESM::Cell* cell = MWBase::Environment::get().getWorld()->getStore().get().search(cellId.first, cellId.second); if (cell && !cell->mName.empty()) addVisitedLocation(cell->mName, cellId.first, cellId.second); } } } void MapWindow::setAlpha(float alpha) { NoDrop::setAlpha(alpha); // can't allow showing map with partial transparency, as the fog of war will also go transparent // and reveal parts of the map you shouldn't be able to see for (MapEntry& entry : mMaps) entry.mMapWidget->setVisible(alpha == 1); } void MapWindow::customMarkerCreated(MyGUI::Widget *marker) { marker->eventMouseDrag += MyGUI::newDelegate(this, &MapWindow::onMouseDrag); marker->eventMouseButtonPressed += MyGUI::newDelegate(this, &MapWindow::onDragStart); marker->eventMouseButtonDoubleClick += MyGUI::newDelegate(this, &MapWindow::onCustomMarkerDoubleClicked); } void MapWindow::doorMarkerCreated(MyGUI::Widget *marker) { marker->eventMouseDrag += MyGUI::newDelegate(this, &MapWindow::onMouseDrag); marker->eventMouseButtonPressed += MyGUI::newDelegate(this, &MapWindow::onDragStart); } // ------------------------------------------------------------------- EditNoteDialog::EditNoteDialog() : WindowModal("openmw_edit_note.layout") { getWidget(mOkButton, "OkButton"); getWidget(mCancelButton, "CancelButton"); getWidget(mDeleteButton, "DeleteButton"); getWidget(mTextEdit, "TextEdit"); mCancelButton->eventMouseButtonClick += MyGUI::newDelegate(this, &EditNoteDialog::onCancelButtonClicked); mOkButton->eventMouseButtonClick += MyGUI::newDelegate(this, &EditNoteDialog::onOkButtonClicked); mDeleteButton->eventMouseButtonClick += MyGUI::newDelegate(this, &EditNoteDialog::onDeleteButtonClicked); } void EditNoteDialog::showDeleteButton(bool show) { mDeleteButton->setVisible(show); } bool EditNoteDialog::getDeleteButtonShown() { return mDeleteButton->getVisible(); } void EditNoteDialog::setText(const std::string &text) { mTextEdit->setCaption(MyGUI::TextIterator::toTagsString(text)); } std::string EditNoteDialog::getText() { return MyGUI::TextIterator::getOnlyText(mTextEdit->getCaption()); } void EditNoteDialog::onOpen() { WindowModal::onOpen(); center(); MWBase::Environment::get().getWindowManager()->setKeyFocusWidget(mTextEdit); } void EditNoteDialog::onCancelButtonClicked(MyGUI::Widget *sender) { setVisible(false); } void EditNoteDialog::onOkButtonClicked(MyGUI::Widget *sender) { eventOkClicked(); } void EditNoteDialog::onDeleteButtonClicked(MyGUI::Widget *sender) { eventDeleteClicked(); } bool LocalMapBase::MarkerUserData::isPositionExplored() const { if (!mLocalMapRender) return true; return mLocalMapRender->isPositionExplored(nX, nY, cellX, cellY); } } ================================================ FILE: apps/openmw/mwgui/mapwindow.hpp ================================================ #ifndef MWGUI_MAPWINDOW_H #define MWGUI_MAPWINDOW_H #include #include #include "windowpinnablebase.hpp" #include #include /* Start of tes3mp addition Declare GUIController here so we can use it for delegates */ namespace mwmp { class GUIController; } /* End of tes3mp addition */ namespace MWRender { class GlobalMap; class LocalMap; } namespace ESM { class ESMReader; class ESMWriter; } namespace MWWorld { class CellStore; } namespace Loading { class Listener; } namespace SceneUtil { class WorkQueue; } namespace MWGui { class CustomMarkerCollection { public: void addMarker(const ESM::CustomMarker& marker, bool triggerEvent=true); void deleteMarker (const ESM::CustomMarker& marker); void updateMarker(const ESM::CustomMarker& marker, const std::string& newNote); void clear(); size_t size() const; typedef std::multimap ContainerType; typedef std::pair RangeType; ContainerType::const_iterator begin() const; ContainerType::const_iterator end() const; RangeType getMarkers(const ESM::CellId& cellId) const; typedef MyGUI::delegates::CMultiDelegate0 EventHandle_Void; EventHandle_Void eventMarkersChanged; private: ContainerType mMarkers; }; class LocalMapBase { /* Start of tes3mp addition Allow the use of GUIController by declaring it as a friend class */ friend class mwmp::GUIController; /* End of tes3mp addition */ public: LocalMapBase(CustomMarkerCollection& markers, MWRender::LocalMap* localMapRender, bool fogOfWarEnabled = true); virtual ~LocalMapBase(); void init(MyGUI::ScrollView* widget, MyGUI::ImageBox* compass); void setCellPrefix(const std::string& prefix); void setActiveCell(const int x, const int y, bool interior=false); void requestMapRender(const MWWorld::CellStore* cell); void setPlayerDir(const float x, const float y); void setPlayerPos(int cellX, int cellY, const float nx, const float ny); void onFrame(float dt); bool toggleFogOfWar(); struct MarkerUserData { MarkerUserData(MWRender::LocalMap* map) : mLocalMapRender(map) , cellX(0) , cellY(0) , nX(0.f) , nY(0.f) { } bool isPositionExplored() const; MWRender::LocalMap* mLocalMapRender; int cellX; int cellY; float nX; float nY; std::vector notes; std::string caption; }; protected: MWRender::LocalMap* mLocalMapRender; int mCurX, mCurY; bool mInterior; MyGUI::ScrollView* mLocalMap; MyGUI::ImageBox* mCompass; std::string mPrefix; bool mChanged; bool mFogOfWarToggled; bool mFogOfWarEnabled; int mMapWidgetSize; int mNumCells; // for convenience, mCellDistance * 2 + 1 int mCellDistance; // Stores markers that were placed by a player. May be shared between multiple map views. CustomMarkerCollection& mCustomMarkers; struct MapEntry { MapEntry(MyGUI::ImageBox* mapWidget, MyGUI::ImageBox* fogWidget) : mMapWidget(mapWidget), mFogWidget(fogWidget), mCellX(0), mCellY(0) {} MyGUI::ImageBox* mMapWidget; MyGUI::ImageBox* mFogWidget; std::shared_ptr mMapTexture; std::shared_ptr mFogTexture; int mCellX; int mCellY; }; std::vector mMaps; // Keep track of created marker widgets, just to easily remove them later. std::vector mDoorMarkerWidgets; std::vector mMagicMarkerWidgets; std::vector mCustomMarkerWidgets; /* Start of tes3mp addition Add a new group of Widgets for player markers */ std::vector mPlayerMarkerWidgets; /* End of tes3mp addition */ virtual void updateCustomMarkers(); /* Start of tes3mp addition Send the LocalMapBase to our GUIController when updating player markers */ virtual void updatePlayerMarkers(); /* End of tes3mp addition */ void applyFogOfWar(); MyGUI::IntPoint getMarkerPosition (float worldX, float worldY, MarkerUserData& markerPos); virtual void notifyPlayerUpdate() {} virtual void notifyMapChanged() {} virtual void customMarkerCreated(MyGUI::Widget* marker) {} virtual void doorMarkerCreated(MyGUI::Widget* marker) {} void updateRequiredMaps(); void updateMagicMarkers(); void addDetectionMarkers(int type); void redraw(); float mMarkerUpdateTimer; float mLastDirectionX; float mLastDirectionY; private: void updateDoorMarkers(); bool mNeedDoorMarkersUpdate; }; class EditNoteDialog : public MWGui::WindowModal { public: EditNoteDialog(); void onOpen() override; void showDeleteButton(bool show); bool getDeleteButtonShown(); void setText(const std::string& text); std::string getText(); typedef MyGUI::delegates::CMultiDelegate0 EventHandle_Void; EventHandle_Void eventDeleteClicked; EventHandle_Void eventOkClicked; private: void onCancelButtonClicked(MyGUI::Widget* sender); void onOkButtonClicked(MyGUI::Widget* sender); void onDeleteButtonClicked(MyGUI::Widget* sender); MyGUI::TextBox* mTextEdit; MyGUI::Button* mOkButton; MyGUI::Button* mCancelButton; MyGUI::Button* mDeleteButton; }; class MapWindow : public MWGui::WindowPinnableBase, public LocalMapBase, public NoDrop { /* Start of tes3mp addition Allow the use of GUIController by declaring it as a friend class */ friend class mwmp::GUIController; /* End of tes3mp addition */ public: MapWindow(CustomMarkerCollection& customMarkers, DragAndDrop* drag, MWRender::LocalMap* localMapRender, SceneUtil::WorkQueue* workQueue); virtual ~MapWindow(); void setCellName(const std::string& cellName); void setAlpha(float alpha) override; void setVisible(bool visible) override; void renderGlobalMap(); /// adds the marker to the global map /// @param name The ESM::Cell::mName void addVisitedLocation(const std::string& name, int x, int y); // reveals this cell's map on the global map void cellExplored(int x, int y); void setGlobalMapPlayerPosition (float worldX, float worldY); void setGlobalMapPlayerDir(const float x, const float y); /* Start of tes3mp addition Allow the setting of the image data for a global map tile from elsewhere in the code */ void setGlobalMapImage(int cellX, int cellY, const std::vector& imageData); /* End of tes3mp addition */ void ensureGlobalMapLoaded(); void onOpen() override; void onFrame(float dt) override; void updateCustomMarkers() override; /* Start of tes3mp addition Send the MapWindow to our GUIController when updating player markers */ virtual void updatePlayerMarkers(); /* End of tes3mp addition */ /// Clear all savegame-specific data void clear() override; void write (ESM::ESMWriter& writer, Loading::Listener& progress); void readRecord (ESM::ESMReader& reader, uint32_t type); private: void onDragStart(MyGUI::Widget* _sender, int _left, int _top, MyGUI::MouseButton _id); void onMouseDrag(MyGUI::Widget* _sender, int _left, int _top, MyGUI::MouseButton _id); void onWorldButtonClicked(MyGUI::Widget* _sender); void onMapDoubleClicked(MyGUI::Widget* sender); void onCustomMarkerDoubleClicked(MyGUI::Widget* sender); void onNoteEditOk(); void onNoteEditDelete(); void onNoteEditDeleteConfirm(); void onNoteDoubleClicked(MyGUI::Widget* sender); void onChangeScrollWindowCoord(MyGUI::Widget* sender); void globalMapUpdatePlayer(); void setGlobalMapMarkerTooltip(MyGUI::Widget* widget, int x, int y); MyGUI::ScrollView* mGlobalMap; std::unique_ptr mGlobalMapTexture; std::unique_ptr mGlobalMapOverlayTexture; MyGUI::ImageBox* mGlobalMapImage; MyGUI::ImageBox* mGlobalMapOverlay; MyGUI::ImageBox* mPlayerArrowLocal; MyGUI::ImageBox* mPlayerArrowGlobal; MyGUI::Button* mButton; MyGUI::IntPoint mLastDragPos; bool mGlobal; MyGUI::IntCoord mLastScrollWindowCoordinates; // Markers on global map typedef std::pair CellId; std::set mMarkers; MyGUI::Button* mEventBoxGlobal; MyGUI::Button* mEventBoxLocal; MWRender::GlobalMap* mGlobalMapRender; std::map, MyGUI::Widget*> mGlobalMapMarkers; EditNoteDialog mEditNoteDialog; ESM::CustomMarker mEditingMarker; void onPinToggled() override; void onTitleDoubleClicked() override; void doorMarkerCreated(MyGUI::Widget* marker) override; void customMarkerCreated(MyGUI::Widget *marker) override; void notifyPlayerUpdate() override; }; } #endif ================================================ FILE: apps/openmw/mwgui/merchantrepair.cpp ================================================ #include "merchantrepair.hpp" #include #include #include #include /* Start of tes3mp addition Include additional headers for multiplayer purposes */ #include "../mwmp/Main.hpp" #include "../mwmp/Networking.hpp" #include "../mwmp/ObjectList.hpp" /* End of tes3mp addition */ #include "../mwbase/world.hpp" #include "../mwbase/environment.hpp" #include "../mwbase/mechanicsmanager.hpp" #include "../mwbase/windowmanager.hpp" #include "../mwmechanics/creaturestats.hpp" #include "../mwmechanics/actorutil.hpp" #include "../mwworld/class.hpp" #include "../mwworld/containerstore.hpp" #include "../mwworld/esmstore.hpp" namespace MWGui { MerchantRepair::MerchantRepair() : WindowBase("openmw_merchantrepair.layout") { getWidget(mList, "RepairView"); getWidget(mOkButton, "OkButton"); getWidget(mGoldLabel, "PlayerGold"); mOkButton->eventMouseButtonClick += MyGUI::newDelegate(this, &MerchantRepair::onOkButtonClick); } void MerchantRepair::setPtr(const MWWorld::Ptr &actor) { mActor = actor; while (mList->getChildCount()) MyGUI::Gui::getInstance().destroyWidget(mList->getChildAt(0)); int lineHeight = MWBase::Environment::get().getWindowManager()->getFontHeight() + 2; int currentY = 0; MWWorld::Ptr player = MWMechanics::getPlayer(); int playerGold = player.getClass().getContainerStore(player).count(MWWorld::ContainerStore::sGoldId); MWWorld::ContainerStore& store = player.getClass().getContainerStore(player); int categories = MWWorld::ContainerStore::Type_Weapon | MWWorld::ContainerStore::Type_Armor; for (MWWorld::ContainerStoreIterator iter (store.begin(categories)); iter!=store.end(); ++iter) { if (iter->getClass().hasItemHealth(*iter)) { int maxDurability = iter->getClass().getItemMaxHealth(*iter); int durability = iter->getClass().getItemHealth(*iter); if (maxDurability == durability || maxDurability == 0) continue; int basePrice = iter->getClass().getValue(*iter); float fRepairMult = MWBase::Environment::get().getWorld()->getStore().get() .find("fRepairMult")->mValue.getFloat(); float p = static_cast(std::max(1, basePrice)); float r = static_cast(std::max(1, static_cast(maxDurability / p))); int x = static_cast((maxDurability - durability) / r); x = static_cast(fRepairMult * x); x = std::max(1, x); int price = MWBase::Environment::get().getMechanicsManager()->getBarterOffer(mActor, x, true); std::string name = iter->getClass().getName(*iter) + " - " + MyGUI::utility::toString(price) + MWBase::Environment::get().getWorld()->getStore().get() .find("sgp")->mValue.getString(); MyGUI::Button* button = mList->createWidget(price <= playerGold ? "SandTextButton" : "SandTextButtonDisabled", // can't use setEnabled since that removes tooltip 0, currentY, 0, lineHeight, MyGUI::Align::Default ); currentY += lineHeight; button->setUserString("Price", MyGUI::utility::toString(price)); button->setUserData(MWWorld::Ptr(*iter)); button->setCaptionWithReplacing(name); button->setSize(mList->getWidth(), lineHeight); button->eventMouseWheel += MyGUI::newDelegate(this, &MerchantRepair::onMouseWheel); button->setUserString("ToolTipType", "ItemPtr"); button->eventMouseButtonClick += MyGUI::newDelegate(this, &MerchantRepair::onRepairButtonClick); } } // Canvas size must be expressed with VScroll disabled, otherwise MyGUI would expand the scroll area when the scrollbar is hidden mList->setVisibleVScroll(false); mList->setCanvasSize (MyGUI::IntSize(mList->getWidth(), std::max(mList->getHeight(), currentY))); mList->setVisibleVScroll(true); mGoldLabel->setCaptionWithReplacing("#{sGold}: " + MyGUI::utility::toString(playerGold)); } void MerchantRepair::onMouseWheel(MyGUI::Widget* _sender, int _rel) { if (mList->getViewOffset().top + _rel*0.3f > 0) mList->setViewOffset(MyGUI::IntPoint(0, 0)); else mList->setViewOffset(MyGUI::IntPoint(0, static_cast(mList->getViewOffset().top + _rel*0.3f))); } void MerchantRepair::onOpen() { center(); // Reset scrollbars mList->setViewOffset(MyGUI::IntPoint(0, 0)); } void MerchantRepair::onRepairButtonClick(MyGUI::Widget *sender) { MWWorld::Ptr player = MWMechanics::getPlayer(); int price = MyGUI::utility::parseInt(sender->getUserString("Price")); if (price > player.getClass().getContainerStore(player).count(MWWorld::ContainerStore::sGoldId)) return; // repair MWWorld::Ptr item = *sender->getUserData(); item.getCellRef().setCharge(item.getClass().getItemMaxHealth(item)); player.getClass().getContainerStore(player).restack(item); MWBase::Environment::get().getWindowManager()->playSound("Repair"); player.getClass().getContainerStore(player).remove(MWWorld::ContainerStore::sGoldId, price, player); // add gold to NPC trading gold pool MWMechanics::CreatureStats& actorStats = mActor.getClass().getCreatureStats(mActor); /* Start of tes3mp change (major) Don't unilaterally change the merchant's gold pool on our client and instead let the server do it */ //actorStats.setGoldPool(actorStats.getGoldPool() + price); mwmp::ObjectList* objectList = mwmp::Main::get().getNetworking()->getObjectList(); objectList->reset(); objectList->packetOrigin = mwmp::CLIENT_GAMEPLAY; objectList->addObjectMiscellaneous(mActor, actorStats.getGoldPool() + price, actorStats.getLastRestockTime().getHour(), actorStats.getLastRestockTime().getDay()); objectList->sendObjectMiscellaneous(); /* End of tes3mp change (major) */ setPtr(mActor); } void MerchantRepair::onOkButtonClick(MyGUI::Widget *sender) { MWBase::Environment::get().getWindowManager()->removeGuiMode(GM_MerchantRepair); } } ================================================ FILE: apps/openmw/mwgui/merchantrepair.hpp ================================================ #ifndef OPENMW_MWGUI_MERCHANTREPAIR_H #define OPENMW_MWGUI_MERCHANTREPAIR_H #include "windowbase.hpp" #include "../mwworld/ptr.hpp" namespace MWGui { class MerchantRepair : public WindowBase { public: MerchantRepair(); void onOpen() override; void setPtr(const MWWorld::Ptr& actor) override; private: MyGUI::ScrollView* mList; MyGUI::Button* mOkButton; MyGUI::TextBox* mGoldLabel; MWWorld::Ptr mActor; protected: void onMouseWheel(MyGUI::Widget* _sender, int _rel); void onRepairButtonClick(MyGUI::Widget* sender); void onOkButtonClick(MyGUI::Widget* sender); }; } #endif ================================================ FILE: apps/openmw/mwgui/messagebox.cpp ================================================ #include "messagebox.hpp" #include #include #include #include #include #include /* Start of tes3mp addition Include additional headers for multiplayer purposes */ #include #include "../mwmp/Main.hpp" #include "../mwmp/GUIController.hpp" /* End of tes3mp addition */ #include "../mwbase/environment.hpp" #include "../mwbase/soundmanager.hpp" #include "../mwbase/inputmanager.hpp" #include "../mwbase/windowmanager.hpp" #undef MessageBox namespace MWGui { MessageBoxManager::MessageBoxManager (float timePerChar) { mInterMessageBoxe = nullptr; mStaticMessageBox = nullptr; mLastButtonPressed = -1; mMessageBoxSpeed = timePerChar; } MessageBoxManager::~MessageBoxManager () { MessageBoxManager::clear(); } int MessageBoxManager::getMessagesCount() { return mMessageBoxes.size(); } void MessageBoxManager::clear() { if (mInterMessageBoxe) { mInterMessageBoxe->setVisible(false); delete mInterMessageBoxe; mInterMessageBoxe = nullptr; } for (MessageBox* messageBox : mMessageBoxes) { if (messageBox == mStaticMessageBox) mStaticMessageBox = nullptr; delete messageBox; } mMessageBoxes.clear(); mLastButtonPressed = -1; } void MessageBoxManager::onFrame (float frameDuration) { std::vector::iterator it; for(it = mMessageBoxes.begin(); it != mMessageBoxes.end();) { (*it)->mCurrentTime += frameDuration; if((*it)->mCurrentTime >= (*it)->mMaxTime && *it != mStaticMessageBox) { delete *it; it = mMessageBoxes.erase(it); } else ++it; } float height = 0; it = mMessageBoxes.begin(); while(it != mMessageBoxes.end()) { (*it)->update(static_cast(height)); height += (*it)->getHeight(); ++it; } if(mInterMessageBoxe != nullptr && mInterMessageBoxe->mMarkedToDelete) { mLastButtonPressed = mInterMessageBoxe->readPressedButton(); /* Start of tes3mp addition If this message box was created by the server, send the input back to it */ if (mInterMessageBoxe->mHasServerOrigin) mwmp::Main::get().getGUIController()->processCustomMessageBoxInput(mLastButtonPressed); /* End of tes3mp addition */ mInterMessageBoxe->setVisible(false); delete mInterMessageBoxe; mInterMessageBoxe = nullptr; MWBase::Environment::get().getInputManager()->changeInputMode( MWBase::Environment::get().getWindowManager()->isGuiMode()); } } void MessageBoxManager::createMessageBox (const std::string& message, bool stat) { MessageBox *box = new MessageBox(*this, message); box->mCurrentTime = 0; std::string realMessage = MyGUI::LanguageManager::getInstance().replaceTags(message); box->mMaxTime = realMessage.length()*mMessageBoxSpeed; if(stat) mStaticMessageBox = box; mMessageBoxes.push_back(box); if(mMessageBoxes.size() > 3) { delete *mMessageBoxes.begin(); mMessageBoxes.erase(mMessageBoxes.begin()); } int height = 0; for (MessageBox* messageBox : mMessageBoxes) { messageBox->update(height); height += messageBox->getHeight(); } } void MessageBoxManager::removeStaticMessageBox () { removeMessageBox(mStaticMessageBox); mStaticMessageBox = nullptr; } /* Start of tes3mp change (major) Add a hasServerOrigin boolean to the list of arguments so those messageboxes can be differentiated from client-only ones */ bool MessageBoxManager::createInteractiveMessageBox (const std::string& message, const std::vector& buttons, bool hasServerOrigin) /* End of tes3mp change (major) */ { if (mInterMessageBoxe != nullptr) { Log(Debug::Warning) << "Warning: replacing an interactive message box that was not answered yet"; mInterMessageBoxe->setVisible(false); delete mInterMessageBoxe; mInterMessageBoxe = nullptr; } mInterMessageBoxe = new InteractiveMessageBox(*this, message, buttons); /* Start of tes3mp addition Track whether the message box has a server origin */ mInterMessageBoxe->mHasServerOrigin = hasServerOrigin; /* End of tes3mp addition */ mLastButtonPressed = -1; return true; } bool MessageBoxManager::isInteractiveMessageBox () { return mInterMessageBoxe != nullptr; } bool MessageBoxManager::removeMessageBox (MessageBox *msgbox) { std::vector::iterator it; for(it = mMessageBoxes.begin(); it != mMessageBoxes.end(); ++it) { if((*it) == msgbox) { delete (*it); mMessageBoxes.erase(it); return true; } } return false; } int MessageBoxManager::readPressedButton (bool reset) { int pressed = mLastButtonPressed; if (reset) mLastButtonPressed = -1; return pressed; } MessageBox::MessageBox(MessageBoxManager& parMessageBoxManager, const std::string& message) : Layout("openmw_messagebox.layout") , mCurrentTime(0) , mMaxTime(0) , mMessageBoxManager(parMessageBoxManager) , mMessage(message) { // defines mBottomPadding = 48; mNextBoxPadding = 4; getWidget(mMessageWidget, "message"); mMessageWidget->setCaptionWithReplacing(mMessage); } void MessageBox::update (int height) { MyGUI::IntSize gameWindowSize = MyGUI::RenderManager::getInstance().getViewSize(); MyGUI::IntPoint pos; pos.left = (gameWindowSize.width - mMainWidget->getWidth())/2; pos.top = (gameWindowSize.height - mMainWidget->getHeight() - height - mBottomPadding); mMainWidget->setPosition(pos); } int MessageBox::getHeight () { return mMainWidget->getHeight()+mNextBoxPadding; } InteractiveMessageBox::InteractiveMessageBox(MessageBoxManager& parMessageBoxManager, const std::string& message, const std::vector& buttons) : WindowModal(MWBase::Environment::get().getWindowManager()->isGuiMode() ? "openmw_interactive_messagebox_notransp.layout" : "openmw_interactive_messagebox.layout") , mMessageBoxManager(parMessageBoxManager) , mButtonPressed(-1) { int textPadding = 10; // padding between text-widget and main-widget int textButtonPadding = 10; // padding between the text-widget und the button-widget int buttonLeftPadding = 10; // padding between the buttons if horizontal int buttonTopPadding = 10; // ^-- if vertical int buttonLabelLeftPadding = 12; // padding between button label and button itself, from left int buttonLabelTopPadding = 4; // padding between button label and button itself, from top int buttonMainPadding = 10; // padding between buttons and bottom of the main widget mMarkedToDelete = false; getWidget(mMessageWidget, "message"); getWidget(mButtonsWidget, "buttons"); mMessageWidget->setSize(400, mMessageWidget->getHeight()); mMessageWidget->setCaptionWithReplacing(message); MyGUI::IntSize textSize = mMessageWidget->getTextSize(); MyGUI::IntSize gameWindowSize = MyGUI::RenderManager::getInstance().getViewSize(); int biggestButtonWidth = 0; int buttonsWidth = 0; int buttonsHeight = 0; int buttonHeight = 0; MyGUI::IntCoord dummyCoord(0, 0, 0, 0); for(const std::string& buttonId : buttons) { MyGUI::Button* button = mButtonsWidget->createWidget( MyGUI::WidgetStyle::Child, std::string("MW_Button"), dummyCoord, MyGUI::Align::Default); button->setCaptionWithReplacing(buttonId); button->eventMouseButtonClick += MyGUI::newDelegate(this, &InteractiveMessageBox::mousePressed); mButtons.push_back(button); if (buttonsWidth != 0) buttonsWidth += buttonLeftPadding; int buttonWidth = button->getTextSize().width + 2*buttonLabelLeftPadding; buttonsWidth += buttonWidth; buttonHeight = button->getTextSize().height + 2*buttonLabelTopPadding; if (buttonsHeight != 0) buttonsHeight += buttonTopPadding; buttonsHeight += buttonHeight; if(buttonWidth > biggestButtonWidth) { biggestButtonWidth = buttonWidth; } } MyGUI::IntSize mainWidgetSize; if(buttonsWidth < textSize.width) { // on one line mainWidgetSize.width = textSize.width + 3*textPadding; mainWidgetSize.height = textPadding + textSize.height + textButtonPadding + buttonHeight + buttonMainPadding; MyGUI::IntSize realSize = mainWidgetSize + // To account for borders (mMainWidget->getSize() - mMainWidget->getClientWidget()->getSize()); MyGUI::IntPoint absPos; absPos.left = (gameWindowSize.width - realSize.width)/2; absPos.top = (gameWindowSize.height - realSize.height)/2; mMainWidget->setPosition(absPos); mMainWidget->setSize(realSize); MyGUI::IntCoord messageWidgetCoord; messageWidgetCoord.left = (mainWidgetSize.width - textSize.width)/2; messageWidgetCoord.top = textPadding; mMessageWidget->setCoord(messageWidgetCoord); mMessageWidget->setSize(textSize); MyGUI::IntCoord buttonCord; MyGUI::IntSize buttonSize(0, buttonHeight); int left = (mainWidgetSize.width - buttonsWidth)/2; for(MyGUI::Button* button : mButtons) { buttonCord.left = left; buttonCord.top = messageWidgetCoord.top + textSize.height + textButtonPadding; buttonSize.width = button->getTextSize().width + 2*buttonLabelLeftPadding; buttonSize.height = button->getTextSize().height + 2*buttonLabelTopPadding; button->setCoord(buttonCord); button->setSize(buttonSize); left += buttonSize.width + buttonLeftPadding; } } else { // among each other if(biggestButtonWidth > textSize.width) { mainWidgetSize.width = biggestButtonWidth + buttonTopPadding*2; } else { mainWidgetSize.width = textSize.width + 3*textPadding; } MyGUI::IntCoord buttonCord; MyGUI::IntSize buttonSize(0, buttonHeight); int top = textPadding + textSize.height + textButtonPadding; for(MyGUI::Button* button : mButtons) { buttonSize.width = button->getTextSize().width + buttonLabelLeftPadding*2; buttonSize.height = button->getTextSize().height + buttonLabelTopPadding*2; buttonCord.top = top; buttonCord.left = (mainWidgetSize.width - buttonSize.width)/2; button->setCoord(buttonCord); button->setSize(buttonSize); top += buttonSize.height + buttonTopPadding; } mainWidgetSize.height = textPadding + textSize.height + textButtonPadding + buttonsHeight + buttonMainPadding; mMainWidget->setSize(mainWidgetSize + // To account for borders (mMainWidget->getSize() - mMainWidget->getClientWidget()->getSize())); MyGUI::IntPoint absPos; absPos.left = (gameWindowSize.width - mainWidgetSize.width)/2; absPos.top = (gameWindowSize.height - mainWidgetSize.height)/2; mMainWidget->setPosition(absPos); MyGUI::IntCoord messageWidgetCoord; messageWidgetCoord.left = (mainWidgetSize.width - textSize.width)/2; messageWidgetCoord.top = textPadding; messageWidgetCoord.width = textSize.width; messageWidgetCoord.height = textSize.height; mMessageWidget->setCoord(messageWidgetCoord); } setVisible(true); } MyGUI::Widget* InteractiveMessageBox::getDefaultKeyFocus() { std::vector keywords { "sOk", "sYes" }; for(MyGUI::Button* button : mButtons) { for (const std::string& keyword : keywords) { if(Misc::StringUtils::ciEqual(MyGUI::LanguageManager::getInstance().replaceTags("#{" + keyword + "}"), button->getCaption())) { return button; } } } return nullptr; } void InteractiveMessageBox::mousePressed (MyGUI::Widget* pressed) { buttonActivated (pressed); } void InteractiveMessageBox::buttonActivated (MyGUI::Widget* pressed) { mMarkedToDelete = true; int index = 0; for(const MyGUI::Button* button : mButtons) { if(button == pressed) { mButtonPressed = index; mMessageBoxManager.onButtonPressed(mButtonPressed); return; } index++; } } int InteractiveMessageBox::readPressedButton () { return mButtonPressed; } } ================================================ FILE: apps/openmw/mwgui/messagebox.hpp ================================================ #ifndef MWGUI_MESSAGE_BOX_H #define MWGUI_MESSAGE_BOX_H #include "windowbase.hpp" #undef MessageBox namespace MyGUI { class Widget; class Button; class EditBox; } namespace MWGui { class InteractiveMessageBox; class MessageBoxManager; class MessageBox; class MessageBoxManager { public: MessageBoxManager (float timePerChar); ~MessageBoxManager (); void onFrame (float frameDuration); void createMessageBox (const std::string& message, bool stat = false); void removeStaticMessageBox (); /* Start of tes3mp change (major) Add a hasServerOrigin boolean to the list of arguments so those messageboxes can be differentiated from client-only ones */ bool createInteractiveMessageBox (const std::string& message, const std::vector& buttons, bool hasServerOrigin = false); /* End of tes3mp change (major) */ bool isInteractiveMessageBox (); int getMessagesCount(); const InteractiveMessageBox* getInteractiveMessageBox() const { return mInterMessageBoxe; } /// Remove all message boxes void clear(); bool removeMessageBox (MessageBox *msgbox); /// @param reset Reset the pressed button to -1 after reading it. int readPressedButton (bool reset=true); typedef MyGUI::delegates::CMultiDelegate1 EventHandle_Int; // Note: this delegate unassigns itself after it was fired, i.e. works once. EventHandle_Int eventButtonPressed; void onButtonPressed(int button) { eventButtonPressed(button); eventButtonPressed.clear(); } private: std::vector mMessageBoxes; InteractiveMessageBox* mInterMessageBoxe; MessageBox* mStaticMessageBox; float mMessageBoxSpeed; int mLastButtonPressed; }; class MessageBox : public Layout { public: MessageBox (MessageBoxManager& parMessageBoxManager, const std::string& message); void setMessage (const std::string& message); int getHeight (); void update (int height); float mCurrentTime; float mMaxTime; protected: MessageBoxManager& mMessageBoxManager; const std::string& mMessage; MyGUI::EditBox* mMessageWidget; int mBottomPadding; int mNextBoxPadding; }; class InteractiveMessageBox : public WindowModal { public: InteractiveMessageBox (MessageBoxManager& parMessageBoxManager, const std::string& message, const std::vector& buttons); void mousePressed (MyGUI::Widget* _widget); int readPressedButton (); MyGUI::Widget* getDefaultKeyFocus() override; bool exit() override { return false; } bool mMarkedToDelete; /* Start of tes3mp addition Track whether the message box has a server origin */ bool mHasServerOrigin = false; /* End of tes3mp addition */ private: void buttonActivated (MyGUI::Widget* _widget); MessageBoxManager& mMessageBoxManager; MyGUI::EditBox* mMessageWidget; MyGUI::Widget* mButtonsWidget; std::vector mButtons; int mButtonPressed; }; } #endif ================================================ FILE: apps/openmw/mwgui/mode.hpp ================================================ #ifndef MWGUI_MODE_H #define MWGUI_MODE_H namespace MWGui { enum GuiMode { GM_None, GM_Settings, // Settings window GM_Inventory, // Inventory mode GM_Container, GM_Companion, GM_MainMenu, // Main menu mode GM_Journal, // Journal mode GM_Scroll, // Read scroll GM_Book, // Read book GM_Alchemy, // Make potions GM_Repair, GM_Dialogue, // NPC interaction GM_Barter, GM_Rest, GM_SpellBuying, GM_Travel, GM_SpellCreation, GM_Enchanting, GM_Recharge, GM_Training, GM_MerchantRepair, GM_Levelup, // Startup character creation dialogs GM_Name, GM_Race, GM_Birth, GM_Class, GM_ClassGenerate, GM_ClassPick, GM_ClassCreate, GM_Review, GM_Loading, GM_LoadingWallpaper, GM_Jail, GM_QuickKeysMenu }; // Windows shown in inventory mode enum GuiWindow { GW_None = 0, GW_Map = 0x01, GW_Inventory = 0x02, GW_Magic = 0x04, GW_Stats = 0x08, GW_ALL = 0xFF }; } #endif ================================================ FILE: apps/openmw/mwgui/pickpocketitemmodel.cpp ================================================ #include "pickpocketitemmodel.hpp" #include #include #include "../mwmechanics/actorutil.hpp" #include "../mwmechanics/creaturestats.hpp" #include "../mwmechanics/pickpocket.hpp" #include "../mwworld/class.hpp" #include "../mwbase/environment.hpp" #include "../mwbase/mechanicsmanager.hpp" #include "../mwbase/windowmanager.hpp" namespace MWGui { PickpocketItemModel::PickpocketItemModel(const MWWorld::Ptr& actor, ItemModel *sourceModel, bool hideItems) : mActor(actor), mPickpocketDetected(false) { MWWorld::Ptr player = MWMechanics::getPlayer(); mSourceModel = sourceModel; float chance = player.getClass().getSkill(player, ESM::Skill::Sneak); mSourceModel->update(); // build list of items that player is unable to find when attempts to pickpocket. if (hideItems) { for (size_t i = 0; igetItemCount(); ++i) { if (Misc::Rng::roll0to99() > chance) mHiddenItems.push_back(mSourceModel->getItem(i)); } } } bool PickpocketItemModel::allowedToUseItems() const { return false; } ItemStack PickpocketItemModel::getItem (ModelIndex index) { if (index < 0) throw std::runtime_error("Invalid index supplied"); if (mItems.size() <= static_cast(index)) throw std::runtime_error("Item index out of range"); return mItems[index]; } size_t PickpocketItemModel::getItemCount() { return mItems.size(); } void PickpocketItemModel::update() { mSourceModel->update(); mItems.clear(); for (size_t i = 0; igetItemCount(); ++i) { const ItemStack& item = mSourceModel->getItem(i); // Bound items may not be stolen if (item.mFlags & ItemStack::Flag_Bound) continue; if (std::find(mHiddenItems.begin(), mHiddenItems.end(), item) == mHiddenItems.end() && item.mType != ItemStack::Type_Equipped) mItems.push_back(item); } } void PickpocketItemModel::removeItem (const ItemStack &item, size_t count) { ProxyItemModel::removeItem(item, count); } bool PickpocketItemModel::onDropItem(const MWWorld::Ptr &item, int count) { // don't allow "reverse pickpocket" (it will be handled by scripts after 1.0) return false; } void PickpocketItemModel::onClose() { // Make sure we were actually closed, rather than just temporarily hidden (e.g. console or main menu opened) if (MWBase::Environment::get().getWindowManager()->containsMode(GM_Container) // If it was already detected while taking an item, no need to check now || mPickpocketDetected) return; MWWorld::Ptr player = MWMechanics::getPlayer(); MWMechanics::Pickpocket pickpocket(player, mActor); if (pickpocket.finish()) { MWBase::Environment::get().getMechanicsManager()->commitCrime( player, mActor, MWBase::MechanicsManager::OT_Pickpocket, std::string(), 0, true); MWBase::Environment::get().getWindowManager()->removeGuiMode(MWGui::GM_Container); mPickpocketDetected = true; } } bool PickpocketItemModel::onTakeItem(const MWWorld::Ptr &item, int count) { if (mActor.getClass().getCreatureStats(mActor).getKnockedDown()) return mSourceModel->onTakeItem(item, count); bool success = stealItem(item, count); if (success) { MWWorld::Ptr player = MWMechanics::getPlayer(); MWBase::Environment::get().getMechanicsManager()->itemTaken(player, item, mActor, count, false); } return success; } bool PickpocketItemModel::stealItem(const MWWorld::Ptr &item, int count) { MWWorld::Ptr player = MWMechanics::getPlayer(); MWMechanics::Pickpocket pickpocket(player, mActor); if (pickpocket.pick(item, count)) { MWBase::Environment::get().getMechanicsManager()->commitCrime( player, mActor, MWBase::MechanicsManager::OT_Pickpocket, std::string(), 0, true); MWBase::Environment::get().getWindowManager()->removeGuiMode(MWGui::GM_Container); mPickpocketDetected = true; return false; } else player.getClass().skillUsageSucceeded(player, ESM::Skill::Sneak, 1); return true; } } ================================================ FILE: apps/openmw/mwgui/pickpocketitemmodel.hpp ================================================ #ifndef MWGUI_PICKPOCKET_ITEM_MODEL_H #define MWGUI_PICKPOCKET_ITEM_MODEL_H #include "itemmodel.hpp" namespace MWGui { /// @brief The pickpocket item model randomly hides item stacks based on a specified chance. Equipped items are always hidden. class PickpocketItemModel : public ProxyItemModel { public: PickpocketItemModel (const MWWorld::Ptr& thief, ItemModel* sourceModel, bool hideItems=true); bool allowedToUseItems() const override; ItemStack getItem (ModelIndex index) override; size_t getItemCount() override; void update() override; void removeItem (const ItemStack& item, size_t count) override; void onClose() override; bool onDropItem(const MWWorld::Ptr &item, int count) override; bool onTakeItem(const MWWorld::Ptr &item, int count) override; protected: MWWorld::Ptr mActor; bool mPickpocketDetected; bool stealItem(const MWWorld::Ptr &item, int count); private: std::vector mHiddenItems; std::vector mItems; }; } #endif ================================================ FILE: apps/openmw/mwgui/quickkeysmenu.cpp ================================================ #include "quickkeysmenu.hpp" #include #include #include #include #include #include #include /* Start of tes3mp addition Include additional headers for multiplayer purposes */ #include #include "../mwmp/Main.hpp" #include "../mwmp/LocalPlayer.hpp" /* End of tes3mp addition */ #include "../mwworld/inventorystore.hpp" #include "../mwworld/class.hpp" #include "../mwworld/player.hpp" #include "../mwworld/esmstore.hpp" #include "../mwbase/environment.hpp" #include "../mwbase/mechanicsmanager.hpp" #include "../mwbase/world.hpp" #include "../mwbase/windowmanager.hpp" #include "../mwmechanics/spellutil.hpp" #include "../mwmechanics/creaturestats.hpp" #include "../mwmechanics/actorutil.hpp" #include "itemselection.hpp" #include "spellview.hpp" #include "itemwidget.hpp" #include "sortfilteritemmodel.hpp" namespace MWGui { QuickKeysMenu::QuickKeysMenu() : WindowBase("openmw_quickkeys_menu.layout") , mKey(std::vector(10)) , mSelected(nullptr) , mActivated(nullptr) , mAssignDialog(nullptr) , mItemSelectionDialog(nullptr) , mMagicSelectionDialog(nullptr) { getWidget(mOkButton, "OKButton"); getWidget(mInstructionLabel, "InstructionLabel"); mMainWidget->setSize(mMainWidget->getWidth(), mMainWidget->getHeight() + (mInstructionLabel->getTextSize().height - mInstructionLabel->getHeight())); mOkButton->eventMouseButtonClick += MyGUI::newDelegate(this, &QuickKeysMenu::onOkButtonClicked); center(); for (int i = 0; i < 10; ++i) { mKey[i].index = i+1; getWidget(mKey[i].button, "QuickKey" + MyGUI::utility::toString(i+1)); mKey[i].button->eventMouseButtonClick += MyGUI::newDelegate(this, &QuickKeysMenu::onQuickKeyButtonClicked); unassign(&mKey[i]); } } void QuickKeysMenu::clear() { mActivated = nullptr; for (int i=0; i<10; ++i) { unassign(&mKey[i]); } } QuickKeysMenu::~QuickKeysMenu() { delete mAssignDialog; delete mItemSelectionDialog; delete mMagicSelectionDialog; } void QuickKeysMenu::onOpen() { 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[i].type) { case Type_Unassigned: case Type_HandToHand: case Type_Magic: break; case Type_Item: case Type_MagicItem: { MWWorld::Ptr item = *mKey[i].button->getUserData(); // 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); if (item) mKey[i].button->setUserData(MWWorld::Ptr(item)); break; } } } } } void QuickKeysMenu::unassign(keyData* key) { key->button->clearUserStrings(); key->button->setItem(MWWorld::Ptr()); while (key->button->getChildCount()) // Destroy number label MyGUI::Gui::getInstance().destroyWidget(key->button->getChildAt(0)); if (key->index == 10) { key->type = Type_HandToHand; MyGUI::ImageBox* image = key->button->createWidget("ImageBox", MyGUI::IntCoord(14, 13, 32, 32), MyGUI::Align::Default); image->setImageTexture("icons\\k\\stealth_handtohand.dds"); image->setNeedMouseFocus(false); } else { key->type = Type_Unassigned; key->id = ""; key->name = ""; MyGUI::TextBox* textBox = key->button->createWidgetReal("SandText", MyGUI::FloatCoord(0,0,1,1), MyGUI::Align::Default); textBox->setTextAlign(MyGUI::Align::Center); textBox->setCaption(MyGUI::utility::toString(key->index)); textBox->setNeedMouseFocus(false); } /* Start of tes3mp addition Send a PLAYER_QUICKKEYS packet whenever a key is unassigned, but only if the player is logged in on the server, so as to avoid doing anything doing at startup when all quick keys get unassigned by default */ if (mwmp::Main::get().getLocalPlayer()->isLoggedIn() && !mwmp::Main::get().getLocalPlayer()->isReceivingQuickKeys) { mwmp::Main::get().getLocalPlayer()->sendQuickKey(key->index, Type_Unassigned); } /* End of tes3mp addition */ } /* Start of tes3mp addition Allow unassigning an index directly from elsewhere in the code */ void QuickKeysMenu::unassignIndex(int index) { unassign(&mKey[index]); } /* End of tes3mp addition */ void QuickKeysMenu::onQuickKeyButtonClicked(MyGUI::Widget* sender) { int index = -1; for (int i = 0; i < 10; ++i) { if (sender == mKey[i].button || sender->getParent() == mKey[i].button) { index = i; break; } } assert(index != -1); if (index < 0) { mSelected = nullptr; return; } mSelected = &mKey[index]; // prevent reallocation of zero key from Type_HandToHand if(mSelected->index == 10) return; // open assign dialog if (!mAssignDialog) mAssignDialog = new QuickKeysMenuAssign(this); mAssignDialog->setVisible(true); } void QuickKeysMenu::onOkButtonClicked (MyGUI::Widget *sender) { MWBase::Environment::get().getWindowManager()->removeGuiMode(GM_QuickKeysMenu); } void QuickKeysMenu::onItemButtonClicked(MyGUI::Widget* sender) { if (!mItemSelectionDialog) { mItemSelectionDialog = new ItemSelectionDialog("#{sQuickMenu6}"); mItemSelectionDialog->eventItemSelected += MyGUI::newDelegate(this, &QuickKeysMenu::onAssignItem); mItemSelectionDialog->eventDialogCanceled += MyGUI::newDelegate(this, &QuickKeysMenu::onAssignItemCancel); } mItemSelectionDialog->setVisible(true); mItemSelectionDialog->openContainer(MWMechanics::getPlayer()); mItemSelectionDialog->setFilter(SortFilterItemModel::Filter_OnlyUsableItems); mAssignDialog->setVisible(false); } void QuickKeysMenu::onMagicButtonClicked(MyGUI::Widget* sender) { if (!mMagicSelectionDialog) { mMagicSelectionDialog = new MagicSelectionDialog(this); } mMagicSelectionDialog->setVisible(true); mAssignDialog->setVisible(false); } void QuickKeysMenu::onUnassignButtonClicked(MyGUI::Widget* sender) { unassign(mSelected); mAssignDialog->setVisible(false); } void QuickKeysMenu::onCancelButtonClicked(MyGUI::Widget* sender) { mAssignDialog->setVisible(false); } void QuickKeysMenu::onAssignItem(MWWorld::Ptr item) { assert(mSelected); while (mSelected->button->getChildCount()) // Destroy number label MyGUI::Gui::getInstance().destroyWidget(mSelected->button->getChildAt(0)); mSelected->type = Type_Item; mSelected->id = item.getCellRef().getRefId(); mSelected->name = item.getClass().getName(item); mSelected->button->setItem(item, ItemWidget::Barter); mSelected->button->setUserString("ToolTipType", "ItemPtr"); mSelected->button->setUserData(item); if (mItemSelectionDialog) mItemSelectionDialog->setVisible(false); /* Start of tes3mp addition Send a PlayerQuickKeys packet whenever a key is assigned to an item by a player, not by a packet received from the server */ if (!mwmp::Main::get().getLocalPlayer()->isReceivingQuickKeys) mwmp::Main::get().getLocalPlayer()->sendQuickKey(mSelected->index, Type_Item, item.getCellRef().getRefId()); /* End of tes3mp addition */ } void QuickKeysMenu::onAssignItemCancel() { mItemSelectionDialog->setVisible(false); } void QuickKeysMenu::onAssignMagicItem(MWWorld::Ptr item) { assert(mSelected); while (mSelected->button->getChildCount()) // Destroy number label MyGUI::Gui::getInstance().destroyWidget(mSelected->button->getChildAt(0)); mSelected->type = Type_MagicItem; mSelected->id = item.getCellRef().getRefId(); mSelected->name = item.getClass().getName(item); float scale = 1.f; MyGUI::ITexture* texture = MyGUI::RenderManager::getInstance().getTexture("textures\\menu_icon_select_magic_magic.dds"); if (texture) scale = texture->getHeight() / 64.f; mSelected->button->setFrame("textures\\menu_icon_select_magic_magic.dds", MyGUI::IntCoord(0, 0, 44*scale, 44*scale)); mSelected->button->setIcon(item); mSelected->button->setUserString("ToolTipType", "ItemPtr"); mSelected->button->setUserData(MWWorld::Ptr(item)); if (mMagicSelectionDialog) mMagicSelectionDialog->setVisible(false); /* Start of tes3mp addition Send a PLAYER_QUICKKEYS packet whenever a key is assigned to an item's magic */ if (!mwmp::Main::get().getLocalPlayer()->isReceivingQuickKeys) mwmp::Main::get().getLocalPlayer()->sendQuickKey(mSelected->index, Type_MagicItem, item.getCellRef().getRefId()); /* End of tes3mp addition */ } void QuickKeysMenu::onAssignMagic(const std::string& spellId) { assert(mSelected); while (mSelected->button->getChildCount()) // Destroy number label MyGUI::Gui::getInstance().destroyWidget(mSelected->button->getChildAt(0)); const MWWorld::ESMStore &esmStore = MWBase::Environment::get().getWorld()->getStore(); const ESM::Spell* spell = esmStore.get().find(spellId); mSelected->type = Type_Magic; mSelected->id = spellId; mSelected->name = spell->mName; mSelected->button->setItem(MWWorld::Ptr()); mSelected->button->setUserString("ToolTipType", "Spell"); mSelected->button->setUserString("Spell", spellId); // use the icon of the first effect const ESM::MagicEffect* effect = esmStore.get().find(spell->mEffects.mList.front().mEffectID); std::string path = effect->mIcon; int slashPos = path.rfind('\\'); path.insert(slashPos+1, "b_"); path = MWBase::Environment::get().getWindowManager()->correctIconPath(path); float scale = 1.f; MyGUI::ITexture* texture = MyGUI::RenderManager::getInstance().getTexture("textures\\menu_icon_select_magic.dds"); if (texture) scale = texture->getHeight() / 64.f; mSelected->button->setFrame("textures\\menu_icon_select_magic.dds", MyGUI::IntCoord(0, 0, 44*scale, 44*scale)); mSelected->button->setIcon(path); if (mMagicSelectionDialog) mMagicSelectionDialog->setVisible(false); /* Start of tes3mp addition Send a PLAYER_QUICKKEYS packet whenever a key is assigned to a spell */ if (!mwmp::Main::get().getLocalPlayer()->isReceivingQuickKeys) mwmp::Main::get().getLocalPlayer()->sendQuickKey(mSelected->index, Type_Magic, spellId); /* End of tes3mp addition */ } void QuickKeysMenu::onAssignMagicCancel() { mMagicSelectionDialog->setVisible(false); } void QuickKeysMenu::updateActivatedQuickKey() { // there is no delayed action, nothing to do. if (!mActivated) return; activateQuickKey(mActivated->index); } void QuickKeysMenu::activateQuickKey(int index) { 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); // Delay action executing, // if player is busy for now (casting a spell, attacking someone, etc.) bool isDelayNeeded = MWBase::Environment::get().getMechanicsManager()->isAttackingOrSpell(player) || playerStats.getKnockedDown() || playerStats.getHitRecovery(); bool godmode = MWBase::Environment::get().getWorld()->getGodModeState(); bool isReturnNeeded = (!godmode && playerStats.isParalyzed()) || playerStats.isDead(); if (isReturnNeeded && key->type != Type_Item) { return; } else if (isDelayNeeded && key->type != Type_Item) { mActivated = key; return; } else { mActivated = nullptr; } if (key->type == Type_Item || key->type == Type_MagicItem) { MWWorld::Ptr item = *key->button->getUserData(); MWWorld::ContainerStoreIterator it = store.begin(); for (; it != store.end(); ++it) { if (*it == item) break; } if (it == store.end()) item = nullptr; // check the item is available and not broken if (!item || item.getRefData().getCount() < 1 || (item.getClass().hasItemHealth(item) && item.getClass().getItemHealth(item) <= 0)) { item = store.findReplacement(key->id); if (!item || item.getRefData().getCount() < 1) { MWBase::Environment::get().getWindowManager()->messageBox( "#{sQuickMenu5} " + key->name); return; } } if (key->type == Type_Item) { bool isWeapon = item.getTypeName() == typeid(ESM::Weapon).name(); bool isTool = item.getTypeName() == typeid(ESM::Probe).name() || item.getTypeName() == typeid(ESM::Lockpick).name(); // delay weapon switching if player is busy if (isDelayNeeded && (isWeapon || isTool)) { mActivated = key; return; } else if (isReturnNeeded && (isWeapon || isTool)) { return; } /* Start of tes3mp change (major) Instead of unilaterally using an item, send an ID_PLAYER_ITEM_USE packet and let the server decide if the item actually gets used */ /* if (!store.isEquipped(item)) MWBase::Environment::get().getWindowManager()->useItem(item); MWWorld::ConstContainerStoreIterator rightHand = store.getSlot(MWWorld::InventoryStore::Slot_CarriedRight); // change draw state only if the item is in player's right hand if (rightHand != store.end() && item == *rightHand) { MWBase::Environment::get().getWorld()->getPlayer().setDrawState(MWMechanics::DrawState_Weapon); } */ bool shouldDraw = isWeapon || isTool; if (!store.isEquipped(item)) { mwmp::Main::get().getLocalPlayer()->sendItemUse(item, false, shouldDraw ? MWMechanics::DrawState_Weapon : MWMechanics::DrawState_Nothing); } /* End of tes3mp change (major) */ } else if (key->type == Type_MagicItem) { // equip, if it can be equipped and isn't yet equipped /* Start of tes3mp change (major) Instead of unilaterally using an item, send an ID_PLAYER_ITEM_USE packet and let the server decide if the item actually gets used */ /* if (!item.getClass().getEquipmentSlots(item).first.empty() && !store.isEquipped(item)) { MWBase::Environment::get().getWindowManager()->useItem(item); // make sure that item was successfully equipped if (!store.isEquipped(item)) return; } store.setSelectedEnchantItem(it); MWBase::Environment::get().getWorld()->getPlayer().setDrawState(MWMechanics::DrawState_Spell); */ mwmp::Main::get().getLocalPlayer()->sendItemUse(item, true, MWMechanics::DrawState_Spell); /* End of tes3mp change (major) */ } } else if (key->type == Type_Magic) { std::string spellId = key->id; // Make sure the player still has this spell MWMechanics::CreatureStats& stats = player.getClass().getCreatureStats(player); MWMechanics::Spells& spells = stats.getSpells(); if (!spells.hasSpell(spellId)) { MWBase::Environment::get().getWindowManager()->messageBox("#{sQuickMenu5} " + key->name); return; } store.setSelectedEnchantItem(store.end()); MWBase::Environment::get().getWindowManager() ->setSelectedSpell(spellId, int(MWMechanics::getSpellSuccessChance(spellId, player))); MWBase::Environment::get().getWorld()->getPlayer().setDrawState(MWMechanics::DrawState_Spell); /* Start of tes3mp addition Send a PlayerMiscellaneous packet with the player's new selected spell */ mwmp::Main::get().getLocalPlayer()->sendSelectedSpell(spellId); /* End of tes3mp addition */ } else if (key->type == Type_HandToHand) { store.unequipSlot(MWWorld::InventoryStore::Slot_CarriedRight, player); MWBase::Environment::get().getWorld()->getPlayer().setDrawState(MWMechanics::DrawState_Weapon); } } /* Start of tes3mp addition Make it possible to add quickKeys from elsewhere in the code */ void QuickKeysMenu::setSelectedIndex(int index) { mSelected = &mKey[index]; } /* End of tes3mp addition */ // --------------------------------------------------------------------------------------------------------- QuickKeysMenuAssign::QuickKeysMenuAssign (QuickKeysMenu* parent) : WindowModal("openmw_quickkeys_menu_assign.layout") , mParent(parent) { getWidget(mLabel, "Label"); getWidget(mItemButton, "ItemButton"); getWidget(mMagicButton, "MagicButton"); getWidget(mUnassignButton, "UnassignButton"); getWidget(mCancelButton, "CancelButton"); mItemButton->eventMouseButtonClick += MyGUI::newDelegate(mParent, &QuickKeysMenu::onItemButtonClicked); mMagicButton->eventMouseButtonClick += MyGUI::newDelegate(mParent, &QuickKeysMenu::onMagicButtonClicked); mUnassignButton->eventMouseButtonClick += MyGUI::newDelegate(mParent, &QuickKeysMenu::onUnassignButtonClicked); mCancelButton->eventMouseButtonClick += MyGUI::newDelegate(mParent, &QuickKeysMenu::onCancelButtonClicked); int maxWidth = mLabel->getTextSize ().width + 24; maxWidth = std::max(maxWidth, mItemButton->getTextSize ().width + 24); maxWidth = std::max(maxWidth, mMagicButton->getTextSize ().width + 24); maxWidth = std::max(maxWidth, mUnassignButton->getTextSize ().width + 24); maxWidth = std::max(maxWidth, mCancelButton->getTextSize ().width + 24); mMainWidget->setSize(maxWidth + 24, mMainWidget->getHeight()); mLabel->setSize(maxWidth, mLabel->getHeight()); mItemButton->setCoord((maxWidth - mItemButton->getTextSize().width-24)/2 + 8, mItemButton->getTop(), mItemButton->getTextSize().width + 24, mItemButton->getHeight()); mMagicButton->setCoord((maxWidth - mMagicButton->getTextSize().width-24)/2 + 8, mMagicButton->getTop(), mMagicButton->getTextSize().width + 24, mMagicButton->getHeight()); mUnassignButton->setCoord((maxWidth - mUnassignButton->getTextSize().width-24)/2 + 8, mUnassignButton->getTop(), mUnassignButton->getTextSize().width + 24, mUnassignButton->getHeight()); mCancelButton->setCoord((maxWidth - mCancelButton->getTextSize().width-24)/2 + 8, mCancelButton->getTop(), mCancelButton->getTextSize().width + 24, mCancelButton->getHeight()); center(); } void QuickKeysMenu::write(ESM::ESMWriter &writer) { writer.startRecord(ESM::REC_KEYS); ESM::QuickKeys keys; // NB: The quick key with index 9 always has Hand-to-Hand type and must not be saved for (int i=0; i<9; ++i) { ItemWidget* button = mKey[i].button; int type = mKey[i].type; ESM::QuickKeys::QuickKey key; key.mType = type; switch (type) { case Type_Unassigned: case Type_HandToHand: break; case Type_Item: case Type_MagicItem: { MWWorld::Ptr item = *button->getUserData(); key.mId = item.getCellRef().getRefId(); break; } case Type_Magic: std::string spellId = button->getUserString("Spell"); key.mId = spellId; break; } keys.mKeys.push_back(key); } keys.save(writer); writer.endRecord(ESM::REC_KEYS); } void QuickKeysMenu::readRecord(ESM::ESMReader &reader, uint32_t type) { if (type != ESM::REC_KEYS) return; ESM::QuickKeys keys; keys.load(reader); MWWorld::Ptr player = MWMechanics::getPlayer(); MWWorld::InventoryStore& store = player.getClass().getInventoryStore(player); int i=0; for (ESM::QuickKeys::QuickKey& quickKey : keys.mKeys) { // NB: The quick key with index 9 always has Hand-to-Hand type and must not be loaded if (i >= 9) return; mSelected = &mKey[i]; switch (quickKey.mType) { case Type_Magic: if (MWBase::Environment::get().getWorld()->getStore().get().search(quickKey.mId)) onAssignMagic(quickKey.mId); break; case Type_Item: case Type_MagicItem: { // Find the item by id MWWorld::Ptr item = store.findReplacement(quickKey.mId); if (item.isEmpty()) unassign(mSelected); else { if (quickKey.mType == Type_Item) onAssignItem(item); else // if (quickKey.mType == Type_MagicItem) onAssignMagicItem(item); } break; } case Type_Unassigned: case Type_HandToHand: unassign(mSelected); break; } ++i; } } // --------------------------------------------------------------------------------------------------------- MagicSelectionDialog::MagicSelectionDialog(QuickKeysMenu* parent) : WindowModal("openmw_magicselection_dialog.layout") , mParent(parent) { getWidget(mCancelButton, "CancelButton"); getWidget(mMagicList, "MagicList"); mCancelButton->eventMouseButtonClick += MyGUI::newDelegate(this, &MagicSelectionDialog::onCancelButtonClicked); mMagicList->setShowCostColumn(false); mMagicList->setHighlightSelected(false); mMagicList->eventSpellClicked += MyGUI::newDelegate(this, &MagicSelectionDialog::onModelIndexSelected); center(); } void MagicSelectionDialog::onCancelButtonClicked (MyGUI::Widget *sender) { exit(); } bool MagicSelectionDialog::exit() { mParent->onAssignMagicCancel(); return true; } void MagicSelectionDialog::onOpen () { WindowModal::onOpen(); mMagicList->setModel(new SpellModel(MWMechanics::getPlayer())); mMagicList->resetScrollbars(); } void MagicSelectionDialog::onModelIndexSelected(SpellModel::ModelIndex index) { const Spell& spell = mMagicList->getModel()->getItem(index); if (spell.mType == Spell::Type_EnchantedItem) mParent->onAssignMagicItem(spell.mItem); else mParent->onAssignMagic(spell.mId); } } ================================================ FILE: apps/openmw/mwgui/quickkeysmenu.hpp ================================================ #ifndef MWGUI_QUICKKEYS_H #define MWGUI_QUICKKEYS_H #include "windowbase.hpp" #include "spellmodel.hpp" namespace MWGui { class QuickKeysMenuAssign; class ItemSelectionDialog; class MagicSelectionDialog; class ItemWidget; class SpellView; class QuickKeysMenu : public WindowBase { public: QuickKeysMenu(); ~QuickKeysMenu(); void onResChange(int, int) override { center(); } void onItemButtonClicked(MyGUI::Widget* sender); void onMagicButtonClicked(MyGUI::Widget* sender); void onUnassignButtonClicked(MyGUI::Widget* sender); void onCancelButtonClicked(MyGUI::Widget* sender); void onAssignItem (MWWorld::Ptr item); void onAssignItemCancel (); void onAssignMagicItem (MWWorld::Ptr item); void onAssignMagic (const std::string& spellId); void onAssignMagicCancel (); void onOpen() override; void activateQuickKey(int index); void updateActivatedQuickKey(); /* Start of tes3mp addition Allow the setting of the selected index from elsewhere in the code */ void setSelectedIndex(int index); /* End of tes3mp addition */ /// @note This enum is serialized, so don't move the items around! enum QuickKeyType { Type_Item, Type_Magic, Type_MagicItem, Type_Unassigned, Type_HandToHand }; void write (ESM::ESMWriter& writer); void readRecord (ESM::ESMReader& reader, uint32_t type); void clear() override; /* Start of tes3mp addition Allow unassigning an index directly from elsewhere in the code */ void unassignIndex(int index); /* End of tes3mp addition */ private: struct keyData { int index; ItemWidget* button; QuickKeysMenu::QuickKeyType type; std::string id; std::string name; keyData(): index(-1), button(nullptr), type(Type_Unassigned), id(""), name("") {} }; std::vector mKey; keyData* mSelected; keyData* mActivated; MyGUI::EditBox* mInstructionLabel; MyGUI::Button* mOkButton; QuickKeysMenuAssign* mAssignDialog; ItemSelectionDialog* mItemSelectionDialog; MagicSelectionDialog* mMagicSelectionDialog; void onQuickKeyButtonClicked(MyGUI::Widget* sender); void onOkButtonClicked(MyGUI::Widget* sender); void unassign(keyData* key); }; class QuickKeysMenuAssign : public WindowModal { public: QuickKeysMenuAssign(QuickKeysMenu* parent); private: MyGUI::TextBox* mLabel; MyGUI::Button* mItemButton; MyGUI::Button* mMagicButton; MyGUI::Button* mUnassignButton; MyGUI::Button* mCancelButton; QuickKeysMenu* mParent; }; class MagicSelectionDialog : public WindowModal { public: MagicSelectionDialog(QuickKeysMenu* parent); void onOpen() override; bool exit() override; private: MyGUI::Button* mCancelButton; SpellView* mMagicList; QuickKeysMenu* mParent; void onCancelButtonClicked (MyGUI::Widget* sender); void onModelIndexSelected(SpellModel::ModelIndex index); }; } #endif ================================================ FILE: apps/openmw/mwgui/race.cpp ================================================ #include "race.hpp" #include #include #include #include #include #include #include "../mwworld/esmstore.hpp" #include "../mwbase/environment.hpp" #include "../mwbase/world.hpp" #include "../mwbase/windowmanager.hpp" #include "../mwrender/characterpreview.hpp" #include "tooltips.hpp" namespace { int wrap(int index, int max) { if (index < 0) return max - 1; else if (index >= max) return 0; else return index; } bool sortRaces(const std::pair& left, const std::pair& right) { return left.second.compare(right.second) < 0; } } namespace MWGui { RaceDialog::RaceDialog(osg::Group* parent, Resource::ResourceSystem* resourceSystem) : WindowModal("openmw_chargen_race.layout") , mParent(parent) , mResourceSystem(resourceSystem) , mGenderIndex(0) , mFaceIndex(0) , mHairIndex(0) , mCurrentAngle(0) , mPreviewDirty(true) { // Centre dialog center(); setText("AppearanceT", MWBase::Environment::get().getWindowManager()->getGameSettingString("sRaceMenu1", "Appearance")); getWidget(mPreviewImage, "PreviewImage"); getWidget(mHeadRotate, "HeadRotate"); mHeadRotate->setScrollRange(1000); mHeadRotate->setScrollPosition(500); mHeadRotate->setScrollViewPage(50); mHeadRotate->setScrollPage(50); mHeadRotate->setScrollWheelPage(50); mHeadRotate->eventScrollChangePosition += MyGUI::newDelegate(this, &RaceDialog::onHeadRotate); // Set up next/previous buttons MyGUI::Button *prevButton, *nextButton; setText("GenderChoiceT", MWBase::Environment::get().getWindowManager()->getGameSettingString("sRaceMenu2", "Change Sex")); getWidget(prevButton, "PrevGenderButton"); getWidget(nextButton, "NextGenderButton"); prevButton->eventMouseButtonClick += MyGUI::newDelegate(this, &RaceDialog::onSelectPreviousGender); nextButton->eventMouseButtonClick += MyGUI::newDelegate(this, &RaceDialog::onSelectNextGender); setText("FaceChoiceT", MWBase::Environment::get().getWindowManager()->getGameSettingString("sRaceMenu3", "Change Face")); getWidget(prevButton, "PrevFaceButton"); getWidget(nextButton, "NextFaceButton"); prevButton->eventMouseButtonClick += MyGUI::newDelegate(this, &RaceDialog::onSelectPreviousFace); nextButton->eventMouseButtonClick += MyGUI::newDelegate(this, &RaceDialog::onSelectNextFace); setText("HairChoiceT", MWBase::Environment::get().getWindowManager()->getGameSettingString("sRaceMenu4", "Change Hair")); getWidget(prevButton, "PrevHairButton"); getWidget(nextButton, "NextHairButton"); prevButton->eventMouseButtonClick += MyGUI::newDelegate(this, &RaceDialog::onSelectPreviousHair); nextButton->eventMouseButtonClick += MyGUI::newDelegate(this, &RaceDialog::onSelectNextHair); setText("RaceT", MWBase::Environment::get().getWindowManager()->getGameSettingString("sRaceMenu5", "Race")); getWidget(mRaceList, "RaceList"); mRaceList->setScrollVisible(true); mRaceList->eventListSelectAccept += MyGUI::newDelegate(this, &RaceDialog::onAccept); mRaceList->eventListChangePosition += MyGUI::newDelegate(this, &RaceDialog::onSelectRace); setText("SkillsT", MWBase::Environment::get().getWindowManager()->getGameSettingString("sBonusSkillTitle", "Skill Bonus")); getWidget(mSkillList, "SkillList"); setText("SpellPowerT", MWBase::Environment::get().getWindowManager()->getGameSettingString("sRaceMenu7", "Specials")); getWidget(mSpellPowerList, "SpellPowerList"); MyGUI::Button* backButton; getWidget(backButton, "BackButton"); backButton->eventMouseButtonClick += MyGUI::newDelegate(this, &RaceDialog::onBackClicked); /* Start of tes3mp change (major) Disable back button here so players can't change their names after logging into their server accounts */ backButton->setVisible(false); /* End of tes3mp change (major) */ MyGUI::Button* okButton; getWidget(okButton, "OKButton"); okButton->setCaption(MWBase::Environment::get().getWindowManager()->getGameSettingString("sOK", "")); okButton->eventMouseButtonClick += MyGUI::newDelegate(this, &RaceDialog::onOkClicked); updateRaces(); updateSkills(); updateSpellPowers(); } void RaceDialog::setNextButtonShow(bool shown) { MyGUI::Button* okButton; getWidget(okButton, "OKButton"); if (shown) okButton->setCaption(MWBase::Environment::get().getWindowManager()->getGameSettingString("sNext", "")); else okButton->setCaption(MWBase::Environment::get().getWindowManager()->getGameSettingString("sOK", "")); } void RaceDialog::onOpen() { WindowModal::onOpen(); updateRaces(); updateSkills(); updateSpellPowers(); mPreviewImage->setRenderItemTexture(nullptr); mPreview.reset(nullptr); mPreviewTexture.reset(nullptr); mPreview.reset(new MWRender::RaceSelectionPreview(mParent, mResourceSystem)); mPreview->rebuild(); mPreview->setAngle (mCurrentAngle); mPreviewTexture.reset(new osgMyGUI::OSGTexture(mPreview->getTexture())); mPreviewImage->setRenderItemTexture(mPreviewTexture.get()); mPreviewImage->getSubWidgetMain()->_setUVSet(MyGUI::FloatRect(0.f, 0.f, 1.f, 1.f)); const ESM::NPC& proto = mPreview->getPrototype(); setRaceId(proto.mRace); setGender(proto.isMale() ? GM_Male : GM_Female); recountParts(); for (unsigned int i=0; igetScrollRange()/2+mHeadRotate->getScrollRange()/10; mHeadRotate->setScrollPosition(initialPos); onHeadRotate(mHeadRotate, initialPos); MWBase::Environment::get().getWindowManager()->setKeyFocusWidget(mRaceList); } void RaceDialog::setRaceId(const std::string &raceId) { mCurrentRaceId = raceId; mRaceList->setIndexSelected(MyGUI::ITEM_NONE); size_t count = mRaceList->getItemCount(); for (size_t i = 0; i < count; ++i) { if (Misc::StringUtils::ciEqual(*mRaceList->getItemDataAt(i), raceId)) { mRaceList->setIndexSelected(i); break; } } updateSkills(); updateSpellPowers(); } void RaceDialog::onClose() { WindowModal::onClose(); mPreviewImage->setRenderItemTexture(nullptr); mPreviewTexture.reset(nullptr); mPreview.reset(nullptr); } // widget controls void RaceDialog::onOkClicked(MyGUI::Widget* _sender) { if(mRaceList->getIndexSelected() == MyGUI::ITEM_NONE) return; eventDone(this); } void RaceDialog::onBackClicked(MyGUI::Widget* _sender) { eventBack(); } void RaceDialog::onHeadRotate(MyGUI::ScrollBar* scroll, size_t _position) { float angle = (float(_position) / (scroll->getScrollRange()-1) - 0.5f) * osg::PI * 2; mPreview->setAngle (angle); mCurrentAngle = angle; } void RaceDialog::onSelectPreviousGender(MyGUI::Widget*) { mGenderIndex = wrap(mGenderIndex - 1, 2); recountParts(); updatePreview(); } void RaceDialog::onSelectNextGender(MyGUI::Widget*) { mGenderIndex = wrap(mGenderIndex + 1, 2); recountParts(); updatePreview(); } void RaceDialog::onSelectPreviousFace(MyGUI::Widget*) { mFaceIndex = wrap(mFaceIndex - 1, mAvailableHeads.size()); updatePreview(); } void RaceDialog::onSelectNextFace(MyGUI::Widget*) { mFaceIndex = wrap(mFaceIndex + 1, mAvailableHeads.size()); updatePreview(); } void RaceDialog::onSelectPreviousHair(MyGUI::Widget*) { mHairIndex = wrap(mHairIndex - 1, mAvailableHairs.size()); updatePreview(); } void RaceDialog::onSelectNextHair(MyGUI::Widget*) { mHairIndex = wrap(mHairIndex + 1, mAvailableHairs.size()); updatePreview(); } void RaceDialog::onSelectRace(MyGUI::ListBox* _sender, size_t _index) { if (_index == MyGUI::ITEM_NONE) return; const std::string *raceId = mRaceList->getItemDataAt(_index); if (Misc::StringUtils::ciEqual(mCurrentRaceId, *raceId)) return; mCurrentRaceId = *raceId; recountParts(); updatePreview(); updateSkills(); updateSpellPowers(); } void RaceDialog::onAccept(MyGUI::ListBox *_sender, size_t _index) { onSelectRace(_sender, _index); if(mRaceList->getIndexSelected() == MyGUI::ITEM_NONE) return; eventDone(this); } void RaceDialog::getBodyParts (int part, std::vector& out) { out.clear(); const MWWorld::Store &store = MWBase::Environment::get().getWorld()->getStore().get(); for (const ESM::BodyPart& bodypart : store) { if (bodypart.mData.mFlags & ESM::BodyPart::BPF_NotPlayable) continue; if (bodypart.mData.mType != ESM::BodyPart::MT_Skin) continue; if (bodypart.mData.mPart != static_cast(part)) continue; if (mGenderIndex != (bodypart.mData.mFlags & ESM::BodyPart::BPF_Female)) continue; bool firstPerson = (bodypart.mId.size() >= 3) && bodypart.mId[bodypart.mId.size()-3] == '1' && bodypart.mId[bodypart.mId.size()-2] == 's' && bodypart.mId[bodypart.mId.size()-1] == 't'; if (firstPerson) continue; if (Misc::StringUtils::ciEqual(bodypart.mRace, mCurrentRaceId)) out.push_back(bodypart.mId); } } void RaceDialog::recountParts() { getBodyParts(ESM::BodyPart::MP_Hair, mAvailableHairs); getBodyParts(ESM::BodyPart::MP_Head, mAvailableHeads); mFaceIndex = 0; mHairIndex = 0; } // update widget content void RaceDialog::updatePreview() { ESM::NPC record = mPreview->getPrototype(); record.mRace = mCurrentRaceId; record.setIsMale(mGenderIndex == 0); if (mFaceIndex >= 0 && mFaceIndex < int(mAvailableHeads.size())) record.mHead = mAvailableHeads[mFaceIndex]; if (mHairIndex >= 0 && mHairIndex < int(mAvailableHairs.size())) record.mHair = mAvailableHairs[mHairIndex]; try { mPreview->setPrototype(record); } catch (std::exception& e) { Log(Debug::Error) << "Error creating preview: " << e.what(); } } void RaceDialog::updateRaces() { mRaceList->removeAllItems(); const MWWorld::Store &races = MWBase::Environment::get().getWorld()->getStore().get(); std::vector > items; // ID, name for (const ESM::Race& race : races) { bool playable = race.mData.mFlags & ESM::Race::Playable; if (!playable) // Only display playable races continue; items.emplace_back(race.mId, race.mName); } std::sort(items.begin(), items.end(), sortRaces); int index = 0; for (auto& item : items) { mRaceList->addItem(item.second, item.first); if (Misc::StringUtils::ciEqual(item.first, mCurrentRaceId)) mRaceList->setIndexSelected(index); ++index; } } void RaceDialog::updateSkills() { for (MyGUI::Widget* widget : mSkillItems) { MyGUI::Gui::getInstance().destroyWidget(widget); } mSkillItems.clear(); if (mCurrentRaceId.empty()) return; Widgets::MWSkillPtr skillWidget; const int lineHeight = 18; MyGUI::IntCoord coord1(0, 0, mSkillList->getWidth(), 18); const MWWorld::ESMStore &store = MWBase::Environment::get().getWorld()->getStore(); const ESM::Race *race = store.get().find(mCurrentRaceId); int count = sizeof(race->mData.mBonus)/sizeof(race->mData.mBonus[0]); // TODO: Find a portable macro for this ARRAYSIZE? for (int i = 0; i < count; ++i) { int skillId = race->mData.mBonus[i].mSkill; if (skillId < 0 || skillId > ESM::Skill::Length) // Skip unknown skill indexes continue; skillWidget = mSkillList->createWidget("MW_StatNameValue", coord1, MyGUI::Align::Default, std::string("Skill") + MyGUI::utility::toString(i)); skillWidget->setSkillNumber(skillId); skillWidget->setSkillValue(Widgets::MWSkill::SkillValue(static_cast(race->mData.mBonus[i].mBonus))); ToolTips::createSkillToolTip(skillWidget, skillId); mSkillItems.push_back(skillWidget); coord1.top += lineHeight; } } void RaceDialog::updateSpellPowers() { for (MyGUI::Widget* widget : mSpellPowerItems) { MyGUI::Gui::getInstance().destroyWidget(widget); } mSpellPowerItems.clear(); if (mCurrentRaceId.empty()) return; const int lineHeight = 18; MyGUI::IntCoord coord(0, 0, mSpellPowerList->getWidth(), 18); const MWWorld::ESMStore &store = MWBase::Environment::get().getWorld()->getStore(); const ESM::Race *race = store.get().find(mCurrentRaceId); int i = 0; for (const std::string& spellpower : race->mPowers.mList) { Widgets::MWSpellPtr spellPowerWidget = mSpellPowerList->createWidget("MW_StatName", coord, MyGUI::Align::Default, std::string("SpellPower") + MyGUI::utility::toString(i)); spellPowerWidget->setSpellId(spellpower); spellPowerWidget->setUserString("ToolTipType", "Spell"); spellPowerWidget->setUserString("Spell", spellpower); mSpellPowerItems.push_back(spellPowerWidget); coord.top += lineHeight; ++i; } } const ESM::NPC& RaceDialog::getResult() const { return mPreview->getPrototype(); } } ================================================ FILE: apps/openmw/mwgui/race.hpp ================================================ #ifndef MWGUI_RACE_H #define MWGUI_RACE_H #include #include "windowbase.hpp" #include namespace MWRender { class RaceSelectionPreview; } namespace ESM { struct NPC; } namespace osg { class Group; } namespace Resource { class ResourceSystem; } namespace MWGui { class RaceDialog : public WindowModal { public: RaceDialog(osg::Group* parent, Resource::ResourceSystem* resourceSystem); enum Gender { GM_Male, GM_Female }; const ESM::NPC &getResult() const; const std::string &getRaceId() const { return mCurrentRaceId; } Gender getGender() const { return mGenderIndex == 0 ? GM_Male : GM_Female; } void setRaceId(const std::string &raceId); void setGender(Gender gender) { mGenderIndex = gender == GM_Male ? 0 : 1; } void setNextButtonShow(bool shown); void onOpen() override; void onClose() override; bool exit() override { return false; } // Events typedef MyGUI::delegates::CMultiDelegate0 EventHandle_Void; /** Event : Back button clicked.\n signature : void method()\n */ EventHandle_Void eventBack; /** Event : Dialog finished, OK button clicked.\n signature : void method()\n */ EventHandle_WindowBase eventDone; protected: void onHeadRotate(MyGUI::ScrollBar* _sender, size_t _position); void onSelectPreviousGender(MyGUI::Widget* _sender); void onSelectNextGender(MyGUI::Widget* _sender); void onSelectPreviousFace(MyGUI::Widget* _sender); void onSelectNextFace(MyGUI::Widget* _sender); void onSelectPreviousHair(MyGUI::Widget* _sender); void onSelectNextHair(MyGUI::Widget* _sender); void onSelectRace(MyGUI::ListBox* _sender, size_t _index); void onAccept(MyGUI::ListBox* _sender, size_t _index); void onOkClicked(MyGUI::Widget* _sender); void onBackClicked(MyGUI::Widget* _sender); private: void updateRaces(); void updateSkills(); void updateSpellPowers(); void updatePreview(); void recountParts(); void getBodyParts (int part, std::vector& out); osg::Group* mParent; Resource::ResourceSystem* mResourceSystem; std::vector mAvailableHeads; std::vector mAvailableHairs; MyGUI::ImageBox* mPreviewImage; MyGUI::ListBox* mRaceList; MyGUI::ScrollBar* mHeadRotate; MyGUI::Widget* mSkillList; std::vector mSkillItems; MyGUI::Widget* mSpellPowerList; std::vector mSpellPowerItems; int mGenderIndex, mFaceIndex, mHairIndex; std::string mCurrentRaceId; float mCurrentAngle; std::unique_ptr mPreview; std::unique_ptr mPreviewTexture; bool mPreviewDirty; }; } #endif ================================================ FILE: apps/openmw/mwgui/recharge.cpp ================================================ #include "recharge.hpp" #include #include #include #include "../mwbase/world.hpp" #include "../mwbase/environment.hpp" #include "../mwbase/windowmanager.hpp" #include "../mwworld/containerstore.hpp" #include "../mwworld/class.hpp" #include "../mwworld/esmstore.hpp" #include "../mwmechanics/actorutil.hpp" #include "../mwmechanics/recharge.hpp" #include "itemselection.hpp" #include "itemwidget.hpp" #include "itemchargeview.hpp" #include "sortfilteritemmodel.hpp" #include "inventoryitemmodel.hpp" namespace MWGui { Recharge::Recharge() : WindowBase("openmw_recharge_dialog.layout") , mItemSelectionDialog(nullptr) { getWidget(mBox, "Box"); getWidget(mGemBox, "GemBox"); getWidget(mGemIcon, "GemIcon"); getWidget(mChargeLabel, "ChargeLabel"); getWidget(mCancelButton, "CancelButton"); mCancelButton->eventMouseButtonClick += MyGUI::newDelegate(this, &Recharge::onCancel); mBox->eventItemClicked += MyGUI::newDelegate(this, &Recharge::onItemClicked); mBox->setDisplayMode(ItemChargeView::DisplayMode_EnchantmentCharge); mGemIcon->eventMouseButtonClick += MyGUI::newDelegate(this, &Recharge::onSelectItem); } void Recharge::onOpen() { center(); SortFilterItemModel * model = new SortFilterItemModel(new InventoryItemModel(MWMechanics::getPlayer())); model->setFilter(SortFilterItemModel::Filter_OnlyRechargable); mBox->setModel(model); // Reset scrollbars mBox->resetScrollbars(); } void Recharge::setPtr (const MWWorld::Ptr &item) { mGemIcon->setItem(item); mGemIcon->setUserString("ToolTipType", "ItemPtr"); mGemIcon->setUserData(MWWorld::Ptr(item)); updateView(); } void Recharge::updateView() { MWWorld::Ptr gem = *mGemIcon->getUserData(); std::string soul = gem.getCellRef().getSoul(); const ESM::Creature *creature = MWBase::Environment::get().getWorld()->getStore().get().find(soul); mChargeLabel->setCaptionWithReplacing("#{sCharges} " + MyGUI::utility::toString(creature->mData.mSoul)); bool toolBoxVisible = (gem.getRefData().getCount() != 0); mGemBox->setVisible(toolBoxVisible); mGemBox->setUserString("Hidden", toolBoxVisible ? "false" : "true"); if (!toolBoxVisible) { mGemIcon->setItem(MWWorld::Ptr()); mGemIcon->clearUserStrings(); } mBox->update(); Gui::Box* box = dynamic_cast(mMainWidget); if (box == nullptr) throw std::runtime_error("main widget must be a box"); box->notifyChildrenSizeChanged(); center(); } void Recharge::onCancel(MyGUI::Widget *sender) { MWBase::Environment::get().getWindowManager()->removeGuiMode(GM_Recharge); } void Recharge::onSelectItem(MyGUI::Widget *sender) { delete mItemSelectionDialog; mItemSelectionDialog = new ItemSelectionDialog("#{sSoulGemsWithSouls}"); mItemSelectionDialog->eventItemSelected += MyGUI::newDelegate(this, &Recharge::onItemSelected); mItemSelectionDialog->eventDialogCanceled += MyGUI::newDelegate(this, &Recharge::onItemCancel); mItemSelectionDialog->setVisible(true); mItemSelectionDialog->openContainer(MWMechanics::getPlayer()); mItemSelectionDialog->setFilter(SortFilterItemModel::Filter_OnlyChargedSoulstones); } void Recharge::onItemSelected(MWWorld::Ptr item) { mItemSelectionDialog->setVisible(false); mGemIcon->setItem(item); mGemIcon->setUserString ("ToolTipType", "ItemPtr"); mGemIcon->setUserData(item); MWBase::Environment::get().getWindowManager()->playSound(item.getClass().getDownSoundId(item)); updateView(); } void Recharge::onItemCancel() { mItemSelectionDialog->setVisible(false); } void Recharge::onItemClicked(MyGUI::Widget *sender, const MWWorld::Ptr& item) { MWWorld::Ptr gem = *mGemIcon->getUserData(); if (!MWMechanics::rechargeItem(item, gem)) return; updateView(); } } ================================================ FILE: apps/openmw/mwgui/recharge.hpp ================================================ #ifndef OPENMW_MWGUI_RECHARGE_H #define OPENMW_MWGUI_RECHARGE_H #include "windowbase.hpp" namespace MWWorld { class Ptr; } namespace MWGui { class ItemSelectionDialog; class ItemWidget; class ItemChargeView; class Recharge : public WindowBase { public: Recharge(); void onOpen() override; void setPtr (const MWWorld::Ptr& gem) override; protected: ItemChargeView* mBox; MyGUI::Widget* mGemBox; ItemWidget* mGemIcon; ItemSelectionDialog* mItemSelectionDialog; MyGUI::TextBox* mChargeLabel; MyGUI::Button* mCancelButton; void updateView(); void onSelectItem(MyGUI::Widget* sender); void onItemSelected(MWWorld::Ptr item); void onItemCancel(); void onItemClicked (MyGUI::Widget* sender, const MWWorld::Ptr& item); void onCancel (MyGUI::Widget* sender); void onMouseWheel(MyGUI::Widget* _sender, int _rel); }; } #endif ================================================ FILE: apps/openmw/mwgui/referenceinterface.cpp ================================================ #include "referenceinterface.hpp" namespace MWGui { ReferenceInterface::ReferenceInterface() { } ReferenceInterface::~ReferenceInterface() { } void ReferenceInterface::checkReferenceAvailable() { // check if count of the reference has become 0 if (!mPtr.isEmpty() && mPtr.getRefData().getCount() == 0) { mPtr = MWWorld::Ptr(); onReferenceUnavailable(); } } } ================================================ FILE: apps/openmw/mwgui/referenceinterface.hpp ================================================ #ifndef MWGUI_REFERENCEINTERFACE_H #define MWGUI_REFERENCEINTERFACE_H #include "../mwworld/ptr.hpp" namespace MWGui { /// \brief this class is intended for GUI interfaces that access an MW-Reference /// for example dialogue window accesses an NPC, or Container window accesses a Container /// these classes have to be automatically closed if the reference becomes unavailable /// make sure that checkReferenceAvailable() is called every frame and that onReferenceUnavailable() has been overridden class ReferenceInterface { public: ReferenceInterface(); virtual ~ReferenceInterface(); void checkReferenceAvailable(); ///< closes the window, if the MW-reference has become unavailable virtual void resetReference() { mPtr = MWWorld::Ptr(); } protected: virtual void onReferenceUnavailable() = 0; ///< called when reference has become unavailable MWWorld::Ptr mPtr; }; } #endif ================================================ FILE: apps/openmw/mwgui/repair.cpp ================================================ #include "repair.hpp" #include #include #include #include "../mwbase/world.hpp" #include "../mwbase/environment.hpp" #include "../mwbase/windowmanager.hpp" #include "../mwmechanics/actorutil.hpp" #include "../mwworld/containerstore.hpp" #include "../mwworld/class.hpp" #include "itemselection.hpp" #include "itemwidget.hpp" #include "itemchargeview.hpp" #include "sortfilteritemmodel.hpp" #include "inventoryitemmodel.hpp" namespace MWGui { Repair::Repair() : WindowBase("openmw_repair.layout") , mItemSelectionDialog(nullptr) { getWidget(mRepairBox, "RepairBox"); getWidget(mToolBox, "ToolBox"); getWidget(mToolIcon, "ToolIcon"); getWidget(mUsesLabel, "UsesLabel"); getWidget(mQualityLabel, "QualityLabel"); getWidget(mCancelButton, "CancelButton"); mCancelButton->eventMouseButtonClick += MyGUI::newDelegate(this, &Repair::onCancel); mRepairBox->eventItemClicked += MyGUI::newDelegate(this, &Repair::onRepairItem); mRepairBox->setDisplayMode(ItemChargeView::DisplayMode_Health); mToolIcon->eventMouseButtonClick += MyGUI::newDelegate(this, &Repair::onSelectItem); } void Repair::onOpen() { center(); SortFilterItemModel * model = new SortFilterItemModel(new InventoryItemModel(MWMechanics::getPlayer())); model->setFilter(SortFilterItemModel::Filter_OnlyRepairable); mRepairBox->setModel(model); // Reset scrollbars mRepairBox->resetScrollbars(); } void Repair::setPtr(const MWWorld::Ptr &item) { MWBase::Environment::get().getWindowManager()->playSound("Item Repair Up"); mRepair.setTool(item); mToolIcon->setItem(item); mToolIcon->setUserString("ToolTipType", "ItemPtr"); mToolIcon->setUserData(MWWorld::Ptr(item)); updateRepairView(); } void Repair::updateRepairView() { MWWorld::LiveCellRef *ref = mRepair.getTool().get(); int uses = mRepair.getTool().getClass().getItemHealth(mRepair.getTool()); float quality = ref->mBase->mData.mQuality; mToolIcon->setUserData(mRepair.getTool()); std::stringstream qualityStr; qualityStr << std::setprecision(3) << quality; mUsesLabel->setCaptionWithReplacing("#{sUses} " + MyGUI::utility::toString(uses)); mQualityLabel->setCaptionWithReplacing("#{sQuality} " + qualityStr.str()); bool toolBoxVisible = (mRepair.getTool().getRefData().getCount() != 0); mToolBox->setVisible(toolBoxVisible); mToolBox->setUserString("Hidden", toolBoxVisible ? "false" : "true"); if (!toolBoxVisible) { mToolIcon->setItem(MWWorld::Ptr()); mToolIcon->clearUserStrings(); } mRepairBox->update(); Gui::Box* box = dynamic_cast(mMainWidget); if (box == nullptr) throw std::runtime_error("main widget must be a box"); box->notifyChildrenSizeChanged(); center(); } void Repair::onSelectItem(MyGUI::Widget *sender) { delete mItemSelectionDialog; mItemSelectionDialog = new ItemSelectionDialog("#{sRepair}"); mItemSelectionDialog->eventItemSelected += MyGUI::newDelegate(this, &Repair::onItemSelected); mItemSelectionDialog->eventDialogCanceled += MyGUI::newDelegate(this, &Repair::onItemCancel); mItemSelectionDialog->setVisible(true); mItemSelectionDialog->openContainer(MWMechanics::getPlayer()); mItemSelectionDialog->setFilter(SortFilterItemModel::Filter_OnlyRepairTools); } void Repair::onItemSelected(MWWorld::Ptr item) { mItemSelectionDialog->setVisible(false); mToolIcon->setItem(item); mToolIcon->setUserString ("ToolTipType", "ItemPtr"); mToolIcon->setUserData(item); mRepair.setTool(item); MWBase::Environment::get().getWindowManager()->playSound(item.getClass().getDownSoundId(item)); updateRepairView(); } void Repair::onItemCancel() { mItemSelectionDialog->setVisible(false); } void Repair::onCancel(MyGUI::Widget* /*sender*/) { MWBase::Environment::get().getWindowManager()->removeGuiMode(GM_Repair); } void Repair::onRepairItem(MyGUI::Widget* /*sender*/, const MWWorld::Ptr& ptr) { if (!mRepair.getTool().getRefData().getCount()) return; mRepair.repair(ptr); updateRepairView(); } } ================================================ FILE: apps/openmw/mwgui/repair.hpp ================================================ #ifndef OPENMW_MWGUI_REPAIR_H #define OPENMW_MWGUI_REPAIR_H #include "windowbase.hpp" #include "../mwmechanics/repair.hpp" namespace MWGui { class ItemSelectionDialog; class ItemWidget; class ItemChargeView; class Repair : public WindowBase { public: Repair(); void onOpen() override; void setPtr (const MWWorld::Ptr& item) override; protected: ItemChargeView* mRepairBox; MyGUI::Widget* mToolBox; ItemWidget* mToolIcon; ItemSelectionDialog* mItemSelectionDialog; MyGUI::TextBox* mUsesLabel; MyGUI::TextBox* mQualityLabel; MyGUI::Button* mCancelButton; MWMechanics::Repair mRepair; void updateRepairView(); void onSelectItem(MyGUI::Widget* sender); void onItemSelected(MWWorld::Ptr item); void onItemCancel(); void onRepairItem(MyGUI::Widget* sender, const MWWorld::Ptr& ptr); void onCancel(MyGUI::Widget* sender); }; } #endif ================================================ FILE: apps/openmw/mwgui/resourceskin.cpp ================================================ #include "resourceskin.hpp" #include #include namespace MWGui { void resizeSkin(MyGUI::xml::ElementPtr _node) { _node->setAttribute("type", "ResourceSkin"); const std::string size = _node->findAttribute("size"); if (!size.empty()) return; const std::string textureName = _node->findAttribute("texture"); if (textureName.empty()) return; MyGUI::ITexture* texture = MyGUI::RenderManager::getInstance().getTexture(textureName); if (!texture) return; MyGUI::IntCoord coord(0, 0, texture->getWidth(), texture->getHeight()); MyGUI::xml::ElementEnumerator basis = _node->getElementEnumerator(); const std::string textureSize = std::to_string(coord.width) + " " + std::to_string(coord.height); _node->addAttribute("size", textureSize); while (basis.next()) { if (basis->getName() != "BasisSkin") continue; const std::string basisSkinType = basis->findAttribute("type"); if (Misc::StringUtils::ciEqual(basisSkinType, "SimpleText")) continue; const std::string offset = basis->findAttribute("offset"); if (!offset.empty()) continue; basis->addAttribute("offset", coord); MyGUI::xml::ElementEnumerator state = basis->getElementEnumerator(); while (state.next()) { if (state->getName() == "State") { const std::string stateOffset = state->findAttribute("offset"); if (!stateOffset.empty()) continue; state->addAttribute("offset", coord); if (Misc::StringUtils::ciEqual(basisSkinType, "TileRect")) { MyGUI::xml::ElementEnumerator property = state->getElementEnumerator(); bool hasTileSize = false; while (property.next("Property")) { const std::string key = property->findAttribute("key"); if (key != "TileSize") continue; hasTileSize = true; } if (!hasTileSize) { MyGUI::xml::ElementPtr tileSizeProperty = state->createChild("Property"); tileSizeProperty->addAttribute("key", "TileSize"); tileSizeProperty->addAttribute("value", textureSize); } } } } } } void AutoSizedResourceSkin::deserialization(MyGUI::xml::ElementPtr _node, MyGUI::Version _version) { resizeSkin(_node); Base::deserialization(_node, _version); } } ================================================ FILE: apps/openmw/mwgui/resourceskin.hpp ================================================ #ifndef MWGUI_RESOURCESKIN_H #define MWGUI_RESOURCESKIN_H #include namespace MWGui { class AutoSizedResourceSkin final : public MyGUI::ResourceSkin { MYGUI_RTTI_DERIVED( AutoSizedResourceSkin ) public: void deserialization(MyGUI::xml::ElementPtr _node, MyGUI::Version _version) override; }; } #endif ================================================ FILE: apps/openmw/mwgui/review.cpp ================================================ #include "review.hpp" #include #include #include #include #include "../mwbase/environment.hpp" #include "../mwbase/world.hpp" #include "../mwbase/windowmanager.hpp" #include "../mwworld/esmstore.hpp" #include "../mwmechanics/autocalcspell.hpp" #include "tooltips.hpp" namespace { void adjustButtonSize(MyGUI::Button *button) { // adjust size of button to fit its text MyGUI::IntSize size = button->getTextSize(); button->setSize(size.width + 24, button->getSize().height); } } namespace MWGui { ReviewDialog::ReviewDialog() : WindowModal("openmw_chargen_review.layout"), mUpdateSkillArea(false) { // Centre dialog center(); // Setup static stats MyGUI::Button* button; getWidget(mNameWidget, "NameText"); getWidget(button, "NameButton"); adjustButtonSize(button); button->eventMouseButtonClick += MyGUI::newDelegate(this, &ReviewDialog::onNameClicked); getWidget(mRaceWidget, "RaceText"); getWidget(button, "RaceButton"); adjustButtonSize(button); button->eventMouseButtonClick += MyGUI::newDelegate(this, &ReviewDialog::onRaceClicked); getWidget(mClassWidget, "ClassText"); getWidget(button, "ClassButton"); adjustButtonSize(button); button->eventMouseButtonClick += MyGUI::newDelegate(this, &ReviewDialog::onClassClicked); getWidget(mBirthSignWidget, "SignText"); getWidget(button, "SignButton"); adjustButtonSize(button); button->eventMouseButtonClick += MyGUI::newDelegate(this, &ReviewDialog::onBirthSignClicked); // Setup dynamic stats getWidget(mHealth, "Health"); mHealth->setTitle(MWBase::Environment::get().getWindowManager()->getGameSettingString("sHealth", "")); mHealth->setValue(45, 45); getWidget(mMagicka, "Magicka"); mMagicka->setTitle(MWBase::Environment::get().getWindowManager()->getGameSettingString("sMagic", "")); mMagicka->setValue(50, 50); getWidget(mFatigue, "Fatigue"); mFatigue->setTitle(MWBase::Environment::get().getWindowManager()->getGameSettingString("sFatigue", "")); mFatigue->setValue(160, 160); // Setup attributes Widgets::MWAttributePtr attribute; for (int idx = 0; idx < ESM::Attribute::Length; ++idx) { getWidget(attribute, std::string("Attribute") + MyGUI::utility::toString(idx)); mAttributeWidgets.insert(std::make_pair(static_cast(ESM::Attribute::sAttributeIds[idx]), attribute)); attribute->setAttributeId(ESM::Attribute::sAttributeIds[idx]); attribute->setAttributeValue(Widgets::MWAttribute::AttributeValue()); } // Setup skills getWidget(mSkillView, "SkillView"); mSkillView->eventMouseWheel += MyGUI::newDelegate(this, &ReviewDialog::onMouseWheel); for (int i = 0; i < ESM::Skill::Length; ++i) { mSkillValues.insert(std::make_pair(i, MWMechanics::SkillValue())); mSkillWidgetMap.insert(std::make_pair(i, static_cast (nullptr))); } MyGUI::Button* backButton; getWidget(backButton, "BackButton"); backButton->eventMouseButtonClick += MyGUI::newDelegate(this, &ReviewDialog::onBackClicked); MyGUI::Button* okButton; getWidget(okButton, "OKButton"); okButton->eventMouseButtonClick += MyGUI::newDelegate(this, &ReviewDialog::onOkClicked); } void ReviewDialog::onOpen() { WindowModal::onOpen(); mUpdateSkillArea = true; } void ReviewDialog::onFrame(float /*duration*/) { if (mUpdateSkillArea) { updateSkillArea(); mUpdateSkillArea = false; } } void ReviewDialog::setPlayerName(const std::string &name) { mNameWidget->setCaption(name); } void ReviewDialog::setRace(const std::string &raceId) { mRaceId = raceId; const ESM::Race *race = MWBase::Environment::get().getWorld()->getStore().get().search(mRaceId); if (race) { ToolTips::createRaceToolTip(mRaceWidget, race); mRaceWidget->setCaption(race->mName); } mUpdateSkillArea = true; } void ReviewDialog::setClass(const ESM::Class& class_) { mKlass = class_; mClassWidget->setCaption(mKlass.mName); ToolTips::createClassToolTip(mClassWidget, mKlass); } void ReviewDialog::setBirthSign(const std::string& signId) { mBirthSignId = signId; const ESM::BirthSign *sign = MWBase::Environment::get().getWorld()->getStore().get().search(mBirthSignId); if (sign) { mBirthSignWidget->setCaption(sign->mName); ToolTips::createBirthsignToolTip(mBirthSignWidget, mBirthSignId); } mUpdateSkillArea = true; } void ReviewDialog::setHealth(const MWMechanics::DynamicStat& value) { int current = std::max(0, static_cast(value.getCurrent())); int modified = static_cast(value.getModified()); mHealth->setValue(current, modified); std::string valStr = MyGUI::utility::toString(current) + " / " + MyGUI::utility::toString(modified); mHealth->setUserString("Caption_HealthDescription", "#{sHealthDesc}\n" + valStr); } void ReviewDialog::setMagicka(const MWMechanics::DynamicStat& value) { int current = std::max(0, static_cast(value.getCurrent())); int modified = static_cast(value.getModified()); mMagicka->setValue(current, modified); std::string valStr = MyGUI::utility::toString(current) + " / " + MyGUI::utility::toString(modified); mMagicka->setUserString("Caption_HealthDescription", "#{sMagDesc}\n" + valStr); } void ReviewDialog::setFatigue(const MWMechanics::DynamicStat& value) { int current = static_cast(value.getCurrent()); int modified = static_cast(value.getModified()); mFatigue->setValue(current, modified); std::string valStr = MyGUI::utility::toString(current) + " / " + MyGUI::utility::toString(modified); mFatigue->setUserString("Caption_HealthDescription", "#{sFatDesc}\n" + valStr); } void ReviewDialog::setAttribute(ESM::Attribute::AttributeID attributeId, const MWMechanics::AttributeValue& value) { std::map::iterator attr = mAttributeWidgets.find(static_cast(attributeId)); if (attr == mAttributeWidgets.end()) return; if (attr->second->getAttributeValue() != value) { attr->second->setAttributeValue(value); mUpdateSkillArea = true; } } void ReviewDialog::setSkillValue(ESM::Skill::SkillEnum skillId, const MWMechanics::SkillValue& value) { mSkillValues[skillId] = value; MyGUI::TextBox* widget = mSkillWidgetMap[skillId]; if (widget) { float modified = static_cast(value.getModified()), base = static_cast(value.getBase()); std::string text = MyGUI::utility::toString(std::floor(modified)); std::string state = "normal"; if (modified > base) state = "increased"; else if (modified < base) state = "decreased"; widget->setCaption(text); widget->_setWidgetState(state); } mUpdateSkillArea = true; } void ReviewDialog::configureSkills(const std::vector& major, const std::vector& minor) { mMajorSkills = major; mMinorSkills = minor; // Update misc skills with the remaining skills not in major or minor std::set skillSet; std::copy(major.begin(), major.end(), std::inserter(skillSet, skillSet.begin())); std::copy(minor.begin(), minor.end(), std::inserter(skillSet, skillSet.begin())); mMiscSkills.clear(); for (const int skill : ESM::Skill::sSkillIds) { if (skillSet.find(skill) == skillSet.end()) mMiscSkills.push_back(skill); } mUpdateSkillArea = true; } void ReviewDialog::addSeparator(MyGUI::IntCoord &coord1, MyGUI::IntCoord &coord2) { MyGUI::ImageBox* separator = mSkillView->createWidget("MW_HLine", MyGUI::IntCoord(10, coord1.top, coord1.width + coord2.width - 4, 18), MyGUI::Align::Default); separator->eventMouseWheel += MyGUI::newDelegate(this, &ReviewDialog::onMouseWheel); mSkillWidgets.push_back(separator); coord1.top += separator->getHeight(); coord2.top += separator->getHeight(); } void ReviewDialog::addGroup(const std::string &label, MyGUI::IntCoord &coord1, MyGUI::IntCoord &coord2) { MyGUI::TextBox* groupWidget = mSkillView->createWidget("SandBrightText", MyGUI::IntCoord(0, coord1.top, coord1.width + coord2.width, coord1.height), MyGUI::Align::Default); groupWidget->eventMouseWheel += MyGUI::newDelegate(this, &ReviewDialog::onMouseWheel); groupWidget->setCaption(label); mSkillWidgets.push_back(groupWidget); int lineHeight = MWBase::Environment::get().getWindowManager()->getFontHeight() + 2; coord1.top += lineHeight; coord2.top += lineHeight; } MyGUI::TextBox* ReviewDialog::addValueItem(const std::string& text, const std::string &value, const std::string& state, MyGUI::IntCoord &coord1, MyGUI::IntCoord &coord2) { MyGUI::TextBox* skillNameWidget; MyGUI::TextBox* skillValueWidget; skillNameWidget = mSkillView->createWidget("SandText", coord1, MyGUI::Align::Default); skillNameWidget->setCaption(text); skillNameWidget->eventMouseWheel += MyGUI::newDelegate(this, &ReviewDialog::onMouseWheel); skillValueWidget = mSkillView->createWidget("SandTextRight", coord2, MyGUI::Align::Default); skillValueWidget->setCaption(value); skillValueWidget->_setWidgetState(state); skillValueWidget->eventMouseWheel += MyGUI::newDelegate(this, &ReviewDialog::onMouseWheel); mSkillWidgets.push_back(skillNameWidget); mSkillWidgets.push_back(skillValueWidget); int lineHeight = MWBase::Environment::get().getWindowManager()->getFontHeight() + 2; coord1.top += lineHeight; coord2.top += lineHeight; return skillValueWidget; } void ReviewDialog::addItem(const std::string& text, MyGUI::IntCoord &coord1, MyGUI::IntCoord &coord2) { MyGUI::TextBox* skillNameWidget; skillNameWidget = mSkillView->createWidget("SandText", coord1 + MyGUI::IntSize(coord2.width, 0), MyGUI::Align::Default); skillNameWidget->setCaption(text); skillNameWidget->eventMouseWheel += MyGUI::newDelegate(this, &ReviewDialog::onMouseWheel); mSkillWidgets.push_back(skillNameWidget); int lineHeight = MWBase::Environment::get().getWindowManager()->getFontHeight() + 2; coord1.top += lineHeight; coord2.top += lineHeight; } void ReviewDialog::addItem(const ESM::Spell* spell, MyGUI::IntCoord& coord1, MyGUI::IntCoord& coord2) { Widgets::MWSpellPtr widget = mSkillView->createWidget("MW_StatName", coord1 + MyGUI::IntSize(coord2.width, 0), MyGUI::Align::Default); widget->setSpellId(spell->mId); widget->setUserString("ToolTipType", "Spell"); widget->setUserString("Spell", spell->mId); widget->eventMouseWheel += MyGUI::newDelegate(this, &ReviewDialog::onMouseWheel); mSkillWidgets.push_back(widget); int lineHeight = MWBase::Environment::get().getWindowManager()->getFontHeight() + 2; coord1.top += lineHeight; coord2.top += lineHeight; } void ReviewDialog::addSkills(const SkillList &skills, const std::string &titleId, const std::string &titleDefault, MyGUI::IntCoord &coord1, MyGUI::IntCoord &coord2) { // Add a line separator if there are items above if (!mSkillWidgets.empty()) { addSeparator(coord1, coord2); } addGroup(MWBase::Environment::get().getWindowManager()->getGameSettingString(titleId, titleDefault), coord1, coord2); for (const int& skillId : skills) { if (skillId < 0 || skillId >= ESM::Skill::Length) // Skip unknown skill indexes continue; assert(skillId >= 0 && skillId < ESM::Skill::Length); const std::string &skillNameId = ESM::Skill::sSkillNameIds[skillId]; const MWMechanics::SkillValue &stat = mSkillValues.find(skillId)->second; int base = stat.getBase(); int modified = stat.getModified(); std::string state = "normal"; if (modified > base) state = "increased"; else if (modified < base) state = "decreased"; MyGUI::TextBox* widget = addValueItem(MWBase::Environment::get().getWindowManager()->getGameSettingString(skillNameId, skillNameId), MyGUI::utility::toString(static_cast(modified)), state, coord1, coord2); for (int i=0; i<2; ++i) { ToolTips::createSkillToolTip(mSkillWidgets[mSkillWidgets.size()-1-i], skillId); } mSkillWidgetMap[skillId] = widget; } } void ReviewDialog::updateSkillArea() { for (MyGUI::Widget* skillWidget : mSkillWidgets) { MyGUI::Gui::getInstance().destroyWidget(skillWidget); } mSkillWidgets.clear(); const int valueSize = 40; MyGUI::IntCoord coord1(10, 0, mSkillView->getWidth() - (10 + valueSize) - 24, 18); MyGUI::IntCoord coord2(coord1.left + coord1.width, coord1.top, valueSize, coord1.height); if (!mMajorSkills.empty()) addSkills(mMajorSkills, "sSkillClassMajor", "Major Skills", coord1, coord2); if (!mMinorSkills.empty()) addSkills(mMinorSkills, "sSkillClassMinor", "Minor Skills", coord1, coord2); if (!mMiscSkills.empty()) addSkills(mMiscSkills, "sSkillClassMisc", "Misc Skills", coord1, coord2); // starting spells std::vector spells; const ESM::Race* race = nullptr; if (!mRaceId.empty()) race = MWBase::Environment::get().getWorld()->getStore().get().find(mRaceId); int skills[ESM::Skill::Length]; for (int i=0; isecond.getBase(); int attributes[ESM::Attribute::Length]; for (int i=0; igetAttributeValue().getBase(); std::vector selectedSpells = MWMechanics::autoCalcPlayerSpells(skills, attributes, race); for (std::string& spellId : selectedSpells) { std::string lower = Misc::StringUtils::lowerCase(spellId); if (std::find(spells.begin(), spells.end(), lower) == spells.end()) spells.push_back(lower); } if (race) { for (const std::string& spellId : race->mPowers.mList) { std::string lower = Misc::StringUtils::lowerCase(spellId); if (std::find(spells.begin(), spells.end(), lower) == spells.end()) spells.push_back(lower); } } if (!mBirthSignId.empty()) { const ESM::BirthSign* sign = MWBase::Environment::get().getWorld()->getStore().get().find(mBirthSignId); for (const std::string& spellId : sign->mPowers.mList) { std::string lower = Misc::StringUtils::lowerCase(spellId); if (std::find(spells.begin(), spells.end(), lower) == spells.end()) spells.push_back(lower); } } if (!mSkillWidgets.empty()) addSeparator(coord1, coord2); addGroup(MWBase::Environment::get().getWindowManager()->getGameSettingString("sTypeAbility", "Abilities"), coord1, coord2); for (std::string& spellId : spells) { const ESM::Spell* spell = MWBase::Environment::get().getWorld()->getStore().get().find(spellId); if (spell->mData.mType == ESM::Spell::ST_Ability) addItem(spell, coord1, coord2); } addSeparator(coord1, coord2); addGroup(MWBase::Environment::get().getWindowManager()->getGameSettingString("sTypePower", "Powers"), coord1, coord2); for (std::string& spellId : spells) { const ESM::Spell* spell = MWBase::Environment::get().getWorld()->getStore().get().find(spellId); if (spell->mData.mType == ESM::Spell::ST_Power) addItem(spell, coord1, coord2); } addSeparator(coord1, coord2); addGroup(MWBase::Environment::get().getWindowManager()->getGameSettingString("sTypeSpell", "Spells"), coord1, coord2); for (std::string& spellId : spells) { const ESM::Spell* spell = MWBase::Environment::get().getWorld()->getStore().get().find(spellId); if (spell->mData.mType == ESM::Spell::ST_Spell) addItem(spell, coord1, coord2); } // Canvas size must be expressed with VScroll disabled, otherwise MyGUI would expand the scroll area when the scrollbar is hidden mSkillView->setVisibleVScroll(false); mSkillView->setCanvasSize (mSkillView->getWidth(), std::max(mSkillView->getHeight(), coord1.top)); mSkillView->setVisibleVScroll(true); } // widget controls void ReviewDialog::onOkClicked(MyGUI::Widget* _sender) { eventDone(this); } void ReviewDialog::onBackClicked(MyGUI::Widget* _sender) { eventBack(); } void ReviewDialog::onNameClicked(MyGUI::Widget* _sender) { eventActivateDialog(NAME_DIALOG); } void ReviewDialog::onRaceClicked(MyGUI::Widget* _sender) { eventActivateDialog(RACE_DIALOG); } void ReviewDialog::onClassClicked(MyGUI::Widget* _sender) { eventActivateDialog(CLASS_DIALOG); } void ReviewDialog::onBirthSignClicked(MyGUI::Widget* _sender) { eventActivateDialog(BIRTHSIGN_DIALOG); } void ReviewDialog::onMouseWheel(MyGUI::Widget* _sender, int _rel) { if (mSkillView->getViewOffset().top + _rel*0.3 > 0) mSkillView->setViewOffset(MyGUI::IntPoint(0, 0)); else mSkillView->setViewOffset(MyGUI::IntPoint(0, static_cast(mSkillView->getViewOffset().top + _rel*0.3))); } } ================================================ FILE: apps/openmw/mwgui/review.hpp ================================================ #ifndef MWGUI_REVIEW_H #define MWGUI_REVIEW_H #include #include #include "windowbase.hpp" #include "widgets.hpp" namespace ESM { struct Spell; } namespace MWGui { class ReviewDialog : public WindowModal { public: enum Dialogs { NAME_DIALOG, RACE_DIALOG, CLASS_DIALOG, BIRTHSIGN_DIALOG }; typedef std::vector SkillList; ReviewDialog(); bool exit() override { return false; } void setPlayerName(const std::string &name); void setRace(const std::string &raceId); void setClass(const ESM::Class& class_); void setBirthSign (const std::string &signId); void setHealth(const MWMechanics::DynamicStat& value); void setMagicka(const MWMechanics::DynamicStat& value); void setFatigue(const MWMechanics::DynamicStat& value); void setAttribute(ESM::Attribute::AttributeID attributeId, const MWMechanics::AttributeValue& value); void configureSkills(const SkillList& major, const SkillList& minor); void setSkillValue(ESM::Skill::SkillEnum skillId, const MWMechanics::SkillValue& value); void onOpen() override; void onFrame(float duration) override; // Events typedef MyGUI::delegates::CMultiDelegate0 EventHandle_Void; typedef MyGUI::delegates::CMultiDelegate1 EventHandle_Int; /** Event : Back button clicked.\n signature : void method()\n */ EventHandle_Void eventBack; /** Event : Dialog finished, OK button clicked.\n signature : void method()\n */ EventHandle_WindowBase eventDone; EventHandle_Int eventActivateDialog; protected: void onOkClicked(MyGUI::Widget* _sender); void onBackClicked(MyGUI::Widget* _sender); void onNameClicked(MyGUI::Widget* _sender); void onRaceClicked(MyGUI::Widget* _sender); void onClassClicked(MyGUI::Widget* _sender); void onBirthSignClicked(MyGUI::Widget* _sender); void onMouseWheel(MyGUI::Widget* _sender, int _rel); private: void addSkills(const SkillList &skills, const std::string &titleId, const std::string &titleDefault, MyGUI::IntCoord &coord1, MyGUI::IntCoord &coord2); void addSeparator(MyGUI::IntCoord &coord1, MyGUI::IntCoord &coord2); void addGroup(const std::string &label, MyGUI::IntCoord &coord1, MyGUI::IntCoord &coord2); MyGUI::TextBox* addValueItem(const std::string& text, const std::string &value, const std::string& state, MyGUI::IntCoord &coord1, MyGUI::IntCoord &coord2); void addItem(const std::string& text, MyGUI::IntCoord &coord1, MyGUI::IntCoord &coord2); void addItem(const ESM::Spell* spell, MyGUI::IntCoord &coord1, MyGUI::IntCoord &coord2); void updateSkillArea(); MyGUI::TextBox *mNameWidget, *mRaceWidget, *mClassWidget, *mBirthSignWidget; MyGUI::ScrollView* mSkillView; Widgets::MWDynamicStatPtr mHealth, mMagicka, mFatigue; std::map mAttributeWidgets; SkillList mMajorSkills, mMinorSkills, mMiscSkills; std::map mSkillValues; std::map mSkillWidgetMap; std::string mName, mRaceId, mBirthSignId; ESM::Class mKlass; std::vector mSkillWidgets; //< Skills and other information bool mUpdateSkillArea; }; } #endif ================================================ FILE: apps/openmw/mwgui/savegamedialog.cpp ================================================ #include "savegamedialog.hpp" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "../mwbase/statemanager.hpp" #include "../mwbase/environment.hpp" #include "../mwbase/world.hpp" #include "../mwbase/windowmanager.hpp" #include "../mwworld/esmstore.hpp" #include "../mwstate/character.hpp" #include "confirmationdialog.hpp" namespace MWGui { SaveGameDialog::SaveGameDialog() : WindowModal("openmw_savegame_dialog.layout") , mSaving(true) , mCurrentCharacter(nullptr) , mCurrentSlot(nullptr) { getWidget(mScreenshot, "Screenshot"); getWidget(mCharacterSelection, "SelectCharacter"); getWidget(mInfoText, "InfoText"); getWidget(mOkButton, "OkButton"); getWidget(mCancelButton, "CancelButton"); getWidget(mDeleteButton, "DeleteButton"); getWidget(mSaveList, "SaveList"); getWidget(mSaveNameEdit, "SaveNameEdit"); getWidget(mSpacer, "Spacer"); mOkButton->eventMouseButtonClick += MyGUI::newDelegate(this, &SaveGameDialog::onOkButtonClicked); mCancelButton->eventMouseButtonClick += MyGUI::newDelegate(this, &SaveGameDialog::onCancelButtonClicked); mDeleteButton->eventMouseButtonClick += MyGUI::newDelegate(this, &SaveGameDialog::onDeleteButtonClicked); mCharacterSelection->eventComboChangePosition += MyGUI::newDelegate(this, &SaveGameDialog::onCharacterSelected); mCharacterSelection->eventComboAccept += MyGUI::newDelegate(this, &SaveGameDialog::onCharacterAccept); mSaveList->eventListChangePosition += MyGUI::newDelegate(this, &SaveGameDialog::onSlotSelected); mSaveList->eventListMouseItemActivate += MyGUI::newDelegate(this, &SaveGameDialog::onSlotMouseClick); mSaveList->eventListSelectAccept += MyGUI::newDelegate(this, &SaveGameDialog::onSlotActivated); mSaveList->eventKeyButtonPressed += MyGUI::newDelegate(this, &SaveGameDialog::onKeyButtonPressed); mSaveNameEdit->eventEditSelectAccept += MyGUI::newDelegate(this, &SaveGameDialog::onEditSelectAccept); mSaveNameEdit->eventEditTextChange += MyGUI::newDelegate(this, &SaveGameDialog::onSaveNameChanged); // To avoid accidental deletions mDeleteButton->setNeedKeyFocus(false); } void SaveGameDialog::onSlotActivated(MyGUI::ListBox *sender, size_t pos) { onSlotSelected(sender, pos); accept(); } void SaveGameDialog::onSlotMouseClick(MyGUI::ListBox* sender, size_t pos) { onSlotSelected(sender, pos); if (pos != MyGUI::ITEM_NONE && MyGUI::InputManager::getInstance().isShiftPressed()) confirmDeleteSave(); } void SaveGameDialog::confirmDeleteSave() { ConfirmationDialog* dialog = MWBase::Environment::get().getWindowManager()->getConfirmationDialog(); dialog->askForConfirmation("#{sMessage3}"); dialog->eventOkClicked.clear(); dialog->eventOkClicked += MyGUI::newDelegate(this, &SaveGameDialog::onDeleteSlotConfirmed); dialog->eventCancelClicked.clear(); dialog->eventCancelClicked += MyGUI::newDelegate(this, &SaveGameDialog::onDeleteSlotCancel); } void SaveGameDialog::onDeleteSlotConfirmed() { MWBase::Environment::get().getStateManager()->deleteGame (mCurrentCharacter, mCurrentSlot); mSaveList->removeItemAt(mSaveList->getIndexSelected()); onSlotSelected(mSaveList, mSaveList->getIndexSelected()); MWBase::Environment::get().getWindowManager()->setKeyFocusWidget(mSaveList); if (mSaveList->getItemCount() == 0) { size_t previousIndex = mCharacterSelection->getIndexSelected(); mCurrentCharacter = nullptr; mCharacterSelection->removeItemAt(previousIndex); if (mCharacterSelection->getItemCount()) { size_t nextCharacter = std::min(previousIndex, mCharacterSelection->getItemCount()-1); mCharacterSelection->setIndexSelected(nextCharacter); onCharacterSelected(mCharacterSelection, nextCharacter); } else fillSaveList(); } } void SaveGameDialog::onDeleteSlotCancel() { MWBase::Environment::get().getWindowManager()->setKeyFocusWidget(mSaveList); } void SaveGameDialog::onSaveNameChanged(MyGUI::EditBox *sender) { // This might have previously been a save slot from the list. If so, that is no longer the case mSaveList->setIndexSelected(MyGUI::ITEM_NONE); onSlotSelected(mSaveList, MyGUI::ITEM_NONE); } void SaveGameDialog::onEditSelectAccept(MyGUI::EditBox *sender) { accept(); // To do not spam onEditSelectAccept() again and again MWBase::Environment::get().getWindowManager()->injectKeyRelease(MyGUI::KeyCode::None); } void SaveGameDialog::onClose() { mSaveList->setIndexSelected(MyGUI::ITEM_NONE); WindowModal::onClose(); } void SaveGameDialog::onOpen() { WindowModal::onOpen(); mSaveNameEdit->setCaption (""); if (mSaving) MWBase::Environment::get().getWindowManager()->setKeyFocusWidget(mSaveNameEdit); else MWBase::Environment::get().getWindowManager()->setKeyFocusWidget(mSaveList); center(); mCharacterSelection->setCaption(""); mCharacterSelection->removeAllItems(); mCurrentCharacter = nullptr; mCurrentSlot = nullptr; mSaveList->removeAllItems(); onSlotSelected(mSaveList, MyGUI::ITEM_NONE); MWBase::StateManager* mgr = MWBase::Environment::get().getStateManager(); if (mgr->characterBegin() == mgr->characterEnd()) return; mCurrentCharacter = mgr->getCurrentCharacter(); std::string directory = Misc::StringUtils::lowerCase (Settings::Manager::getString ("character", "Saves")); size_t selectedIndex = MyGUI::ITEM_NONE; for (MWBase::StateManager::CharacterIterator it = mgr->characterBegin(); it != mgr->characterEnd(); ++it) { if (it->begin()!=it->end()) { std::stringstream title; title << it->getSignature().mPlayerName; // For a custom class, we will not find it in the store (unless we loaded the savegame first). // Fall back to name stored in savegame header in that case. std::string className; if (it->getSignature().mPlayerClassId.empty()) className = it->getSignature().mPlayerClassName; else { // Find the localised name for this class from the store const ESM::Class* class_ = MWBase::Environment::get().getWorld()->getStore().get().search( it->getSignature().mPlayerClassId); if (class_) className = class_->mName; else className = "?"; // From an older savegame format that did not support custom classes properly. } title << " (#{sLevel} " << it->getSignature().mPlayerLevel << " " << MyGUI::TextIterator::toTagsString(className) << ")"; mCharacterSelection->addItem (MyGUI::LanguageManager::getInstance().replaceTags(title.str())); if (mCurrentCharacter == &*it || (!mCurrentCharacter && !mSaving && directory==Misc::StringUtils::lowerCase ( it->begin()->mPath.parent_path().filename().string()))) { mCurrentCharacter = &*it; selectedIndex = mCharacterSelection->getItemCount()-1; } } } mCharacterSelection->setIndexSelected(selectedIndex); if (selectedIndex == MyGUI::ITEM_NONE) mCharacterSelection->setCaption("Select Character ..."); fillSaveList(); } void SaveGameDialog::setLoadOrSave(bool load) { mSaving = !load; mSaveNameEdit->setVisible(!load); mCharacterSelection->setUserString("Hidden", load ? "false" : "true"); mCharacterSelection->setVisible(load); mSpacer->setUserString("Hidden", load ? "false" : "true"); mDeleteButton->setUserString("Hidden", load ? "false" : "true"); mDeleteButton->setVisible(load); if (!load) { mCurrentCharacter = MWBase::Environment::get().getStateManager()->getCurrentCharacter(); } center(); } void SaveGameDialog::onCancelButtonClicked(MyGUI::Widget *sender) { setVisible(false); } void SaveGameDialog::onDeleteButtonClicked(MyGUI::Widget *sender) { if (mCurrentSlot) confirmDeleteSave(); } void SaveGameDialog::onConfirmationGiven() { accept(true); } void SaveGameDialog::onConfirmationCancel() { MWBase::Environment::get().getWindowManager()->setKeyFocusWidget(mSaveList); } void SaveGameDialog::accept(bool reallySure) { if (mSaving) { // If overwriting an existing slot, ask for confirmation first if (mCurrentSlot != nullptr && !reallySure) { ConfirmationDialog* dialog = MWBase::Environment::get().getWindowManager()->getConfirmationDialog(); dialog->askForConfirmation("#{sMessage4}"); dialog->eventOkClicked.clear(); dialog->eventOkClicked += MyGUI::newDelegate(this, &SaveGameDialog::onConfirmationGiven); dialog->eventCancelClicked.clear(); dialog->eventCancelClicked += MyGUI::newDelegate(this, &SaveGameDialog::onConfirmationCancel); return; } if (mSaveNameEdit->getCaption().empty()) { MWBase::Environment::get().getWindowManager()->messageBox("#{sNotifyMessage65}"); return; } } else { MWBase::StateManager::State state = MWBase::Environment::get().getStateManager()->getState(); // If game is running, ask for confirmation first if (state == MWBase::StateManager::State_Running && !reallySure) { ConfirmationDialog* dialog = MWBase::Environment::get().getWindowManager()->getConfirmationDialog(); dialog->askForConfirmation("#{sMessage1}"); dialog->eventOkClicked.clear(); dialog->eventOkClicked += MyGUI::newDelegate(this, &SaveGameDialog::onConfirmationGiven); dialog->eventCancelClicked.clear(); dialog->eventCancelClicked += MyGUI::newDelegate(this, &SaveGameDialog::onConfirmationCancel); return; } } setVisible(false); MWBase::Environment::get().getWindowManager()->removeGuiMode (MWGui::GM_MainMenu); if (mSaving) { MWBase::Environment::get().getStateManager()->saveGame (mSaveNameEdit->getCaption(), mCurrentSlot); } else { assert (mCurrentCharacter && mCurrentSlot); MWBase::Environment::get().getStateManager()->loadGame (mCurrentCharacter, mCurrentSlot->mPath.string()); } } void SaveGameDialog::onKeyButtonPressed(MyGUI::Widget* _sender, MyGUI::KeyCode key, MyGUI::Char character) { if (key == MyGUI::KeyCode::Delete && mCurrentSlot) confirmDeleteSave(); } void SaveGameDialog::onOkButtonClicked(MyGUI::Widget *sender) { accept(); } void SaveGameDialog::onCharacterSelected(MyGUI::ComboBox *sender, size_t pos) { MWBase::StateManager* mgr = MWBase::Environment::get().getStateManager(); unsigned int i=0; const MWState::Character* character = nullptr; for (MWBase::StateManager::CharacterIterator it = mgr->characterBegin(); it != mgr->characterEnd(); ++it, ++i) { if (i == pos) character = &*it; } assert(character && "Can't find selected character"); mCurrentCharacter = character; mCurrentSlot = nullptr; fillSaveList(); } void SaveGameDialog::onCharacterAccept(MyGUI::ComboBox* sender, size_t pos) { // Give key focus to save list so we can confirm the selection with Enter MWBase::Environment::get().getWindowManager()->setKeyFocusWidget(mSaveList); } void SaveGameDialog::fillSaveList() { mSaveList->removeAllItems(); if (!mCurrentCharacter) return; for (MWState::Character::SlotIterator it = mCurrentCharacter->begin(); it != mCurrentCharacter->end(); ++it) { mSaveList->addItem(it->mProfile.mDescription); } // When loading, Auto-select the first save, if there is one if (mSaveList->getItemCount() && !mSaving) { mSaveList->setIndexSelected(0); onSlotSelected(mSaveList, 0); } else onSlotSelected(mSaveList, MyGUI::ITEM_NONE); } std::string formatTimeplayed(const double timeInSeconds) { int timePlayed = (int)floor(timeInSeconds); int days = timePlayed / 60 / 60 / 24; int hours = (timePlayed / 60 / 60) % 24; int minutes = (timePlayed / 60) % 60; int seconds = timePlayed % 60; std::stringstream stream; stream << std::setfill('0') << std::setw(2) << days << ":"; stream << std::setfill('0') << std::setw(2) << hours << ":"; stream << std::setfill('0') << std::setw(2) << minutes << ":"; stream << std::setfill('0') << std::setw(2) << seconds; return stream.str(); } void SaveGameDialog::onSlotSelected(MyGUI::ListBox *sender, size_t pos) { mOkButton->setEnabled(pos != MyGUI::ITEM_NONE || mSaving); mDeleteButton->setEnabled(pos != MyGUI::ITEM_NONE); if (pos == MyGUI::ITEM_NONE || !mCurrentCharacter) { mCurrentSlot = nullptr; mInfoText->setCaption(""); mScreenshot->setImageTexture(""); return; } if (mSaving) mSaveNameEdit->setCaption(sender->getItemNameAt(pos)); mCurrentSlot = nullptr; unsigned int i=0; for (MWState::Character::SlotIterator it = mCurrentCharacter->begin(); it != mCurrentCharacter->end(); ++it, ++i) { if (i == pos) mCurrentSlot = &*it; } if (!mCurrentSlot) throw std::runtime_error("Can't find selected slot"); std::stringstream text; time_t time = mCurrentSlot->mTimeStamp; struct tm* timeinfo; timeinfo = localtime(&time); text << std::put_time(timeinfo, "%Y.%m.%d %T") << "\n"; text << "#{sLevel} " << mCurrentSlot->mProfile.mPlayerLevel << "\n"; text << "#{sCell=" << mCurrentSlot->mProfile.mPlayerCell << "}\n"; int hour = int(mCurrentSlot->mProfile.mInGameTime.mGameHour); bool pm = hour >= 12; if (hour >= 13) hour -= 12; if (hour == 0) hour = 12; text << mCurrentSlot->mProfile.mInGameTime.mDay << " " << MWBase::Environment::get().getWorld()->getMonthName(mCurrentSlot->mProfile.mInGameTime.mMonth) << " " << hour << " " << (pm ? "#{sSaveMenuHelp05}" : "#{sSaveMenuHelp04}"); if (Settings::Manager::getBool("timeplayed","Saves")) { text << "\n" << "Time played: " << formatTimeplayed(mCurrentSlot->mProfile.mTimePlayed); } mInfoText->setCaptionWithReplacing(text.str()); // Decode screenshot const std::vector& data = mCurrentSlot->mProfile.mScreenshot; Files::IMemStream instream (&data[0], data.size()); osgDB::ReaderWriter* readerwriter = osgDB::Registry::instance()->getReaderWriterForExtension("jpg"); if (!readerwriter) { Log(Debug::Error) << "Error: Can't open savegame screenshot, no jpg readerwriter found"; return; } osgDB::ReaderWriter::ReadResult result = readerwriter->readImage(instream); if (!result.success()) { Log(Debug::Error) << "Error: Failed to read savegame screenshot: " << result.message() << " code " << result.status(); return; } osg::ref_ptr texture (new osg::Texture2D); texture->setImage(result.getImage()); texture->setWrap(osg::Texture::WRAP_S, osg::Texture::CLAMP_TO_EDGE); texture->setWrap(osg::Texture::WRAP_T, osg::Texture::CLAMP_TO_EDGE); texture->setFilter(osg::Texture::MIN_FILTER, osg::Texture::LINEAR); texture->setFilter(osg::Texture::MAG_FILTER, osg::Texture::LINEAR); texture->setResizeNonPowerOfTwoHint(false); texture->setUnRefImageDataAfterApply(true); mScreenshotTexture.reset(new osgMyGUI::OSGTexture(texture)); mScreenshot->setRenderItemTexture(mScreenshotTexture.get()); mScreenshot->getSubWidgetMain()->_setUVSet(MyGUI::FloatRect(0.f, 0.f, 1.f, 1.f)); } } ================================================ FILE: apps/openmw/mwgui/savegamedialog.hpp ================================================ #ifndef OPENMW_MWGUI_SAVEGAMEDIALOG_H #define OPENMW_MWGUI_SAVEGAMEDIALOG_H #include #include "windowbase.hpp" namespace MWState { class Character; struct Slot; } namespace MWGui { class SaveGameDialog : public MWGui::WindowModal { public: SaveGameDialog(); void onOpen() override; void onClose() override; void setLoadOrSave(bool load); private: void confirmDeleteSave(); void onKeyButtonPressed(MyGUI::Widget* _sender, MyGUI::KeyCode key, MyGUI::Char character); void onCancelButtonClicked (MyGUI::Widget* sender); void onOkButtonClicked (MyGUI::Widget* sender); void onDeleteButtonClicked (MyGUI::Widget* sender); void onCharacterSelected (MyGUI::ComboBox* sender, size_t pos); void onCharacterAccept(MyGUI::ComboBox* sender, size_t pos); // Slot selected (mouse click or arrow keys) void onSlotSelected (MyGUI::ListBox* sender, size_t pos); // Slot activated (double click or enter key) void onSlotActivated (MyGUI::ListBox* sender, size_t pos); // Slot clicked with mouse void onSlotMouseClick(MyGUI::ListBox* sender, size_t pos); void onDeleteSlotConfirmed(); void onDeleteSlotCancel(); void onEditSelectAccept (MyGUI::EditBox* sender); void onSaveNameChanged (MyGUI::EditBox* sender); void onConfirmationGiven(); void onConfirmationCancel(); void accept(bool reallySure=false); void fillSaveList(); std::unique_ptr mScreenshotTexture; MyGUI::ImageBox* mScreenshot; bool mSaving; MyGUI::ComboBox* mCharacterSelection; MyGUI::EditBox* mInfoText; MyGUI::Button* mOkButton; MyGUI::Button* mCancelButton; MyGUI::Button* mDeleteButton; MyGUI::ListBox* mSaveList; MyGUI::EditBox* mSaveNameEdit; MyGUI::Widget* mSpacer; const MWState::Character* mCurrentCharacter; const MWState::Slot* mCurrentSlot; }; } #endif ================================================ FILE: apps/openmw/mwgui/screenfader.cpp ================================================ #include "screenfader.hpp" #include #include #include namespace MWGui { FadeOp::FadeOp(ScreenFader * fader, float time, float targetAlpha, float delay) : mFader(fader), mRemainingTime(time+delay), mTargetTime(time), mTargetAlpha(targetAlpha), mStartAlpha(0.f), mDelay(delay), mRunning(false) { } bool FadeOp::isRunning() { return mRunning; } void FadeOp::start() { if (mRunning) return; mRemainingTime = mTargetTime + mDelay; mStartAlpha = mFader->getCurrentAlpha(); mRunning = true; } void FadeOp::update(float dt) { if (!mRunning) return; if (mStartAlpha == mTargetAlpha) { finish(); return; } if (mRemainingTime <= 0) { // Make sure the target alpha is applied mFader->notifyAlphaChanged(mTargetAlpha); finish(); return; } if (mRemainingTime > mTargetTime) { mRemainingTime -= dt; return; } float currentAlpha = mFader->getCurrentAlpha(); if (mStartAlpha > mTargetAlpha) { currentAlpha -= dt/mTargetTime * (mStartAlpha-mTargetAlpha); if (currentAlpha < mTargetAlpha) currentAlpha = mTargetAlpha; } else { currentAlpha += dt/mTargetTime * (mTargetAlpha-mStartAlpha); if (currentAlpha > mTargetAlpha) currentAlpha = mTargetAlpha; } mFader->notifyAlphaChanged(currentAlpha); mRemainingTime -= dt; } void FadeOp::finish() { mRunning = false; mFader->notifyOperationFinished(); } ScreenFader::ScreenFader(const std::string & texturePath, const std::string& layout, const MyGUI::FloatCoord& texCoordOverride) : WindowBase(layout) , mCurrentAlpha(0.f) , mFactor(1.f) , mRepeat(false) { MyGUI::Gui::getInstance().eventFrameStart += MyGUI::newDelegate(this, &ScreenFader::onFrameStart); mMainWidget->setSize(MyGUI::RenderManager::getInstance().getViewSize()); MyGUI::ImageBox* imageBox = mMainWidget->castType(false); if (imageBox) { imageBox->setImageTexture(texturePath); const MyGUI::IntSize imageSize = imageBox->getImageSize(); imageBox->setImageCoord(MyGUI::IntCoord(texCoordOverride.left * imageSize.width, texCoordOverride.top * imageSize.height, texCoordOverride.width * imageSize.width, texCoordOverride.height * imageSize.height)); } } ScreenFader::~ScreenFader() { try { MyGUI::Gui::getInstance().eventFrameStart -= MyGUI::newDelegate(this, &ScreenFader::onFrameStart); } catch(const MyGUI::Exception& e) { Log(Debug::Error) << "Error in the destructor: " << e.what(); } } void ScreenFader::onFrameStart(float dt) { if (!mQueue.empty()) { if (!mQueue.front()->isRunning()) mQueue.front()->start(); mQueue.front()->update(dt); } } void ScreenFader::applyAlpha() { setVisible(true); mMainWidget->setAlpha(1.f-((1.f-mCurrentAlpha) * mFactor)); } void ScreenFader::fadeIn(float time, float delay) { queue(time, 1.f, delay); } void ScreenFader::fadeOut(const float time, float delay) { queue(time, 0.f, delay); } void ScreenFader::fadeTo(const int percent, const float time, float delay) { queue(time, percent/100.f, delay); } void ScreenFader::clear() { clearQueue(); notifyAlphaChanged(0.f); } void ScreenFader::setFactor(float factor) { mFactor = factor; applyAlpha(); } void ScreenFader::setRepeat(bool repeat) { mRepeat = repeat; } void ScreenFader::queue(float time, float targetAlpha, float delay) { if (time < 0.f) return; if (time == 0.f && delay == 0.f) { mCurrentAlpha = targetAlpha; applyAlpha(); return; } mQueue.push_back(FadeOp::Ptr(new FadeOp(this, time, targetAlpha, delay))); } bool ScreenFader::isEmpty() { return mQueue.empty(); } void ScreenFader::clearQueue() { mQueue.clear(); } void ScreenFader::notifyAlphaChanged(float alpha) { if (mCurrentAlpha == alpha) return; mCurrentAlpha = alpha; if (1.f-((1.f-mCurrentAlpha) * mFactor) == 0.f) mMainWidget->setVisible(false); else applyAlpha(); } void ScreenFader::notifyOperationFinished() { FadeOp::Ptr op = mQueue.front(); mQueue.pop_front(); if (mRepeat) mQueue.push_back(op); } float ScreenFader::getCurrentAlpha() { return mCurrentAlpha; } } ================================================ FILE: apps/openmw/mwgui/screenfader.hpp ================================================ #ifndef OPENMW_MWGUI_SCREENFADER_H #define OPENMW_MWGUI_SCREENFADER_H #include #include #include "windowbase.hpp" namespace MWGui { class ScreenFader; class FadeOp { public: typedef std::shared_ptr Ptr; FadeOp(ScreenFader * fader, float time, float targetAlpha, float delay); bool isRunning(); void start(); void update(float dt); void finish(); private: ScreenFader * mFader; float mRemainingTime; float mTargetTime; float mTargetAlpha; float mStartAlpha; float mDelay; bool mRunning; }; class ScreenFader : public WindowBase { public: ScreenFader(const std::string & texturePath, const std::string& layout = "openmw_screen_fader.layout", const MyGUI::FloatCoord& texCoordOverride = MyGUI::FloatCoord(0,0,1,1)); ~ScreenFader(); void onFrameStart(float dt); void fadeIn(const float time, float delay=0); void fadeOut(const float time, float delay=0); void fadeTo(const int percent, const float time, float delay=0); void clear() override; void setFactor (float factor); void setRepeat(bool repeat); void queue(float time, float targetAlpha, float delay); bool isEmpty(); void clearQueue(); void notifyAlphaChanged(float alpha); void notifyOperationFinished(); float getCurrentAlpha(); private: void applyAlpha(); float mCurrentAlpha; float mFactor; bool mRepeat; // repeat queued operations without removing them std::deque mQueue; }; } #endif ================================================ FILE: apps/openmw/mwgui/scrollwindow.cpp ================================================ #include "scrollwindow.hpp" #include #include #include #include "../mwbase/environment.hpp" #include "../mwbase/world.hpp" #include "../mwbase/windowmanager.hpp" #include "../mwmechanics/actorutil.hpp" #include "../mwworld/actiontake.hpp" #include "../mwworld/class.hpp" #include "formatting.hpp" namespace MWGui { ScrollWindow::ScrollWindow () : BookWindowBase("openmw_scroll.layout") , mTakeButtonShow(true) , mTakeButtonAllowed(true) { getWidget(mTextView, "TextView"); getWidget(mCloseButton, "CloseButton"); mCloseButton->eventMouseButtonClick += MyGUI::newDelegate(this, &ScrollWindow::onCloseButtonClicked); getWidget(mTakeButton, "TakeButton"); mTakeButton->eventMouseButtonClick += MyGUI::newDelegate(this, &ScrollWindow::onTakeButtonClicked); adjustButton("CloseButton"); adjustButton("TakeButton"); mCloseButton->eventKeyButtonPressed += MyGUI::newDelegate(this, &ScrollWindow::onKeyButtonPressed); mTakeButton->eventKeyButtonPressed += MyGUI::newDelegate(this, &ScrollWindow::onKeyButtonPressed); center(); } void ScrollWindow::setPtr (const MWWorld::Ptr& scroll) { mScroll = scroll; MWWorld::Ptr player = MWMechanics::getPlayer(); bool showTakeButton = scroll.getContainerStore() != &player.getClass().getContainerStore(player); MWWorld::LiveCellRef *ref = mScroll.get(); Formatting::BookFormatter formatter; formatter.markupToWidget(mTextView, ref->mBase->mText, 390, mTextView->getHeight()); MyGUI::IntSize size = mTextView->getChildAt(0)->getSize(); // Canvas size must be expressed with VScroll disabled, otherwise MyGUI would expand the scroll area when the scrollbar is hidden mTextView->setVisibleVScroll(false); if (size.height > mTextView->getSize().height) mTextView->setCanvasSize(mTextView->getWidth(), size.height); else mTextView->setCanvasSize(mTextView->getWidth(), mTextView->getSize().height); mTextView->setVisibleVScroll(true); mTextView->setViewOffset(MyGUI::IntPoint(0,0)); setTakeButtonShow(showTakeButton); MWBase::Environment::get().getWindowManager()->setKeyFocusWidget(mCloseButton); } void ScrollWindow::onKeyButtonPressed(MyGUI::Widget *sender, MyGUI::KeyCode key, MyGUI::Char character) { int scroll = 0; if (key == MyGUI::KeyCode::ArrowUp) scroll = 40; else if (key == MyGUI::KeyCode::ArrowDown) scroll = -40; if (scroll != 0) mTextView->setViewOffset(mTextView->getViewOffset() + MyGUI::IntPoint(0, scroll)); } void ScrollWindow::setTakeButtonShow(bool show) { mTakeButtonShow = show; mTakeButton->setVisible(mTakeButtonShow && mTakeButtonAllowed); } void ScrollWindow::setInventoryAllowed(bool allowed) { mTakeButtonAllowed = allowed; mTakeButton->setVisible(mTakeButtonShow && mTakeButtonAllowed); } void ScrollWindow::onCloseButtonClicked (MyGUI::Widget* _sender) { MWBase::Environment::get().getWindowManager()->removeGuiMode(GM_Scroll); } void ScrollWindow::onTakeButtonClicked (MyGUI::Widget* _sender) { MWBase::Environment::get().getWindowManager()->playSound("Item Book Up"); MWWorld::ActionTake take(mScroll); take.execute (MWMechanics::getPlayer()); MWBase::Environment::get().getWindowManager()->removeGuiMode(GM_Scroll, true); } } ================================================ FILE: apps/openmw/mwgui/scrollwindow.hpp ================================================ #ifndef MWGUI_SCROLLWINDOW_H #define MWGUI_SCROLLWINDOW_H #include "windowbase.hpp" #include "../mwworld/ptr.hpp" namespace Gui { class ImageButton; } namespace MWGui { class ScrollWindow : public BookWindowBase { public: ScrollWindow (); void setPtr (const MWWorld::Ptr& scroll) override; void setInventoryAllowed(bool allowed); void onResChange(int, int) override { center(); } protected: void onCloseButtonClicked (MyGUI::Widget* _sender); void onTakeButtonClicked (MyGUI::Widget* _sender); void setTakeButtonShow(bool show); void onKeyButtonPressed(MyGUI::Widget* sender, MyGUI::KeyCode key, MyGUI::Char character); private: Gui::ImageButton* mCloseButton; Gui::ImageButton* mTakeButton; MyGUI::ScrollView* mTextView; MWWorld::Ptr mScroll; bool mTakeButtonShow; bool mTakeButtonAllowed; }; } #endif ================================================ FILE: apps/openmw/mwgui/settingswindow.cpp ================================================ #include "settingswindow.hpp" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "../mwbase/environment.hpp" #include "../mwbase/world.hpp" #include "../mwbase/soundmanager.hpp" #include "../mwbase/inputmanager.hpp" #include "../mwbase/mechanicsmanager.hpp" #include "../mwbase/windowmanager.hpp" #include "confirmationdialog.hpp" namespace { std::string textureMipmappingToStr(const std::string& val) { if (val == "linear") return "Trilinear"; if (val == "nearest") return "Bilinear"; if (val != "none") Log(Debug::Warning) << "Warning: Invalid texture mipmap option: "<< val; return "Other"; } void parseResolution (int &x, int &y, const std::string& str) { std::vector split; Misc::StringUtils::split (str, split, "@(x"); assert (split.size() >= 2); Misc::StringUtils::trim(split[0]); Misc::StringUtils::trim(split[1]); x = MyGUI::utility::parseInt (split[0]); y = MyGUI::utility::parseInt (split[1]); } bool sortResolutions (std::pair left, std::pair right) { if (left.first == right.first) return left.second > right.second; return left.first > right.first; } std::string getAspect (int x, int y) { int gcd = std::gcd (x, y); if (gcd == 0) return std::string(); int xaspect = x / gcd; int yaspect = y / gcd; // special case: 8 : 5 is usually referred to as 16:10 if (xaspect == 8 && yaspect == 5) return "16 : 10"; return MyGUI::utility::toString(xaspect) + " : " + MyGUI::utility::toString(yaspect); } const char* checkButtonType = "CheckButton"; const char* sliderType = "Slider"; std::string getSettingType(MyGUI::Widget* widget) { return widget->getUserString("SettingType"); } std::string getSettingName(MyGUI::Widget* widget) { return widget->getUserString("SettingName"); } std::string getSettingCategory(MyGUI::Widget* widget) { return widget->getUserString("SettingCategory"); } std::string getSettingValueType(MyGUI::Widget* widget) { return widget->getUserString("SettingValueType"); } void getSettingMinMax(MyGUI::Widget* widget, float& min, float& max) { const char* settingMin = "SettingMin"; const char* settingMax = "SettingMax"; min = 0.f; max = 1.f; if (!widget->getUserString(settingMin).empty()) min = MyGUI::utility::parseFloat(widget->getUserString(settingMin)); if (!widget->getUserString(settingMax).empty()) max = MyGUI::utility::parseFloat(widget->getUserString(settingMax)); } void updateMaxLightsComboBox(MyGUI::ComboBox* box) { constexpr int min = 8; constexpr int max = 32; constexpr int increment = 8; int maxLights = Settings::Manager::getInt("max lights", "Shaders"); // show increments of 8 in dropdown if (maxLights >= min && maxLights <= max && !(maxLights % increment)) box->setIndexSelected((maxLights / increment)-1); else box->setIndexSelected(MyGUI::ITEM_NONE); } } namespace MWGui { void SettingsWindow::configureWidgets(MyGUI::Widget* widget, bool init) { MyGUI::EnumeratorWidgetPtr widgets = widget->getEnumerator(); while (widgets.next()) { MyGUI::Widget* current = widgets.current(); std::string type = getSettingType(current); if (type == checkButtonType) { std::string initialValue = Settings::Manager::getBool(getSettingName(current), getSettingCategory(current)) ? "#{sOn}" : "#{sOff}"; current->castType()->setCaptionWithReplacing(initialValue); if (init) current->eventMouseButtonClick += MyGUI::newDelegate(this, &SettingsWindow::onButtonToggled); } if (type == sliderType) { /* Start of tes3mp addition Hide difficulty widget because it has no use in multiplayer, with the difficulty being set by the server instead */ if (getSettingName(current) == "difficulty") { widget->setEnabled(false); widget->setVisible(false); } /* End of tes3mp addition */ MyGUI::ScrollBar* scroll = current->castType(); std::string valueStr; std::string valueType = getSettingValueType(current); if (valueType == "Float" || valueType == "Integer" || valueType == "Cell") { // TODO: ScrollBar isn't meant for this. should probably use a dedicated FloatSlider widget float min,max; getSettingMinMax(scroll, min, max); float value = Settings::Manager::getFloat(getSettingName(current), getSettingCategory(current)); if (valueType == "Cell") { std::stringstream ss; ss << std::fixed << std::setprecision(2) << value/Constants::CellSizeInUnits; valueStr = ss.str(); } else if (valueType == "Float") { std::stringstream ss; ss << std::fixed << std::setprecision(2) << value; valueStr = ss.str(); } else valueStr = MyGUI::utility::toString(int(value)); value = std::max(min, std::min(value, max)); value = (value-min)/(max-min); scroll->setScrollPosition(static_cast(value * (scroll->getScrollRange() - 1))); } else { int value = Settings::Manager::getInt(getSettingName(current), getSettingCategory(current)); valueStr = MyGUI::utility::toString(value); scroll->setScrollPosition(value); } if (init) scroll->eventScrollChangePosition += MyGUI::newDelegate(this, &SettingsWindow::onSliderChangePosition); if (scroll->getVisible()) updateSliderLabel(scroll, valueStr); } configureWidgets(current, init); } } void SettingsWindow::updateSliderLabel(MyGUI::ScrollBar *scroller, const std::string& value) { std::string labelWidgetName = scroller->getUserString("SettingLabelWidget"); if (!labelWidgetName.empty()) { MyGUI::TextBox* textBox; getWidget(textBox, labelWidgetName); std::string labelCaption = scroller->getUserString("SettingLabelCaption"); labelCaption = Misc::StringUtils::format(labelCaption, value); textBox->setCaptionWithReplacing(labelCaption); } } SettingsWindow::SettingsWindow() : WindowBase("openmw_settings_window.layout"), mKeyboardMode(true) { bool terrain = Settings::Manager::getBool("distant terrain", "Terrain"); const std::string widgetName = terrain ? "RenderingDistanceSlider" : "LargeRenderingDistanceSlider"; MyGUI::Widget* unusedSlider; getWidget(unusedSlider, widgetName); unusedSlider->setVisible(false); configureWidgets(mMainWidget, true); setTitle("#{sOptions}"); getWidget(mSettingsTab, "SettingsTab"); getWidget(mOkButton, "OkButton"); getWidget(mResolutionList, "ResolutionList"); getWidget(mFullscreenButton, "FullscreenButton"); getWidget(mWindowBorderButton, "WindowBorderButton"); getWidget(mTextureFilteringButton, "TextureFilteringButton"); getWidget(mAnisotropyBox, "AnisotropyBox"); getWidget(mControlsBox, "ControlsBox"); getWidget(mResetControlsButton, "ResetControlsButton"); getWidget(mKeyboardSwitch, "KeyboardButton"); getWidget(mControllerSwitch, "ControllerButton"); getWidget(mWaterTextureSize, "WaterTextureSize"); getWidget(mWaterReflectionDetail, "WaterReflectionDetail"); getWidget(mLightingMethodButton, "LightingMethodButton"); getWidget(mLightsResetButton, "LightsResetButton"); getWidget(mMaxLights, "MaxLights"); #ifndef WIN32 // hide gamma controls since it currently does not work under Linux MyGUI::ScrollBar *gammaSlider; getWidget(gammaSlider, "GammaSlider"); gammaSlider->setVisible(false); MyGUI::TextBox *textBox; getWidget(textBox, "GammaText"); textBox->setVisible(false); getWidget(textBox, "GammaTextDark"); textBox->setVisible(false); getWidget(textBox, "GammaTextLight"); textBox->setVisible(false); #endif mMainWidget->castType()->eventWindowChangeCoord += MyGUI::newDelegate(this, &SettingsWindow::onWindowResize); mSettingsTab->eventTabChangeSelect += MyGUI::newDelegate(this, &SettingsWindow::onTabChanged); mOkButton->eventMouseButtonClick += MyGUI::newDelegate(this, &SettingsWindow::onOkButtonClicked); mTextureFilteringButton->eventComboChangePosition += MyGUI::newDelegate(this, &SettingsWindow::onTextureFilteringChanged); mResolutionList->eventListChangePosition += MyGUI::newDelegate(this, &SettingsWindow::onResolutionSelected); mWaterTextureSize->eventComboChangePosition += MyGUI::newDelegate(this, &SettingsWindow::onWaterTextureSizeChanged); mWaterReflectionDetail->eventComboChangePosition += MyGUI::newDelegate(this, &SettingsWindow::onWaterReflectionDetailChanged); mLightingMethodButton->eventComboChangePosition += MyGUI::newDelegate(this, &SettingsWindow::onLightingMethodButtonChanged); mLightsResetButton->eventMouseButtonClick += MyGUI::newDelegate(this, &SettingsWindow::onLightsResetButtonClicked); mMaxLights->eventComboChangePosition += MyGUI::newDelegate(this, &SettingsWindow::onMaxLightsChanged); mKeyboardSwitch->eventMouseButtonClick += MyGUI::newDelegate(this, &SettingsWindow::onKeyboardSwitchClicked); mControllerSwitch->eventMouseButtonClick += MyGUI::newDelegate(this, &SettingsWindow::onControllerSwitchClicked); center(); mResetControlsButton->eventMouseButtonClick += MyGUI::newDelegate(this, &SettingsWindow::onResetDefaultBindings); // fill resolution list int screen = Settings::Manager::getInt("screen", "Video"); int numDisplayModes = SDL_GetNumDisplayModes(screen); std::vector < std::pair > resolutions; for (int i = 0; i < numDisplayModes; i++) { SDL_DisplayMode mode; SDL_GetDisplayMode(screen, i, &mode); resolutions.emplace_back(mode.w, mode.h); } std::sort(resolutions.begin(), resolutions.end(), sortResolutions); for (std::pair& resolution : resolutions) { std::string str = MyGUI::utility::toString(resolution.first) + " x " + MyGUI::utility::toString(resolution.second); std::string aspect = getAspect(resolution.first, resolution.second); if (!aspect.empty()) str = str + " (" + aspect + ")"; if (mResolutionList->findItemIndexWith(str) == MyGUI::ITEM_NONE) mResolutionList->addItem(str); } highlightCurrentResolution(); std::string tmip = Settings::Manager::getString("texture mipmap", "General"); mTextureFilteringButton->setCaption(textureMipmappingToStr(tmip)); int waterTextureSize = Settings::Manager::getInt("rtt size", "Water"); if (waterTextureSize >= 512) mWaterTextureSize->setIndexSelected(0); if (waterTextureSize >= 1024) mWaterTextureSize->setIndexSelected(1); if (waterTextureSize >= 2048) mWaterTextureSize->setIndexSelected(2); int waterReflectionDetail = Settings::Manager::getInt("reflection detail", "Water"); waterReflectionDetail = std::min(5, std::max(0, waterReflectionDetail)); mWaterReflectionDetail->setIndexSelected(waterReflectionDetail); updateMaxLightsComboBox(mMaxLights); mWindowBorderButton->setEnabled(!Settings::Manager::getBool("fullscreen", "Video")); mKeyboardSwitch->setStateSelected(true); mControllerSwitch->setStateSelected(false); } void SettingsWindow::onTabChanged(MyGUI::TabControl* /*_sender*/, size_t /*index*/) { resetScrollbars(); } void SettingsWindow::onOkButtonClicked(MyGUI::Widget* _sender) { MWBase::Environment::get().getWindowManager()->removeGuiMode(GM_Settings); } void SettingsWindow::onResolutionSelected(MyGUI::ListBox* _sender, size_t index) { if (index == MyGUI::ITEM_NONE) return; ConfirmationDialog* dialog = MWBase::Environment::get().getWindowManager()->getConfirmationDialog(); dialog->askForConfirmation("#{sNotifyMessage67}"); dialog->eventOkClicked.clear(); dialog->eventOkClicked += MyGUI::newDelegate(this, &SettingsWindow::onResolutionAccept); dialog->eventCancelClicked.clear(); dialog->eventCancelClicked += MyGUI::newDelegate(this, &SettingsWindow::onResolutionCancel); } void SettingsWindow::onResolutionAccept() { std::string resStr = mResolutionList->getItemNameAt(mResolutionList->getIndexSelected()); int resX, resY; parseResolution (resX, resY, resStr); Settings::Manager::setInt("resolution x", "Video", resX); Settings::Manager::setInt("resolution y", "Video", resY); apply(); } void SettingsWindow::onResolutionCancel() { highlightCurrentResolution(); } void SettingsWindow::highlightCurrentResolution() { mResolutionList->setIndexSelected(MyGUI::ITEM_NONE); int currentX = Settings::Manager::getInt("resolution x", "Video"); int currentY = Settings::Manager::getInt("resolution y", "Video"); for (size_t i=0; igetItemCount(); ++i) { int resX, resY; parseResolution (resX, resY, mResolutionList->getItemNameAt(i)); if (resX == currentX && resY == currentY) { mResolutionList->setIndexSelected(i); break; } } } void SettingsWindow::onWaterTextureSizeChanged(MyGUI::ComboBox* _sender, size_t pos) { int size = 0; if (pos == 0) size = 512; else if (pos == 1) size = 1024; else if (pos == 2) size = 2048; Settings::Manager::setInt("rtt size", "Water", size); apply(); } void SettingsWindow::onWaterReflectionDetailChanged(MyGUI::ComboBox* _sender, size_t pos) { unsigned int level = std::min((unsigned int)5, (unsigned int)pos); Settings::Manager::setInt("reflection detail", "Water", level); apply(); } void SettingsWindow::onLightingMethodButtonChanged(MyGUI::ComboBox* _sender, size_t pos) { if (pos == MyGUI::ITEM_NONE) return; std::string message = "This change requires a restart to take effect."; MWBase::Environment::get().getWindowManager()->interactiveMessageBox(message, {"#{sOK}"}, true); Settings::Manager::setString("lighting method", "Shaders", _sender->getItemNameAt(pos)); apply(); } void SettingsWindow::onMaxLightsChanged(MyGUI::ComboBox* _sender, size_t pos) { int count = 8 * (pos + 1); Settings::Manager::setInt("max lights", "Shaders", count); apply(); configureWidgets(mMainWidget, false); } void SettingsWindow::onLightsResetButtonClicked(MyGUI::Widget* _sender) { std::vector buttons = {"#{sYes}", "#{sNo}"}; std::string message = "Resets to default values, would you like to continue? Changes to lighting method will require a restart."; MWBase::Environment::get().getWindowManager()->interactiveMessageBox(message, buttons, true); int selectedButton = MWBase::Environment::get().getWindowManager()->readPressedButton(); if (selectedButton == 1 || selectedButton == -1) return; constexpr std::array settings = { "light bounds multiplier", "maximum light distance", "light fade start", "minimum interior brightness", "max lights", "lighting method", }; for (const auto& setting : settings) Settings::Manager::setString(setting, "Shaders", Settings::Manager::mDefaultSettings[{"Shaders", setting}]); mLightingMethodButton->setIndexSelected(mLightingMethodButton->findItemIndexWith(Settings::Manager::mDefaultSettings[{"Shaders", "lighting method"}])); updateMaxLightsComboBox(mMaxLights); apply(); configureWidgets(mMainWidget, false); } void SettingsWindow::onButtonToggled(MyGUI::Widget* _sender) { std::string on = MWBase::Environment::get().getWindowManager()->getGameSettingString("sOn", "On"); std::string off = MWBase::Environment::get().getWindowManager()->getGameSettingString("sOff", "On"); bool newState; if (_sender->castType()->getCaption() == on) { _sender->castType()->setCaption(off); newState = false; } else { _sender->castType()->setCaption(on); newState = true; } if (_sender == mFullscreenButton) { // check if this resolution is supported in fullscreen if (mResolutionList->getIndexSelected() != MyGUI::ITEM_NONE) { std::string resStr = mResolutionList->getItemNameAt(mResolutionList->getIndexSelected()); int resX, resY; parseResolution (resX, resY, resStr); Settings::Manager::setInt("resolution x", "Video", resX); Settings::Manager::setInt("resolution y", "Video", resY); } bool supported = false; int fallbackX = 0, fallbackY = 0; for (unsigned int i=0; igetItemCount(); ++i) { std::string resStr = mResolutionList->getItemNameAt(i); int resX, resY; parseResolution (resX, resY, resStr); if (i == 0) { fallbackX = resX; fallbackY = resY; } if (resX == Settings::Manager::getInt("resolution x", "Video") && resY == Settings::Manager::getInt("resolution y", "Video")) supported = true; } if (!supported && mResolutionList->getItemCount()) { if (fallbackX != 0 && fallbackY != 0) { Settings::Manager::setInt("resolution x", "Video", fallbackX); Settings::Manager::setInt("resolution y", "Video", fallbackY); } } mWindowBorderButton->setEnabled(!newState); } if (getSettingType(_sender) == checkButtonType) { Settings::Manager::setBool(getSettingName(_sender), getSettingCategory(_sender), newState); apply(); return; } } void SettingsWindow::onTextureFilteringChanged(MyGUI::ComboBox* _sender, size_t pos) { if(pos == 0) Settings::Manager::setString("texture mipmap", "General", "nearest"); else if(pos == 1) Settings::Manager::setString("texture mipmap", "General", "linear"); else Log(Debug::Warning) << "Unexpected option pos " << pos; apply(); } void SettingsWindow::onSliderChangePosition(MyGUI::ScrollBar* scroller, size_t pos) { if (getSettingType(scroller) == "Slider") { std::string valueStr; std::string valueType = getSettingValueType(scroller); if (valueType == "Float" || valueType == "Integer" || valueType == "Cell") { float value = pos / float(scroller->getScrollRange()-1); float min,max; getSettingMinMax(scroller, min, max); value = min + (max-min) * value; if (valueType == "Float") Settings::Manager::setFloat(getSettingName(scroller), getSettingCategory(scroller), value); else Settings::Manager::setInt(getSettingName(scroller), getSettingCategory(scroller), (int)value); if (valueType == "Cell") { std::stringstream ss; ss << std::fixed << std::setprecision(2) << value/Constants::CellSizeInUnits; valueStr = ss.str(); } else if (valueType == "Float") { std::stringstream ss; ss << std::fixed << std::setprecision(2) << value; valueStr = ss.str(); } else valueStr = MyGUI::utility::toString(int(value)); } else { Settings::Manager::setInt(getSettingName(scroller), getSettingCategory(scroller), pos); valueStr = MyGUI::utility::toString(pos); } updateSliderLabel(scroller, valueStr); apply(); } } void SettingsWindow::apply() { const Settings::CategorySettingVector changed = Settings::Manager::getPendingChanges(); MWBase::Environment::get().getWorld()->processChangedSettings(changed); MWBase::Environment::get().getSoundManager()->processChangedSettings(changed); MWBase::Environment::get().getWindowManager()->processChangedSettings(changed); MWBase::Environment::get().getInputManager()->processChangedSettings(changed); MWBase::Environment::get().getMechanicsManager()->processChangedSettings(changed); Settings::Manager::resetPendingChanges(); } void SettingsWindow::onKeyboardSwitchClicked(MyGUI::Widget* _sender) { if(mKeyboardMode) return; mKeyboardMode = true; mKeyboardSwitch->setStateSelected(true); mControllerSwitch->setStateSelected(false); updateControlsBox(); resetScrollbars(); } void SettingsWindow::onControllerSwitchClicked(MyGUI::Widget* _sender) { if(!mKeyboardMode) return; mKeyboardMode = false; mKeyboardSwitch->setStateSelected(false); mControllerSwitch->setStateSelected(true); updateControlsBox(); resetScrollbars(); } void SettingsWindow::updateControlsBox() { while (mControlsBox->getChildCount()) MyGUI::Gui::getInstance().destroyWidget(mControlsBox->getChildAt(0)); MWBase::Environment::get().getWindowManager()->removeStaticMessageBox(); std::vector actions; if(mKeyboardMode) actions = MWBase::Environment::get().getInputManager()->getActionKeySorting(); else actions = MWBase::Environment::get().getInputManager()->getActionControllerSorting(); for (const int& action : actions) { std::string desc = MWBase::Environment::get().getInputManager()->getActionDescription (action); if (desc == "") continue; std::string binding; if(mKeyboardMode) binding = MWBase::Environment::get().getInputManager()->getActionKeyBindingName(action); else binding = MWBase::Environment::get().getInputManager()->getActionControllerBindingName(action); Gui::SharedStateButton* leftText = mControlsBox->createWidget("SandTextButton", MyGUI::IntCoord(), MyGUI::Align::Default); leftText->setCaptionWithReplacing(desc); Gui::SharedStateButton* rightText = mControlsBox->createWidget("SandTextButton", MyGUI::IntCoord(), MyGUI::Align::Default); rightText->setCaptionWithReplacing(binding); rightText->setTextAlign (MyGUI::Align::Right); rightText->setUserData(action); // save the action id for callbacks rightText->eventMouseButtonClick += MyGUI::newDelegate(this, &SettingsWindow::onRebindAction); rightText->eventMouseWheel += MyGUI::newDelegate(this, &SettingsWindow::onInputTabMouseWheel); Gui::ButtonGroup group; group.push_back(leftText); group.push_back(rightText); Gui::SharedStateButton::createButtonGroup(group); } layoutControlsBox(); } void SettingsWindow::updateLightSettings() { auto lightingMethod = MWBase::Environment::get().getResourceSystem()->getSceneManager()->getLightingMethod(); std::string lightingMethodStr = SceneUtil::LightManager::getLightingMethodString(lightingMethod); mLightingMethodButton->removeAllItems(); std::array methods = { SceneUtil::LightingMethod::FFP, SceneUtil::LightingMethod::PerObjectUniform, SceneUtil::LightingMethod::SingleUBO, }; for (const auto& method : methods) { if (!MWBase::Environment::get().getResourceSystem()->getSceneManager()->isSupportedLightingMethod(method)) continue; mLightingMethodButton->addItem(SceneUtil::LightManager::getLightingMethodString(method)); } mLightingMethodButton->setIndexSelected(mLightingMethodButton->findItemIndexWith(lightingMethodStr)); } void SettingsWindow::layoutControlsBox() { const int h = 18; const int w = mControlsBox->getWidth() - 28; const int noWidgetsInRow = 2; const int totalH = mControlsBox->getChildCount() / noWidgetsInRow * h; for (size_t i = 0; i < mControlsBox->getChildCount(); i++) { MyGUI::Widget * widget = mControlsBox->getChildAt(i); widget->setCoord(0, i / noWidgetsInRow * h, w, h); } // Canvas size must be expressed with VScroll disabled, otherwise MyGUI would expand the scroll area when the scrollbar is hidden mControlsBox->setVisibleVScroll(false); mControlsBox->setCanvasSize (mControlsBox->getWidth(), std::max(totalH, mControlsBox->getHeight())); mControlsBox->setVisibleVScroll(true); } void SettingsWindow::onRebindAction(MyGUI::Widget* _sender) { int actionId = *_sender->getUserData(); _sender->castType()->setCaptionWithReplacing("#{sNone}"); MWBase::Environment::get().getWindowManager ()->staticMessageBox ("#{sControlsMenu3}"); MWBase::Environment::get().getWindowManager ()->disallowMouse(); MWBase::Environment::get().getInputManager ()->enableDetectingBindingMode (actionId, mKeyboardMode); } void SettingsWindow::onInputTabMouseWheel(MyGUI::Widget* _sender, int _rel) { if (mControlsBox->getViewOffset().top + _rel*0.3f > 0) mControlsBox->setViewOffset(MyGUI::IntPoint(0, 0)); else mControlsBox->setViewOffset(MyGUI::IntPoint(0, static_cast(mControlsBox->getViewOffset().top + _rel*0.3f))); } void SettingsWindow::onResetDefaultBindings(MyGUI::Widget* _sender) { ConfirmationDialog* dialog = MWBase::Environment::get().getWindowManager()->getConfirmationDialog(); dialog->askForConfirmation("#{sNotifyMessage66}"); dialog->eventOkClicked.clear(); dialog->eventOkClicked += MyGUI::newDelegate(this, &SettingsWindow::onResetDefaultBindingsAccept); dialog->eventCancelClicked.clear(); } void SettingsWindow::onResetDefaultBindingsAccept() { if(mKeyboardMode) MWBase::Environment::get().getInputManager ()->resetToDefaultKeyBindings (); else MWBase::Environment::get().getInputManager()->resetToDefaultControllerBindings(); updateControlsBox (); } void SettingsWindow::onOpen() { highlightCurrentResolution(); updateControlsBox(); updateLightSettings(); resetScrollbars(); MWBase::Environment::get().getWindowManager()->setKeyFocusWidget(mOkButton); } void SettingsWindow::onWindowResize(MyGUI::Window *_sender) { layoutControlsBox(); } void SettingsWindow::resetScrollbars() { mResolutionList->setScrollPosition(0); mControlsBox->setViewOffset(MyGUI::IntPoint(0, 0)); } } ================================================ FILE: apps/openmw/mwgui/settingswindow.hpp ================================================ #ifndef MWGUI_SETTINGS_H #define MWGUI_SETTINGS_H #include "windowbase.hpp" namespace MWGui { class SettingsWindow : public WindowBase { public: SettingsWindow(); void onOpen() override; void updateControlsBox(); void updateLightSettings(); void onResChange(int, int) override { center(); } protected: MyGUI::TabControl* mSettingsTab; MyGUI::Button* mOkButton; // graphics MyGUI::ListBox* mResolutionList; MyGUI::Button* mFullscreenButton; MyGUI::Button* mWindowBorderButton; MyGUI::ComboBox* mTextureFilteringButton; MyGUI::Widget* mAnisotropyBox; MyGUI::ComboBox* mWaterTextureSize; MyGUI::ComboBox* mWaterReflectionDetail; MyGUI::ComboBox* mMaxLights; MyGUI::ComboBox* mLightingMethodButton; MyGUI::Button* mLightsResetButton; // controls MyGUI::ScrollView* mControlsBox; MyGUI::Button* mResetControlsButton; MyGUI::Button* mKeyboardSwitch; MyGUI::Button* mControllerSwitch; bool mKeyboardMode; //if true, setting up the keyboard. Otherwise, it's controller void onTabChanged(MyGUI::TabControl* _sender, size_t index); void onOkButtonClicked(MyGUI::Widget* _sender); void onTextureFilteringChanged(MyGUI::ComboBox* _sender, size_t pos); void onSliderChangePosition(MyGUI::ScrollBar* scroller, size_t pos); void onButtonToggled(MyGUI::Widget* _sender); void onResolutionSelected(MyGUI::ListBox* _sender, size_t index); void onResolutionAccept(); void onResolutionCancel(); void highlightCurrentResolution(); void onWaterTextureSizeChanged(MyGUI::ComboBox* _sender, size_t pos); void onWaterReflectionDetailChanged(MyGUI::ComboBox* _sender, size_t pos); void onLightingMethodButtonChanged(MyGUI::ComboBox* _sender, size_t pos); void onLightsResetButtonClicked(MyGUI::Widget* _sender); void onMaxLightsChanged(MyGUI::ComboBox* _sender, size_t pos); void onRebindAction(MyGUI::Widget* _sender); void onInputTabMouseWheel(MyGUI::Widget* _sender, int _rel); void onResetDefaultBindings(MyGUI::Widget* _sender); void onResetDefaultBindingsAccept (); void onKeyboardSwitchClicked(MyGUI::Widget* _sender); void onControllerSwitchClicked(MyGUI::Widget* _sender); void onWindowResize(MyGUI::Window* _sender); void apply(); void configureWidgets(MyGUI::Widget* widget, bool init); void updateSliderLabel(MyGUI::ScrollBar* scroller, const std::string& value); void layoutControlsBox(); private: void resetScrollbars(); }; } #endif ================================================ FILE: apps/openmw/mwgui/sortfilteritemmodel.cpp ================================================ #include "sortfilteritemmodel.hpp" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "../mwbase/environment.hpp" #include "../mwbase/world.hpp" #include "../mwworld/class.hpp" #include "../mwworld/nullaction.hpp" #include "../mwworld/esmstore.hpp" #include "../mwmechanics/alchemy.hpp" namespace { bool compareType(const std::string& type1, const std::string& type2) { // this defines the sorting order of types. types that are first in the vector appear before other types. std::vector mapping; mapping.emplace_back(typeid(ESM::Weapon).name() ); mapping.emplace_back(typeid(ESM::Armor).name() ); mapping.emplace_back(typeid(ESM::Clothing).name() ); mapping.emplace_back(typeid(ESM::Potion).name() ); mapping.emplace_back(typeid(ESM::Ingredient).name() ); mapping.emplace_back(typeid(ESM::Apparatus).name() ); mapping.emplace_back(typeid(ESM::Book).name() ); mapping.emplace_back(typeid(ESM::Light).name() ); mapping.emplace_back(typeid(ESM::Miscellaneous).name() ); mapping.emplace_back(typeid(ESM::Lockpick).name() ); mapping.emplace_back(typeid(ESM::Repair).name() ); mapping.emplace_back(typeid(ESM::Probe).name() ); assert( std::find(mapping.begin(), mapping.end(), type1) != mapping.end() ); assert( std::find(mapping.begin(), mapping.end(), type2) != mapping.end() ); return std::find(mapping.begin(), mapping.end(), type1) < std::find(mapping.begin(), mapping.end(), type2); } struct Compare { bool mSortByType; Compare() : mSortByType(true) {} bool operator() (const MWGui::ItemStack& left, const MWGui::ItemStack& right) { if (mSortByType && left.mType != right.mType) return left.mType < right.mType; float result = 0; // compare items by type std::string leftName = left.mBase.getTypeName(); std::string rightName = right.mBase.getTypeName(); if (leftName != rightName) return compareType(leftName, rightName); // compare items by name leftName = Misc::StringUtils::lowerCaseUtf8(left.mBase.getClass().getName(left.mBase)); rightName = Misc::StringUtils::lowerCaseUtf8(right.mBase.getClass().getName(right.mBase)); result = leftName.compare(rightName); if (result != 0) return result < 0; // compare items by enchantment: // 1. enchanted items showed before non-enchanted // 2. item with lesser charge percent comes after items with more charge percent // 3. item with constant effect comes before items with non-constant effects int leftChargePercent = -1; int rightChargePercent = -1; leftName = left.mBase.getClass().getEnchantment(left.mBase); rightName = right.mBase.getClass().getEnchantment(right.mBase); if (!leftName.empty()) { const ESM::Enchantment* ench = MWBase::Environment::get().getWorld()->getStore().get().search(leftName); if (ench) { if (ench->mData.mType == ESM::Enchantment::ConstantEffect) leftChargePercent = 101; else leftChargePercent = static_cast(left.mBase.getCellRef().getNormalizedEnchantmentCharge(ench->mData.mCharge) * 100); } } if (!rightName.empty()) { const ESM::Enchantment* ench = MWBase::Environment::get().getWorld()->getStore().get().search(rightName); if (ench) { if (ench->mData.mType == ESM::Enchantment::ConstantEffect) rightChargePercent = 101; else rightChargePercent = static_cast(right.mBase.getCellRef().getNormalizedEnchantmentCharge(ench->mData.mCharge) * 100); } } result = leftChargePercent - rightChargePercent; if (result != 0) return result > 0; // compare items by condition if (left.mBase.getClass().hasItemHealth(left.mBase) && right.mBase.getClass().hasItemHealth(right.mBase)) { result = left.mBase.getClass().getItemHealth(left.mBase) - right.mBase.getClass().getItemHealth(right.mBase); if (result != 0) return result > 0; } // compare items by remaining usage time result = left.mBase.getClass().getRemainingUsageTime(left.mBase) - right.mBase.getClass().getRemainingUsageTime(right.mBase); if (result != 0) return result > 0; // compare items by value result = left.mBase.getClass().getValue(left.mBase) - right.mBase.getClass().getValue(right.mBase); if (result != 0) return result > 0; // compare items by weight result = left.mBase.getClass().getWeight(left.mBase) - right.mBase.getClass().getWeight(right.mBase); if (result != 0) return result > 0; // compare items by Id leftName = left.mBase.getCellRef().getRefId(); rightName = right.mBase.getCellRef().getRefId(); result = leftName.compare(rightName); return result < 0; } }; } namespace MWGui { SortFilterItemModel::SortFilterItemModel(ItemModel *sourceModel) : mCategory(Category_All) , mFilter(0) , mSortByType(true) , mNameFilter("") , mEffectFilter("") { mSourceModel = sourceModel; } bool SortFilterItemModel::allowedToUseItems() const { return mSourceModel->allowedToUseItems(); } void SortFilterItemModel::addDragItem (const MWWorld::Ptr& dragItem, size_t count) { mDragItems.emplace_back(dragItem, count); } void SortFilterItemModel::clearDragItems() { mDragItems.clear(); } bool SortFilterItemModel::filterAccepts (const ItemStack& item) { MWWorld::Ptr base = item.mBase; int category = 0; if (base.getTypeName() == typeid(ESM::Armor).name() || base.getTypeName() == typeid(ESM::Clothing).name()) category = Category_Apparel; else if (base.getTypeName() == typeid(ESM::Weapon).name()) category = Category_Weapon; else if (base.getTypeName() == typeid(ESM::Ingredient).name() || base.getTypeName() == typeid(ESM::Potion).name()) category = Category_Magic; else if (base.getTypeName() == typeid(ESM::Miscellaneous).name() || base.getTypeName() == typeid(ESM::Ingredient).name() || base.getTypeName() == typeid(ESM::Repair).name() || base.getTypeName() == typeid(ESM::Lockpick).name() || base.getTypeName() == typeid(ESM::Light).name() || base.getTypeName() == typeid(ESM::Apparatus).name() || base.getTypeName() == typeid(ESM::Book).name() || base.getTypeName() == typeid(ESM::Probe).name()) category = Category_Misc; if (item.mFlags & ItemStack::Flag_Enchanted) category |= Category_Magic; if (!(category & mCategory)) return false; if (mFilter & Filter_OnlyIngredients) { if (base.getTypeName() != typeid(ESM::Ingredient).name()) return false; if (!mNameFilter.empty() && !mEffectFilter.empty()) throw std::logic_error("name and magic effect filter are mutually exclusive"); if (!mNameFilter.empty()) { const auto itemName = Misc::StringUtils::lowerCaseUtf8(base.getClass().getName(base)); return itemName.find(mNameFilter) != std::string::npos; } if (!mEffectFilter.empty()) { MWWorld::Ptr player = MWBase::Environment::get().getWorld ()->getPlayerPtr(); const auto alchemySkill = player.getClass().getSkill(player, ESM::Skill::Alchemy); const auto effects = MWMechanics::Alchemy::effectsDescription(base, alchemySkill); for (const auto& effect : effects) { const auto ciEffect = Misc::StringUtils::lowerCaseUtf8(effect); if (ciEffect.find(mEffectFilter) != std::string::npos) return true; } return false; } return true; } if ((mFilter & Filter_OnlyEnchanted) && !(item.mFlags & ItemStack::Flag_Enchanted)) return false; if ((mFilter & Filter_OnlyChargedSoulstones) && (base.getTypeName() != typeid(ESM::Miscellaneous).name() || base.getCellRef().getSoul() == "" || !MWBase::Environment::get().getWorld()->getStore().get().search(base.getCellRef().getSoul()))) return false; if ((mFilter & Filter_OnlyRepairTools) && (base.getTypeName() != typeid(ESM::Repair).name())) return false; if ((mFilter & Filter_OnlyEnchantable) && (item.mFlags & ItemStack::Flag_Enchanted || (base.getTypeName() != typeid(ESM::Armor).name() && base.getTypeName() != typeid(ESM::Clothing).name() && base.getTypeName() != typeid(ESM::Weapon).name() && base.getTypeName() != typeid(ESM::Book).name()))) return false; if ((mFilter & Filter_OnlyEnchantable) && base.getTypeName() == typeid(ESM::Book).name() && !base.get()->mBase->mData.mIsScroll) return false; if ((mFilter & Filter_OnlyUsableItems) && base.getClass().getScript(base).empty()) { std::shared_ptr actionOnUse = base.getClass().use(base); if (!actionOnUse || actionOnUse->isNullAction()) return false; } if ((mFilter & Filter_OnlyRepairable) && ( !base.getClass().hasItemHealth(base) || (base.getClass().getItemHealth(base) == base.getClass().getItemMaxHealth(base)) || (base.getTypeName() != typeid(ESM::Weapon).name() && base.getTypeName() != typeid(ESM::Armor).name()))) return false; if (mFilter & Filter_OnlyRechargable) { if (!(item.mFlags & ItemStack::Flag_Enchanted)) return false; std::string enchId = base.getClass().getEnchantment(base); const ESM::Enchantment* ench = MWBase::Environment::get().getWorld()->getStore().get().search(enchId); if (!ench) { Log(Debug::Warning) << "Warning: Can't find enchantment '" << enchId << "' on item " << base.getCellRef().getRefId(); return false; } if (base.getCellRef().getEnchantmentCharge() >= ench->mData.mCharge || base.getCellRef().getEnchantmentCharge() == -1) return false; } std::string compare = Misc::StringUtils::lowerCaseUtf8(item.mBase.getClass().getName(item.mBase)); if(compare.find(mNameFilter) == std::string::npos) return false; return true; } ItemStack SortFilterItemModel::getItem (ModelIndex index) { if (index < 0) throw std::runtime_error("Invalid index supplied"); if (mItems.size() <= static_cast(index)) throw std::runtime_error("Item index out of range"); return mItems[index]; } size_t SortFilterItemModel::getItemCount() { return mItems.size(); } void SortFilterItemModel::setCategory (int category) { mCategory = category; } void SortFilterItemModel::setFilter (int filter) { mFilter = filter; } void SortFilterItemModel::setNameFilter (const std::string& filter) { mNameFilter = Misc::StringUtils::lowerCaseUtf8(filter); } void SortFilterItemModel::setEffectFilter (const std::string& filter) { mEffectFilter = Misc::StringUtils::lowerCaseUtf8(filter); } void SortFilterItemModel::update() { mSourceModel->update(); size_t count = mSourceModel->getItemCount(); mItems.clear(); for (size_t i=0; igetItem(i); for (std::vector >::iterator it = mDragItems.begin(); it != mDragItems.end(); ++it) { if (item.mBase == it->first) { if (item.mCount < it->second) throw std::runtime_error("Dragging more than present in the model"); item.mCount -= it->second; } } if (item.mCount > 0 && filterAccepts(item)) mItems.push_back(item); } Compare cmp; cmp.mSortByType = mSortByType; std::sort(mItems.begin(), mItems.end(), cmp); } void SortFilterItemModel::onClose() { mSourceModel->onClose(); } bool SortFilterItemModel::onDropItem(const MWWorld::Ptr &item, int count) { return mSourceModel->onDropItem(item, count); } bool SortFilterItemModel::onTakeItem(const MWWorld::Ptr &item, int count) { return mSourceModel->onTakeItem(item, count); } } ================================================ FILE: apps/openmw/mwgui/sortfilteritemmodel.hpp ================================================ #ifndef MWGUI_SORT_FILTER_ITEM_MODEL_H #define MWGUI_SORT_FILTER_ITEM_MODEL_H #include "itemmodel.hpp" namespace MWGui { class SortFilterItemModel : public ProxyItemModel { public: SortFilterItemModel (ItemModel* sourceModel); void update() override; bool filterAccepts (const ItemStack& item); bool allowedToUseItems() const override; ItemStack getItem (ModelIndex index) override; size_t getItemCount() override; /// Dragged items are not displayed. void addDragItem (const MWWorld::Ptr& dragItem, size_t count); void clearDragItems(); void setCategory (int category); void setFilter (int filter); void setNameFilter (const std::string& filter); void setEffectFilter (const std::string& filter); /// Use ItemStack::Type for sorting? void setSortByType(bool sort) { mSortByType = sort; } void onClose() override; bool onDropItem(const MWWorld::Ptr &item, int count) override; bool onTakeItem(const MWWorld::Ptr &item, int count) override; static constexpr int Category_Weapon = (1<<1); static constexpr int Category_Apparel = (1<<2); static constexpr int Category_Misc = (1<<3); static constexpr int Category_Magic = (1<<4); static constexpr int Category_All = 255; static constexpr int Filter_OnlyIngredients = (1<<0); static constexpr int Filter_OnlyEnchanted = (1<<1); static constexpr int Filter_OnlyEnchantable = (1<<2); static constexpr int Filter_OnlyChargedSoulstones = (1<<3); static constexpr int Filter_OnlyUsableItems = (1<<4); // Only items with a Use action static constexpr int Filter_OnlyRepairable = (1<<5); static constexpr int Filter_OnlyRechargable = (1<<6); static constexpr int Filter_OnlyRepairTools = (1<<7); private: std::vector mItems; std::vector > mDragItems; int mCategory; int mFilter; bool mSortByType; std::string mNameFilter; // filter by item name std::string mEffectFilter; // filter by magic effect }; } #endif ================================================ FILE: apps/openmw/mwgui/soulgemdialog.cpp ================================================ #include "soulgemdialog.hpp" #include "../mwbase/environment.hpp" #include "../mwbase/windowmanager.hpp" #include "messagebox.hpp" namespace MWGui { void SoulgemDialog::show(const MWWorld::Ptr &soulgem) { mSoulgem = soulgem; std::vector buttons; buttons.emplace_back("#{sRechargeEnchantment}"); buttons.emplace_back("#{sMake Enchantment}"); mManager->createInteractiveMessageBox("#{sDoYouWantTo}", buttons); mManager->eventButtonPressed += MyGUI::newDelegate(this, &SoulgemDialog::onButtonPressed); } void SoulgemDialog::onButtonPressed(int button) { if (button == 0) { MWBase::Environment::get().getWindowManager()->pushGuiMode(GM_Recharge, mSoulgem); } else { MWBase::Environment::get().getWindowManager()->pushGuiMode(GM_Enchanting, mSoulgem); } } } ================================================ FILE: apps/openmw/mwgui/soulgemdialog.hpp ================================================ #ifndef OPENMW_MWGUI_SOULGEMDIALOG_H #define OPENMW_MWGUI_SOULGEMDIALOG_H #include "../mwworld/ptr.hpp" namespace MWGui { class MessageBoxManager; class SoulgemDialog { public: SoulgemDialog (MessageBoxManager* manager) : mManager(manager) {} void show (const MWWorld::Ptr& soulgem); void onButtonPressed(int button); private: MessageBoxManager* mManager; MWWorld::Ptr mSoulgem; }; } #endif ================================================ FILE: apps/openmw/mwgui/spellbuyingwindow.cpp ================================================ #include "spellbuyingwindow.hpp" #include #include #include /* Start of tes3mp addition Include additional headers for multiplayer purposes */ #include "../mwmp/Main.hpp" #include "../mwmp/Networking.hpp" #include "../mwmp/LocalPlayer.hpp" #include "../mwmp/ObjectList.hpp" /* End of tes3mp addition */ #include "../mwbase/environment.hpp" #include "../mwbase/world.hpp" #include "../mwbase/windowmanager.hpp" #include "../mwbase/mechanicsmanager.hpp" #include "../mwworld/class.hpp" #include "../mwworld/containerstore.hpp" #include "../mwworld/esmstore.hpp" #include "../mwmechanics/creaturestats.hpp" #include "../mwmechanics/actorutil.hpp" namespace MWGui { SpellBuyingWindow::SpellBuyingWindow() : WindowBase("openmw_spell_buying_window.layout") , mCurrentY(0) { getWidget(mCancelButton, "CancelButton"); getWidget(mPlayerGold, "PlayerGold"); getWidget(mSpellsView, "SpellsView"); mCancelButton->eventMouseButtonClick += MyGUI::newDelegate(this, &SpellBuyingWindow::onCancelButtonClicked); } bool SpellBuyingWindow::sortSpells (const ESM::Spell* left, const ESM::Spell* right) { std::string leftName = Misc::StringUtils::lowerCase(left->mName); std::string rightName = Misc::StringUtils::lowerCase(right->mName); return leftName.compare(rightName) < 0; } void SpellBuyingWindow::addSpell(const ESM::Spell& spell) { const MWWorld::ESMStore &store = MWBase::Environment::get().getWorld()->getStore(); int price = std::max(1, static_cast(spell.mData.mCost*store.get().find("fSpellValueMult")->mValue.getFloat())); price = MWBase::Environment::get().getMechanicsManager()->getBarterOffer(mPtr,price,true); MWWorld::Ptr player = MWMechanics::getPlayer(); int playerGold = player.getClass().getContainerStore(player).count(MWWorld::ContainerStore::sGoldId); // TODO: refactor to use MyGUI::ListBox int lineHeight = MWBase::Environment::get().getWindowManager()->getFontHeight() + 2; MyGUI::Button* toAdd = mSpellsView->createWidget( price <= playerGold ? "SandTextButton" : "SandTextButtonDisabled", // can't use setEnabled since that removes tooltip 0, mCurrentY, 200, lineHeight, MyGUI::Align::Default ); mCurrentY += lineHeight; toAdd->setUserData(price); toAdd->setCaptionWithReplacing(spell.mName+" - "+MyGUI::utility::toString(price)+"#{sgp}"); toAdd->setSize(mSpellsView->getWidth(), lineHeight); toAdd->eventMouseWheel += MyGUI::newDelegate(this, &SpellBuyingWindow::onMouseWheel); toAdd->setUserString("ToolTipType", "Spell"); toAdd->setUserString("Spell", spell.mId); toAdd->setUserString("SpellCost", std::to_string(spell.mData.mCost)); toAdd->eventMouseButtonClick += MyGUI::newDelegate(this, &SpellBuyingWindow::onSpellButtonClick); mSpellsWidgetMap.insert(std::make_pair (toAdd, spell.mId)); } void SpellBuyingWindow::clearSpells() { mSpellsView->setViewOffset(MyGUI::IntPoint(0,0)); mCurrentY = 0; while (mSpellsView->getChildCount()) MyGUI::Gui::getInstance().destroyWidget(mSpellsView->getChildAt(0)); mSpellsWidgetMap.clear(); } void SpellBuyingWindow::setPtr(const MWWorld::Ptr &actor) { setPtr(actor, 0); } void SpellBuyingWindow::setPtr(const MWWorld::Ptr& actor, int startOffset) { center(); mPtr = actor; clearSpells(); MWMechanics::Spells& merchantSpells = actor.getClass().getCreatureStats (actor).getSpells(); std::vector spellsToSort; for (MWMechanics::Spells::TIterator iter = merchantSpells.begin(); iter!=merchantSpells.end(); ++iter) { const ESM::Spell* spell = iter->first; if (spell->mData.mType!=ESM::Spell::ST_Spell) continue; // don't try to sell diseases, curses or powers if (actor.getClass().isNpc()) { const ESM::Race* race = MWBase::Environment::get().getWorld()->getStore().get().find( actor.get()->mBase->mRace); if (race->mPowers.exists(spell->mId)) continue; } if (playerHasSpell(iter->first->mId)) continue; spellsToSort.push_back(iter->first); } std::stable_sort(spellsToSort.begin(), spellsToSort.end(), sortSpells); for (const ESM::Spell* spell : spellsToSort) { addSpell(*spell); } spellsToSort.clear(); updateLabels(); // Canvas size must be expressed with VScroll disabled, otherwise MyGUI would expand the scroll area when the scrollbar is hidden mSpellsView->setVisibleVScroll(false); mSpellsView->setCanvasSize (MyGUI::IntSize(mSpellsView->getWidth(), std::max(mSpellsView->getHeight(), mCurrentY))); mSpellsView->setVisibleVScroll(true); mSpellsView->setViewOffset(MyGUI::IntPoint(0, startOffset)); } bool SpellBuyingWindow::playerHasSpell(const std::string &id) { MWWorld::Ptr player = MWMechanics::getPlayer(); return player.getClass().getCreatureStats(player).getSpells().hasSpell(id); } void SpellBuyingWindow::onSpellButtonClick(MyGUI::Widget* _sender) { int price = *_sender->getUserData(); MWWorld::Ptr player = MWMechanics::getPlayer(); if (price > player.getClass().getContainerStore(player).count(MWWorld::ContainerStore::sGoldId)) return; MWMechanics::CreatureStats& stats = player.getClass().getCreatureStats(player); MWMechanics::Spells& spells = stats.getSpells(); spells.add (mSpellsWidgetMap.find(_sender)->second); /* Start of tes3mp addition Send an ID_PLAYER_SPELLBOOK packet every time a player buys a spell */ mwmp::Main::get().getLocalPlayer()->sendSpellChange(mSpellsWidgetMap.find(_sender)->second, mwmp::SpellbookChanges::ADD); /* End of tes3mp addition */ player.getClass().getContainerStore(player).remove(MWWorld::ContainerStore::sGoldId, price, player); // add gold to NPC trading gold pool MWMechanics::CreatureStats& npcStats = mPtr.getClass().getCreatureStats(mPtr); /* Start of tes3mp change (major) Don't unilaterally change the merchant's gold pool on our client and instead let the server do it */ //npcStats.setGoldPool(npcStats.getGoldPool() + price); mwmp::ObjectList* objectList = mwmp::Main::get().getNetworking()->getObjectList(); objectList->reset(); objectList->packetOrigin = mwmp::CLIENT_GAMEPLAY; objectList->addObjectMiscellaneous(mPtr, npcStats.getGoldPool() + price, npcStats.getLastRestockTime().getHour(), npcStats.getLastRestockTime().getDay()); objectList->sendObjectMiscellaneous(); /* End of tes3mp change (major) */ setPtr(mPtr, mSpellsView->getViewOffset().top); MWBase::Environment::get().getWindowManager()->playSound("Item Gold Up"); } void SpellBuyingWindow::onCancelButtonClicked(MyGUI::Widget* _sender) { MWBase::Environment::get().getWindowManager()->removeGuiMode (MWGui::GM_SpellBuying); } void SpellBuyingWindow::updateLabels() { MWWorld::Ptr player = MWMechanics::getPlayer(); int playerGold = player.getClass().getContainerStore(player).count(MWWorld::ContainerStore::sGoldId); mPlayerGold->setCaptionWithReplacing("#{sGold}: " + MyGUI::utility::toString(playerGold)); mPlayerGold->setCoord(8, mPlayerGold->getTop(), mPlayerGold->getTextSize().width, mPlayerGold->getHeight()); } void SpellBuyingWindow::onReferenceUnavailable() { // remove both Spells and Dialogue (since you always trade with the NPC/creature that you have previously talked to) MWBase::Environment::get().getWindowManager()->removeGuiMode(GM_SpellBuying); MWBase::Environment::get().getWindowManager()->exitCurrentGuiMode(); } void SpellBuyingWindow::onMouseWheel(MyGUI::Widget* _sender, int _rel) { if (mSpellsView->getViewOffset().top + _rel*0.3 > 0) mSpellsView->setViewOffset(MyGUI::IntPoint(0, 0)); else mSpellsView->setViewOffset(MyGUI::IntPoint(0, static_cast(mSpellsView->getViewOffset().top + _rel*0.3f))); } } ================================================ FILE: apps/openmw/mwgui/spellbuyingwindow.hpp ================================================ #ifndef MWGUI_SpellBuyingWINDOW_H #define MWGUI_SpellBuyingWINDOW_H #include "windowbase.hpp" #include "referenceinterface.hpp" namespace ESM { struct Spell; } namespace MyGUI { class Gui; class Widget; } namespace MWGui { class SpellBuyingWindow : public ReferenceInterface, public WindowBase { public: SpellBuyingWindow(); void setPtr(const MWWorld::Ptr& actor) override; void setPtr(const MWWorld::Ptr& actor, int startOffset); void onFrame(float dt) override { checkReferenceAvailable(); } void clear() override { resetReference(); } void onResChange(int, int) override { center(); } protected: MyGUI::Button* mCancelButton; MyGUI::TextBox* mPlayerGold; MyGUI::ScrollView* mSpellsView; std::map mSpellsWidgetMap; void onCancelButtonClicked(MyGUI::Widget* _sender); void onSpellButtonClick(MyGUI::Widget* _sender); void onMouseWheel(MyGUI::Widget* _sender, int _rel); void addSpell(const ESM::Spell& spell); void clearSpells(); int mCurrentY; void updateLabels(); void onReferenceUnavailable() override; bool playerHasSpell (const std::string& id); private: static bool sortSpells (const ESM::Spell* left, const ESM::Spell* right); }; } #endif ================================================ FILE: apps/openmw/mwgui/spellcreationdialog.cpp ================================================ #include "spellcreationdialog.hpp" #include #include #include #include /* Start of tes3mp addition Include additional headers for multiplayer purposes */ #include "../mwmp/Main.hpp" #include "../mwmp/Networking.hpp" #include "../mwmp/Worldstate.hpp" /* End of tes3mp addition */ #include "../mwbase/windowmanager.hpp" #include "../mwbase/mechanicsmanager.hpp" #include "../mwbase/environment.hpp" #include "../mwbase/world.hpp" #include "../mwworld/containerstore.hpp" #include "../mwworld/class.hpp" #include "../mwworld/esmstore.hpp" #include "../mwmechanics/spells.hpp" #include "../mwmechanics/creaturestats.hpp" #include "../mwmechanics/actorutil.hpp" #include "../mwmechanics/spellutil.hpp" #include "tooltips.hpp" #include "class.hpp" #include "widgets.hpp" namespace { bool sortMagicEffects (short id1, short id2) { const MWWorld::Store &gmst = MWBase::Environment::get().getWorld()->getStore().get(); return gmst.find(ESM::MagicEffect::effectIdToString (id1))->mValue.getString() < gmst.find(ESM::MagicEffect::effectIdToString (id2))->mValue.getString(); } void init(ESM::ENAMstruct& effect) { effect.mArea = 0; effect.mDuration = 0; effect.mEffectID = -1; effect.mMagnMax = 0; effect.mMagnMin = 0; effect.mRange = 0; effect.mSkill = -1; effect.mAttribute = -1; } } namespace MWGui { EditEffectDialog::EditEffectDialog() : WindowModal("openmw_edit_effect.layout") , mEditing(false) , mMagicEffect(nullptr) , mConstantEffect(false) { init(mEffect); init(mOldEffect); getWidget(mCancelButton, "CancelButton"); getWidget(mOkButton, "OkButton"); getWidget(mDeleteButton, "DeleteButton"); getWidget(mRangeButton, "RangeButton"); getWidget(mMagnitudeMinValue, "MagnitudeMinValue"); getWidget(mMagnitudeMaxValue, "MagnitudeMaxValue"); getWidget(mDurationValue, "DurationValue"); getWidget(mAreaValue, "AreaValue"); getWidget(mMagnitudeMinSlider, "MagnitudeMinSlider"); getWidget(mMagnitudeMaxSlider, "MagnitudeMaxSlider"); getWidget(mDurationSlider, "DurationSlider"); getWidget(mAreaSlider, "AreaSlider"); getWidget(mEffectImage, "EffectImage"); getWidget(mEffectName, "EffectName"); getWidget(mAreaText, "AreaText"); getWidget(mDurationBox, "DurationBox"); getWidget(mAreaBox, "AreaBox"); getWidget(mMagnitudeBox, "MagnitudeBox"); mRangeButton->eventMouseButtonClick += MyGUI::newDelegate(this, &EditEffectDialog::onRangeButtonClicked); mOkButton->eventMouseButtonClick += MyGUI::newDelegate(this, &EditEffectDialog::onOkButtonClicked); mCancelButton->eventMouseButtonClick += MyGUI::newDelegate(this, &EditEffectDialog::onCancelButtonClicked); mDeleteButton->eventMouseButtonClick += MyGUI::newDelegate(this, &EditEffectDialog::onDeleteButtonClicked); mMagnitudeMinSlider->eventScrollChangePosition += MyGUI::newDelegate(this, &EditEffectDialog::onMagnitudeMinChanged); mMagnitudeMaxSlider->eventScrollChangePosition += MyGUI::newDelegate(this, &EditEffectDialog::onMagnitudeMaxChanged); mDurationSlider->eventScrollChangePosition += MyGUI::newDelegate(this, &EditEffectDialog::onDurationChanged); mAreaSlider->eventScrollChangePosition += MyGUI::newDelegate(this, &EditEffectDialog::onAreaChanged); } void EditEffectDialog::setConstantEffect(bool constant) { mConstantEffect = constant; } void EditEffectDialog::onOpen() { WindowModal::onOpen(); center(); } bool EditEffectDialog::exit() { if(mEditing) eventEffectModified(mOldEffect); else eventEffectRemoved(mEffect); return true; } void EditEffectDialog::newEffect (const ESM::MagicEffect *effect) { bool allowSelf = (effect->mData.mFlags & ESM::MagicEffect::CastSelf) != 0; bool allowTouch = (effect->mData.mFlags & ESM::MagicEffect::CastTouch) && !mConstantEffect; bool allowTarget = (effect->mData.mFlags & ESM::MagicEffect::CastTarget) && !mConstantEffect; if (!allowSelf && !allowTouch && !allowTarget) return; // TODO: Show an error message popup? setMagicEffect(effect); mEditing = false; mDeleteButton->setVisible (false); mEffect.mRange = ESM::RT_Self; if (!allowSelf) mEffect.mRange = ESM::RT_Touch; if (!allowTouch) mEffect.mRange = ESM::RT_Target; mEffect.mMagnMin = 1; mEffect.mMagnMax = 1; mEffect.mDuration = 1; mEffect.mArea = 0; mEffect.mSkill = -1; mEffect.mAttribute = -1; eventEffectAdded(mEffect); onRangeButtonClicked(mRangeButton); mMagnitudeMinSlider->setScrollPosition (0); mMagnitudeMaxSlider->setScrollPosition (0); mAreaSlider->setScrollPosition (0); mDurationSlider->setScrollPosition (0); mDurationValue->setCaption("1"); mMagnitudeMinValue->setCaption("1"); const std::string to = MWBase::Environment::get().getWindowManager()->getGameSettingString("sTo", "-"); mMagnitudeMaxValue->setCaption(to + " 1"); mAreaValue->setCaption("0"); setVisible(true); } void EditEffectDialog::editEffect (ESM::ENAMstruct effect) { const ESM::MagicEffect* magicEffect = MWBase::Environment::get().getWorld()->getStore().get().find(effect.mEffectID); setMagicEffect(magicEffect); mOldEffect = effect; mEffect = effect; mEditing = true; mDeleteButton->setVisible (true); mMagnitudeMinSlider->setScrollPosition (effect.mMagnMin-1); mMagnitudeMaxSlider->setScrollPosition (effect.mMagnMax-1); mAreaSlider->setScrollPosition (effect.mArea); mDurationSlider->setScrollPosition (effect.mDuration-1); if (mEffect.mRange == ESM::RT_Self) mRangeButton->setCaptionWithReplacing ("#{sRangeSelf}"); else if (mEffect.mRange == ESM::RT_Target) mRangeButton->setCaptionWithReplacing ("#{sRangeTarget}"); else if (mEffect.mRange == ESM::RT_Touch) mRangeButton->setCaptionWithReplacing ("#{sRangeTouch}"); onMagnitudeMinChanged (mMagnitudeMinSlider, effect.mMagnMin-1); onMagnitudeMaxChanged (mMagnitudeMinSlider, effect.mMagnMax-1); onAreaChanged (mAreaSlider, effect.mArea); onDurationChanged (mDurationSlider, effect.mDuration-1); eventEffectModified(mEffect); updateBoxes(); } void EditEffectDialog::setMagicEffect (const ESM::MagicEffect *effect) { mEffectImage->setImageTexture(MWBase::Environment::get().getWindowManager()->correctIconPath(effect->mIcon)); mEffectName->setCaptionWithReplacing("#{"+ESM::MagicEffect::effectIdToString (effect->mIndex)+"}"); mEffect.mEffectID = effect->mIndex; mMagicEffect = effect; updateBoxes(); } void EditEffectDialog::updateBoxes() { static int startY = mMagnitudeBox->getPosition().top; int curY = startY; mMagnitudeBox->setVisible (false); mDurationBox->setVisible (false); mAreaBox->setVisible (false); if (!(mMagicEffect->mData.mFlags & ESM::MagicEffect::NoMagnitude)) { mMagnitudeBox->setPosition(mMagnitudeBox->getPosition().left, curY); mMagnitudeBox->setVisible (true); curY += mMagnitudeBox->getSize().height; } if (!(mMagicEffect->mData.mFlags & ESM::MagicEffect::NoDuration)&&mConstantEffect==false) { mDurationBox->setPosition(mDurationBox->getPosition().left, curY); mDurationBox->setVisible (true); curY += mDurationBox->getSize().height; } if (mEffect.mRange != ESM::RT_Self) { mAreaBox->setPosition(mAreaBox->getPosition().left, curY); mAreaBox->setVisible (true); //curY += mAreaBox->getSize().height; } } void EditEffectDialog::onRangeButtonClicked (MyGUI::Widget* sender) { mEffect.mRange = (mEffect.mRange+1)%3; // cycle through range types until we find something that's allowed // does not handle the case where nothing is allowed (this should be prevented before opening the Add Effect dialog) bool allowSelf = (mMagicEffect->mData.mFlags & ESM::MagicEffect::CastSelf) != 0; bool allowTouch = (mMagicEffect->mData.mFlags & ESM::MagicEffect::CastTouch) && !mConstantEffect; bool allowTarget = (mMagicEffect->mData.mFlags & ESM::MagicEffect::CastTarget) && !mConstantEffect; if (mEffect.mRange == ESM::RT_Self && !allowSelf) mEffect.mRange = (mEffect.mRange+1)%3; if (mEffect.mRange == ESM::RT_Touch && !allowTouch) mEffect.mRange = (mEffect.mRange+1)%3; if (mEffect.mRange == ESM::RT_Target && !allowTarget) mEffect.mRange = (mEffect.mRange+1)%3; if(mEffect.mRange == ESM::RT_Self) { mAreaSlider->setScrollPosition(0); onAreaChanged(mAreaSlider,0); } if (mEffect.mRange == ESM::RT_Self) mRangeButton->setCaptionWithReplacing ("#{sRangeSelf}"); else if (mEffect.mRange == ESM::RT_Target) mRangeButton->setCaptionWithReplacing ("#{sRangeTarget}"); else if (mEffect.mRange == ESM::RT_Touch) mRangeButton->setCaptionWithReplacing ("#{sRangeTouch}"); updateBoxes(); eventEffectModified(mEffect); } void EditEffectDialog::onDeleteButtonClicked (MyGUI::Widget* sender) { setVisible(false); eventEffectRemoved(mEffect); } void EditEffectDialog::onOkButtonClicked (MyGUI::Widget* sender) { setVisible(false); } void EditEffectDialog::onCancelButtonClicked (MyGUI::Widget* sender) { setVisible(false); exit(); } void EditEffectDialog::setSkill (int skill) { mEffect.mSkill = skill; eventEffectModified(mEffect); } void EditEffectDialog::setAttribute (int attribute) { mEffect.mAttribute = attribute; eventEffectModified(mEffect); } void EditEffectDialog::onMagnitudeMinChanged (MyGUI::ScrollBar* sender, size_t pos) { mMagnitudeMinValue->setCaption(MyGUI::utility::toString(pos+1)); mEffect.mMagnMin = pos+1; // trigger the check again (see below) onMagnitudeMaxChanged(mMagnitudeMaxSlider, mMagnitudeMaxSlider->getScrollPosition ()); eventEffectModified(mEffect); } void EditEffectDialog::onMagnitudeMaxChanged (MyGUI::ScrollBar* sender, size_t pos) { // make sure the max value is actually larger or equal than the min value size_t magnMin = std::abs(mEffect.mMagnMin); // should never be < 0, this is just here to avoid the compiler warning if (pos+1 < magnMin) { pos = mEffect.mMagnMin-1; sender->setScrollPosition (pos); } mEffect.mMagnMax = pos+1; const std::string to = MWBase::Environment::get().getWindowManager()->getGameSettingString("sTo", "-"); mMagnitudeMaxValue->setCaption(to + " " + MyGUI::utility::toString(pos+1)); eventEffectModified(mEffect); } void EditEffectDialog::onDurationChanged (MyGUI::ScrollBar* sender, size_t pos) { mDurationValue->setCaption(MyGUI::utility::toString(pos+1)); mEffect.mDuration = pos+1; eventEffectModified(mEffect); } void EditEffectDialog::onAreaChanged (MyGUI::ScrollBar* sender, size_t pos) { mAreaValue->setCaption(MyGUI::utility::toString(pos)); mEffect.mArea = pos; eventEffectModified(mEffect); } // ------------------------------------------------------------------------------------------------ SpellCreationDialog::SpellCreationDialog() : WindowBase("openmw_spellcreation_dialog.layout") , EffectEditorBase(EffectEditorBase::Spellmaking) { getWidget(mNameEdit, "NameEdit"); getWidget(mMagickaCost, "MagickaCost"); getWidget(mSuccessChance, "SuccessChance"); getWidget(mAvailableEffectsList, "AvailableEffects"); getWidget(mUsedEffectsView, "UsedEffects"); getWidget(mPriceLabel, "PriceLabel"); getWidget(mBuyButton, "BuyButton"); getWidget(mCancelButton, "CancelButton"); mCancelButton->eventMouseButtonClick += MyGUI::newDelegate(this, &SpellCreationDialog::onCancelButtonClicked); mBuyButton->eventMouseButtonClick += MyGUI::newDelegate(this, &SpellCreationDialog::onBuyButtonClicked); mNameEdit->eventEditSelectAccept += MyGUI::newDelegate(this, &SpellCreationDialog::onAccept); setWidgets(mAvailableEffectsList, mUsedEffectsView); } void SpellCreationDialog::setPtr (const MWWorld::Ptr& actor) { mPtr = actor; mNameEdit->setCaption(""); startEditing(); } void SpellCreationDialog::onCancelButtonClicked (MyGUI::Widget* sender) { MWBase::Environment::get().getWindowManager()->removeGuiMode (MWGui::GM_SpellCreation); } void SpellCreationDialog::onBuyButtonClicked (MyGUI::Widget* sender) { if (mEffects.size() <= 0) { MWBase::Environment::get().getWindowManager()->messageBox ("#{sNotifyMessage30}"); return; } if (mNameEdit->getCaption () == "") { MWBase::Environment::get().getWindowManager()->messageBox ("#{sNotifyMessage10}"); return; } if (mMagickaCost->getCaption() == "0") { MWBase::Environment::get().getWindowManager()->messageBox ("#{sEnchantmentMenu8}"); return; } MWWorld::Ptr player = MWMechanics::getPlayer(); int playerGold = player.getClass().getContainerStore(player).count(MWWorld::ContainerStore::sGoldId); int price = MyGUI::utility::parseInt(mPriceLabel->getCaption()); if (price > playerGold) { MWBase::Environment::get().getWindowManager()->messageBox ("#{sNotifyMessage18}"); return; } mSpell.mName = mNameEdit->getCaption(); player.getClass().getContainerStore(player).remove(MWWorld::ContainerStore::sGoldId, price, player); // add gold to NPC trading gold pool MWMechanics::CreatureStats& npcStats = mPtr.getClass().getCreatureStats(mPtr); /* Start of tes3mp change (major) Don't unilaterally change the merchant's gold pool on our client and instead let the server do it */ //npcStats.setGoldPool(npcStats.getGoldPool() + price); mwmp::ObjectList* objectList = mwmp::Main::get().getNetworking()->getObjectList(); objectList->reset(); objectList->packetOrigin = mwmp::CLIENT_GAMEPLAY; objectList->addObjectMiscellaneous(mPtr, npcStats.getGoldPool() + price, npcStats.getLastRestockTime().getHour(), npcStats.getLastRestockTime().getDay()); objectList->sendObjectMiscellaneous(); /* End of tes3mp change (major) */ MWBase::Environment::get().getWindowManager()->playSound ("Mysticism Hit"); /* Start of tes3mp change (major) Don't create a record and don't add the spell to the player's spellbook; instead just send its record to the server and expect the server to add it to the player's spellbook */ /* const ESM::Spell* spell = MWBase::Environment::get().getWorld()->createRecord(mSpell); MWMechanics::CreatureStats& stats = player.getClass().getCreatureStats(player); MWMechanics::Spells& spells = stats.getSpells(); spells.add(spell->mId); */ mwmp::Main::get().getNetworking()->getWorldstate()->sendSpellRecord(&mSpell); /* End of tes3mp addition */ MWBase::Environment::get().getWindowManager()->removeGuiMode (GM_SpellCreation); } void SpellCreationDialog::onAccept(MyGUI::EditBox *sender) { onBuyButtonClicked(sender); // To do not spam onAccept() again and again MWBase::Environment::get().getWindowManager()->injectKeyRelease(MyGUI::KeyCode::None); } void SpellCreationDialog::onOpen() { center(); MWBase::Environment::get().getWindowManager()->setKeyFocusWidget(mNameEdit); } void SpellCreationDialog::onReferenceUnavailable () { MWBase::Environment::get().getWindowManager()->removeGuiMode (GM_Dialogue); MWBase::Environment::get().getWindowManager()->removeGuiMode (GM_SpellCreation); } void SpellCreationDialog::notifyEffectsChanged () { if (mEffects.empty()) { mMagickaCost->setCaption("0"); mPriceLabel->setCaption("0"); mSuccessChance->setCaption("0"); return; } float y = 0; const MWWorld::ESMStore &store = MWBase::Environment::get().getWorld()->getStore(); for (const ESM::ENAMstruct& effect : mEffects) { y += std::max(1.f, MWMechanics::calcEffectCost(effect)); if (effect.mRange == ESM::RT_Target) y *= 1.5; } ESM::EffectList effectList; effectList.mList = mEffects; mSpell.mEffects = effectList; mSpell.mData.mCost = int(y); mSpell.mData.mType = ESM::Spell::ST_Spell; mSpell.mData.mFlags = 0; mMagickaCost->setCaption(MyGUI::utility::toString(int(y))); float fSpellMakingValueMult = store.get().find("fSpellMakingValueMult")->mValue.getFloat(); int price = std::max(1, static_cast(y * fSpellMakingValueMult)); price = MWBase::Environment::get().getMechanicsManager()->getBarterOffer(mPtr, price, true); mPriceLabel->setCaption(MyGUI::utility::toString(int(price))); float chance = MWMechanics::calcSpellBaseSuccessChance(&mSpell, MWMechanics::getPlayer(), nullptr); int intChance = std::min(100, int(chance)); mSuccessChance->setCaption(MyGUI::utility::toString(intChance)); } // ------------------------------------------------------------------------------------------------ EffectEditorBase::EffectEditorBase(Type type) : mAvailableEffectsList(nullptr) , mUsedEffectsView(nullptr) , mAddEffectDialog() , mSelectAttributeDialog(nullptr) , mSelectSkillDialog(nullptr) , mSelectedEffect(0) , mSelectedKnownEffectId(0) , mConstantEffect(false) , mType(type) { mAddEffectDialog.eventEffectAdded += MyGUI::newDelegate(this, &EffectEditorBase::onEffectAdded); mAddEffectDialog.eventEffectModified += MyGUI::newDelegate(this, &EffectEditorBase::onEffectModified); mAddEffectDialog.eventEffectRemoved += MyGUI::newDelegate(this, &EffectEditorBase::onEffectRemoved); mAddEffectDialog.setVisible (false); } EffectEditorBase::~EffectEditorBase() { } void EffectEditorBase::startEditing () { // get the list of magic effects that are known to the player MWWorld::Ptr player = MWMechanics::getPlayer(); MWMechanics::CreatureStats& stats = player.getClass().getCreatureStats(player); MWMechanics::Spells& spells = stats.getSpells(); std::vector knownEffects; for (MWMechanics::Spells::TIterator it = spells.begin(); it != spells.end(); ++it) { const ESM::Spell* spell = it->first; // only normal spells count if (spell->mData.mType != ESM::Spell::ST_Spell) continue; for (const ESM::ENAMstruct& effectInfo : spell->mEffects.mList) { const ESM::MagicEffect * effect = MWBase::Environment::get().getWorld()->getStore().get().find(effectInfo.mEffectID); // skip effects that do not allow spellmaking/enchanting int requiredFlags = (mType == Spellmaking) ? ESM::MagicEffect::AllowSpellmaking : ESM::MagicEffect::AllowEnchanting; if (!(effect->mData.mFlags & requiredFlags)) continue; if (std::find(knownEffects.begin(), knownEffects.end(), effectInfo.mEffectID) == knownEffects.end()) knownEffects.push_back(effectInfo.mEffectID); } } std::sort(knownEffects.begin(), knownEffects.end(), sortMagicEffects); mAvailableEffectsList->clear (); int i=0; for (const short effectId : knownEffects) { mAvailableEffectsList->addItem(MWBase::Environment::get().getWorld ()->getStore ().get().find( ESM::MagicEffect::effectIdToString(effectId))->mValue.getString()); mButtonMapping[i] = effectId; ++i; } mAvailableEffectsList->adjustSize (); mAvailableEffectsList->scrollToTop(); for (const short effectId : knownEffects) { std::string name = MWBase::Environment::get().getWorld ()->getStore ().get().find( ESM::MagicEffect::effectIdToString(effectId))->mValue.getString(); MyGUI::Widget* w = mAvailableEffectsList->getItemWidget(name); ToolTips::createMagicEffectToolTip (w, effectId); } mEffects.clear(); updateEffectsView (); } void EffectEditorBase::setWidgets (Gui::MWList *availableEffectsList, MyGUI::ScrollView *usedEffectsView) { mAvailableEffectsList = availableEffectsList; mUsedEffectsView = usedEffectsView; mAvailableEffectsList->eventWidgetSelected += MyGUI::newDelegate(this, &EffectEditorBase::onAvailableEffectClicked); } void EffectEditorBase::onSelectAttribute () { const ESM::MagicEffect* effect = MWBase::Environment::get().getWorld()->getStore().get().find(mSelectedKnownEffectId); mAddEffectDialog.newEffect(effect); mAddEffectDialog.setAttribute (mSelectAttributeDialog->getAttributeId()); MWBase::Environment::get().getWindowManager ()->removeDialog (mSelectAttributeDialog); mSelectAttributeDialog = nullptr; } void EffectEditorBase::onSelectSkill () { const ESM::MagicEffect* effect = MWBase::Environment::get().getWorld()->getStore().get().find(mSelectedKnownEffectId); mAddEffectDialog.newEffect(effect); mAddEffectDialog.setSkill (mSelectSkillDialog->getSkillId()); MWBase::Environment::get().getWindowManager ()->removeDialog (mSelectSkillDialog); mSelectSkillDialog = nullptr; } void EffectEditorBase::onAttributeOrSkillCancel () { if (mSelectSkillDialog) MWBase::Environment::get().getWindowManager ()->removeDialog (mSelectSkillDialog); if (mSelectAttributeDialog) MWBase::Environment::get().getWindowManager ()->removeDialog (mSelectAttributeDialog); mSelectSkillDialog = nullptr; mSelectAttributeDialog = nullptr; } void EffectEditorBase::onAvailableEffectClicked (MyGUI::Widget* sender) { if (mEffects.size() >= 8) { MWBase::Environment::get().getWindowManager()->messageBox("#{sNotifyMessage28}"); return; } int buttonId = *sender->getUserData(); mSelectedKnownEffectId = mButtonMapping[buttonId]; const ESM::MagicEffect* effect = MWBase::Environment::get().getWorld()->getStore().get().find(mSelectedKnownEffectId); if (effect->mData.mFlags & ESM::MagicEffect::TargetSkill) { delete mSelectSkillDialog; mSelectSkillDialog = new SelectSkillDialog(); mSelectSkillDialog->eventCancel += MyGUI::newDelegate(this, &SpellCreationDialog::onAttributeOrSkillCancel); mSelectSkillDialog->eventItemSelected += MyGUI::newDelegate(this, &SpellCreationDialog::onSelectSkill); mSelectSkillDialog->setVisible (true); } else if (effect->mData.mFlags & ESM::MagicEffect::TargetAttribute) { delete mSelectAttributeDialog; mSelectAttributeDialog = new SelectAttributeDialog(); mSelectAttributeDialog->eventCancel += MyGUI::newDelegate(this, &SpellCreationDialog::onAttributeOrSkillCancel); mSelectAttributeDialog->eventItemSelected += MyGUI::newDelegate(this, &SpellCreationDialog::onSelectAttribute); mSelectAttributeDialog->setVisible (true); } else { for (const ESM::ENAMstruct& effectInfo : mEffects) { if (effectInfo.mEffectID == mSelectedKnownEffectId) { MWBase::Environment::get().getWindowManager()->messageBox ("#{sOnetypeEffectMessage}"); return; } } mAddEffectDialog.newEffect(effect); } } void EffectEditorBase::onEffectModified (ESM::ENAMstruct effect) { mEffects[mSelectedEffect] = effect; updateEffectsView(); } void EffectEditorBase::onEffectRemoved (ESM::ENAMstruct effect) { mEffects.erase(mEffects.begin() + mSelectedEffect); updateEffectsView(); } void EffectEditorBase::updateEffectsView () { MyGUI::EnumeratorWidgetPtr oldWidgets = mUsedEffectsView->getEnumerator (); MyGUI::Gui::getInstance ().destroyWidgets (oldWidgets); MyGUI::IntSize size(0,0); int i = 0; for (const ESM::ENAMstruct& effectInfo : mEffects) { Widgets::SpellEffectParams params; params.mEffectID = effectInfo.mEffectID; params.mSkill = effectInfo.mSkill; params.mAttribute = effectInfo.mAttribute; params.mDuration = effectInfo.mDuration; params.mMagnMin = effectInfo.mMagnMin; params.mMagnMax = effectInfo.mMagnMax; params.mRange = effectInfo.mRange; params.mArea = effectInfo.mArea; params.mIsConstant = mConstantEffect; MyGUI::Button* button = mUsedEffectsView->createWidget("", MyGUI::IntCoord(0, size.height, 0, 24), MyGUI::Align::Default); button->setUserData(i); button->eventMouseButtonClick += MyGUI::newDelegate(this, &SpellCreationDialog::onEditEffect); button->setNeedMouseFocus (true); Widgets::MWSpellEffectPtr effect = button->createWidget("MW_EffectImage", MyGUI::IntCoord(0,0,0,24), MyGUI::Align::Default); effect->setNeedMouseFocus (false); effect->setSpellEffect (params); effect->setSize(effect->getRequestedWidth (), 24); button->setSize(effect->getRequestedWidth (), 24); size.width = std::max(size.width, effect->getRequestedWidth ()); size.height += 24; ++i; } // Canvas size must be expressed with HScroll disabled, otherwise MyGUI would expand the scroll area when the scrollbar is hidden mUsedEffectsView->setVisibleHScroll(false); mUsedEffectsView->setCanvasSize(size); mUsedEffectsView->setVisibleHScroll(true); notifyEffectsChanged(); } void EffectEditorBase::onEffectAdded (ESM::ENAMstruct effect) { mEffects.push_back(effect); mSelectedEffect=mEffects.size()-1; updateEffectsView(); } void EffectEditorBase::onEditEffect (MyGUI::Widget *sender) { int id = *sender->getUserData(); mSelectedEffect = id; mAddEffectDialog.editEffect (mEffects[id]); mAddEffectDialog.setVisible (true); } void EffectEditorBase::setConstantEffect(bool constant) { mAddEffectDialog.setConstantEffect(constant); mConstantEffect = constant; if (!constant) return; for (auto it = mEffects.begin(); it != mEffects.end();) { if (it->mRange != ESM::RT_Self) { auto& store = MWBase::Environment::get().getWorld()->getStore(); auto magicEffect = store.get().find(it->mEffectID); if ((magicEffect->mData.mFlags & ESM::MagicEffect::CastSelf) == 0) { it = mEffects.erase(it); continue; } it->mRange = ESM::RT_Self; } ++it; } } } ================================================ FILE: apps/openmw/mwgui/spellcreationdialog.hpp ================================================ #ifndef MWGUI_SPELLCREATION_H #define MWGUI_SPELLCREATION_H #include #include #include "windowbase.hpp" #include "referenceinterface.hpp" namespace Gui { class MWList; } namespace MWGui { class SelectSkillDialog; class SelectAttributeDialog; class EditEffectDialog : public WindowModal { public: EditEffectDialog(); void onOpen() override; bool exit() override; void setConstantEffect(bool constant); void setSkill(int skill); void setAttribute(int attribute); void newEffect (const ESM::MagicEffect* effect); void editEffect (ESM::ENAMstruct effect); typedef MyGUI::delegates::CMultiDelegate1 EventHandle_Effect; EventHandle_Effect eventEffectAdded; EventHandle_Effect eventEffectModified; EventHandle_Effect eventEffectRemoved; protected: MyGUI::Button* mCancelButton; MyGUI::Button* mOkButton; MyGUI::Button* mDeleteButton; MyGUI::Button* mRangeButton; MyGUI::Widget* mDurationBox; MyGUI::Widget* mMagnitudeBox; MyGUI::Widget* mAreaBox; MyGUI::TextBox* mMagnitudeMinValue; MyGUI::TextBox* mMagnitudeMaxValue; MyGUI::TextBox* mDurationValue; MyGUI::TextBox* mAreaValue; MyGUI::ScrollBar* mMagnitudeMinSlider; MyGUI::ScrollBar* mMagnitudeMaxSlider; MyGUI::ScrollBar* mDurationSlider; MyGUI::ScrollBar* mAreaSlider; MyGUI::TextBox* mAreaText; MyGUI::ImageBox* mEffectImage; MyGUI::TextBox* mEffectName; bool mEditing; protected: void onRangeButtonClicked (MyGUI::Widget* sender); void onDeleteButtonClicked (MyGUI::Widget* sender); void onOkButtonClicked (MyGUI::Widget* sender); void onCancelButtonClicked (MyGUI::Widget* sender); void onMagnitudeMinChanged (MyGUI::ScrollBar* sender, size_t pos); void onMagnitudeMaxChanged (MyGUI::ScrollBar* sender, size_t pos); void onDurationChanged (MyGUI::ScrollBar* sender, size_t pos); void onAreaChanged (MyGUI::ScrollBar* sender, size_t pos); void setMagicEffect(const ESM::MagicEffect* effect); void updateBoxes(); protected: ESM::ENAMstruct mEffect; ESM::ENAMstruct mOldEffect; const ESM::MagicEffect* mMagicEffect; bool mConstantEffect; }; class EffectEditorBase { public: enum Type { Spellmaking, Enchanting }; EffectEditorBase(Type type); virtual ~EffectEditorBase(); void setConstantEffect(bool constant); protected: std::map mButtonMapping; // maps button ID to effect ID Gui::MWList* mAvailableEffectsList; MyGUI::ScrollView* mUsedEffectsView; EditEffectDialog mAddEffectDialog; SelectAttributeDialog* mSelectAttributeDialog; SelectSkillDialog* mSelectSkillDialog; int mSelectedEffect; short mSelectedKnownEffectId; bool mConstantEffect; std::vector mEffects; void onEffectAdded(ESM::ENAMstruct effect); void onEffectModified(ESM::ENAMstruct effect); void onEffectRemoved(ESM::ENAMstruct effect); void onAvailableEffectClicked (MyGUI::Widget* sender); void onAttributeOrSkillCancel(); void onSelectAttribute(); void onSelectSkill(); void onEditEffect(MyGUI::Widget* sender); void updateEffectsView(); void startEditing(); void setWidgets (Gui::MWList* availableEffectsList, MyGUI::ScrollView* usedEffectsView); virtual void notifyEffectsChanged () {} private: Type mType; }; class SpellCreationDialog : public WindowBase, public ReferenceInterface, public EffectEditorBase { public: SpellCreationDialog(); void onOpen() override; void clear() override { resetReference(); } void onFrame(float dt) override { checkReferenceAvailable(); } void setPtr(const MWWorld::Ptr& actor) override; protected: void onReferenceUnavailable() override; void onCancelButtonClicked (MyGUI::Widget* sender); void onBuyButtonClicked (MyGUI::Widget* sender); void onAccept(MyGUI::EditBox* sender); void notifyEffectsChanged() override; MyGUI::EditBox* mNameEdit; MyGUI::TextBox* mMagickaCost; MyGUI::TextBox* mSuccessChance; MyGUI::Button* mBuyButton; MyGUI::Button* mCancelButton; MyGUI::TextBox* mPriceLabel; ESM::Spell mSpell; }; } #endif ================================================ FILE: apps/openmw/mwgui/spellicons.cpp ================================================ #include "spellicons.hpp" #include #include #include #include #include #include "../mwbase/world.hpp" #include "../mwbase/environment.hpp" #include "../mwbase/windowmanager.hpp" #include "../mwworld/class.hpp" #include "../mwworld/esmstore.hpp" #include "../mwworld/inventorystore.hpp" #include "../mwmechanics/creaturestats.hpp" #include "../mwmechanics/actorutil.hpp" #include "tooltips.hpp" namespace MWGui { void EffectSourceVisitor::visit (MWMechanics::EffectKey key, int effectIndex, const std::string& sourceName, const std::string& sourceId, int casterActorId, float magnitude, float remainingTime, float totalTime) { MagicEffectInfo newEffectSource; newEffectSource.mKey = key; newEffectSource.mMagnitude = static_cast(magnitude); newEffectSource.mPermanent = mIsPermanent; newEffectSource.mRemainingTime = remainingTime; newEffectSource.mSource = sourceName; newEffectSource.mTotalTime = totalTime; mEffectSources[key.mId].push_back(newEffectSource); } void SpellIcons::updateWidgets(MyGUI::Widget *parent, bool adjustSize) { // TODO: Tracking add/remove/expire would be better than force updating every frame MWWorld::Ptr player = MWMechanics::getPlayer(); const MWMechanics::CreatureStats& stats = player.getClass().getCreatureStats(player); EffectSourceVisitor visitor; // permanent item enchantments & permanent spells visitor.mIsPermanent = true; MWWorld::InventoryStore& store = player.getClass().getInventoryStore(player); store.visitEffectSources(visitor); stats.getSpells().visitEffectSources(visitor); // now add lasting effects visitor.mIsPermanent = false; stats.getActiveSpells().visitEffectSources(visitor); std::map >& effects = visitor.mEffectSources; int w=2; for (auto& effectInfoPair : effects) { const int effectId = effectInfoPair.first; const ESM::MagicEffect* effect = MWBase::Environment::get().getWorld ()->getStore ().get().find(effectId); float remainingDuration = 0; float totalDuration = 0; std::string sourcesDescription; static const float fadeTime = MWBase::Environment::get().getWorld()->getStore().get().find("fMagicStartIconBlink")->mValue.getFloat(); std::vector& effectInfos = effectInfoPair.second; bool addNewLine = false; for (const MagicEffectInfo& effectInfo : effectInfos) { if (addNewLine) sourcesDescription += "\n"; // if at least one of the effect sources is permanent, the effect will never wear off if (effectInfo.mPermanent) { remainingDuration = fadeTime; totalDuration = fadeTime; } else { remainingDuration = std::max(remainingDuration, effectInfo.mRemainingTime); totalDuration = std::max(totalDuration, effectInfo.mTotalTime); } sourcesDescription += effectInfo.mSource; if (effect->mData.mFlags & ESM::MagicEffect::TargetSkill) sourcesDescription += " (" + MWBase::Environment::get().getWindowManager()->getGameSettingString( ESM::Skill::sSkillNameIds[effectInfo.mKey.mArg], "") + ")"; if (effect->mData.mFlags & ESM::MagicEffect::TargetAttribute) sourcesDescription += " (" + MWBase::Environment::get().getWindowManager()->getGameSettingString( ESM::Attribute::sGmstAttributeIds[effectInfo.mKey.mArg], "") + ")"; ESM::MagicEffect::MagnitudeDisplayType displayType = effect->getMagnitudeDisplayType(); if (displayType == ESM::MagicEffect::MDT_TimesInt) { std::string timesInt = MWBase::Environment::get().getWindowManager()->getGameSettingString("sXTimesINT", ""); std::stringstream formatter; formatter << std::fixed << std::setprecision(1) << " " << (effectInfo.mMagnitude / 10.0f) << timesInt; sourcesDescription += formatter.str(); } else if ( displayType != ESM::MagicEffect::MDT_None ) { sourcesDescription += ": " + MyGUI::utility::toString(effectInfo.mMagnitude); if ( displayType == ESM::MagicEffect::MDT_Percentage ) sourcesDescription += MWBase::Environment::get().getWindowManager()->getGameSettingString("spercent", ""); else if ( displayType == ESM::MagicEffect::MDT_Feet ) sourcesDescription += " " + MWBase::Environment::get().getWindowManager()->getGameSettingString("sfeet", ""); else if ( displayType == ESM::MagicEffect::MDT_Level ) { sourcesDescription += " " + ((effectInfo.mMagnitude > 1) ? MWBase::Environment::get().getWindowManager()->getGameSettingString("sLevels", "") : MWBase::Environment::get().getWindowManager()->getGameSettingString("sLevel", "") ); } else // ESM::MagicEffect::MDT_Points { sourcesDescription += " " + ((effectInfo.mMagnitude > 1) ? MWBase::Environment::get().getWindowManager()->getGameSettingString("spoints", "") : MWBase::Environment::get().getWindowManager()->getGameSettingString("spoint", "") ); } } if (effectInfo.mRemainingTime > -1 && Settings::Manager::getBool("show effect duration","Game")) sourcesDescription += MWGui::ToolTips::getDurationString(effectInfo.mRemainingTime, " #{sDuration}"); addNewLine = true; } if (remainingDuration > 0.f) { MyGUI::ImageBox* image; if (mWidgetMap.find(effectId) == mWidgetMap.end()) { image = parent->createWidget ("ImageBox", MyGUI::IntCoord(w,2,16,16), MyGUI::Align::Default); mWidgetMap[effectId] = image; image->setImageTexture(MWBase::Environment::get().getWindowManager()->correctIconPath(effect->mIcon)); std::string name = ESM::MagicEffect::effectIdToString (effectId); ToolTipInfo tooltipInfo; tooltipInfo.caption = "#{" + name + "}"; tooltipInfo.icon = effect->mIcon; tooltipInfo.imageSize = 16; tooltipInfo.wordWrap = false; image->setUserData(tooltipInfo); image->setUserString("ToolTipType", "ToolTipInfo"); } else image = mWidgetMap[effectId]; image->setPosition(w,2); image->setVisible(true); w += 16; ToolTipInfo* tooltipInfo = image->getUserData(); tooltipInfo->text = sourcesDescription; // Fade out if (totalDuration >= fadeTime && fadeTime > 0.f) image->setAlpha(std::min(remainingDuration/fadeTime, 1.f)); } else if (mWidgetMap.find(effectId) != mWidgetMap.end()) { MyGUI::ImageBox* image = mWidgetMap[effectId]; image->setVisible(false); image->setAlpha(1.f); } } if (adjustSize) { int s = w + 2; if (effects.empty()) s = 0; int diff = parent->getWidth() - s; parent->setSize(s, parent->getHeight()); parent->setPosition(parent->getLeft()+diff, parent->getTop()); } // hide inactive effects for (auto& widgetPair : mWidgetMap) { if (effects.find(widgetPair.first) == effects.end()) widgetPair.second->setVisible(false); } } } ================================================ FILE: apps/openmw/mwgui/spellicons.hpp ================================================ #ifndef MWGUI_SPELLICONS_H #define MWGUI_SPELLICONS_H #include #include #include "../mwmechanics/magiceffects.hpp" namespace MyGUI { class Widget; class ImageBox; } namespace ESM { struct ENAMstruct; struct EffectList; } namespace MWGui { // information about a single magic effect source as required for display in the tooltip struct MagicEffectInfo { MagicEffectInfo() : mMagnitude(0) , mRemainingTime(0.f) , mTotalTime(0.f) , mPermanent(false) {} std::string mSource; // display name for effect source (e.g. potion name) MWMechanics::EffectKey mKey; int mMagnitude; float mRemainingTime; float mTotalTime; bool mPermanent; // the effect is permanent }; class EffectSourceVisitor : public MWMechanics::EffectSourceVisitor { public: bool mIsPermanent; std::map > mEffectSources; virtual ~EffectSourceVisitor() {} void visit (MWMechanics::EffectKey key, int effectIndex, const std::string& sourceName, const std::string& sourceId, int casterActorId, float magnitude, float remainingTime = -1, float totalTime = -1) override; }; class SpellIcons { public: void updateWidgets(MyGUI::Widget* parent, bool adjustSize); private: std::map mWidgetMap; }; } #endif ================================================ FILE: apps/openmw/mwgui/spellmodel.cpp ================================================ #include "spellmodel.hpp" #include #include "../mwbase/environment.hpp" #include "../mwbase/world.hpp" #include "../mwbase/windowmanager.hpp" #include "../mwmechanics/creaturestats.hpp" #include "../mwmechanics/spellutil.hpp" #include "../mwworld/esmstore.hpp" #include "../mwworld/inventorystore.hpp" #include "../mwworld/class.hpp" namespace { bool sortSpells(const MWGui::Spell& left, const MWGui::Spell& right) { if (left.mType != right.mType) return left.mType < right.mType; std::string leftName = Misc::StringUtils::lowerCase(left.mName); std::string rightName = Misc::StringUtils::lowerCase(right.mName); return leftName.compare(rightName) < 0; } } namespace MWGui { SpellModel::SpellModel(const MWWorld::Ptr &actor, const std::string& filter) : mActor(actor), mFilter(filter) { } SpellModel::SpellModel(const MWWorld::Ptr &actor) : mActor(actor) { } bool SpellModel::matchingEffectExists(std::string filter, const ESM::EffectList &effects) { auto wm = MWBase::Environment::get().getWindowManager(); const MWWorld::ESMStore &store = MWBase::Environment::get().getWorld()->getStore(); for (const auto& effect : effects.mList) { short effectId = effect.mEffectID; if (effectId != -1) { const ESM::MagicEffect *magicEffect = store.get().search(effectId); std::string effectIDStr = ESM::MagicEffect::effectIdToString(effectId); std::string fullEffectName = wm->getGameSettingString(effectIDStr, ""); if (magicEffect->mData.mFlags & ESM::MagicEffect::TargetSkill && effect.mSkill != -1) { fullEffectName += " " + wm->getGameSettingString(ESM::Skill::sSkillNameIds[effect.mSkill], ""); } if (magicEffect->mData.mFlags & ESM::MagicEffect::TargetAttribute && effect.mAttribute != -1) { fullEffectName += " " + wm->getGameSettingString(ESM::Attribute::sGmstAttributeIds[effect.mAttribute], ""); } std::string convert = Misc::StringUtils::lowerCaseUtf8(fullEffectName); if (convert.find(filter) != std::string::npos) { return true; } } } return false; } void SpellModel::update() { mSpells.clear(); MWMechanics::CreatureStats& stats = mActor.getClass().getCreatureStats(mActor); const MWMechanics::Spells& spells = stats.getSpells(); const MWWorld::ESMStore &esmStore = MWBase::Environment::get().getWorld()->getStore(); std::string filter = Misc::StringUtils::lowerCaseUtf8(mFilter); for (MWMechanics::Spells::TIterator it = spells.begin(); it != spells.end(); ++it) { const ESM::Spell* spell = it->first; if (spell->mData.mType != ESM::Spell::ST_Power && spell->mData.mType != ESM::Spell::ST_Spell) continue; std::string name = Misc::StringUtils::lowerCaseUtf8(spell->mName); if (name.find(filter) == std::string::npos && !matchingEffectExists(filter, spell->mEffects)) continue; Spell newSpell; newSpell.mName = spell->mName; if (spell->mData.mType == ESM::Spell::ST_Spell) { newSpell.mType = Spell::Type_Spell; std::string cost = std::to_string(spell->mData.mCost); std::string chance = std::to_string(int(MWMechanics::getSpellSuccessChance(spell, mActor))); newSpell.mCostColumn = cost + "/" + chance; } else newSpell.mType = Spell::Type_Power; newSpell.mId = spell->mId; newSpell.mSelected = (MWBase::Environment::get().getWindowManager()->getSelectedSpell() == spell->mId); newSpell.mActive = true; newSpell.mCount = 1; mSpells.push_back(newSpell); } MWWorld::InventoryStore& invStore = mActor.getClass().getInventoryStore(mActor); for (MWWorld::ContainerStoreIterator it = invStore.begin(); it != invStore.end(); ++it) { MWWorld::Ptr item = *it; const std::string enchantId = item.getClass().getEnchantment(item); if (enchantId.empty()) continue; const ESM::Enchantment* enchant = esmStore.get().search(enchantId); if (!enchant) { Log(Debug::Warning) << "Warning: Can't find enchantment '" << enchantId << "' on item " << item.getCellRef().getRefId(); continue; } if (enchant->mData.mType != ESM::Enchantment::WhenUsed && enchant->mData.mType != ESM::Enchantment::CastOnce) continue; std::string name = Misc::StringUtils::lowerCaseUtf8(item.getClass().getName(item)); if (name.find(filter) == std::string::npos && !matchingEffectExists(filter, enchant->mEffects)) continue; Spell newSpell; newSpell.mItem = item; newSpell.mId = item.getCellRef().getRefId(); newSpell.mName = item.getClass().getName(item); newSpell.mCount = item.getRefData().getCount(); newSpell.mType = Spell::Type_EnchantedItem; newSpell.mSelected = invStore.getSelectedEnchantItem() == it; // FIXME: move to mwmechanics if (enchant->mData.mType == ESM::Enchantment::CastOnce) { newSpell.mCostColumn = "100/100"; newSpell.mActive = false; } else { if (!item.getClass().getEquipmentSlots(item).first.empty() && item.getClass().canBeEquipped(item, mActor).first == 0) continue; int castCost = MWMechanics::getEffectiveEnchantmentCastCost(static_cast(enchant->mData.mCost), mActor); std::string cost = std::to_string(castCost); int currentCharge = int(item.getCellRef().getEnchantmentCharge()); if (currentCharge == -1) currentCharge = enchant->mData.mCharge; std::string charge = std::to_string(currentCharge); newSpell.mCostColumn = cost + "/" + charge; newSpell.mActive = invStore.isEquipped(item); } mSpells.push_back(newSpell); } std::stable_sort(mSpells.begin(), mSpells.end(), sortSpells); } size_t SpellModel::getItemCount() const { return mSpells.size(); } SpellModel::ModelIndex SpellModel::getSelectedIndex() const { ModelIndex selected = -1; for (SpellModel::ModelIndex i = 0; i= int(mSpells.size())) throw std::runtime_error("invalid spell index supplied"); return mSpells[index]; } } ================================================ FILE: apps/openmw/mwgui/spellmodel.hpp ================================================ #ifndef OPENMW_GUI_SPELLMODEL_H #define OPENMW_GUI_SPELLMODEL_H #include "../mwworld/ptr.hpp" #include namespace MWGui { struct Spell { enum Type { Type_Power, Type_Spell, Type_EnchantedItem }; Type mType; std::string mName; std::string mCostColumn; // Cost/chance or Cost/charge std::string mId; // Item ID or spell ID MWWorld::Ptr mItem; // Only for Type_EnchantedItem int mCount; // Only for Type_EnchantedItem bool mSelected; // Is this the currently selected spell/item (only one can be selected at a time) bool mActive; // (Items only) is the item equipped? Spell() : mType(Type_Spell) , mCount(0) , mSelected(false) , mActive(false) { } }; ///@brief Model that lists all usable powers, spells and enchanted items for an actor. class SpellModel { public: SpellModel(const MWWorld::Ptr& actor, const std::string& filter); SpellModel(const MWWorld::Ptr& actor); typedef int ModelIndex; void update(); Spell getItem (ModelIndex index) const; ///< throws for invalid index size_t getItemCount() const; ModelIndex getSelectedIndex() const; ///< returns -1 if nothing is selected private: MWWorld::Ptr mActor; std::vector mSpells; std::string mFilter; bool matchingEffectExists(std::string filter, const ESM::EffectList &effects); }; } #endif ================================================ FILE: apps/openmw/mwgui/spellview.cpp ================================================ #include "spellview.hpp" #include #include #include #include #include #include #include "tooltips.hpp" namespace MWGui { const char* SpellView::sSpellModelIndex = "SpellModelIndex"; SpellView::LineInfo::LineInfo(MyGUI::Widget* leftWidget, MyGUI::Widget* rightWidget, SpellModel::ModelIndex spellIndex) : mLeftWidget(leftWidget) , mRightWidget(rightWidget) , mSpellIndex(spellIndex) { } SpellView::SpellView() : mScrollView(nullptr) , mShowCostColumn(true) , mHighlightSelected(true) { } void SpellView::initialiseOverride() { Base::initialiseOverride(); assignWidget(mScrollView, "ScrollView"); if (mScrollView == nullptr) throw std::runtime_error("Item view needs a scroll view"); mScrollView->setCanvasAlign(MyGUI::Align::Left | MyGUI::Align::Top); } void SpellView::registerComponents() { MyGUI::FactoryManager::getInstance().registerFactory("Widget"); } void SpellView::setModel(SpellModel *model) { mModel.reset(model); update(); } SpellModel* SpellView::getModel() { return mModel.get(); } void SpellView::setShowCostColumn(bool show) { if (show != mShowCostColumn) { mShowCostColumn = show; update(); } } void SpellView::setHighlightSelected(bool highlight) { if (highlight != mHighlightSelected) { mHighlightSelected = highlight; update(); } } void SpellView::update() { if (!mModel.get()) return; mModel->update(); int curType = -1; const int spellHeight = 18; mLines.clear(); while (mScrollView->getChildCount()) MyGUI::Gui::getInstance().destroyWidget(mScrollView->getChildAt(0)); for (SpellModel::ModelIndex i = 0; igetItemCount()); ++i) { const Spell& spell = mModel->getItem(i); if (curType != spell.mType) { if (spell.mType == Spell::Type_Power) addGroup("#{sPowers}", ""); else if (spell.mType == Spell::Type_Spell) addGroup("#{sSpells}", mShowCostColumn ? "#{sCostChance}" : ""); else addGroup("#{sMagicItem}", mShowCostColumn ? "#{sCostCharge}" : ""); curType = spell.mType; } const std::string skin = spell.mActive ? "SandTextButton" : "SpellTextUnequipped"; const std::string captionSuffix = MWGui::ToolTips::getCountString(spell.mCount); Gui::SharedStateButton* t = mScrollView->createWidget(skin, MyGUI::IntCoord(0, 0, 0, spellHeight), MyGUI::Align::Left | MyGUI::Align::Top); t->setNeedKeyFocus(true); t->setCaption(spell.mName + captionSuffix); t->setTextAlign(MyGUI::Align::Left); adjustSpellWidget(spell, i, t); if (!spell.mCostColumn.empty() && mShowCostColumn) { Gui::SharedStateButton* costChance = mScrollView->createWidget(skin, MyGUI::IntCoord(0, 0, 0, spellHeight), MyGUI::Align::Left | MyGUI::Align::Top); costChance->setCaption(spell.mCostColumn); costChance->setTextAlign(MyGUI::Align::Right); adjustSpellWidget(spell, i, costChance); Gui::ButtonGroup group; group.push_back(t); group.push_back(costChance); Gui::SharedStateButton::createButtonGroup(group); mLines.emplace_back(t, costChance, i); } else mLines.emplace_back(t, (MyGUI::Widget*)nullptr, i); t->setStateSelected(spell.mSelected); } layoutWidgets(); } void SpellView::incrementalUpdate() { if (!mModel.get()) { return; } mModel->update(); bool fullUpdateRequired = false; SpellModel::ModelIndex maxSpellIndexFound = -1; for (LineInfo& line : mLines) { // only update the lines that are "updateable" SpellModel::ModelIndex spellIndex(line.mSpellIndex); if (spellIndex != NoSpellIndex) { Gui::SharedStateButton* nameButton = reinterpret_cast(line.mLeftWidget); // match model against line // if don't match, then major change has happened, so do a full update if (mModel->getItemCount() <= static_cast(spellIndex)) { fullUpdateRequired = true; break; } // more checking for major change. const Spell& spell = mModel->getItem(spellIndex); const std::string captionSuffix = MWGui::ToolTips::getCountString(spell.mCount); if (nameButton->getCaption() != (spell.mName + captionSuffix)) { fullUpdateRequired = true; break; } else { maxSpellIndexFound = spellIndex; Gui::SharedStateButton* costButton = reinterpret_cast(line.mRightWidget); if ((costButton != nullptr) && (costButton->getCaption() != spell.mCostColumn)) { costButton->setCaption(spell.mCostColumn); } } } } // special case, look for spells added to model that are beyond last updatable item SpellModel::ModelIndex topSpellIndex = mModel->getItemCount() - 1; if (fullUpdateRequired || ((0 <= topSpellIndex) && (maxSpellIndexFound < topSpellIndex))) { update(); } } void SpellView::layoutWidgets() { int height = 0; for (LineInfo& line : mLines) { height += line.mLeftWidget->getHeight(); } bool scrollVisible = height > mScrollView->getHeight(); int width = mScrollView->getWidth() - (scrollVisible ? 18 : 0); height = 0; for (LineInfo& line : mLines) { int lineHeight = line.mLeftWidget->getHeight(); line.mLeftWidget->setCoord(4, height, width - 8, lineHeight); if (line.mRightWidget) { line.mRightWidget->setCoord(4, height, width - 8, lineHeight); MyGUI::TextBox* second = line.mRightWidget->castType(false); if (second) line.mLeftWidget->setSize(width - 8 - second->getTextSize().width, lineHeight); } height += lineHeight; } // Canvas size must be expressed with VScroll disabled, otherwise MyGUI would expand the scroll area when the scrollbar is hidden mScrollView->setVisibleVScroll(false); mScrollView->setCanvasSize(mScrollView->getWidth(), std::max(mScrollView->getHeight(), height)); mScrollView->setVisibleVScroll(true); } void SpellView::addGroup(const std::string &label, const std::string& label2) { if (mScrollView->getChildCount() > 0) { MyGUI::ImageBox* separator = mScrollView->createWidget("MW_HLine", MyGUI::IntCoord(0, 0, mScrollView->getWidth(), 18), MyGUI::Align::Left | MyGUI::Align::Top); separator->setNeedMouseFocus(false); mLines.emplace_back(separator, (MyGUI::Widget*)nullptr, NoSpellIndex); } MyGUI::TextBox* groupWidget = mScrollView->createWidget("SandBrightText", MyGUI::IntCoord(0, 0, mScrollView->getWidth(), 24), MyGUI::Align::Left | MyGUI::Align::Top); groupWidget->setCaptionWithReplacing(label); groupWidget->setTextAlign(MyGUI::Align::Left); groupWidget->setNeedMouseFocus(false); if (label2 != "") { MyGUI::TextBox* groupWidget2 = mScrollView->createWidget("SandBrightText", MyGUI::IntCoord(0, 0, mScrollView->getWidth(), 24), MyGUI::Align::Left | MyGUI::Align::Top); groupWidget2->setCaptionWithReplacing(label2); groupWidget2->setTextAlign(MyGUI::Align::Right); groupWidget2->setNeedMouseFocus(false); mLines.emplace_back(groupWidget, groupWidget2, NoSpellIndex); } else mLines.emplace_back(groupWidget, (MyGUI::Widget*)nullptr, NoSpellIndex); } void SpellView::setSize(const MyGUI::IntSize &_value) { bool changed = (_value.width != getWidth() || _value.height != getHeight()); Base::setSize(_value); if (changed) layoutWidgets(); } void SpellView::setCoord(const MyGUI::IntCoord &_value) { bool changed = (_value.width != getWidth() || _value.height != getHeight()); Base::setCoord(_value); if (changed) layoutWidgets(); } void SpellView::adjustSpellWidget(const Spell &spell, SpellModel::ModelIndex index, MyGUI::Widget *widget) { if (spell.mType == Spell::Type_EnchantedItem) { widget->setUserData(MWWorld::Ptr(spell.mItem)); widget->setUserString("ToolTipType", "ItemPtr"); } else { widget->setUserString("ToolTipType", "Spell"); widget->setUserString("Spell", spell.mId); } widget->setUserString(sSpellModelIndex, MyGUI::utility::toString(index)); widget->eventMouseWheel += MyGUI::newDelegate(this, &SpellView::onMouseWheelMoved); widget->eventMouseButtonClick += MyGUI::newDelegate(this, &SpellView::onSpellSelected); } SpellModel::ModelIndex SpellView::getSpellModelIndex(MyGUI::Widget* widget) { return MyGUI::utility::parseInt(widget->getUserString(sSpellModelIndex)); } void SpellView::onSpellSelected(MyGUI::Widget* _sender) { eventSpellClicked(getSpellModelIndex(_sender)); } void SpellView::onMouseWheelMoved(MyGUI::Widget* _sender, int _rel) { if (mScrollView->getViewOffset().top + _rel*0.3f > 0) mScrollView->setViewOffset(MyGUI::IntPoint(0, 0)); else mScrollView->setViewOffset(MyGUI::IntPoint(0, static_cast(mScrollView->getViewOffset().top + _rel*0.3f))); } void SpellView::resetScrollbars() { mScrollView->setViewOffset(MyGUI::IntPoint(0, 0)); } } ================================================ FILE: apps/openmw/mwgui/spellview.hpp ================================================ #ifndef OPENMW_GUI_SPELLVIEW_H #define OPENMW_GUI_SPELLVIEW_H #include #include #include #include "spellmodel.hpp" namespace MyGUI { class ScrollView; } namespace MWGui { class SpellModel; ///@brief Displays a SpellModel in a list widget class SpellView final : public MyGUI::Widget { MYGUI_RTTI_DERIVED(SpellView) public: SpellView(); /// Register needed components with MyGUI's factory manager static void registerComponents (); /// Should the cost/chance column be shown? void setShowCostColumn(bool show); void setHighlightSelected(bool highlight); /// Takes ownership of \a model void setModel (SpellModel* model); SpellModel* getModel(); void update(); /// simplified update called each frame void incrementalUpdate(); typedef MyGUI::delegates::CMultiDelegate1 EventHandle_ModelIndex; /// Fired when a spell was clicked EventHandle_ModelIndex eventSpellClicked; void initialiseOverride() override; void setSize(const MyGUI::IntSize& _value) override; void setCoord(const MyGUI::IntCoord& _value) override; void resetScrollbars(); private: MyGUI::ScrollView* mScrollView; std::unique_ptr mModel; /// tracks a row in the spell view struct LineInfo { /// the widget on the left side of the row MyGUI::Widget* mLeftWidget; /// the widget on the left side of the row (if there is one) MyGUI::Widget* mRightWidget; /// index to item in mModel that row is showing information for SpellModel::ModelIndex mSpellIndex; LineInfo(MyGUI::Widget* leftWidget, MyGUI::Widget* rightWidget, SpellModel::ModelIndex spellIndex); }; /// magic number indicating LineInfo does not correspond to an item in mModel enum { NoSpellIndex = -1 }; std::vector< LineInfo > mLines; bool mShowCostColumn; bool mHighlightSelected; void layoutWidgets(); void addGroup(const std::string& label1, const std::string& label2); void adjustSpellWidget(const Spell& spell, SpellModel::ModelIndex index, MyGUI::Widget* widget); void onSpellSelected(MyGUI::Widget* _sender); void onMouseWheelMoved(MyGUI::Widget* _sender, int _rel); SpellModel::ModelIndex getSpellModelIndex(MyGUI::Widget* _sender); static const char* sSpellModelIndex; }; } #endif ================================================ FILE: apps/openmw/mwgui/spellwindow.cpp ================================================ #include "spellwindow.hpp" #include #include #include #include /* Start of tes3mp addition Include additional headers for multiplayer purposes */ #include"../mwmp/Main.hpp" #include"../mwmp/LocalPlayer.hpp" /* End of tes3mp addition */ #include "../mwbase/windowmanager.hpp" #include "../mwbase/environment.hpp" #include "../mwbase/world.hpp" #include "../mwbase/mechanicsmanager.hpp" #include "../mwworld/inventorystore.hpp" #include "../mwworld/class.hpp" #include "../mwworld/esmstore.hpp" #include "../mwworld/player.hpp" #include "../mwmechanics/spellutil.hpp" #include "../mwmechanics/spells.hpp" #include "../mwmechanics/creaturestats.hpp" #include "../mwmechanics/actorutil.hpp" #include "spellicons.hpp" #include "confirmationdialog.hpp" #include "spellview.hpp" namespace MWGui { SpellWindow::SpellWindow(DragAndDrop* drag) : WindowPinnableBase("openmw_spell_window.layout") , NoDrop(drag, mMainWidget) , mSpellView(nullptr) , mUpdateTimer(0.0f) { mSpellIcons = new SpellIcons(); MyGUI::Widget* deleteButton; getWidget(deleteButton, "DeleteSpellButton"); getWidget(mSpellView, "SpellView"); getWidget(mEffectBox, "EffectsBox"); getWidget(mFilterEdit, "FilterEdit"); mSpellView->eventSpellClicked += MyGUI::newDelegate(this, &SpellWindow::onModelIndexSelected); mFilterEdit->eventEditTextChange += MyGUI::newDelegate(this, &SpellWindow::onFilterChanged); deleteButton->eventMouseButtonClick += MyGUI::newDelegate(this, &SpellWindow::onDeleteClicked); setCoord(498, 300, 302, 300); // Adjust the spell filtering widget size because of MyGUI limitations. int filterWidth = mSpellView->getSize().width - deleteButton->getSize().width - 3; mFilterEdit->setSize(filterWidth, mFilterEdit->getSize().height); } SpellWindow::~SpellWindow() { delete mSpellIcons; } void SpellWindow::onPinToggled() { Settings::Manager::setBool("spells pin", "Windows", mPinned); MWBase::Environment::get().getWindowManager()->setSpellVisibility(!mPinned); } void SpellWindow::onTitleDoubleClicked() { if (MyGUI::InputManager::getInstance().isShiftPressed()) MWBase::Environment::get().getWindowManager()->toggleMaximized(this); else if (!mPinned) MWBase::Environment::get().getWindowManager()->toggleVisible(GW_Magic); } void SpellWindow::onOpen() { // Reset the filter focus when opening the window MyGUI::Widget* focus = MyGUI::InputManager::getInstance().getKeyFocusWidget(); if (focus == mFilterEdit) MWBase::Environment::get().getWindowManager()->setKeyFocusWidget(nullptr); updateSpells(); } void SpellWindow::onFrame(float dt) { NoDrop::onFrame(dt); mUpdateTimer += dt; if (0.5f < mUpdateTimer) { mUpdateTimer = 0; mSpellView->incrementalUpdate(); } // Update effects in-game too if the window is pinned if (mPinned && !MWBase::Environment::get().getWindowManager()->isGuiMode()) mSpellIcons->updateWidgets(mEffectBox, false); } void SpellWindow::updateSpells() { mSpellIcons->updateWidgets(mEffectBox, false); mSpellView->setModel(new SpellModel(MWMechanics::getPlayer(), mFilterEdit->getCaption())); } void SpellWindow::onEnchantedItemSelected(MWWorld::Ptr item, bool alreadyEquipped) { MWWorld::Ptr player = MWMechanics::getPlayer(); MWWorld::InventoryStore& store = player.getClass().getInventoryStore(player); // retrieve ContainerStoreIterator to the item MWWorld::ContainerStoreIterator it = store.begin(); for (; it != store.end(); ++it) { if (*it == item) { break; } } if (it == store.end()) throw std::runtime_error("can't find selected item"); // equip, if it can be equipped and is not already equipped if (!alreadyEquipped && !item.getClass().getEquipmentSlots(item).first.empty()) { MWBase::Environment::get().getWindowManager()->useItem(item); // make sure that item was successfully equipped if (!store.isEquipped(item)) return; } store.setSelectedEnchantItem(it); // to reset WindowManager::mSelectedSpell immediately MWBase::Environment::get().getWindowManager()->setSelectedEnchantItem(*it); updateSpells(); } void SpellWindow::askDeleteSpell(const std::string &spellId) { // delete spell, if allowed const ESM::Spell* spell = MWBase::Environment::get().getWorld()->getStore().get().find(spellId); MWWorld::Ptr player = MWMechanics::getPlayer(); std::string raceId = player.get()->mBase->mRace; const ESM::Race* race = MWBase::Environment::get().getWorld()->getStore().get().find(raceId); // can't delete racial spells, birthsign spells or powers bool isInherent = race->mPowers.exists(spell->mId) || spell->mData.mType == ESM::Spell::ST_Power; const std::string& signId = MWBase::Environment::get().getWorld()->getPlayer().getBirthSign(); if (!isInherent && !signId.empty()) { const ESM::BirthSign* sign = MWBase::Environment::get().getWorld()->getStore().get().find(signId); isInherent = sign->mPowers.exists(spell->mId); } if (isInherent) { MWBase::Environment::get().getWindowManager()->messageBox("#{sDeleteSpellError}"); } else { // ask for confirmation mSpellToDelete = spellId; ConfirmationDialog* dialog = MWBase::Environment::get().getWindowManager()->getConfirmationDialog(); std::string question = MWBase::Environment::get().getWindowManager()->getGameSettingString("sQuestionDeleteSpell", "Delete %s?"); question = Misc::StringUtils::format(question, spell->mName); dialog->askForConfirmation(question); dialog->eventOkClicked.clear(); dialog->eventOkClicked += MyGUI::newDelegate(this, &SpellWindow::onDeleteSpellAccept); dialog->eventCancelClicked.clear(); } } void SpellWindow::onModelIndexSelected(SpellModel::ModelIndex index) { const Spell& spell = mSpellView->getModel()->getItem(index); if (spell.mType == Spell::Type_EnchantedItem) { onEnchantedItemSelected(spell.mItem, spell.mActive); } else { if (MyGUI::InputManager::getInstance().isShiftPressed()) askDeleteSpell(spell.mId); else onSpellSelected(spell.mId); } } void SpellWindow::onFilterChanged(MyGUI::EditBox *sender) { mSpellView->setModel(new SpellModel(MWMechanics::getPlayer(), sender->getCaption())); } void SpellWindow::onDeleteClicked(MyGUI::Widget *widget) { SpellModel::ModelIndex selected = mSpellView->getModel()->getSelectedIndex(); if (selected < 0) return; const Spell& spell = mSpellView->getModel()->getItem(selected); if (spell.mType != Spell::Type_EnchantedItem) askDeleteSpell(spell.mId); } void SpellWindow::onSpellSelected(const std::string& spellId) { MWWorld::Ptr player = MWMechanics::getPlayer(); MWWorld::InventoryStore& store = player.getClass().getInventoryStore(player); store.setSelectedEnchantItem(store.end()); MWBase::Environment::get().getWindowManager()->setSelectedSpell(spellId, int(MWMechanics::getSpellSuccessChance(spellId, player))); updateSpells(); /* Start of tes3mp addition Send a PlayerMiscellaneous packet with the player's new selected spell */ mwmp::Main::get().getLocalPlayer()->sendSelectedSpell(spellId); /* End of tes3mp addition */ } void SpellWindow::onDeleteSpellAccept() { MWWorld::Ptr player = MWMechanics::getPlayer(); MWMechanics::CreatureStats& stats = player.getClass().getCreatureStats(player); MWMechanics::Spells& spells = stats.getSpells(); if (MWBase::Environment::get().getWindowManager()->getSelectedSpell() == mSpellToDelete) MWBase::Environment::get().getWindowManager()->unsetSelectedSpell(); spells.remove(mSpellToDelete); /* Start of tes3mp addition Send an ID_PLAYER_SPELLBOOK packet every time a player deletes one of their spells */ mwmp::Main::get().getLocalPlayer()->sendSpellChange(mSpellToDelete, mwmp::SpellbookChanges::REMOVE); /* End of tes3mp addition */ updateSpells(); } void SpellWindow::cycle(bool next) { MWWorld::Ptr player = MWMechanics::getPlayer(); if (MWBase::Environment::get().getMechanicsManager()->isAttackingOrSpell(player)) return; bool godmode = MWBase::Environment::get().getWorld()->getGodModeState(); const MWMechanics::CreatureStats &stats = player.getClass().getCreatureStats(player); if ((!godmode && stats.isParalyzed()) || stats.getKnockedDown() || stats.isDead() || stats.getHitRecovery()) return; mSpellView->setModel(new SpellModel(MWMechanics::getPlayer(), "")); SpellModel::ModelIndex selected = mSpellView->getModel()->getSelectedIndex(); if (selected < 0) selected = 0; selected += next ? 1 : -1; int itemcount = mSpellView->getModel()->getItemCount(); if (itemcount == 0) return; selected = (selected + itemcount) % itemcount; const Spell& spell = mSpellView->getModel()->getItem(selected); if (spell.mType == Spell::Type_EnchantedItem) onEnchantedItemSelected(spell.mItem, spell.mActive); else onSpellSelected(spell.mId); } } ================================================ FILE: apps/openmw/mwgui/spellwindow.hpp ================================================ #ifndef MWGUI_SPELLWINDOW_H #define MWGUI_SPELLWINDOW_H #include "windowpinnablebase.hpp" #include "spellmodel.hpp" namespace MWGui { class SpellIcons; class SpellView; class SpellWindow : public WindowPinnableBase, public NoDrop { public: SpellWindow(DragAndDrop* drag); virtual ~SpellWindow(); void updateSpells(); void onFrame(float dt) override; /// Cycle to next/previous spell void cycle(bool next); protected: MyGUI::Widget* mEffectBox; std::string mSpellToDelete; void onEnchantedItemSelected(MWWorld::Ptr item, bool alreadyEquipped); void onSpellSelected(const std::string& spellId); void onModelIndexSelected(SpellModel::ModelIndex index); void onFilterChanged(MyGUI::EditBox *sender); void onDeleteClicked(MyGUI::Widget *widget); void onDeleteSpellAccept(); void askDeleteSpell(const std::string& spellId); void onPinToggled() override; void onTitleDoubleClicked() override; void onOpen() override; SpellView* mSpellView; SpellIcons* mSpellIcons; MyGUI::EditBox* mFilterEdit; private: float mUpdateTimer; }; } #endif ================================================ FILE: apps/openmw/mwgui/statswatcher.cpp ================================================ #include "statswatcher.hpp" #include "../mwbase/environment.hpp" #include "../mwbase/windowmanager.hpp" #include "../mwbase/world.hpp" #include "../mwmechanics/npcstats.hpp" #include "../mwmechanics/spellutil.hpp" #include "../mwworld/class.hpp" #include "../mwworld/esmstore.hpp" #include "../mwworld/inventorystore.hpp" namespace MWGui { // mWatchedTimeToStartDrowning = -1 for correct drowning state check, // if stats.getTimeToStartDrowning() == 0 already on game start StatsWatcher::StatsWatcher() : mWatchedLevel(-1), mWatchedTimeToStartDrowning(-1), mWatchedStatsEmpty(true) { } void StatsWatcher::watchActor(const MWWorld::Ptr& ptr) { mWatched = ptr; } void StatsWatcher::update() { if (mWatched.isEmpty()) return; MWBase::WindowManager *winMgr = MWBase::Environment::get().getWindowManager(); const MWMechanics::NpcStats &stats = mWatched.getClass().getNpcStats(mWatched); for (int i = 0;i < ESM::Attribute::Length;++i) { if (stats.getAttribute(i) != mWatchedAttributes[i] || mWatchedStatsEmpty) { std::stringstream attrname; attrname << "AttribVal"<<(i+1); mWatchedAttributes[i] = stats.getAttribute(i); setValue(attrname.str(), stats.getAttribute(i)); } } if (stats.getHealth() != mWatchedHealth || mWatchedStatsEmpty) { static const std::string hbar("HBar"); mWatchedHealth = stats.getHealth(); setValue(hbar, stats.getHealth()); } if (stats.getMagicka() != mWatchedMagicka || mWatchedStatsEmpty) { static const std::string mbar("MBar"); mWatchedMagicka = stats.getMagicka(); setValue(mbar, stats.getMagicka()); } if (stats.getFatigue() != mWatchedFatigue || mWatchedStatsEmpty) { static const std::string fbar("FBar"); mWatchedFatigue = stats.getFatigue(); setValue(fbar, stats.getFatigue()); } float timeToDrown = stats.getTimeToStartDrowning(); if (timeToDrown != mWatchedTimeToStartDrowning) { static const float fHoldBreathTime = MWBase::Environment::get().getWorld()->getStore().get() .find("fHoldBreathTime")->mValue.getFloat(); mWatchedTimeToStartDrowning = timeToDrown; if(timeToDrown >= fHoldBreathTime || timeToDrown == -1.0) // -1.0 is a special value during initialization winMgr->setDrowningBarVisibility(false); else { winMgr->setDrowningBarVisibility(true); winMgr->setDrowningTimeLeft(stats.getTimeToStartDrowning(), fHoldBreathTime); } } //Loop over ESM::Skill::SkillEnum for (int i = 0; i < ESM::Skill::Length; ++i) { if(stats.getSkill(i) != mWatchedSkills[i] || mWatchedStatsEmpty) { mWatchedSkills[i] = stats.getSkill(i); setValue((ESM::Skill::SkillEnum)i, stats.getSkill(i)); } } if (stats.getLevel() != mWatchedLevel || mWatchedStatsEmpty) { mWatchedLevel = stats.getLevel(); setValue("level", mWatchedLevel); } if (mWatched.getClass().isNpc()) { const ESM::NPC *watchedRecord = mWatched.get()->mBase; if (watchedRecord->mName != mWatchedName || mWatchedStatsEmpty) { mWatchedName = watchedRecord->mName; setValue("name", watchedRecord->mName); } if (watchedRecord->mRace != mWatchedRace || mWatchedStatsEmpty) { mWatchedRace = watchedRecord->mRace; const ESM::Race *race = MWBase::Environment::get().getWorld()->getStore() .get().find(watchedRecord->mRace); setValue("race", race->mName); } if (watchedRecord->mClass != mWatchedClass || mWatchedStatsEmpty) { mWatchedClass = watchedRecord->mClass; const ESM::Class *cls = MWBase::Environment::get().getWorld()->getStore() .get().find(watchedRecord->mClass); setValue("class", cls->mName); MWBase::WindowManager::SkillList majorSkills (5); MWBase::WindowManager::SkillList minorSkills (5); for (int i=0; i<5; ++i) { minorSkills[i] = cls->mData.mSkills[i][0]; majorSkills[i] = cls->mData.mSkills[i][1]; } configureSkills(majorSkills, minorSkills); } } mWatchedStatsEmpty = false; } void StatsWatcher::addListener(StatsListener* listener) { mListeners.insert(listener); } void StatsWatcher::removeListener(StatsListener* listener) { mListeners.erase(listener); } void StatsWatcher::setValue(const std::string& id, const MWMechanics::AttributeValue& value) { for (StatsListener* listener : mListeners) listener->setValue(id, value); } void StatsWatcher::setValue(ESM::Skill::SkillEnum parSkill, const MWMechanics::SkillValue& value) { /// \todo Don't use the skill enum as a parameter type (we will have to drop it anyway, once we /// allow custom skills. for (StatsListener* listener : mListeners) listener->setValue(parSkill, value); } void StatsWatcher::setValue(const std::string& id, const MWMechanics::DynamicStat& value) { for (StatsListener* listener : mListeners) listener->setValue(id, value); } void StatsWatcher::setValue(const std::string& id, const std::string& value) { for (StatsListener* listener : mListeners) listener->setValue(id, value); } void StatsWatcher::setValue(const std::string& id, int value) { for (StatsListener* listener : mListeners) listener->setValue(id, value); } void StatsWatcher::configureSkills(const std::vector& major, const std::vector& minor) { for (StatsListener* listener : mListeners) listener->configureSkills(major, minor); } } ================================================ FILE: apps/openmw/mwgui/statswatcher.hpp ================================================ #ifndef MWGUI_STATSWATCHER_H #define MWGUI_STATSWATCHER_H #include #include #include #include "../mwmechanics/stat.hpp" #include "../mwworld/ptr.hpp" namespace MWGui { class StatsListener { public: /// Set value for the given ID. virtual void setValue(const std::string& id, const MWMechanics::AttributeValue& value) {} virtual void setValue(const std::string& id, const MWMechanics::DynamicStat& value) {} virtual void setValue(const std::string& id, const std::string& value) {} virtual void setValue(const std::string& id, int value) {} virtual void setValue(const ESM::Skill::SkillEnum parSkill, const MWMechanics::SkillValue& value) {} virtual void configureSkills(const std::vector& major, const std::vector& minor) {} }; class StatsWatcher { MWWorld::Ptr mWatched; MWMechanics::AttributeValue mWatchedAttributes[ESM::Attribute::Length]; MWMechanics::SkillValue mWatchedSkills[ESM::Skill::Length]; MWMechanics::DynamicStat mWatchedHealth; MWMechanics::DynamicStat mWatchedMagicka; MWMechanics::DynamicStat mWatchedFatigue; std::string mWatchedName; std::string mWatchedRace; std::string mWatchedClass; int mWatchedLevel; float mWatchedTimeToStartDrowning; bool mWatchedStatsEmpty; std::set mListeners; void setValue(const std::string& id, const MWMechanics::AttributeValue& value); void setValue(const std::string& id, const MWMechanics::DynamicStat& value); void setValue(const std::string& id, const std::string& value); void setValue(const std::string& id, int value); void setValue(const ESM::Skill::SkillEnum parSkill, const MWMechanics::SkillValue& value); void configureSkills(const std::vector& major, const std::vector& minor); public: StatsWatcher(); void update(); void addListener(StatsListener* listener); void removeListener(StatsListener* listener); void watchActor(const MWWorld::Ptr& ptr); MWWorld::Ptr getWatchedActor() const { return mWatched; } void forceUpdate() { mWatchedStatsEmpty = true; } }; } #endif ================================================ FILE: apps/openmw/mwgui/statswindow.cpp ================================================ #include "statswindow.hpp" #include #include #include #include #include #include #include #include #include "../mwbase/environment.hpp" #include "../mwbase/world.hpp" #include "../mwbase/windowmanager.hpp" #include "../mwworld/class.hpp" #include "../mwworld/player.hpp" #include "../mwworld/esmstore.hpp" #include "../mwmechanics/npcstats.hpp" #include "../mwmechanics/actorutil.hpp" #include "tooltips.hpp" namespace MWGui { StatsWindow::StatsWindow (DragAndDrop* drag) : WindowPinnableBase("openmw_stats_window.layout") , NoDrop(drag, mMainWidget) , mSkillView(nullptr) , mMajorSkills() , mMinorSkills() , mMiscSkills() , mSkillValues() , mSkillWidgetMap() , mFactionWidgetMap() , mFactions() , mBirthSignId() , mReputation(0) , mBounty(0) , mSkillWidgets() , mChanged(true) , mMinFullWidth(mMainWidget->getSize().width) { const char *names[][2] = { { "Attrib1", "sAttributeStrength" }, { "Attrib2", "sAttributeIntelligence" }, { "Attrib3", "sAttributeWillpower" }, { "Attrib4", "sAttributeAgility" }, { "Attrib5", "sAttributeSpeed" }, { "Attrib6", "sAttributeEndurance" }, { "Attrib7", "sAttributePersonality" }, { "Attrib8", "sAttributeLuck" }, { 0, 0 } }; const MWWorld::ESMStore &store = MWBase::Environment::get().getWorld()->getStore(); for (int i=0; names[i][0]; ++i) { setText (names[i][0], store.get().find (names[i][1])->mValue.getString()); } getWidget(mSkillView, "SkillView"); getWidget(mLeftPane, "LeftPane"); getWidget(mRightPane, "RightPane"); for (int i = 0; i < ESM::Skill::Length; ++i) { mSkillValues.insert(std::make_pair(i, MWMechanics::SkillValue())); mSkillWidgetMap.insert(std::make_pair(i, std::make_pair((MyGUI::TextBox*)nullptr, (MyGUI::TextBox*)nullptr))); } MyGUI::Window* t = mMainWidget->castType(); t->eventWindowChangeCoord += MyGUI::newDelegate(this, &StatsWindow::onWindowResize); onWindowResize(t); } void StatsWindow::onMouseWheel(MyGUI::Widget* _sender, int _rel) { if (mSkillView->getViewOffset().top + _rel*0.3 > 0) mSkillView->setViewOffset(MyGUI::IntPoint(0, 0)); else mSkillView->setViewOffset(MyGUI::IntPoint(0, static_cast(mSkillView->getViewOffset().top + _rel*0.3))); } void StatsWindow::onWindowResize(MyGUI::Window* window) { int windowWidth = window->getSize().width; int windowHeight = window->getSize().height; //initial values defined in openmw_stats_window.layout, if custom options are not present in .layout, a default is loaded float leftPaneRatio = 0.44f; if (mLeftPane->isUserString("LeftPaneRatio")) leftPaneRatio = MyGUI::utility::parseFloat(mLeftPane->getUserString("LeftPaneRatio")); int leftOffsetWidth = 24; if (mLeftPane->isUserString("LeftOffsetWidth")) leftOffsetWidth = MyGUI::utility::parseInt(mLeftPane->getUserString("LeftOffsetWidth")); float rightPaneRatio = 1.f - leftPaneRatio; int minLeftWidth = static_cast(mMinFullWidth * leftPaneRatio); int minLeftOffsetWidth = minLeftWidth + leftOffsetWidth; //if there's no space for right pane mRightPane->setVisible(windowWidth >= minLeftOffsetWidth); if (!mRightPane->getVisible()) { mLeftPane->setCoord(MyGUI::IntCoord(0, 0, windowWidth - leftOffsetWidth, windowHeight)); } //if there's some space for right pane else if (windowWidth < mMinFullWidth) { mLeftPane->setCoord(MyGUI::IntCoord(0, 0, minLeftWidth, windowHeight)); mRightPane->setCoord(MyGUI::IntCoord(minLeftWidth, 0, windowWidth - minLeftWidth, windowHeight)); } //if there's enough space for both panes else { mLeftPane->setCoord(MyGUI::IntCoord(0, 0, static_cast(leftPaneRatio*windowWidth), windowHeight)); mRightPane->setCoord(MyGUI::IntCoord(static_cast(leftPaneRatio*windowWidth), 0, static_cast(rightPaneRatio*windowWidth), windowHeight)); } // Canvas size must be expressed with VScroll disabled, otherwise MyGUI would expand the scroll area when the scrollbar is hidden mSkillView->setVisibleVScroll(false); mSkillView->setCanvasSize (mSkillView->getWidth(), mSkillView->getCanvasSize().height); mSkillView->setVisibleVScroll(true); } void StatsWindow::setBar(const std::string& name, const std::string& tname, int val, int max) { MyGUI::ProgressBar* pt; getWidget(pt, name); std::stringstream out; out << val << "/" << max; setText(tname, out.str()); pt->setProgressRange(std::max(0, max)); pt->setProgressPosition(std::max(0, val)); } void StatsWindow::setPlayerName(const std::string& playerName) { mMainWidget->castType()->setCaption(playerName); } void StatsWindow::setValue (const std::string& id, const MWMechanics::AttributeValue& value) { static const char *ids[] = { "AttribVal1", "AttribVal2", "AttribVal3", "AttribVal4", "AttribVal5", "AttribVal6", "AttribVal7", "AttribVal8", 0 }; for (int i=0; ids[i]; ++i) if (ids[i]==id) { setText (id, std::to_string(static_cast(value.getModified()))); MyGUI::TextBox* box; getWidget(box, id); if (value.getModified()>value.getBase()) box->_setWidgetState("increased"); else if (value.getModified()_setWidgetState("decreased"); else box->_setWidgetState("normal"); break; } } void StatsWindow::setValue (const std::string& id, const MWMechanics::DynamicStat& value) { int current = static_cast(value.getCurrent()); int modified = static_cast(value.getModified()); // Fatigue can be negative if (id != "FBar") current = std::max(0, current); setBar (id, id + "T", current, modified); // health, magicka, fatigue tooltip MyGUI::Widget* w; std::string valStr = MyGUI::utility::toString(current) + " / " + MyGUI::utility::toString(modified); if (id == "HBar") { getWidget(w, "Health"); w->setUserString("Caption_HealthDescription", "#{sHealthDesc}\n" + valStr); } else if (id == "MBar") { getWidget(w, "Magicka"); w->setUserString("Caption_HealthDescription", "#{sMagDesc}\n" + valStr); } else if (id == "FBar") { getWidget(w, "Fatigue"); w->setUserString("Caption_HealthDescription", "#{sFatDesc}\n" + valStr); } } void StatsWindow::setValue (const std::string& id, const std::string& value) { if (id=="name") setPlayerName (value); else if (id=="race") setText ("RaceText", value); else if (id=="class") setText ("ClassText", value); } void StatsWindow::setValue (const std::string& id, int value) { if (id=="level") { std::ostringstream text; text << value; setText("LevelText", text.str()); } } void setSkillProgress(MyGUI::Widget* w, float progress, int skillId) { MWWorld::Ptr player = MWMechanics::getPlayer(); const MWWorld::ESMStore &esmStore = MWBase::Environment::get().getWorld()->getStore(); float progressRequirement = player.getClass().getNpcStats(player).getSkillProgressRequirement(skillId, *esmStore.get().find(player.get()->mBase->mClass)); // This is how vanilla MW displays the progress bar (I think). Note it's slightly inaccurate, // due to the int casting in the skill levelup logic. Also the progress label could in rare cases // reach 100% without the skill levelling up. // Leaving the original display logic for now, for consistency with ess-imported savegames. int progressPercent = int(float(progress) / float(progressRequirement) * 100.f + 0.5f); w->setUserString("Caption_SkillProgressText", MyGUI::utility::toString(progressPercent)+"/100"); w->setUserString("RangePosition_SkillProgress", MyGUI::utility::toString(progressPercent)); } void StatsWindow::setValue(const ESM::Skill::SkillEnum parSkill, const MWMechanics::SkillValue& value) { mSkillValues[parSkill] = value; std::pair widgets = mSkillWidgetMap[(int)parSkill]; MyGUI::TextBox* valueWidget = widgets.second; MyGUI::TextBox* nameWidget = widgets.first; if (valueWidget && nameWidget) { int modified = value.getModified(), base = value.getBase(); std::string text = MyGUI::utility::toString(modified); std::string state = "normal"; if (modified > base) state = "increased"; else if (modified < base) state = "decreased"; int widthBefore = valueWidget->getTextSize().width; valueWidget->setCaption(text); valueWidget->_setWidgetState(state); int widthAfter = valueWidget->getTextSize().width; if (widthBefore != widthAfter) { valueWidget->setCoord(valueWidget->getLeft() - (widthAfter-widthBefore), valueWidget->getTop(), valueWidget->getWidth() + (widthAfter-widthBefore), valueWidget->getHeight()); nameWidget->setSize(nameWidget->getWidth() - (widthAfter-widthBefore), nameWidget->getHeight()); } if (value.getBase() < 100) { nameWidget->setUserString("Visible_SkillMaxed", "false"); nameWidget->setUserString("UserData^Hidden_SkillMaxed", "true"); nameWidget->setUserString("Visible_SkillProgressVBox", "true"); nameWidget->setUserString("UserData^Hidden_SkillProgressVBox", "false"); valueWidget->setUserString("Visible_SkillMaxed", "false"); valueWidget->setUserString("UserData^Hidden_SkillMaxed", "true"); valueWidget->setUserString("Visible_SkillProgressVBox", "true"); valueWidget->setUserString("UserData^Hidden_SkillProgressVBox", "false"); setSkillProgress(nameWidget, value.getProgress(), parSkill); setSkillProgress(valueWidget, value.getProgress(), parSkill); } else { nameWidget->setUserString("Visible_SkillMaxed", "true"); nameWidget->setUserString("UserData^Hidden_SkillMaxed", "false"); nameWidget->setUserString("Visible_SkillProgressVBox", "false"); nameWidget->setUserString("UserData^Hidden_SkillProgressVBox", "true"); valueWidget->setUserString("Visible_SkillMaxed", "true"); valueWidget->setUserString("UserData^Hidden_SkillMaxed", "false"); valueWidget->setUserString("Visible_SkillProgressVBox", "false"); valueWidget->setUserString("UserData^Hidden_SkillProgressVBox", "true"); } } } void StatsWindow::configureSkills (const std::vector& major, const std::vector& minor) { mMajorSkills = major; mMinorSkills = minor; // Update misc skills with the remaining skills not in major or minor std::set skillSet; std::copy(major.begin(), major.end(), std::inserter(skillSet, skillSet.begin())); std::copy(minor.begin(), minor.end(), std::inserter(skillSet, skillSet.begin())); mMiscSkills.clear(); for (const int skill : ESM::Skill::sSkillIds) { if (skillSet.find(skill) == skillSet.end()) mMiscSkills.push_back(skill); } updateSkillArea(); } void StatsWindow::onFrame (float dt) { NoDrop::onFrame(dt); MWWorld::Ptr player = MWMechanics::getPlayer(); const MWMechanics::NpcStats &PCstats = player.getClass().getNpcStats(player); // level progress MyGUI::Widget* levelWidget; for (int i=0; i<2; ++i) { int max = MWBase::Environment::get().getWorld()->getStore().get().find("iLevelUpTotal")->mValue.getInteger(); getWidget(levelWidget, i==0 ? "Level_str" : "LevelText"); levelWidget->setUserString("RangePosition_LevelProgress", MyGUI::utility::toString(PCstats.getLevelProgress())); levelWidget->setUserString("Range_LevelProgress", MyGUI::utility::toString(max)); levelWidget->setUserString("Caption_LevelProgressText", MyGUI::utility::toString(PCstats.getLevelProgress()) + "/" + MyGUI::utility::toString(max)); } std::stringstream detail; for (int attribute = 0; attribute < ESM::Attribute::Length; ++attribute) { float mult = PCstats.getLevelupAttributeMultiplier(attribute); mult = std::min(mult, 100 - PCstats.getAttribute(attribute).getBase()); if (mult > 1) detail << (detail.str().empty() ? "" : "\n") << "#{" << MyGUI::TextIterator::toTagsString(ESM::Attribute::sGmstAttributeIds[attribute]) << "} x" << MyGUI::utility::toString(mult); } levelWidget->setUserString("Caption_LevelDetailText", MyGUI::LanguageManager::getInstance().replaceTags(detail.str())); setFactions(PCstats.getFactionRanks()); setExpelled(PCstats.getExpelled ()); const std::string &signId = MWBase::Environment::get().getWorld()->getPlayer().getBirthSign(); setBirthSign(signId); setReputation (PCstats.getReputation ()); setBounty (PCstats.getBounty ()); if (mChanged) updateSkillArea(); } void StatsWindow::setFactions (const FactionList& factions) { if (mFactions != factions) { mFactions = factions; mChanged = true; } } void StatsWindow::setExpelled (const std::set& expelled) { if (mExpelled != expelled) { mExpelled = expelled; mChanged = true; } } void StatsWindow::setBirthSign (const std::string& signId) { if (signId != mBirthSignId) { mBirthSignId = signId; mChanged = true; } } void StatsWindow::addSeparator(MyGUI::IntCoord &coord1, MyGUI::IntCoord &coord2) { MyGUI::ImageBox* separator = mSkillView->createWidget("MW_HLine", MyGUI::IntCoord(10, coord1.top, coord1.width + coord2.width - 4, 18), MyGUI::Align::Left | MyGUI::Align::Top | MyGUI::Align::HStretch); separator->eventMouseWheel += MyGUI::newDelegate(this, &StatsWindow::onMouseWheel); mSkillWidgets.push_back(separator); coord1.top += separator->getHeight(); coord2.top += separator->getHeight(); } void StatsWindow::addGroup(const std::string &label, MyGUI::IntCoord &coord1, MyGUI::IntCoord &coord2) { MyGUI::TextBox* groupWidget = mSkillView->createWidget("SandBrightText", MyGUI::IntCoord(0, coord1.top, coord1.width + coord2.width, coord1.height), MyGUI::Align::Left | MyGUI::Align::Top | MyGUI::Align::HStretch); groupWidget->setCaption(label); groupWidget->eventMouseWheel += MyGUI::newDelegate(this, &StatsWindow::onMouseWheel); mSkillWidgets.push_back(groupWidget); int lineHeight = MWBase::Environment::get().getWindowManager()->getFontHeight() + 2; coord1.top += lineHeight; coord2.top += lineHeight; } std::pair StatsWindow::addValueItem(const std::string& text, const std::string &value, const std::string& state, MyGUI::IntCoord &coord1, MyGUI::IntCoord &coord2) { MyGUI::TextBox *skillNameWidget, *skillValueWidget; skillNameWidget = mSkillView->createWidget("SandText", coord1, MyGUI::Align::Left | MyGUI::Align::Top | MyGUI::Align::HStretch); skillNameWidget->setCaption(text); skillNameWidget->eventMouseWheel += MyGUI::newDelegate(this, &StatsWindow::onMouseWheel); skillValueWidget = mSkillView->createWidget("SandTextRight", coord2, MyGUI::Align::Right | MyGUI::Align::Top); skillValueWidget->setCaption(value); skillValueWidget->_setWidgetState(state); skillValueWidget->eventMouseWheel += MyGUI::newDelegate(this, &StatsWindow::onMouseWheel); // resize dynamically according to text size int textWidthPlusMargin = skillValueWidget->getTextSize().width + 12; skillValueWidget->setCoord(coord2.left + coord2.width - textWidthPlusMargin, coord2.top, textWidthPlusMargin, coord2.height); skillNameWidget->setSize(skillNameWidget->getSize() + MyGUI::IntSize(coord2.width - textWidthPlusMargin, 0)); mSkillWidgets.push_back(skillNameWidget); mSkillWidgets.push_back(skillValueWidget); int lineHeight = MWBase::Environment::get().getWindowManager()->getFontHeight() + 2; coord1.top += lineHeight; coord2.top += lineHeight; return std::make_pair(skillNameWidget, skillValueWidget); } MyGUI::Widget* StatsWindow::addItem(const std::string& text, MyGUI::IntCoord &coord1, MyGUI::IntCoord &coord2) { MyGUI::TextBox* skillNameWidget; skillNameWidget = mSkillView->createWidget("SandText", coord1, MyGUI::Align::Default); skillNameWidget->setCaption(text); skillNameWidget->eventMouseWheel += MyGUI::newDelegate(this, &StatsWindow::onMouseWheel); int textWidth = skillNameWidget->getTextSize().width; skillNameWidget->setSize(textWidth, skillNameWidget->getHeight()); mSkillWidgets.push_back(skillNameWidget); int lineHeight = MWBase::Environment::get().getWindowManager()->getFontHeight() + 2; coord1.top += lineHeight; coord2.top += lineHeight; return skillNameWidget; } void StatsWindow::addSkills(const SkillList &skills, const std::string &titleId, const std::string &titleDefault, MyGUI::IntCoord &coord1, MyGUI::IntCoord &coord2) { // Add a line separator if there are items above if (!mSkillWidgets.empty()) { addSeparator(coord1, coord2); } addGroup(MWBase::Environment::get().getWindowManager()->getGameSettingString(titleId, titleDefault), coord1, coord2); for (const int skillId : skills) { if (skillId < 0 || skillId >= ESM::Skill::Length) // Skip unknown skill indexes continue; const std::string &skillNameId = ESM::Skill::sSkillNameIds[skillId]; const MWWorld::ESMStore &esmStore = MWBase::Environment::get().getWorld()->getStore(); const ESM::Skill* skill = esmStore.get().find(skillId); std::string icon = "icons\\k\\" + ESM::Skill::sIconNames[skillId]; const ESM::Attribute* attr = esmStore.get().find(skill->mData.mAttribute); std::pair widgets = addValueItem(MWBase::Environment::get().getWindowManager()->getGameSettingString(skillNameId, skillNameId), "", "normal", coord1, coord2); mSkillWidgetMap[skillId] = widgets; for (int i=0; i<2; ++i) { mSkillWidgets[mSkillWidgets.size()-1-i]->setUserString("ToolTipType", "Layout"); mSkillWidgets[mSkillWidgets.size()-1-i]->setUserString("ToolTipLayout", "SkillToolTip"); mSkillWidgets[mSkillWidgets.size()-1-i]->setUserString("Caption_SkillName", "#{"+skillNameId+"}"); mSkillWidgets[mSkillWidgets.size()-1-i]->setUserString("Caption_SkillDescription", skill->mDescription); mSkillWidgets[mSkillWidgets.size()-1-i]->setUserString("Caption_SkillAttribute", "#{sGoverningAttribute}: #{" + attr->mName + "}"); mSkillWidgets[mSkillWidgets.size()-1-i]->setUserString("ImageTexture_SkillImage", icon); mSkillWidgets[mSkillWidgets.size()-1-i]->setUserString("Range_SkillProgress", "100"); } setValue(static_cast(skillId), mSkillValues.find(skillId)->second); } } void StatsWindow::updateSkillArea() { mChanged = false; for (MyGUI::Widget* widget : mSkillWidgets) { MyGUI::Gui::getInstance().destroyWidget(widget); } mSkillWidgets.clear(); const int valueSize = 40; MyGUI::IntCoord coord1(10, 0, mSkillView->getWidth() - (10 + valueSize) - 24, 18); MyGUI::IntCoord coord2(coord1.left + coord1.width, coord1.top, valueSize, coord1.height); if (!mMajorSkills.empty()) addSkills(mMajorSkills, "sSkillClassMajor", "Major Skills", coord1, coord2); if (!mMinorSkills.empty()) addSkills(mMinorSkills, "sSkillClassMinor", "Minor Skills", coord1, coord2); if (!mMiscSkills.empty()) addSkills(mMiscSkills, "sSkillClassMisc", "Misc Skills", coord1, coord2); MWBase::World *world = MWBase::Environment::get().getWorld(); const MWWorld::ESMStore &store = world->getStore(); const ESM::NPC *player = world->getPlayerPtr().get()->mBase; // race tooltip const ESM::Race* playerRace = store.get().find(player->mRace); MyGUI::Widget* raceWidget; getWidget(raceWidget, "RaceText"); ToolTips::createRaceToolTip(raceWidget, playerRace); getWidget(raceWidget, "Race_str"); ToolTips::createRaceToolTip(raceWidget, playerRace); // class tooltip MyGUI::Widget* classWidget; const ESM::Class *playerClass = store.get().find(player->mClass); getWidget(classWidget, "ClassText"); ToolTips::createClassToolTip(classWidget, *playerClass); getWidget(classWidget, "Class_str"); ToolTips::createClassToolTip(classWidget, *playerClass); if (!mFactions.empty()) { MWWorld::Ptr playerPtr = MWMechanics::getPlayer(); const MWMechanics::NpcStats &PCstats = playerPtr.getClass().getNpcStats(playerPtr); const std::set &expelled = PCstats.getExpelled(); bool firstFaction=true; for (auto& factionPair : mFactions) { const std::string& factionId = factionPair.first; const ESM::Faction *faction = store.get().find(factionId); if (faction->mData.mIsHidden == 1) continue; if (firstFaction) { // Add a line separator if there are items above if (!mSkillWidgets.empty()) addSeparator(coord1, coord2); addGroup(MWBase::Environment::get().getWindowManager()->getGameSettingString("sFaction", "Faction"), coord1, coord2); firstFaction = false; } MyGUI::Widget* w = addItem(faction->mName, coord1, coord2); std::string text; text += std::string("#{fontcolourhtml=header}") + faction->mName; if (expelled.find(factionId) != expelled.end()) text += "\n#{fontcolourhtml=normal}#{sExpelled}"; else { int rank = factionPair.second; rank = std::max(0, std::min(9, rank)); text += std::string("\n#{fontcolourhtml=normal}") + faction->mRanks[rank]; if (rank < 9) { // player doesn't have max rank yet text += std::string("\n\n#{fontcolourhtml=header}#{sNextRank} ") + faction->mRanks[rank+1]; ESM::RankData rankData = faction->mData.mRankData[rank+1]; const ESM::Attribute* attr1 = store.get().find(faction->mData.mAttribute[0]); const ESM::Attribute* attr2 = store.get().find(faction->mData.mAttribute[1]); text += "\n#{fontcolourhtml=normal}#{" + attr1->mName + "}: " + MyGUI::utility::toString(rankData.mAttribute1) + ", #{" + attr2->mName + "}: " + MyGUI::utility::toString(rankData.mAttribute2); text += "\n\n#{fontcolourhtml=header}#{sFavoriteSkills}"; text += "\n#{fontcolourhtml=normal}"; bool firstSkill = true; for (int i=0; i<7; ++i) { if (faction->mData.mSkills[i] != -1) { if (!firstSkill) text += ", "; firstSkill = false; text += "#{"+ESM::Skill::sSkillNameIds[faction->mData.mSkills[i]]+"}"; } } text += "\n"; if (rankData.mPrimarySkill > 0) text += "\n#{sNeedOneSkill} " + MyGUI::utility::toString(rankData.mPrimarySkill); if (rankData.mFavouredSkill > 0) text += " #{sand} #{sNeedTwoSkills} " + MyGUI::utility::toString(rankData.mFavouredSkill); } } w->setUserString("ToolTipType", "Layout"); w->setUserString("ToolTipLayout", "FactionToolTip"); w->setUserString("Caption_FactionText", text); } } if (!mBirthSignId.empty()) { // Add a line separator if there are items above if (!mSkillWidgets.empty()) addSeparator(coord1, coord2); addGroup(MWBase::Environment::get().getWindowManager()->getGameSettingString("sBirthSign", "Sign"), coord1, coord2); const ESM::BirthSign *sign = store.get().find(mBirthSignId); MyGUI::Widget* w = addItem(sign->mName, coord1, coord2); ToolTips::createBirthsignToolTip(w, mBirthSignId); } // Add a line separator if there are items above if (!mSkillWidgets.empty()) addSeparator(coord1, coord2); addValueItem(MWBase::Environment::get().getWindowManager()->getGameSettingString("sReputation", "Reputation"), MyGUI::utility::toString(static_cast(mReputation)), "normal", coord1, coord2); for (int i=0; i<2; ++i) { mSkillWidgets[mSkillWidgets.size()-1-i]->setUserString("ToolTipType", "Layout"); mSkillWidgets[mSkillWidgets.size()-1-i]->setUserString("ToolTipLayout", "TextToolTip"); mSkillWidgets[mSkillWidgets.size()-1-i]->setUserString("Caption_Text", "#{sSkillsMenuReputationHelp}"); } addValueItem(MWBase::Environment::get().getWindowManager()->getGameSettingString("sBounty", "Bounty"), MyGUI::utility::toString(static_cast(mBounty)), "normal", coord1, coord2); for (int i=0; i<2; ++i) { mSkillWidgets[mSkillWidgets.size()-1-i]->setUserString("ToolTipType", "Layout"); mSkillWidgets[mSkillWidgets.size()-1-i]->setUserString("ToolTipLayout", "TextToolTip"); mSkillWidgets[mSkillWidgets.size()-1-i]->setUserString("Caption_Text", "#{sCrimeHelp}"); } // Canvas size must be expressed with VScroll disabled, otherwise MyGUI would expand the scroll area when the scrollbar is hidden mSkillView->setVisibleVScroll(false); mSkillView->setCanvasSize (mSkillView->getWidth(), std::max(mSkillView->getHeight(), coord1.top)); mSkillView->setVisibleVScroll(true); } void StatsWindow::onPinToggled() { Settings::Manager::setBool("stats pin", "Windows", mPinned); MWBase::Environment::get().getWindowManager()->setHMSVisibility(!mPinned); } void StatsWindow::onTitleDoubleClicked() { if (MyGUI::InputManager::getInstance().isShiftPressed()) { MWBase::Environment::get().getWindowManager()->toggleMaximized(this); MyGUI::Window* t = mMainWidget->castType(); onWindowResize(t); } else if (!mPinned) MWBase::Environment::get().getWindowManager()->toggleVisible(GW_Stats); } } ================================================ FILE: apps/openmw/mwgui/statswindow.hpp ================================================ #ifndef MWGUI_STATS_WINDOW_H #define MWGUI_STATS_WINDOW_H #include "statswatcher.hpp" #include "windowpinnablebase.hpp" namespace MWGui { class StatsWindow : public WindowPinnableBase, public NoDrop, public StatsListener { public: typedef std::map FactionList; typedef std::vector SkillList; StatsWindow(DragAndDrop* drag); /// automatically updates all the data in the stats window, but only if it has changed. void onFrame(float dt) override; void setBar(const std::string& name, const std::string& tname, int val, int max); void setPlayerName(const std::string& playerName); /// Set value for the given ID. void setValue (const std::string& id, const MWMechanics::AttributeValue& value) override; void setValue (const std::string& id, const MWMechanics::DynamicStat& value) override; void setValue (const std::string& id, const std::string& value) override; void setValue (const std::string& id, int value) override; void setValue(const ESM::Skill::SkillEnum parSkill, const MWMechanics::SkillValue& value) override; void configureSkills(const SkillList& major, const SkillList& minor) override; void setReputation (int reputation) { if (reputation != mReputation) mChanged = true; this->mReputation = reputation; } void setBounty (int bounty) { if (bounty != mBounty) mChanged = true; this->mBounty = bounty; } void updateSkillArea(); void onOpen() override { onWindowResize(mMainWidget->castType()); } private: void addSkills(const SkillList &skills, const std::string &titleId, const std::string &titleDefault, MyGUI::IntCoord &coord1, MyGUI::IntCoord &coord2); void addSeparator(MyGUI::IntCoord &coord1, MyGUI::IntCoord &coord2); void addGroup(const std::string &label, MyGUI::IntCoord &coord1, MyGUI::IntCoord &coord2); std::pair addValueItem(const std::string& text, const std::string &value, const std::string& state, MyGUI::IntCoord &coord1, MyGUI::IntCoord &coord2); MyGUI::Widget* addItem(const std::string& text, MyGUI::IntCoord &coord1, MyGUI::IntCoord &coord2); void setFactions (const FactionList& factions); void setExpelled (const std::set& expelled); void setBirthSign (const std::string &signId); void onWindowResize(MyGUI::Window* window); void onMouseWheel(MyGUI::Widget* _sender, int _rel); MyGUI::Widget* mLeftPane; MyGUI::Widget* mRightPane; MyGUI::ScrollView* mSkillView; SkillList mMajorSkills, mMinorSkills, mMiscSkills; std::map mSkillValues; std::map > mSkillWidgetMap; std::map mFactionWidgetMap; FactionList mFactions; ///< Stores a list of factions and the current rank std::string mBirthSignId; int mReputation, mBounty; std::vector mSkillWidgets; //< Skills and other information std::set mExpelled; bool mChanged; const int mMinFullWidth; protected: void onPinToggled() override; void onTitleDoubleClicked() override; }; } #endif ================================================ FILE: apps/openmw/mwgui/textcolours.cpp ================================================ #include "textcolours.hpp" #include #include namespace MWGui { MyGUI::Colour getTextColour(const std::string& type) { return MyGUI::Colour::parse(MyGUI::LanguageManager::getInstance().replaceTags("#{fontcolour=" + type + "}")); } void TextColours::loadColours() { header = getTextColour("header"); normal = getTextColour("normal"); notify = getTextColour("notify"); link = getTextColour("link"); linkOver = getTextColour("link_over"); linkPressed = getTextColour("link_pressed"); answer = getTextColour("answer"); answerOver = getTextColour("answer_over"); answerPressed = getTextColour("answer_pressed"); journalLink = getTextColour("journal_link"); journalLinkOver = getTextColour("journal_link_over"); journalLinkPressed = getTextColour("journal_link_pressed"); journalTopic = getTextColour("journal_topic"); journalTopicOver = getTextColour("journal_topic_over"); journalTopicPressed = getTextColour("journal_topic_pressed"); } } ================================================ FILE: apps/openmw/mwgui/textcolours.hpp ================================================ #ifndef MWGUI_TEXTCOLORS_H #define MWGUI_TEXTCOLORS_H #include namespace MWGui { struct TextColours { MyGUI::Colour header; MyGUI::Colour normal; MyGUI::Colour notify; MyGUI::Colour link; MyGUI::Colour linkOver; MyGUI::Colour linkPressed; MyGUI::Colour answer; MyGUI::Colour answerOver; MyGUI::Colour answerPressed; MyGUI::Colour journalLink; MyGUI::Colour journalLinkOver; MyGUI::Colour journalLinkPressed; MyGUI::Colour journalTopic; MyGUI::Colour journalTopicOver; MyGUI::Colour journalTopicPressed; public: void loadColours(); }; } #endif ================================================ FILE: apps/openmw/mwgui/textinput.cpp ================================================ #include "textinput.hpp" #include "../mwbase/windowmanager.hpp" #include "../mwbase/environment.hpp" #include #include namespace MWGui { TextInputDialog::TextInputDialog() : WindowModal("openmw_text_input.layout") { // Centre dialog center(); getWidget(mTextEdit, "TextEdit"); mTextEdit->eventEditSelectAccept += newDelegate(this, &TextInputDialog::onTextAccepted); MyGUI::Button* okButton; getWidget(okButton, "OKButton"); okButton->eventMouseButtonClick += MyGUI::newDelegate(this, &TextInputDialog::onOkClicked); // Make sure the edit box has focus MWBase::Environment::get().getWindowManager()->setKeyFocusWidget(mTextEdit); } void TextInputDialog::setNextButtonShow(bool shown) { MyGUI::Button* okButton; getWidget(okButton, "OKButton"); if (shown) okButton->setCaption(MWBase::Environment::get().getWindowManager()->getGameSettingString("sNext", "")); else okButton->setCaption(MWBase::Environment::get().getWindowManager()->getGameSettingString("sOK", "")); } void TextInputDialog::setTextLabel(const std::string &label) { setText("LabelT", label); } void TextInputDialog::onOpen() { WindowModal::onOpen(); // Make sure the edit box has focus MWBase::Environment::get().getWindowManager()->setKeyFocusWidget(mTextEdit); } // widget controls void TextInputDialog::onOkClicked(MyGUI::Widget* _sender) { if (mTextEdit->getCaption() == "") { MWBase::Environment::get().getWindowManager()->messageBox ("#{sNotifyMessage37}"); MWBase::Environment::get().getWindowManager()->setKeyFocusWidget (mTextEdit); } else eventDone(this); } void TextInputDialog::onTextAccepted(MyGUI::Edit* _sender) { onOkClicked(_sender); // To do not spam onTextAccepted() again and again MWBase::Environment::get().getWindowManager()->injectKeyRelease(MyGUI::KeyCode::None); } std::string TextInputDialog::getTextInput() const { return mTextEdit->getCaption(); } void TextInputDialog::setTextInput(const std::string &text) { mTextEdit->setCaption(text); } } ================================================ FILE: apps/openmw/mwgui/textinput.hpp ================================================ #ifndef MWGUI_TEXT_INPUT_H #define MWGUI_TEXT_INPUT_H #include "windowbase.hpp" namespace MWGui { class TextInputDialog : public WindowModal { public: TextInputDialog(); std::string getTextInput() const; void setTextInput(const std::string &text); void setNextButtonShow(bool shown); void setTextLabel(const std::string &label); void onOpen() override; bool exit() override { return false; } /** Event : Dialog finished, OK button clicked.\n signature : void method()\n */ EventHandle_WindowBase eventDone; protected: void onOkClicked(MyGUI::Widget* _sender); void onTextAccepted(MyGUI::Edit* _sender); private: MyGUI::EditBox* mTextEdit; }; } #endif ================================================ FILE: apps/openmw/mwgui/timeadvancer.cpp ================================================ #include "timeadvancer.hpp" namespace MWGui { TimeAdvancer::TimeAdvancer(float delay) : mRunning(false), mCurHour(0), mHours(1), mInterruptAt(-1), mDelay(delay), mRemainingTime(delay) { } void TimeAdvancer::run(int hours, int interruptAt) { mHours = hours; mCurHour = 0; mInterruptAt = interruptAt; mRemainingTime = mDelay; mRunning = true; } void TimeAdvancer::stop() { mRunning = false; } void TimeAdvancer::onFrame(float dt) { if (!mRunning) return; if (mCurHour == mInterruptAt) { stop(); eventInterrupted(); return; } mRemainingTime -= dt; while (mRemainingTime <= 0) { mRemainingTime += mDelay; ++mCurHour; if (mCurHour <= mHours) eventProgressChanged(mCurHour, mHours); else { stop(); eventFinished(); return; } } } int TimeAdvancer::getHours() const { return mHours; } bool TimeAdvancer::isRunning() const { return mRunning; } } ================================================ FILE: apps/openmw/mwgui/timeadvancer.hpp ================================================ #ifndef MWGUI_TIMEADVANCER_H #define MWGUI_TIMEADVANCER_H #include namespace MWGui { class TimeAdvancer { public: TimeAdvancer(float delay); void run(int hours, int interruptAt=-1); void stop(); void onFrame(float dt); int getHours() const; bool isRunning() const; // signals typedef MyGUI::delegates::CMultiDelegate0 EventHandle_Void; typedef MyGUI::delegates::CMultiDelegate2 EventHandle_IntInt; EventHandle_IntInt eventProgressChanged; EventHandle_Void eventInterrupted; EventHandle_Void eventFinished; private: bool mRunning; int mCurHour; int mHours; int mInterruptAt; float mDelay; float mRemainingTime; }; } #endif ================================================ FILE: apps/openmw/mwgui/tooltips.cpp ================================================ #include "tooltips.hpp" #include #include #include #include #include #include #include #include "../mwbase/world.hpp" #include "../mwbase/environment.hpp" #include "../mwbase/windowmanager.hpp" #include "../mwbase/mechanicsmanager.hpp" #include "../mwworld/class.hpp" #include "../mwworld/esmstore.hpp" #include "../mwmechanics/spellutil.hpp" #include "../mwmechanics/actorutil.hpp" #include "mapwindow.hpp" #include "inventorywindow.hpp" #include "itemmodel.hpp" namespace MWGui { std::string ToolTips::sSchoolNames[] = {"#{sSchoolAlteration}", "#{sSchoolConjuration}", "#{sSchoolDestruction}", "#{sSchoolIllusion}", "#{sSchoolMysticism}", "#{sSchoolRestoration}"}; ToolTips::ToolTips() : Layout("openmw_tooltips.layout") , mFocusToolTipX(0.0) , mFocusToolTipY(0.0) , mHorizontalScrollIndex(0) , mDelay(0.0) , mRemainingDelay(0.0) , mLastMouseX(0) , mLastMouseY(0) , mEnabled(true) , mFullHelp(false) , mShowOwned(0) , mFrameDuration(0.f) { getWidget(mDynamicToolTipBox, "DynamicToolTipBox"); mDynamicToolTipBox->setVisible(false); // turn off mouse focus so that getMouseFocusWidget returns the correct widget, // even if the mouse is over the tooltip mDynamicToolTipBox->setNeedMouseFocus(false); mMainWidget->setNeedMouseFocus(false); mDelay = Settings::Manager::getFloat("tooltip delay", "GUI"); mRemainingDelay = mDelay; for (unsigned int i=0; i < mMainWidget->getChildCount(); ++i) { mMainWidget->getChildAt(i)->setVisible(false); } mShowOwned = Settings::Manager::getInt("show owned", "Game"); } void ToolTips::setEnabled(bool enabled) { mEnabled = enabled; } void ToolTips::onFrame(float frameDuration) { mFrameDuration = frameDuration; } void ToolTips::update(float frameDuration) { while (mDynamicToolTipBox->getChildCount()) { MyGUI::Gui::getInstance().destroyWidget(mDynamicToolTipBox->getChildAt(0)); } // start by hiding everything for (unsigned int i=0; i < mMainWidget->getChildCount(); ++i) { mMainWidget->getChildAt(i)->setVisible(false); } const MyGUI::IntSize &viewSize = MyGUI::RenderManager::getInstance().getViewSize(); if (!mEnabled) { return; } MWBase::WindowManager *winMgr = MWBase::Environment::get().getWindowManager(); bool guiMode = winMgr->isGuiMode(); if (guiMode) { if (!winMgr->getCursorVisible()) return; const MyGUI::IntPoint& mousePos = MyGUI::InputManager::getInstance().getMousePosition(); if (winMgr->getWorldMouseOver() && (winMgr->isConsoleMode() || (winMgr->getMode() == GM_Container) || (winMgr->getMode() == GM_Inventory))) { if (mFocusObject.isEmpty ()) return; const MWWorld::Class& objectclass = mFocusObject.getClass(); MyGUI::IntSize tooltipSize; if (!objectclass.hasToolTip(mFocusObject) && winMgr->isConsoleMode()) { setCoord(0, 0, 300, 300); mDynamicToolTipBox->setVisible(true); ToolTipInfo info; info.caption = mFocusObject.getClass().getName(mFocusObject); if (info.caption.empty()) info.caption=mFocusObject.getCellRef().getRefId(); info.icon=""; tooltipSize = createToolTip(info, checkOwned()); } else tooltipSize = getToolTipViaPtr(mFocusObject.getRefData().getCount(), true); MyGUI::IntPoint tooltipPosition = MyGUI::InputManager::getInstance().getMousePosition(); position(tooltipPosition, tooltipSize, viewSize); setCoord(tooltipPosition.left, tooltipPosition.top, tooltipSize.width, tooltipSize.height); } else { if (mousePos.left == mLastMouseX && mousePos.top == mLastMouseY) { mRemainingDelay -= frameDuration; } else { mHorizontalScrollIndex = 0; mRemainingDelay = mDelay; } mLastMouseX = mousePos.left; mLastMouseY = mousePos.top; if (mRemainingDelay > 0) return; MyGUI::Widget* focus = MyGUI::InputManager::getInstance().getMouseFocusWidget(); if (focus == nullptr) return; MyGUI::IntSize tooltipSize; // try to go 1 level up until there is a widget that has tooltip // this is necessary because some skin elements are actually separate widgets int i=0; while (!focus->isUserString("ToolTipType")) { focus = focus->getParent(); if (!focus) return; ++i; } std::string type = focus->getUserString("ToolTipType"); if (type == "") { return; } // special handling for markers on the local map: the tooltip should only be visible // if the marker is not hidden due to the fog of war. if (type == "MapMarker") { LocalMapBase::MarkerUserData data = *focus->getUserData(); if (!data.isPositionExplored()) return; ToolTipInfo info; info.text = data.caption; info.notes = data.notes; tooltipSize = createToolTip(info); } else if (type == "ItemPtr") { mFocusObject = *focus->getUserData(); if (!mFocusObject) return; tooltipSize = getToolTipViaPtr(mFocusObject.getRefData().getCount(), false, checkOwned()); } else if (type == "ItemModelIndex") { std::pair pair = *focus->getUserData >(); mFocusObject = pair.second->getItem(pair.first).mBase; bool isAllowedToUse = pair.second->allowedToUseItems(); tooltipSize = getToolTipViaPtr(pair.second->getItem(pair.first).mCount, false, !isAllowedToUse); } else if (type == "ToolTipInfo") { tooltipSize = createToolTip(*focus->getUserData()); } else if (type == "AvatarItemSelection") { MyGUI::IntCoord avatarPos = focus->getAbsoluteCoord(); MyGUI::IntPoint relMousePos = MyGUI::InputManager::getInstance ().getMousePosition () - MyGUI::IntPoint(avatarPos.left, avatarPos.top); MWWorld::Ptr item = winMgr->getInventoryWindow ()->getAvatarSelectedItem (relMousePos.left, relMousePos.top); mFocusObject = item; if (!mFocusObject.isEmpty ()) tooltipSize = getToolTipViaPtr(mFocusObject.getRefData().getCount(), false); } else if (type == "Spell") { ToolTipInfo info; const ESM::Spell *spell = MWBase::Environment::get().getWorld()->getStore().get().find(focus->getUserString("Spell")); info.caption = spell->mName; Widgets::SpellEffectList effects; for (const ESM::ENAMstruct& spellEffect : spell->mEffects.mList) { Widgets::SpellEffectParams params; params.mEffectID = spellEffect.mEffectID; params.mSkill = spellEffect.mSkill; params.mAttribute = spellEffect.mAttribute; params.mDuration = spellEffect.mDuration; params.mMagnMin = spellEffect.mMagnMin; params.mMagnMax = spellEffect.mMagnMax; params.mRange = spellEffect.mRange; params.mArea = spellEffect.mArea; params.mIsConstant = (spell->mData.mType == ESM::Spell::ST_Ability); params.mNoTarget = false; effects.push_back(params); } if (MWMechanics::spellIncreasesSkill(spell)) // display school of spells that contribute to skill progress { MWWorld::Ptr player = MWMechanics::getPlayer(); int school = MWMechanics::getSpellSchool(spell, player); info.text = "#{sSchool}: " + sSchoolNames[school]; } std::string cost = focus->getUserString("SpellCost"); if (cost != "" && cost != "0") info.text += MWGui::ToolTips::getValueString(spell->mData.mCost, "#{sCastCost}"); info.effects = effects; tooltipSize = createToolTip(info); } else if (type == "Layout") { // tooltip defined in the layout MyGUI::Widget* tooltip; getWidget(tooltip, focus->getUserString("ToolTipLayout")); tooltip->setVisible(true); std::map userStrings = focus->getUserStrings(); for (auto& userStringPair : userStrings) { size_t underscorePos = userStringPair.first.find('_'); if (underscorePos == std::string::npos) continue; std::string key = userStringPair.first.substr(0, underscorePos); std::string widgetName = userStringPair.first.substr(underscorePos+1, userStringPair.first.size()-(underscorePos+1)); type = "Property"; size_t caretPos = key.find('^'); if (caretPos != std::string::npos) { type = key.substr(0, caretPos); key.erase(key.begin(), key.begin() + caretPos + 1); } MyGUI::Widget* w; getWidget(w, widgetName); if (type == "Property") w->setProperty(key, userStringPair.second); else if (type == "UserData") w->setUserString(key, userStringPair.second); } tooltipSize = tooltip->getSize(); tooltip->setCoord(0, 0, tooltipSize.width, tooltipSize.height); } else throw std::runtime_error ("unknown tooltip type"); MyGUI::IntPoint tooltipPosition = MyGUI::InputManager::getInstance().getMousePosition(); position(tooltipPosition, tooltipSize, viewSize); setCoord(tooltipPosition.left, tooltipPosition.top, tooltipSize.width, tooltipSize.height); } } else { if (!mFocusObject.isEmpty()) { MyGUI::IntSize tooltipSize = getToolTipViaPtr(mFocusObject.getRefData().getCount(), true, checkOwned()); setCoord(viewSize.width/2 - tooltipSize.width/2, std::max(0, int(mFocusToolTipY*viewSize.height - tooltipSize.height)), tooltipSize.width, tooltipSize.height); mDynamicToolTipBox->setVisible(true); } } } void ToolTips::position(MyGUI::IntPoint& position, MyGUI::IntSize size, MyGUI::IntSize viewportSize) { position += MyGUI::IntPoint(0, 32) - MyGUI::IntPoint(static_cast(MyGUI::InputManager::getInstance().getMousePosition().left / float(viewportSize.width) * size.width), 0); if ((position.left + size.width) > viewportSize.width) { position.left = viewportSize.width - size.width; } if ((position.top + size.height) > viewportSize.height) { position.top = MyGUI::InputManager::getInstance().getMousePosition().top - size.height - 8; } } void ToolTips::clear() { mFocusObject = MWWorld::Ptr(); while (mDynamicToolTipBox->getChildCount()) { MyGUI::Gui::getInstance().destroyWidget(mDynamicToolTipBox->getChildAt(0)); } for (unsigned int i=0; i < mMainWidget->getChildCount(); ++i) { mMainWidget->getChildAt(i)->setVisible(false); } } void ToolTips::setFocusObject(const MWWorld::Ptr& focus) { mFocusObject = focus; update(mFrameDuration); } MyGUI::IntSize ToolTips::getToolTipViaPtr (int count, bool image, bool isOwned) { // this the maximum width of the tooltip before it starts word-wrapping setCoord(0, 0, 300, 300); MyGUI::IntSize tooltipSize; const MWWorld::Class& object = mFocusObject.getClass(); if (!object.hasToolTip(mFocusObject)) { mDynamicToolTipBox->setVisible(false); } else { mDynamicToolTipBox->setVisible(true); ToolTipInfo info = object.getToolTipInfo(mFocusObject, count); if (!image) info.icon = ""; tooltipSize = createToolTip(info, isOwned); } return tooltipSize; } bool ToolTips::checkOwned() { if(mFocusObject.isEmpty()) return false; MWWorld::Ptr ptr = MWMechanics::getPlayer(); MWWorld::Ptr victim; MWBase::MechanicsManager* mm = MWBase::Environment::get().getMechanicsManager(); return !mm->isAllowedToUse(ptr, mFocusObject, victim); } MyGUI::IntSize ToolTips::createToolTip(const MWGui::ToolTipInfo& info, bool isOwned) { mDynamicToolTipBox->setVisible(true); if((mShowOwned == 1 || mShowOwned == 3) && isOwned) mDynamicToolTipBox->changeWidgetSkin(MWBase::Environment::get().getWindowManager()->isGuiMode() ? "HUD_Box_NoTransp_Owned" : "HUD_Box_Owned"); else mDynamicToolTipBox->changeWidgetSkin(MWBase::Environment::get().getWindowManager()->isGuiMode() ? "HUD_Box_NoTransp" : "HUD_Box"); std::string caption = info.caption; std::string image = info.icon; int imageSize = (image != "") ? info.imageSize : 0; std::string text = info.text; // remove the first newline (easier this way) if (text.size() > 0 && text[0] == '\n') text.erase(0, 1); const ESM::Enchantment* enchant = nullptr; const MWWorld::ESMStore& store = MWBase::Environment::get().getWorld()->getStore(); if (info.enchant != "") { enchant = store.get().search(info.enchant); if (enchant) { if (enchant->mData.mType == ESM::Enchantment::CastOnce) text += "\n#{sItemCastOnce}"; else if (enchant->mData.mType == ESM::Enchantment::WhenStrikes) text += "\n#{sItemCastWhenStrikes}"; else if (enchant->mData.mType == ESM::Enchantment::WhenUsed) text += "\n#{sItemCastWhenUsed}"; else if (enchant->mData.mType == ESM::Enchantment::ConstantEffect) text += "\n#{sItemCastConstant}"; } } // this the maximum width of the tooltip before it starts word-wrapping setCoord(0, 0, 300, 300); const MyGUI::IntPoint padding(8, 8); const int imageCaptionHPadding = (caption != "" ? 8 : 0); const int imageCaptionVPadding = (caption != "" ? 4 : 0); const int maximumWidth = MyGUI::RenderManager::getInstance().getViewSize().width - imageCaptionHPadding * 2; std::string realImage = MWBase::Environment::get().getWindowManager()->correctIconPath(image); Gui::EditBox* captionWidget = mDynamicToolTipBox->createWidget("NormalText", MyGUI::IntCoord(0, 0, 300, 300), MyGUI::Align::Left | MyGUI::Align::Top, "ToolTipCaption"); captionWidget->setEditStatic(true); captionWidget->setNeedKeyFocus(false); captionWidget->setCaptionWithReplacing(caption); MyGUI::IntSize captionSize = captionWidget->getTextSize(); int captionHeight = std::max(caption != "" ? captionSize.height : 0, imageSize); Gui::EditBox* textWidget = mDynamicToolTipBox->createWidget("SandText", MyGUI::IntCoord(0, captionHeight+imageCaptionVPadding, 300, 300-captionHeight-imageCaptionVPadding), MyGUI::Align::Stretch, "ToolTipText"); textWidget->setEditStatic(true); textWidget->setEditMultiLine(true); textWidget->setEditWordWrap(info.wordWrap); textWidget->setCaptionWithReplacing(text); textWidget->setTextAlign(MyGUI::Align::HCenter | MyGUI::Align::Top); textWidget->setNeedKeyFocus(false); MyGUI::IntSize textSize = textWidget->getTextSize(); captionSize += MyGUI::IntSize(imageSize, 0); // adjust for image MyGUI::IntSize totalSize = MyGUI::IntSize( std::min(std::max(textSize.width,captionSize.width + ((image != "") ? imageCaptionHPadding : 0)),maximumWidth), ((text != "") ? textSize.height + imageCaptionVPadding : 0) + captionHeight ); for (const std::string& note : info.notes) { MyGUI::ImageBox* icon = mDynamicToolTipBox->createWidget("MarkerButton", MyGUI::IntCoord(padding.left, totalSize.height+padding.top, 8, 8), MyGUI::Align::Default); icon->setColour(MyGUI::Colour(1.0f, 0.3f, 0.3f)); Gui::EditBox* edit = mDynamicToolTipBox->createWidget("SandText", MyGUI::IntCoord(padding.left+8+4, totalSize.height+padding.top, 300-padding.left-8-4, 300-totalSize.height), MyGUI::Align::Default); edit->setEditMultiLine(true); edit->setEditWordWrap(true); edit->setCaption(note); edit->setSize(edit->getWidth(), edit->getTextSize().height); icon->setPosition(icon->getLeft(),(edit->getTop()+edit->getBottom())/2-icon->getHeight()/2); totalSize.height += std::max(edit->getHeight(), icon->getHeight()); totalSize.width = std::max(totalSize.width, edit->getWidth()+8+4); } if (!info.effects.empty()) { MyGUI::Widget* effectArea = mDynamicToolTipBox->createWidget("", MyGUI::IntCoord(padding.left, totalSize.height, 300-padding.left, 300-totalSize.height), MyGUI::Align::Stretch); MyGUI::IntCoord coord(0, 6, totalSize.width, 24); Widgets::MWEffectListPtr effectsWidget = effectArea->createWidget ("MW_StatName", coord, MyGUI::Align::Default); effectsWidget->setEffectList(info.effects); std::vector effectItems; int flag = info.isPotion ? Widgets::MWEffectList::EF_NoTarget : 0; flag |= info.isIngredient ? Widgets::MWEffectList::EF_NoMagnitude : 0; effectsWidget->createEffectWidgets(effectItems, effectArea, coord, true, flag); totalSize.height += coord.top-6; totalSize.width = std::max(totalSize.width, coord.width); } if (enchant) { MyGUI::Widget* enchantArea = mDynamicToolTipBox->createWidget("", MyGUI::IntCoord(padding.left, totalSize.height, 300-padding.left, 300-totalSize.height), MyGUI::Align::Stretch); MyGUI::IntCoord coord(0, 6, totalSize.width, 24); Widgets::MWEffectListPtr enchantWidget = enchantArea->createWidget ("MW_StatName", coord, MyGUI::Align::Default); enchantWidget->setEffectList(Widgets::MWEffectList::effectListFromESM(&enchant->mEffects)); std::vector enchantEffectItems; int flag = (enchant->mData.mType == ESM::Enchantment::ConstantEffect) ? Widgets::MWEffectList::EF_Constant : 0; enchantWidget->createEffectWidgets(enchantEffectItems, enchantArea, coord, true, flag); totalSize.height += coord.top-6; totalSize.width = std::max(totalSize.width, coord.width); if (enchant->mData.mType == ESM::Enchantment::WhenStrikes || enchant->mData.mType == ESM::Enchantment::WhenUsed) { int maxCharge = enchant->mData.mCharge; int charge = (info.remainingEnchantCharge == -1) ? maxCharge : info.remainingEnchantCharge; const int chargeWidth = 204; MyGUI::TextBox* chargeText = enchantArea->createWidget("SandText", MyGUI::IntCoord(0, 0, 10, 18), MyGUI::Align::Default, "ToolTipEnchantChargeText"); chargeText->setCaptionWithReplacing("#{sCharges}"); const int chargeTextWidth = chargeText->getTextSize().width + 5; const int chargeAndTextWidth = chargeWidth + chargeTextWidth; totalSize.width = std::max(totalSize.width, chargeAndTextWidth); chargeText->setCoord((totalSize.width - chargeAndTextWidth)/2, coord.top+6, chargeTextWidth, 18); MyGUI::IntCoord chargeCoord; if (totalSize.width < chargeWidth) { totalSize.width = chargeWidth; chargeCoord = MyGUI::IntCoord(0, coord.top+6, chargeWidth, 18); } else { chargeCoord = MyGUI::IntCoord((totalSize.width - chargeAndTextWidth)/2 + chargeTextWidth, coord.top+6, chargeWidth, 18); } Widgets::MWDynamicStatPtr chargeWidget = enchantArea->createWidget ("MW_ChargeBar", chargeCoord, MyGUI::Align::Default); chargeWidget->setValue(charge, maxCharge); totalSize.height += 24; } } captionWidget->setCoord( (totalSize.width - captionSize.width)/2 + imageSize, (captionHeight-captionSize.height)/2, captionSize.width-imageSize, captionSize.height); //if its too long we do hscroll with the caption if (captionSize.width > maximumWidth) { mHorizontalScrollIndex = mHorizontalScrollIndex + 2; if (mHorizontalScrollIndex > captionSize.width){ mHorizontalScrollIndex = -totalSize.width; } int horizontal_scroll = mHorizontalScrollIndex; if (horizontal_scroll < 40){ horizontal_scroll = 40; }else{ horizontal_scroll = 80 - mHorizontalScrollIndex; } captionWidget->setPosition (MyGUI::IntPoint(horizontal_scroll, captionWidget->getPosition().top + padding.top)); } else { captionWidget->setPosition (captionWidget->getPosition() + padding); } textWidget->setPosition (textWidget->getPosition() + MyGUI::IntPoint(0, padding.top)); // only apply vertical padding, the horizontal works automatically due to Align::HCenter if (image != "") { MyGUI::ImageBox* imageWidget = mDynamicToolTipBox->createWidget("ImageBox", MyGUI::IntCoord((totalSize.width - captionSize.width - imageCaptionHPadding)/2, 0, imageSize, imageSize), MyGUI::Align::Left | MyGUI::Align::Top); imageWidget->setImageTexture(realImage); imageWidget->setPosition (imageWidget->getPosition() + padding); } totalSize += MyGUI::IntSize(padding.left*2, padding.top*2); return totalSize; } std::string ToolTips::toString(const float value) { std::ostringstream stream; if (value != int(value)) stream << std::setprecision(3); stream << value; return stream.str(); } std::string ToolTips::toString(const int value) { return std::to_string(value); } std::string ToolTips::getWeightString(const float weight, const std::string& prefix) { if (weight == 0) return ""; else return "\n" + prefix + ": " + toString(weight); } std::string ToolTips::getPercentString(const float value, const std::string& prefix) { if (value == 0) return ""; else return "\n" + prefix + ": " + toString(value*100) +"%"; } std::string ToolTips::getValueString(const int value, const std::string& prefix) { if (value == 0) return ""; else return "\n" + prefix + ": " + toString(value); } std::string ToolTips::getMiscString(const std::string& text, const std::string& prefix) { if (text == "") return ""; else return "\n" + prefix + ": " + text; } std::string ToolTips::getCountString(const int value) { if (value == 1) return ""; else return " (" + MyGUI::utility::toString(value) + ")"; } std::string ToolTips::getSoulString(const MWWorld::CellRef& cellref) { std::string soul = cellref.getSoul(); if (soul.empty()) return std::string(); const MWWorld::ESMStore &store = MWBase::Environment::get().getWorld()->getStore(); const ESM::Creature *creature = store.get().search(soul); if (!creature) return std::string(); if (creature->mName.empty()) return " (" + creature->mId + ")"; return " (" + creature->mName + ")"; } std::string ToolTips::getCellRefString(const MWWorld::CellRef& cellref) { std::string ret; ret += getMiscString(cellref.getOwner(), "Owner"); const std::string factionId = cellref.getFaction(); if (!factionId.empty()) { const MWWorld::ESMStore &store = MWBase::Environment::get().getWorld()->getStore(); const ESM::Faction *fact = store.get().search(factionId); if (fact != nullptr) { ret += getMiscString(fact->mName.empty() ? factionId : fact->mName, "Owner Faction"); if (cellref.getFactionRank() >= 0) { int rank = cellref.getFactionRank(); const std::string rankName = fact->mRanks[rank]; if (rankName.empty()) ret += getValueString(cellref.getFactionRank(), "Rank"); else ret += getMiscString(rankName, "Rank"); } } } std::vector > itemOwners = MWBase::Environment::get().getMechanicsManager()->getStolenItemOwners(cellref.getRefId()); for (std::pair& owner : itemOwners) { if (owner.second == std::numeric_limits::max()) ret += std::string("\nStolen from ") + owner.first; // for legacy (ESS) savegames else ret += std::string("\nStolen ") + MyGUI::utility::toString(owner.second) + " from " + owner.first; } ret += getMiscString(cellref.getGlobalVariable(), "Global"); return ret; } std::string ToolTips::getDurationString(float duration, const std::string& prefix) { std::string ret; ret = prefix + ": "; if (duration < 1.f) { ret += "0 s"; return ret; } constexpr int secondsPerMinute = 60; // 60 seconds constexpr int secondsPerHour = secondsPerMinute * 60; // 60 minutes constexpr int secondsPerDay = secondsPerHour * 24; // 24 hours constexpr int secondsPerMonth = secondsPerDay * 30; // 30 days constexpr int secondsPerYear = secondsPerDay * 365; int fullDuration = static_cast(duration); int units = 0; int years = fullDuration / secondsPerYear; int months = fullDuration % secondsPerYear / secondsPerMonth; int days = fullDuration % secondsPerYear % secondsPerMonth / secondsPerDay; // Because a year is not exactly 12 "months" int hours = fullDuration % secondsPerDay / secondsPerHour; int minutes = fullDuration % secondsPerHour / secondsPerMinute; int seconds = fullDuration % secondsPerMinute; if (years) { units++; ret += toString(years) + " y "; } if (months) { units++; ret += toString(months) + " mo "; } if (units < 2 && days) { units++; ret += toString(days) + " d "; } if (units < 2 && hours) { units++; ret += toString(hours) + " h "; } if (units >= 2) return ret; if (minutes) ret += toString(minutes) + " min "; if (seconds) ret += toString(seconds) + " s "; return ret; } bool ToolTips::toggleFullHelp() { mFullHelp = !mFullHelp; return mFullHelp; } bool ToolTips::getFullHelp() const { return mFullHelp; } void ToolTips::setFocusObjectScreenCoords(float min_x, float min_y, float max_x, float max_y) { mFocusToolTipX = (min_x + max_x) / 2; mFocusToolTipY = min_y; } void ToolTips::createSkillToolTip(MyGUI::Widget* widget, int skillId) { if (skillId == -1) return; const MWWorld::ESMStore &store = MWBase::Environment::get().getWorld()->getStore(); const std::string &skillNameId = ESM::Skill::sSkillNameIds[skillId]; const ESM::Skill* skill = store.get().find(skillId); assert(skill); const ESM::Attribute* attr = store.get().find(skill->mData.mAttribute); assert(attr); std::string icon = "icons\\k\\" + ESM::Skill::sIconNames[skillId]; widget->setUserString("ToolTipType", "Layout"); widget->setUserString("ToolTipLayout", "SkillNoProgressToolTip"); widget->setUserString("Caption_SkillNoProgressName", "#{"+skillNameId+"}"); widget->setUserString("Caption_SkillNoProgressDescription", skill->mDescription); widget->setUserString("Caption_SkillNoProgressAttribute", "#{sGoverningAttribute}: #{" + attr->mName + "}"); widget->setUserString("ImageTexture_SkillNoProgressImage", icon); } void ToolTips::createAttributeToolTip(MyGUI::Widget* widget, int attributeId) { if (attributeId == -1) return; std::string icon = ESM::Attribute::sAttributeIcons[attributeId]; std::string name = ESM::Attribute::sGmstAttributeIds[attributeId]; std::string desc = ESM::Attribute::sGmstAttributeDescIds[attributeId]; widget->setUserString("ToolTipType", "Layout"); widget->setUserString("ToolTipLayout", "AttributeToolTip"); widget->setUserString("Caption_AttributeName", "#{"+name+"}"); widget->setUserString("Caption_AttributeDescription", "#{"+desc+"}"); widget->setUserString("ImageTexture_AttributeImage", icon); } void ToolTips::createSpecializationToolTip(MyGUI::Widget* widget, const std::string& name, int specId) { widget->setUserString("Caption_Caption", name); std::string specText; // get all skills of this specialisation const MWWorld::Store &skills = MWBase::Environment::get().getWorld()->getStore().get(); bool isFirst = true; for (auto& skillPair : skills) { if (skillPair.second.mData.mSpecialization == specId) { if (isFirst) isFirst = false; else specText += "\n"; specText += std::string("#{") + ESM::Skill::sSkillNameIds[skillPair.first] + "}"; } } widget->setUserString("Caption_ColumnText", specText); widget->setUserString("ToolTipLayout", "SpecializationToolTip"); widget->setUserString("ToolTipType", "Layout"); } void ToolTips::createBirthsignToolTip(MyGUI::Widget* widget, const std::string& birthsignId) { const MWWorld::ESMStore &store = MWBase::Environment::get().getWorld()->getStore(); const ESM::BirthSign *sign = store.get().find(birthsignId); widget->setUserString("ToolTipType", "Layout"); widget->setUserString("ToolTipLayout", "BirthSignToolTip"); widget->setUserString("ImageTexture_BirthSignImage", MWBase::Environment::get().getWindowManager()->correctTexturePath(sign->mTexture)); std::string text; text += sign->mName; text += "\n#{fontcolourhtml=normal}" + sign->mDescription; std::vector abilities, powers, spells; for (const std::string& spellId : sign->mPowers.mList) { const ESM::Spell *spell = store.get().search(spellId); if (!spell) continue; // Skip spells which cannot be found ESM::Spell::SpellType type = static_cast(spell->mData.mType); if (type != ESM::Spell::ST_Spell && type != ESM::Spell::ST_Ability && type != ESM::Spell::ST_Power) continue; // We only want spell, ability and powers. if (type == ESM::Spell::ST_Ability) abilities.push_back(spellId); else if (type == ESM::Spell::ST_Power) powers.push_back(spellId); else if (type == ESM::Spell::ST_Spell) spells.push_back(spellId); } struct { const std::vector &spells; std::string label; } categories[3] = { {abilities, "sBirthsignmenu1"}, {powers, "sPowers"}, {spells, "sBirthsignmenu2"} }; for (int category = 0; category < 3; ++category) { bool addHeader = true; for (const std::string& spellId : categories[category].spells) { if (addHeader) { text += std::string("\n\n#{fontcolourhtml=header}") + std::string("#{") + categories[category].label + "}"; addHeader = false; } const ESM::Spell *spell = store.get().find(spellId); text += "\n#{fontcolourhtml=normal}" + spell->mName; } } widget->setUserString("Caption_BirthSignText", text); } void ToolTips::createRaceToolTip(MyGUI::Widget* widget, const ESM::Race* playerRace) { widget->setUserString("Caption_CenteredCaption", playerRace->mName); widget->setUserString("Caption_CenteredCaptionText", playerRace->mDescription); widget->setUserString("ToolTipType", "Layout"); widget->setUserString("ToolTipLayout", "RaceToolTip"); } void ToolTips::createClassToolTip(MyGUI::Widget* widget, const ESM::Class& playerClass) { if (playerClass.mName == "") return; int spec = playerClass.mData.mSpecialization; std::string specStr; if (spec == 0) specStr = "#{sSpecializationCombat}"; else if (spec == 1) specStr = "#{sSpecializationMagic}"; else if (spec == 2) specStr = "#{sSpecializationStealth}"; widget->setUserString("Caption_ClassName", playerClass.mName); widget->setUserString("Caption_ClassDescription", playerClass.mDescription); widget->setUserString("Caption_ClassSpecialisation", "#{sSpecialization}: " + specStr); widget->setUserString("ToolTipType", "Layout"); widget->setUserString("ToolTipLayout", "ClassToolTip"); } void ToolTips::createMagicEffectToolTip(MyGUI::Widget* widget, short id) { const ESM::MagicEffect* effect = MWBase::Environment::get().getWorld ()->getStore ().get().find(id); const std::string &name = ESM::MagicEffect::effectIdToString (id); std::string icon = effect->mIcon; int slashPos = icon.rfind('\\'); icon.insert(slashPos+1, "b_"); icon = MWBase::Environment::get().getWindowManager()->correctIconPath(icon); widget->setUserString("ToolTipType", "Layout"); widget->setUserString("ToolTipLayout", "MagicEffectToolTip"); widget->setUserString("Caption_MagicEffectName", "#{" + name + "}"); widget->setUserString("Caption_MagicEffectDescription", effect->mDescription); widget->setUserString("Caption_MagicEffectSchool", "#{sSchool}: " + sSchoolNames[effect->mData.mSchool]); widget->setUserString("ImageTexture_MagicEffectImage", icon); } void ToolTips::setDelay(float delay) { mDelay = delay; mRemainingDelay = mDelay; } } ================================================ FILE: apps/openmw/mwgui/tooltips.hpp ================================================ #ifndef MWGUI_TOOLTIPS_H #define MWGUI_TOOLTIPS_H #include "layout.hpp" #include "../mwworld/ptr.hpp" #include "widgets.hpp" namespace ESM { struct Class; struct Race; } namespace MWGui { // Info about tooltip that is supplied by the MWWorld::Class object struct ToolTipInfo { public: ToolTipInfo() : imageSize(32) , remainingEnchantCharge(-1) , isPotion(false) , isIngredient(false) , wordWrap(true) {} std::string caption; std::string text; std::string icon; int imageSize; // enchantment (for cloth, armor, weapons) std::string enchant; int remainingEnchantCharge; // effects (for potions, ingredients) Widgets::SpellEffectList effects; // local map notes std::vector notes; bool isPotion; // potions do not show target in the tooltip bool isIngredient; // ingredients have no effect magnitude bool wordWrap; }; class ToolTips : public Layout { public: ToolTips(); void onFrame(float frameDuration); void update(float frameDuration); void setEnabled(bool enabled); bool toggleFullHelp(); ///< show extra info in item tooltips (owner, script) bool getFullHelp() const; void setDelay(float delay); void clear(); void setFocusObject(const MWWorld::Ptr& focus); void setFocusObjectScreenCoords(float min_x, float min_y, float max_x, float max_y); ///< set the screen-space position of the tooltip for focused object static std::string getWeightString(const float weight, const std::string& prefix); static std::string getPercentString(const float value, const std::string& prefix); static std::string getValueString(const int value, const std::string& prefix); ///< @return "prefix: value" or "" if value is 0 static std::string getMiscString(const std::string& text, const std::string& prefix); ///< @return "prefix: text" or "" if text is empty static std::string toString(const float value); static std::string toString(const int value); static std::string getCountString(const int value); ///< @return blank string if count is 1, or else " (value)" static std::string getSoulString(const MWWorld::CellRef& cellref); ///< Returns a string containing the name of the creature that the ID in the cellref's soul field belongs to. static std::string getCellRefString(const MWWorld::CellRef& cellref); ///< Returns a string containing debug tooltip information about the given cellref. static std::string getDurationString (float duration, const std::string& prefix); ///< Returns duration as two largest time units, rounded down. Note: not localized; no line break. // these do not create an actual tooltip, but they fill in the data that is required so the tooltip // system knows what to show in case this widget is hovered static void createSkillToolTip(MyGUI::Widget* widget, int skillId); static void createAttributeToolTip(MyGUI::Widget* widget, int attributeId); static void createSpecializationToolTip(MyGUI::Widget* widget, const std::string& name, int specId); static void createBirthsignToolTip(MyGUI::Widget* widget, const std::string& birthsignId); static void createRaceToolTip(MyGUI::Widget* widget, const ESM::Race* playerRace); static void createClassToolTip(MyGUI::Widget* widget, const ESM::Class& playerClass); static void createMagicEffectToolTip(MyGUI::Widget* widget, short id); bool checkOwned(); /// Returns True if taking mFocusObject would be crime private: MyGUI::Widget* mDynamicToolTipBox; MWWorld::Ptr mFocusObject; MyGUI::IntSize getToolTipViaPtr (int count, bool image = true, bool isOwned = false); ///< @return requested tooltip size MyGUI::IntSize createToolTip(const ToolTipInfo& info, bool isOwned = false); ///< @return requested tooltip size /// @param isFocusObject Is the object this tooltips originates from mFocusObject? float mFocusToolTipX; float mFocusToolTipY; /// Adjust position for a tooltip so that it doesn't leave the screen and does not obscure the mouse cursor void position(MyGUI::IntPoint& position, MyGUI::IntSize size, MyGUI::IntSize viewportSize); static std::string sSchoolNames[6]; int mHorizontalScrollIndex; float mDelay; float mRemainingDelay; // remaining time until tooltip will show int mLastMouseX; int mLastMouseY; bool mEnabled; bool mFullHelp; int mShowOwned; float mFrameDuration; }; } #endif ================================================ FILE: apps/openmw/mwgui/tradeitemmodel.cpp ================================================ #include "tradeitemmodel.hpp" #include #include #include "../mwworld/class.hpp" #include "../mwworld/containerstore.hpp" #include "../mwworld/inventorystore.hpp" namespace MWGui { TradeItemModel::TradeItemModel(ItemModel *sourceModel, const MWWorld::Ptr& merchant) : mMerchant(merchant) { mSourceModel = sourceModel; } bool TradeItemModel::allowedToUseItems() const { return true; } ItemStack TradeItemModel::getItem (ModelIndex index) { if (index < 0) throw std::runtime_error("Invalid index supplied"); if (mItems.size() <= static_cast(index)) throw std::runtime_error("Item index out of range"); return mItems[index]; } size_t TradeItemModel::getItemCount() { return mItems.size(); } void TradeItemModel::borrowImpl(const ItemStack &item, std::vector &out) { bool found = false; for (ItemStack& itemStack : out) { if (itemStack.mBase == item.mBase) { itemStack.mCount += item.mCount; found = true; break; } } if (!found) out.push_back(item); } void TradeItemModel::unborrowImpl(const ItemStack &item, size_t count, std::vector &out) { std::vector::iterator it = out.begin(); bool found = false; for (; it != out.end(); ++it) { if (it->mBase == item.mBase) { if (it->mCount < count) throw std::runtime_error("Not enough borrowed items to return"); it->mCount -= count; if (it->mCount == 0) out.erase(it); found = true; break; } } if (!found) throw std::runtime_error("Can't find borrowed item to return"); } void TradeItemModel::borrowItemFromUs (ModelIndex itemIndex, size_t count) { ItemStack item = getItem(itemIndex); item.mCount = count; borrowImpl(item, mBorrowedFromUs); } void TradeItemModel::borrowItemToUs (ModelIndex itemIndex, ItemModel* source, size_t count) { ItemStack item = source->getItem(itemIndex); item.mCount = count; borrowImpl(item, mBorrowedToUs); } void TradeItemModel::returnItemBorrowedToUs (ModelIndex itemIndex, size_t count) { ItemStack item = getItem(itemIndex); unborrowImpl(item, count, mBorrowedToUs); } void TradeItemModel::returnItemBorrowedFromUs (ModelIndex itemIndex, ItemModel* source, size_t count) { ItemStack item = source->getItem(itemIndex); unborrowImpl(item, count, mBorrowedFromUs); } void TradeItemModel::adjustEncumbrance(float &encumbrance) { for (ItemStack& itemStack : mBorrowedToUs) { MWWorld::Ptr& item = itemStack.mBase; encumbrance += item.getClass().getWeight(item) * itemStack.mCount; } for (ItemStack& itemStack : mBorrowedFromUs) { MWWorld::Ptr& item = itemStack.mBase; encumbrance -= item.getClass().getWeight(item) * itemStack.mCount; } encumbrance = std::max(0.f, encumbrance); } void TradeItemModel::abort() { mBorrowedFromUs.clear(); mBorrowedToUs.clear(); } const std::vector TradeItemModel::getItemsBorrowedToUs() const { return mBorrowedToUs; } void TradeItemModel::transferItems() { for (ItemStack& itemStack : mBorrowedToUs) { // get index in the source model ItemModel* sourceModel = itemStack.mCreator; size_t i=0; for (; igetItemCount(); ++i) { if (itemStack.mBase == sourceModel->getItem(i).mBase) break; } if (i == sourceModel->getItemCount()) throw std::runtime_error("The borrowed item disappeared"); const ItemStack& item = sourceModel->getItem(i); static const bool prevent = Settings::Manager::getBool("prevent merchant equipping", "Game"); // copy the borrowed items to our model copyItem(item, itemStack.mCount, !prevent); // then remove them from the source model sourceModel->removeItem(item, itemStack.mCount); } mBorrowedToUs.clear(); mBorrowedFromUs.clear(); } void TradeItemModel::update() { mSourceModel->update(); int services = 0; if (!mMerchant.isEmpty()) services = mMerchant.getClass().getServices(mMerchant); mItems.clear(); // add regular items for (size_t i=0; igetItemCount(); ++i) { ItemStack item = mSourceModel->getItem(i); if(!mMerchant.isEmpty()) { MWWorld::Ptr base = item.mBase; if(Misc::StringUtils::ciEqual(base.getCellRef().getRefId(), MWWorld::ContainerStore::sGoldId)) continue; if (!base.getClass().showsInInventory(base)) return; if(!base.getClass().canSell(base, services)) continue; // Bound items may not be bought if (item.mFlags & ItemStack::Flag_Bound) continue; // don't show equipped items if(mMerchant.getClass().hasInventoryStore(mMerchant)) { MWWorld::InventoryStore& store = mMerchant.getClass().getInventoryStore(mMerchant); if (store.isEquipped(base)) continue; } } // don't show items that we borrowed to someone else for (ItemStack& itemStack : mBorrowedFromUs) { if (itemStack.mBase == item.mBase) { if (item.mCount < itemStack.mCount) throw std::runtime_error("Lent more items than present"); item.mCount -= itemStack.mCount; } } if (item.mCount > 0) mItems.push_back(item); } // add items borrowed to us for (ItemStack& itemStack : mBorrowedToUs) { itemStack.mType = ItemStack::Type_Barter; mItems.push_back(itemStack); } } } ================================================ FILE: apps/openmw/mwgui/tradeitemmodel.hpp ================================================ #ifndef MWGUI_TRADE_ITEM_MODEL_H #define MWGUI_TRADE_ITEM_MODEL_H #include "itemmodel.hpp" namespace MWGui { class ItemModel; /// @brief An item model that allows 'borrowing' items from another item model. Used for previewing barter offers. /// Also filters items that the merchant does not sell. class TradeItemModel : public ProxyItemModel { public: TradeItemModel (ItemModel* sourceModel, const MWWorld::Ptr& merchant); bool allowedToUseItems() const override; ItemStack getItem (ModelIndex index) override; size_t getItemCount() override; void update() override; void borrowItemFromUs (ModelIndex itemIndex, size_t count); void borrowItemToUs (ModelIndex itemIndex, ItemModel* source, size_t count); ///< @note itemIndex points to an item in \a source void returnItemBorrowedToUs (ModelIndex itemIndex, size_t count); void returnItemBorrowedFromUs (ModelIndex itemIndex, ItemModel* source, size_t count); /// Permanently transfers items that were borrowed to us from another model to this model void transferItems (); /// Aborts trade void abort(); /// Adjusts the given encumbrance by adding weight for items that have been lent to us, /// and removing weight for items we've lent to someone else. void adjustEncumbrance (float& encumbrance); const std::vector getItemsBorrowedToUs() const; private: void borrowImpl(const ItemStack& item, std::vector& out); void unborrowImpl(const ItemStack& item, size_t count, std::vector& out); std::vector mItems; std::vector mBorrowedToUs; std::vector mBorrowedFromUs; MWWorld::Ptr mMerchant; }; } #endif ================================================ FILE: apps/openmw/mwgui/tradewindow.cpp ================================================ #include "tradewindow.hpp" #include #include #include #include #include /* Start of tes3mp addition Include additional headers for multiplayer purposes */ #include "../mwmp/Main.hpp" #include "../mwmp/Networking.hpp" #include "../mwmp/ObjectList.hpp" /* End of tes3mp addition */ #include "../mwbase/environment.hpp" #include "../mwbase/world.hpp" #include "../mwbase/windowmanager.hpp" #include "../mwbase/mechanicsmanager.hpp" #include "../mwbase/dialoguemanager.hpp" #include "../mwworld/class.hpp" #include "../mwworld/containerstore.hpp" #include "../mwworld/esmstore.hpp" #include "../mwmechanics/actorutil.hpp" #include "../mwmechanics/creaturestats.hpp" #include "inventorywindow.hpp" #include "itemview.hpp" #include "sortfilteritemmodel.hpp" #include "containeritemmodel.hpp" #include "tradeitemmodel.hpp" #include "countdialog.hpp" #include "tooltips.hpp" namespace { int getEffectiveValue (MWWorld::Ptr item, int count) { float price = static_cast(item.getClass().getValue(item)); if (item.getClass().hasItemHealth(item)) { price *= item.getClass().getItemNormalizedHealth(item); } return static_cast(price * count); } } namespace MWGui { TradeWindow::TradeWindow() : WindowBase("openmw_trade_window.layout") , mSortModel(nullptr) , mTradeModel(nullptr) , mItemToSell(-1) , mCurrentBalance(0) , mCurrentMerchantOffer(0) { getWidget(mFilterAll, "AllButton"); getWidget(mFilterWeapon, "WeaponButton"); getWidget(mFilterApparel, "ApparelButton"); getWidget(mFilterMagic, "MagicButton"); getWidget(mFilterMisc, "MiscButton"); getWidget(mMaxSaleButton, "MaxSaleButton"); getWidget(mCancelButton, "CancelButton"); getWidget(mOfferButton, "OfferButton"); getWidget(mPlayerGold, "PlayerGold"); getWidget(mMerchantGold, "MerchantGold"); getWidget(mIncreaseButton, "IncreaseButton"); getWidget(mDecreaseButton, "DecreaseButton"); getWidget(mTotalBalance, "TotalBalance"); getWidget(mTotalBalanceLabel, "TotalBalanceLabel"); getWidget(mBottomPane, "BottomPane"); getWidget(mFilterEdit, "FilterEdit"); getWidget(mItemView, "ItemView"); mItemView->eventItemClicked += MyGUI::newDelegate(this, &TradeWindow::onItemSelected); mFilterAll->setStateSelected(true); mFilterAll->eventMouseButtonClick += MyGUI::newDelegate(this, &TradeWindow::onFilterChanged); mFilterWeapon->eventMouseButtonClick += MyGUI::newDelegate(this, &TradeWindow::onFilterChanged); mFilterApparel->eventMouseButtonClick += MyGUI::newDelegate(this, &TradeWindow::onFilterChanged); mFilterMagic->eventMouseButtonClick += MyGUI::newDelegate(this, &TradeWindow::onFilterChanged); mFilterMisc->eventMouseButtonClick += MyGUI::newDelegate(this, &TradeWindow::onFilterChanged); mFilterEdit->eventEditTextChange += MyGUI::newDelegate(this, &TradeWindow::onNameFilterChanged); mCancelButton->eventMouseButtonClick += MyGUI::newDelegate(this, &TradeWindow::onCancelButtonClicked); mOfferButton->eventMouseButtonClick += MyGUI::newDelegate(this, &TradeWindow::onOfferButtonClicked); mMaxSaleButton->eventMouseButtonClick += MyGUI::newDelegate(this, &TradeWindow::onMaxSaleButtonClicked); mIncreaseButton->eventMouseButtonPressed += MyGUI::newDelegate(this, &TradeWindow::onIncreaseButtonPressed); mIncreaseButton->eventMouseButtonReleased += MyGUI::newDelegate(this, &TradeWindow::onBalanceButtonReleased); mDecreaseButton->eventMouseButtonPressed += MyGUI::newDelegate(this, &TradeWindow::onDecreaseButtonPressed); mDecreaseButton->eventMouseButtonReleased += MyGUI::newDelegate(this, &TradeWindow::onBalanceButtonReleased); mTotalBalance->eventValueChanged += MyGUI::newDelegate(this, &TradeWindow::onBalanceValueChanged); mTotalBalance->eventEditSelectAccept += MyGUI::newDelegate(this, &TradeWindow::onAccept); mTotalBalance->setMinValue(std::numeric_limits::min()+1); // disallow INT_MIN since abs(INT_MIN) is undefined setCoord(400, 0, 400, 300); } void TradeWindow::setPtr(const MWWorld::Ptr& actor) { mPtr = actor; mCurrentBalance = 0; mCurrentMerchantOffer = 0; std::vector itemSources; // Important: actor goes first, so purchased items come out of the actor's pocket first itemSources.push_back(actor); MWBase::Environment::get().getWorld()->getContainersOwnedBy(actor, itemSources); std::vector worldItems; MWBase::Environment::get().getWorld()->getItemsOwnedBy(actor, worldItems); mTradeModel = new TradeItemModel(new ContainerItemModel(itemSources, worldItems), mPtr); mSortModel = new SortFilterItemModel(mTradeModel); mItemView->setModel (mSortModel); mItemView->resetScrollBars(); updateLabels(); setTitle(actor.getClass().getName(actor)); onFilterChanged(mFilterAll); mFilterEdit->setCaption(""); } void TradeWindow::onFrame(float dt) { checkReferenceAvailable(); } void TradeWindow::onNameFilterChanged(MyGUI::EditBox* _sender) { mSortModel->setNameFilter(_sender->getCaption()); mItemView->update(); } void TradeWindow::onFilterChanged(MyGUI::Widget* _sender) { if (_sender == mFilterAll) mSortModel->setCategory(SortFilterItemModel::Category_All); else if (_sender == mFilterWeapon) mSortModel->setCategory(SortFilterItemModel::Category_Weapon); else if (_sender == mFilterApparel) mSortModel->setCategory(SortFilterItemModel::Category_Apparel); else if (_sender == mFilterMagic) mSortModel->setCategory(SortFilterItemModel::Category_Magic); else if (_sender == mFilterMisc) mSortModel->setCategory(SortFilterItemModel::Category_Misc); mFilterAll->setStateSelected(false); mFilterWeapon->setStateSelected(false); mFilterApparel->setStateSelected(false); mFilterMagic->setStateSelected(false); mFilterMisc->setStateSelected(false); _sender->castType()->setStateSelected(true); mItemView->update(); } int TradeWindow::getMerchantServices() { return mPtr.getClass().getServices(mPtr); } bool TradeWindow::exit() { mTradeModel->abort(); MWBase::Environment::get().getWindowManager()->getInventoryWindow()->getTradeModel()->abort(); return true; } void TradeWindow::onItemSelected (int index) { const ItemStack& item = mSortModel->getItem(index); MWWorld::Ptr object = item.mBase; int count = item.mCount; bool shift = MyGUI::InputManager::getInstance().isShiftPressed(); if (MyGUI::InputManager::getInstance().isControlPressed()) count = 1; if (count > 1 && !shift) { CountDialog* dialog = MWBase::Environment::get().getWindowManager()->getCountDialog(); std::string message = "#{sQuanityMenuMessage02}"; std::string name = object.getClass().getName(object) + MWGui::ToolTips::getSoulString(object.getCellRef()); dialog->openCountDialog(name, message, count); dialog->eventOkClicked.clear(); dialog->eventOkClicked += MyGUI::newDelegate(this, &TradeWindow::sellItem); mItemToSell = mSortModel->mapToSource(index); } else { mItemToSell = mSortModel->mapToSource(index); sellItem (nullptr, count); } } void TradeWindow::sellItem(MyGUI::Widget* sender, int count) { const ItemStack& item = mTradeModel->getItem(mItemToSell); std::string sound = item.mBase.getClass().getUpSoundId(item.mBase); MWBase::Environment::get().getWindowManager()->playSound(sound); TradeItemModel* playerTradeModel = MWBase::Environment::get().getWindowManager()->getInventoryWindow()->getTradeModel(); if (item.mType == ItemStack::Type_Barter) { // this was an item borrowed to us by the player mTradeModel->returnItemBorrowedToUs(mItemToSell, count); playerTradeModel->returnItemBorrowedFromUs(mItemToSell, mTradeModel, count); buyFromNpc(item.mBase, count, true); } else { // borrow item to player playerTradeModel->borrowItemToUs(mItemToSell, mTradeModel, count); mTradeModel->borrowItemFromUs(mItemToSell, count); buyFromNpc(item.mBase, count, false); } MWBase::Environment::get().getWindowManager()->getInventoryWindow()->updateItemView(); mItemView->update(); } void TradeWindow::borrowItem (int index, size_t count) { TradeItemModel* playerTradeModel = MWBase::Environment::get().getWindowManager()->getInventoryWindow()->getTradeModel(); mTradeModel->borrowItemToUs(index, playerTradeModel, count); mItemView->update(); sellToNpc(playerTradeModel->getItem(index).mBase, count, false); } void TradeWindow::returnItem (int index, size_t count) { TradeItemModel* playerTradeModel = MWBase::Environment::get().getWindowManager()->getInventoryWindow()->getTradeModel(); const ItemStack& item = playerTradeModel->getItem(index); mTradeModel->returnItemBorrowedFromUs(index, playerTradeModel, count); mItemView->update(); sellToNpc(item.mBase, count, true); } void TradeWindow::addOrRemoveGold(int amount, const MWWorld::Ptr& actor) { MWWorld::ContainerStore& store = actor.getClass().getContainerStore(actor); if (amount > 0) { store.add(MWWorld::ContainerStore::sGoldId, amount, actor); } else { store.remove(MWWorld::ContainerStore::sGoldId, - amount, actor); } } void TradeWindow::onOfferButtonClicked(MyGUI::Widget* _sender) { TradeItemModel* playerItemModel = MWBase::Environment::get().getWindowManager()->getInventoryWindow()->getTradeModel(); const MWWorld::Store &gmst = MWBase::Environment::get().getWorld()->getStore().get(); // were there any items traded at all? const std::vector& playerBought = playerItemModel->getItemsBorrowedToUs(); const std::vector& merchantBought = mTradeModel->getItemsBorrowedToUs(); if (playerBought.empty() && merchantBought.empty()) { // user notification MWBase::Environment::get().getWindowManager()-> messageBox("#{sBarterDialog11}"); return; } MWWorld::Ptr player = MWMechanics::getPlayer(); int playerGold = player.getClass().getContainerStore(player).count(MWWorld::ContainerStore::sGoldId); // check if the player can afford this if (mCurrentBalance < 0 && playerGold < std::abs(mCurrentBalance)) { // user notification MWBase::Environment::get().getWindowManager()-> messageBox("#{sBarterDialog1}"); return; } // check if the merchant can afford this if (mCurrentBalance > 0 && getMerchantGold() < mCurrentBalance) { // user notification MWBase::Environment::get().getWindowManager()-> messageBox("#{sBarterDialog2}"); return; } // check if the player is attempting to sell back an item stolen from this actor for (const ItemStack& itemStack : merchantBought) { if (MWBase::Environment::get().getMechanicsManager()->isItemStolenFrom(itemStack.mBase.getCellRef().getRefId(), mPtr)) { std::string msg = gmst.find("sNotifyMessage49")->mValue.getString(); msg = Misc::StringUtils::format(msg, itemStack.mBase.getClass().getName(itemStack.mBase)); MWBase::Environment::get().getWindowManager()->messageBox(msg); MWBase::Environment::get().getMechanicsManager()->confiscateStolenItemToOwner(player, itemStack.mBase, mPtr, itemStack.mCount); onCancelButtonClicked(mCancelButton); MWBase::Environment::get().getWindowManager()->exitCurrentGuiMode(); return; } } bool offerAccepted = mTrading.haggle(player, mPtr, mCurrentBalance, mCurrentMerchantOffer); // apply disposition change if merchant is NPC if ( mPtr.getClass().isNpc() ) { int dispositionDelta = offerAccepted ? gmst.find("iBarterSuccessDisposition")->mValue.getInteger() : gmst.find("iBarterFailDisposition")->mValue.getInteger(); MWBase::Environment::get().getDialogueManager()->applyBarterDispositionChange(dispositionDelta); } // display message on haggle failure if ( !offerAccepted ) { MWBase::Environment::get().getWindowManager()-> messageBox("#{sNotifyMessage9}"); return; } // make the item transfer mTradeModel->transferItems(); playerItemModel->transferItems(); // transfer the gold if (mCurrentBalance != 0) { addOrRemoveGold(mCurrentBalance, player); /* Start of tes3mp change (major) Don't unilaterally change the merchant's gold pool on our client and instead let the server do it */ //mPtr.getClass().getCreatureStats(mPtr).setGoldPool( // mPtr.getClass().getCreatureStats(mPtr).getGoldPool() - mCurrentBalance); mwmp::ObjectList *objectList = mwmp::Main::get().getNetworking()->getObjectList(); objectList->reset(); objectList->packetOrigin = mwmp::CLIENT_GAMEPLAY; MWMechanics::CreatureStats& merchantCreatureStats = mPtr.getClass().getCreatureStats(mPtr); objectList->addObjectMiscellaneous(mPtr, merchantCreatureStats.getGoldPool() - mCurrentBalance, merchantCreatureStats.getLastRestockTime().getHour(), merchantCreatureStats.getLastRestockTime().getDay()); objectList->sendObjectMiscellaneous(); /* End of tes3mp change (major) */ } eventTradeDone(); MWBase::Environment::get().getWindowManager()->playSound("Item Gold Up"); MWBase::Environment::get().getWindowManager()->removeGuiMode(GM_Barter); } void TradeWindow::onAccept(MyGUI::EditBox *sender) { onOfferButtonClicked(sender); // To do not spam onAccept() again and again MWBase::Environment::get().getWindowManager()->injectKeyRelease(MyGUI::KeyCode::None); } void TradeWindow::onCancelButtonClicked(MyGUI::Widget* _sender) { exit(); MWBase::Environment::get().getWindowManager()->removeGuiMode(GM_Barter); } void TradeWindow::onMaxSaleButtonClicked(MyGUI::Widget* _sender) { mCurrentBalance = getMerchantGold(); updateLabels(); } void TradeWindow::addRepeatController(MyGUI::Widget *widget) { MyGUI::ControllerItem* item = MyGUI::ControllerManager::getInstance().createItem(MyGUI::ControllerRepeatClick::getClassTypeName()); MyGUI::ControllerRepeatClick* controller = static_cast(item); controller->eventRepeatClick += newDelegate(this, &TradeWindow::onRepeatClick); MyGUI::ControllerManager::getInstance().addItem(widget, controller); } void TradeWindow::onIncreaseButtonPressed(MyGUI::Widget* _sender, int _left, int _top, MyGUI::MouseButton _id) { addRepeatController(_sender); onIncreaseButtonTriggered(); } void TradeWindow::onDecreaseButtonPressed(MyGUI::Widget* _sender, int _left, int _top, MyGUI::MouseButton _id) { addRepeatController(_sender); onDecreaseButtonTriggered(); } void TradeWindow::onRepeatClick(MyGUI::Widget* widget, MyGUI::ControllerItem* controller) { if (widget == mIncreaseButton) onIncreaseButtonTriggered(); else if (widget == mDecreaseButton) onDecreaseButtonTriggered(); } void TradeWindow::onBalanceButtonReleased(MyGUI::Widget *_sender, int _left, int _top, MyGUI::MouseButton _id) { MyGUI::ControllerManager::getInstance().removeItem(_sender); } void TradeWindow::onBalanceValueChanged(int value) { // Entering a "-" sign inverts the buying/selling state mCurrentBalance = (mCurrentBalance >= 0 ? 1 : -1) * value; updateLabels(); if (value != std::abs(value)) mTotalBalance->setValue(std::abs(value)); } void TradeWindow::onIncreaseButtonTriggered() { // prevent overflows, and prevent entering INT_MIN since abs(INT_MIN) is undefined if (mCurrentBalance == std::numeric_limits::max() || mCurrentBalance == std::numeric_limits::min()+1) return; if (mCurrentBalance < 0) mCurrentBalance -= 1; else mCurrentBalance += 1; updateLabels(); } void TradeWindow::onDecreaseButtonTriggered() { if (mCurrentBalance < 0) mCurrentBalance += 1; else mCurrentBalance -= 1; updateLabels(); } void TradeWindow::updateLabels() { MWWorld::Ptr player = MWMechanics::getPlayer(); int playerGold = player.getClass().getContainerStore(player).count(MWWorld::ContainerStore::sGoldId); mPlayerGold->setCaptionWithReplacing("#{sYourGold} " + MyGUI::utility::toString(playerGold)); if (mCurrentBalance < 0) { mTotalBalanceLabel->setCaptionWithReplacing("#{sTotalCost}"); } else { mTotalBalanceLabel->setCaptionWithReplacing("#{sTotalSold}"); } mTotalBalance->setValue(std::abs(mCurrentBalance)); mMerchantGold->setCaptionWithReplacing("#{sSellerGold} " + MyGUI::utility::toString(getMerchantGold())); } void TradeWindow::updateOffer() { TradeItemModel* playerTradeModel = MWBase::Environment::get().getWindowManager()->getInventoryWindow()->getTradeModel(); int merchantOffer = 0; // The offered price must be capped at 75% of the base price to avoid exploits // connected to buying and selling the same item. // This value has been determined by researching the limitations of the vanilla formula // and may not be sufficient if getBarterOffer behavior has been changed. const std::vector& playerBorrowed = playerTradeModel->getItemsBorrowedToUs(); for (const ItemStack& itemStack : playerBorrowed) { const int basePrice = getEffectiveValue(itemStack.mBase, itemStack.mCount); const int cap = static_cast(std::max(1.f, 0.75f * basePrice)); // Minimum buying price -- 75% of the base const int buyingPrice = MWBase::Environment::get().getMechanicsManager()->getBarterOffer(mPtr, basePrice, true); merchantOffer -= std::max(cap, buyingPrice); } const std::vector& merchantBorrowed = mTradeModel->getItemsBorrowedToUs(); for (const ItemStack& itemStack : merchantBorrowed) { const int basePrice = getEffectiveValue(itemStack.mBase, itemStack.mCount); const int cap = static_cast(std::max(1.f, 0.75f * basePrice)); // Maximum selling price -- 75% of the base const int sellingPrice = MWBase::Environment::get().getMechanicsManager()->getBarterOffer(mPtr, basePrice, false); merchantOffer += mPtr.getClass().isNpc() ? std::min(cap, sellingPrice) : sellingPrice; } int diff = merchantOffer - mCurrentMerchantOffer; mCurrentMerchantOffer = merchantOffer; mCurrentBalance += diff; updateLabels(); } void TradeWindow::sellToNpc(const MWWorld::Ptr& item, int count, bool boughtItem) { updateOffer(); } void TradeWindow::buyFromNpc(const MWWorld::Ptr& item, int count, bool soldItem) { updateOffer(); } void TradeWindow::onReferenceUnavailable() { // remove both Trade and Dialogue (since you always trade with the NPC/creature that you have previously talked to) MWBase::Environment::get().getWindowManager()->removeGuiMode(GM_Barter); MWBase::Environment::get().getWindowManager()->exitCurrentGuiMode(); } int TradeWindow::getMerchantGold() { int merchantGold = mPtr.getClass().getCreatureStats(mPtr).getGoldPool(); return merchantGold; } void TradeWindow::resetReference() { ReferenceInterface::resetReference(); mItemView->setModel(nullptr); mTradeModel = nullptr; mSortModel = nullptr; } void TradeWindow::onClose() { // Make sure the window was actually closed and not temporarily hidden. if (MWBase::Environment::get().getWindowManager()->containsMode(GM_Barter)) return; resetReference(); } } ================================================ FILE: apps/openmw/mwgui/tradewindow.hpp ================================================ #ifndef MWGUI_TRADEWINDOW_H #define MWGUI_TRADEWINDOW_H #include "../mwmechanics/trading.hpp" #include "referenceinterface.hpp" #include "windowbase.hpp" namespace Gui { class NumericEditBox; } namespace MyGUI { class ControllerItem; } namespace MWGui { class ItemView; class SortFilterItemModel; class TradeItemModel; class TradeWindow : public WindowBase, public ReferenceInterface { public: TradeWindow(); void setPtr(const MWWorld::Ptr& actor) override; void onClose() override; void onFrame(float dt) override; void clear() override { resetReference(); } void borrowItem (int index, size_t count); void returnItem (int index, size_t count); int getMerchantServices(); bool exit() override; void resetReference() override; typedef MyGUI::delegates::CMultiDelegate0 EventHandle_TradeDone; EventHandle_TradeDone eventTradeDone; private: ItemView* mItemView; SortFilterItemModel* mSortModel; TradeItemModel* mTradeModel; MWMechanics::Trading mTrading; static const float sBalanceChangeInitialPause; // in seconds static const float sBalanceChangeInterval; // in seconds MyGUI::Button* mFilterAll; MyGUI::Button* mFilterWeapon; MyGUI::Button* mFilterApparel; MyGUI::Button* mFilterMagic; MyGUI::Button* mFilterMisc; MyGUI::EditBox* mFilterEdit; MyGUI::Button* mIncreaseButton; MyGUI::Button* mDecreaseButton; MyGUI::TextBox* mTotalBalanceLabel; Gui::NumericEditBox* mTotalBalance; MyGUI::Widget* mBottomPane; MyGUI::Button* mMaxSaleButton; MyGUI::Button* mCancelButton; MyGUI::Button* mOfferButton; MyGUI::TextBox* mPlayerGold; MyGUI::TextBox* mMerchantGold; int mItemToSell; int mCurrentBalance; int mCurrentMerchantOffer; void sellToNpc(const MWWorld::Ptr& item, int count, bool boughtItem); ///< only used for adjusting the gold balance void buyFromNpc(const MWWorld::Ptr& item, int count, bool soldItem); ///< only used for adjusting the gold balance void updateOffer(); void onItemSelected (int index); void sellItem (MyGUI::Widget* sender, int count); void onFilterChanged(MyGUI::Widget* _sender); void onNameFilterChanged(MyGUI::EditBox* _sender); void onOfferButtonClicked(MyGUI::Widget* _sender); void onAccept(MyGUI::EditBox* sender); void onCancelButtonClicked(MyGUI::Widget* _sender); void onMaxSaleButtonClicked(MyGUI::Widget* _sender); void onIncreaseButtonPressed(MyGUI::Widget* _sender, int _left, int _top, MyGUI::MouseButton _id); void onDecreaseButtonPressed(MyGUI::Widget* _sender, int _left, int _top, MyGUI::MouseButton _id); void onBalanceButtonReleased(MyGUI::Widget* _sender, int _left, int _top, MyGUI::MouseButton _id); void onBalanceValueChanged(int value); void onRepeatClick(MyGUI::Widget* widget, MyGUI::ControllerItem* controller); void addRepeatController(MyGUI::Widget* widget); void onIncreaseButtonTriggered(); void onDecreaseButtonTriggered(); void addOrRemoveGold(int gold, const MWWorld::Ptr& actor); void updateLabels(); void onReferenceUnavailable() override; int getMerchantGold(); }; } #endif ================================================ FILE: apps/openmw/mwgui/trainingwindow.cpp ================================================ #include "trainingwindow.hpp" #include /* Start of tes3mp addition Include additional headers for multiplayer purposes */ #include "../mwmp/Main.hpp" #include "../mwmp/Networking.hpp" #include "../mwmp/ObjectList.hpp" /* End of tes3mp addition */ #include "../mwbase/windowmanager.hpp" #include "../mwbase/environment.hpp" #include "../mwbase/world.hpp" #include "../mwbase/mechanicsmanager.hpp" #include "../mwworld/class.hpp" #include "../mwworld/containerstore.hpp" #include "../mwworld/esmstore.hpp" #include "../mwmechanics/npcstats.hpp" #include "../mwmechanics/actorutil.hpp" #include #include "tooltips.hpp" namespace { // Sorts a container descending by skill value. If skill value is equal, sorts ascending by skill ID. // pair bool sortSkills (const std::pair& left, const std::pair& right) { if (left == right) return false; if (left.second > right.second) return true; else if (left.second < right.second) return false; return left.first < right.first; } } namespace MWGui { TrainingWindow::TrainingWindow() : WindowBase("openmw_trainingwindow.layout") , mTimeAdvancer(0.05f) , mTrainingSkillBasedOnBaseSkill(Settings::Manager::getBool("trainers training skills based on base skill", "Game")) { getWidget(mTrainingOptions, "TrainingOptions"); getWidget(mCancelButton, "CancelButton"); getWidget(mPlayerGold, "PlayerGold"); mCancelButton->eventMouseButtonClick += MyGUI::newDelegate(this, &TrainingWindow::onCancelButtonClicked); mTimeAdvancer.eventProgressChanged += MyGUI::newDelegate(this, &TrainingWindow::onTrainingProgressChanged); mTimeAdvancer.eventFinished += MyGUI::newDelegate(this, &TrainingWindow::onTrainingFinished); } void TrainingWindow::onOpen() { if (mTimeAdvancer.isRunning()) { mProgressBar.setVisible(true); setVisible(false); } else mProgressBar.setVisible(false); center(); } void TrainingWindow::setPtr (const MWWorld::Ptr& actor) { mPtr = actor; MWWorld::Ptr player = MWMechanics::getPlayer(); int playerGold = player.getClass().getContainerStore(player).count(MWWorld::ContainerStore::sGoldId); mPlayerGold->setCaptionWithReplacing("#{sGold}: " + MyGUI::utility::toString(playerGold)); // NPC can train you in his best 3 skills std::vector< std::pair > skills; MWMechanics::NpcStats const& actorStats(actor.getClass().getNpcStats(actor)); for (int i=0; igetEnumerator (); MyGUI::Gui::getInstance ().destroyWidgets (widgets); MWMechanics::NpcStats& pcStats = player.getClass().getNpcStats (player); const MWWorld::Store &gmst = MWBase::Environment::get().getWorld()->getStore().get(); for (int i=0; i<3; ++i) { int price = static_cast(pcStats.getSkill (skills[i].first).getBase() * gmst.find("iTrainingMod")->mValue.getInteger()); price = std::max(1, price); price = MWBase::Environment::get().getMechanicsManager()->getBarterOffer(mPtr, price, true); MyGUI::Button* button = mTrainingOptions->createWidget(price <= playerGold ? "SandTextButton" : "SandTextButtonDisabled", // can't use setEnabled since that removes tooltip MyGUI::IntCoord(5, 5+i*18, mTrainingOptions->getWidth()-10, 18), MyGUI::Align::Default); button->setUserData(skills[i].first); button->eventMouseButtonClick += MyGUI::newDelegate(this, &TrainingWindow::onTrainingSelected); button->setCaptionWithReplacing("#{" + ESM::Skill::sSkillNameIds[skills[i].first] + "} - " + MyGUI::utility::toString(price)); button->setSize(button->getTextSize ().width+12, button->getSize().height); ToolTips::createSkillToolTip (button, skills[i].first); } center(); } void TrainingWindow::onReferenceUnavailable () { MWBase::Environment::get().getWindowManager()->removeGuiMode(GM_Training); } void TrainingWindow::onCancelButtonClicked (MyGUI::Widget *sender) { MWBase::Environment::get().getWindowManager()->removeGuiMode(GM_Training); } void TrainingWindow::onTrainingSelected (MyGUI::Widget *sender) { int skillId = *sender->getUserData(); MWWorld::Ptr player = MWBase::Environment::get().getWorld ()->getPlayerPtr(); MWMechanics::NpcStats& pcStats = player.getClass().getNpcStats (player); const MWWorld::ESMStore &store = MWBase::Environment::get().getWorld()->getStore(); int price = pcStats.getSkill (skillId).getBase() * store.get().find("iTrainingMod")->mValue.getInteger(); price = MWBase::Environment::get().getMechanicsManager()->getBarterOffer(mPtr,price,true); if (price > player.getClass().getContainerStore(player).count(MWWorld::ContainerStore::sGoldId)) return; if (getSkillForTraining(mPtr.getClass().getNpcStats(mPtr), skillId) <= pcStats.getSkill(skillId).getBase()) { MWBase::Environment::get().getWindowManager()->messageBox ("#{sServiceTrainingWords}"); return; } // You can not train a skill above its governing attribute const ESM::Skill* skill = MWBase::Environment::get().getWorld()->getStore().get().find(skillId); if (pcStats.getSkill(skillId).getBase() >= pcStats.getAttribute(skill->mData.mAttribute).getBase()) { MWBase::Environment::get().getWindowManager()->messageBox ("#{sNotifyMessage17}"); return; } // increase skill MWWorld::LiveCellRef *playerRef = player.get(); const ESM::Class *class_ = store.get().find(playerRef->mBase->mClass); pcStats.increaseSkill (skillId, *class_, true); // remove gold player.getClass().getContainerStore(player).remove(MWWorld::ContainerStore::sGoldId, price, player); // add gold to NPC trading gold pool MWMechanics::NpcStats& npcStats = mPtr.getClass().getNpcStats(mPtr); /* Start of tes3mp change (major) Don't unilaterally change the merchant's gold pool on our client and instead let the server do it */ //npcStats.setGoldPool(npcStats.getGoldPool() + price); mwmp::ObjectList* objectList = mwmp::Main::get().getNetworking()->getObjectList(); objectList->reset(); objectList->packetOrigin = mwmp::CLIENT_GAMEPLAY; objectList->addObjectMiscellaneous(mPtr, npcStats.getGoldPool() + price, npcStats.getLastRestockTime().getHour(), npcStats.getLastRestockTime().getDay()); objectList->sendObjectMiscellaneous(); /* End of tes3mp change (major) */ // advance time MWBase::Environment::get().getMechanicsManager()->rest(2, false); /* Start of tes3mp change (major) Multiplayer requires that time not get advanced here */ //MWBase::Environment::get().getWorld ()->advanceTime (2); /* End of tes3mp change (major) */ setVisible(false); mProgressBar.setVisible(true); mProgressBar.setProgress(0, 2); mTimeAdvancer.run(2); MWBase::Environment::get().getWindowManager()->fadeScreenOut(0.25); MWBase::Environment::get().getWindowManager()->fadeScreenIn(0.25, false, 0.25); } void TrainingWindow::onTrainingProgressChanged(int cur, int total) { mProgressBar.setProgress(cur, total); } void TrainingWindow::onTrainingFinished() { mProgressBar.setVisible(false); // go back to game mode MWBase::Environment::get().getWindowManager()->removeGuiMode (GM_Training); MWBase::Environment::get().getWindowManager()->exitCurrentGuiMode(); } float TrainingWindow::getSkillForTraining(const MWMechanics::NpcStats& stats, int skillId) const { if (mTrainingSkillBasedOnBaseSkill) return stats.getSkill(skillId).getBase(); return stats.getSkill(skillId).getModified(); } void TrainingWindow::onFrame(float dt) { checkReferenceAvailable(); mTimeAdvancer.onFrame(dt); } bool TrainingWindow::exit() { return !mTimeAdvancer.isRunning(); } } ================================================ FILE: apps/openmw/mwgui/trainingwindow.hpp ================================================ #ifndef MWGUI_TRAININGWINDOW_H #define MWGUI_TRAININGWINDOW_H #include "windowbase.hpp" #include "referenceinterface.hpp" #include "timeadvancer.hpp" #include "waitdialog.hpp" namespace MWMechanics { class NpcStats; } namespace MWGui { class TrainingWindow : public WindowBase, public ReferenceInterface { public: TrainingWindow(); void onOpen() override; bool exit() override; void setPtr(const MWWorld::Ptr& actor) override; void onFrame(float dt) override; WindowBase* getProgressBar() { return &mProgressBar; } void clear() override { resetReference(); } protected: void onReferenceUnavailable() override; void onCancelButtonClicked (MyGUI::Widget* sender); void onTrainingSelected(MyGUI::Widget* sender); void onTrainingProgressChanged(int cur, int total); void onTrainingFinished(); // Retrieve the base skill value if the setting 'training skills based on base skill' is set; // otherwise returns the modified skill float getSkillForTraining(const MWMechanics::NpcStats& stats, int skillId) const; MyGUI::Widget* mTrainingOptions; MyGUI::Button* mCancelButton; MyGUI::TextBox* mPlayerGold; WaitDialogProgressBar mProgressBar; TimeAdvancer mTimeAdvancer; bool mTrainingSkillBasedOnBaseSkill; //corresponds to the setting 'training skills based on base skill' }; } #endif ================================================ FILE: apps/openmw/mwgui/travelwindow.cpp ================================================ #include "travelwindow.hpp" #include #include #include #include /* Start of tes3mp addition Include additional headers for multiplayer purposes */ #include "../mwmp/Main.hpp" #include "../mwmp/Networking.hpp" #include "../mwmp/Worldstate.hpp" /* End of tes3mp addition */ #include "../mwbase/environment.hpp" #include "../mwbase/world.hpp" #include "../mwbase/windowmanager.hpp" #include "../mwbase/mechanicsmanager.hpp" #include "../mwmechanics/creaturestats.hpp" #include "../mwmechanics/actorutil.hpp" #include "../mwworld/class.hpp" #include "../mwworld/containerstore.hpp" #include "../mwworld/actionteleport.hpp" #include "../mwworld/esmstore.hpp" #include "../mwworld/cellstore.hpp" namespace MWGui { TravelWindow::TravelWindow() : WindowBase("openmw_travel_window.layout") , mCurrentY(0) { getWidget(mCancelButton, "CancelButton"); getWidget(mPlayerGold, "PlayerGold"); getWidget(mSelect, "Select"); getWidget(mDestinations, "Travel"); getWidget(mDestinationsView, "DestinationsView"); mCancelButton->eventMouseButtonClick += MyGUI::newDelegate(this, &TravelWindow::onCancelButtonClicked); mDestinations->setCoord(450/2-mDestinations->getTextSize().width/2, mDestinations->getTop(), mDestinations->getTextSize().width, mDestinations->getHeight()); mSelect->setCoord(8, mSelect->getTop(), mSelect->getTextSize().width, mSelect->getHeight()); } void TravelWindow::addDestination(const std::string& name, ESM::Position pos, bool interior) { int price; const MWWorld::Store &gmst = MWBase::Environment::get().getWorld()->getStore().get(); MWWorld::Ptr player = MWBase::Environment::get().getWorld ()->getPlayerPtr(); int playerGold = player.getClass().getContainerStore(player).count(MWWorld::ContainerStore::sGoldId); if (!mPtr.getCell()->isExterior()) { price = gmst.find("fMagesGuildTravel")->mValue.getInteger(); } else { ESM::Position PlayerPos = player.getRefData().getPosition(); float d = sqrt(pow(pos.pos[0] - PlayerPos.pos[0], 2) + pow(pos.pos[1] - PlayerPos.pos[1], 2) + pow(pos.pos[2] - PlayerPos.pos[2], 2)); float fTravelMult = gmst.find("fTravelMult")->mValue.getFloat(); if (fTravelMult != 0) price = static_cast(d / fTravelMult); else price = static_cast(d); } price = std::max(1, price); price = MWBase::Environment::get().getMechanicsManager()->getBarterOffer(mPtr, price, true); // Add price for the travelling followers std::set followers; MWWorld::ActionTeleport::getFollowers(player, followers); // Apply followers cost, unlike vanilla the first follower doesn't travel for free price *= 1 + static_cast(followers.size()); int lineHeight = MWBase::Environment::get().getWindowManager()->getFontHeight() + 2; MyGUI::Button* toAdd = mDestinationsView->createWidget("SandTextButton", 0, mCurrentY, 200, lineHeight, MyGUI::Align::Default); toAdd->setEnabled(price <= playerGold); mCurrentY += lineHeight; if(interior) toAdd->setUserString("interior","y"); else toAdd->setUserString("interior","n"); toAdd->setUserString("price", std::to_string(price)); toAdd->setCaptionWithReplacing("#{sCell=" + name + "} - " + MyGUI::utility::toString(price)+"#{sgp}"); toAdd->setSize(mDestinationsView->getWidth(),lineHeight); toAdd->eventMouseWheel += MyGUI::newDelegate(this, &TravelWindow::onMouseWheel); toAdd->setUserString("Destination", name); toAdd->setUserData(pos); toAdd->eventMouseButtonClick += MyGUI::newDelegate(this, &TravelWindow::onTravelButtonClick); } void TravelWindow::clearDestinations() { mDestinationsView->setViewOffset(MyGUI::IntPoint(0,0)); mCurrentY = 0; while (mDestinationsView->getChildCount()) MyGUI::Gui::getInstance().destroyWidget(mDestinationsView->getChildAt(0)); } void TravelWindow::setPtr(const MWWorld::Ptr& actor) { center(); mPtr = actor; clearDestinations(); std::vector transport; if (mPtr.getClass().isNpc()) transport = mPtr.get()->mBase->getTransport(); else if (mPtr.getTypeName() == typeid(ESM::Creature).name()) transport = mPtr.get()->mBase->getTransport(); for(unsigned int i = 0;ipositionToIndex(transport[i].mPos.pos[0], transport[i].mPos.pos[1],x,y); if (cellname == "") { MWWorld::CellStore* cell = MWBase::Environment::get().getWorld()->getExterior(x,y); cellname = MWBase::Environment::get().getWorld()->getCellName(cell); interior = false; } addDestination(cellname,transport[i].mPos,interior); } updateLabels(); // Canvas size must be expressed with VScroll disabled, otherwise MyGUI would expand the scroll area when the scrollbar is hidden mDestinationsView->setVisibleVScroll(false); mDestinationsView->setCanvasSize (MyGUI::IntSize(mDestinationsView->getWidth(), std::max(mDestinationsView->getHeight(), mCurrentY))); mDestinationsView->setVisibleVScroll(true); } void TravelWindow::onTravelButtonClick(MyGUI::Widget* _sender) { std::istringstream iss(_sender->getUserString("price")); int price; iss >> price; MWWorld::Ptr player = MWMechanics::getPlayer(); int playerGold = player.getClass().getContainerStore(player).count(MWWorld::ContainerStore::sGoldId); if (playerGoldsetPlayerTraveling(true); if (!mPtr.getCell()->isExterior()) // Interior cell -> mages guild transport MWBase::Environment::get().getWindowManager()->playSound("mysticism cast"); player.getClass().getContainerStore(player).remove(MWWorld::ContainerStore::sGoldId, price, player); // add gold to NPC trading gold pool MWMechanics::CreatureStats& npcStats = mPtr.getClass().getCreatureStats(mPtr); /* Start of tes3mp change (major) Don't unilaterally change the merchant's gold pool on our client and instead let the server do it */ //npcStats.setGoldPool(npcStats.getGoldPool() + price); mwmp::ObjectList* objectList = mwmp::Main::get().getNetworking()->getObjectList(); objectList->reset(); objectList->packetOrigin = mwmp::CLIENT_GAMEPLAY; objectList->addObjectMiscellaneous(mPtr, npcStats.getGoldPool() + price, npcStats.getLastRestockTime().getHour(), npcStats.getLastRestockTime().getDay()); objectList->sendObjectMiscellaneous(); /* End of tes3mp change (major) */ MWBase::Environment::get().getWindowManager()->fadeScreenOut(1); ESM::Position pos = *_sender->getUserData(); std::string cellname = _sender->getUserString("Destination"); bool interior = _sender->getUserString("interior") == "y"; if (mPtr.getCell()->isExterior()) { ESM::Position playerPos = player.getRefData().getPosition(); float d = (osg::Vec3f(pos.pos[0], pos.pos[1], 0) - osg::Vec3f(playerPos.pos[0], playerPos.pos[1], 0)).length(); int hours = static_cast(d /MWBase::Environment::get().getWorld()->getStore().get().find("fTravelTimeMult")->mValue.getFloat()); MWBase::Environment::get().getMechanicsManager()->rest(hours, true); /* Start of tes3mp change (major) Multiplayer requires that time not get advanced here */ //MWBase::Environment::get().getWorld()->advanceTime(hours); /* End of tes3mp change (major) */ } MWBase::Environment::get().getWindowManager()->removeGuiMode(GM_Travel); MWBase::Environment::get().getWindowManager()->exitCurrentGuiMode(); MWBase::Environment::get().getWindowManager()->fadeScreenOut(1); // Teleports any followers, too. MWWorld::ActionTeleport action(interior ? cellname : "", pos, true); action.execute(player); MWBase::Environment::get().getWindowManager()->fadeScreenOut(0); MWBase::Environment::get().getWindowManager()->fadeScreenIn(1); } void TravelWindow::onCancelButtonClicked(MyGUI::Widget* _sender) { MWBase::Environment::get().getWindowManager()->removeGuiMode(GM_Travel); } void TravelWindow::updateLabels() { MWWorld::Ptr player = MWBase::Environment::get().getWorld ()->getPlayerPtr(); int playerGold = player.getClass().getContainerStore(player).count(MWWorld::ContainerStore::sGoldId); mPlayerGold->setCaptionWithReplacing("#{sGold}: " + MyGUI::utility::toString(playerGold)); mPlayerGold->setCoord(8, mPlayerGold->getTop(), mPlayerGold->getTextSize().width, mPlayerGold->getHeight()); } void TravelWindow::onReferenceUnavailable() { MWBase::Environment::get().getWindowManager()->removeGuiMode(GM_Travel); MWBase::Environment::get().getWindowManager()->exitCurrentGuiMode(); } void TravelWindow::onMouseWheel(MyGUI::Widget* _sender, int _rel) { if (mDestinationsView->getViewOffset().top + _rel*0.3f > 0) mDestinationsView->setViewOffset(MyGUI::IntPoint(0, 0)); else mDestinationsView->setViewOffset(MyGUI::IntPoint(0, static_cast(mDestinationsView->getViewOffset().top + _rel*0.3f))); } } ================================================ FILE: apps/openmw/mwgui/travelwindow.hpp ================================================ #ifndef MWGUI_TravelWINDOW_H #define MWGUI_TravelWINDOW_H #include "windowbase.hpp" #include "referenceinterface.hpp" namespace MyGUI { class Gui; class Widget; } namespace MWGui { class TravelWindow : public ReferenceInterface, public WindowBase { public: TravelWindow(); void setPtr (const MWWorld::Ptr& actor) override; protected: MyGUI::Button* mCancelButton; MyGUI::TextBox* mPlayerGold; MyGUI::TextBox* mDestinations; MyGUI::TextBox* mSelect; MyGUI::ScrollView* mDestinationsView; void onCancelButtonClicked(MyGUI::Widget* _sender); void onTravelButtonClick(MyGUI::Widget* _sender); void onMouseWheel(MyGUI::Widget* _sender, int _rel); void addDestination(const std::string& name, ESM::Position pos, bool interior); void clearDestinations(); int mCurrentY; void updateLabels(); void onReferenceUnavailable() override; }; } #endif ================================================ FILE: apps/openmw/mwgui/videowidget.cpp ================================================ #include "videowidget.hpp" #include #include #include #include #include #include #include "../mwsound/movieaudiofactory.hpp" namespace MWGui { VideoWidget::VideoWidget() : mVFS(nullptr) { mPlayer.reset(new Video::VideoPlayer()); setNeedKeyFocus(true); } VideoWidget::~VideoWidget() = default; void VideoWidget::setVFS(const VFS::Manager *vfs) { mVFS = vfs; } void VideoWidget::playVideo(const std::string &video) { mPlayer->setAudioFactory(new MWSound::MovieAudioFactory()); Files::IStreamPtr videoStream; try { videoStream = mVFS->get(video); } catch (std::exception& e) { Log(Debug::Error) << "Failed to open video: " << e.what(); return; } mPlayer->playVideo(videoStream, video); osg::ref_ptr texture = mPlayer->getVideoTexture(); if (!texture) return; mTexture.reset(new osgMyGUI::OSGTexture(texture)); setRenderItemTexture(mTexture.get()); getSubWidgetMain()->_setUVSet(MyGUI::FloatRect(0.f, 1.f, 1.f, 0.f)); } int VideoWidget::getVideoWidth() { return mPlayer->getVideoWidth(); } int VideoWidget::getVideoHeight() { return mPlayer->getVideoHeight(); } bool VideoWidget::update() { return mPlayer->update(); } void VideoWidget::stop() { mPlayer->close(); } void VideoWidget::pause() { mPlayer->pause(); } void VideoWidget::resume() { mPlayer->play(); } bool VideoWidget::isPaused() const { return mPlayer->isPaused(); } bool VideoWidget::hasAudioStream() { return mPlayer->hasAudioStream(); } void VideoWidget::autoResize(bool stretch) { MyGUI::IntSize screenSize = MyGUI::RenderManager::getInstance().getViewSize(); if (getParent()) screenSize = getParent()->getSize(); if (getVideoHeight() > 0 && !stretch) { double imageaspect = static_cast(getVideoWidth())/getVideoHeight(); int leftPadding = std::max(0, static_cast(screenSize.width - screenSize.height * imageaspect) / 2); int topPadding = std::max(0, static_cast(screenSize.height - screenSize.width / imageaspect) / 2); setCoord(leftPadding, topPadding, screenSize.width - leftPadding*2, screenSize.height - topPadding*2); } else setCoord(0,0,screenSize.width,screenSize.height); } } ================================================ FILE: apps/openmw/mwgui/videowidget.hpp ================================================ #ifndef OPENMW_MWGUI_VIDEOWIDGET_H #define OPENMW_MWGUI_VIDEOWIDGET_H #include #include namespace Video { class VideoPlayer; } namespace VFS { class Manager; } namespace MWGui { /** * Widget that plays a video. */ class VideoWidget : public MyGUI::Widget { public: MYGUI_RTTI_DERIVED(VideoWidget) VideoWidget(); ~VideoWidget(); /// Set the VFS (virtual file system) to find the videos on. void setVFS(const VFS::Manager* vfs); void playVideo (const std::string& video); int getVideoWidth(); int getVideoHeight(); /// @return Is the video still playing? bool update(); /// Return true if a video is currently playing and it has an audio stream. bool hasAudioStream(); /// Stop video and free resources (done automatically on destruction) void stop(); void pause(); void resume(); bool isPaused() const; /// Adjust the coordinates of this video widget relative to its parent, /// based on the dimensions of the playing video. /// @param stretch Stretch the video to fill the whole screen? If false, /// black bars may be added to fix the aspect ratio. void autoResize (bool stretch); private: const VFS::Manager* mVFS; std::unique_ptr mTexture; std::unique_ptr mPlayer; }; } #endif ================================================ FILE: apps/openmw/mwgui/waitdialog.cpp ================================================ #include "waitdialog.hpp" #include #include #include #include #include #include /* Start of tes3mp addition Include additional headers for multiplayer purposes */ #include "../mwmp/Main.hpp" #include "../mwmp/LocalPlayer.hpp" /* End of tes3mp addition */ #include "../mwbase/windowmanager.hpp" #include "../mwbase/world.hpp" #include "../mwbase/environment.hpp" #include "../mwbase/mechanicsmanager.hpp" #include "../mwbase/statemanager.hpp" #include "../mwworld/class.hpp" #include "../mwworld/cellstore.hpp" #include "../mwworld/esmstore.hpp" #include "../mwmechanics/creaturestats.hpp" #include "../mwmechanics/npcstats.hpp" #include "../mwmechanics/actorutil.hpp" namespace MWGui { WaitDialogProgressBar::WaitDialogProgressBar() : WindowBase("openmw_wait_dialog_progressbar.layout") { getWidget(mProgressBar, "ProgressBar"); getWidget(mProgressText, "ProgressText"); } void WaitDialogProgressBar::onOpen() { center(); } void WaitDialogProgressBar::setProgress (int cur, int total) { mProgressBar->setProgressRange (total); mProgressBar->setProgressPosition (cur); mProgressText->setCaption(MyGUI::utility::toString(cur) + "/" + MyGUI::utility::toString(total)); } // --------------------------------------------------------------------------------------------------------- WaitDialog::WaitDialog() : WindowBase("openmw_wait_dialog.layout") , mTimeAdvancer(0.05f) , mSleeping(false) , mHours(1) , mManualHours(1) , mFadeTimeRemaining(0) , mInterruptAt(-1) , mProgressBar() { getWidget(mDateTimeText, "DateTimeText"); getWidget(mRestText, "RestText"); getWidget(mHourText, "HourText"); getWidget(mUntilHealedButton, "UntilHealedButton"); getWidget(mWaitButton, "WaitButton"); getWidget(mCancelButton, "CancelButton"); getWidget(mHourSlider, "HourSlider"); mCancelButton->eventMouseButtonClick += MyGUI::newDelegate(this, &WaitDialog::onCancelButtonClicked); mUntilHealedButton->eventMouseButtonClick += MyGUI::newDelegate(this, &WaitDialog::onUntilHealedButtonClicked); mWaitButton->eventMouseButtonClick += MyGUI::newDelegate(this, &WaitDialog::onWaitButtonClicked); mHourSlider->eventScrollChangePosition += MyGUI::newDelegate(this, &WaitDialog::onHourSliderChangedPosition); mCancelButton->eventKeyButtonPressed += MyGUI::newDelegate(this, &WaitDialog::onKeyButtonPressed); mWaitButton->eventKeyButtonPressed += MyGUI::newDelegate(this, &WaitDialog::onKeyButtonPressed); mUntilHealedButton->eventKeyButtonPressed += MyGUI::newDelegate(this, &WaitDialog::onKeyButtonPressed); mTimeAdvancer.eventProgressChanged += MyGUI::newDelegate(this, &WaitDialog::onWaitingProgressChanged); mTimeAdvancer.eventInterrupted += MyGUI::newDelegate(this, &WaitDialog::onWaitingInterrupted); mTimeAdvancer.eventFinished += MyGUI::newDelegate(this, &WaitDialog::onWaitingFinished); } void WaitDialog::setPtr(const MWWorld::Ptr &ptr) { setCanRest(!ptr.isEmpty() || MWBase::Environment::get().getWorld ()->canRest () == MWBase::World::Rest_Allowed); if (ptr.isEmpty() && MWBase::Environment::get().getWorld ()->canRest() == MWBase::World::Rest_PlayerIsInAir) { // Resting in air is not allowed unless you're using a bed MWBase::Environment::get().getWindowManager()->messageBox ("#{sNotifyMessage1}"); MWBase::Environment::get().getWindowManager()->removeGuiMode(GM_Rest); } if (mUntilHealedButton->getVisible()) MWBase::Environment::get().getWindowManager()->setKeyFocusWidget(mUntilHealedButton); else MWBase::Environment::get().getWindowManager()->setKeyFocusWidget(mWaitButton); } bool WaitDialog::exit() { return (!mTimeAdvancer.isRunning()); //Only exit if not currently waiting } void WaitDialog::clear() { mSleeping = false; mTimeAdvancer.stop(); } void WaitDialog::onOpen() { if (mTimeAdvancer.isRunning()) { mProgressBar.setVisible(true); setVisible(false); return; } else { mProgressBar.setVisible(false); } if (!MWBase::Environment::get().getWindowManager ()->getRestEnabled ()) { MWBase::Environment::get().getWindowManager()->popGuiMode (); } MWBase::World::RestPermitted canRest = MWBase::Environment::get().getWorld ()->canRest (); if (canRest == MWBase::World::Rest_EnemiesAreNearby) { MWBase::Environment::get().getWindowManager()->messageBox("#{sNotifyMessage2}"); MWBase::Environment::get().getWindowManager()->popGuiMode (); } else if (canRest == MWBase::World::Rest_PlayerIsUnderwater) { // resting underwater not allowed MWBase::Environment::get().getWindowManager()->messageBox ("#{sNotifyMessage1}"); MWBase::Environment::get().getWindowManager()->popGuiMode (); } /* Start of tes3mp addition Prevent resting and waiting if they have been disabled by the server for the local player */ else if (canRest == MWBase::World::Rest_Allowed && !mwmp::Main::get().getLocalPlayer()->wildernessRestAllowed && !mwmp::Main::get().getLocalPlayer()->isUsingBed) { MWBase::Environment::get().getWindowManager()->messageBox("You are not allowed to rest without a bed."); MWBase::Environment::get().getWindowManager()->popGuiMode(); } else if (canRest == MWBase::World::Rest_OnlyWaiting && !mwmp::Main::get().getLocalPlayer()->waitAllowed && !mwmp::Main::get().getLocalPlayer()->isUsingBed) { MWBase::Environment::get().getWindowManager()->messageBox("You are not allowed to wait."); MWBase::Environment::get().getWindowManager()->popGuiMode(); } /* End of tes3mp addition */ onHourSliderChangedPosition(mHourSlider, 0); mHourSlider->setScrollPosition (0); std::string month = MWBase::Environment::get().getWorld ()->getMonthName(); int hour = static_cast(MWBase::Environment::get().getWorld()->getTimeStamp().getHour()); bool pm = hour >= 12; if (hour >= 13) hour -= 12; if (hour == 0) hour = 12; ESM::EpochTimeStamp currentDate = MWBase::Environment::get().getWorld()->getEpochTimeStamp(); int daysPassed = MWBase::Environment::get().getWorld()->getTimeStamp().getDay(); std::string formattedHour = pm ? "#{sSaveMenuHelp05}" : "#{sSaveMenuHelp04}"; std::string dateTimeText = Misc::StringUtils::format("%i %s (#{sDay} %i) %i %s", currentDate.mDay, month, daysPassed, hour, formattedHour); mDateTimeText->setCaptionWithReplacing (dateTimeText); } void WaitDialog::onUntilHealedButtonClicked(MyGUI::Widget* sender) { int autoHours = MWBase::Environment::get().getMechanicsManager()->getHoursToRest(); startWaiting(autoHours); } void WaitDialog::onWaitButtonClicked(MyGUI::Widget* sender) { startWaiting(mManualHours); } void WaitDialog::startWaiting(int hoursToWait) { /* Start of tes3mp change (major) It should not be possible to autosave the game in multiplayer, so it has been disabled */ /* if(Settings::Manager::getBool("autosave","Saves")) //autosaves when enabled MWBase::Environment::get().getStateManager()->quickSave("Autosave"); */ /* End of tes3mp change (major) */ MWBase::World* world = MWBase::Environment::get().getWorld(); MWBase::Environment::get().getWindowManager()->fadeScreenOut(0.2f); mFadeTimeRemaining = 0.4f; setVisible(false); mHours = hoursToWait; // FIXME: move this somewhere else? mInterruptAt = -1; MWWorld::Ptr player = world->getPlayerPtr(); if (mSleeping && player.getCell()->isExterior()) { std::string regionstr = player.getCell()->getCell()->mRegion; if (!regionstr.empty()) { const ESM::Region *region = world->getStore().get().find (regionstr); if (!region->mSleepList.empty()) { // figure out if player will be woken while sleeping int x = Misc::Rng::rollDice(hoursToWait); float fSleepRandMod = world->getStore().get().find("fSleepRandMod")->mValue.getFloat(); if (x < fSleepRandMod * hoursToWait) { float fSleepRestMod = world->getStore().get().find("fSleepRestMod")->mValue.getFloat(); int interruptAtHoursRemaining = int(fSleepRestMod * hoursToWait); if (interruptAtHoursRemaining != 0) { mInterruptAt = hoursToWait - interruptAtHoursRemaining; mInterruptCreatureList = region->mSleepList; } } } } } mProgressBar.setProgress (0, hoursToWait); } void WaitDialog::onCancelButtonClicked(MyGUI::Widget* sender) { MWBase::Environment::get().getWindowManager()->removeGuiMode(GM_Rest); } void WaitDialog::onHourSliderChangedPosition(MyGUI::ScrollBar* sender, size_t position) { mHourText->setCaptionWithReplacing (MyGUI::utility::toString(position+1) + " #{sRestMenu2}"); mManualHours = position+1; MWBase::Environment::get().getWindowManager()->setKeyFocusWidget(mWaitButton); } void WaitDialog::onKeyButtonPressed(MyGUI::Widget *sender, MyGUI::KeyCode key, MyGUI::Char character) { if (key == MyGUI::KeyCode::ArrowUp) mHourSlider->setScrollPosition(std::min(mHourSlider->getScrollPosition()+1, mHourSlider->getScrollRange()-1)); else if (key == MyGUI::KeyCode::ArrowDown) mHourSlider->setScrollPosition(std::max(static_cast(mHourSlider->getScrollPosition())-1, 0)); else return; onHourSliderChangedPosition(mHourSlider, mHourSlider->getScrollPosition()); } void WaitDialog::onWaitingProgressChanged(int cur, int total) { mProgressBar.setProgress(cur, total); MWBase::Environment::get().getMechanicsManager()->rest(1, mSleeping); /* Start of tes3mp change (major) Multiplayer requires that time not get advanced here */ //MWBase::Environment::get().getWorld()->advanceTime(1); /* End of tes3mp change (major) */ MWWorld::Ptr player = MWBase::Environment::get().getWorld()->getPlayerPtr(); if (player.getClass().getCreatureStats(player).isDead()) stopWaiting(); } void WaitDialog::onWaitingInterrupted() { MWBase::Environment::get().getWindowManager()->messageBox("#{sSleepInterrupt}"); MWBase::Environment::get().getWorld()->spawnRandomCreature(mInterruptCreatureList); stopWaiting(); } void WaitDialog::onWaitingFinished() { stopWaiting(); MWWorld::Ptr player = MWMechanics::getPlayer(); const MWMechanics::NpcStats &pcstats = player.getClass().getNpcStats(player); // trigger levelup if possible const MWWorld::Store &gmst = MWBase::Environment::get().getWorld()->getStore().get(); if (mSleeping && pcstats.getLevelProgress () >= gmst.find("iLevelUpTotal")->mValue.getInteger()) { MWBase::Environment::get().getWindowManager()->pushGuiMode (GM_Levelup); } } void WaitDialog::setCanRest (bool canRest) { MWWorld::Ptr player = MWMechanics::getPlayer(); MWMechanics::CreatureStats& stats = player.getClass().getCreatureStats(player); bool full = (stats.getHealth().getCurrent() >= stats.getHealth().getModified()) && (stats.getMagicka().getCurrent() >= stats.getMagicka().getModified()); MWMechanics::NpcStats& npcstats = player.getClass().getNpcStats(player); bool werewolf = npcstats.isWerewolf(); mUntilHealedButton->setVisible(canRest && !full); mWaitButton->setCaptionWithReplacing (canRest ? "#{sRest}" : "#{sWait}"); mRestText->setCaptionWithReplacing (canRest ? "#{sRestMenu3}" : (werewolf ? "#{sWerewolfRestMessage}" : "#{sRestIllegal}")); mSleeping = canRest; Gui::Box* box = dynamic_cast(mMainWidget); if (box == nullptr) throw std::runtime_error("main widget must be a box"); box->notifyChildrenSizeChanged(); center(); } void WaitDialog::onFrame(float dt) { mTimeAdvancer.onFrame(dt); if (mFadeTimeRemaining <= 0) return; mFadeTimeRemaining -= dt; if (mFadeTimeRemaining <= 0) { mProgressBar.setVisible(true); mTimeAdvancer.run(mHours, mInterruptAt); } } void WaitDialog::stopWaiting () { MWBase::Environment::get().getWindowManager()->fadeScreenIn(0.2f); mProgressBar.setVisible (false); MWBase::Environment::get().getWindowManager()->removeGuiMode (GM_Rest); mTimeAdvancer.stop(); } void WaitDialog::wakeUp () { mSleeping = false; if (mInterruptAt != -1) onWaitingInterrupted(); else stopWaiting(); } } ================================================ FILE: apps/openmw/mwgui/waitdialog.hpp ================================================ #ifndef MWGUI_WAIT_DIALOG_H #define MWGUI_WAIT_DIALOG_H #include "timeadvancer.hpp" #include "windowbase.hpp" namespace MWGui { class WaitDialogProgressBar : public WindowBase { public: WaitDialogProgressBar(); void onOpen() override; void setProgress(int cur, int total); protected: MyGUI::ProgressBar* mProgressBar; MyGUI::TextBox* mProgressText; }; class WaitDialog : public WindowBase { public: WaitDialog(); void setPtr(const MWWorld::Ptr &ptr) override; void onOpen() override; bool exit() override; void clear() override; void onFrame(float dt) override; bool getSleeping() { return mTimeAdvancer.isRunning() && mSleeping; } void wakeUp(); void autosave(); WindowBase* getProgressBar() { return &mProgressBar; } protected: MyGUI::TextBox* mDateTimeText; MyGUI::TextBox* mRestText; MyGUI::TextBox* mHourText; MyGUI::Button* mUntilHealedButton; MyGUI::Button* mWaitButton; MyGUI::Button* mCancelButton; MyGUI::ScrollBar* mHourSlider; TimeAdvancer mTimeAdvancer; bool mSleeping; int mHours; int mManualHours; // stores the hours to rest selected via slider float mFadeTimeRemaining; int mInterruptAt; std::string mInterruptCreatureList; WaitDialogProgressBar mProgressBar; void onUntilHealedButtonClicked(MyGUI::Widget* sender); void onWaitButtonClicked(MyGUI::Widget* sender); void onCancelButtonClicked(MyGUI::Widget* sender); void onHourSliderChangedPosition(MyGUI::ScrollBar* sender, size_t position); void onKeyButtonPressed(MyGUI::Widget* sender, MyGUI::KeyCode key, MyGUI::Char character); void onWaitingProgressChanged(int cur, int total); void onWaitingInterrupted(); void onWaitingFinished(); void setCanRest(bool canRest); void startWaiting(int hoursToWait); void stopWaiting(); }; } #endif ================================================ FILE: apps/openmw/mwgui/widgets.cpp ================================================ #include "widgets.hpp" #include #include #include #include #include #include "../mwbase/environment.hpp" #include "../mwbase/world.hpp" #include "../mwbase/windowmanager.hpp" #include "../mwworld/esmstore.hpp" #include "controllers.hpp" namespace MWGui { namespace Widgets { /* MWSkill */ MWSkill::MWSkill() : mSkillId(ESM::Skill::Length) , mSkillNameWidget(nullptr) , mSkillValueWidget(nullptr) { } void MWSkill::setSkillId(ESM::Skill::SkillEnum skill) { mSkillId = skill; updateWidgets(); } void MWSkill::setSkillNumber(int skill) { if (skill < 0) setSkillId(ESM::Skill::Length); else if (skill < ESM::Skill::Length) setSkillId(static_cast(skill)); else throw std::runtime_error("Skill number out of range"); } void MWSkill::setSkillValue(const SkillValue& value) { mValue = value; updateWidgets(); } void MWSkill::updateWidgets() { if (mSkillNameWidget) { if (mSkillId == ESM::Skill::Length) { mSkillNameWidget->setCaption(""); } else { const std::string &name = MWBase::Environment::get().getWindowManager()->getGameSettingString(ESM::Skill::sSkillNameIds[mSkillId], ""); mSkillNameWidget->setCaption(name); } } if (mSkillValueWidget) { SkillValue::Type modified = mValue.getModified(), base = mValue.getBase(); mSkillValueWidget->setCaption(MyGUI::utility::toString(modified)); if (modified > base) mSkillValueWidget->_setWidgetState("increased"); else if (modified < base) mSkillValueWidget->_setWidgetState("decreased"); else mSkillValueWidget->_setWidgetState("normal"); } } void MWSkill::onClicked(MyGUI::Widget* _sender) { eventClicked(this); } MWSkill::~MWSkill() { } void MWSkill::initialiseOverride() { Base::initialiseOverride(); assignWidget(mSkillNameWidget, "StatName"); assignWidget(mSkillValueWidget, "StatValue"); MyGUI::Button* button; assignWidget(button, "StatNameButton"); if (button) { mSkillNameWidget = button; button->eventMouseButtonClick += MyGUI::newDelegate(this, &MWSkill::onClicked); } button = nullptr; assignWidget(button, "StatValueButton"); if (button) { mSkillValueWidget = button; button->eventMouseButtonClick += MyGUI::newDelegate(this, &MWSkill::onClicked); } } /* MWAttribute */ MWAttribute::MWAttribute() : mId(-1) , mAttributeNameWidget(nullptr) , mAttributeValueWidget(nullptr) { } void MWAttribute::setAttributeId(int attributeId) { mId = attributeId; updateWidgets(); } void MWAttribute::setAttributeValue(const AttributeValue& value) { mValue = value; updateWidgets(); } void MWAttribute::onClicked(MyGUI::Widget* _sender) { eventClicked(this); } void MWAttribute::updateWidgets() { if (mAttributeNameWidget) { if (mId < 0 || mId >= 8) { mAttributeNameWidget->setCaption(""); } else { static const char *attributes[8] = { "sAttributeStrength", "sAttributeIntelligence", "sAttributeWillpower", "sAttributeAgility", "sAttributeSpeed", "sAttributeEndurance", "sAttributePersonality", "sAttributeLuck" }; const std::string &name = MWBase::Environment::get().getWindowManager()->getGameSettingString(attributes[mId], ""); mAttributeNameWidget->setCaption(name); } } if (mAttributeValueWidget) { int modified = mValue.getModified(), base = mValue.getBase(); mAttributeValueWidget->setCaption(MyGUI::utility::toString(modified)); if (modified > base) mAttributeValueWidget->_setWidgetState("increased"); else if (modified < base) mAttributeValueWidget->_setWidgetState("decreased"); else mAttributeValueWidget->_setWidgetState("normal"); } } MWAttribute::~MWAttribute() { } void MWAttribute::initialiseOverride() { Base::initialiseOverride(); assignWidget(mAttributeNameWidget, "StatName"); assignWidget(mAttributeValueWidget, "StatValue"); MyGUI::Button* button; assignWidget(button, "StatNameButton"); if (button) { mAttributeNameWidget = button; button->eventMouseButtonClick += MyGUI::newDelegate(this, &MWAttribute::onClicked); } button = nullptr; assignWidget(button, "StatValueButton"); if (button) { mAttributeValueWidget = button; button->eventMouseButtonClick += MyGUI::newDelegate(this, &MWAttribute::onClicked); } } /* MWSpell */ MWSpell::MWSpell() : mSpellNameWidget(nullptr) { } void MWSpell::setSpellId(const std::string &spellId) { mId = spellId; updateWidgets(); } void MWSpell::createEffectWidgets(std::vector &effects, MyGUI::Widget* creator, MyGUI::IntCoord &coord, int flags) { const MWWorld::ESMStore &store = MWBase::Environment::get().getWorld()->getStore(); const ESM::Spell *spell = store.get().search(mId); MYGUI_ASSERT(spell, "spell with id '" << mId << "' not found"); for (const ESM::ENAMstruct& effectInfo : spell->mEffects.mList) { MWSpellEffectPtr effect = creator->createWidget("MW_EffectImage", coord, MyGUI::Align::Default); SpellEffectParams params; params.mEffectID = effectInfo.mEffectID; params.mSkill = effectInfo.mSkill; params.mAttribute = effectInfo.mAttribute; params.mDuration = effectInfo.mDuration; params.mMagnMin = effectInfo.mMagnMin; params.mMagnMax = effectInfo.mMagnMax; params.mRange = effectInfo.mRange; params.mIsConstant = (flags & MWEffectList::EF_Constant) != 0; params.mNoTarget = (flags & MWEffectList::EF_NoTarget); params.mNoMagnitude = (flags & MWEffectList::EF_NoMagnitude); effect->setSpellEffect(params); effects.push_back(effect); coord.top += effect->getHeight(); coord.width = std::max(coord.width, effect->getRequestedWidth()); } } void MWSpell::updateWidgets() { if (mSpellNameWidget && MWBase::Environment::get().getWindowManager()) { const MWWorld::ESMStore &store = MWBase::Environment::get().getWorld()->getStore(); const ESM::Spell *spell = store.get().search(mId); if (spell) mSpellNameWidget->setCaption(spell->mName); else mSpellNameWidget->setCaption(""); } } void MWSpell::initialiseOverride() { Base::initialiseOverride(); assignWidget(mSpellNameWidget, "StatName"); } MWSpell::~MWSpell() { } /* MWEffectList */ MWEffectList::MWEffectList() : mEffectList(0) { } void MWEffectList::setEffectList(const SpellEffectList& list) { mEffectList = list; updateWidgets(); } void MWEffectList::createEffectWidgets(std::vector &effects, MyGUI::Widget* creator, MyGUI::IntCoord &coord, bool center, int flags) { // We don't know the width of all the elements beforehand, so we do it in // 2 steps: first, create all widgets and check their width.... MWSpellEffectPtr effect = nullptr; int maxwidth = coord.width; for (auto& effectInfo : mEffectList) { effect = creator->createWidget("MW_EffectImage", coord, MyGUI::Align::Default); effectInfo.mIsConstant = (flags & EF_Constant) || effectInfo.mIsConstant; effectInfo.mNoTarget = (flags & EF_NoTarget) || effectInfo.mNoTarget; effectInfo.mNoMagnitude = (flags & EF_NoMagnitude) || effectInfo.mNoMagnitude; effect->setSpellEffect(effectInfo); effects.push_back(effect); if (effect->getRequestedWidth() > maxwidth) maxwidth = effect->getRequestedWidth(); coord.top += effect->getHeight(); } // ... then adjust the size for all widgets for (MyGUI::Widget* effectWidget : effects) { effect = effectWidget->castType(); bool needcenter = center && (maxwidth > effect->getRequestedWidth()); int diff = maxwidth - effect->getRequestedWidth(); if (needcenter) { effect->setCoord(diff/2, effect->getCoord().top, effect->getRequestedWidth(), effect->getCoord().height); } else { effect->setCoord(0, effect->getCoord().top, effect->getRequestedWidth(), effect->getCoord().height); } } // inform the parent about width coord.width = maxwidth; } void MWEffectList::updateWidgets() { } void MWEffectList::initialiseOverride() { Base::initialiseOverride(); } MWEffectList::~MWEffectList() { } SpellEffectList MWEffectList::effectListFromESM(const ESM::EffectList* effects) { SpellEffectList result; for (const ESM::ENAMstruct& effectInfo : effects->mList) { SpellEffectParams params; params.mEffectID = effectInfo.mEffectID; params.mSkill = effectInfo.mSkill; params.mAttribute = effectInfo.mAttribute; params.mDuration = effectInfo.mDuration; params.mMagnMin = effectInfo.mMagnMin; params.mMagnMax = effectInfo.mMagnMax; params.mRange = effectInfo.mRange; params.mArea = effectInfo.mArea; result.push_back(params); } return result; } /* MWSpellEffect */ MWSpellEffect::MWSpellEffect() : mImageWidget(nullptr) , mTextWidget(nullptr) , mRequestedWidth(0) { } void MWSpellEffect::setSpellEffect(const SpellEffectParams& params) { mEffectParams = params; updateWidgets(); } void MWSpellEffect::updateWidgets() { if (!mEffectParams.mKnown) { mTextWidget->setCaption ("?"); mTextWidget->setCoord(sIconOffset / 2, mTextWidget->getCoord().top, mTextWidget->getCoord().width, mTextWidget->getCoord().height); // Compensates for the missing image when effect is not known mRequestedWidth = mTextWidget->getTextSize().width + sIconOffset; mImageWidget->setImageTexture (""); return; } const MWWorld::ESMStore &store = MWBase::Environment::get().getWorld()->getStore(); const ESM::MagicEffect *magicEffect = store.get().search(mEffectParams.mEffectID); assert(magicEffect); std::string pt = MWBase::Environment::get().getWindowManager()->getGameSettingString("spoint", ""); std::string pts = MWBase::Environment::get().getWindowManager()->getGameSettingString("spoints", ""); std::string pct = MWBase::Environment::get().getWindowManager()->getGameSettingString("spercent", ""); std::string ft = MWBase::Environment::get().getWindowManager()->getGameSettingString("sfeet", ""); std::string lvl = MWBase::Environment::get().getWindowManager()->getGameSettingString("sLevel", ""); std::string lvls = MWBase::Environment::get().getWindowManager()->getGameSettingString("sLevels", ""); std::string to = " " + MWBase::Environment::get().getWindowManager()->getGameSettingString("sTo", "") + " "; std::string sec = " " + MWBase::Environment::get().getWindowManager()->getGameSettingString("ssecond", ""); std::string secs = " " + MWBase::Environment::get().getWindowManager()->getGameSettingString("sseconds", ""); std::string effectIDStr = ESM::MagicEffect::effectIdToString(mEffectParams.mEffectID); std::string spellLine = MWBase::Environment::get().getWindowManager()->getGameSettingString(effectIDStr, ""); if (magicEffect->mData.mFlags & ESM::MagicEffect::TargetSkill && mEffectParams.mSkill != -1) { spellLine += " " + MWBase::Environment::get().getWindowManager()->getGameSettingString(ESM::Skill::sSkillNameIds[mEffectParams.mSkill], ""); } if (magicEffect->mData.mFlags & ESM::MagicEffect::TargetAttribute && mEffectParams.mAttribute != -1) { spellLine += " " + MWBase::Environment::get().getWindowManager()->getGameSettingString(ESM::Attribute::sGmstAttributeIds[mEffectParams.mAttribute], ""); } if (mEffectParams.mMagnMin || mEffectParams.mMagnMax) { ESM::MagicEffect::MagnitudeDisplayType displayType = magicEffect->getMagnitudeDisplayType(); if ( displayType == ESM::MagicEffect::MDT_TimesInt ) { std::string timesInt = MWBase::Environment::get().getWindowManager()->getGameSettingString("sXTimesINT", ""); std::stringstream formatter; formatter << std::fixed << std::setprecision(1) << " " << (mEffectParams.mMagnMin / 10.0f); if (mEffectParams.mMagnMin != mEffectParams.mMagnMax) formatter << to << (mEffectParams.mMagnMax / 10.0f); formatter << timesInt; spellLine += formatter.str(); } else if ( displayType != ESM::MagicEffect::MDT_None && !mEffectParams.mNoMagnitude) { spellLine += " " + MyGUI::utility::toString(mEffectParams.mMagnMin); if (mEffectParams.mMagnMin != mEffectParams.mMagnMax) spellLine += to + MyGUI::utility::toString(mEffectParams.mMagnMax); if ( displayType == ESM::MagicEffect::MDT_Percentage ) spellLine += pct; else if ( displayType == ESM::MagicEffect::MDT_Feet ) spellLine += " " + ft; else if ( displayType == ESM::MagicEffect::MDT_Level ) spellLine += " " + ((mEffectParams.mMagnMin == mEffectParams.mMagnMax && std::abs(mEffectParams.mMagnMin) == 1) ? lvl : lvls ); else // ESM::MagicEffect::MDT_Points spellLine += " " + ((mEffectParams.mMagnMin == mEffectParams.mMagnMax && std::abs(mEffectParams.mMagnMin) == 1) ? pt : pts ); } } // constant effects have no duration and no target if (!mEffectParams.mIsConstant) { if (!(magicEffect->mData.mFlags & ESM::MagicEffect::AppliedOnce)) mEffectParams.mDuration = std::max(1, mEffectParams.mDuration); if (mEffectParams.mDuration > 0 && !(magicEffect->mData.mFlags & ESM::MagicEffect::NoDuration)) { spellLine += " " + MWBase::Environment::get().getWindowManager()->getGameSettingString("sfor", "") + " " + MyGUI::utility::toString(mEffectParams.mDuration) + ((mEffectParams.mDuration == 1) ? sec : secs); } if (mEffectParams.mArea > 0) { spellLine += " #{sin} " + MyGUI::utility::toString(mEffectParams.mArea) + " #{sfootarea}"; } // potions have no target if (!mEffectParams.mNoTarget) { std::string on = MWBase::Environment::get().getWindowManager()->getGameSettingString("sonword", ""); if (mEffectParams.mRange == ESM::RT_Self) spellLine += " " + on + " " + MWBase::Environment::get().getWindowManager()->getGameSettingString("sRangeSelf", ""); else if (mEffectParams.mRange == ESM::RT_Touch) spellLine += " " + on + " " + MWBase::Environment::get().getWindowManager()->getGameSettingString("sRangeTouch", ""); else if (mEffectParams.mRange == ESM::RT_Target) spellLine += " " + on + " " + MWBase::Environment::get().getWindowManager()->getGameSettingString("sRangeTarget", ""); } } mTextWidget->setCaptionWithReplacing(spellLine); mRequestedWidth = mTextWidget->getTextSize().width + sIconOffset; mImageWidget->setImageTexture(MWBase::Environment::get().getWindowManager()->correctIconPath(magicEffect->mIcon)); } MWSpellEffect::~MWSpellEffect() { } void MWSpellEffect::initialiseOverride() { Base::initialiseOverride(); assignWidget(mTextWidget, "Text"); assignWidget(mImageWidget, "Image"); } /* MWDynamicStat */ MWDynamicStat::MWDynamicStat() : mValue(0) , mMax(1) , mTextWidget(nullptr) , mBarWidget(nullptr) , mBarTextWidget(nullptr) { } void MWDynamicStat::setValue(int cur, int max) { mValue = cur; mMax = max; if (mBarWidget) { mBarWidget->setProgressRange(std::max(0, mMax)); mBarWidget->setProgressPosition(std::max(0, mValue)); } if (mBarTextWidget) { std::stringstream out; out << mValue << "/" << mMax; mBarTextWidget->setCaption(out.str().c_str()); } } void MWDynamicStat::setTitle(const std::string& text) { if (mTextWidget) mTextWidget->setCaption(text); } MWDynamicStat::~MWDynamicStat() { } void MWDynamicStat::initialiseOverride() { Base::initialiseOverride(); assignWidget(mTextWidget, "Text"); assignWidget(mBarWidget, "Bar"); assignWidget(mBarTextWidget, "BarText"); } } } ================================================ FILE: apps/openmw/mwgui/widgets.hpp ================================================ #ifndef MWGUI_WIDGETS_H #define MWGUI_WIDGETS_H #include "../mwmechanics/stat.hpp" #include #include #include #include #include namespace MyGUI { class ImageBox; class ControllerItem; } namespace MWBase { class WindowManager; } /* This file contains various custom widgets used in OpenMW. */ namespace MWGui { namespace Widgets { class MWEffectList; void fixTexturePath(std::string &path); struct SpellEffectParams { SpellEffectParams() : mNoTarget(false) , mIsConstant(false) , mNoMagnitude(false) , mKnown(true) , mEffectID(-1) , mSkill(-1) , mAttribute(-1) , mMagnMin(-1) , mMagnMax(-1) , mRange(-1) , mDuration(-1) , mArea(0) { } bool mNoTarget; // potion effects for example have no target (target is always the player) bool mIsConstant; // constant effect means that duration will not be displayed bool mNoMagnitude; // effect magnitude will not be displayed (e.g ingredients) bool mKnown; // is this effect known to the player? (If not, will display as a question mark instead) // value of -1 here means the effect is unknown to the player short mEffectID; // value of -1 here means there is no skill/attribute signed char mSkill, mAttribute; // value of -1 here means the value is unavailable int mMagnMin, mMagnMax, mRange, mDuration; // value of 0 -> no area effect int mArea; bool operator==(const SpellEffectParams& other) const { if (mEffectID != other.mEffectID) return false; bool involvesAttribute = (mEffectID == 74 // restore attribute || mEffectID == 85 // absorb attribute || mEffectID == 17 // drain attribute || mEffectID == 79 // fortify attribute || mEffectID == 22); // damage attribute bool involvesSkill = (mEffectID == 78 // restore skill || mEffectID == 89 // absorb skill || mEffectID == 21 // drain skill || mEffectID == 83 // fortify skill || mEffectID == 26); // damage skill return ((other.mSkill == mSkill) || !involvesSkill) && ((other.mAttribute == mAttribute) && !involvesAttribute) && (other.mArea == mArea); } }; typedef std::vector SpellEffectList; class MWSkill final : public MyGUI::Widget { MYGUI_RTTI_DERIVED( MWSkill ) public: MWSkill(); typedef MWMechanics::Stat SkillValue; void setSkillId(ESM::Skill::SkillEnum skillId); void setSkillNumber(int skillId); void setSkillValue(const SkillValue& value); ESM::Skill::SkillEnum getSkillId() const { return mSkillId; } const SkillValue& getSkillValue() const { return mValue; } // Events typedef MyGUI::delegates::CMultiDelegate1 EventHandle_SkillVoid; /** Event : Skill clicked.\n signature : void method(MWSkill* _sender)\n */ EventHandle_SkillVoid eventClicked; protected: virtual ~MWSkill(); void initialiseOverride() override; void onClicked(MyGUI::Widget* _sender); private: void updateWidgets(); ESM::Skill::SkillEnum mSkillId; SkillValue mValue; MyGUI::TextBox* mSkillNameWidget; MyGUI::TextBox* mSkillValueWidget; }; typedef MWSkill* MWSkillPtr; class MWAttribute final : public MyGUI::Widget { MYGUI_RTTI_DERIVED( MWAttribute ) public: MWAttribute(); typedef MWMechanics::AttributeValue AttributeValue; void setAttributeId(int attributeId); void setAttributeValue(const AttributeValue& value); int getAttributeId() const { return mId; } const AttributeValue& getAttributeValue() const { return mValue; } // Events typedef MyGUI::delegates::CMultiDelegate1 EventHandle_AttributeVoid; /** Event : Attribute clicked.\n signature : void method(MWAttribute* _sender)\n */ EventHandle_AttributeVoid eventClicked; protected: virtual ~MWAttribute(); void initialiseOverride() override; void onClicked(MyGUI::Widget* _sender); private: void updateWidgets(); int mId; AttributeValue mValue; MyGUI::TextBox* mAttributeNameWidget; MyGUI::TextBox* mAttributeValueWidget; }; typedef MWAttribute* MWAttributePtr; /** * @todo remove this class and use MWEffectList instead */ class MWSpellEffect; class MWSpell final : public MyGUI::Widget { MYGUI_RTTI_DERIVED( MWSpell ) public: MWSpell(); typedef MWMechanics::Stat SpellValue; void setSpellId(const std::string &id); /** * @param vector to store the created effect widgets * @param parent widget * @param coordinates to use, will be expanded if more space is needed * @param spell category, if this is 0, this means the spell effects are permanent and won't display e.g. duration * @param various flags, see MWEffectList::EffectFlags */ void createEffectWidgets(std::vector &effects, MyGUI::Widget* creator, MyGUI::IntCoord &coord, int flags); const std::string &getSpellId() const { return mId; } protected: virtual ~MWSpell(); void initialiseOverride() override; private: void updateWidgets(); std::string mId; MyGUI::TextBox* mSpellNameWidget; }; typedef MWSpell* MWSpellPtr; class MWEffectList final : public MyGUI::Widget { MYGUI_RTTI_DERIVED( MWEffectList ) public: MWEffectList(); typedef MWMechanics::Stat EnchantmentValue; enum EffectFlags { EF_NoTarget = 0x01, // potions have no target (target is always the player) EF_Constant = 0x02, // constant effect means that duration will not be displayed EF_NoMagnitude = 0x04 // ingredients have no magnitude }; void setEffectList(const SpellEffectList& list); static SpellEffectList effectListFromESM(const ESM::EffectList* effects); /** * @param vector to store the created effect widgets * @param parent widget * @param coordinates to use, will be expanded if more space is needed * @param center the effect widgets horizontally * @param various flags, see MWEffectList::EffectFlags */ void createEffectWidgets(std::vector &effects, MyGUI::Widget* creator, MyGUI::IntCoord &coord, bool center, int flags); protected: virtual ~MWEffectList(); void initialiseOverride() override; private: void updateWidgets(); SpellEffectList mEffectList; }; typedef MWEffectList* MWEffectListPtr; class MWSpellEffect final : public MyGUI::Widget { MYGUI_RTTI_DERIVED( MWSpellEffect ) public: MWSpellEffect(); typedef ESM::ENAMstruct SpellEffectValue; void setSpellEffect(const SpellEffectParams& params); int getRequestedWidth() const { return mRequestedWidth; } protected: virtual ~MWSpellEffect(); void initialiseOverride() override; private: static constexpr int sIconOffset = 24; void updateWidgets(); SpellEffectParams mEffectParams; MyGUI::ImageBox* mImageWidget; MyGUI::TextBox* mTextWidget; int mRequestedWidth; }; typedef MWSpellEffect* MWSpellEffectPtr; class MWDynamicStat final : public MyGUI::Widget { MYGUI_RTTI_DERIVED( MWDynamicStat ) public: MWDynamicStat(); void setValue(int value, int max); void setTitle(const std::string& text); int getValue() const { return mValue; } int getMax() const { return mMax; } protected: virtual ~MWDynamicStat(); void initialiseOverride() override; private: int mValue, mMax; MyGUI::TextBox* mTextWidget; MyGUI::ProgressBar* mBarWidget; MyGUI::TextBox* mBarTextWidget; }; typedef MWDynamicStat* MWDynamicStatPtr; } } #endif ================================================ FILE: apps/openmw/mwgui/windowbase.cpp ================================================ #include "windowbase.hpp" #include #include #include #include "../mwbase/windowmanager.hpp" #include "../mwbase/environment.hpp" #include #include "draganddrop.hpp" #include "exposedwindow.hpp" using namespace MWGui; WindowBase::WindowBase(const std::string& parLayout) : Layout(parLayout) { mMainWidget->setVisible(false); Window* window = mMainWidget->castType(false); if (!window) return; MyGUI::Button* button = nullptr; MyGUI::VectorWidgetPtr widgets = window->getSkinWidgetsByName("Action"); for (MyGUI::Widget* widget : widgets) { if (widget->isUserString("SupportDoubleClick")) button = widget->castType(); } if (button) button->eventMouseButtonDoubleClick += MyGUI::newDelegate(this, &WindowBase::onDoubleClick); } void WindowBase::onTitleDoubleClicked() { if (MyGUI::InputManager::getInstance().isShiftPressed()) MWBase::Environment::get().getWindowManager()->toggleMaximized(this); } void WindowBase::onDoubleClick(MyGUI::Widget *_sender) { onTitleDoubleClicked(); } void WindowBase::setVisible(bool visible) { bool wasVisible = mMainWidget->getVisible(); mMainWidget->setVisible(visible); if (visible) onOpen(); else if (wasVisible) onClose(); } bool WindowBase::isVisible() { return mMainWidget->getVisible(); } void WindowBase::center() { // Centre dialog MyGUI::IntSize layerSize = MyGUI::RenderManager::getInstance().getViewSize(); if (mMainWidget->getLayer()) layerSize = mMainWidget->getLayer()->getSize(); MyGUI::IntCoord coord = mMainWidget->getCoord(); coord.left = (layerSize.width - coord.width)/2; coord.top = (layerSize.height - coord.height)/2; mMainWidget->setCoord(coord); } WindowModal::WindowModal(const std::string& parLayout) : WindowBase(parLayout) { } void WindowModal::onOpen() { MWBase::Environment::get().getWindowManager()->addCurrentModal(this); //Set so we can escape it if needed MyGUI::Widget* focus = MyGUI::InputManager::getInstance().getKeyFocusWidget(); MyGUI::InputManager::getInstance ().addWidgetModal (mMainWidget); MyGUI::InputManager::getInstance().setKeyFocusWidget(focus); } void WindowModal::onClose() { MWBase::Environment::get().getWindowManager()->removeCurrentModal(this); MyGUI::InputManager::getInstance ().removeWidgetModal (mMainWidget); } NoDrop::NoDrop(DragAndDrop *drag, MyGUI::Widget *widget) : mWidget(widget), mDrag(drag), mTransparent(false) { } void NoDrop::onFrame(float dt) { if (!mWidget) return; MyGUI::IntPoint mousePos = MyGUI::InputManager::getInstance().getMousePosition(); if (mDrag->mIsOnDragAndDrop) { MyGUI::Widget* focus = MyGUI::InputManager::getInstance().getMouseFocusWidget(); while (focus && focus != mWidget) focus = focus->getParent(); if (focus == mWidget) mTransparent = true; } if (!mWidget->getAbsoluteCoord().inside(mousePos)) mTransparent = false; if (mTransparent) { mWidget->setNeedMouseFocus(false); // Allow click-through setAlpha(std::max(0.13f, mWidget->getAlpha() - dt*5)); } else { mWidget->setNeedMouseFocus(true); setAlpha(std::min(1.0f, mWidget->getAlpha() + dt*5)); } } void NoDrop::setAlpha(float alpha) { if (mWidget) mWidget->setAlpha(alpha); } BookWindowBase::BookWindowBase(const std::string& parLayout) : WindowBase(parLayout) { } float BookWindowBase::adjustButton (char const * name) { Gui::ImageButton* button; WindowBase::getWidget (button, name); MyGUI::IntSize requested = button->getRequestedSize(); float scale = float(requested.height) / button->getSize().height; MyGUI::IntSize newSize = requested; newSize.width /= scale; newSize.height /= scale; button->setSize(newSize); if (button->getAlign().isRight()) { MyGUI::IntSize diff = (button->getSize() - requested); diff.width /= scale; diff.height /= scale; button->setPosition(button->getPosition() + MyGUI::IntPoint(diff.width,0)); } return scale; } ================================================ FILE: apps/openmw/mwgui/windowbase.hpp ================================================ #ifndef MWGUI_WINDOW_BASE_H #define MWGUI_WINDOW_BASE_H #include "layout.hpp" namespace MWWorld { class Ptr; } namespace MWGui { class DragAndDrop; class WindowBase: public Layout { public: WindowBase(const std::string& parLayout); virtual MyGUI::Widget* getDefaultKeyFocus() { return nullptr; } // Events typedef MyGUI::delegates::CMultiDelegate1 EventHandle_WindowBase; /// Open this object in the GUI, for windows that support it virtual void setPtr(const MWWorld::Ptr& ptr) {} /// Called every frame if the window is in an active GUI mode virtual void onFrame(float duration) {} /// Notify that window has been made visible virtual void onOpen() {} /// Notify that window has been hidden virtual void onClose () {} /// Gracefully exits the window virtual bool exit() {return true;} /// Sets the visibility of the window void setVisible(bool visible) override; /// Returns the visibility state of the window bool isVisible(); void center(); /// Clear any state specific to the running game virtual void clear() {} /// Called when GUI viewport changes size virtual void onResChange(int width, int height) {} protected: virtual void onTitleDoubleClicked(); private: void onDoubleClick(MyGUI::Widget* _sender); }; /* * "Modal" windows cause the rest of the interface to be inaccessible while they are visible */ class WindowModal : public WindowBase { public: WindowModal(const std::string& parLayout); void onOpen() override; void onClose() override; bool exit() override {return true;} }; /// A window that cannot be the target of a drag&drop action. /// When hovered with a drag item, the window will become transparent and allow click-through. class NoDrop { public: NoDrop(DragAndDrop* drag, MyGUI::Widget* widget); void onFrame(float dt); virtual void setAlpha(float alpha); virtual ~NoDrop() = default; private: MyGUI::Widget* mWidget; DragAndDrop* mDrag; bool mTransparent; }; class BookWindowBase : public WindowBase { public: BookWindowBase(const std::string& parLayout); protected: float adjustButton (char const * name); }; } #endif ================================================ FILE: apps/openmw/mwgui/windowmanagerimp.cpp ================================================ #include "windowmanagerimp.hpp" #include #include #include #include #include #include #include #include #include #include #include #include #include #include // For BT_NO_PROFILE #include #include #include /* Start of tes3mp addition Include additional headers for multiplayer purposes */ #include #include "../mwmp/Main.hpp" #include "../mwmp/GUIController.hpp" /* End of tes3mp addition */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "../mwbase/inputmanager.hpp" #include "../mwbase/statemanager.hpp" #include "../mwbase/soundmanager.hpp" #include "../mwbase/world.hpp" #include "../mwrender/vismask.hpp" #include "../mwworld/class.hpp" #include "../mwworld/player.hpp" #include "../mwworld/cellstore.hpp" #include "../mwworld/esmstore.hpp" #include "../mwmechanics/npcstats.hpp" #include "../mwmechanics/actorutil.hpp" #include "../mwrender/localmap.hpp" #include "console.hpp" #include "journalwindow.hpp" #include "journalviewmodel.hpp" #include "charactercreation.hpp" #include "dialogue.hpp" #include "statswindow.hpp" #include "messagebox.hpp" #include "tooltips.hpp" #include "scrollwindow.hpp" #include "bookwindow.hpp" #include "hud.hpp" #include "mainmenu.hpp" #include "countdialog.hpp" #include "tradewindow.hpp" #include "spellbuyingwindow.hpp" #include "travelwindow.hpp" #include "settingswindow.hpp" #include "confirmationdialog.hpp" #include "alchemywindow.hpp" #include "spellwindow.hpp" #include "quickkeysmenu.hpp" #include "loadingscreen.hpp" #include "levelupdialog.hpp" #include "waitdialog.hpp" #include "enchantingdialog.hpp" #include "trainingwindow.hpp" #include "recharge.hpp" #include "exposedwindow.hpp" #include "cursor.hpp" #include "merchantrepair.hpp" #include "repair.hpp" #include "soulgemdialog.hpp" #include "companionwindow.hpp" #include "inventorywindow.hpp" #include "bookpage.hpp" #include "itemview.hpp" #include "videowidget.hpp" #include "backgroundimage.hpp" #include "itemwidget.hpp" #include "screenfader.hpp" #include "debugwindow.hpp" #include "spellview.hpp" #include "draganddrop.hpp" #include "container.hpp" #include "controllers.hpp" #include "jailscreen.hpp" #include "itemchargeview.hpp" #include "keyboardnavigation.hpp" #include "resourceskin.hpp" namespace MWGui { WindowManager::WindowManager( SDL_Window* window, osgViewer::Viewer* viewer, osg::Group* guiRoot, Resource::ResourceSystem* resourceSystem, SceneUtil::WorkQueue* workQueue, const std::string& logpath, const std::string& resourcePath, bool consoleOnlyScripts, Translation::Storage& translationDataStorage, ToUTF8::FromType encoding, bool exportFonts, const std::string& versionDescription, const std::string& userDataPath) : mOldUpdateMask(0) , mOldCullMask(0) , mStore(nullptr) , mResourceSystem(resourceSystem) , mWorkQueue(workQueue) , mViewer(viewer) , mConsoleOnlyScripts(consoleOnlyScripts) , mCurrentModals() , mHud(nullptr) , mMap(nullptr) , mLocalMapRender(nullptr) , mToolTips(nullptr) , mStatsWindow(nullptr) , mMessageBoxManager(nullptr) , mConsole(nullptr) , mDialogueWindow(nullptr) , mDragAndDrop(nullptr) , mInventoryWindow(nullptr) , mScrollWindow(nullptr) , mBookWindow(nullptr) , mCountDialog(nullptr) , mTradeWindow(nullptr) , mSettingsWindow(nullptr) , mConfirmationDialog(nullptr) , mSpellWindow(nullptr) , mQuickKeysMenu(nullptr) , mLoadingScreen(nullptr) , mWaitDialog(nullptr) , mSoulgemDialog(nullptr) , mVideoBackground(nullptr) , mVideoWidget(nullptr) , mWerewolfFader(nullptr) , mBlindnessFader(nullptr) , mHitFader(nullptr) , mScreenFader(nullptr) , mDebugWindow(nullptr) , mJailScreen(nullptr) , mTranslationDataStorage (translationDataStorage) , mCharGen(nullptr) , mInputBlocker(nullptr) , mCrosshairEnabled(Settings::Manager::getBool ("crosshair", "HUD")) , mSubtitlesEnabled(Settings::Manager::getBool ("subtitles", "GUI")) , mHitFaderEnabled(Settings::Manager::getBool ("hit fader", "GUI")) , mWerewolfOverlayEnabled(Settings::Manager::getBool ("werewolf overlay", "GUI")) , mHudEnabled(true) , mCursorVisible(true) , mCursorActive(true) , mPlayerBounty(-1) , mGui(nullptr) , mGuiModes() , mCursorManager(nullptr) , mGarbageDialogs() , mShown(GW_ALL) , mForceHidden(GW_None) , mAllowed(GW_ALL) , mRestAllowed(true) , mShowOwned(0) , mEncoding(encoding) , mVersionDescription(versionDescription) , mWindowVisible(true) { mScalingFactor = std::clamp(Settings::Manager::getFloat("scaling factor", "GUI"), 0.5f, 8.f); mGuiPlatform = new osgMyGUI::Platform(viewer, guiRoot, resourceSystem->getImageManager(), mScalingFactor); mGuiPlatform->initialise(resourcePath, (boost::filesystem::path(logpath) / "MyGUI.log").generic_string()); mGui = new MyGUI::Gui; mGui->initialise(""); createTextures(); MyGUI::LanguageManager::getInstance().eventRequestTag = MyGUI::newDelegate(this, &WindowManager::onRetrieveTag); // Load fonts mFontLoader.reset(new Gui::FontLoader(encoding, resourceSystem->getVFS(), userDataPath, mScalingFactor)); mFontLoader->loadBitmapFonts(exportFonts); //Register own widgets with MyGUI MyGUI::FactoryManager::getInstance().registerFactory("Widget"); MyGUI::FactoryManager::getInstance().registerFactory("Widget"); MyGUI::FactoryManager::getInstance().registerFactory("Widget"); MyGUI::FactoryManager::getInstance().registerFactory("Widget"); MyGUI::FactoryManager::getInstance().registerFactory("Widget"); MyGUI::FactoryManager::getInstance().registerFactory("Widget"); MyGUI::FactoryManager::getInstance().registerFactory("Widget"); MyGUI::FactoryManager::getInstance().registerFactory("Widget"); MyGUI::FactoryManager::getInstance().registerFactory("Widget"); MyGUI::FactoryManager::getInstance().registerFactory("Layer"); MyGUI::FactoryManager::getInstance().registerFactory("Layer"); BookPage::registerMyGUIComponents (); ItemView::registerComponents(); ItemChargeView::registerComponents(); ItemWidget::registerComponents(); SpellView::registerComponents(); Gui::registerAllWidgets(); MyGUI::FactoryManager::getInstance().registerFactory("Controller"); MyGUI::FactoryManager::getInstance().registerFactory("Resource", "ResourceImageSetPointer"); MyGUI::FactoryManager::getInstance().registerFactory("Resource", "AutoSizedResourceSkin"); MyGUI::ResourceManager::getInstance().load("core.xml"); WindowManager::loadUserFonts(); bool keyboardNav = Settings::Manager::getBool("keyboard navigation", "GUI"); mKeyboardNavigation.reset(new KeyboardNavigation()); mKeyboardNavigation->setEnabled(keyboardNav); Gui::ImageButton::setDefaultNeedKeyFocus(keyboardNav); mLoadingScreen = new LoadingScreen(mResourceSystem, mViewer); mWindows.push_back(mLoadingScreen); //set up the hardware cursor manager mCursorManager = new SDLUtil::SDLCursorManager(); MyGUI::PointerManager::getInstance().eventChangeMousePointer += MyGUI::newDelegate(this, &WindowManager::onCursorChange); MyGUI::InputManager::getInstance().eventChangeKeyFocus += MyGUI::newDelegate(this, &WindowManager::onKeyFocusChanged); // Create all cursors in advance createCursors(); onCursorChange(MyGUI::PointerManager::getInstance().getDefaultPointer()); mCursorManager->setEnabled(true); // hide mygui's pointer MyGUI::PointerManager::getInstance().setVisible(false); mVideoBackground = MyGUI::Gui::getInstance().createWidgetReal("ImageBox", 0,0,1,1, MyGUI::Align::Default, "InputBlocker"); mVideoBackground->setImageTexture("black"); mVideoBackground->setVisible(false); mVideoBackground->setNeedMouseFocus(true); mVideoBackground->setNeedKeyFocus(true); mVideoWidget = mVideoBackground->createWidgetReal("ImageBox", 0,0,1,1, MyGUI::Align::Default); mVideoWidget->setNeedMouseFocus(true); mVideoWidget->setNeedKeyFocus(true); mVideoWidget->setVFS(resourceSystem->getVFS()); // Removes default MyGUI system clipboard implementation, which supports windows only MyGUI::ClipboardManager::getInstance().eventClipboardChanged.clear(); MyGUI::ClipboardManager::getInstance().eventClipboardRequested.clear(); MyGUI::ClipboardManager::getInstance().eventClipboardChanged += MyGUI::newDelegate(this, &WindowManager::onClipboardChanged); MyGUI::ClipboardManager::getInstance().eventClipboardRequested += MyGUI::newDelegate(this, &WindowManager::onClipboardRequested); mShowOwned = Settings::Manager::getInt("show owned", "Game"); mVideoWrapper = new SDLUtil::VideoWrapper(window, viewer); mVideoWrapper->setGammaContrast(Settings::Manager::getFloat("gamma", "Video"), Settings::Manager::getFloat("contrast", "Video")); mStatsWatcher.reset(new StatsWatcher()); } void WindowManager::loadUserFonts() { mFontLoader->loadTrueTypeFonts(); } void WindowManager::initUI() { // Get size info from the Gui object int w = MyGUI::RenderManager::getInstance().getViewSize().width; int h = MyGUI::RenderManager::getInstance().getViewSize().height; mTextColours.loadColours(); mDragAndDrop = new DragAndDrop(); Recharge* recharge = new Recharge(); mGuiModeStates[GM_Recharge] = GuiModeState(recharge); mWindows.push_back(recharge); MainMenu* menu = new MainMenu(w, h, mResourceSystem->getVFS(), mVersionDescription); mGuiModeStates[GM_MainMenu] = GuiModeState(menu); mWindows.push_back(menu); mLocalMapRender = new MWRender::LocalMap(mViewer->getSceneData()->asGroup()); mMap = new MapWindow(mCustomMarkers, mDragAndDrop, mLocalMapRender, mWorkQueue); mWindows.push_back(mMap); mMap->renderGlobalMap(); trackWindow(mMap, "map"); mStatsWindow = new StatsWindow(mDragAndDrop); mWindows.push_back(mStatsWindow); trackWindow(mStatsWindow, "stats"); mInventoryWindow = new InventoryWindow(mDragAndDrop, mViewer->getSceneData()->asGroup(), mResourceSystem); mWindows.push_back(mInventoryWindow); mSpellWindow = new SpellWindow(mDragAndDrop); mWindows.push_back(mSpellWindow); trackWindow(mSpellWindow, "spells"); mGuiModeStates[GM_Inventory] = GuiModeState({mMap, mInventoryWindow, mSpellWindow, mStatsWindow}); mGuiModeStates[GM_None] = GuiModeState({mMap, mInventoryWindow, mSpellWindow, mStatsWindow}); mTradeWindow = new TradeWindow(); mWindows.push_back(mTradeWindow); trackWindow(mTradeWindow, "barter"); mGuiModeStates[GM_Barter] = GuiModeState({mInventoryWindow, mTradeWindow}); mConsole = new Console(w,h, mConsoleOnlyScripts); mWindows.push_back(mConsole); trackWindow(mConsole, "console"); bool questList = mResourceSystem->getVFS()->exists("textures/tx_menubook_options_over.dds"); JournalWindow* journal = JournalWindow::create(JournalViewModel::create (), questList, mEncoding); mWindows.push_back(journal); mGuiModeStates[GM_Journal] = GuiModeState(journal); mGuiModeStates[GM_Journal].mCloseSound = "book close"; mGuiModeStates[GM_Journal].mOpenSound = "book open"; mMessageBoxManager = new MessageBoxManager(mStore->get().find("fMessageTimePerChar")->mValue.getFloat()); SpellBuyingWindow* spellBuyingWindow = new SpellBuyingWindow(); mWindows.push_back(spellBuyingWindow); mGuiModeStates[GM_SpellBuying] = GuiModeState(spellBuyingWindow); TravelWindow* travelWindow = new TravelWindow(); mWindows.push_back(travelWindow); mGuiModeStates[GM_Travel] = GuiModeState(travelWindow); mDialogueWindow = new DialogueWindow(); mWindows.push_back(mDialogueWindow); trackWindow(mDialogueWindow, "dialogue"); mGuiModeStates[GM_Dialogue] = GuiModeState(mDialogueWindow); mTradeWindow->eventTradeDone += MyGUI::newDelegate(mDialogueWindow, &DialogueWindow::onTradeComplete); /* Start of tes3mp change (major) Use a member variable (mContainerWIndow) instead of a local one so we can access it from elsewhere */ mContainerWindow = new ContainerWindow(mDragAndDrop); mWindows.push_back(mContainerWindow); trackWindow(mContainerWindow, "container"); mGuiModeStates[GM_Container] = GuiModeState({mContainerWindow, mInventoryWindow}); /* End of tes3mp change (major) */ mHud = new HUD(mCustomMarkers, mDragAndDrop, mLocalMapRender); mWindows.push_back(mHud); mToolTips = new ToolTips(); mScrollWindow = new ScrollWindow(); mWindows.push_back(mScrollWindow); mGuiModeStates[GM_Scroll] = GuiModeState(mScrollWindow); mGuiModeStates[GM_Scroll].mOpenSound = "scroll"; mGuiModeStates[GM_Scroll].mCloseSound = "scroll"; mBookWindow = new BookWindow(); mWindows.push_back(mBookWindow); mGuiModeStates[GM_Book] = GuiModeState(mBookWindow); mGuiModeStates[GM_Book].mOpenSound = "book open"; mGuiModeStates[GM_Book].mCloseSound = "book close"; mCountDialog = new CountDialog(); mWindows.push_back(mCountDialog); mSettingsWindow = new SettingsWindow(); mWindows.push_back(mSettingsWindow); mGuiModeStates[GM_Settings] = GuiModeState(mSettingsWindow); mConfirmationDialog = new ConfirmationDialog(); mWindows.push_back(mConfirmationDialog); AlchemyWindow* alchemyWindow = new AlchemyWindow(); mWindows.push_back(alchemyWindow); trackWindow(alchemyWindow, "alchemy"); mGuiModeStates[GM_Alchemy] = GuiModeState(alchemyWindow); mQuickKeysMenu = new QuickKeysMenu(); mWindows.push_back(mQuickKeysMenu); mGuiModeStates[GM_QuickKeysMenu] = GuiModeState(mQuickKeysMenu); LevelupDialog* levelupDialog = new LevelupDialog(); mWindows.push_back(levelupDialog); mGuiModeStates[GM_Levelup] = GuiModeState(levelupDialog); mWaitDialog = new WaitDialog(); mWindows.push_back(mWaitDialog); mGuiModeStates[GM_Rest] = GuiModeState({mWaitDialog->getProgressBar(), mWaitDialog}); SpellCreationDialog* spellCreationDialog = new SpellCreationDialog(); mWindows.push_back(spellCreationDialog); mGuiModeStates[GM_SpellCreation] = GuiModeState(spellCreationDialog); EnchantingDialog* enchantingDialog = new EnchantingDialog(); mWindows.push_back(enchantingDialog); mGuiModeStates[GM_Enchanting] = GuiModeState(enchantingDialog); TrainingWindow* trainingWindow = new TrainingWindow(); mWindows.push_back(trainingWindow); mGuiModeStates[GM_Training] = GuiModeState({trainingWindow->getProgressBar(), trainingWindow}); MerchantRepair* merchantRepair = new MerchantRepair(); mWindows.push_back(merchantRepair); mGuiModeStates[GM_MerchantRepair] = GuiModeState(merchantRepair); Repair* repair = new Repair(); mWindows.push_back(repair); mGuiModeStates[GM_Repair] = GuiModeState(repair); mSoulgemDialog = new SoulgemDialog(mMessageBoxManager); CompanionWindow* companionWindow = new CompanionWindow(mDragAndDrop, mMessageBoxManager); mWindows.push_back(companionWindow); trackWindow(companionWindow, "companion"); mGuiModeStates[GM_Companion] = GuiModeState({mInventoryWindow, companionWindow}); mJailScreen = new JailScreen(); mWindows.push_back(mJailScreen); mGuiModeStates[GM_Jail] = GuiModeState(mJailScreen); std::string werewolfFaderTex = "textures\\werewolfoverlay.dds"; if (mResourceSystem->getVFS()->exists(werewolfFaderTex)) { mWerewolfFader = new ScreenFader(werewolfFaderTex); mWindows.push_back(mWerewolfFader); } mBlindnessFader = new ScreenFader("black"); mWindows.push_back(mBlindnessFader); // fall back to player_hit_01.dds if bm_player_hit_01.dds is not available std::string hitFaderTexture = "textures\\bm_player_hit_01.dds"; const std::string hitFaderLayout = "openmw_screen_fader_hit.layout"; MyGUI::FloatCoord hitFaderCoord (0,0,1,1); if(!mResourceSystem->getVFS()->exists(hitFaderTexture)) { hitFaderTexture = "textures\\player_hit_01.dds"; hitFaderCoord = MyGUI::FloatCoord(0.2, 0.25, 0.6, 0.5); } mHitFader = new ScreenFader(hitFaderTexture, hitFaderLayout, hitFaderCoord); mWindows.push_back(mHitFader); mScreenFader = new ScreenFader("black"); mWindows.push_back(mScreenFader); mDebugWindow = new DebugWindow(); mWindows.push_back(mDebugWindow); mInputBlocker = MyGUI::Gui::getInstance().createWidget("",0,0,w,h,MyGUI::Align::Stretch,"InputBlocker"); mHud->setVisible(true); mCharGen = new CharacterCreation(mViewer->getSceneData()->asGroup(), mResourceSystem); updatePinnedWindows(); // Set up visibility updateVisible(); mStatsWatcher->addListener(mHud); mStatsWatcher->addListener(mStatsWindow); mStatsWatcher->addListener(mCharGen); } int WindowManager::getFontHeight() const { return mFontLoader->getFontHeight(); } void WindowManager::setNewGame(bool newgame) { if (newgame) { disallowAll(); mStatsWatcher->removeListener(mCharGen); delete mCharGen; mCharGen = new CharacterCreation(mViewer->getSceneData()->asGroup(), mResourceSystem); mStatsWatcher->addListener(mCharGen); } else allow(GW_ALL); mStatsWatcher->forceUpdate(); } WindowManager::~WindowManager() { try { mStatsWatcher.reset(); MyGUI::LanguageManager::getInstance().eventRequestTag.clear(); MyGUI::PointerManager::getInstance().eventChangeMousePointer.clear(); MyGUI::InputManager::getInstance().eventChangeKeyFocus.clear(); MyGUI::ClipboardManager::getInstance().eventClipboardChanged.clear(); MyGUI::ClipboardManager::getInstance().eventClipboardRequested.clear(); for (WindowBase* window : mWindows) delete window; mWindows.clear(); delete mMessageBoxManager; delete mLocalMapRender; delete mCharGen; delete mDragAndDrop; delete mSoulgemDialog; delete mCursorManager; delete mToolTips; mKeyboardNavigation.reset(); cleanupGarbage(); mFontLoader.reset(); mGui->shutdown(); delete mGui; mGuiPlatform->shutdown(); delete mGuiPlatform; delete mVideoWrapper; } catch(const MyGUI::Exception& e) { Log(Debug::Error) << "Error in the destructor: " << e.what(); } } void WindowManager::setStore(const MWWorld::ESMStore &store) { mStore = &store; } void WindowManager::cleanupGarbage() { // Delete any dialogs which are no longer in use if (!mGarbageDialogs.empty()) { for (Layout* widget : mGarbageDialogs) { delete widget; } mGarbageDialogs.clear(); } } void WindowManager::enableScene(bool enable) { unsigned int disablemask = MWRender::Mask_GUI|MWRender::Mask_PreCompile; if (!enable && mViewer->getCamera()->getCullMask() != disablemask) { mOldUpdateMask = mViewer->getUpdateVisitor()->getTraversalMask(); mOldCullMask = mViewer->getCamera()->getCullMask(); mViewer->getUpdateVisitor()->setTraversalMask(disablemask); mViewer->getCamera()->setCullMask(disablemask); } else if (enable && mViewer->getCamera()->getCullMask() == disablemask) { mViewer->getUpdateVisitor()->setTraversalMask(mOldUpdateMask); mViewer->getCamera()->setCullMask(mOldCullMask); } } void WindowManager::updateConsoleObjectPtr(const MWWorld::Ptr& currentPtr, const MWWorld::Ptr& newPtr) { mConsole->updateSelectedObjectPtr(currentPtr, newPtr); } void WindowManager::updateVisible() { bool loading = (getMode() == GM_Loading || getMode() == GM_LoadingWallpaper); bool mainmenucover = containsMode(GM_MainMenu) && MWBase::Environment::get().getStateManager()->getState() == MWBase::StateManager::State_NoGame; enableScene(!loading && !mainmenucover); if (!mMap) return; // UI not created yet mHud->setVisible(mHudEnabled && !loading); mToolTips->setVisible(mHudEnabled && !loading); bool gameMode = !isGuiMode(); MWBase::Environment::get().getInputManager()->changeInputMode(!gameMode); mInputBlocker->setVisible (gameMode); if (loading) setCursorVisible(mMessageBoxManager && mMessageBoxManager->isInteractiveMessageBox()); else setCursorVisible(!gameMode); if (gameMode) setKeyFocusWidget (nullptr); // Icons of forced hidden windows are displayed setMinimapVisibility((mAllowed & GW_Map) && (!mMap->pinned() || (mForceHidden & GW_Map))); setWeaponVisibility((mAllowed & GW_Inventory) && (!mInventoryWindow->pinned() || (mForceHidden & GW_Inventory))); setSpellVisibility((mAllowed & GW_Magic) && (!mSpellWindow->pinned() || (mForceHidden & GW_Magic))); setHMSVisibility((mAllowed & GW_Stats) && (!mStatsWindow->pinned() || (mForceHidden & GW_Stats))); mInventoryWindow->setGuiMode(getMode()); // If in game mode (or interactive messagebox), show the pinned windows if (mGuiModes.empty()) { mMap->setVisible(mMap->pinned() && !isConsoleMode() && !(mForceHidden & GW_Map) && (mAllowed & GW_Map)); mStatsWindow->setVisible(mStatsWindow->pinned() && !isConsoleMode() && !(mForceHidden & GW_Stats) && (mAllowed & GW_Stats)); mInventoryWindow->setVisible(mInventoryWindow->pinned() && !isConsoleMode() && !(mForceHidden & GW_Inventory) && (mAllowed & GW_Inventory)); mSpellWindow->setVisible(mSpellWindow->pinned() && !isConsoleMode() && !(mForceHidden & GW_Magic) && (mAllowed & GW_Magic)); return; } else if (getMode() != GM_Inventory) { mMap->setVisible(false); mStatsWindow->setVisible(false); mSpellWindow->setVisible(false); mInventoryWindow->setVisible(getMode() == GM_Container || getMode() == GM_Barter || getMode() == GM_Companion); } GuiMode mode = mGuiModes.back(); mInventoryWindow->setTrading(mode == GM_Barter); if (getMode() == GM_Inventory) { // For the inventory mode, compute the effective set of windows to show. // This is controlled both by what windows the // user has opened/closed (the 'shown' variable) and by what // windows we are allowed to show (the 'allowed' var.) int eff = mShown & mAllowed & ~mForceHidden; mMap->setVisible(eff & GW_Map); mInventoryWindow->setVisible(eff & GW_Inventory); mSpellWindow->setVisible(eff & GW_Magic); mStatsWindow->setVisible(eff & GW_Stats); } switch (mode) { // FIXME: refactor chargen windows to use modes properly (or not use them at all) case GM_Name: case GM_Race: case GM_Class: case GM_ClassPick: case GM_ClassCreate: case GM_Birth: case GM_ClassGenerate: case GM_Review: mCharGen->spawnDialog(mode); break; default: /* Start of tes3mp addition Pass the GuiMode further on to the multiplayer-specific GUI controller */ mwmp::Main::get().getGUIController()->WM_UpdateVisible(mode); /* End of tes3mp addition */ break; } } void WindowManager::setDrowningTimeLeft (float time, float maxTime) { mHud->setDrowningTimeLeft(time, maxTime); } void WindowManager::removeDialog(Layout*dialog) { if (!dialog) return; dialog->setVisible(false); mGarbageDialogs.push_back(dialog); } void WindowManager::exitCurrentGuiMode() { if (mDragAndDrop && mDragAndDrop->mIsOnDragAndDrop) { mDragAndDrop->finish(); return; } GuiModeState& state = mGuiModeStates[mGuiModes.back()]; for (WindowBase* window : state.mWindows) { if (!window->exit()) { // unable to exit window, but give access to main menu if (!MyGUI::InputManager::getInstance().isModalAny() && getMode() != GM_MainMenu) pushGuiMode (GM_MainMenu); return; } } popGuiMode(); } /* Start of tes3mp change (major) Add a hasServerOrigin boolean to the list of arguments so those messageboxes can be differentiated from client-only ones Use the hasServerOrigin argument when creating an interactive message box */ void WindowManager::interactiveMessageBox(const std::string &message, const std::vector &buttons, bool block, bool hasServerOrigin) { mMessageBoxManager->createInteractiveMessageBox(message, buttons, hasServerOrigin); /* End of tes3mp change (major) */ updateVisible(); if (block) { Misc::FrameRateLimiter frameRateLimiter = Misc::makeFrameRateLimiter(MWBase::Environment::get().getFrameRateLimit()); while (mMessageBoxManager->readPressedButton(false) == -1 && !MWBase::Environment::get().getStateManager()->hasQuitRequest()) { const double dt = std::chrono::duration_cast>(frameRateLimiter.getLastFrameDuration()).count(); mKeyboardNavigation->onFrame(); mMessageBoxManager->onFrame(dt); MWBase::Environment::get().getInputManager()->update(dt, true, false); if (!mWindowVisible) std::this_thread::sleep_for(std::chrono::milliseconds(5)); else { mViewer->eventTraversal(); mViewer->updateTraversal(); mViewer->renderingTraversals(); } // at the time this function is called we are in the middle of a frame, // so out of order calls are necessary to get a correct frameNumber for the next frame. // refer to the advance() and frame() order in Engine::go() mViewer->advance(mViewer->getFrameStamp()->getSimulationTime()); frameRateLimiter.limit(); } } } void WindowManager::messageBox (const std::string& message, enum MWGui::ShowInDialogueMode showInDialogueMode) { if (getMode() == GM_Dialogue && showInDialogueMode != MWGui::ShowInDialogueMode_Never) { mDialogueWindow->addMessageBox(MyGUI::LanguageManager::getInstance().replaceTags(message)); } else if (showInDialogueMode != MWGui::ShowInDialogueMode_Only) { mMessageBoxManager->createMessageBox(message); } } void WindowManager::staticMessageBox(const std::string& message) { mMessageBoxManager->createMessageBox(message, true); } void WindowManager::removeStaticMessageBox() { mMessageBoxManager->removeStaticMessageBox(); } int WindowManager::readPressedButton () { return mMessageBoxManager->readPressedButton(); } std::string WindowManager::getGameSettingString(const std::string &id, const std::string &default_) { const ESM::GameSetting *setting = mStore->get().search(id); if (setting && setting->mValue.getType()==ESM::VT_String) return setting->mValue.getString(); return default_; } void WindowManager::updateMap() { if (!mLocalMapRender) return; MWWorld::ConstPtr player = MWMechanics::getPlayer(); osg::Vec3f playerPosition = player.getRefData().getPosition().asVec3(); osg::Quat playerOrientation (-player.getRefData().getPosition().rot[2], osg::Vec3(0,0,1)); osg::Vec3f playerdirection; int x,y; float u,v; mLocalMapRender->updatePlayer(playerPosition, playerOrientation, u, v, x, y, playerdirection); if (!player.getCell()->isExterior()) { setActiveMap(x, y, true); } // else: need to know the current grid center, call setActiveMap from changeCell mMap->setPlayerDir(playerdirection.x(), playerdirection.y()); mMap->setPlayerPos(x, y, u, v); mHud->setPlayerDir(playerdirection.x(), playerdirection.y()); mHud->setPlayerPos(x, y, u, v); } void WindowManager::update (float frameDuration) { bool gameRunning = MWBase::Environment::get().getStateManager()->getState()!= MWBase::StateManager::State_NoGame; if (gameRunning) updateMap(); if (!mGuiModes.empty()) { GuiModeState& state = mGuiModeStates[mGuiModes.back()]; for (WindowBase* window : state.mWindows) window->onFrame(frameDuration); } else { // update pinned windows if visible for (WindowBase* window : mGuiModeStates[GM_Inventory].mWindows) if (window->isVisible()) window->onFrame(frameDuration); } /* Start of tes3mp addition Fix crashes caused by messageboxes that never have their modals erased elsewhere, working around one of the main GUI-related problems that arise in an unpaused environment */ for (auto modalIterator = mCurrentModals.begin(); modalIterator != mCurrentModals.end();) { if ((*modalIterator)->mMainWidget == 0) { mCurrentModals.erase(modalIterator); } else { ++modalIterator; } } /* End of tes3mp addition */ // Make sure message boxes are always in front // This is an awful workaround for a series of awfully interwoven issues that couldn't be worked around // in a better way because of an impressive number of even more awfully interwoven issues. if (mMessageBoxManager && mMessageBoxManager->isInteractiveMessageBox() && mCurrentModals.back() != mMessageBoxManager->getInteractiveMessageBox()) { std::vector::iterator found = std::find(mCurrentModals.begin(), mCurrentModals.end(), mMessageBoxManager->getInteractiveMessageBox()); if (found != mCurrentModals.end()) { WindowModal* msgbox = *found; std::swap(*found, mCurrentModals.back()); MyGUI::InputManager::getInstance().addWidgetModal(msgbox->mMainWidget); mKeyboardNavigation->setModalWindow(msgbox->mMainWidget); mKeyboardNavigation->setDefaultFocus(msgbox->mMainWidget, msgbox->getDefaultKeyFocus()); } } if (!mCurrentModals.empty()) mCurrentModals.back()->onFrame(frameDuration); mKeyboardNavigation->onFrame(); if (mMessageBoxManager) mMessageBoxManager->onFrame(frameDuration); mToolTips->onFrame(frameDuration); if (mLocalMapRender) mLocalMapRender->cleanupCameras(); if (!gameRunning) return; // We should display message about crime only once per frame, even if there are several crimes. // Otherwise we will get message spam when stealing several items via Take All button. const MWWorld::Ptr player = MWMechanics::getPlayer(); int currentBounty = player.getClass().getNpcStats(player).getBounty(); if (currentBounty != mPlayerBounty) { if (mPlayerBounty >= 0 && currentBounty > mPlayerBounty) messageBox("#{sCrimeMessage}"); mPlayerBounty = currentBounty; } mDragAndDrop->onFrame(); mHud->onFrame(frameDuration); mDebugWindow->onFrame(frameDuration); if (mCharGen) mCharGen->onFrame(frameDuration); updateActivatedQuickKey(); mStatsWatcher->update(); cleanupGarbage(); } void WindowManager::changeCell(const MWWorld::CellStore* cell) { mMap->requestMapRender(cell); std::string name = MWBase::Environment::get().getWorld()->getCellName (cell); mMap->setCellName( name ); mHud->setCellName( name ); if (cell->getCell()->isExterior()) { if (!cell->getCell()->mName.empty()) mMap->addVisitedLocation (name, cell->getCell()->getGridX (), cell->getCell()->getGridY ()); mMap->cellExplored (cell->getCell()->getGridX(), cell->getCell()->getGridY()); setActiveMap(cell->getCell()->getGridX(), cell->getCell()->getGridY(), false); } else { mMap->setCellPrefix (cell->getCell()->mName ); mHud->setCellPrefix (cell->getCell()->mName ); osg::Vec3f worldPos; if (!MWBase::Environment::get().getWorld()->findInteriorPositionInWorldSpace(cell, worldPos)) worldPos = MWBase::Environment::get().getWorld()->getPlayer().getLastKnownExteriorPosition(); else MWBase::Environment::get().getWorld()->getPlayer().setLastKnownExteriorPosition(worldPos); mMap->setGlobalMapPlayerPosition(worldPos.x(), worldPos.y()); setActiveMap(0, 0, true); } } /* Start of tes3mp addition Allow the setting of the image data for a global map tile from elsewhere in the code */ void WindowManager::setGlobalMapImage(int cellX, int cellY, const std::vector& imageData) { mMap->setGlobalMapImage(cellX, cellY, imageData); } /* End of tes3mp addition */ void WindowManager::setActiveMap(int x, int y, bool interior) { mMap->setActiveCell(x,y, interior); mHud->setActiveCell(x,y, interior); } void WindowManager::setDrowningBarVisibility(bool visible) { mHud->setDrowningBarVisible(visible); } void WindowManager::setHMSVisibility(bool visible) { mHud->setHmsVisible (visible); } void WindowManager::setMinimapVisibility(bool visible) { mHud->setMinimapVisible (visible); } bool WindowManager::toggleFogOfWar() { mMap->toggleFogOfWar(); return mHud->toggleFogOfWar(); } void WindowManager::setFocusObject(const MWWorld::Ptr& focus) { mToolTips->setFocusObject(focus); if(mHud && (mShowOwned == 2 || mShowOwned == 3)) { bool owned = mToolTips->checkOwned(); mHud->setCrosshairOwned(owned); } } void WindowManager::setFocusObjectScreenCoords(float min_x, float min_y, float max_x, float max_y) { mToolTips->setFocusObjectScreenCoords(min_x, min_y, max_x, max_y); } bool WindowManager::toggleFullHelp() { return mToolTips->toggleFullHelp(); } bool WindowManager::getFullHelp() const { return mToolTips->getFullHelp(); } void WindowManager::setWeaponVisibility(bool visible) { mHud->setWeapVisible (visible); } void WindowManager::setSpellVisibility(bool visible) { mHud->setSpellVisible (visible); mHud->setEffectVisible (visible); } void WindowManager::setSneakVisibility(bool visible) { mHud->setSneakVisible(visible); } void WindowManager::setDragDrop(bool dragDrop) { mToolTips->setEnabled(!dragDrop); MWBase::Environment::get().getInputManager()->setDragDrop(dragDrop); } /* Start of tes3mp addition Allow the completion of a drag and drop from elsewhere in the code */ void WindowManager::finishDragDrop() { if (mDragAndDrop->mIsOnDragAndDrop) mDragAndDrop->finish(); } /* End of tes3mp addition */ void WindowManager::setCursorVisible(bool visible) { mCursorVisible = visible; } void WindowManager::setCursorActive(bool active) { mCursorActive = active; } void WindowManager::onRetrieveTag(const MyGUI::UString& _tag, MyGUI::UString& _result) { std::string tag(_tag); std::string MyGuiPrefix = "setting="; size_t MyGuiPrefixLength = MyGuiPrefix.length(); std::string tokenToFind = "sCell="; size_t tokenLength = tokenToFind.length(); if(tag.compare(0, MyGuiPrefixLength, MyGuiPrefix) == 0) { tag = tag.substr(MyGuiPrefixLength, tag.length()); size_t comma_pos = tag.find(','); std::string settingSection = tag.substr(0, comma_pos); std::string settingTag = tag.substr(comma_pos+1, tag.length()); _result = Settings::Manager::getString(settingTag, settingSection); } else if (tag.compare(0, tokenLength, tokenToFind) == 0) { _result = mTranslationDataStorage.translateCellName(tag.substr(tokenLength)); _result = MyGUI::TextIterator::toTagsString(_result); } else if (Gui::replaceTag(tag, _result)) { return; } else { if (!mStore) { Log(Debug::Error) << "Error: WindowManager::onRetrieveTag: no Store set up yet, can not replace '" << tag << "'"; return; } const ESM::GameSetting *setting = mStore->get().find(tag); if (setting && setting->mValue.getType()==ESM::VT_String) _result = setting->mValue.getString(); else _result = tag; } } void WindowManager::processChangedSettings(const Settings::CategorySettingVector& changed) { mToolTips->setDelay(Settings::Manager::getFloat("tooltip delay", "GUI")); bool changeRes = false; for (const auto& setting : changed) { if (setting.first == "HUD" && setting.second == "crosshair") mCrosshairEnabled = Settings::Manager::getBool ("crosshair", "HUD"); else if (setting.first == "GUI" && setting.second == "subtitles") mSubtitlesEnabled = Settings::Manager::getBool ("subtitles", "GUI"); else if (setting.first == "GUI" && setting.second == "menu transparency") setMenuTransparency(Settings::Manager::getFloat("menu transparency", "GUI")); else if (setting.first == "Video" && ( setting.second == "resolution x" || setting.second == "resolution y" || setting.second == "fullscreen" || setting.second == "window border")) changeRes = true; else if (setting.first == "Video" && setting.second == "vsync") mVideoWrapper->setSyncToVBlank(Settings::Manager::getBool("vsync", "Video")); else if (setting.first == "Video" && (setting.second == "gamma" || setting.second == "contrast")) mVideoWrapper->setGammaContrast(Settings::Manager::getFloat("gamma", "Video"), Settings::Manager::getFloat("contrast", "Video")); } if (changeRes) { mVideoWrapper->setVideoMode(Settings::Manager::getInt("resolution x", "Video"), Settings::Manager::getInt("resolution y", "Video"), Settings::Manager::getBool("fullscreen", "Video"), Settings::Manager::getBool("window border", "Video")); } } 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"); mGuiPlatform->getRenderManagerPtr()->setViewSize(x, y); // scaled size const MyGUI::IntSize& viewSize = MyGUI::RenderManager::getInstance().getViewSize(); x = viewSize.width; y = viewSize.height; sizeVideo(x, y); if (!mHud) return; // UI not initialized yet for (std::map::iterator it = mTrackedWindows.begin(); it != mTrackedWindows.end(); ++it) { std::string settingName = it->second; if (Settings::Manager::getBool(settingName + " maximized", "Windows")) settingName += " maximized"; MyGUI::IntPoint pos(static_cast(Settings::Manager::getFloat(settingName + " x", "Windows") * x), static_cast(Settings::Manager::getFloat(settingName + " y", "Windows") * y)); MyGUI::IntSize size(static_cast(Settings::Manager::getFloat(settingName + " w", "Windows") * x), static_cast(Settings::Manager::getFloat(settingName + " h", "Windows") * y)); it->first->setPosition(pos); it->first->setSize(size); } for (WindowBase* window : mWindows) window->onResChange(x, y); // We should reload TrueType fonts to fit new resolution loadUserFonts(); // TODO: check if any windows are now off-screen and move them back if so } bool WindowManager::isWindowVisible() { return mWindowVisible; } void WindowManager::windowVisibilityChange(bool visible) { mWindowVisible = visible; } void WindowManager::windowClosed() { MWBase::Environment::get().getStateManager()->requestQuit(); } void WindowManager::onCursorChange(const std::string &name) { mCursorManager->cursorChanged(name); } void WindowManager::pushGuiMode(GuiMode mode) { pushGuiMode(mode, MWWorld::Ptr()); } void WindowManager::pushGuiMode(GuiMode mode, const MWWorld::Ptr& arg) { if (mode==GM_Inventory && mAllowed==GW_None) return; if (mGuiModes.empty() || mGuiModes.back() != mode) { // If this mode already exists somewhere in the stack, just bring it to the front. if (std::find(mGuiModes.begin(), mGuiModes.end(), mode) != mGuiModes.end()) { mGuiModes.erase(std::find(mGuiModes.begin(), mGuiModes.end(), mode)); } if (!mGuiModes.empty()) { mKeyboardNavigation->saveFocus(mGuiModes.back()); mGuiModeStates[mGuiModes.back()].update(false); } mGuiModes.push_back(mode); mGuiModeStates[mode].update(true); playSound(mGuiModeStates[mode].mOpenSound); } for (WindowBase* window : mGuiModeStates[mode].mWindows) window->setPtr(arg); mKeyboardNavigation->restoreFocus(mode); updateVisible(); } void WindowManager::popGuiMode(bool noSound) { if (mDragAndDrop && mDragAndDrop->mIsOnDragAndDrop) { mDragAndDrop->finish(); } if (!mGuiModes.empty()) { const GuiMode mode = mGuiModes.back(); mKeyboardNavigation->saveFocus(mode); mGuiModes.pop_back(); mGuiModeStates[mode].update(false); if (!noSound) playSound(mGuiModeStates[mode].mCloseSound); } if (!mGuiModes.empty()) { const GuiMode mode = mGuiModes.back(); mGuiModeStates[mode].update(true); mKeyboardNavigation->restoreFocus(mode); } updateVisible(); // To make sure that console window get focus again if (mConsole && mConsole->isVisible()) mConsole->onOpen(); } void WindowManager::removeGuiMode(GuiMode mode, bool noSound) { if (!mGuiModes.empty() && mGuiModes.back() == mode) { popGuiMode(noSound); return; } std::vector::iterator it = mGuiModes.begin(); while (it != mGuiModes.end()) { if (*it == mode) it = mGuiModes.erase(it); else ++it; } updateVisible(); } void WindowManager::goToJail(int days) { pushGuiMode(MWGui::GM_Jail); mJailScreen->goToJail(days); } void WindowManager::setSelectedSpell(const std::string& spellId, int successChancePercent) { mSelectedSpell = spellId; mSelectedEnchantItem = MWWorld::Ptr(); mHud->setSelectedSpell(spellId, successChancePercent); const ESM::Spell* spell = mStore->get().find(spellId); mSpellWindow->setTitle(spell->mName); } void WindowManager::setSelectedEnchantItem(const MWWorld::Ptr& item) { mSelectedEnchantItem = item; mSelectedSpell = ""; const ESM::Enchantment* ench = mStore->get() .find(item.getClass().getEnchantment(item)); int chargePercent = static_cast(item.getCellRef().getNormalizedEnchantmentCharge(ench->mData.mCharge) * 100); mHud->setSelectedEnchantItem(item, chargePercent); mSpellWindow->setTitle(item.getClass().getName(item)); } const MWWorld::Ptr &WindowManager::getSelectedEnchantItem() const { return mSelectedEnchantItem; } void WindowManager::setSelectedWeapon(const MWWorld::Ptr& item) { mSelectedWeapon = item; int durabilityPercent = 100; if (item.getClass().hasItemHealth(item)) { durabilityPercent = static_cast(item.getClass().getItemNormalizedHealth(item) * 100); } mHud->setSelectedWeapon(item, durabilityPercent); mInventoryWindow->setTitle(item.getClass().getName(item)); } const MWWorld::Ptr &WindowManager::getSelectedWeapon() const { return mSelectedWeapon; } void WindowManager::unsetSelectedSpell() { mSelectedSpell = ""; mSelectedEnchantItem = MWWorld::Ptr(); mHud->unsetSelectedSpell(); MWWorld::Player* player = &MWBase::Environment::get().getWorld()->getPlayer(); if (player->getDrawState() == MWMechanics::DrawState_Spell) player->setDrawState(MWMechanics::DrawState_Nothing); mSpellWindow->setTitle("#{sNone}"); } void WindowManager::unsetSelectedWeapon() { mSelectedWeapon = MWWorld::Ptr(); mHud->unsetSelectedWeapon(); mInventoryWindow->setTitle("#{sSkillHandtohand}"); } void WindowManager::getMousePosition(int &x, int &y) { const MyGUI::IntPoint& pos = MyGUI::InputManager::getInstance().getMousePosition(); x = pos.left; y = pos.top; } void WindowManager::getMousePosition(float &x, float &y) { const MyGUI::IntPoint& pos = MyGUI::InputManager::getInstance().getMousePosition(); x = static_cast(pos.left); y = static_cast(pos.top); const MyGUI::IntSize& viewSize = MyGUI::RenderManager::getInstance().getViewSize(); x /= viewSize.width; y /= viewSize.height; } bool WindowManager::getWorldMouseOver() { return mHud->getWorldMouseOver(); } float WindowManager::getScalingFactor() { return mScalingFactor; } void WindowManager::executeInConsole (const std::string& path) { mConsole->executeFile (path); } /* Start of tes3mp addition Allow the execution of console commands from elsewhere in the code */ void WindowManager::executeCommandInConsole(const std::string& command) { mConsole->execute(command); } /* End of tes3mp addition */ MWGui::InventoryWindow* WindowManager::getInventoryWindow() { return mInventoryWindow; } MWGui::CountDialog* WindowManager::getCountDialog() { return mCountDialog; } MWGui::ConfirmationDialog* WindowManager::getConfirmationDialog() { return mConfirmationDialog; } MWGui::TradeWindow* WindowManager::getTradeWindow() { return mTradeWindow; } /* Start of tes3mp addition Make it possible to get the ContainerWindow from elsewhere in the code */ MWGui::ContainerWindow* WindowManager::getContainerWindow() { return mContainerWindow; } /* End of tes3mp addition */ /* Start of tes3mp addition Make it possible to get the DialogueWindow from elsewhere */ MWGui::DialogueWindow* WindowManager::getDialogueWindow() { return mDialogueWindow; } /* End of tes3mp addition */ void WindowManager::useItem(const MWWorld::Ptr &item, bool bypassBeastRestrictions) { if (mInventoryWindow) mInventoryWindow->useItem(item, bypassBeastRestrictions); } bool WindowManager::isAllowed (GuiWindow wnd) const { return (mAllowed & wnd) != 0; } void WindowManager::allow (GuiWindow wnd) { mAllowed = (GuiWindow)(mAllowed | wnd); if (wnd & GW_Inventory) { mBookWindow->setInventoryAllowed (true); mScrollWindow->setInventoryAllowed (true); } updateVisible(); } void WindowManager::disallowAll() { mAllowed = GW_None; mRestAllowed = false; mBookWindow->setInventoryAllowed (false); mScrollWindow->setInventoryAllowed (false); updateVisible(); } void WindowManager::toggleVisible (GuiWindow wnd) { if (getMode() != GM_Inventory) return; std::string settingName; switch (wnd) { case GW_Inventory: settingName = "inventory"; break; case GW_Map: settingName = "map"; break; case GW_Magic: settingName = "spells"; break; case GW_Stats: settingName = "stats"; break; default: break; } if (!settingName.empty()) { settingName += " hidden"; bool hidden = Settings::Manager::getBool(settingName, "Windows"); Settings::Manager::setBool(settingName, "Windows", !hidden); } mShown = (GuiWindow)(mShown ^ wnd); updateVisible(); } void WindowManager::forceHide(GuiWindow wnd) { mForceHidden = (GuiWindow)(mForceHidden | wnd); updateVisible(); } void WindowManager::unsetForceHide(GuiWindow wnd) { mForceHidden = (GuiWindow)(mForceHidden & ~wnd); updateVisible(); } bool WindowManager::isGuiMode() const { return !mGuiModes.empty() || isConsoleMode() || (mMessageBoxManager && mMessageBoxManager->isInteractiveMessageBox()); } bool WindowManager::isConsoleMode() const { return mConsole && mConsole->isVisible(); } MWGui::GuiMode WindowManager::getMode() const { if (mGuiModes.empty()) return GM_None; return mGuiModes.back(); } void WindowManager::disallowMouse() { mInputBlocker->setVisible (true); } void WindowManager::allowMouse() { mInputBlocker->setVisible (!isGuiMode ()); } void WindowManager::notifyInputActionBound () { mSettingsWindow->updateControlsBox (); allowMouse(); } bool WindowManager::containsMode(GuiMode mode) const { if(mGuiModes.empty()) return false; return std::find(mGuiModes.begin(), mGuiModes.end(), mode) != mGuiModes.end(); } void WindowManager::showCrosshair (bool show) { if (mHud) mHud->setCrosshairVisible (show && mCrosshairEnabled); } void WindowManager::updateActivatedQuickKey () { mQuickKeysMenu->updateActivatedQuickKey(); } void WindowManager::activateQuickKey (int index) { mQuickKeysMenu->activateQuickKey(index); } /* Start of tes3mp addition Make it possible to add quickKeys from elsewhere in the code */ void WindowManager::setQuickKey(int slot, int quickKeyType, MWWorld::Ptr item, const std::string& spellId) { if (slot > 0) { // The actual indexes recorded for quick keys are always 1 higher than their // indexes in the mKey vector, so adjust for the latter mQuickKeysMenu->setSelectedIndex(slot - 1); switch (quickKeyType) { case QuickKeysMenu::Type_Unassigned: mQuickKeysMenu->unassignIndex(slot - 1); break; case QuickKeysMenu::Type_Item: mQuickKeysMenu->onAssignItem(item); break; case QuickKeysMenu::Type_MagicItem: mQuickKeysMenu->onAssignMagicItem(item); break; case QuickKeysMenu::Type_Magic: mQuickKeysMenu->onAssignMagic(spellId); break; } } } /* End of tes3mp addition */ bool WindowManager::getSubtitlesEnabled () { return mSubtitlesEnabled; } bool WindowManager::toggleHud() { mHudEnabled = !mHudEnabled; updateVisible(); return mHudEnabled; } bool WindowManager::getRestEnabled() { //Enable rest dialogue if character creation finished if(mRestAllowed==false && MWBase::Environment::get().getWorld()->getGlobalFloat ("chargenstate")==-1) mRestAllowed=true; return mRestAllowed; } bool WindowManager::getPlayerSleeping () { return mWaitDialog->getSleeping(); } void WindowManager::wakeUpPlayer() { mWaitDialog->wakeUp(); } void WindowManager::addVisitedLocation(const std::string& name, int x, int y) { mMap->addVisitedLocation (name, x, y); } const Translation::Storage& WindowManager::getTranslationDataStorage() const { return mTranslationDataStorage; } void WindowManager::changePointer(const std::string &name) { MyGUI::PointerManager::getInstance().setPointer(name); onCursorChange(name); } void WindowManager::showSoulgemDialog(MWWorld::Ptr item) { mSoulgemDialog->show(item); updateVisible(); } void WindowManager::updatePlayer() { mInventoryWindow->updatePlayer(); const MWWorld::Ptr player = MWMechanics::getPlayer(); if (player.getClass().getNpcStats(player).isWerewolf()) { setWerewolfOverlay(true); forceHide((GuiWindow)(MWGui::GW_Inventory | MWGui::GW_Magic)); } } // Remove this wrapper once onKeyFocusChanged call is rendered unnecessary void WindowManager::setKeyFocusWidget(MyGUI::Widget *widget) { MyGUI::InputManager::getInstance().setKeyFocusWidget(widget); onKeyFocusChanged(widget); } void WindowManager::onKeyFocusChanged(MyGUI::Widget *widget) { if (widget && widget->castType(false)) SDL_StartTextInput(); else SDL_StopTextInput(); } void WindowManager::setEnemy(const MWWorld::Ptr &enemy) { mHud->setEnemy(enemy); } int WindowManager::getMessagesCount() const { int count = 0; if (mMessageBoxManager) count = mMessageBoxManager->getMessagesCount(); return count; } Loading::Listener* WindowManager::getLoadingScreen() { return mLoadingScreen; } bool WindowManager::getCursorVisible() { return mCursorVisible && mCursorActive; } void WindowManager::trackWindow(Layout *layout, const std::string &name) { std::string settingName = name; MyGUI::IntSize viewSize = MyGUI::RenderManager::getInstance().getViewSize(); bool isMaximized = Settings::Manager::getBool(name + " maximized", "Windows"); if (isMaximized) settingName += " maximized"; MyGUI::IntPoint pos(static_cast(Settings::Manager::getFloat(settingName + " x", "Windows") * viewSize.width), static_cast(Settings::Manager::getFloat(settingName + " y", "Windows") * viewSize.height)); MyGUI::IntSize size (static_cast(Settings::Manager::getFloat(settingName + " w", "Windows") * viewSize.width), static_cast(Settings::Manager::getFloat(settingName + " h", "Windows") * viewSize.height)); layout->mMainWidget->setPosition(pos); layout->mMainWidget->setSize(size); MyGUI::Window* window = layout->mMainWidget->castType(); window->eventWindowChangeCoord += MyGUI::newDelegate(this, &WindowManager::onWindowChangeCoord); mTrackedWindows[window] = name; } void WindowManager::toggleMaximized(Layout *layout) { MyGUI::Window* window = layout->mMainWidget->castType(); std::string setting = mTrackedWindows[window]; if (setting.empty()) return; bool maximized = !Settings::Manager::getBool(setting + " maximized", "Windows"); if (maximized) setting += " maximized"; MyGUI::IntSize viewSize = MyGUI::RenderManager::getInstance().getViewSize(); float x = Settings::Manager::getFloat(setting + " x", "Windows") * float(viewSize.width); float y = Settings::Manager::getFloat(setting + " y", "Windows") * float(viewSize.height); float w = Settings::Manager::getFloat(setting + " w", "Windows") * float(viewSize.width); float h = Settings::Manager::getFloat(setting + " h", "Windows") * float(viewSize.height); window->setCoord(x, y, w, h); Settings::Manager::setBool(mTrackedWindows[window] + " maximized", "Windows", maximized); } void WindowManager::onWindowChangeCoord(MyGUI::Window *_sender) { std::string setting = mTrackedWindows[_sender]; MyGUI::IntSize viewSize = MyGUI::RenderManager::getInstance().getViewSize(); float x = _sender->getPosition().left / float(viewSize.width); float y = _sender->getPosition().top / float(viewSize.height); float w = _sender->getSize().width / float(viewSize.width); float h = _sender->getSize().height / float(viewSize.height); Settings::Manager::setFloat(setting + " x", "Windows", x); Settings::Manager::setFloat(setting + " y", "Windows", y); Settings::Manager::setFloat(setting + " w", "Windows", w); Settings::Manager::setFloat(setting + " h", "Windows", h); bool maximized = Settings::Manager::getBool(setting + " maximized", "Windows"); if (maximized) Settings::Manager::setBool(setting + " maximized", "Windows", false); } void WindowManager::clear() { mPlayerBounty = -1; for (WindowBase* window : mWindows) window->clear(); if (mLocalMapRender) mLocalMapRender->clear(); mMessageBoxManager->clear(); mToolTips->clear(); mSelectedSpell.clear(); mCustomMarkers.clear(); mForceHidden = GW_None; mRestAllowed = true; while (!mGuiModes.empty()) popGuiMode(); updateVisible(); } void WindowManager::write(ESM::ESMWriter &writer, Loading::Listener& progress) { mMap->write(writer, progress); mQuickKeysMenu->write(writer); if (!mSelectedSpell.empty()) { writer.startRecord(ESM::REC_ASPL); writer.writeHNString("ID__", mSelectedSpell); writer.endRecord(ESM::REC_ASPL); } for (CustomMarkerCollection::ContainerType::const_iterator it = mCustomMarkers.begin(); it != mCustomMarkers.end(); ++it) { writer.startRecord(ESM::REC_MARK); it->second.save(writer); writer.endRecord(ESM::REC_MARK); } } void WindowManager::readRecord(ESM::ESMReader &reader, uint32_t type) { if (type == ESM::REC_GMAP) mMap->readRecord(reader, type); else if (type == ESM::REC_KEYS) mQuickKeysMenu->readRecord(reader, type); else if (type == ESM::REC_ASPL) { reader.getSubNameIs("ID__"); std::string spell = reader.getHString(); if (mStore->get().search(spell)) mSelectedSpell = spell; } else if (type == ESM::REC_MARK) { ESM::CustomMarker marker; marker.load(reader); mCustomMarkers.addMarker(marker, false); } } int WindowManager::countSavedGameRecords() const { return 1 // Global map + 1 // QuickKeysMenu + mCustomMarkers.size() + (!mSelectedSpell.empty() ? 1 : 0); } bool WindowManager::isSavingAllowed() const { return !MyGUI::InputManager::getInstance().isModalAny() && !isConsoleMode() // TODO: remove this, once we have properly serialized the state of open windows && (!isGuiMode() || (mGuiModes.size() == 1 && (getMode() == GM_MainMenu || getMode() == GM_Rest))); } void WindowManager::playVideo(const std::string &name, bool allowSkipping) { mVideoWidget->playVideo("video\\" + name); mVideoWidget->eventKeyButtonPressed.clear(); mVideoBackground->eventKeyButtonPressed.clear(); if (allowSkipping) { mVideoWidget->eventKeyButtonPressed += MyGUI::newDelegate(this, &WindowManager::onVideoKeyPressed); mVideoBackground->eventKeyButtonPressed += MyGUI::newDelegate(this, &WindowManager::onVideoKeyPressed); } enableScene(false); MyGUI::IntSize screenSize = MyGUI::RenderManager::getInstance().getViewSize(); sizeVideo(screenSize.width, screenSize.height); MyGUI::Widget* oldKeyFocus = MyGUI::InputManager::getInstance().getKeyFocusWidget(); setKeyFocusWidget(mVideoWidget); mVideoBackground->setVisible(true); bool cursorWasVisible = mCursorVisible; setCursorVisible(false); if (mVideoWidget->hasAudioStream()) MWBase::Environment::get().getSoundManager()->pauseSounds(MWSound::VideoPlayback, ~MWSound::Type::Movie & MWSound::Type::Mask ); Misc::FrameRateLimiter frameRateLimiter = Misc::makeFrameRateLimiter(MWBase::Environment::get().getFrameRateLimit()); while (mVideoWidget->update() && !MWBase::Environment::get().getStateManager()->hasQuitRequest()) { const double dt = std::chrono::duration_cast>(frameRateLimiter.getLastFrameDuration()).count(); MWBase::Environment::get().getInputManager()->update(dt, true, false); if (!mWindowVisible) { mVideoWidget->pause(); std::this_thread::sleep_for(std::chrono::milliseconds(5)); } else { if (mVideoWidget->isPaused()) mVideoWidget->resume(); mViewer->eventTraversal(); mViewer->updateTraversal(); mViewer->renderingTraversals(); } // at the time this function is called we are in the middle of a frame, // so out of order calls are necessary to get a correct frameNumber for the next frame. // refer to the advance() and frame() order in Engine::go() mViewer->advance(mViewer->getFrameStamp()->getSimulationTime()); frameRateLimiter.limit(); } mVideoWidget->stop(); MWBase::Environment::get().getSoundManager()->resumeSounds(MWSound::VideoPlayback); setKeyFocusWidget(oldKeyFocus); setCursorVisible(cursorWasVisible); // Restore normal rendering updateVisible(); mVideoBackground->setVisible(false); } void WindowManager::sizeVideo(int screenWidth, int screenHeight) { // Use black bars to correct aspect ratio bool stretch = Settings::Manager::getBool("stretch menu background", "GUI"); mVideoBackground->setSize(screenWidth, screenHeight); mVideoWidget->autoResize(stretch); } void WindowManager::exitCurrentModal() { if (!mCurrentModals.empty()) { WindowModal* window = mCurrentModals.back(); if (!window->exit()) return; window->setVisible(false); } } void WindowManager::addCurrentModal(WindowModal *input) { if (mCurrentModals.empty()) mKeyboardNavigation->saveFocus(getMode()); mCurrentModals.push_back(input); mKeyboardNavigation->restoreFocus(-1); mKeyboardNavigation->setModalWindow(input->mMainWidget); mKeyboardNavigation->setDefaultFocus(input->mMainWidget, input->getDefaultKeyFocus()); } void WindowManager::removeCurrentModal(WindowModal* input) { if(!mCurrentModals.empty()) { if(input == mCurrentModals.back()) { mCurrentModals.pop_back(); mKeyboardNavigation->saveFocus(-1); } else { auto found = std::find(mCurrentModals.begin(), mCurrentModals.end(), input); if (found != mCurrentModals.end()) mCurrentModals.erase(found); else Log(Debug::Warning) << "Warning: can't find modal window " << input; } } if (mCurrentModals.empty()) { mKeyboardNavigation->setModalWindow(nullptr); mKeyboardNavigation->restoreFocus(getMode()); } else mKeyboardNavigation->setModalWindow(mCurrentModals.back()->mMainWidget); } void WindowManager::onVideoKeyPressed(MyGUI::Widget *_sender, MyGUI::KeyCode _key, MyGUI::Char _char) { if (_key == MyGUI::KeyCode::Escape) mVideoWidget->stop(); } void WindowManager::updatePinnedWindows() { mInventoryWindow->setPinned(Settings::Manager::getBool("inventory pin", "Windows")); if (Settings::Manager::getBool("inventory hidden", "Windows")) mShown = (GuiWindow)(mShown ^ GW_Inventory); mMap->setPinned(Settings::Manager::getBool("map pin", "Windows")); if (Settings::Manager::getBool("map hidden", "Windows")) mShown = (GuiWindow)(mShown ^ GW_Map); mSpellWindow->setPinned(Settings::Manager::getBool("spells pin", "Windows")); if (Settings::Manager::getBool("spells hidden", "Windows")) mShown = (GuiWindow)(mShown ^ GW_Magic); mStatsWindow->setPinned(Settings::Manager::getBool("stats pin", "Windows")); if (Settings::Manager::getBool("stats hidden", "Windows")) mShown = (GuiWindow)(mShown ^ GW_Stats); } void WindowManager::pinWindow(GuiWindow window) { switch (window) { case GW_Inventory: mInventoryWindow->setPinned(true); break; case GW_Map: mMap->setPinned(true); break; case GW_Magic: mSpellWindow->setPinned(true); break; case GW_Stats: mStatsWindow->setPinned(true); break; default: break; } updateVisible(); } void WindowManager::fadeScreenIn(const float time, bool clearQueue, float delay) { if (clearQueue) mScreenFader->clearQueue(); mScreenFader->fadeOut(time, delay); } void WindowManager::fadeScreenOut(const float time, bool clearQueue, float delay) { if (clearQueue) mScreenFader->clearQueue(); mScreenFader->fadeIn(time, delay); } void WindowManager::fadeScreenTo(const int percent, const float time, bool clearQueue, float delay) { if (clearQueue) mScreenFader->clearQueue(); mScreenFader->fadeTo(percent, time, delay); } void WindowManager::setBlindness(const int percent) { mBlindnessFader->notifyAlphaChanged(percent / 100.f); } void WindowManager::activateHitOverlay(bool interrupt) { if (!mHitFaderEnabled) return; if (!interrupt && !mHitFader->isEmpty()) return; mHitFader->clearQueue(); mHitFader->fadeTo(100, 0.0f); mHitFader->fadeTo(0, 0.5f); } void WindowManager::setWerewolfOverlay(bool set) { if (!mWerewolfOverlayEnabled) return; if (mWerewolfFader) mWerewolfFader->notifyAlphaChanged(set ? 1.0f : 0.0f); } void WindowManager::onClipboardChanged(const std::string &_type, const std::string &_data) { if (_type == "Text") SDL_SetClipboardText(MyGUI::TextIterator::getOnlyText(MyGUI::UString(_data)).asUTF8().c_str()); } void WindowManager::onClipboardRequested(const std::string &_type, std::string &_data) { if (_type != "Text") return; char* text=nullptr; text = SDL_GetClipboardText(); if (text) _data = MyGUI::TextIterator::toTagsString(text); SDL_free(text); } void WindowManager::toggleConsole() { bool visible = mConsole->isVisible(); if (!visible && !mGuiModes.empty()) mKeyboardNavigation->saveFocus(mGuiModes.back()); mConsole->setVisible(!visible); if (visible && !mGuiModes.empty()) mKeyboardNavigation->restoreFocus(mGuiModes.back()); updateVisible(); } void WindowManager::toggleDebugWindow() { #ifndef BT_NO_PROFILE mDebugWindow->setVisible(!mDebugWindow->isVisible()); #endif } void WindowManager::cycleSpell(bool next) { if (!isGuiMode()) mSpellWindow->cycle(next); } void WindowManager::cycleWeapon(bool next) { if (!isGuiMode()) mInventoryWindow->cycle(next); } void WindowManager::playSound(const std::string& soundId, float volume, float pitch) { if (soundId.empty()) return; MWBase::Environment::get().getSoundManager()->playSound(soundId, volume, pitch, MWSound::Type::Sfx, MWSound::PlayMode::NoEnv); } void WindowManager::updateSpellWindow() { if (mSpellWindow) mSpellWindow->updateSpells(); } void WindowManager::setConsoleSelectedObject(const MWWorld::Ptr &object) { mConsole->setSelectedObject(object); } /* Start of tes3mp addition Allow the direct setting of a console's Ptr, without the assumption that an object was clicked and that key focus should be restored to the console window, for console commands executed via server scripts */ void WindowManager::setConsolePtr(const MWWorld::Ptr &object) { mConsole->setPtr(object); } /* End of tes3mp addition */ /* Start of tes3mp addition Allow the clearing of the console's Ptr from elsewhere in the code, so that Ptrs used in console commands run from server scripts do not stay selected */ void WindowManager::clearConsolePtr() { mConsole->resetReference(); } /* End of tes3mp addition */ std::string WindowManager::correctIconPath(const std::string& path) { return Misc::ResourceHelpers::correctIconPath(path, mResourceSystem->getVFS()); } std::string WindowManager::correctBookartPath(const std::string& path, int width, int height, bool* exists) { std::string corrected = Misc::ResourceHelpers::correctBookartPath(path, width, height, mResourceSystem->getVFS()); if (exists) *exists = mResourceSystem->getVFS()->exists(corrected); return corrected; } std::string WindowManager::correctTexturePath(const std::string& path) { return Misc::ResourceHelpers::correctTexturePath(path, mResourceSystem->getVFS()); } bool WindowManager::textureExists(const std::string &path) { std::string corrected = Misc::ResourceHelpers::correctTexturePath(path, mResourceSystem->getVFS()); return mResourceSystem->getVFS()->exists(corrected); } void WindowManager::createCursors() { // FIXME: currently we do not scale cursor since it is not a MyGUI widget. // In theory, we can do it manually (rescale the cursor image via osg::Imag::scaleImage() and scale the hotspot position). // Unfortunately, this apploach can lead to driver crashes on some setups (e.g. on laptops with nvidia-prime on Linux). MyGUI::ResourceManager::EnumeratorPtr enumerator = MyGUI::ResourceManager::getInstance().getEnumerator(); while (enumerator.next()) { MyGUI::IResource* resource = enumerator.current().second; ResourceImageSetPointerFix* imgSetPointer = resource->castType(false); if (!imgSetPointer) continue; std::string tex_name = imgSetPointer->getImageSet()->getIndexInfo(0,0).texture; osg::ref_ptr image = mResourceSystem->getImageManager()->getImage(tex_name); if(image.valid()) { //everything looks good, send it to the cursor manager Uint8 hotspot_x = imgSetPointer->getHotSpot().left; Uint8 hotspot_y = imgSetPointer->getHotSpot().top; int rotation = imgSetPointer->getRotation(); mCursorManager->createCursor(imgSetPointer->getResourceName(), rotation, image, hotspot_x, hotspot_y); } } } void WindowManager::createTextures() { { MyGUI::ITexture* tex = MyGUI::RenderManager::getInstance().createTexture("white"); tex->createManual(8, 8, MyGUI::TextureUsage::Write, MyGUI::PixelFormat::R8G8B8); unsigned char* data = reinterpret_cast(tex->lock(MyGUI::TextureUsage::Write)); for (int x=0; x<8; ++x) for (int y=0; y<8; ++y) { *(data++) = 255; *(data++) = 255; *(data++) = 255; } tex->unlock(); } { MyGUI::ITexture* tex = MyGUI::RenderManager::getInstance().createTexture("black"); tex->createManual(8, 8, MyGUI::TextureUsage::Write, MyGUI::PixelFormat::R8G8B8); unsigned char* data = reinterpret_cast(tex->lock(MyGUI::TextureUsage::Write)); for (int x=0; x<8; ++x) for (int y=0; y<8; ++y) { *(data++) = 0; *(data++) = 0; *(data++) = 0; } tex->unlock(); } { MyGUI::ITexture* tex = MyGUI::RenderManager::getInstance().createTexture("transparent"); tex->createManual(8, 8, MyGUI::TextureUsage::Write, MyGUI::PixelFormat::R8G8B8A8); setMenuTransparency(Settings::Manager::getFloat("menu transparency", "GUI")); } } void WindowManager::setMenuTransparency(float value) { MyGUI::ITexture* tex = MyGUI::RenderManager::getInstance().getTexture("transparent"); unsigned char* data = reinterpret_cast(tex->lock(MyGUI::TextureUsage::Write)); for (int x=0; x<8; ++x) for (int y=0; y<8; ++y) { *(data++) = 255; *(data++) = 255; *(data++) = 255; *(data++) = static_cast(value*255); } tex->unlock(); } void WindowManager::addCell(MWWorld::CellStore* cell) { mLocalMapRender->addCell(cell); } void WindowManager::removeCell(MWWorld::CellStore *cell) { mLocalMapRender->removeCell(cell); } void WindowManager::writeFog(MWWorld::CellStore *cell) { mLocalMapRender->saveFogOfWar(cell); } const MWGui::TextColours& WindowManager::getTextColours() { return mTextColours; } bool WindowManager::injectKeyPress(MyGUI::KeyCode key, unsigned int text, bool repeat) { if (!mKeyboardNavigation->injectKeyPress(key, text, repeat)) { MyGUI::Widget* focus = MyGUI::InputManager::getInstance().getKeyFocusWidget(); bool widgetActive = MyGUI::InputManager::getInstance().injectKeyPress(key, text); if (!widgetActive || !focus) return false; // FIXME: MyGUI doesn't allow widgets to state if a given key was actually used, so make a guess if (focus->getTypeName().find("Button") != std::string::npos) { switch (key.getValue()) { case MyGUI::KeyCode::ArrowDown: case MyGUI::KeyCode::ArrowUp: case MyGUI::KeyCode::ArrowLeft: case MyGUI::KeyCode::ArrowRight: case MyGUI::KeyCode::Return: case MyGUI::KeyCode::NumpadEnter: case MyGUI::KeyCode::Space: return true; default: return false; } } return false; } else return true; } bool WindowManager::injectKeyRelease(MyGUI::KeyCode key) { return MyGUI::InputManager::getInstance().injectKeyRelease(key); } void WindowManager::GuiModeState::update(bool visible) { for (unsigned int i=0; isetVisible(visible); } void WindowManager::watchActor(const MWWorld::Ptr& ptr) { mStatsWatcher->watchActor(ptr); } MWWorld::Ptr WindowManager::getWatchedActor() const { return mStatsWatcher->getWatchedActor(); } } ================================================ FILE: apps/openmw/mwgui/windowmanagerimp.hpp ================================================ #ifndef MWGUI_WINDOWMANAGERIMP_H #define MWGUI_WINDOWMANAGERIMP_H /** This class owns and controls all the MW specific windows in the GUI. It can enable/disable Gui mode, and is responsible for sending and retrieving information from the Gui. **/ #include #include #include "../mwbase/windowmanager.hpp" #include #include #include #include "mapwindow.hpp" #include "statswatcher.hpp" #include "textcolours.hpp" #include #include namespace MyGUI { class Gui; class Widget; class Window; class UString; class ImageBox; } namespace MWWorld { class ESMStore; } namespace Compiler { class Extensions; } namespace Translation { class Storage; } namespace osg { class Group; } namespace osgViewer { class Viewer; } namespace Resource { class ResourceSystem; } namespace SceneUtil { class WorkQueue; } namespace SDLUtil { class SDLCursorManager; class VideoWrapper; } namespace osgMyGUI { class Platform; } namespace Gui { class FontLoader; } namespace MWRender { class LocalMap; } namespace MWGui { class WindowBase; class HUD; class MapWindow; class MainMenu; class StatsWindow; class InventoryWindow; struct JournalWindow; class CharacterCreation; class DragAndDrop; class ToolTips; class TextInputDialog; class InfoBoxDialog; class MessageBoxManager; class SettingsWindow; class AlchemyWindow; class QuickKeysMenu; class LoadingScreen; class LevelupDialog; class WaitDialog; class SpellCreationDialog; class EnchantingDialog; class TrainingWindow; class SpellIcons; class MerchantRepair; class SoulgemDialog; class Recharge; class CompanionWindow; class VideoWidget; class WindowModal; class ScreenFader; class DebugWindow; class JailScreen; class KeyboardNavigation; class WindowManager : public MWBase::WindowManager { public: typedef std::pair Faction; typedef std::vector FactionList; WindowManager(SDL_Window* window, osgViewer::Viewer* viewer, osg::Group* guiRoot, Resource::ResourceSystem* resourceSystem, SceneUtil::WorkQueue* workQueue, const std::string& logpath, const std::string& cacheDir, bool consoleOnlyScripts, Translation::Storage& translationDataStorage, ToUTF8::FromType encoding, bool exportFonts, const std::string& versionDescription, const std::string& localPath); virtual ~WindowManager(); /// Set the ESMStore to use for retrieving of GUI-related strings. void setStore (const MWWorld::ESMStore& store); void initUI(); void loadUserFonts() override; Loading::Listener* getLoadingScreen() override; /// @note This method will block until the video finishes playing /// (and will continually update the window while doing so) void playVideo(const std::string& name, bool allowSkipping) override; /// Warning: do not use MyGUI::InputManager::setKeyFocusWidget directly. Instead use this. void setKeyFocusWidget (MyGUI::Widget* widget) override; void setNewGame(bool newgame) override; void pushGuiMode(GuiMode mode, const MWWorld::Ptr& arg) override; void pushGuiMode (GuiMode mode) override; void popGuiMode(bool noSound=false) override; void removeGuiMode(GuiMode mode, bool noSound=false) override; ///< can be anywhere in the stack void goToJail(int days) override; GuiMode getMode() const override; bool containsMode(GuiMode mode) const override; bool isGuiMode() const override; bool isConsoleMode() const override; void toggleVisible(GuiWindow wnd) override; void forceHide(MWGui::GuiWindow wnd) override; void unsetForceHide(MWGui::GuiWindow wnd) override; /// Disallow all inventory mode windows void disallowAll() override; /// Allow one or more windows void allow(GuiWindow wnd) override; bool isAllowed(GuiWindow wnd) const override; /// \todo investigate, if we really need to expose every single lousy UI element to the outside world MWGui::InventoryWindow* getInventoryWindow() override; MWGui::CountDialog* getCountDialog() override; MWGui::ConfirmationDialog* getConfirmationDialog() override; MWGui::TradeWindow* getTradeWindow() override; /* Start of tes3mp addition Make it possible to get the ContainerWindow from elsewhere in the code */ virtual MWGui::ContainerWindow* getContainerWindow(); /* End of tes3mp addition */ /* Start of tes3mp addition Make it possible to get the DialogueWindow from elsewhere */ virtual MWGui::DialogueWindow* getDialogueWindow(); /* End of tes3mp addition */ /// Make the player use an item, while updating GUI state accordingly void useItem(const MWWorld::Ptr& item, bool bypassBeastRestrictions=false) override; void updateSpellWindow() override; void setConsoleSelectedObject(const MWWorld::Ptr& object) override; /* Start of tes3mp addition Allow the direct setting of a console's Ptr, without the assumption that an object was clicked and that key focus should be restored to the console window, for console commands executed via server scripts */ virtual void setConsolePtr(const MWWorld::Ptr& object); /* End of tes3mp addition */ /* Start of tes3mp addition Allow the clearing of the console's Ptr from elsewhere in the code, so that Ptrs used in console commands run from server scripts do not stay selected */ virtual void clearConsolePtr(); /* End of tes3mp addition */ /// Set time left for the player to start drowning (update the drowning bar) /// @param time time left to start drowning /// @param maxTime how long we can be underwater (in total) until drowning starts void setDrowningTimeLeft (float time, float maxTime) override; void changeCell(const MWWorld::CellStore* cell) override; ///< change the active cell /* Start of tes3mp addition Allow the setting of the image data for a global map tile from elsewhere in the code */ virtual void setGlobalMapImage(int cellX, int cellY, const std::vector& imageData); /* End of tes3mp addition */ void setFocusObject(const MWWorld::Ptr& focus) override; void setFocusObjectScreenCoords(float min_x, float min_y, float max_x, float max_y) override; void getMousePosition(int& x, int& y) override; void getMousePosition(float& x, float& y) override; void setDragDrop(bool dragDrop) override; /* Start of tes3mp addition Allow the completion of a drag and drop from elsewhere in the code */ virtual void finishDragDrop(); /* End of tes3mp addition */ bool getWorldMouseOver() override; float getScalingFactor() override; bool toggleFogOfWar() override; bool toggleFullHelp() override; ///< show extra info in item tooltips (owner, script) bool getFullHelp() const override; void setActiveMap(int x, int y, bool interior) override; ///< set the indices of the map texture that should be used /// sets the visibility of the drowning bar void setDrowningBarVisibility(bool visible) override; // sets the visibility of the hud health/magicka/stamina bars void setHMSVisibility(bool visible) override; // sets the visibility of the hud minimap void setMinimapVisibility(bool visible) override; void setWeaponVisibility(bool visible) override; void setSpellVisibility(bool visible) override; void setSneakVisibility(bool visible) override; /// activate selected quick key void activateQuickKey (int index) override; /// update activated quick key state (if action executing was delayed for some reason) void updateActivatedQuickKey () override; /* Start of tes3mp addition Make it possible to add quickKeys from elsewhere in the code */ virtual void setQuickKey(int slot, int quickKeyType, MWWorld::Ptr item, const std::string& spellId = ""); /* End of tes3mp addition */ std::string getSelectedSpell() override { return mSelectedSpell; } void setSelectedSpell(const std::string& spellId, int successChancePercent) override; void setSelectedEnchantItem(const MWWorld::Ptr& item) override; const MWWorld::Ptr& getSelectedEnchantItem() const override; void setSelectedWeapon(const MWWorld::Ptr& item) override; const MWWorld::Ptr& getSelectedWeapon() const override; int getFontHeight() const override; void unsetSelectedSpell() override; void unsetSelectedWeapon() override; void updateConsoleObjectPtr(const MWWorld::Ptr& currentPtr, const MWWorld::Ptr& newPtr) override; void showCrosshair(bool show) override; bool getSubtitlesEnabled() override; /// Turn visibility of HUD on or off bool toggleHud() override; void disallowMouse() override; void allowMouse() override; void notifyInputActionBound() override; void addVisitedLocation(const std::string& name, int x, int y) override; ///Hides dialog and schedules dialog to be deleted. void removeDialog(Layout* dialog) override; ///Gracefully attempts to exit the topmost GUI mode void exitCurrentGuiMode() override; void messageBox(const std::string & message, enum MWGui::ShowInDialogueMode showInDialogueMode = MWGui::ShowInDialogueMode_IfPossible) override; void staticMessageBox(const std::string& message) override; void removeStaticMessageBox() override; /* Start of tes3mp change (major) Add a hasServerOrigin boolean to the list of arguments so those messageboxes can be differentiated from client-only ones */ void interactiveMessageBox(const std::string& message, const std::vector& buttons = std::vector(), bool block = false, bool hasServerOrigin = false) override; /* End of tes3mp change (major) */ int readPressedButton () override; ///< returns the index of the pressed button or -1 if no button was pressed (->MessageBoxmanager->InteractiveMessageBox) void update (float duration) override; /** * Fetches a GMST string from the store, if there is no setting with the given * ID or it is not a string the default string is returned. * * @param id Identifier for the GMST setting, e.g. "aName" * @param default Default value if the GMST setting cannot be used. */ std::string getGameSettingString(const std::string &id, const std::string &default_) override; void processChangedSettings(const Settings::CategorySettingVector& changed) override; void windowVisibilityChange(bool visible) override; void windowResized(int x, int y) override; void windowClosed() override; bool isWindowVisible() override; void watchActor(const MWWorld::Ptr& ptr) override; MWWorld::Ptr getWatchedActor() const override; void executeInConsole (const std::string& path) override; /* Start of tes3mp addition Allow the execution of console commands from elsewhere in the code */ virtual void executeCommandInConsole(const std::string& command); /* End of tes3mp addition */ void enableRest() override { mRestAllowed = true; } bool getRestEnabled() override; bool getJournalAllowed() override { return (mAllowed & GW_Magic) != 0; } bool getPlayerSleeping() override; void wakeUpPlayer() override; void updatePlayer() override; void showSoulgemDialog (MWWorld::Ptr item) override; void changePointer (const std::string& name) override; void setEnemy (const MWWorld::Ptr& enemy) override; int getMessagesCount() const override; const Translation::Storage& getTranslationDataStorage() const override; void onSoulgemDialogButtonPressed (int button); bool getCursorVisible() override; /// Call when mouse cursor or buttons are used. void setCursorActive(bool active) override; /// Clear all savegame-specific data void clear() override; void write (ESM::ESMWriter& writer, Loading::Listener& progress) override; void readRecord (ESM::ESMReader& reader, uint32_t type) override; int countSavedGameRecords() const override; /// Does the current stack of GUI-windows permit saving? bool isSavingAllowed() const override; /// Send exit command to active Modal window **/ void exitCurrentModal() override; /// Sets the current Modal /** Used to send exit command to active Modal when Esc is pressed **/ void addCurrentModal(WindowModal* input) override; /// Removes the top Modal /** Used when one Modal adds another Modal \param input Pointer to the current modal, to ensure proper modal is removed **/ void removeCurrentModal(WindowModal* input) override; void pinWindow (MWGui::GuiWindow window) override; void toggleMaximized(Layout *layout) override; /// Fade the screen in, over \a time seconds void fadeScreenIn(const float time, bool clearQueue, float delay) override; /// Fade the screen out to black, over \a time seconds void fadeScreenOut(const float time, bool clearQueue, float delay) override; /// Fade the screen to a specified percentage of black, over \a time seconds void fadeScreenTo(const int percent, const float time, bool clearQueue, float delay) override; /// Darken the screen to a specified percentage void setBlindness(const int percent) override; void activateHitOverlay(bool interrupt) override; void setWerewolfOverlay(bool set) override; void toggleConsole() override; void toggleDebugWindow() override; /// Cycle to next or previous spell void cycleSpell(bool next) override; /// Cycle to next or previous weapon void cycleWeapon(bool next) override; void playSound(const std::string& soundId, float volume = 1.f, float pitch = 1.f) override; // In WindowManager for now since there isn't a VFS singleton std::string correctIconPath(const std::string& path) override; std::string correctBookartPath(const std::string& path, int width, int height, bool* exists = nullptr) override; std::string correctTexturePath(const std::string& path) override; bool textureExists(const std::string& path) override; void addCell(MWWorld::CellStore* cell) override; void removeCell(MWWorld::CellStore* cell) override; void writeFog(MWWorld::CellStore* cell) override; const MWGui::TextColours& getTextColours() override; bool injectKeyPress(MyGUI::KeyCode key, unsigned int text, bool repeat=false) override; bool injectKeyRelease(MyGUI::KeyCode key) override; private: unsigned int mOldUpdateMask; unsigned int mOldCullMask; const MWWorld::ESMStore* mStore; Resource::ResourceSystem* mResourceSystem; osg::ref_ptr mWorkQueue; osgMyGUI::Platform* mGuiPlatform; osgViewer::Viewer* mViewer; std::unique_ptr mFontLoader; std::unique_ptr mStatsWatcher; bool mConsoleOnlyScripts; std::map mTrackedWindows; void trackWindow(Layout* layout, const std::string& name); void onWindowChangeCoord(MyGUI::Window* _sender); std::string mSelectedSpell; MWWorld::Ptr mSelectedEnchantItem; MWWorld::Ptr mSelectedWeapon; std::vector mCurrentModals; // Markers placed manually by the player. Must be shared between both map views (the HUD map and the map window). CustomMarkerCollection mCustomMarkers; HUD *mHud; MapWindow *mMap; MWRender::LocalMap* mLocalMapRender; ToolTips *mToolTips; StatsWindow *mStatsWindow; MessageBoxManager *mMessageBoxManager; Console *mConsole; DialogueWindow *mDialogueWindow; DragAndDrop* mDragAndDrop; InventoryWindow *mInventoryWindow; ScrollWindow* mScrollWindow; BookWindow* mBookWindow; CountDialog* mCountDialog; TradeWindow* mTradeWindow; SettingsWindow* mSettingsWindow; ConfirmationDialog* mConfirmationDialog; SpellWindow* mSpellWindow; QuickKeysMenu* mQuickKeysMenu; LoadingScreen* mLoadingScreen; WaitDialog* mWaitDialog; SoulgemDialog* mSoulgemDialog; MyGUI::ImageBox* mVideoBackground; VideoWidget* mVideoWidget; ScreenFader* mWerewolfFader; ScreenFader* mBlindnessFader; ScreenFader* mHitFader; ScreenFader* mScreenFader; DebugWindow* mDebugWindow; JailScreen* mJailScreen; /* Start of tes3mp addition Keep a pointer to the container window because of its usefulness in multiplayer for container sync */ ContainerWindow* mContainerWindow; /* End of tes3mp addition */ std::vector mWindows; Translation::Storage& mTranslationDataStorage; CharacterCreation* mCharGen; MyGUI::Widget* mInputBlocker; bool mCrosshairEnabled; bool mSubtitlesEnabled; bool mHitFaderEnabled; bool mWerewolfOverlayEnabled; bool mHudEnabled; bool mCursorVisible; bool mCursorActive; int mPlayerBounty; void setCursorVisible(bool visible) override; MyGUI::Gui *mGui; // Gui struct GuiModeState { GuiModeState(WindowBase* window) { mWindows.push_back(window); } GuiModeState(const std::vector& windows) : mWindows(windows) {} GuiModeState() {} void update(bool visible); std::vector mWindows; std::string mCloseSound; std::string mOpenSound; }; // Defines the windows that should be shown in a particular GUI mode. std::map mGuiModeStates; // The currently active stack of GUI modes (top mode is the one we are in). std::vector mGuiModes; SDLUtil::SDLCursorManager* mCursorManager; std::vector mGarbageDialogs; void cleanupGarbage(); GuiWindow mShown; // Currently shown windows in inventory mode GuiWindow mForceHidden; // Hidden windows (overrides mShown) /* Currently ALLOWED windows in inventory mode. This is used at the start of the game, when windows are enabled one by one through script commands. You can manipulate this through using allow() and disableAll(). */ GuiWindow mAllowed; // is the rest window allowed? bool mRestAllowed; void updateVisible(); // Update visibility of all windows based on mode, shown and allowed settings void updateMap(); int mShowOwned; ToUTF8::FromType mEncoding; std::string mVersionDescription; bool mWindowVisible; MWGui::TextColours mTextColours; std::unique_ptr mKeyboardNavigation; SDLUtil::VideoWrapper* mVideoWrapper; float mScalingFactor; /** * Called when MyGUI tries to retrieve a tag's value. Tags must be denoted in #{tag} notation and will be replaced upon setting a user visible text/property. * Supported syntax: * #{GMSTName}: retrieves String value of the GMST called GMSTName * #{setting=CATEGORY_NAME,SETTING_NAME}: retrieves String value of SETTING_NAME under category CATEGORY_NAME from settings.cfg * #{sCell=CellID}: retrieves translated name of the given CellID (used only by some Morrowind localisations, in others cell ID is == cell name) * #{fontcolour=FontColourName}: retrieves the value of the fallback setting "FontColor_color_" from openmw.cfg, * in the format "r g b a", float values in range 0-1. Useful for "Colour" and "TextColour" properties in skins. * #{fontcolourhtml=FontColourName}: retrieves the value of the fallback setting "FontColor_color_" from openmw.cfg, * in the format "#xxxxxx" where x are hexadecimal numbers. Useful in an EditBox's caption to change the color of following text. */ void onRetrieveTag(const MyGUI::UString& _tag, MyGUI::UString& _result); void onCursorChange(const std::string& name); void onKeyFocusChanged(MyGUI::Widget* widget); // Key pressed while playing a video void onVideoKeyPressed(MyGUI::Widget *_sender, MyGUI::KeyCode _key, MyGUI::Char _char); void sizeVideo(int screenWidth, int screenHeight); void onClipboardChanged(const std::string& _type, const std::string& _data); void onClipboardRequested(const std::string& _type, std::string& _data); void createTextures(); void createCursors(); void setMenuTransparency(float value); void updatePinnedWindows(); void enableScene(bool enable); }; } #endif ================================================ FILE: apps/openmw/mwgui/windowpinnablebase.cpp ================================================ #include "windowpinnablebase.hpp" #include "exposedwindow.hpp" namespace MWGui { WindowPinnableBase::WindowPinnableBase(const std::string& parLayout) : WindowBase(parLayout), mPinned(false) { Window* window = mMainWidget->castType(); mPinButton = window->getSkinWidget ("Button"); mPinButton->eventMouseButtonPressed += MyGUI::newDelegate(this, &WindowPinnableBase::onPinButtonPressed); } void WindowPinnableBase::onPinButtonPressed(MyGUI::Widget* _sender, int left, int top, MyGUI::MouseButton id) { if (id != MyGUI::MouseButton::Left) return; mPinned = !mPinned; if (mPinned) mPinButton->changeWidgetSkin ("PinDown"); else mPinButton->changeWidgetSkin ("PinUp"); onPinToggled(); } void WindowPinnableBase::setPinned(bool pinned) { if (pinned != mPinned) onPinButtonPressed(mPinButton, 0, 0, MyGUI::MouseButton::Left); } void WindowPinnableBase::setPinButtonVisible(bool visible) { mPinButton->setVisible(visible); } } ================================================ FILE: apps/openmw/mwgui/windowpinnablebase.hpp ================================================ #ifndef MWGUI_WINDOW_PINNABLE_BASE_H #define MWGUI_WINDOW_PINNABLE_BASE_H #include "windowbase.hpp" namespace MWGui { class WindowPinnableBase: public WindowBase { public: WindowPinnableBase(const std::string& parLayout); bool pinned() { return mPinned; } void setPinned (bool pinned); void setPinButtonVisible(bool visible); private: void onPinButtonPressed(MyGUI::Widget* _sender, int left, int top, MyGUI::MouseButton id); protected: virtual void onPinToggled() = 0; MyGUI::Widget* mPinButton; bool mPinned; }; } #endif ================================================ FILE: apps/openmw/mwinput/actionmanager.cpp ================================================ #include "actionmanager.hpp" #include #include #include /* Start of tes3mp addition Include additional headers for multiplayer purposes */ #include "../mwmp/Main.hpp" #include "../mwmp/LocalPlayer.hpp" #include "../mwmp/GUIController.hpp" /* End of tes3mp addition */ #include "../mwbase/inputmanager.hpp" #include "../mwbase/statemanager.hpp" #include "../mwbase/environment.hpp" #include "../mwbase/mechanicsmanager.hpp" #include "../mwbase/windowmanager.hpp" #include "../mwbase/world.hpp" #include "../mwworld/player.hpp" #include "../mwworld/class.hpp" #include "../mwworld/inventorystore.hpp" #include "../mwworld/esmstore.hpp" #include "../mwmechanics/npcstats.hpp" #include "../mwmechanics/actorutil.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, osg::ref_ptr viewer, osg::ref_ptr screenCaptureHandler) : mBindingsManager(bindingsManager) , mViewer(viewer) , mScreenCaptureHandler(screenCaptureHandler) , mScreenCaptureOperation(screenCaptureOperation) , mAlwaysRunActive(Settings::Manager::getBool("always run", "Input")) , mSneaking(false) , mAttemptJump(false) , mOverencumberedMessageDelay(0.f) , mPreviewPOVDelay(0.f) , mTimeIdle(0.f) { } void ActionManager::update(float dt, bool triedToMove) { // Disable movement in Gui mode if (MWBase::Environment::get().getWindowManager()->isGuiMode() || MWBase::Environment::get().getStateManager()->getState() != MWBase::StateManager::State_Running) { mAttemptJump = false; return; } // Configure player movement according to keyboard input. Actual movement will // be done in the physics system. if (MWBase::Environment::get().getInputManager()->getControlSwitch("playercontrols")) { bool alwaysRunAllowed = false; MWWorld::Player& player = MWBase::Environment::get().getWorld()->getPlayer(); if (mBindingsManager->actionIsActive(A_MoveLeft) != mBindingsManager->actionIsActive(A_MoveRight)) { alwaysRunAllowed = true; triedToMove = true; player.setLeftRight(mBindingsManager->actionIsActive(A_MoveRight) ? 1 : -1); } if (mBindingsManager->actionIsActive(A_MoveForward) != mBindingsManager->actionIsActive(A_MoveBackward)) { alwaysRunAllowed = true; triedToMove = true; player.setAutoMove (false); player.setForwardBackward(mBindingsManager->actionIsActive(A_MoveForward) ? 1 : -1); } if (player.getAutoMove()) { alwaysRunAllowed = true; triedToMove = true; player.setForwardBackward (1); } if (mAttemptJump && MWBase::Environment::get().getInputManager()->getControlSwitch("playerjumping")) { 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) { MWBase::Environment::get().getWindowManager()->messageBox("#{sNotifyMessage59}"); mOverencumberedMessageDelay = 1.0; } } } 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 (triedToMove) MWBase::Environment::get().getInputManager()->resetIdleTime(); static const bool isToggleSneak = Settings::Manager::getBool("toggle sneak", "Input"); if (!isToggleSneak) { if(!MWBase::Environment::get().getInputManager()->joystickLastUsed()) player.setSneak(mBindingsManager->actionIsActive(A_Sneak)); } float xAxis = mBindingsManager->getActionValue(A_MoveLeftRight); float yAxis = mBindingsManager->getActionValue(A_MoveForwardBackward); bool isRunning = osg::Vec2f(xAxis * 2 - 1, yAxis * 2 - 1).length2() > 0.25f; if ((mAlwaysRunActive && alwaysRunAllowed) || isRunning) player.setRunState(!mBindingsManager->actionIsActive(A_Run)); else player.setRunState(mBindingsManager->actionIsActive(A_Run)); } if (mBindingsManager->actionIsActive(A_MoveForward) || mBindingsManager->actionIsActive(A_MoveBackward) || mBindingsManager->actionIsActive(A_MoveLeft) || mBindingsManager->actionIsActive(A_MoveRight) || mBindingsManager->actionIsActive(A_Jump) || mBindingsManager->actionIsActive(A_Sneak) || mBindingsManager->actionIsActive(A_TogglePOV) || mBindingsManager->actionIsActive(A_ZoomIn) || mBindingsManager->actionIsActive(A_ZoomOut)) { resetIdleTime(); } else { updateIdleTime(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() .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) { auto* inputManager = MWBase::Environment::get().getInputManager(); auto* windowManager = MWBase::Environment::get().getWindowManager(); // trigger action activated switch (action) { case A_GameMenu: toggleMainMenu (); break; case A_Screenshot: screenshot(); break; case A_Inventory: toggleInventory (); break; case A_Console: toggleConsole (); break; case A_Activate: inputManager->resetIdleTime(); activate(); break; case A_MoveLeft: case A_MoveRight: case A_MoveForward: case A_MoveBackward: handleGuiArrowKey(action); break; case A_Journal: toggleJournal(); break; case A_AutoMove: toggleAutoMove(); break; case A_AlwaysRun: toggleWalking(); break; case A_ToggleWeapon: toggleWeapon(); break; case A_Rest: rest(); break; case A_ToggleSpell: toggleSpell(); break; case A_QuickKey1: quickKey(1); break; case A_QuickKey2: quickKey(2); break; case A_QuickKey3: quickKey(3); break; case A_QuickKey4: quickKey(4); break; case A_QuickKey5: quickKey(5); break; case A_QuickKey6: quickKey(6); break; case A_QuickKey7: quickKey(7); break; case A_QuickKey8: quickKey(8); break; case A_QuickKey9: quickKey(9); break; case A_QuickKey10: quickKey(10); break; case A_QuickKeysMenu: showQuickKeysMenu(); break; case A_ToggleHUD: windowManager->toggleHud(); break; 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; case A_QuickLoad: quickLoad(); break; case A_CycleSpellLeft: if (checkAllowedToUseItems() && windowManager->isAllowed(MWGui::GW_Magic)) MWBase::Environment::get().getWindowManager()->cycleSpell(false); break; case A_CycleSpellRight: if (checkAllowedToUseItems() && windowManager->isAllowed(MWGui::GW_Magic)) MWBase::Environment::get().getWindowManager()->cycleSpell(true); break; case A_CycleWeaponLeft: if (checkAllowedToUseItems() && windowManager->isAllowed(MWGui::GW_Inventory)) MWBase::Environment::get().getWindowManager()->cycleWeapon(false); break; case A_CycleWeaponRight: if (checkAllowedToUseItems() && windowManager->isAllowed(MWGui::GW_Inventory)) MWBase::Environment::get().getWindowManager()->cycleWeapon(true); break; case A_Sneak: static const bool isToggleSneak = Settings::Manager::getBool("toggle sneak", "Input"); if (isToggleSneak) { toggleSneaking(); } break; } } bool ActionManager::checkAllowedToUseItems() const { MWWorld::Ptr player = MWMechanics::getPlayer(); if (player.getClass().getNpcStats(player).isWerewolf()) { // Cannot use items or spells while in werewolf form MWBase::Environment::get().getWindowManager()->messageBox("#{sWerewolfRefusal}"); return false; } return true; } void ActionManager::screenshot() { const std::string& settingStr = Settings::Manager::getString("screenshot type", "Video"); bool regularScreenshot = settingStr.size() == 0 || settingStr.compare("regular") == 0; if (regularScreenshot) { mScreenCaptureHandler->setFramesToCapture(1); mScreenCaptureHandler->captureNextFrame(*mViewer); } else { osg::ref_ptr screenshot (new osg::Image); if (MWBase::Environment::get().getWorld()->screenshot360(screenshot.get())) { (*mScreenCaptureOperation) (*(screenshot.get()), 0); // FIXME: mScreenCaptureHandler->getCaptureOperation() causes crash for some reason } } } void ActionManager::toggleMainMenu() { /* Start of tes3mp addition Don't allow the main menu to be toggled while TES3MP listboxes are open */ if (MWBase::Environment::get().getWindowManager()->getMode() == mwmp::GUIController::GM_TES3MP_ListBox) { return; } /* End of tes3mp addition */ if (MyGUI::InputManager::getInstance().isModalAny()) { MWBase::Environment::get().getWindowManager()->exitCurrentModal(); return; } if (MWBase::Environment::get().getWindowManager()->isConsoleMode()) { MWBase::Environment::get().getWindowManager()->toggleConsole(); return; } if (!MWBase::Environment::get().getWindowManager()->isGuiMode()) //No open GUIs, open up the MainMenu { MWBase::Environment::get().getWindowManager()->pushGuiMode (MWGui::GM_MainMenu); } else //Close current GUI { MWBase::Environment::get().getWindowManager()->exitCurrentGuiMode(); } } void ActionManager::toggleSpell() { if (MWBase::Environment::get().getWindowManager()->isGuiMode()) return; // Not allowed before the magic window is accessible if (!MWBase::Environment::get().getInputManager()->getControlSwitch("playermagic") || !MWBase::Environment::get().getInputManager()->getControlSwitch("playercontrols")) return; if (!checkAllowedToUseItems()) return; // Not allowed if no spell selected MWWorld::Player& player = MWBase::Environment::get().getWorld()->getPlayer(); MWWorld::InventoryStore& inventory = player.getPlayer().getClass().getInventoryStore(player.getPlayer()); if (MWBase::Environment::get().getWindowManager()->getSelectedSpell().empty() && inventory.getSelectedEnchantItem() == inventory.end()) return; if (MWBase::Environment::get().getMechanicsManager()->isAttackingOrSpell(player.getPlayer())) return; MWMechanics::DrawState_ state = player.getDrawState(); if (state == MWMechanics::DrawState_Weapon || state == MWMechanics::DrawState_Nothing) player.setDrawState(MWMechanics::DrawState_Spell); else player.setDrawState(MWMechanics::DrawState_Nothing); } void ActionManager::quickLoad() { if (!MyGUI::InputManager::getInstance().isModalAny()) MWBase::Environment::get().getStateManager()->quickLoad(); } void ActionManager::quickSave() { if (!MyGUI::InputManager::getInstance().isModalAny()) MWBase::Environment::get().getStateManager()->quickSave(); } void ActionManager::toggleWeapon() { if (MWBase::Environment::get().getWindowManager()->isGuiMode()) return; // Not allowed before the inventory window is accessible if (!MWBase::Environment::get().getInputManager()->getControlSwitch("playerfighting") || !MWBase::Environment::get().getInputManager()->getControlSwitch("playercontrols")) return; MWWorld::Player& player = MWBase::Environment::get().getWorld()->getPlayer(); // We want to interrupt animation only if attack is preparing, but still is not triggered // Otherwise we will get a "speedshooting" exploit, when player can skip reload animation by hitting "Toggle Weapon" key twice if (MWBase::Environment::get().getMechanicsManager()->isAttackPreparing(player.getPlayer())) player.setAttackingOrSpell(false); else if (MWBase::Environment::get().getMechanicsManager()->isAttackingOrSpell(player.getPlayer())) return; MWMechanics::DrawState_ state = player.getDrawState(); if (state == MWMechanics::DrawState_Spell || state == MWMechanics::DrawState_Nothing) player.setDrawState(MWMechanics::DrawState_Weapon); else player.setDrawState(MWMechanics::DrawState_Nothing); } void ActionManager::rest() { if (!MWBase::Environment::get().getInputManager()->getControlSwitch("playercontrols")) return; if (!MWBase::Environment::get().getWindowManager()->getRestEnabled() || MWBase::Environment::get().getWindowManager()->isGuiMode()) return; /* Start of tes3mp addition Ignore attempts to rest if the player has not logged in on the server yet Set LocalPlayer's isUsingBed to be able to distinguish bed use from regular rest menu use */ if (!mwmp::Main::get().getLocalPlayer()->isLoggedIn()) return; mwmp::Main::get().getLocalPlayer()->isUsingBed = false; /* End of tes3mp addition */ MWBase::Environment::get().getWindowManager()->pushGuiMode(MWGui::GM_Rest); //Open rest GUI } void ActionManager::toggleInventory() { if (!MWBase::Environment::get().getInputManager()->getControlSwitch("playercontrols")) return; if (MyGUI::InputManager::getInstance().isModalAny()) return; if (MWBase::Environment::get().getWindowManager()->isConsoleMode()) return; /* Start of tes3mp addition Ignore attempts to open inventory if the player has not logged in on the server yet */ if (!mwmp::Main::get().getLocalPlayer()->isLoggedIn()) return; /* End of tes3mp addition */ // Toggle between game mode and inventory mode if(!MWBase::Environment::get().getWindowManager()->isGuiMode()) MWBase::Environment::get().getWindowManager()->pushGuiMode(MWGui::GM_Inventory); else { MWGui::GuiMode mode = MWBase::Environment::get().getWindowManager()->getMode(); if(mode == MWGui::GM_Inventory || mode == MWGui::GM_Container) MWBase::Environment::get().getWindowManager()->popGuiMode(); } // .. but don't touch any other mode, except container. } void ActionManager::toggleConsole() { if (MyGUI::InputManager::getInstance().isModalAny()) return; /* Start of tes3mp addition If a player's console is disabled by the server, go no further */ if (!mwmp::Main::get().getLocalPlayer()->consoleAllowed) return; /* End of tes3mp addition */ MWBase::Environment::get().getWindowManager()->toggleConsole(); } void ActionManager::toggleJournal() { if (!MWBase::Environment::get().getInputManager()->getControlSwitch("playercontrols")) return; if (MyGUI::InputManager::getInstance ().isModalAny()) return; if (MWBase::Environment::get().getWindowManager()->getMode() != MWGui::GM_Journal && MWBase::Environment::get().getWindowManager()->getMode() != MWGui::GM_MainMenu && MWBase::Environment::get().getWindowManager()->getMode() != MWGui::GM_Settings && MWBase::Environment::get().getWindowManager ()->getJournalAllowed()) { MWBase::Environment::get().getWindowManager()->pushGuiMode(MWGui::GM_Journal); } else if (MWBase::Environment::get().getWindowManager()->containsMode(MWGui::GM_Journal)) { MWBase::Environment::get().getWindowManager()->removeGuiMode(MWGui::GM_Journal); } } void ActionManager::quickKey (int index) { if (!MWBase::Environment::get().getInputManager()->getControlSwitch("playercontrols") || !MWBase::Environment::get().getInputManager()->getControlSwitch("playerfighting") || !MWBase::Environment::get().getInputManager()->getControlSwitch("playermagic")) return; if (!checkAllowedToUseItems()) return; if (MWBase::Environment::get().getWorld()->getGlobalFloat ("chargenstate")!=-1) return; if (!MWBase::Environment::get().getWindowManager()->isGuiMode()) MWBase::Environment::get().getWindowManager()->activateQuickKey (index); } void ActionManager::showQuickKeysMenu() { if (!MWBase::Environment::get().getWindowManager()->isGuiMode () && MWBase::Environment::get().getWorld()->getGlobalFloat ("chargenstate")==-1) { if (!checkAllowedToUseItems()) return; MWBase::Environment::get().getWindowManager()->pushGuiMode (MWGui::GM_QuickKeysMenu); } else if (MWBase::Environment::get().getWindowManager()->getMode () == MWGui::GM_QuickKeysMenu) { while (MyGUI::InputManager::getInstance().isModalAny()) { //Handle any open Modal windows MWBase::Environment::get().getWindowManager()->exitCurrentModal(); } MWBase::Environment::get().getWindowManager()->exitCurrentGuiMode(); //And handle the actual main window } } void ActionManager::activate() { if (MWBase::Environment::get().getWindowManager()->isGuiMode()) { bool joystickUsed = MWBase::Environment::get().getInputManager()->joystickLastUsed(); if (!SDL_IsTextInputActive() && !mBindingsManager->isLeftOrRightButton(A_Activate, joystickUsed)) MWBase::Environment::get().getWindowManager()->injectKeyPress(MyGUI::KeyCode::Return, 0, false); } else if (MWBase::Environment::get().getInputManager()->getControlSwitch("playercontrols")) { MWWorld::Player& player = MWBase::Environment::get().getWorld()->getPlayer(); player.activate(); } } void ActionManager::toggleAutoMove() { if (MWBase::Environment::get().getWindowManager()->isGuiMode()) return; if (MWBase::Environment::get().getInputManager()->getControlSwitch("playercontrols")) { MWWorld::Player& player = MWBase::Environment::get().getWorld()->getPlayer(); player.setAutoMove (!player.getAutoMove()); } } void ActionManager::toggleWalking() { if (MWBase::Environment::get().getWindowManager()->isGuiMode() || SDL_IsTextInputActive()) return; mAlwaysRunActive = !mAlwaysRunActive; Settings::Manager::setBool("always run", "Input", mAlwaysRunActive); } void ActionManager::toggleSneaking() { if (MWBase::Environment::get().getWindowManager()->isGuiMode()) return; if (!MWBase::Environment::get().getInputManager()->getControlSwitch("playercontrols")) return; mSneaking = !mSneaking; MWWorld::Player& player = MWBase::Environment::get().getWorld()->getPlayer(); player.setSneak(mSneaking); } void ActionManager::handleGuiArrowKey(int action) { bool joystickUsed = MWBase::Environment::get().getInputManager()->joystickLastUsed(); // This is currently keyboard-specific code // TODO: see if GUI controls can be refactored into a single function if (joystickUsed) return; if (SDL_IsTextInputActive()) return; if (mBindingsManager->isLeftOrRightButton(action, joystickUsed)) return; MyGUI::KeyCode key; switch (action) { case A_MoveLeft: key = MyGUI::KeyCode::ArrowLeft; break; case A_MoveRight: key = MyGUI::KeyCode::ArrowRight; break; case A_MoveForward: key = MyGUI::KeyCode::ArrowUp; break; case A_MoveBackward: default: key = MyGUI::KeyCode::ArrowDown; break; } MWBase::Environment::get().getWindowManager()->injectKeyPress(key, 0, false); } } ================================================ FILE: apps/openmw/mwinput/actionmanager.hpp ================================================ #ifndef MWINPUT_ACTIONMANAGER_H #define MWINPUT_ACTIONMANAGER_H #include #include namespace osgViewer { class Viewer; class ScreenCaptureHandler; } namespace MWInput { class BindingsManager; class ActionManager { public: ActionManager(BindingsManager* bindingsManager, osgViewer::ScreenCaptureHandler::CaptureOperation* screenCaptureOperation, osg::ref_ptr viewer, osg::ref_ptr screenCaptureHandler); void update(float dt, bool triedToMove); void executeAction(int action); bool checkAllowedToUseItems() const; void toggleMainMenu(); void toggleSpell(); void toggleWeapon(); void toggleInventory(); void toggleConsole(); void screenshot(); void toggleJournal(); void activate(); void toggleWalking(); void toggleSneaking(); void toggleAutoMove(); void rest(); void quickLoad(); void quickSave(); void quickKey (int index); void showQuickKeysMenu(); void resetIdleTime(); bool isAlwaysRunActive() const { return mAlwaysRunActive; }; bool isSneaking() const { return mSneaking; }; void setAttemptJump(bool enabled) { mAttemptJump = enabled; } bool isPreviewModeEnabled(); private: void handleGuiArrowKey(int action); void updateIdleTime(float dt); BindingsManager* mBindingsManager; osg::ref_ptr mViewer; osg::ref_ptr mScreenCaptureHandler; osgViewer::ScreenCaptureHandler::CaptureOperation* mScreenCaptureOperation; bool mAlwaysRunActive; bool mSneaking; bool mAttemptJump; float mOverencumberedMessageDelay; float mPreviewPOVDelay; float mTimeIdle; }; } #endif ================================================ FILE: apps/openmw/mwinput/actions.hpp ================================================ #ifndef MWINPUT_ACTIONS_H #define MWINPUT_ACTIONS_H namespace MWInput { enum Actions { // please add new actions at the bottom, in order to preserve the channel IDs in the key configuration files A_GameMenu, A_Unused, A_Screenshot, // Take a screenshot A_Inventory, // Toggle inventory screen A_Console, // Toggle console screen A_MoveLeft, // Move player left / right A_MoveRight, A_MoveForward, // Forward / Backward A_MoveBackward, A_Activate, A_Use, //Use weapon, spell, etc. A_Jump, A_AutoMove, //Toggle Auto-move forward A_Rest, //Rest A_Journal, //Journal A_Weapon, //Draw/Sheath weapon A_Spell, //Ready/Unready Casting A_Run, //Run when held A_CycleSpellLeft, //cycling through spells A_CycleSpellRight, A_CycleWeaponLeft, //Cycling through weapons A_CycleWeaponRight, A_ToggleSneak, //Toggles Sneak A_AlwaysRun, //Toggle Walking/Running A_Sneak, A_QuickSave, A_QuickLoad, A_QuickMenu, A_ToggleWeapon, A_ToggleSpell, A_TogglePOV, A_QuickKey1, A_QuickKey2, A_QuickKey3, A_QuickKey4, A_QuickKey5, A_QuickKey6, A_QuickKey7, A_QuickKey8, A_QuickKey9, A_QuickKey10, A_QuickKeysMenu, A_ToggleHUD, A_ToggleDebug, A_LookUpDown, //Joystick look A_LookLeftRight, A_MoveForwardBackward, A_MoveLeftRight, A_ZoomIn, A_ZoomOut, A_Last // Marker for the last item }; } #endif ================================================ FILE: apps/openmw/mwinput/bindingsmanager.cpp ================================================ #include "bindingsmanager.hpp" #include #include #include /* Start of tes3mp addition Include additional headers for multiplayer purposes */ #include "../mwmp/Main.hpp" #include "../mwmp/GUIController.hpp" /* End of tes3mp addition */ #include "../mwbase/environment.hpp" #include "../mwbase/inputmanager.hpp" #include "../mwbase/windowmanager.hpp" #include "../mwbase/world.hpp" #include "../mwworld/player.hpp" #include "actions.hpp" #include "sdlmappings.hpp" namespace MWInput { static const int sFakeDeviceId = 1; //As we only support one controller at a time, use a fake deviceID so we don't lose bindings when switching controllers void clearAllKeyBindings(ICS::InputControlSystem* inputBinder, ICS::Control* control) { // right now we don't really need multiple bindings for the same action, so remove all others first if (inputBinder->getKeyBinding(control, ICS::Control::INCREASE) != SDL_SCANCODE_UNKNOWN) inputBinder->removeKeyBinding(inputBinder->getKeyBinding(control, ICS::Control::INCREASE)); if (inputBinder->getMouseButtonBinding(control, ICS::Control::INCREASE) != ICS_MAX_DEVICE_BUTTONS) inputBinder->removeMouseButtonBinding(inputBinder->getMouseButtonBinding(control, ICS::Control::INCREASE)); if (inputBinder->getMouseWheelBinding(control, ICS::Control::INCREASE) != ICS::InputControlSystem::MouseWheelClick::UNASSIGNED) inputBinder->removeMouseWheelBinding(inputBinder->getMouseWheelBinding(control, ICS::Control::INCREASE)); } void clearAllControllerBindings(ICS::InputControlSystem* inputBinder, ICS::Control* control) { // right now we don't really need multiple bindings for the same action, so remove all others first if (inputBinder->getJoystickAxisBinding(control, sFakeDeviceId, ICS::Control::INCREASE) != SDL_SCANCODE_UNKNOWN) inputBinder->removeJoystickAxisBinding(sFakeDeviceId, inputBinder->getJoystickAxisBinding(control, sFakeDeviceId, ICS::Control::INCREASE)); if (inputBinder->getJoystickButtonBinding(control, sFakeDeviceId, ICS::Control::INCREASE) != ICS_MAX_DEVICE_BUTTONS) inputBinder->removeJoystickButtonBinding(sFakeDeviceId, inputBinder->getJoystickButtonBinding(control, sFakeDeviceId, ICS::Control::INCREASE)); } class InputControlSystem : public ICS::InputControlSystem { public: InputControlSystem(const std::string& bindingsFile) : ICS::InputControlSystem(bindingsFile, true, nullptr, nullptr, A_Last) { } }; class BindingsListener : public ICS::ChannelListener, public ICS::DetectingBindingListener { public: BindingsListener(ICS::InputControlSystem* inputBinder, BindingsManager* bindingsManager) : mInputBinder(inputBinder) , mBindingsManager(bindingsManager) , mDetectingKeyboard(false) { } virtual ~BindingsListener() = default; void channelChanged(ICS::Channel* channel, float currentValue, float previousValue) override { int action = channel->getNumber(); mBindingsManager->actionValueChanged(action, currentValue, previousValue); } void keyBindingDetected(ICS::InputControlSystem* ICS, ICS::Control* control , SDL_Scancode key, ICS::Control::ControlChangingDirection direction) override { //Disallow binding escape key if (key==SDL_SCANCODE_ESCAPE) { //Stop binding if esc pressed mInputBinder->cancelDetectingBindingState(); MWBase::Environment::get().getWindowManager()->notifyInputActionBound(); return; } // Disallow binding reserved keys if (key == SDL_SCANCODE_F3 || key == SDL_SCANCODE_F4 || key == SDL_SCANCODE_F10) return; #ifndef __APPLE__ // Disallow binding Windows/Meta keys if (key == SDL_SCANCODE_LGUI || key == SDL_SCANCODE_RGUI) return; #endif if (!mDetectingKeyboard) return; clearAllKeyBindings(mInputBinder, control); control->setInitialValue(0.0f); ICS::DetectingBindingListener::keyBindingDetected(ICS, control, key, direction); MWBase::Environment::get().getWindowManager()->notifyInputActionBound(); } void mouseAxisBindingDetected(ICS::InputControlSystem* ICS, ICS::Control* control , ICS::InputControlSystem::NamedAxis axis, ICS::Control::ControlChangingDirection direction) override { // we don't want mouse movement bindings return; } void mouseButtonBindingDetected(ICS::InputControlSystem* ICS, ICS::Control* control , unsigned int button, ICS::Control::ControlChangingDirection direction) override { if (!mDetectingKeyboard) return; clearAllKeyBindings(mInputBinder, control); control->setInitialValue(0.0f); ICS::DetectingBindingListener::mouseButtonBindingDetected(ICS, control, button, direction); MWBase::Environment::get().getWindowManager()->notifyInputActionBound(); } void mouseWheelBindingDetected(ICS::InputControlSystem* ICS, ICS::Control* control , ICS::InputControlSystem::MouseWheelClick click, ICS::Control::ControlChangingDirection direction) override { if (!mDetectingKeyboard) return; clearAllKeyBindings(mInputBinder, control); control->setInitialValue(0.0f); ICS::DetectingBindingListener::mouseWheelBindingDetected(ICS, control, click, direction); MWBase::Environment::get().getWindowManager()->notifyInputActionBound(); } void joystickAxisBindingDetected(ICS::InputControlSystem* ICS, int deviceID, ICS::Control* control , int axis, ICS::Control::ControlChangingDirection direction) override { //only allow binding to the trigers if (axis != SDL_CONTROLLER_AXIS_TRIGGERLEFT && axis != SDL_CONTROLLER_AXIS_TRIGGERRIGHT) return; if (mDetectingKeyboard) return; clearAllControllerBindings(mInputBinder, control); control->setValue(0.5f); //axis bindings must start at 0.5 control->setInitialValue(0.5f); ICS::DetectingBindingListener::joystickAxisBindingDetected(ICS, deviceID, control, axis, direction); MWBase::Environment::get().getWindowManager()->notifyInputActionBound(); } void joystickButtonBindingDetected(ICS::InputControlSystem* ICS, int deviceID, ICS::Control* control , unsigned int button, ICS::Control::ControlChangingDirection direction) override { if (mDetectingKeyboard) return; clearAllControllerBindings(mInputBinder,control); control->setInitialValue(0.0f); ICS::DetectingBindingListener::joystickButtonBindingDetected (ICS, deviceID, control, button, direction); MWBase::Environment::get().getWindowManager()->notifyInputActionBound(); } void setDetectingKeyboard(bool detecting) { mDetectingKeyboard = detecting; } private: ICS::InputControlSystem* mInputBinder; BindingsManager* mBindingsManager; bool mDetectingKeyboard; }; BindingsManager::BindingsManager(const std::string& userFile, bool userFileExists) : mUserFile(userFile) , mDragDrop(false) { std::string file = userFileExists ? userFile : ""; mInputBinder = std::make_unique(file); mListener = std::make_unique(mInputBinder.get(), this); mInputBinder->setDetectingBindingListener(mListener.get()); loadKeyDefaults(); loadControllerDefaults(); for (int i = 0; i < A_Last; ++i) { mInputBinder->getChannel(i)->addListener(mListener.get()); } } void BindingsManager::setDragDrop(bool dragDrop) { mDragDrop = dragDrop; } BindingsManager::~BindingsManager() { mInputBinder->save(mUserFile); } void BindingsManager::update(float dt) { // update values of channels (as a result of pressed keys) mInputBinder->update(dt); } bool BindingsManager::isLeftOrRightButton(int action, bool joystick) const { int mouseBinding = mInputBinder->getMouseButtonBinding(mInputBinder->getControl(action), ICS::Control::INCREASE); if (mouseBinding != ICS_MAX_DEVICE_BUTTONS) return true; int buttonBinding = mInputBinder->getJoystickButtonBinding(mInputBinder->getControl(action), sFakeDeviceId, ICS::Control::INCREASE); if (joystick && (buttonBinding == 0 || buttonBinding == 1)) return true; return false; } void BindingsManager::setPlayerControlsEnabled(bool enabled) { int playerChannels[] = {A_AutoMove, A_AlwaysRun, A_ToggleWeapon, A_ToggleSpell, A_Rest, A_QuickKey1, A_QuickKey2, A_QuickKey3, A_QuickKey4, A_QuickKey5, A_QuickKey6, A_QuickKey7, A_QuickKey8, A_QuickKey9, A_QuickKey10, A_Use, A_Journal}; for(int pc : playerChannels) { mInputBinder->getChannel(pc)->setEnabled(enabled); } } void BindingsManager::setJoystickDeadZone(float deadZone) { mInputBinder->setJoystickDeadZone(deadZone); } float BindingsManager::getActionValue (int id) const { return mInputBinder->getChannel(id)->getValue(); } bool BindingsManager::actionIsActive (int id) const { return getActionValue(id) == 1.0; } void BindingsManager::loadKeyDefaults (bool force) { // using hardcoded key defaults is inevitable, if we want the configuration files to stay valid // across different versions of OpenMW (in the case where another input action is added) std::map defaultKeyBindings; //Gets the Keyvalue from the Scancode; gives the button in the same place reguardless of keyboard format defaultKeyBindings[A_Activate] = SDL_SCANCODE_SPACE; defaultKeyBindings[A_MoveBackward] = SDL_SCANCODE_S; defaultKeyBindings[A_MoveForward] = SDL_SCANCODE_W; defaultKeyBindings[A_MoveLeft] = SDL_SCANCODE_A; defaultKeyBindings[A_MoveRight] = SDL_SCANCODE_D; defaultKeyBindings[A_ToggleWeapon] = SDL_SCANCODE_F; defaultKeyBindings[A_ToggleSpell] = SDL_SCANCODE_R; defaultKeyBindings[A_CycleSpellLeft] = SDL_SCANCODE_MINUS; defaultKeyBindings[A_CycleSpellRight] = SDL_SCANCODE_EQUALS; defaultKeyBindings[A_CycleWeaponLeft] = SDL_SCANCODE_LEFTBRACKET; defaultKeyBindings[A_CycleWeaponRight] = SDL_SCANCODE_RIGHTBRACKET; defaultKeyBindings[A_QuickKeysMenu] = SDL_SCANCODE_F1; defaultKeyBindings[A_Console] = SDL_SCANCODE_GRAVE; defaultKeyBindings[A_Run] = SDL_SCANCODE_LSHIFT; defaultKeyBindings[A_Sneak] = SDL_SCANCODE_LCTRL; defaultKeyBindings[A_AutoMove] = SDL_SCANCODE_Q; defaultKeyBindings[A_Jump] = SDL_SCANCODE_E; defaultKeyBindings[A_Journal] = SDL_SCANCODE_J; defaultKeyBindings[A_Rest] = SDL_SCANCODE_T; defaultKeyBindings[A_GameMenu] = SDL_SCANCODE_ESCAPE; defaultKeyBindings[A_TogglePOV] = SDL_SCANCODE_TAB; defaultKeyBindings[A_QuickKey1] = SDL_SCANCODE_1; defaultKeyBindings[A_QuickKey2] = SDL_SCANCODE_2; defaultKeyBindings[A_QuickKey3] = SDL_SCANCODE_3; defaultKeyBindings[A_QuickKey4] = SDL_SCANCODE_4; defaultKeyBindings[A_QuickKey5] = SDL_SCANCODE_5; defaultKeyBindings[A_QuickKey6] = SDL_SCANCODE_6; defaultKeyBindings[A_QuickKey7] = SDL_SCANCODE_7; defaultKeyBindings[A_QuickKey8] = SDL_SCANCODE_8; defaultKeyBindings[A_QuickKey9] = SDL_SCANCODE_9; defaultKeyBindings[A_QuickKey10] = SDL_SCANCODE_0; defaultKeyBindings[A_Screenshot] = SDL_SCANCODE_F12; defaultKeyBindings[A_ToggleHUD] = SDL_SCANCODE_F11; defaultKeyBindings[A_ToggleDebug] = SDL_SCANCODE_F10; defaultKeyBindings[A_AlwaysRun] = SDL_SCANCODE_CAPSLOCK; defaultKeyBindings[A_QuickSave] = SDL_SCANCODE_F5; defaultKeyBindings[A_QuickLoad] = SDL_SCANCODE_F9; std::map defaultMouseButtonBindings; defaultMouseButtonBindings[A_Inventory] = SDL_BUTTON_RIGHT; defaultMouseButtonBindings[A_Use] = SDL_BUTTON_LEFT; std::map defaultMouseWheelBindings; defaultMouseWheelBindings[A_ZoomIn] = ICS::InputControlSystem::MouseWheelClick::UP; defaultMouseWheelBindings[A_ZoomOut] = ICS::InputControlSystem::MouseWheelClick::DOWN; for (int i = 0; i < A_Last; ++i) { ICS::Control* control; bool controlExists = mInputBinder->getChannel(i)->getControlsCount() != 0; if (!controlExists) { control = new ICS::Control(std::to_string(i), false, true, 0, ICS::ICS_MAX, ICS::ICS_MAX); mInputBinder->addControl(control); control->attachChannel(mInputBinder->getChannel(i), ICS::Channel::DIRECT); } else { control = mInputBinder->getChannel(i)->getAttachedControls().front().control; } if (!controlExists || force || (mInputBinder->getKeyBinding(control, ICS::Control::INCREASE) == SDL_SCANCODE_UNKNOWN && mInputBinder->getMouseButtonBinding(control, ICS::Control::INCREASE) == ICS_MAX_DEVICE_BUTTONS && mInputBinder->getMouseWheelBinding(control, ICS::Control::INCREASE) == ICS::InputControlSystem::MouseWheelClick::UNASSIGNED)) { clearAllKeyBindings(mInputBinder.get(), control); if (defaultKeyBindings.find(i) != defaultKeyBindings.end() && (force || !mInputBinder->isKeyBound(defaultKeyBindings[i]))) { control->setInitialValue(0.0f); mInputBinder->addKeyBinding(control, defaultKeyBindings[i], ICS::Control::INCREASE); } else if (defaultMouseButtonBindings.find(i) != defaultMouseButtonBindings.end() && (force || !mInputBinder->isMouseButtonBound(defaultMouseButtonBindings[i]))) { control->setInitialValue(0.0f); mInputBinder->addMouseButtonBinding(control, defaultMouseButtonBindings[i], ICS::Control::INCREASE); } else if (defaultMouseWheelBindings.find(i) != defaultMouseWheelBindings.end() && (force || !mInputBinder->isMouseWheelBound(defaultMouseWheelBindings[i]))) { control->setInitialValue(0.f); mInputBinder->addMouseWheelBinding(control, defaultMouseWheelBindings[i], ICS::Control::INCREASE); } if (i == A_LookLeftRight && !mInputBinder->isKeyBound(SDL_SCANCODE_KP_4) && !mInputBinder->isKeyBound(SDL_SCANCODE_KP_6)) { mInputBinder->addKeyBinding(control, SDL_SCANCODE_KP_6, ICS::Control::INCREASE); mInputBinder->addKeyBinding(control, SDL_SCANCODE_KP_4, ICS::Control::DECREASE); } if (i == A_LookUpDown && !mInputBinder->isKeyBound(SDL_SCANCODE_KP_8) && !mInputBinder->isKeyBound(SDL_SCANCODE_KP_2)) { mInputBinder->addKeyBinding(control, SDL_SCANCODE_KP_2, ICS::Control::INCREASE); mInputBinder->addKeyBinding(control, SDL_SCANCODE_KP_8, ICS::Control::DECREASE); } } } } void BindingsManager::loadControllerDefaults(bool force) { // using hardcoded key defaults is inevitable, if we want the configuration files to stay valid // across different versions of OpenMW (in the case where another input action is added) std::map defaultButtonBindings; defaultButtonBindings[A_Activate] = SDL_CONTROLLER_BUTTON_A; defaultButtonBindings[A_ToggleWeapon] = SDL_CONTROLLER_BUTTON_X; defaultButtonBindings[A_ToggleSpell] = SDL_CONTROLLER_BUTTON_Y; //defaultButtonBindings[A_QuickButtonsMenu] = SDL_GetButtonFromScancode(SDL_SCANCODE_F1); // Need to implement, should be ToggleSpell(5) AND Wait(9) defaultButtonBindings[A_Sneak] = SDL_CONTROLLER_BUTTON_LEFTSTICK; defaultButtonBindings[A_Journal] = SDL_CONTROLLER_BUTTON_LEFTSHOULDER; defaultButtonBindings[A_Rest] = SDL_CONTROLLER_BUTTON_RIGHTSHOULDER; defaultButtonBindings[A_TogglePOV] = SDL_CONTROLLER_BUTTON_RIGHTSTICK; defaultButtonBindings[A_Inventory] = SDL_CONTROLLER_BUTTON_B; defaultButtonBindings[A_GameMenu] = SDL_CONTROLLER_BUTTON_START; defaultButtonBindings[A_QuickSave] = SDL_CONTROLLER_BUTTON_GUIDE; defaultButtonBindings[A_MoveForward] = SDL_CONTROLLER_BUTTON_DPAD_UP; defaultButtonBindings[A_MoveLeft] = SDL_CONTROLLER_BUTTON_DPAD_LEFT; defaultButtonBindings[A_MoveBackward] = SDL_CONTROLLER_BUTTON_DPAD_DOWN; defaultButtonBindings[A_MoveRight] = SDL_CONTROLLER_BUTTON_DPAD_RIGHT; std::map defaultAxisBindings; defaultAxisBindings[A_MoveForwardBackward] = SDL_CONTROLLER_AXIS_LEFTY; defaultAxisBindings[A_MoveLeftRight] = SDL_CONTROLLER_AXIS_LEFTX; defaultAxisBindings[A_LookUpDown] = SDL_CONTROLLER_AXIS_RIGHTY; defaultAxisBindings[A_LookLeftRight] = SDL_CONTROLLER_AXIS_RIGHTX; defaultAxisBindings[A_Use] = SDL_CONTROLLER_AXIS_TRIGGERRIGHT; defaultAxisBindings[A_Jump] = SDL_CONTROLLER_AXIS_TRIGGERLEFT; for (int i = 0; i < A_Last; i++) { ICS::Control* control; bool controlExists = mInputBinder->getChannel(i)->getControlsCount() != 0; if (!controlExists) { float initial; if (defaultAxisBindings.find(i) == defaultAxisBindings.end()) initial = 0.0f; else initial = 0.5f; control = new ICS::Control(std::to_string(i), false, true, initial, ICS::ICS_MAX, ICS::ICS_MAX); mInputBinder->addControl(control); control->attachChannel(mInputBinder->getChannel(i), ICS::Channel::DIRECT); } else { control = mInputBinder->getChannel(i)->getAttachedControls().front().control; } if (!controlExists || force || (mInputBinder->getJoystickAxisBinding(control, sFakeDeviceId, ICS::Control::INCREASE) == ICS::InputControlSystem::UNASSIGNED && mInputBinder->getJoystickButtonBinding(control, sFakeDeviceId, ICS::Control::INCREASE) == ICS_MAX_DEVICE_BUTTONS)) { clearAllControllerBindings(mInputBinder.get(), control); if (defaultButtonBindings.find(i) != defaultButtonBindings.end() && (force || !mInputBinder->isJoystickButtonBound(sFakeDeviceId, defaultButtonBindings[i]))) { control->setInitialValue(0.0f); mInputBinder->addJoystickButtonBinding(control, sFakeDeviceId, defaultButtonBindings[i], ICS::Control::INCREASE); } else if (defaultAxisBindings.find(i) != defaultAxisBindings.end() && (force || !mInputBinder->isJoystickAxisBound(sFakeDeviceId, defaultAxisBindings[i]))) { control->setValue(0.5f); control->setInitialValue(0.5f); mInputBinder->addJoystickAxisBinding(control, sFakeDeviceId, defaultAxisBindings[i], ICS::Control::INCREASE); } } } } std::string BindingsManager::getActionDescription(int action) { switch (action) { case A_Screenshot: return "Screenshot"; case A_ZoomIn: return "Zoom In"; case A_ZoomOut: return "Zoom Out"; case A_ToggleHUD: return "Toggle HUD"; case A_Use: return "#{sUse}"; case A_Activate: return "#{sActivate}"; case A_MoveBackward: return "#{sBack}"; case A_MoveForward: return "#{sForward}"; case A_MoveLeft: return "#{sLeft}"; case A_MoveRight: return "#{sRight}"; case A_ToggleWeapon: return "#{sReady_Weapon}"; case A_ToggleSpell: return "#{sReady_Magic}"; case A_CycleSpellLeft: return "#{sPrevSpell}"; case A_CycleSpellRight: return "#{sNextSpell}"; case A_CycleWeaponLeft: return "#{sPrevWeapon}"; case A_CycleWeaponRight: return "#{sNextWeapon}"; case A_Console: return "#{sConsoleTitle}"; case A_Run: return "#{sRun}"; case A_Sneak: return "#{sCrouch_Sneak}"; case A_AutoMove: return "#{sAuto_Run}"; case A_Jump: return "#{sJump}"; case A_Journal: return "#{sJournal}"; case A_Rest: return "#{sRestKey}"; case A_Inventory: return "#{sInventory}"; case A_TogglePOV: return "#{sTogglePOVCmd}"; case A_QuickKeysMenu: return "#{sQuickMenu}"; case A_QuickKey1: return "#{sQuick1Cmd}"; case A_QuickKey2: return "#{sQuick2Cmd}"; case A_QuickKey3: return "#{sQuick3Cmd}"; case A_QuickKey4: return "#{sQuick4Cmd}"; case A_QuickKey5: return "#{sQuick5Cmd}"; case A_QuickKey6: return "#{sQuick6Cmd}"; case A_QuickKey7: return "#{sQuick7Cmd}"; case A_QuickKey8: return "#{sQuick8Cmd}"; case A_QuickKey9: return "#{sQuick9Cmd}"; case A_QuickKey10: return "#{sQuick10Cmd}"; case A_AlwaysRun: return "#{sAlways_Run}"; case A_QuickSave: return "#{sQuickSaveCmd}"; case A_QuickLoad: return "#{sQuickLoadCmd}"; default: return std::string(); // not configurable } } std::string BindingsManager::getActionKeyBindingName(int action) { if (mInputBinder->getChannel(action)->getControlsCount() == 0) return "#{sNone}"; ICS::Control* c = mInputBinder->getChannel(action)->getAttachedControls().front().control; SDL_Scancode key = mInputBinder->getKeyBinding(c, ICS::Control::INCREASE); unsigned int mouse = mInputBinder->getMouseButtonBinding(c, ICS::Control::INCREASE); ICS::InputControlSystem::MouseWheelClick wheel = mInputBinder->getMouseWheelBinding(c, ICS::Control::INCREASE); if (key != SDL_SCANCODE_UNKNOWN) return MyGUI::TextIterator::toTagsString(mInputBinder->scancodeToString(key)); else if (mouse != ICS_MAX_DEVICE_BUTTONS) return "#{sMouse} " + std::to_string(mouse); else if (wheel != ICS::InputControlSystem::MouseWheelClick::UNASSIGNED) switch (wheel) { case ICS::InputControlSystem::MouseWheelClick::UP: return "Mouse Wheel Up"; case ICS::InputControlSystem::MouseWheelClick::DOWN: return "Mouse Wheel Down"; case ICS::InputControlSystem::MouseWheelClick::RIGHT: return "Mouse Wheel Right"; case ICS::InputControlSystem::MouseWheelClick::LEFT: return "Mouse Wheel Left"; default: return "#{sNone}"; } else return "#{sNone}"; } std::string BindingsManager::getActionControllerBindingName(int action) { if (mInputBinder->getChannel(action)->getControlsCount() == 0) return "#{sNone}"; 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)); else if (mInputBinder->getJoystickButtonBinding(c, sFakeDeviceId, ICS::Control::INCREASE) != ICS_MAX_DEVICE_BUTTONS) return sdlControllerButtonToString(mInputBinder->getJoystickButtonBinding(c, sFakeDeviceId, ICS::Control::INCREASE)); else return "#{sNone}"; } std::vector BindingsManager::getActionKeySorting() { static const std::vector actions { A_MoveForward, A_MoveBackward, A_MoveLeft, A_MoveRight, A_TogglePOV, A_ZoomIn, A_ZoomOut, A_Run, A_AlwaysRun, A_Sneak, A_Activate, A_Use, A_ToggleWeapon, A_ToggleSpell, A_CycleSpellLeft, A_CycleSpellRight, A_CycleWeaponLeft, A_CycleWeaponRight, A_AutoMove, A_Jump, A_Inventory, A_Journal, A_Rest, A_Console, A_QuickSave, A_QuickLoad, A_ToggleHUD, A_Screenshot, A_QuickKeysMenu, A_QuickKey1, A_QuickKey2, A_QuickKey3, A_QuickKey4, A_QuickKey5, A_QuickKey6, A_QuickKey7, A_QuickKey8, A_QuickKey9, A_QuickKey10 }; return actions; } std::vector BindingsManager::getActionControllerSorting() { static const std::vector actions { A_TogglePOV, A_ZoomIn, A_ZoomOut, A_Sneak, A_Activate, A_Use, A_ToggleWeapon, A_ToggleSpell, A_AutoMove, A_Jump, A_Inventory, A_Journal, A_Rest, A_QuickSave, A_QuickLoad, A_ToggleHUD, A_Screenshot, A_QuickKeysMenu, A_QuickKey1, A_QuickKey2, A_QuickKey3, A_QuickKey4, A_QuickKey5, A_QuickKey6, A_QuickKey7, A_QuickKey8, A_QuickKey9, A_QuickKey10, A_CycleSpellLeft, A_CycleSpellRight, A_CycleWeaponLeft, A_CycleWeaponRight }; return actions; } void BindingsManager::enableDetectingBindingMode(int action, bool keyboard) { mListener->setDetectingKeyboard(keyboard); ICS::Control* c = mInputBinder->getChannel(action)->getAttachedControls().front().control; mInputBinder->enableDetectingBindingState(c, ICS::Control::INCREASE); } bool BindingsManager::isDetectingBindingState() const { return mInputBinder->detectingBindingState(); } void BindingsManager::mousePressed(const SDL_MouseButtonEvent &arg, int deviceID) { mInputBinder->mousePressed(arg, deviceID); } void BindingsManager::mouseReleased(const SDL_MouseButtonEvent &arg, int deviceID) { mInputBinder->mouseReleased(arg, deviceID); } void BindingsManager::mouseMoved(const SDLUtil::MouseMotionEvent &arg) { mInputBinder->mouseMoved(arg); } void BindingsManager::mouseWheelMoved(const SDL_MouseWheelEvent &arg) { mInputBinder->mouseWheelMoved(arg); } void BindingsManager::keyPressed(const SDL_KeyboardEvent &arg) { /* Start of tes3mp addition Pass the pressed key to the multiplayer-specific GUI controller */ mwmp::Main::get().getGUIController()->pressedKey(arg.keysym.scancode); /* End of tes3mp addition */ mInputBinder->keyPressed(arg); } void BindingsManager::keyReleased(const SDL_KeyboardEvent &arg) { mInputBinder->keyReleased(arg); } void BindingsManager::controllerAdded(int deviceID, const SDL_ControllerDeviceEvent &arg) { mInputBinder->controllerAdded(deviceID, arg); } void BindingsManager::controllerRemoved(const SDL_ControllerDeviceEvent &arg) { mInputBinder->controllerRemoved(arg); } void BindingsManager::controllerButtonPressed(int deviceID, const SDL_ControllerButtonEvent &arg) { mInputBinder->buttonPressed(deviceID, arg); } void BindingsManager::controllerButtonReleased(int deviceID, const SDL_ControllerButtonEvent &arg) { mInputBinder->buttonReleased(deviceID, arg); } void BindingsManager::controllerAxisMoved(int deviceID, const SDL_ControllerAxisEvent &arg) { mInputBinder->axisMoved(deviceID, arg); } SDL_Scancode BindingsManager::getKeyBinding(int actionId) { return mInputBinder->getKeyBinding(mInputBinder->getControl(actionId), ICS::Control::INCREASE); } void BindingsManager::actionValueChanged(int action, float currentValue, float previousValue) { MWBase::Environment::get().getInputManager()->resetIdleTime(); if (mDragDrop && action != A_GameMenu && action != A_Inventory) return; if ((previousValue == 1 || previousValue == 0) && (currentValue==1 || currentValue==0)) { //Is a normal button press, so don't change it at all } //Otherwise only trigger button presses as they go through specific points else if (previousValue >= 0.8 && currentValue < 0.8) { currentValue = 0.0; previousValue = 1.0; } else if (previousValue <= 0.6 && currentValue > 0.6) { currentValue = 1.0; previousValue = 0.0; } else { //If it's not switching between those values, ignore the channel change. return; } if (MWBase::Environment::get().getInputManager()->getControlSwitch("playercontrols")) { bool joystickUsed = MWBase::Environment::get().getInputManager()->joystickLastUsed(); if (action == A_Use) { if (joystickUsed && currentValue == 1.0 && actionIsActive(A_ToggleWeapon)) action = A_CycleWeaponRight; else if (joystickUsed && currentValue == 1.0 && actionIsActive(A_ToggleSpell)) action = A_CycleSpellRight; else { /* Start of tes3mp addition Prevent players from starting attacks while in the persuasion submenu in dialogue */ if (MWBase::Environment::get().getWindowManager()->containsMode(MWGui::GM_Dialogue)) return; /* End of tes3mp addition */ MWWorld::Player& player = MWBase::Environment::get().getWorld()->getPlayer(); MWMechanics::DrawState_ state = player.getDrawState(); player.setAttackingOrSpell(currentValue != 0 && state != MWMechanics::DrawState_Nothing); } } else if (action == A_Jump) { if (joystickUsed && currentValue == 1.0 && actionIsActive(A_ToggleWeapon)) action = A_CycleWeaponLeft; else if (joystickUsed && currentValue == 1.0 && actionIsActive(A_ToggleSpell)) action = A_CycleSpellLeft; else MWBase::Environment::get().getInputManager()->setAttemptJump(currentValue == 1.0 && previousValue == 0.0); } } if (currentValue == 1) MWBase::Environment::get().getInputManager()->executeAction(action); } } ================================================ FILE: apps/openmw/mwinput/bindingsmanager.hpp ================================================ #ifndef MWINPUT_MWBINDINGSMANAGER_H #define MWINPUT_MWBINDINGSMANAGER_H #include #include #include #include namespace MWInput { class BindingsListener; class InputControlSystem; class BindingsManager { public: BindingsManager(const std::string& userFile, bool userFileExists); virtual ~BindingsManager(); std::string getActionDescription (int action); std::string getActionKeyBindingName (int action); std::string getActionControllerBindingName (int action); std::vector getActionKeySorting(); std::vector getActionControllerSorting(); void enableDetectingBindingMode (int action, bool keyboard); bool isDetectingBindingState() const; void loadKeyDefaults(bool force = false); void loadControllerDefaults(bool force = false); void setDragDrop(bool dragDrop); void update(float dt); void setPlayerControlsEnabled(bool enabled); void setJoystickDeadZone(float deadZone); bool isLeftOrRightButton(int action, bool joystick) const; bool actionIsActive(int id) const; float getActionValue(int id) const; void mousePressed(const SDL_MouseButtonEvent &evt, int deviceID); void mouseReleased(const SDL_MouseButtonEvent &arg, int deviceID); void mouseMoved(const SDLUtil::MouseMotionEvent &arg); void mouseWheelMoved(const SDL_MouseWheelEvent &arg); void keyPressed(const SDL_KeyboardEvent &arg); void keyReleased(const SDL_KeyboardEvent &arg); void controllerAdded(int deviceID, const SDL_ControllerDeviceEvent &arg); void controllerRemoved(const SDL_ControllerDeviceEvent &arg); void controllerButtonPressed(int deviceID, const SDL_ControllerButtonEvent &arg); void controllerButtonReleased(int deviceID, const SDL_ControllerButtonEvent &arg); void controllerAxisMoved(int deviceID, const SDL_ControllerAxisEvent &arg); SDL_Scancode getKeyBinding(int actionId); void actionValueChanged(int action, float currentValue, float previousValue); private: void setupSDLKeyMappings(); std::unique_ptr mInputBinder; std::unique_ptr mListener; std::string mUserFile; bool mDragDrop; }; } #endif ================================================ FILE: apps/openmw/mwinput/controllermanager.cpp ================================================ #include "controllermanager.hpp" #include #include #include #include #include "../mwbase/environment.hpp" #include "../mwbase/inputmanager.hpp" #include "../mwbase/statemanager.hpp" #include "../mwbase/windowmanager.hpp" #include "../mwbase/world.hpp" #include "../mwworld/player.hpp" #include "actions.hpp" #include "actionmanager.hpp" #include "bindingsmanager.hpp" #include "mousemanager.hpp" #include "sdlmappings.hpp" namespace MWInput { ControllerManager::ControllerManager(BindingsManager* bindingsManager, ActionManager* actionManager, MouseManager* mouseManager, const std::string& userControllerBindingsFile, const std::string& controllerBindingsFile) : mBindingsManager(bindingsManager) , mActionManager(actionManager) , mMouseManager(mouseManager) , 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()) { SDL_GameControllerAddMappingsFromFile(controllerBindingsFile.c_str()); } if (!userControllerBindingsFile.empty()) { SDL_GameControllerAddMappingsFromFile(userControllerBindingsFile.c_str()); } // Open all presently connected sticks int numSticks = SDL_NumJoysticks(); for (int i = 0; i < numSticks; i++) { if (SDL_IsGameController(i)) { SDL_ControllerDeviceEvent evt; evt.which = i; static const int fakeDeviceID = 1; controllerAdded(fakeDeviceID, evt); Log(Debug::Info) << "Detected game controller: " << SDL_GameControllerNameForIndex(i); } else { Log(Debug::Info) << "Detected unusable controller: " << SDL_JoystickNameForIndex(i); } } float deadZoneRadius = Settings::Manager::getFloat("joystick dead zone", "Input"); deadZoneRadius = std::min(std::max(deadZoneRadius, 0.0f), 0.5f); mBindingsManager->setJoystickDeadZone(deadZoneRadius); } void ControllerManager::processChangedSettings(const Settings::CategorySettingVector& changed) { for (const auto& setting : changed) { if (setting.first == "Input" && setting.second == "enable controller") mJoystickEnabled = Settings::Manager::getBool("enable controller", "Input"); } } bool ControllerManager::update(float dt) { mGamepadPreviewMode = mActionManager->isPreviewModeEnabled(); if (mGuiCursorEnabled && !(mJoystickLastUsed && !mGamepadGuiCursorEnabled)) { float xAxis = mBindingsManager->getActionValue(A_MoveLeftRight) * 2.0f - 1.0f; float yAxis = mBindingsManager->getActionValue(A_MoveForwardBackward) * 2.0f - 1.0f; float zAxis = mBindingsManager->getActionValue(A_LookUpDown) * 2.0f - 1.0f; xAxis *= (1.5f - mBindingsManager->getActionValue(A_Use)); yAxis *= (1.5f - mBindingsManager->getActionValue(A_Use)); // We keep track of our own mouse position, so that moving the mouse while in // game mode does not move the position of the GUI cursor float uiScale = MWBase::Environment::get().getWindowManager()->getScalingFactor(); float xMove = xAxis * dt * 1500.0f / uiScale * mGamepadCursorSpeed; float yMove = yAxis * dt * 1500.0f / uiScale * mGamepadCursorSpeed; float mouseWheelMove = -zAxis * dt * 1500.0f; if (xMove != 0 || yMove != 0 || mouseWheelMove != 0) { mMouseManager->injectMouseMove(xMove, yMove, mouseWheelMove); mMouseManager->warpMouse(); MWBase::Environment::get().getWindowManager()->setCursorActive(true); } } // Disable movement in Gui mode if (MWBase::Environment::get().getWindowManager()->isGuiMode() || MWBase::Environment::get().getStateManager()->getState() != MWBase::StateManager::State_Running) { mGamepadZoom = 0; return false; } MWWorld::Player& player = MWBase::Environment::get().getWorld()->getPlayer(); bool triedToMove = false; // Configure player movement according to controller input. Actual movement will // be done in the physics system. if (MWBase::Environment::get().getInputManager()->getControlSwitch("playercontrols")) { float xAxis = mBindingsManager->getActionValue(A_MoveLeftRight); float yAxis = mBindingsManager->getActionValue(A_MoveForwardBackward); if (xAxis != 0.5) { triedToMove = true; player.setLeftRight((xAxis - 0.5f) * 2); } if (yAxis != 0.5) { triedToMove = true; player.setAutoMove (false); player.setForwardBackward((0.5f - yAxis) * 2); } if (triedToMove) { mJoystickLastUsed = true; MWBase::Environment::get().getInputManager()->resetIdleTime(); } static const bool isToggleSneak = Settings::Manager::getBool("toggle sneak", "Input"); if (!isToggleSneak) { if (mJoystickLastUsed) { if (mBindingsManager->actionIsActive(A_Sneak)) { if (mSneakToggleShortcutTimer) // New Sneak Button Press { if (mSneakToggleShortcutTimer <= 0.3f) { mSneakGamepadShortcut = true; mActionManager->toggleSneaking(); } else mSneakGamepadShortcut = false; } if (!mActionManager->isSneaking()) mActionManager->toggleSneaking(); mSneakToggleShortcutTimer = 0.f; } else { if (!mSneakGamepadShortcut && mActionManager->isSneaking()) mActionManager->toggleSneaking(); if (mSneakToggleShortcutTimer <= 0.3f) mSneakToggleShortcutTimer += dt; } } else player.setSneak(mBindingsManager->actionIsActive(A_Sneak)); } } if (MWBase::Environment::get().getInputManager()->getControlSwitch("playerviewswitch")) { if (!mBindingsManager->actionIsActive(A_TogglePOV)) mGamepadZoom = 0; if (mGamepadZoom) MWBase::Environment::get().getWorld()->adjustCameraDistance(-mGamepadZoom); } return triedToMove; } void ControllerManager::buttonPressed(int deviceID, const SDL_ControllerButtonEvent &arg) { if (!mJoystickEnabled || mBindingsManager->isDetectingBindingState()) return; mJoystickLastUsed = true; if (MWBase::Environment::get().getWindowManager()->isGuiMode()) { if (gamepadToGuiControl(arg)) return; if (mGamepadGuiCursorEnabled) { // Temporary mouse binding until keyboard controls are available: if (arg.button == SDL_CONTROLLER_BUTTON_A) // We'll pretend that A is left click. { bool mousePressSuccess = mMouseManager->injectMouseButtonPress(SDL_BUTTON_LEFT); if (MyGUI::InputManager::getInstance().getMouseFocusWidget()) { MyGUI::Button* b = MyGUI::InputManager::getInstance().getMouseFocusWidget()->castType(false); if (b && b->getEnabled()) MWBase::Environment::get().getWindowManager()->playSound("Menu Click"); } mBindingsManager->setPlayerControlsEnabled(!mousePressSuccess); } } } else mBindingsManager->setPlayerControlsEnabled(true); //esc, to leave initial movie screen auto kc = sdlKeyToMyGUI(SDLK_ESCAPE); mBindingsManager->setPlayerControlsEnabled(!MyGUI::InputManager::getInstance().injectKeyPress(kc, 0)); if (!MWBase::Environment::get().getInputManager()->controlsDisabled()) mBindingsManager->controllerButtonPressed(deviceID, arg); } void ControllerManager::buttonReleased(int deviceID, const SDL_ControllerButtonEvent &arg) { if (mBindingsManager->isDetectingBindingState()) { mBindingsManager->controllerButtonReleased(deviceID, arg); return; } if (!mJoystickEnabled || MWBase::Environment::get().getInputManager()->controlsDisabled()) return; mJoystickLastUsed = true; if (MWBase::Environment::get().getWindowManager()->isGuiMode()) { if (mGamepadGuiCursorEnabled) { // Temporary mouse binding until keyboard controls are available: if (arg.button == SDL_CONTROLLER_BUTTON_A) // We'll pretend that A is left click. { bool mousePressSuccess = mMouseManager->injectMouseButtonRelease(SDL_BUTTON_LEFT); if (mBindingsManager->isDetectingBindingState()) // If the player just triggered binding, don't let button release bind. return; mBindingsManager->setPlayerControlsEnabled(!mousePressSuccess); } } } else mBindingsManager->setPlayerControlsEnabled(true); //esc, to leave initial movie screen auto kc = sdlKeyToMyGUI(SDLK_ESCAPE); mBindingsManager->setPlayerControlsEnabled(!MyGUI::InputManager::getInstance().injectKeyRelease(kc)); mBindingsManager->controllerButtonReleased(deviceID, arg); } void ControllerManager::axisMoved(int deviceID, const SDL_ControllerAxisEvent &arg) { if (!mJoystickEnabled || MWBase::Environment::get().getInputManager()->controlsDisabled()) return; mJoystickLastUsed = true; if (MWBase::Environment::get().getWindowManager()->isGuiMode()) { gamepadToGuiControl(arg); } else { 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. } } } mBindingsManager->controllerAxisMoved(deviceID, arg); } void ControllerManager::controllerAdded(int deviceID, const SDL_ControllerDeviceEvent &arg) { mBindingsManager->controllerAdded(deviceID, arg); } void ControllerManager::controllerRemoved(const SDL_ControllerDeviceEvent &arg) { mBindingsManager->controllerRemoved(arg); } bool ControllerManager::gamepadToGuiControl(const SDL_ControllerButtonEvent &arg) { // Presumption of GUI mode will be removed in the future. // MyGUI KeyCodes *may* change. MyGUI::KeyCode key = MyGUI::KeyCode::None; switch (arg.button) { case SDL_CONTROLLER_BUTTON_DPAD_UP: key = MyGUI::KeyCode::ArrowUp; break; case SDL_CONTROLLER_BUTTON_DPAD_RIGHT: key = MyGUI::KeyCode::ArrowRight; break; case SDL_CONTROLLER_BUTTON_DPAD_DOWN: key = MyGUI::KeyCode::ArrowDown; break; case SDL_CONTROLLER_BUTTON_DPAD_LEFT: key = MyGUI::KeyCode::ArrowLeft; break; case SDL_CONTROLLER_BUTTON_A: // If we are using the joystick as a GUI mouse, A must be handled via mouse. if (mGamepadGuiCursorEnabled) return false; key = MyGUI::KeyCode::Space; break; case SDL_CONTROLLER_BUTTON_B: if (MyGUI::InputManager::getInstance().isModalAny()) MWBase::Environment::get().getWindowManager()->exitCurrentModal(); else MWBase::Environment::get().getWindowManager()->exitCurrentGuiMode(); return true; case SDL_CONTROLLER_BUTTON_X: key = MyGUI::KeyCode::Semicolon; break; case SDL_CONTROLLER_BUTTON_Y: key = MyGUI::KeyCode::Apostrophe; break; case SDL_CONTROLLER_BUTTON_LEFTSHOULDER: key = MyGUI::KeyCode::Period; break; case SDL_CONTROLLER_BUTTON_RIGHTSHOULDER: key = MyGUI::KeyCode::Slash; break; case SDL_CONTROLLER_BUTTON_LEFTSTICK: mGamepadGuiCursorEnabled = !mGamepadGuiCursorEnabled; MWBase::Environment::get().getWindowManager()->setCursorActive(mGamepadGuiCursorEnabled); return true; default: return false; } // Some keys will work even when Text Input windows/modals are in focus. if (SDL_IsTextInputActive()) return false; MWBase::Environment::get().getWindowManager()->injectKeyPress(key, 0, false); return true; } bool ControllerManager::gamepadToGuiControl(const SDL_ControllerAxisEvent &arg) { switch (arg.axis) { case SDL_CONTROLLER_AXIS_TRIGGERRIGHT: if (arg.value == 32767) // Treat like a button. MWBase::Environment::get().getWindowManager()->injectKeyPress(MyGUI::KeyCode::Minus, 0, false); break; case SDL_CONTROLLER_AXIS_TRIGGERLEFT: if (arg.value == 32767) // Treat like a button. MWBase::Environment::get().getWindowManager()->injectKeyPress(MyGUI::KeyCode::Equals, 0, false); break; case SDL_CONTROLLER_AXIS_LEFTX: case SDL_CONTROLLER_AXIS_LEFTY: case SDL_CONTROLLER_AXIS_RIGHTX: case SDL_CONTROLLER_AXIS_RIGHTY: // If we are using the joystick as a GUI mouse, process mouse movement elsewhere. if (mGamepadGuiCursorEnabled) return false; break; default: return false; } return true; } } ================================================ FILE: apps/openmw/mwinput/controllermanager.hpp ================================================ #ifndef MWINPUT_MWCONTROLLERMANAGER_H #define MWINPUT_MWCONTROLLERMANAGER_H #include #include #include namespace MWInput { class ActionManager; class BindingsManager; class MouseManager; class ControllerManager : public SDLUtil::ControllerListener { public: ControllerManager(BindingsManager* bindingsManager, ActionManager* actionManager, MouseManager* mouseManager, const std::string& userControllerBindingsFile, const std::string& controllerBindingsFile); virtual ~ControllerManager() = default; bool update(float dt); void buttonPressed(int deviceID, const SDL_ControllerButtonEvent &arg) override; void buttonReleased(int deviceID, const SDL_ControllerButtonEvent &arg) override; void axisMoved(int deviceID, const SDL_ControllerAxisEvent &arg) override; void controllerAdded(int deviceID, const SDL_ControllerDeviceEvent &arg) override; void controllerRemoved(const SDL_ControllerDeviceEvent &arg) override; void processChangedSettings(const Settings::CategorySettingVector& changed); void setJoystickLastUsed(bool enabled) { mJoystickLastUsed = enabled; } bool joystickLastUsed() { return mJoystickLastUsed; } void setGuiCursorEnabled(bool enabled) { mGuiCursorEnabled = enabled; } void setGamepadGuiCursorEnabled(bool enabled) { mGamepadGuiCursorEnabled = enabled; } bool gamepadGuiCursorEnabled() { return mGamepadGuiCursorEnabled; } private: // Return true if GUI consumes input. bool gamepadToGuiControl(const SDL_ControllerButtonEvent &arg); bool gamepadToGuiControl(const SDL_ControllerAxisEvent &arg); BindingsManager* mBindingsManager; ActionManager* mActionManager; MouseManager* mMouseManager; bool mJoystickEnabled; float mGamepadCursorSpeed; float mSneakToggleShortcutTimer; float mGamepadZoom; bool mGamepadGuiCursorEnabled; bool mGuiCursorEnabled; bool mJoystickLastUsed; bool mSneakGamepadShortcut; bool mGamepadPreviewMode; }; } #endif ================================================ FILE: apps/openmw/mwinput/controlswitch.cpp ================================================ #include "controlswitch.hpp" #include #include #include #include #include "../mwbase/environment.hpp" #include "../mwbase/world.hpp" #include "../mwworld/player.hpp" namespace MWInput { ControlSwitch::ControlSwitch() { clear(); } void ControlSwitch::clear() { mSwitches["playercontrols"] = true; mSwitches["playerfighting"] = true; mSwitches["playerjumping"] = true; mSwitches["playerlooking"] = true; mSwitches["playermagic"] = true; mSwitches["playerviewswitch"] = true; mSwitches["vanitymode"] = true; } bool ControlSwitch::get(const std::string& key) { return mSwitches[key]; } void ControlSwitch::set(const std::string& key, bool value) { MWWorld::Player& player = MWBase::Environment::get().getWorld()->getPlayer(); /// \note 7 switches at all, if-else is relevant if (key == "playercontrols" && !value) { player.setLeftRight(0); player.setForwardBackward(0); player.setAutoMove(false); player.setUpDown(0); } else if (key == "playerjumping" && !value) { /// \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(), 0.f, 0.f, 0.f); } mSwitches[key] = value; } void ControlSwitch::write(ESM::ESMWriter& writer, Loading::Listener& /*progress*/) { ESM::ControlsState controls; controls.mViewSwitchDisabled = !mSwitches["playerviewswitch"]; controls.mControlsDisabled = !mSwitches["playercontrols"]; controls.mJumpingDisabled = !mSwitches["playerjumping"]; controls.mLookingDisabled = !mSwitches["playerlooking"]; controls.mVanityModeDisabled = !mSwitches["vanitymode"]; controls.mWeaponDrawingDisabled = !mSwitches["playerfighting"]; controls.mSpellDrawingDisabled = !mSwitches["playermagic"]; writer.startRecord (ESM::REC_INPU); controls.save(writer); writer.endRecord (ESM::REC_INPU); } void ControlSwitch::readRecord(ESM::ESMReader& reader, uint32_t type) { ESM::ControlsState controls; controls.load(reader); set("playerviewswitch", !controls.mViewSwitchDisabled); set("playercontrols", !controls.mControlsDisabled); set("playerjumping", !controls.mJumpingDisabled); set("playerlooking", !controls.mLookingDisabled); set("vanitymode", !controls.mVanityModeDisabled); set("playerfighting", !controls.mWeaponDrawingDisabled); set("playermagic", !controls.mSpellDrawingDisabled); } int ControlSwitch::countSavedGameRecords() const { return 1; } } ================================================ FILE: apps/openmw/mwinput/controlswitch.hpp ================================================ #ifndef MWINPUT_CONTROLSWITCH_H #define MWINPUT_CONTROLSWITCH_H #include #include #include namespace ESM { struct ControlsState; class ESMReader; class ESMWriter; } namespace Loading { class Listener; } namespace MWInput { class ControlSwitch { public: ControlSwitch(); bool get(const std::string& key); void set(const std::string& key, bool value); void clear(); void write(ESM::ESMWriter& writer, Loading::Listener& progress); void readRecord(ESM::ESMReader& reader, uint32_t type); int countSavedGameRecords() const; private: std::map mSwitches; }; } #endif ================================================ FILE: apps/openmw/mwinput/inputmanagerimp.cpp ================================================ #include "inputmanagerimp.hpp" #include #include #include #include #include "../mwbase/windowmanager.hpp" #include "../mwbase/environment.hpp" #include "../mwbase/world.hpp" #include "../mwworld/esmstore.hpp" #include "actionmanager.hpp" #include "bindingsmanager.hpp" #include "controllermanager.hpp" #include "controlswitch.hpp" #include "keyboardmanager.hpp" #include "mousemanager.hpp" #include "sdlmappings.hpp" #include "sensormanager.hpp" namespace MWInput { InputManager::InputManager( SDL_Window* window, osg::ref_ptr viewer, osg::ref_ptr screenCaptureHandler, osgViewer::ScreenCaptureHandler::CaptureOperation *screenCaptureOperation, const std::string& userFile, bool userFileExists, const std::string& userControllerBindingsFile, const std::string& controllerBindingsFile, bool grab) : mControlsDisabled(false) { mInputWrapper = new SDLUtil::InputWrapper(window, viewer, grab); mInputWrapper->setWindowEventCallback(MWBase::Environment::get().getWindowManager()); mBindingsManager = new BindingsManager(userFile, userFileExists); mControlSwitch = new ControlSwitch(); mActionManager = new ActionManager(mBindingsManager, screenCaptureOperation, viewer, screenCaptureHandler); mKeyboardManager = new KeyboardManager(mBindingsManager); mInputWrapper->setKeyboardEventCallback(mKeyboardManager); mMouseManager = new MouseManager(mBindingsManager, mInputWrapper, window); mInputWrapper->setMouseEventCallback(mMouseManager); mControllerManager = new ControllerManager(mBindingsManager, mActionManager, mMouseManager, userControllerBindingsFile, controllerBindingsFile); mInputWrapper->setControllerEventCallback(mControllerManager); mSensorManager = new SensorManager(); mInputWrapper->setSensorEventCallback(mSensorManager); } void InputManager::clear() { // Enable all controls mControlSwitch->clear(); } InputManager::~InputManager() { delete mActionManager; delete mControllerManager; delete mKeyboardManager; delete mMouseManager; delete mSensorManager; delete mControlSwitch; delete mBindingsManager; delete mInputWrapper; } void InputManager::setAttemptJump(bool jumping) { mActionManager->setAttemptJump(jumping); } void InputManager::update(float dt, bool disableControls, bool disableEvents) { mControlsDisabled = disableControls; mInputWrapper->setMouseVisible(MWBase::Environment::get().getWindowManager()->getCursorVisible()); mInputWrapper->capture(disableEvents); if (disableControls) { mMouseManager->updateCursorMode(); return; } mBindingsManager->update(dt); mMouseManager->updateCursorMode(); bool controllerMove = mControllerManager->update(dt); mMouseManager->update(dt); mSensorManager->update(dt); mActionManager->update(dt, controllerMove); MWBase::Environment::get().getWorld()->applyDeferredPreviewRotationToPlayer(dt); } void InputManager::setDragDrop(bool dragDrop) { mBindingsManager->setDragDrop(dragDrop); } void InputManager::setGamepadGuiCursorEnabled(bool enabled) { mControllerManager->setGamepadGuiCursorEnabled(enabled); } void InputManager::changeInputMode(bool guiMode) { mControllerManager->setGuiCursorEnabled(guiMode); mMouseManager->setGuiCursorEnabled(guiMode); mSensorManager->setGuiCursorEnabled(guiMode); mMouseManager->setMouseLookEnabled(!guiMode); if (guiMode) MWBase::Environment::get().getWindowManager()->showCrosshair(false); bool isCursorVisible = guiMode && (!mControllerManager->joystickLastUsed() || mControllerManager->gamepadGuiCursorEnabled()); MWBase::Environment::get().getWindowManager()->setCursorVisible(isCursorVisible); // if not in gui mode, the camera decides whether to show crosshair or not. } void InputManager::processChangedSettings(const Settings::CategorySettingVector& changed) { mMouseManager->processChangedSettings(changed); mSensorManager->processChangedSettings(changed); } bool InputManager::getControlSwitch(const std::string& sw) { return mControlSwitch->get(sw); } void InputManager::toggleControlSwitch(const std::string& sw, bool value) { mControlSwitch->set(sw, value); } void InputManager::resetIdleTime() { mActionManager->resetIdleTime(); } std::string InputManager::getActionDescription(int action) { return mBindingsManager->getActionDescription(action); } std::string InputManager::getActionKeyBindingName(int action) { return mBindingsManager->getActionKeyBindingName(action); } std::string InputManager::getActionControllerBindingName(int action) { return mBindingsManager->getActionControllerBindingName(action); } std::vector InputManager::getActionKeySorting() { return mBindingsManager->getActionKeySorting(); } std::vector InputManager::getActionControllerSorting() { return mBindingsManager->getActionControllerSorting(); } void InputManager::enableDetectingBindingMode(int action, bool keyboard) { mBindingsManager->enableDetectingBindingMode(action, keyboard); } int InputManager::countSavedGameRecords() const { return mControlSwitch->countSavedGameRecords(); } void InputManager::write(ESM::ESMWriter& writer, Loading::Listener& progress) { mControlSwitch->write(writer, progress); } void InputManager::readRecord(ESM::ESMReader& reader, uint32_t type) { if (type == ESM::REC_INPU) { mControlSwitch->readRecord(reader, type); } } void InputManager::resetToDefaultKeyBindings() { mBindingsManager->loadKeyDefaults(true); } void InputManager::resetToDefaultControllerBindings() { mBindingsManager->loadControllerDefaults(true); } void InputManager::setJoystickLastUsed(bool enabled) { mControllerManager->setJoystickLastUsed(enabled); } bool InputManager::joystickLastUsed() { return mControllerManager->joystickLastUsed(); } void InputManager::executeAction(int action) { mActionManager->executeAction(action); } } ================================================ FILE: apps/openmw/mwinput/inputmanagerimp.hpp ================================================ #ifndef MWINPUT_MWINPUTMANAGERIMP_H #define MWINPUT_MWINPUTMANAGERIMP_H #include #include #include #include #include "../mwbase/inputmanager.hpp" #include "../mwgui/mode.hpp" #include "actions.hpp" namespace MWWorld { class Player; } namespace MWBase { class WindowManager; } namespace SDLUtil { class InputWrapper; } struct SDL_Window; namespace MWInput { class ControlSwitch; class ActionManager; class BindingsManager; class ControllerManager; class KeyboardManager; class MouseManager; class SensorManager; /** * @brief Class that provides a high-level API for game input */ class InputManager : public MWBase::InputManager { public: InputManager( SDL_Window* window, osg::ref_ptr viewer, osg::ref_ptr screenCaptureHandler, osgViewer::ScreenCaptureHandler::CaptureOperation *screenCaptureOperation, const std::string& userFile, bool userFileExists, const std::string& userControllerBindingsFile, const std::string& controllerBindingsFile, bool grab); virtual ~InputManager(); /// Clear all savegame-specific data void clear() override; void update(float dt, bool disableControls=false, bool disableEvents=false) override; void changeInputMode(bool guiMode) override; void processChangedSettings(const Settings::CategorySettingVector& changed) override; void setDragDrop(bool dragDrop) override; 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; std::string getActionDescription (int action) override; std::string getActionKeyBindingName (int action) override; std::string getActionControllerBindingName (int action) override; int getNumActions() override { return A_Last; } std::vector getActionKeySorting() override; std::vector getActionControllerSorting() override; void enableDetectingBindingMode (int action, bool keyboard) override; void resetToDefaultKeyBindings() override; void resetToDefaultControllerBindings() override; void setJoystickLastUsed(bool enabled) override; bool joystickLastUsed() override; int countSavedGameRecords() const override; void write(ESM::ESMWriter& writer, Loading::Listener& progress) override; void readRecord(ESM::ESMReader& reader, uint32_t type) override; void resetIdleTime() override; void executeAction(int action) override; bool controlsDisabled() override { return mControlsDisabled; } private: void convertMousePosForMyGUI(int& x, int& y); void handleGuiArrowKey(int action); void quickKey(int index); void showQuickKeysMenu(); void loadKeyDefaults(bool force = false); void loadControllerDefaults(bool force = false); SDLUtil::InputWrapper* mInputWrapper; bool mControlsDisabled; ControlSwitch* mControlSwitch; ActionManager* mActionManager; BindingsManager* mBindingsManager; ControllerManager* mControllerManager; KeyboardManager* mKeyboardManager; MouseManager* mMouseManager; SensorManager* mSensorManager; }; } #endif ================================================ FILE: apps/openmw/mwinput/keyboardmanager.cpp ================================================ #include "keyboardmanager.hpp" #include #include #include "../mwbase/environment.hpp" #include "../mwbase/inputmanager.hpp" #include "../mwbase/windowmanager.hpp" #include "../mwworld/player.hpp" #include "actions.hpp" #include "bindingsmanager.hpp" #include "sdlmappings.hpp" namespace MWInput { KeyboardManager::KeyboardManager(BindingsManager* bindingsManager) : mBindingsManager(bindingsManager) { } void KeyboardManager::textInput(const SDL_TextInputEvent &arg) { MyGUI::UString ustring(&arg.text[0]); MyGUI::UString::utf32string utf32string = ustring.asUTF32(); for (MyGUI::UString::utf32string::const_iterator it = utf32string.begin(); it != utf32string.end(); ++it) MyGUI::InputManager::getInstance().injectKeyPress(MyGUI::KeyCode::None, *it); } void KeyboardManager::keyPressed(const SDL_KeyboardEvent &arg) { // 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); 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))); if (kc != MyGUI::KeyCode::None && !mBindingsManager->isDetectingBindingState()) { if (MWBase::Environment::get().getWindowManager()->injectKeyPress(kc, 0, arg.repeat)) consumed = true; mBindingsManager->setPlayerControlsEnabled(!consumed); } if (arg.repeat) return; MWBase::InputManager* input = MWBase::Environment::get().getInputManager(); if (!input->controlsDisabled() && !consumed) mBindingsManager->keyPressed(arg); input->setJoystickLastUsed(false); } void KeyboardManager::keyReleased(const SDL_KeyboardEvent &arg) { MWBase::Environment::get().getInputManager()->setJoystickLastUsed(false); auto kc = sdlKeyToMyGUI(arg.keysym.sym); if (!mBindingsManager->isDetectingBindingState()) mBindingsManager->setPlayerControlsEnabled(!MyGUI::InputManager::getInstance().injectKeyRelease(kc)); mBindingsManager->keyReleased(arg); } } ================================================ FILE: apps/openmw/mwinput/keyboardmanager.hpp ================================================ #ifndef MWINPUT_MWKEYBOARDMANAGER_H #define MWINPUT_MWKEYBOARDMANAGER_H #include namespace MWInput { class BindingsManager; class KeyboardManager : public SDLUtil::KeyListener { public: KeyboardManager(BindingsManager* bindingsManager); virtual ~KeyboardManager() = default; void textInput(const SDL_TextInputEvent &arg) override; void keyPressed(const SDL_KeyboardEvent &arg) override; void keyReleased(const SDL_KeyboardEvent &arg) override; private: BindingsManager* mBindingsManager; }; } #endif ================================================ FILE: apps/openmw/mwinput/mousemanager.cpp ================================================ #include "mousemanager.hpp" #include #include #include #include #include #include #include "../mwbase/environment.hpp" #include "../mwbase/inputmanager.hpp" #include "../mwbase/windowmanager.hpp" #include "../mwbase/world.hpp" #include "../mwworld/player.hpp" #include "actions.hpp" #include "bindingsmanager.hpp" #include "sdlmappings.hpp" namespace MWInput { MouseManager::MouseManager(BindingsManager* bindingsManager, SDLUtil::InputWrapper* inputWrapper, SDL_Window* window) : mInvertX(Settings::Manager::getBool("invert x axis", "Input")) , mInvertY(Settings::Manager::getBool("invert y axis", "Input")) , mGrabCursor(Settings::Manager::getBool("grab cursor", "Input")) , mCameraSensitivity(Settings::Manager::getFloat("camera sensitivity", "Input")) , mCameraYMultiplier(Settings::Manager::getFloat("camera y multiplier", "Input")) , mBindingsManager(bindingsManager) , mInputWrapper(inputWrapper) , mGuiCursorX(0) , mGuiCursorY(0) , mMouseWheel(0) , mMouseLookEnabled(false) , mGuiCursorEnabled(true) { int w,h; SDL_GetWindowSize(window, &w, &h); float uiScale = MWBase::Environment::get().getWindowManager()->getScalingFactor(); mGuiCursorX = w / (2.f * uiScale); mGuiCursorY = h / (2.f * uiScale); } void MouseManager::processChangedSettings(const Settings::CategorySettingVector& changed) { for (const auto& setting : changed) { if (setting.first == "Input" && setting.second == "invert x axis") mInvertX = Settings::Manager::getBool("invert x axis", "Input"); if (setting.first == "Input" && setting.second == "invert y axis") mInvertY = Settings::Manager::getBool("invert y axis", "Input"); if (setting.first == "Input" && setting.second == "camera sensitivity") mCameraSensitivity = Settings::Manager::getFloat("camera sensitivity", "Input"); if (setting.first == "Input" && setting.second == "grab cursor") mGrabCursor = Settings::Manager::getBool("grab cursor", "Input"); } } void MouseManager::mouseMoved(const SDLUtil::MouseMotionEvent &arg) { mBindingsManager->mouseMoved(arg); MWBase::InputManager* input = MWBase::Environment::get().getInputManager(); input->setJoystickLastUsed(false); input->resetIdleTime(); if (mGuiCursorEnabled) { input->setGamepadGuiCursorEnabled(true); // We keep track of our own mouse position, so that moving the mouse while in // game mode does not move the position of the GUI cursor float uiScale = MWBase::Environment::get().getWindowManager()->getScalingFactor(); mGuiCursorX = static_cast(arg.x) / uiScale; mGuiCursorY = static_cast(arg.y) / uiScale; mMouseWheel = static_cast(arg.z); MyGUI::InputManager::getInstance().injectMouseMove(static_cast(mGuiCursorX), static_cast(mGuiCursorY), mMouseWheel); // FIXME: inject twice to force updating focused widget states (tooltips) resulting from changing the viewport by scroll wheel MyGUI::InputManager::getInstance().injectMouseMove(static_cast(mGuiCursorX), static_cast(mGuiCursorY), mMouseWheel); MWBase::Environment::get().getWindowManager()->setCursorActive(true); } if (mMouseLookEnabled && !input->controlsDisabled()) { MWBase::World* world = MWBase::Environment::get().getWorld(); float x = arg.xrel * mCameraSensitivity * (mInvertX ? -1 : 1) / 256.f; float y = arg.yrel * mCameraSensitivity * (mInvertY ? -1 : 1) * mCameraYMultiplier / 256.f; float rot[3]; rot[0] = -y; rot[1] = 0.0f; rot[2] = -x; // Only actually turn player when we're not in vanity mode if (!world->vanityRotateCamera(rot) && input->getControlSwitch("playerlooking")) { MWWorld::Player& player = world->getPlayer(); player.yaw(x); player.pitch(y); } else if (!input->getControlSwitch("playerlooking")) MWBase::Environment::get().getWorld()->disableDeferredPreviewRotation(); } } void MouseManager::mouseReleased(const SDL_MouseButtonEvent &arg, Uint8 id) { MWBase::Environment::get().getInputManager()->setJoystickLastUsed(false); if (mBindingsManager->isDetectingBindingState()) { mBindingsManager->mouseReleased(arg, id); } else { bool guiMode = MWBase::Environment::get().getWindowManager()->isGuiMode(); guiMode = MyGUI::InputManager::getInstance().injectMouseRelease(static_cast(mGuiCursorX), static_cast(mGuiCursorY), sdlButtonToMyGUI(id)) && guiMode; if (mBindingsManager->isDetectingBindingState()) return; // don't allow same mouseup to bind as initiated bind mBindingsManager->setPlayerControlsEnabled(!guiMode); mBindingsManager->mouseReleased(arg, id); } } void MouseManager::mouseWheelMoved(const SDL_MouseWheelEvent &arg) { MWBase::InputManager* input = MWBase::Environment::get().getInputManager(); if (mBindingsManager->isDetectingBindingState() || !input->controlsDisabled()) mBindingsManager->mouseWheelMoved(arg); input->setJoystickLastUsed(false); } void MouseManager::mousePressed(const SDL_MouseButtonEvent &arg, Uint8 id) { MWBase::InputManager* input = MWBase::Environment::get().getInputManager(); input->setJoystickLastUsed(false); bool guiMode = false; 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(mGuiCursorX), static_cast(mGuiCursorY), sdlButtonToMyGUI(id)) && guiMode; if (MyGUI::InputManager::getInstance().getMouseFocusWidget () != nullptr) { MyGUI::Button* b = MyGUI::InputManager::getInstance().getMouseFocusWidget()->castType(false); if (b && b->getEnabled() && id == SDL_BUTTON_LEFT) { MWBase::Environment::get().getWindowManager()->playSound("Menu Click"); } } MWBase::Environment::get().getWindowManager()->setCursorActive(true); } mBindingsManager->setPlayerControlsEnabled(!guiMode); // Don't trigger any mouse bindings while in settings menu, otherwise rebinding controls becomes impossible // Also do not trigger bindings when input controls are disabled, e.g. during save loading if (MWBase::Environment::get().getWindowManager()->getMode() != MWGui::GM_Settings && !input->controlsDisabled()) mBindingsManager->mousePressed(arg, id); } void MouseManager::updateCursorMode() { bool grab = !MWBase::Environment::get().getWindowManager()->containsMode(MWGui::GM_MainMenu) && !MWBase::Environment::get().getWindowManager()->isConsoleMode(); bool wasRelative = mInputWrapper->getMouseRelative(); bool isRelative = !MWBase::Environment::get().getWindowManager()->isGuiMode(); // don't keep the pointer away from the window edge in gui mode // stop using raw mouse motions and switch to system cursor movements mInputWrapper->setMouseRelative(isRelative); //we let the mouse escape in the main menu mInputWrapper->setGrabPointer(grab && (mGrabCursor || isRelative)); //we switched to non-relative mode, move our cursor to where the in-game //cursor is if (!isRelative && wasRelative != isRelative) { warpMouse(); } } void MouseManager::update(float dt) { if (!mMouseLookEnabled) return; float xAxis = mBindingsManager->getActionValue(A_LookLeftRight) * 2.0f - 1.0f; float yAxis = mBindingsManager->getActionValue(A_LookUpDown) * 2.0f - 1.0f; if (xAxis == 0 && yAxis == 0) return; float rot[3]; rot[0] = -yAxis * dt * 1000.0f * mCameraSensitivity * (mInvertY ? -1 : 1) * mCameraYMultiplier / 256.f; rot[1] = 0.0f; rot[2] = -xAxis * dt * 1000.0f * mCameraSensitivity * (mInvertX ? -1 : 1) / 256.f; // Only actually turn player when we're not in vanity mode bool controls = MWBase::Environment::get().getInputManager()->getControlSwitch("playercontrols"); if (!MWBase::Environment::get().getWorld()->vanityRotateCamera(rot) && controls) { MWWorld::Player& player = MWBase::Environment::get().getWorld()->getPlayer(); player.yaw(-rot[2]); player.pitch(-rot[0]); } else if (!controls) MWBase::Environment::get().getWorld()->disableDeferredPreviewRotation(); MWBase::Environment::get().getInputManager()->resetIdleTime(); } bool MouseManager::injectMouseButtonPress(Uint8 button) { return MyGUI::InputManager::getInstance().injectMousePress(static_cast(mGuiCursorX), static_cast(mGuiCursorY), sdlButtonToMyGUI(button)); } bool MouseManager::injectMouseButtonRelease(Uint8 button) { return MyGUI::InputManager::getInstance().injectMouseRelease(static_cast(mGuiCursorX), static_cast(mGuiCursorY), sdlButtonToMyGUI(button)); } void MouseManager::injectMouseMove(float xMove, float yMove, float mouseWheelMove) { mGuiCursorX += xMove; mGuiCursorY += yMove; 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))); MyGUI::InputManager::getInstance().injectMouseMove(static_cast(mGuiCursorX), static_cast(mGuiCursorY), static_cast(mMouseWheel)); } void MouseManager::warpMouse() { float uiScale = MWBase::Environment::get().getWindowManager()->getScalingFactor(); mInputWrapper->warpMouse(static_cast(mGuiCursorX*uiScale), static_cast(mGuiCursorY*uiScale)); } } ================================================ FILE: apps/openmw/mwinput/mousemanager.hpp ================================================ #ifndef MWINPUT_MWMOUSEMANAGER_H #define MWINPUT_MWMOUSEMANAGER_H #include #include namespace SDLUtil { class InputWrapper; } namespace MWInput { class BindingsManager; class MouseManager : public SDLUtil::MouseListener { public: MouseManager(BindingsManager* bindingsManager, SDLUtil::InputWrapper* inputWrapper, SDL_Window* window); virtual ~MouseManager() = default; void updateCursorMode(); void update(float dt); void mouseMoved(const SDLUtil::MouseMotionEvent &arg) override; void mousePressed(const SDL_MouseButtonEvent &arg, Uint8 id) override; void mouseReleased(const SDL_MouseButtonEvent &arg, Uint8 id) override; void mouseWheelMoved(const SDL_MouseWheelEvent &arg) override; void processChangedSettings(const Settings::CategorySettingVector& changed); bool injectMouseButtonPress(Uint8 button); bool injectMouseButtonRelease(Uint8 button); void injectMouseMove(float xMove, float yMove, float mouseWheelMove); void warpMouse(); void setMouseLookEnabled(bool enabled) { mMouseLookEnabled = enabled; } void setGuiCursorEnabled(bool enabled) { mGuiCursorEnabled = enabled; } private: bool mInvertX; bool mInvertY; bool mGrabCursor; float mCameraSensitivity; float mCameraYMultiplier; BindingsManager* mBindingsManager; SDLUtil::InputWrapper* mInputWrapper; float mGuiCursorX; float mGuiCursorY; int mMouseWheel; bool mMouseLookEnabled; bool mGuiCursorEnabled; }; } #endif ================================================ FILE: apps/openmw/mwinput/sdlmappings.cpp ================================================ #include "sdlmappings.hpp" #include #include #include #include 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& 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 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; } } ================================================ FILE: apps/openmw/mwinput/sdlmappings.hpp ================================================ #ifndef MWINPUT_SDLMAPPINGS_H #define MWINPUT_SDLMAPPINGS_H #include #include #include namespace MyGUI { struct MouseButton; } namespace MWInput { std::string sdlControllerButtonToString(int button); std::string sdlControllerAxisToString(int axis); MyGUI::MouseButton sdlButtonToMyGUI(Uint8 button); MyGUI::KeyCode sdlKeyToMyGUI(SDL_Keycode code); } #endif ================================================ FILE: apps/openmw/mwinput/sensormanager.cpp ================================================ #include "sensormanager.hpp" #include #include "../mwbase/environment.hpp" #include "../mwbase/inputmanager.hpp" #include "../mwbase/world.hpp" #include "../mwworld/player.hpp" namespace MWInput { SensorManager::SensorManager() : mInvertX(Settings::Manager::getBool("invert x axis", "Input")) , mInvertY(Settings::Manager::getBool("invert y axis", "Input")) , mGyroXSpeed(0.f) , mGyroYSpeed(0.f) , mGyroUpdateTimer(0.f) , mGyroHSensitivity(Settings::Manager::getFloat("gyro horizontal sensitivity", "Input")) , mGyroVSensitivity(Settings::Manager::getFloat("gyro vertical sensitivity", "Input")) , mGyroHAxis(GyroscopeAxis::Minus_X) , mGyroVAxis(GyroscopeAxis::Y) , mGyroInputThreshold(Settings::Manager::getFloat("gyro input threshold", "Input")) , mGyroscope(nullptr) , mGuiCursorEnabled(true) { init(); } void SensorManager::init() { correctGyroscopeAxes(); updateSensors(); } SensorManager::~SensorManager() { if (mGyroscope != nullptr) { SDL_SensorClose(mGyroscope); mGyroscope = nullptr; } } SensorManager::GyroscopeAxis SensorManager::mapGyroscopeAxis(const std::string& axis) { if (axis == "x") return GyroscopeAxis::X; else if (axis == "y") return GyroscopeAxis::Y; else if (axis == "z") return GyroscopeAxis::Z; else if (axis == "-x") return GyroscopeAxis::Minus_X; else if (axis == "-y") return GyroscopeAxis::Minus_Y; else if (axis == "-z") return GyroscopeAxis::Minus_Z; return GyroscopeAxis::Unknown; } void SensorManager::correctGyroscopeAxes() { if (!Settings::Manager::getBool("enable gyroscope", "Input")) return; // Treat setting from config as axes for landscape mode. // If the device does not support orientation change, do nothing. // Note: in is unclear how to correct axes for devices with non-standart Z axis direction. mGyroHAxis = mapGyroscopeAxis(Settings::Manager::getString("gyro horizontal axis", "Input")); mGyroVAxis = mapGyroscopeAxis(Settings::Manager::getString("gyro vertical axis", "Input")); SDL_DisplayOrientation currentOrientation = SDL_GetDisplayOrientation(Settings::Manager::getInt("screen", "Video")); switch (currentOrientation) { case SDL_ORIENTATION_UNKNOWN: return; case SDL_ORIENTATION_LANDSCAPE: break; case SDL_ORIENTATION_LANDSCAPE_FLIPPED: { mGyroHAxis = GyroscopeAxis(-mGyroHAxis); mGyroVAxis = GyroscopeAxis(-mGyroVAxis); break; } case SDL_ORIENTATION_PORTRAIT: { GyroscopeAxis oldVAxis = mGyroVAxis; mGyroVAxis = mGyroHAxis; mGyroHAxis = GyroscopeAxis(-oldVAxis); break; } case SDL_ORIENTATION_PORTRAIT_FLIPPED: { GyroscopeAxis oldVAxis = mGyroVAxis; mGyroVAxis = GyroscopeAxis(-mGyroHAxis); mGyroHAxis = oldVAxis; break; } } } void SensorManager::updateSensors() { if (Settings::Manager::getBool("enable gyroscope", "Input")) { int numSensors = SDL_NumSensors(); for (int i = 0; i < numSensors; ++i) { if (SDL_SensorGetDeviceType(i) == SDL_SENSOR_GYRO) { // It is unclear how to handle several enabled gyroscopes, so use the first one. // Note: Android registers some gyroscope as two separate sensors, for non-wake-up mode and for wake-up mode. if (mGyroscope != nullptr) { SDL_SensorClose(mGyroscope); mGyroscope = nullptr; mGyroXSpeed = mGyroYSpeed = 0.f; mGyroUpdateTimer = 0.f; } // FIXME: SDL2 does not provide a way to configure a sensor update frequency so far. SDL_Sensor *sensor = SDL_SensorOpen(i); if (sensor == nullptr) Log(Debug::Error) << "Couldn't open sensor " << SDL_SensorGetDeviceName(i) << ": " << SDL_GetError(); else { mGyroscope = sensor; break; } } } } else { if (mGyroscope != nullptr) { SDL_SensorClose(mGyroscope); mGyroscope = nullptr; mGyroXSpeed = mGyroYSpeed = 0.f; mGyroUpdateTimer = 0.f; } } } void SensorManager::processChangedSettings(const Settings::CategorySettingVector& changed) { for (const auto& setting : changed) { if (setting.first == "Input" && setting.second == "invert x axis") mInvertX = Settings::Manager::getBool("invert x axis", "Input"); if (setting.first == "Input" && setting.second == "invert y axis") mInvertY = Settings::Manager::getBool("invert y axis", "Input"); if (setting.first == "Input" && setting.second == "gyro horizontal sensitivity") mGyroHSensitivity = Settings::Manager::getFloat("gyro horizontal sensitivity", "Input"); if (setting.first == "Input" && setting.second == "gyro vertical sensitivity") mGyroVSensitivity = Settings::Manager::getFloat("gyro vertical sensitivity", "Input"); if (setting.first == "Input" && setting.second == "enable gyroscope") init(); if (setting.first == "Input" && setting.second == "gyro horizontal axis") correctGyroscopeAxes(); if (setting.first == "Input" && setting.second == "gyro vertical axis") correctGyroscopeAxes(); if (setting.first == "Input" && setting.second == "gyro input threshold") mGyroInputThreshold = Settings::Manager::getFloat("gyro input threshold", "Input"); } } float SensorManager::getGyroAxisSpeed(GyroscopeAxis axis, const SDL_SensorEvent &arg) const { switch (axis) { case GyroscopeAxis::X: case GyroscopeAxis::Y: case GyroscopeAxis::Z: return std::abs(arg.data[0]) >= mGyroInputThreshold ? arg.data[axis-1] : 0.f; case GyroscopeAxis::Minus_X: case GyroscopeAxis::Minus_Y: case GyroscopeAxis::Minus_Z: return std::abs(arg.data[0]) >= mGyroInputThreshold ? -arg.data[std::abs(axis)-1] : 0.f; default: return 0.f; } } void SensorManager::displayOrientationChanged() { correctGyroscopeAxes(); } void SensorManager::sensorUpdated(const SDL_SensorEvent &arg) { if (!Settings::Manager::getBool("enable gyroscope", "Input")) return; SDL_Sensor *sensor = SDL_SensorFromInstanceID(arg.which); if (!sensor) { Log(Debug::Info) << "Couldn't get sensor for sensor event"; return; } switch (SDL_SensorGetType(sensor)) { case SDL_SENSOR_ACCEL: break; case SDL_SENSOR_GYRO: { mGyroXSpeed = getGyroAxisSpeed(mGyroHAxis, arg); mGyroYSpeed = getGyroAxisSpeed(mGyroVAxis, arg); mGyroUpdateTimer = 0.f; break; } default: break; } } void SensorManager::update(float dt) { if (mGyroXSpeed == 0.f && mGyroYSpeed == 0.f) return; if (mGyroUpdateTimer > 0.5f) { // More than half of second passed since the last gyroscope update. // A device more likely was disconnected or switched to the sleep mode. // Reset current rotation speed and wait for update. mGyroXSpeed = 0.f; mGyroYSpeed = 0.f; mGyroUpdateTimer = 0.f; return; } mGyroUpdateTimer += dt; if (!mGuiCursorEnabled) { float rot[3]; rot[0] = -mGyroYSpeed * dt * mGyroVSensitivity * 4 * (mInvertY ? -1 : 1); rot[1] = 0.0f; rot[2] = -mGyroXSpeed * dt * mGyroHSensitivity * 4 * (mInvertX ? -1 : 1); // Only actually turn player when we're not in vanity mode bool playerLooking = MWBase::Environment::get().getInputManager()->getControlSwitch("playerlooking"); if (!MWBase::Environment::get().getWorld()->vanityRotateCamera(rot) && playerLooking) { MWWorld::Player& player = MWBase::Environment::get().getWorld()->getPlayer(); player.yaw(-rot[2]); player.pitch(-rot[0]); } else if (!playerLooking) MWBase::Environment::get().getWorld()->disableDeferredPreviewRotation(); MWBase::Environment::get().getInputManager()->resetIdleTime(); } } } ================================================ FILE: apps/openmw/mwinput/sensormanager.hpp ================================================ #ifndef MWINPUT_MWSENSORMANAGER_H #define MWINPUT_MWSENSORMANAGER_H #include #include #include namespace SDLUtil { class InputWrapper; } namespace MWWorld { class Player; } namespace MWInput { class SensorManager : public SDLUtil::SensorListener { public: SensorManager(); virtual ~SensorManager(); void init(); void update(float dt); void sensorUpdated(const SDL_SensorEvent &arg) override; void displayOrientationChanged() override; void processChangedSettings(const Settings::CategorySettingVector& changed); void setGuiCursorEnabled(bool enabled) { mGuiCursorEnabled = enabled; } private: enum GyroscopeAxis { Unknown = 0, X = 1, Y = 2, Z = 3, Minus_X = -1, Minus_Y = -2, Minus_Z = -3 }; void updateSensors(); void correctGyroscopeAxes(); GyroscopeAxis mapGyroscopeAxis(const std::string& axis); float getGyroAxisSpeed(GyroscopeAxis axis, const SDL_SensorEvent &arg) const; bool mInvertX; bool mInvertY; float mGyroXSpeed; float mGyroYSpeed; float mGyroUpdateTimer; float mGyroHSensitivity; float mGyroVSensitivity; GyroscopeAxis mGyroHAxis; GyroscopeAxis mGyroVAxis; float mGyroInputThreshold; SDL_Sensor* mGyroscope; bool mGuiCursorEnabled; }; } #endif ================================================ FILE: apps/openmw/mwmechanics/activespells.cpp ================================================ #include "activespells.hpp" #include #include #include /* Start of tes3mp addition Include additional headers for multiplayer purposes */ #include "../mwmechanics/actorutil.hpp" #include "../mwmechanics/creaturestats.hpp" #include "../mwworld/class.hpp" #include "../mwmp/Main.hpp" #include "../mwmp/LocalPlayer.hpp" #include "../mwmp/CellController.hpp" #include "../mwmp/MechanicsHelper.hpp" /* End of tes3mp addition */ #include "../mwbase/environment.hpp" #include "../mwbase/world.hpp" #include "../mwworld/esmstore.hpp" namespace MWMechanics { void ActiveSpells::update(float duration) const { bool rebuild = false; // Erase no longer active spells and effects if (duration > 0) { TContainer::iterator iter (mSpells.begin()); while (iter!=mSpells.end()) { if (!timeToExpire (iter)) { /* Start of tes3mp addition Whenever the local player loses an active spell, send an ID_PLAYER_SPELLS_ACTIVE packet to the server with it Whenever a local actor loses an active spell, send an ID_ACTOR_SPELLS_ACTIVE packet to the server with it */ if (this == &MWMechanics::getPlayer().getClass().getCreatureStats(MWMechanics::getPlayer()).getActiveSpells()) { mwmp::Main::get().getLocalPlayer()->sendSpellsActiveRemoval(iter->first, MechanicsHelper::isStackingSpell(iter->first), iter->second.mTimeStamp); } else { MWWorld::Ptr actorPtr = MWBase::Environment::get().getWorld()->searchPtrViaActorId(getActorId()); if (mwmp::Main::get().getCellController()->isLocalActor(actorPtr)) mwmp::Main::get().getCellController()->getLocalActor(actorPtr)->sendSpellsActiveRemoval(iter->first, MechanicsHelper::isStackingSpell(iter->first), iter->second.mTimeStamp); } /* End of tes3mp addition */ mSpells.erase (iter++); rebuild = true; } else { bool interrupt = false; std::vector& effects = iter->second.mEffects; for (std::vector::iterator effectIt = effects.begin(); effectIt != effects.end();) { if (effectIt->mTimeLeft <= 0) { rebuild = true; // Note: it we expire a Corprus effect, we should remove the whole spell. if (effectIt->mEffectId == ESM::MagicEffect::Corprus) { iter = mSpells.erase (iter); interrupt = true; break; } effectIt = effects.erase(effectIt); } else { effectIt->mTimeLeft -= duration; ++effectIt; } } if (!interrupt) ++iter; } } } if (mSpellsChanged) { mSpellsChanged = false; rebuild = true; } if (rebuild) rebuildEffects(); } void ActiveSpells::rebuildEffects() const { mEffects = MagicEffects(); for (TIterator iter (begin()); iter!=end(); ++iter) { const std::vector& effects = iter->second.mEffects; for (std::vector::const_iterator effectIt = effects.begin(); effectIt != effects.end(); ++effectIt) { if (effectIt->mTimeLeft > 0) mEffects.add(MWMechanics::EffectKey(effectIt->mEffectId, effectIt->mArg), MWMechanics::EffectParam(effectIt->mMagnitude)); } } } ActiveSpells::ActiveSpells() : mSpellsChanged (false) {} const MagicEffects& ActiveSpells::getMagicEffects() const { update(0.f); return mEffects; } ActiveSpells::TIterator ActiveSpells::begin() const { return mSpells.begin(); } ActiveSpells::TIterator ActiveSpells::end() const { return mSpells.end(); } double ActiveSpells::timeToExpire (const TIterator& iterator) const { const std::vector& effects = iterator->second.mEffects; float duration = 0; for (std::vector::const_iterator iter (effects.begin()); iter!=effects.end(); ++iter) { if (iter->mTimeLeft > duration) duration = iter->mTimeLeft; } if (duration < 0) return 0; return duration; } bool ActiveSpells::isSpellActive(const std::string& id) const { for (TContainer::iterator iter = mSpells.begin(); iter != mSpells.end(); ++iter) { if (Misc::StringUtils::ciEqual(iter->first, id)) return true; } return false; } const ActiveSpells::TContainer& ActiveSpells::getActiveSpells() const { return mSpells; } /* Start of tes3mp change (major) Add a timestamp argument so spells received from other clients can have the same timestamps they had there, as well as a sendPacket argument used to prevent packets from being sent back to the server when we've just received them from it */ void ActiveSpells::addSpell(const std::string &id, bool stack, std::vector effects, const std::string &displayName, int casterActorId, MWWorld::TimeStamp timestamp, bool sendPacket) /* End of tes3mp change (major) */ { TContainer::iterator it(mSpells.find(id)); ActiveSpellParams params; params.mEffects = effects; params.mDisplayName = displayName; params.mCasterActorId = casterActorId; /* Start of tes3mp addition Track the timestamp of this active spell so that, if spells are stacked, the correct one can be removed */ params.mTimeStamp = timestamp; /* End of tes3mp addition */ if (it == end() || stack) { mSpells.insert(std::make_pair(id, params)); } else { // addSpell() is called with effects for a range. // but a spell may have effects with different ranges (e.g. Touch & Target) // so, if we see new effects for same spell assume additional // spell effects and add to existing effects of spell mergeEffects(params.mEffects, it->second.mEffects); it->second = params; } /* Start of tes3mp addition Whenever a player gains an active spell as a result of gameplay, send an ID_PLAYER_SPELLS_ACTIVE packet to the server with it */ if (sendPacket) { if (this == &MWMechanics::getPlayer().getClass().getCreatureStats(MWMechanics::getPlayer()).getActiveSpells()) { mwmp::Main::get().getLocalPlayer()->sendSpellsActiveAddition(id, stack, params); } else { MWWorld::Ptr actorPtr = MWBase::Environment::get().getWorld()->searchPtrViaActorId(getActorId()); if (mwmp::Main::get().getCellController()->isLocalActor(actorPtr)) mwmp::Main::get().getCellController()->getLocalActor(actorPtr)->sendSpellsActiveAddition(id, stack, params); } } /* End of tes3mp addition */ mSpellsChanged = true; } /* Start of tes3mp addition Declare addSpell() without the timestamp argument and make it call the version with that argument, using the current time for the timestamp */ void ActiveSpells::addSpell(const std::string& id, bool stack, std::vector effects, const std::string& displayName, int casterActorId) { MWWorld::TimeStamp timestamp = MWBase::Environment::get().getWorld()->getTimeStamp(); addSpell(id, stack, effects, displayName, casterActorId, timestamp); } /* End of tes3mp addition */ void ActiveSpells::mergeEffects(std::vector& addTo, const std::vector& from) { for (std::vector::const_iterator effect(from.begin()); effect != from.end(); ++effect) { // if effect is not in addTo, add it bool missing = true; for (std::vector::const_iterator iter(addTo.begin()); iter != addTo.end(); ++iter) { if ((effect->mEffectId == iter->mEffectId) && (effect->mArg == iter->mArg)) { missing = false; break; } } if (missing) { addTo.push_back(*effect); } } } void ActiveSpells::removeEffects(const std::string &id) { for (TContainer::iterator spell = mSpells.begin(); spell != mSpells.end(); ++spell) { if (spell->first == id) { spell->second.mEffects.clear(); mSpellsChanged = true; } } } /* Start of tes3mp addition Remove the spell with a certain ID and a certain timestamp, useful when there are stacked spells with the same ID Returns a boolean that indicates whether the corresponding spell was found */ bool ActiveSpells::removeSpellByTimestamp(const std::string& id, MWWorld::TimeStamp timestamp) { for (TContainer::iterator spell = mSpells.begin(); spell != mSpells.end(); ++spell) { if (spell->first == id) { if (spell->second.mTimeStamp == timestamp) { spell->second.mEffects.clear(); mSpellsChanged = true; return true; } } } return false; } /* End of tes3mp addition */ void ActiveSpells::visitEffectSources(EffectSourceVisitor &visitor) const { for (TContainer::const_iterator it = begin(); it != end(); ++it) { for (std::vector::const_iterator effectIt = it->second.mEffects.begin(); effectIt != it->second.mEffects.end(); ++effectIt) { std::string name = it->second.mDisplayName; float magnitude = effectIt->mMagnitude; if (magnitude) visitor.visit(MWMechanics::EffectKey(effectIt->mEffectId, effectIt->mArg), effectIt->mEffectIndex, name, it->first, it->second.mCasterActorId, magnitude, effectIt->mTimeLeft, effectIt->mDuration); } } } void ActiveSpells::purgeAll(float chance, bool spellOnly) { for (TContainer::iterator it = mSpells.begin(); it != mSpells.end(); ) { const std::string spellId = it->first; // if spellOnly is true, dispell only spells. Leave potions, enchanted items etc. if (spellOnly) { const ESM::Spell* spell = MWBase::Environment::get().getWorld()->getStore().get().search(spellId); if (!spell || spell->mData.mType != ESM::Spell::ST_Spell) { ++it; continue; } } if (Misc::Rng::roll0to99() < chance) mSpells.erase(it++); else ++it; } mSpellsChanged = true; } void ActiveSpells::purgeEffect(short effectId) { for (TContainer::iterator it = mSpells.begin(); it != mSpells.end(); ++it) { for (std::vector::iterator effectIt = it->second.mEffects.begin(); effectIt != it->second.mEffects.end();) { if (effectIt->mEffectId == effectId) effectIt = it->second.mEffects.erase(effectIt); else ++effectIt; } } mSpellsChanged = true; } void ActiveSpells::purgeEffect(short effectId, const std::string& sourceId, int effectIndex) { for (TContainer::iterator it = mSpells.begin(); it != mSpells.end(); ++it) { for (std::vector::iterator effectIt = it->second.mEffects.begin(); effectIt != it->second.mEffects.end();) { if (effectIt->mEffectId == effectId && it->first == sourceId && (effectIndex < 0 || effectIndex == effectIt->mEffectIndex)) effectIt = it->second.mEffects.erase(effectIt); else ++effectIt; } } mSpellsChanged = true; } void ActiveSpells::purge(int casterActorId) { for (TContainer::iterator it = mSpells.begin(); it != mSpells.end(); ++it) { for (std::vector::iterator effectIt = it->second.mEffects.begin(); effectIt != it->second.mEffects.end();) { if (it->second.mCasterActorId == casterActorId) effectIt = it->second.mEffects.erase(effectIt); else ++effectIt; } } mSpellsChanged = true; } /* Start of tes3mp addition Allow the purging of an effect for a specific arg (attribute or skill) */ void ActiveSpells::purgeEffectByArg(short effectId, int effectArg) { for (TContainer::iterator it = mSpells.begin(); it != mSpells.end(); ++it) { for (std::vector::iterator effectIt = it->second.mEffects.begin(); effectIt != it->second.mEffects.end();) { if (effectIt->mEffectId == effectId && effectIt->mArg == effectArg) effectIt = it->second.mEffects.erase(effectIt); else ++effectIt; } } mSpellsChanged = true; } /* End of tes3mp addition */ /* Start of tes3mp addition Make it easy to get an effect's duration */ float ActiveSpells::getEffectDuration(short effectId, std::string sourceId) { for (TContainer::iterator it = mSpells.begin(); it != mSpells.end(); ++it) { if (sourceId.compare(it->first) == 0) { for (std::vector::iterator effectIt = it->second.mEffects.begin(); effectIt != it->second.mEffects.end(); ++effectIt) { if (effectIt->mEffectId == effectId) return effectIt->mDuration; } } } return 0.f; } /* End of tes3mp addition */ void ActiveSpells::purgeCorprusDisease() { for (TContainer::iterator iter = mSpells.begin(); iter!=mSpells.end();) { bool hasCorprusEffect = false; for (std::vector::iterator effectIt = iter->second.mEffects.begin(); effectIt != iter->second.mEffects.end();++effectIt) { if (effectIt->mEffectId == ESM::MagicEffect::Corprus) { hasCorprusEffect = true; break; } } if (hasCorprusEffect) { mSpells.erase(iter++); mSpellsChanged = true; } else ++iter; } } void ActiveSpells::clear() { mSpells.clear(); mSpellsChanged = true; } void ActiveSpells::writeState(ESM::ActiveSpells &state) const { for (TContainer::const_iterator it = mSpells.begin(); it != mSpells.end(); ++it) { // Stupid copying of almost identical structures. ESM::TimeStamp <-> MWWorld::TimeStamp ESM::ActiveSpells::ActiveSpellParams params; params.mEffects = it->second.mEffects; params.mCasterActorId = it->second.mCasterActorId; params.mDisplayName = it->second.mDisplayName; state.mSpells.insert (std::make_pair(it->first, params)); } } void ActiveSpells::readState(const ESM::ActiveSpells &state) { for (ESM::ActiveSpells::TContainer::const_iterator it = state.mSpells.begin(); it != state.mSpells.end(); ++it) { // Stupid copying of almost identical structures. ESM::TimeStamp <-> MWWorld::TimeStamp ActiveSpellParams params; params.mEffects = it->second.mEffects; params.mCasterActorId = it->second.mCasterActorId; params.mDisplayName = it->second.mDisplayName; mSpells.insert (std::make_pair(it->first, params)); mSpellsChanged = true; } } /* Start of tes3mp addition Make it possible to set and get the actorId for these ActiveSpells */ int ActiveSpells::getActorId() const { return mActorId; } void ActiveSpells::setActorId(int actorId) { mActorId = actorId; } /* End of tes3mp addition */ } ================================================ FILE: apps/openmw/mwmechanics/activespells.hpp ================================================ #ifndef GAME_MWMECHANICS_ACTIVESPELLS_H #define GAME_MWMECHANICS_ACTIVESPELLS_H #include #include #include #include #include "../mwworld/timestamp.hpp" #include "magiceffects.hpp" namespace MWMechanics { /// \brief Lasting spell effects /// /// \note The name of this class is slightly misleading, since it also handels lasting potion /// effects. class ActiveSpells { public: typedef ESM::ActiveEffect ActiveEffect; struct ActiveSpellParams { std::vector mEffects; MWWorld::TimeStamp mTimeStamp; std::string mDisplayName; // The caster that inflicted this spell on us int mCasterActorId; }; typedef std::multimap TContainer; typedef TContainer::const_iterator TIterator; void readState (const ESM::ActiveSpells& state); void writeState (ESM::ActiveSpells& state) const; TIterator begin() const; TIterator end() const; void update(float duration) const; private: mutable TContainer mSpells; mutable MagicEffects mEffects; mutable bool mSpellsChanged; /* Start of tes3mp addition Track the actorId corresponding to these ActiveSpells */ int mActorId; /* End of tes3mp addition */ void rebuildEffects() const; /// Add any effects that are in "from" and not in "addTo" to "addTo" void mergeEffects(std::vector& addTo, const std::vector& from); double timeToExpire (const TIterator& iterator) const; ///< Returns time (in in-game hours) until the spell pointed to by \a iterator /// expires. const TContainer& getActiveSpells() const; public: ActiveSpells(); /// Add lasting effects /// /// \brief addSpell /// \param id ID for stacking purposes. /// \param stack If false, the spell is not added if one with the same ID exists already. /// \param effects /// \param displayName Name for display in magic menu. /// void addSpell (const std::string& id, bool stack, std::vector effects, const std::string& displayName, int casterActorId); /* Start of tes3mp addition Add a separate addSpell() with a timestamp argument */ void addSpell (const std::string& id, bool stack, std::vector effects, const std::string& displayName, int casterActorId, MWWorld::TimeStamp timestamp, bool sendPacket = true); /* End of tes3mp addition */ /// Removes the active effects from this spell/potion/.. with \a id void removeEffects (const std::string& id); /* Start of tes3mp addition Remove the spell with a certain ID and a certain timestamp, useful when there are stacked spells with the same ID */ bool removeSpellByTimestamp(const std::string& id, MWWorld::TimeStamp timestamp); /* End of tes3mp addition */ /// Remove all active effects with this effect id void purgeEffect (short effectId); /// Remove all active effects with this effect id and source id void purgeEffect (short effectId, const std::string& sourceId, int effectIndex=-1); /// Remove all active effects, if roll succeeds (for each effect) void purgeAll(float chance, bool spellOnly = false); /// Remove all effects with CASTER_LINKED flag that were cast by \a casterActorId void purge (int casterActorId); /* Start of tes3mp addition Allow the purging of an effect for a specific arg (attribute or skill) */ void purgeEffectByArg(short effectId, int effectArg); /* End of tes3mp addition */ /* Start of tes3mp addition Make it easy to get an effect's duration */ float getEffectDuration(short effectId, std::string sourceId); /* End of tes3mp addition */ /// Remove all spells void clear(); bool isSpellActive (const std::string& id) const; ///< case insensitive void purgeCorprusDisease(); const MagicEffects& getMagicEffects() const; void visitEffectSources (MWMechanics::EffectSourceVisitor& visitor) const; /* Start of tes3mp addition Make it possible to set and get the actorId for these ActiveSpells */ int getActorId() const; void setActorId(int actorId); /* End of tes3mp addition */ }; } #endif ================================================ FILE: apps/openmw/mwmechanics/actor.cpp ================================================ #include "actor.hpp" #include "character.hpp" namespace MWMechanics { Actor::Actor(const MWWorld::Ptr &ptr, MWRender::Animation *animation) { mCharacterController.reset(new CharacterController(ptr, animation)); } void Actor::updatePtr(const MWWorld::Ptr &newPtr) { mCharacterController->updatePtr(newPtr); } CharacterController* Actor::getCharacterController() { return mCharacterController.get(); } int Actor::getGreetingTimer() const { return mGreetingTimer; } void Actor::setGreetingTimer(int timer) { mGreetingTimer = timer; } float Actor::getAngleToPlayer() const { return mTargetAngleRadians; } void Actor::setAngleToPlayer(float angle) { mTargetAngleRadians = angle; } GreetingState Actor::getGreetingState() const { return mGreetingState; } void Actor::setGreetingState(GreetingState state) { mGreetingState = state; } bool Actor::isTurningToPlayer() const { return mIsTurningToPlayer; } void Actor::setTurningToPlayer(bool turning) { mIsTurningToPlayer = turning; } } ================================================ FILE: apps/openmw/mwmechanics/actor.hpp ================================================ #ifndef OPENMW_MECHANICS_ACTOR_H #define OPENMW_MECHANICS_ACTOR_H #include #include "../mwmechanics/actorutil.hpp" #include namespace MWRender { class Animation; } namespace MWWorld { class Ptr; } namespace MWMechanics { class CharacterController; /// @brief Holds temporary state for an actor that will be discarded when the actor leaves the scene. class Actor { public: Actor(const MWWorld::Ptr& ptr, MWRender::Animation* animation); /// Notify this actor of its new base object Ptr, use when the object changed cells void updatePtr(const MWWorld::Ptr& newPtr); CharacterController* getCharacterController(); int getGreetingTimer() const; void setGreetingTimer(int timer); float getAngleToPlayer() const; void setAngleToPlayer(float angle); GreetingState getGreetingState() const; void setGreetingState(GreetingState state); bool isTurningToPlayer() const; void setTurningToPlayer(bool turning); Misc::TimerStatus updateEngageCombatTimer(float duration) { return mEngageCombat.update(duration); } private: std::unique_ptr mCharacterController; int mGreetingTimer{0}; float mTargetAngleRadians{0.f}; GreetingState mGreetingState{Greet_None}; bool mIsTurningToPlayer{false}; Misc::DeviatingPeriodicTimer mEngageCombat{1.0f, 0.25f, Misc::Rng::deviate(0, 0.25f)}; }; } #endif ================================================ FILE: apps/openmw/mwmechanics/actors.cpp ================================================ #include "actors.hpp" #include #include #include #include #include #include #include #include /* Start of tes3mp addition Include additional headers for multiplayer purposes */ #include #include #include "../mwmp/Main.hpp" #include "../mwmp/Networking.hpp" #include "../mwmp/LocalPlayer.hpp" #include "../mwmp/PlayerList.hpp" #include "../mwmp/DedicatedPlayer.hpp" #include "../mwmp/CellController.hpp" #include "../mwmp/MechanicsHelper.hpp" #include "../mwmp/ObjectList.hpp" /* End of tes3mp addition */ #include "../mwworld/esmstore.hpp" #include "../mwworld/class.hpp" #include "../mwworld/inventorystore.hpp" #include "../mwworld/actionequip.hpp" #include "../mwworld/player.hpp" #include "../mwbase/world.hpp" #include "../mwbase/environment.hpp" #include "../mwbase/windowmanager.hpp" #include "../mwbase/dialoguemanager.hpp" #include "../mwbase/soundmanager.hpp" #include "../mwbase/mechanicsmanager.hpp" #include "../mwbase/statemanager.hpp" #include "../mwmechanics/aibreathe.hpp" #include "../mwrender/vismask.hpp" #include "spellcasting.hpp" #include "steering.hpp" #include "npcstats.hpp" #include "creaturestats.hpp" #include "movement.hpp" #include "character.hpp" #include "aicombat.hpp" #include "aicombataction.hpp" #include "aifollow.hpp" #include "aipursue.hpp" #include "aiwander.hpp" #include "actor.hpp" #include "summoning.hpp" #include "actorutil.hpp" #include "tickableeffects.hpp" namespace { bool isConscious(const MWWorld::Ptr& ptr) { const MWMechanics::CreatureStats& stats = ptr.getClass().getCreatureStats(ptr); return !stats.isDead() && !stats.getKnockedDown(); } int getBoundItemSlot (const std::string& itemId) { static std::map boundItemsMap; if (boundItemsMap.empty()) { std::string boundId = MWBase::Environment::get().getWorld()->getStore().get().find("sMagicBoundBootsID")->mValue.getString(); boundItemsMap[boundId] = MWWorld::InventoryStore::Slot_Boots; boundId = MWBase::Environment::get().getWorld()->getStore().get().find("sMagicBoundCuirassID")->mValue.getString(); boundItemsMap[boundId] = MWWorld::InventoryStore::Slot_Cuirass; boundId = MWBase::Environment::get().getWorld()->getStore().get().find("sMagicBoundLeftGauntletID")->mValue.getString(); boundItemsMap[boundId] = MWWorld::InventoryStore::Slot_LeftGauntlet; boundId = MWBase::Environment::get().getWorld()->getStore().get().find("sMagicBoundRightGauntletID")->mValue.getString(); boundItemsMap[boundId] = MWWorld::InventoryStore::Slot_RightGauntlet; boundId = MWBase::Environment::get().getWorld()->getStore().get().find("sMagicBoundHelmID")->mValue.getString(); boundItemsMap[boundId] = MWWorld::InventoryStore::Slot_Helmet; boundId = MWBase::Environment::get().getWorld()->getStore().get().find("sMagicBoundShieldID")->mValue.getString(); boundItemsMap[boundId] = MWWorld::InventoryStore::Slot_CarriedLeft; } int slot = MWWorld::InventoryStore::Slot_CarriedRight; std::map::iterator it = boundItemsMap.find(itemId); if (it != boundItemsMap.end()) slot = it->second; return slot; } class CheckActorCommanded : public MWMechanics::EffectSourceVisitor { MWWorld::Ptr mActor; public: bool mCommanded; CheckActorCommanded(const MWWorld::Ptr& actor) : mActor(actor) , mCommanded(false){} void visit (MWMechanics::EffectKey key, int effectIndex, const std::string& sourceName, const std::string& sourceId, int casterActorId, float magnitude, float remainingTime = -1, float totalTime = -1) override { if (((key.mId == ESM::MagicEffect::CommandHumanoid && mActor.getClass().isNpc()) || (key.mId == ESM::MagicEffect::CommandCreature && mActor.getTypeName() == typeid(ESM::Creature).name())) && magnitude >= mActor.getClass().getCreatureStats(mActor).getLevel()) mCommanded = true; } }; // Check for command effects having ended and remove package if necessary void adjustCommandedActor (const MWWorld::Ptr& actor) { CheckActorCommanded check(actor); MWMechanics::CreatureStats& stats = actor.getClass().getCreatureStats(actor); stats.getActiveSpells().visitEffectSources(check); bool hasCommandPackage = false; auto it = stats.getAiSequence().begin(); for (; it != stats.getAiSequence().end(); ++it) { if ((*it)->getTypeId() == MWMechanics::AiPackageTypeId::Follow && static_cast(it->get())->isCommanded()) { hasCommandPackage = true; break; } } if (!check.mCommanded && hasCommandPackage) stats.getAiSequence().erase(it); } void getRestorationPerHourOfSleep (const MWWorld::Ptr& ptr, float& health, float& magicka) { MWMechanics::CreatureStats& stats = ptr.getClass().getCreatureStats (ptr); const MWWorld::Store& settings = MWBase::Environment::get().getWorld()->getStore().get(); float endurance = stats.getAttribute (ESM::Attribute::Endurance).getModified (); health = 0.1f * endurance; float fRestMagicMult = settings.find("fRestMagicMult")->mValue.getFloat (); magicka = fRestMagicMult * stats.getAttribute(ESM::Attribute::Intelligence).getModified(); } template void forEachFollowingPackage(MWMechanics::Actors::PtrActorMap& actors, const MWWorld::Ptr& actor, const MWWorld::Ptr& player, T&& func) { for(auto& iter : actors) { const MWWorld::Ptr &iteratedActor = iter.first; if (iteratedActor == player || iteratedActor == actor) continue; const MWMechanics::CreatureStats &stats = iteratedActor.getClass().getCreatureStats(iteratedActor); if (stats.isDead()) continue; // An actor counts as following if AiFollow is the current AiPackage, // or there are only Combat and Wander packages before the AiFollow package for (const auto& package : stats.getAiSequence()) { if(!func(iter, package)) break; } } } } namespace MWMechanics { static const int GREETING_SHOULD_START = 4; // how many updates should pass before NPC can greet player static const int GREETING_SHOULD_END = 20; // how many updates should pass before NPC stops turning to player static const int GREETING_COOLDOWN = 40; // how many updates should pass before NPC can continue movement static const float DECELERATE_DISTANCE = 512.f; namespace { float getTimeToDestination(const AiPackage& package, const osg::Vec3f& position, float speed, float duration, const osg::Vec3f& halfExtents) { const auto distanceToNextPathPoint = (package.getNextPathPoint(package.getDestination()) - position).length(); return (distanceToNextPathPoint - package.getNextPathPointTolerance(speed, duration, halfExtents)) / speed; } } class GetStuntedMagickaDuration : public MWMechanics::EffectSourceVisitor { public: float mRemainingTime; GetStuntedMagickaDuration(const MWWorld::Ptr& actor) : mRemainingTime(0.f){} void visit (MWMechanics::EffectKey key, int effectIndex, const std::string& sourceName, const std::string& sourceId, int casterActorId, float magnitude, float remainingTime = -1, float totalTime = -1) override { if (mRemainingTime == -1) return; if (key.mId == ESM::MagicEffect::StuntedMagicka) { if (totalTime == -1) { mRemainingTime = -1; return; } if (remainingTime > mRemainingTime) mRemainingTime = remainingTime; } } }; class GetCurrentMagnitudes : public MWMechanics::EffectSourceVisitor { std::string mSpellId; public: GetCurrentMagnitudes(const std::string& spellId) : mSpellId(spellId) { } void visit (MWMechanics::EffectKey key, int effectIndex, const std::string& sourceName, const std::string& sourceId, int casterActorId, float magnitude, float remainingTime = -1, float totalTime = -1) override { if (magnitude <= 0) return; if (sourceId != mSpellId) return; mMagnitudes.emplace_back(key, magnitude); } std::vector> mMagnitudes; }; class GetCorprusSpells : public MWMechanics::EffectSourceVisitor { public: void visit (MWMechanics::EffectKey key, int effectIndex, const std::string& sourceName, const std::string& sourceId, int casterActorId, float magnitude, float remainingTime = -1, float totalTime = -1) override { if (key.mId != ESM::MagicEffect::Corprus) return; mSpells.push_back(sourceId); } std::vector mSpells; }; class SoulTrap : public MWMechanics::EffectSourceVisitor { MWWorld::Ptr mCreature; MWWorld::Ptr mActor; bool mTrapped; public: SoulTrap(const MWWorld::Ptr& trappedCreature) : mCreature(trappedCreature) , mTrapped(false) { } void visit (MWMechanics::EffectKey key, int effectIndex, const std::string& sourceName, const std::string& sourceId, int casterActorId, float magnitude, float remainingTime = -1, float totalTime = -1) override { if (mTrapped) return; if (key.mId != ESM::MagicEffect::Soultrap) return; if (magnitude <= 0) return; MWBase::World* world = MWBase::Environment::get().getWorld(); MWWorld::Ptr caster = world->searchPtrViaActorId(casterActorId); if (caster.isEmpty() || !caster.getClass().isActor()) return; static const float fSoulgemMult = world->getStore().get().find("fSoulgemMult")->mValue.getFloat(); int creatureSoulValue = mCreature.get()->mBase->mData.mSoul; if (creatureSoulValue == 0) return; // Use the smallest soulgem that is large enough to hold the soul MWWorld::ContainerStore& container = caster.getClass().getContainerStore(caster); MWWorld::ContainerStoreIterator gem = container.end(); float gemCapacity = std::numeric_limits::max(); std::string soulgemFilter = "misc_soulgem"; // no other way to check for soulgems? :/ for (MWWorld::ContainerStoreIterator it = container.begin(MWWorld::ContainerStore::Type_Miscellaneous); it != container.end(); ++it) { const std::string& id = it->getCellRef().getRefId(); if (id.size() >= soulgemFilter.size() && id.substr(0,soulgemFilter.size()) == soulgemFilter) { float thisGemCapacity = it->get()->mBase->mData.mValue * fSoulgemMult; if (thisGemCapacity >= creatureSoulValue && thisGemCapacity < gemCapacity && it->getCellRef().getSoul().empty()) { gem = it; gemCapacity = thisGemCapacity; } } } if (gem == container.end()) return; // Set the soul on just one of the gems, not the whole stack gem->getContainerStore()->unstack(*gem, caster); /* Start of tes3mp change (minor) Send PlayerInventory packets that replace the original gem with the new one */ mwmp::LocalPlayer *localPlayer = mwmp::Main::get().getLocalPlayer(); localPlayer->sendItemChange(*gem, 1, mwmp::InventoryChanges::REMOVE); gem->getCellRef().setSoul(mCreature.getCellRef().getRefId()); localPlayer->sendItemChange(*gem, 1, mwmp::InventoryChanges::ADD); /* End of tes3mp change (minor) */ // Restack the gem with other gems with the same soul gem->getContainerStore()->restack(*gem); mTrapped = true; if (caster == getPlayer()) MWBase::Environment::get().getWindowManager()->messageBox("#{sSoultrapSuccess}"); const ESM::Static* fx = MWBase::Environment::get().getWorld()->getStore().get() .search("VFX_Soul_Trap"); if (fx) MWBase::Environment::get().getWorld()->spawnEffect("meshes\\" + fx->mModel, "", mCreature.getRefData().getPosition().asVec3()); MWBase::Environment::get().getSoundManager()->playSound3D( mCreature.getRefData().getPosition().asVec3(), "conjuration hit", 1.f, 1.f ); } }; void Actors::addBoundItem (const std::string& itemId, const MWWorld::Ptr& actor) { MWWorld::InventoryStore& store = actor.getClass().getInventoryStore(actor); int slot = getBoundItemSlot(itemId); if (actor.getClass().getContainerStore(actor).count(itemId) != 0) return; MWWorld::ContainerStoreIterator prevItem = store.getSlot(slot); MWWorld::Ptr boundPtr = *store.MWWorld::ContainerStore::add(itemId, 1, actor); MWWorld::ActionEquip action(boundPtr); action.execute(actor); if (actor != MWMechanics::getPlayer()) return; MWWorld::Ptr newItem; auto it = store.getSlot(slot); // Equip can fail because beast races cannot equip boots/helmets if(it != store.end()) newItem = *it; if (newItem.isEmpty() || boundPtr != newItem) return; MWWorld::Player& player = MWBase::Environment::get().getWorld()->getPlayer(); // change draw state only if the item is in player's right hand if (slot == MWWorld::InventoryStore::Slot_CarriedRight) player.setDrawState(MWMechanics::DrawState_Weapon); if (prevItem != store.end()) player.setPreviousItem(itemId, prevItem->getCellRef().getRefId()); } void Actors::removeBoundItem (const std::string& itemId, const MWWorld::Ptr& actor) { MWWorld::InventoryStore& store = actor.getClass().getInventoryStore(actor); int slot = getBoundItemSlot(itemId); MWWorld::ContainerStoreIterator currentItem = store.getSlot(slot); bool wasEquipped = currentItem != store.end() && Misc::StringUtils::ciEqual(currentItem->getCellRef().getRefId(), itemId); if (actor != MWMechanics::getPlayer()) { store.remove(itemId, 1, actor); // Equip a replacement if (!wasEquipped) return; std::string type = currentItem->getTypeName(); if (type != typeid(ESM::Weapon).name() && type != typeid(ESM::Armor).name() && type != typeid(ESM::Clothing).name()) return; if (actor.getClass().getCreatureStats(actor).isDead()) return; if (!actor.getClass().hasInventoryStore(actor)) return; if (actor.getClass().isNpc() && actor.getClass().getNpcStats(actor).isWerewolf()) return; actor.getClass().getInventoryStore(actor).autoEquip(actor); return; } MWWorld::Player& player = MWBase::Environment::get().getWorld()->getPlayer(); std::string prevItemId = player.getPreviousItem(itemId); player.erasePreviousItem(itemId); if (!prevItemId.empty()) { // Find previous item (or its replacement) by id. // we should equip previous item only if expired bound item was equipped. MWWorld::Ptr item = store.findReplacement(prevItemId); if (!item.isEmpty() && wasEquipped) { MWWorld::ActionEquip action(item); action.execute(actor); } } store.remove(itemId, 1, actor); } void Actors::updateActor (const MWWorld::Ptr& ptr, float duration) { // magic effects adjustMagicEffects (ptr); if (ptr.getClass().getCreatureStats(ptr).needToRecalcDynamicStats()) calculateDynamicStats (ptr); calculateCreatureStatModifiers (ptr, duration); // fatigue restoration calculateRestoration(ptr, duration); } void Actors::updateHeadTracking(const MWWorld::Ptr& actor, const MWWorld::Ptr& targetActor, MWWorld::Ptr& headTrackTarget, float& sqrHeadTrackDistance, bool inCombatOrPursue) { if (!actor.getRefData().getBaseNode()) return; if (targetActor.getClass().getCreatureStats(targetActor).isDead()) return; static const float fMaxHeadTrackDistance = MWBase::Environment::get().getWorld()->getStore().get() .find("fMaxHeadTrackDistance")->mValue.getFloat(); static const float fInteriorHeadTrackMult = MWBase::Environment::get().getWorld()->getStore().get() .find("fInteriorHeadTrackMult")->mValue.getFloat(); float maxDistance = fMaxHeadTrackDistance; const ESM::Cell* currentCell = actor.getCell()->getCell(); if (!currentCell->isExterior() && !(currentCell->mData.mFlags & ESM::Cell::QuasiEx)) maxDistance *= fInteriorHeadTrackMult; const osg::Vec3f actor1Pos(actor.getRefData().getPosition().asVec3()); const osg::Vec3f actor2Pos(targetActor.getRefData().getPosition().asVec3()); float sqrDist = (actor1Pos - actor2Pos).length2(); if (sqrDist > std::min(maxDistance * maxDistance, sqrHeadTrackDistance) && !inCombatOrPursue) return; // stop tracking when target is behind the actor osg::Vec3f actorDirection = actor.getRefData().getBaseNode()->getAttitude() * osg::Vec3f(0,1,0); osg::Vec3f targetDirection(actor2Pos - actor1Pos); actorDirection.z() = 0; targetDirection.z() = 0; if ((actorDirection * targetDirection > 0 || inCombatOrPursue) && MWBase::Environment::get().getWorld()->getLOS(actor, targetActor) // check LOS and awareness last as it's the most expensive function && MWBase::Environment::get().getMechanicsManager()->awarenessCheck(targetActor, actor)) { sqrHeadTrackDistance = sqrDist; headTrackTarget = targetActor; } } void Actors::playIdleDialogue(const MWWorld::Ptr& actor) { if (!actor.getClass().isActor() || actor == getPlayer() || MWBase::Environment::get().getSoundManager()->sayActive(actor)) return; const CreatureStats &stats = actor.getClass().getCreatureStats(actor); if (stats.getAiSetting(CreatureStats::AI_Hello).getModified() == 0) return; const MWMechanics::AiSequence& seq = stats.getAiSequence(); if (seq.isInCombat() || seq.hasPackage(AiPackageTypeId::Follow) || seq.hasPackage(AiPackageTypeId::Escort)) return; const osg::Vec3f playerPos(getPlayer().getRefData().getPosition().asVec3()); const osg::Vec3f actorPos(actor.getRefData().getPosition().asVec3()); MWBase::World* world = MWBase::Environment::get().getWorld(); if (world->isSwimming(actor) || (playerPos - actorPos).length2() >= 3000 * 3000) return; // Our implementation is not FPS-dependent unlike Morrowind's so it needs to be recalibrated. // We chose to use the chance MW would have when run at 60 FPS with the default value of the GMST. const float delta = MWBase::Environment::get().getFrameDuration() * 6.f; static const float fVoiceIdleOdds = world->getStore().get().find("fVoiceIdleOdds")->mValue.getFloat(); if (Misc::Rng::rollProbability() * 10000.f < fVoiceIdleOdds * delta && world->getLOS(getPlayer(), actor)) MWBase::Environment::get().getDialogueManager()->say(actor, "idle"); } void Actors::updateMovementSpeed(const MWWorld::Ptr& actor) { if (mSmoothMovement) return; CreatureStats &stats = actor.getClass().getCreatureStats(actor); MWMechanics::AiSequence& seq = stats.getAiSequence(); if (!seq.isEmpty() && seq.getActivePackage().useVariableSpeed()) { osg::Vec3f targetPos = seq.getActivePackage().getDestination(); osg::Vec3f actorPos = actor.getRefData().getPosition().asVec3(); float distance = (targetPos - actorPos).length(); if (distance < DECELERATE_DISTANCE) { float speedCoef = std::max(0.7f, 0.2f + 0.8f * distance / DECELERATE_DISTANCE); auto& movement = actor.getClass().getMovementSettings(actor); movement.mPosition[0] *= speedCoef; movement.mPosition[1] *= speedCoef; } } } void Actors::updateGreetingState(const MWWorld::Ptr& actor, Actor& actorState, bool turnOnly) { if (!actor.getClass().isActor() || actor == getPlayer()) return; CreatureStats &stats = actor.getClass().getCreatureStats(actor); const MWMechanics::AiSequence& seq = stats.getAiSequence(); const auto packageId = seq.getTypeId(); if (seq.isInCombat() || MWBase::Environment::get().getWorld()->isSwimming(actor) || (packageId != AiPackageTypeId::Wander && packageId != AiPackageTypeId::Travel && packageId != AiPackageTypeId::None)) { actorState.setTurningToPlayer(false); actorState.setGreetingTimer(0); actorState.setGreetingState(Greet_None); return; } MWWorld::Ptr player = getPlayer(); osg::Vec3f playerPos(player.getRefData().getPosition().asVec3()); osg::Vec3f actorPos(actor.getRefData().getPosition().asVec3()); osg::Vec3f dir = playerPos - actorPos; if (actorState.isTurningToPlayer()) { // Reduce the turning animation glitch by using a *HUGE* value of // epsilon... TODO: a proper fix might be in either the physics or the // animation subsystem if (zTurn(actor, actorState.getAngleToPlayer(), osg::DegreesToRadians(5.f))) { actorState.setTurningToPlayer(false); // An original engine launches an endless idle2 when an actor greets player. playAnimationGroup (actor, "idle2", 0, std::numeric_limits::max(), false); } } if (turnOnly) return; // Play a random voice greeting if the player gets too close static int iGreetDistanceMultiplier = MWBase::Environment::get().getWorld()->getStore() .get().find("iGreetDistanceMultiplier")->mValue.getInteger(); float helloDistance = static_cast(stats.getAiSetting(CreatureStats::AI_Hello).getModified() * iGreetDistanceMultiplier); int greetingTimer = actorState.getGreetingTimer(); GreetingState greetingState = actorState.getGreetingState(); if (greetingState == Greet_None) { if ((playerPos - actorPos).length2() <= helloDistance*helloDistance && !player.getClass().getCreatureStats(player).isDead() && !actor.getClass().getCreatureStats(actor).isParalyzed() && MWBase::Environment::get().getWorld()->getLOS(player, actor) && MWBase::Environment::get().getMechanicsManager()->awarenessCheck(player, actor)) greetingTimer++; if (greetingTimer >= GREETING_SHOULD_START) { greetingState = Greet_InProgress; MWBase::Environment::get().getDialogueManager()->say(actor, "hello"); greetingTimer = 0; } } if (greetingState == Greet_InProgress) { greetingTimer++; if (!stats.getMovementFlag(CreatureStats::Flag_ForceJump) && !stats.getMovementFlag(CreatureStats::Flag_ForceSneak) && (greetingTimer <= GREETING_SHOULD_END || MWBase::Environment::get().getSoundManager()->sayActive(actor))) turnActorToFacePlayer(actor, actorState, dir); if (greetingTimer >= GREETING_COOLDOWN) { greetingState = Greet_Done; greetingTimer = 0; } } if (greetingState == Greet_Done) { float resetDist = 2 * helloDistance; if ((playerPos - actorPos).length2() >= resetDist*resetDist) greetingState = Greet_None; } actorState.setGreetingTimer(greetingTimer); actorState.setGreetingState(greetingState); } void Actors::turnActorToFacePlayer(const MWWorld::Ptr& actor, Actor& actorState, const osg::Vec3f& dir) { actor.getClass().getMovementSettings(actor).mPosition[1] = 0; actor.getClass().getMovementSettings(actor).mPosition[0] = 0; if (!actorState.isTurningToPlayer()) { float from = dir.x(); float to = dir.y(); float angle = std::atan2(from, to); actorState.setAngleToPlayer(angle); float deltaAngle = Misc::normalizeAngle(angle - actor.getRefData().getPosition().rot[2]); if (!mSmoothMovement || std::abs(deltaAngle) > osg::DegreesToRadians(60.f)) actorState.setTurningToPlayer(true); } } void Actors::engageCombat (const MWWorld::Ptr& actor1, const MWWorld::Ptr& actor2, std::map >& cachedAllies, bool againstPlayer) { // No combat for totally static creatures if (!actor1.getClass().isMobile(actor1)) return; CreatureStats& creatureStats1 = actor1.getClass().getCreatureStats(actor1); if (creatureStats1.isDead() || creatureStats1.getAiSequence().isInCombat(actor2)) return; const CreatureStats& creatureStats2 = actor2.getClass().getCreatureStats(actor2); if (creatureStats2.isDead()) return; const osg::Vec3f actor1Pos(actor1.getRefData().getPosition().asVec3()); const osg::Vec3f actor2Pos(actor2.getRefData().getPosition().asVec3()); float sqrDist = (actor1Pos - actor2Pos).length2(); if (sqrDist > mActorsProcessingRange*mActorsProcessingRange) return; // If this is set to true, actor1 will start combat with actor2 if the awareness check at the end of the method returns true bool aggressive = false; // Get actors allied with actor1. Includes those following or escorting actor1, actors following or escorting those actors, (recursive) // and any actor currently being followed or escorted by actor1 std::set allies1; getActorsSidingWith(actor1, allies1, cachedAllies); // If an ally of actor1 has been attacked by actor2 or has attacked actor2, start combat between actor1 and actor2 for (const MWWorld::Ptr &ally : allies1) { if (creatureStats1.getAiSequence().isInCombat(ally)) continue; if (creatureStats2.matchesActorId(ally.getClass().getCreatureStats(ally).getHitAttemptActorId())) { MWBase::Environment::get().getMechanicsManager()->startCombat(actor1, actor2); // Also set the same hit attempt actor. Otherwise, if fighting the player, they may stop combat // if the player gets out of reach, while the ally would continue combat with the player creatureStats1.setHitAttemptActorId(ally.getClass().getCreatureStats(ally).getHitAttemptActorId()); return; } // If there's been no attack attempt yet but an ally of actor1 is in combat with actor2, become aggressive to actor2 if (ally.getClass().getCreatureStats(ally).getAiSequence().isInCombat(actor2)) aggressive = true; } std::set playerAllies; getActorsSidingWith(MWMechanics::getPlayer(), playerAllies, cachedAllies); bool isPlayerFollowerOrEscorter = playerAllies.find(actor1) != playerAllies.end(); // If actor2 and at least one actor2 are in combat with actor1, actor1 and its allies start combat with them // Doesn't apply for player followers/escorters if (!aggressive && !isPlayerFollowerOrEscorter) { // Check that actor2 is in combat with actor1 if (actor2.getClass().getCreatureStats(actor2).getAiSequence().isInCombat(actor1)) { std::set allies2; getActorsSidingWith(actor2, allies2, cachedAllies); // Check that an ally of actor2 is also in combat with actor1 for (const MWWorld::Ptr &ally2 : allies2) { if (ally2.getClass().getCreatureStats(ally2).getAiSequence().isInCombat(actor1)) { MWBase::Environment::get().getMechanicsManager()->startCombat(actor1, actor2); // Also have actor1's allies start combat for (const MWWorld::Ptr& ally1 : allies1) MWBase::Environment::get().getMechanicsManager()->startCombat(ally1, actor2); return; } } } } // Stop here if target is unreachable if (!canFight(actor1, actor2)) return; // If set in the settings file, player followers and escorters will become aggressive toward enemies in combat with them or the player static const bool followersAttackOnSight = Settings::Manager::getBool("followers attack on sight", "Game"); if (!aggressive && isPlayerFollowerOrEscorter && followersAttackOnSight) { if (actor2.getClass().getCreatureStats(actor2).getAiSequence().isInCombat(actor1)) aggressive = true; else { for (const MWWorld::Ptr &ally : allies1) { if (actor2.getClass().getCreatureStats(actor2).getAiSequence().isInCombat(ally)) { aggressive = true; break; } } } } // Do aggression check if actor2 is the player or a player follower or escorter if (!aggressive) { if (againstPlayer || playerAllies.find(actor2) != playerAllies.end()) { // Player followers and escorters with high fight should not initiate combat with the player or with // other player followers or escorters if (!isPlayerFollowerOrEscorter) aggressive = MWBase::Environment::get().getMechanicsManager()->isAggressive(actor1, actor2); } /* Start of tes3mp addition Make aggressive actors initiate combat with DedicatedPlayers */ else if (mwmp::PlayerList::isDedicatedPlayer(actor2)) { aggressive = MWBase::Environment::get().getMechanicsManager()->isAggressive(actor1, actor2); } /* End of tes3mp addition */ } // Make guards go aggressive with creatures that are in combat, unless the creature is a follower or escorter if (!aggressive && actor1.getClass().isClass(actor1, "Guard") && !actor2.getClass().isNpc() && creatureStats2.getAiSequence().isInCombat()) { // Check if the creature is too far static const float fAlarmRadius = MWBase::Environment::get().getWorld()->getStore().get().find("fAlarmRadius")->mValue.getFloat(); if (sqrDist > fAlarmRadius * fAlarmRadius) return; bool followerOrEscorter = false; for (const auto& package : creatureStats2.getAiSequence()) { // The follow package must be first or have nothing but combat before it if (package->sideWithTarget()) { followerOrEscorter = true; break; } else if (package->getTypeId() != MWMechanics::AiPackageTypeId::Combat) break; } if (!followerOrEscorter) aggressive = true; } // If any of the above conditions turned actor1 aggressive towards actor2, do an awareness check. If it passes, start combat with actor2. if (aggressive) { bool LOS = MWBase::Environment::get().getWorld()->getLOS(actor1, actor2) && MWBase::Environment::get().getMechanicsManager()->awarenessCheck(actor2, actor1); if (LOS) MWBase::Environment::get().getMechanicsManager()->startCombat(actor1, actor2); } } void Actors::adjustMagicEffects (const MWWorld::Ptr& creature) { CreatureStats& creatureStats = creature.getClass().getCreatureStats (creature); if (creatureStats.isDeathAnimationFinished()) return; MagicEffects now = creatureStats.getSpells().getMagicEffects(); if (creature.getClass().hasInventoryStore(creature)) { MWWorld::InventoryStore& store = creature.getClass().getInventoryStore (creature); now += store.getMagicEffects(); } now += creatureStats.getActiveSpells().getMagicEffects(); creatureStats.modifyMagicEffects(now); } void Actors::calculateDynamicStats (const MWWorld::Ptr& ptr) { CreatureStats& creatureStats = ptr.getClass().getCreatureStats (ptr); float intelligence = creatureStats.getAttribute(ESM::Attribute::Intelligence).getModified(); float base = 1.f; if (ptr == getPlayer()) base = MWBase::Environment::get().getWorld()->getStore().get().find("fPCbaseMagickaMult")->mValue.getFloat(); else base = MWBase::Environment::get().getWorld()->getStore().get().find("fNPCbaseMagickaMult")->mValue.getFloat(); double magickaFactor = base + creatureStats.getMagicEffects().get (EffectKey (ESM::MagicEffect::FortifyMaximumMagicka)).getMagnitude() * 0.1; DynamicStat magicka = creatureStats.getMagicka(); float diff = (static_cast(magickaFactor*intelligence)) - magicka.getBase(); float currentToBaseRatio = (magicka.getCurrent() / magicka.getBase()); magicka.setModified(magicka.getModified() + diff, 0); magicka.setCurrent(magicka.getBase() * currentToBaseRatio, false, true); creatureStats.setMagicka(magicka); } void Actors::restoreDynamicStats (const MWWorld::Ptr& ptr, double hours, bool sleep) { MWMechanics::CreatureStats& stats = ptr.getClass().getCreatureStats (ptr); if (stats.isDead()) return; const MWWorld::Store& settings = MWBase::Environment::get().getWorld()->getStore().get(); if (sleep) { float health, magicka; getRestorationPerHourOfSleep(ptr, health, magicka); DynamicStat stat = stats.getHealth(); stat.setCurrent(stat.getCurrent() + health * hours); stats.setHealth(stat); double restoreHours = hours; bool stunted = stats.getMagicEffects ().get(ESM::MagicEffect::StuntedMagicka).getMagnitude() > 0; if (stunted) { // Stunted Magicka effect should be taken into account. GetStuntedMagickaDuration visitor(ptr); stats.getActiveSpells().visitEffectSources(visitor); stats.getSpells().visitEffectSources(visitor); if (ptr.getClass().hasInventoryStore(ptr)) ptr.getClass().getInventoryStore(ptr).visitEffectSources(visitor); // Take a maximum remaining duration of Stunted Magicka effects (-1 is a constant one) in game hours. if (visitor.mRemainingTime > 0) { double timeScale = MWBase::Environment::get().getWorld()->getTimeScaleFactor(); if(timeScale == 0.0) timeScale = 1; restoreHours = std::max(0.0, hours - visitor.mRemainingTime * timeScale / 3600.f); } else if (visitor.mRemainingTime == -1) restoreHours = 0; } if (restoreHours > 0) { stat = stats.getMagicka(); stat.setCurrent(stat.getCurrent() + magicka * restoreHours); stats.setMagicka(stat); } } // Current fatigue can be above base value due to a fortify effect. // In that case stop here and don't try to restore. DynamicStat fatigue = stats.getFatigue(); if (fatigue.getCurrent() >= fatigue.getBase()) return; // Restore fatigue float fFatigueReturnBase = settings.find("fFatigueReturnBase")->mValue.getFloat (); float fFatigueReturnMult = settings.find("fFatigueReturnMult")->mValue.getFloat (); float fEndFatigueMult = settings.find("fEndFatigueMult")->mValue.getFloat (); float endurance = stats.getAttribute (ESM::Attribute::Endurance).getModified (); float normalizedEncumbrance = ptr.getClass().getNormalizedEncumbrance(ptr); if (normalizedEncumbrance > 1) normalizedEncumbrance = 1; float x = fFatigueReturnBase + fFatigueReturnMult * (1 - normalizedEncumbrance); x *= fEndFatigueMult * endurance; fatigue.setCurrent (fatigue.getCurrent() + 3600 * x * hours); stats.setFatigue (fatigue); } void Actors::calculateRestoration (const MWWorld::Ptr& ptr, float duration) { if (ptr.getClass().getCreatureStats(ptr).isDead()) return; MWMechanics::CreatureStats& stats = ptr.getClass().getCreatureStats (ptr); // Current fatigue can be above base value due to a fortify effect. // In that case stop here and don't try to restore. DynamicStat fatigue = stats.getFatigue(); if (fatigue.getCurrent() >= fatigue.getBase()) return; // Restore fatigue float endurance = stats.getAttribute(ESM::Attribute::Endurance).getModified(); const MWWorld::Store& settings = MWBase::Environment::get().getWorld()->getStore().get(); static const float fFatigueReturnBase = settings.find("fFatigueReturnBase")->mValue.getFloat (); static const float fFatigueReturnMult = settings.find("fFatigueReturnMult")->mValue.getFloat (); float x = fFatigueReturnBase + fFatigueReturnMult * endurance; fatigue.setCurrent (fatigue.getCurrent() + duration * x); stats.setFatigue (fatigue); } class ExpiryVisitor : public EffectSourceVisitor { private: MWWorld::Ptr mActor; float mDuration; public: ExpiryVisitor(const MWWorld::Ptr& actor, float duration) : mActor(actor), mDuration(duration) { } void visit (MWMechanics::EffectKey key, int /*effectIndex*/, const std::string& /*sourceName*/, const std::string& /*sourceId*/, int /*casterActorId*/, float magnitude, float remainingTime = -1, float /*totalTime*/ = -1) override { if (magnitude > 0 && remainingTime > 0 && remainingTime < mDuration) { CreatureStats& creatureStats = mActor.getClass().getCreatureStats(mActor); if (effectTick(creatureStats, mActor, key, magnitude * remainingTime)) creatureStats.getMagicEffects().add(key, -magnitude); } } }; void Actors::applyCureEffects(const MWWorld::Ptr& actor) { CreatureStats &creatureStats = actor.getClass().getCreatureStats(actor); const MagicEffects &effects = creatureStats.getMagicEffects(); if (effects.get(ESM::MagicEffect::CurePoison).getModifier() > 0) { creatureStats.getActiveSpells().purgeEffect(ESM::MagicEffect::Poison); creatureStats.getSpells().purgeEffect(ESM::MagicEffect::Poison); if (actor.getClass().hasInventoryStore(actor)) actor.getClass().getInventoryStore(actor).purgeEffect(ESM::MagicEffect::Poison); } if (effects.get(ESM::MagicEffect::CureParalyzation).getModifier() > 0) { creatureStats.getActiveSpells().purgeEffect(ESM::MagicEffect::Paralyze); creatureStats.getSpells().purgeEffect(ESM::MagicEffect::Paralyze); if (actor.getClass().hasInventoryStore(actor)) actor.getClass().getInventoryStore(actor).purgeEffect(ESM::MagicEffect::Paralyze); } if (effects.get(ESM::MagicEffect::CureCommonDisease).getModifier() > 0) { creatureStats.getSpells().purgeCommonDisease(); } if (effects.get(ESM::MagicEffect::CureBlightDisease).getModifier() > 0) { creatureStats.getSpells().purgeBlightDisease(); } if (effects.get(ESM::MagicEffect::CureCorprusDisease).getModifier() > 0) { creatureStats.getActiveSpells().purgeCorprusDisease(); creatureStats.getSpells().purgeCorprusDisease(); if (actor.getClass().hasInventoryStore(actor)) actor.getClass().getInventoryStore(actor).purgeEffect(ESM::MagicEffect::Corprus, true); } if (effects.get(ESM::MagicEffect::RemoveCurse).getModifier() > 0) { creatureStats.getSpells().purgeCurses(); } } void Actors::calculateCreatureStatModifiers (const MWWorld::Ptr& ptr, float duration) { CreatureStats &creatureStats = ptr.getClass().getCreatureStats(ptr); const MagicEffects &effects = creatureStats.getMagicEffects(); bool godmode = ptr == getPlayer() && MWBase::Environment::get().getWorld()->getGodModeState(); applyCureEffects(ptr); bool wasDead = creatureStats.isDead(); if (duration > 0) { // Apply correct magnitude for tickable effects that have just expired, // in case duration > remaining time of effect. // One case where this will happen is when the player uses the rest/wait command // while there is a tickable effect active that should expire before the end of the rest/wait. ExpiryVisitor visitor(ptr, duration); creatureStats.getActiveSpells().visitEffectSources(visitor); for (MagicEffects::Collection::const_iterator it = effects.begin(); it != effects.end(); ++it) { // tickable effects (i.e. effects having a lasting impact after expiry) effectTick(creatureStats, ptr, it->first, it->second.getMagnitude() * duration); // instant effects are already applied on spell impact in spellcasting.cpp, but may also come from permanent abilities if (it->second.getMagnitude() > 0) { CastSpell cast(ptr, ptr); if (cast.applyInstantEffect(ptr, ptr, it->first, it->second.getMagnitude())) { creatureStats.getSpells().purgeEffect(it->first.mId); creatureStats.getActiveSpells().purgeEffect(it->first.mId); if (ptr.getClass().hasInventoryStore(ptr)) ptr.getClass().getInventoryStore(ptr).purgeEffect(it->first.mId); } } } } // purge levitate effect if levitation is disabled // check only modifier, because base value can be setted from SetFlying console command. if (MWBase::Environment::get().getWorld()->isLevitationEnabled() == false && effects.get(ESM::MagicEffect::Levitate).getModifier() > 0) { creatureStats.getSpells().purgeEffect(ESM::MagicEffect::Levitate); creatureStats.getActiveSpells().purgeEffect(ESM::MagicEffect::Levitate); if (ptr.getClass().hasInventoryStore(ptr)) ptr.getClass().getInventoryStore(ptr).purgeEffect(ESM::MagicEffect::Levitate); if (ptr == getPlayer()) { MWBase::Environment::get().getWindowManager()->messageBox ("#{sLevitateDisabled}"); } } // dynamic stats for (int i = 0; i < 3; ++i) { DynamicStat stat = creatureStats.getDynamic(i); float fortify = effects.get(ESM::MagicEffect::FortifyHealth + i).getMagnitude(); float drain = 0.f; if (!godmode) drain = effects.get(ESM::MagicEffect::DrainHealth + i).getMagnitude(); stat.setCurrentModifier(fortify - drain, // Magicka can be decreased below zero due to a fortify effect wearing off // Fatigue can be decreased below zero meaning the actor will be knocked out i == 1 || i == 2); creatureStats.setDynamic(i, stat); } // attributes for(int i = 0;i < ESM::Attribute::Length;++i) { AttributeValue stat = creatureStats.getAttribute(i); float fortify = effects.get(EffectKey(ESM::MagicEffect::FortifyAttribute, i)).getMagnitude(); float drain = 0.f, absorb = 0.f; if (!godmode) { drain = effects.get(EffectKey(ESM::MagicEffect::DrainAttribute, i)).getMagnitude(); absorb = effects.get(EffectKey(ESM::MagicEffect::AbsorbAttribute, i)).getMagnitude(); } stat.setModifier(static_cast(fortify - drain - absorb)); creatureStats.setAttribute(i, stat); } if (creatureStats.needToRecalcDynamicStats()) calculateDynamicStats(ptr); if (ptr == getPlayer()) { GetCorprusSpells getCorprusSpellsVisitor; creatureStats.getSpells().visitEffectSources(getCorprusSpellsVisitor); creatureStats.getActiveSpells().visitEffectSources(getCorprusSpellsVisitor); ptr.getClass().getInventoryStore(ptr).visitEffectSources(getCorprusSpellsVisitor); std::vector corprusSpells = getCorprusSpellsVisitor.mSpells; std::vector corprusSpellsToRemove; for (auto it = creatureStats.getCorprusSpells().begin(); it != creatureStats.getCorprusSpells().end(); ++it) { if(std::find(corprusSpells.begin(), corprusSpells.end(), it->first) == corprusSpells.end()) { // Corprus effect expired, remove entry and restore stats. MWBase::Environment::get().getMechanicsManager()->restoreStatsAfterCorprus(ptr, it->first); corprusSpellsToRemove.push_back(it->first); corprusSpells.erase(std::remove(corprusSpells.begin(), corprusSpells.end(), it->first), corprusSpells.end()); continue; } corprusSpells.erase(std::remove(corprusSpells.begin(), corprusSpells.end(), it->first), corprusSpells.end()); if (MWBase::Environment::get().getWorld()->getTimeStamp() >= it->second.mNextWorsening) { it->second.mNextWorsening += CorprusStats::sWorseningPeriod; GetCurrentMagnitudes getMagnitudesVisitor (it->first); creatureStats.getSpells().visitEffectSources(getMagnitudesVisitor); creatureStats.getActiveSpells().visitEffectSources(getMagnitudesVisitor); ptr.getClass().getInventoryStore(ptr).visitEffectSources(getMagnitudesVisitor); for (auto& effectMagnitude : getMagnitudesVisitor.mMagnitudes) { if (effectMagnitude.first.mId == ESM::MagicEffect::FortifyAttribute) { AttributeValue attr = creatureStats.getAttribute(effectMagnitude.first.mArg); attr.damage(-effectMagnitude.second); creatureStats.setAttribute(effectMagnitude.first.mArg, attr); it->second.mWorsenings[effectMagnitude.first.mArg] = 0; } else if (effectMagnitude.first.mId == ESM::MagicEffect::DrainAttribute) { AttributeValue attr = creatureStats.getAttribute(effectMagnitude.first.mArg); int currentDamage = attr.getDamage(); if (currentDamage >= 0) it->second.mWorsenings[effectMagnitude.first.mArg] = std::min(it->second.mWorsenings[effectMagnitude.first.mArg], currentDamage); it->second.mWorsenings[effectMagnitude.first.mArg] += effectMagnitude.second; attr.damage(effectMagnitude.second); creatureStats.setAttribute(effectMagnitude.first.mArg, attr); } } MWBase::Environment::get().getWindowManager()->messageBox("#{sMagicCorprusWorsens}"); } } for (std::string& oldCorprusSpell : corprusSpellsToRemove) { creatureStats.removeCorprusSpell(oldCorprusSpell); } for (std::string& newCorprusSpell : corprusSpells) { CorprusStats corprus; for (int i=0; igetTimeStamp() + CorprusStats::sWorseningPeriod; creatureStats.addCorprusSpell(newCorprusSpell, corprus); } } // AI setting modifiers int creature = !ptr.getClass().isNpc(); if (creature && ptr.get()->mBase->mData.mType == ESM::Creature::Humanoid) creature = false; // Note: the Creature variants only work on normal creatures, not on daedra or undead creatures. if (!creature || ptr.get()->mBase->mData.mType == ESM::Creature::Creatures) { Stat stat = creatureStats.getAiSetting(CreatureStats::AI_Fight); stat.setModifier(static_cast(effects.get(ESM::MagicEffect::FrenzyHumanoid + creature).getMagnitude() - effects.get(ESM::MagicEffect::CalmHumanoid+creature).getMagnitude())); creatureStats.setAiSetting(CreatureStats::AI_Fight, stat); stat = creatureStats.getAiSetting(CreatureStats::AI_Flee); stat.setModifier(static_cast(effects.get(ESM::MagicEffect::DemoralizeHumanoid + creature).getMagnitude() - effects.get(ESM::MagicEffect::RallyHumanoid+creature).getMagnitude())); creatureStats.setAiSetting(CreatureStats::AI_Flee, stat); } if (creature && ptr.get()->mBase->mData.mType == ESM::Creature::Undead) { Stat stat = creatureStats.getAiSetting(CreatureStats::AI_Flee); stat.setModifier(static_cast(effects.get(ESM::MagicEffect::TurnUndead).getMagnitude())); creatureStats.setAiSetting(CreatureStats::AI_Flee, stat); } if (!wasDead && creatureStats.isDead()) { // The actor was killed by a magic effect. Figure out if the player was responsible for it. const ActiveSpells& spells = creatureStats.getActiveSpells(); MWWorld::Ptr player = getPlayer(); std::set playerFollowers; getActorsSidingWith(player, playerFollowers); for (ActiveSpells::TIterator it = spells.begin(); it != spells.end(); ++it) { bool actorKilled = false; const ActiveSpells::ActiveSpellParams& spell = it->second; MWWorld::Ptr caster = MWBase::Environment::get().getWorld()->searchPtrViaActorId(spell.mCasterActorId); for (std::vector::const_iterator effectIt = spell.mEffects.begin(); effectIt != spell.mEffects.end(); ++effectIt) { int effectId = effectIt->mEffectId; bool isDamageEffect = false; int damageEffects[] = { ESM::MagicEffect::FireDamage, ESM::MagicEffect::ShockDamage, ESM::MagicEffect::FrostDamage, ESM::MagicEffect::Poison, ESM::MagicEffect::SunDamage, ESM::MagicEffect::DamageHealth, ESM::MagicEffect::AbsorbHealth }; for (unsigned int i=0; ikiller = killer; } else if (mwmp::Main::get().getCellController()->isLocalActor(ptr)) { mwmp::Main::get().getCellController()->getLocalActor(ptr)->killer = killer; } /* End of tes3mp addition */ if (caster == player || playerFollowers.find(caster) != playerFollowers.end()) { if (caster.getClass().isNpc() && caster.getClass().getNpcStats(caster).isWerewolf()) caster.getClass().getNpcStats(caster).addWerewolfKill(); MWBase::Environment::get().getMechanicsManager()->actorKilled(ptr, player); actorKilled = true; break; } } } if (actorKilled) break; } } // TODO: dirty flag for magic effects to avoid some unnecessary work below? // any value of calm > 0 will stop the actor from fighting if ((effects.get(ESM::MagicEffect::CalmHumanoid).getMagnitude() > 0 && ptr.getClass().isNpc()) || (effects.get(ESM::MagicEffect::CalmCreature).getMagnitude() > 0 && !ptr.getClass().isNpc())) creatureStats.getAiSequence().stopCombat(); // Update bound effects // Note: in vanilla MW multiple bound items of the same type can be created by different spells. // As these extra copies are kinda useless this may or may not be important. static std::map boundItemsMap; if (boundItemsMap.empty()) { boundItemsMap[ESM::MagicEffect::BoundBattleAxe] = "sMagicBoundBattleAxeID"; boundItemsMap[ESM::MagicEffect::BoundBoots] = "sMagicBoundBootsID"; boundItemsMap[ESM::MagicEffect::BoundCuirass] = "sMagicBoundCuirassID"; boundItemsMap[ESM::MagicEffect::BoundDagger] = "sMagicBoundDaggerID"; boundItemsMap[ESM::MagicEffect::BoundGloves] = "sMagicBoundLeftGauntletID"; // Note: needs RightGauntlet variant too (see below) boundItemsMap[ESM::MagicEffect::BoundHelm] = "sMagicBoundHelmID"; boundItemsMap[ESM::MagicEffect::BoundLongbow] = "sMagicBoundLongbowID"; boundItemsMap[ESM::MagicEffect::BoundLongsword] = "sMagicBoundLongswordID"; boundItemsMap[ESM::MagicEffect::BoundMace] = "sMagicBoundMaceID"; boundItemsMap[ESM::MagicEffect::BoundShield] = "sMagicBoundShieldID"; boundItemsMap[ESM::MagicEffect::BoundSpear] = "sMagicBoundSpearID"; } if(ptr.getClass().hasInventoryStore(ptr)) { for (const auto& [effect, itemGmst] : boundItemsMap) { bool found = creatureStats.mBoundItems.find(effect) != creatureStats.mBoundItems.end(); float magnitude = effects.get(effect).getMagnitude(); if (found != (magnitude > 0)) { if (magnitude > 0) creatureStats.mBoundItems.insert(effect); else creatureStats.mBoundItems.erase(effect); std::string item = MWBase::Environment::get().getWorld()->getStore().get().find( itemGmst)->mValue.getString(); magnitude > 0 ? addBoundItem(item, ptr) : removeBoundItem(item, ptr); if (effect == ESM::MagicEffect::BoundGloves) { item = MWBase::Environment::get().getWorld()->getStore().get().find( "sMagicBoundRightGauntletID")->mValue.getString(); magnitude > 0 ? addBoundItem(item, ptr) : removeBoundItem(item, ptr); } } } } // Summoned creature update visitor assumes the actor belongs to a cell. // This assumption isn't always valid for the player character. if (!ptr.isInCell()) return; bool hasSummonEffect = false; for (MagicEffects::Collection::const_iterator it = effects.begin(); it != effects.end(); ++it) { if (isSummoningEffect(it->first.mId)) { hasSummonEffect = true; break; } } if (!creatureStats.getSummonedCreatureMap().empty() || !creatureStats.getSummonedCreatureGraveyard().empty() || hasSummonEffect) { UpdateSummonedCreatures updateSummonedCreatures(ptr); creatureStats.getActiveSpells().visitEffectSources(updateSummonedCreatures); creatureStats.getSpells().visitEffectSources(updateSummonedCreatures); if (ptr.getClass().hasInventoryStore(ptr)) ptr.getClass().getInventoryStore(ptr).visitEffectSources(updateSummonedCreatures); updateSummonedCreatures.process(mTimerDisposeSummonsCorpses == 0.f); } } void Actors::calculateNpcStatModifiers (const MWWorld::Ptr& ptr, float duration) { NpcStats &npcStats = ptr.getClass().getNpcStats(ptr); const MagicEffects &effects = npcStats.getMagicEffects(); bool godmode = ptr == getPlayer() && MWBase::Environment::get().getWorld()->getGodModeState(); // skills for(int i = 0;i < ESM::Skill::Length;++i) { SkillValue& skill = npcStats.getSkill(i); float fortify = effects.get(EffectKey(ESM::MagicEffect::FortifySkill, i)).getMagnitude(); float drain = 0.f, absorb = 0.f; if (!godmode) { drain = effects.get(EffectKey(ESM::MagicEffect::DrainSkill, i)).getMagnitude(); absorb = effects.get(EffectKey(ESM::MagicEffect::AbsorbSkill, i)).getMagnitude(); } skill.setModifier(static_cast(fortify - drain - absorb)); } } bool Actors::isAttackPreparing(const MWWorld::Ptr& ptr) { PtrActorMap::iterator it = mActors.find(ptr); if (it == mActors.end()) return false; CharacterController* ctrl = it->second->getCharacterController(); return ctrl->isAttackPreparing(); } bool Actors::isRunning(const MWWorld::Ptr& ptr) { PtrActorMap::iterator it = mActors.find(ptr); if (it == mActors.end()) return false; CharacterController* ctrl = it->second->getCharacterController(); return ctrl->isRunning(); } bool Actors::isSneaking(const MWWorld::Ptr& ptr) { PtrActorMap::iterator it = mActors.find(ptr); if (it == mActors.end()) return false; CharacterController* ctrl = it->second->getCharacterController(); return ctrl->isSneaking(); } void Actors::updateDrowning(const MWWorld::Ptr& ptr, float duration, bool isKnockedOut, bool isPlayer) { NpcStats &stats = ptr.getClass().getNpcStats(ptr); // When npc stats are just initialized, mTimeToStartDrowning == -1 and we should get value from GMST static const float fHoldBreathTime = MWBase::Environment::get().getWorld()->getStore().get().find("fHoldBreathTime")->mValue.getFloat(); if (stats.getTimeToStartDrowning() == -1.f) stats.setTimeToStartDrowning(fHoldBreathTime); if (!isPlayer && stats.getTimeToStartDrowning() < fHoldBreathTime / 2) { AiSequence& seq = ptr.getClass().getCreatureStats(ptr).getAiSequence(); if (seq.getTypeId() != AiPackageTypeId::Breathe) //Only add it once seq.stack(AiBreathe(), ptr); } MWBase::World *world = MWBase::Environment::get().getWorld(); bool knockedOutUnderwater = (isKnockedOut && world->isUnderwater(ptr.getCell(), osg::Vec3f(ptr.getRefData().getPosition().asVec3()))); if((world->isSubmerged(ptr) || knockedOutUnderwater) && stats.getMagicEffects().get(ESM::MagicEffect::WaterBreathing).getMagnitude() == 0) { float timeLeft = 0.0f; if(knockedOutUnderwater) stats.setTimeToStartDrowning(0); else { timeLeft = stats.getTimeToStartDrowning() - duration; if(timeLeft < 0.0f) timeLeft = 0.0f; stats.setTimeToStartDrowning(timeLeft); } bool godmode = isPlayer && MWBase::Environment::get().getWorld()->getGodModeState(); if(timeLeft == 0.0f && !godmode) { // If drowning, apply 3 points of damage per second static const float fSuffocationDamage = world->getStore().get().find("fSuffocationDamage")->mValue.getFloat(); DynamicStat health = stats.getHealth(); health.setCurrent(health.getCurrent() - fSuffocationDamage*duration); stats.setHealth(health); // Play a drowning sound MWBase::SoundManager *sndmgr = MWBase::Environment::get().getSoundManager(); if(!sndmgr->getSoundPlaying(ptr, "drown")) sndmgr->playSound3D(ptr, "drown", 1.0f, 1.0f); if(isPlayer) MWBase::Environment::get().getWindowManager()->activateHitOverlay(false); } } else stats.setTimeToStartDrowning(fHoldBreathTime); } void Actors::updateEquippedLight (const MWWorld::Ptr& ptr, float duration, bool mayEquip) { bool isPlayer = (ptr == getPlayer()); MWWorld::InventoryStore &inventoryStore = ptr.getClass().getInventoryStore(ptr); MWWorld::ContainerStoreIterator heldIter = inventoryStore.getSlot(MWWorld::InventoryStore::Slot_CarriedLeft); /** * Automatically equip NPCs torches at night and unequip them at day */ /* Start of tes3mp change (major) We need DedicatedPlayers and DedicatedActors to not automatically equip their light-emitting items, so additional conditions have been added for them */ if (!isPlayer && !mwmp::PlayerList::isDedicatedPlayer(ptr) && !mwmp::Main::get().getCellController()->isDedicatedActor(ptr)) { /* End of tes3mp change (major) */ MWWorld::ContainerStoreIterator torch = inventoryStore.end(); for (MWWorld::ContainerStoreIterator it = inventoryStore.begin(); it != inventoryStore.end(); ++it) { if (it->getTypeName() == typeid(ESM::Light).name() && it->getClass().canBeEquipped(*it, ptr).first) { torch = it; break; } } if (mayEquip) { if (torch != inventoryStore.end()) { if (!ptr.getClass().getCreatureStats (ptr).getAiSequence().isInCombat()) { // For non-hostile NPCs, unequip whatever is in the left slot in favor of a light. if (heldIter != inventoryStore.end() && heldIter->getTypeName() != typeid(ESM::Light).name()) inventoryStore.unequipItem(*heldIter, ptr); } else if (heldIter == inventoryStore.end() || heldIter->getTypeName() == typeid(ESM::Light).name()) { // For hostile NPCs, see if they have anything better to equip first auto shield = inventoryStore.getPreferredShield(ptr); if(shield != inventoryStore.end()) inventoryStore.equip(MWWorld::InventoryStore::Slot_CarriedLeft, shield, ptr); } heldIter = inventoryStore.getSlot(MWWorld::InventoryStore::Slot_CarriedLeft); // If we have a torch and can equip it, then equip it now. if (heldIter == inventoryStore.end()) { inventoryStore.equip(MWWorld::InventoryStore::Slot_CarriedLeft, torch, ptr); } } } else { if (heldIter != inventoryStore.end() && heldIter->getTypeName() == typeid(ESM::Light).name()) { // At day, unequip lights and auto equip shields or other suitable items // (Note: autoEquip will ignore lights) inventoryStore.autoEquip(ptr); } } } heldIter = inventoryStore.getSlot(MWWorld::InventoryStore::Slot_CarriedLeft); //If holding a light... if(heldIter.getType() == MWWorld::ContainerStore::Type_Light) { // Use time from the player's light if(isPlayer) { // But avoid using it up if the light source is hidden MWRender::Animation *anim = MWBase::Environment::get().getWorld()->getAnimation(ptr); if (anim && anim->getCarriedLeftShown()) { float timeRemaining = heldIter->getClass().getRemainingUsageTime(*heldIter); // -1 is infinite light source. Other negative values are treated as 0. if (timeRemaining != -1.0f) { timeRemaining -= duration; if (timeRemaining <= 0.f) { inventoryStore.remove(*heldIter, 1, ptr); // remove it return; } heldIter->getClass().setRemainingUsageTime(*heldIter, timeRemaining); } } } // Both NPC and player lights extinguish in water. if(MWBase::Environment::get().getWorld()->isSwimming(ptr)) { inventoryStore.remove(*heldIter, 1, ptr); // remove it // ...But, only the player makes a sound. if(isPlayer) MWBase::Environment::get().getSoundManager()->playSound("torch out", 1.0, 1.0, MWSound::Type::Sfx, MWSound::PlayMode::NoEnv); } } } void Actors::updateCrimePursuit(const MWWorld::Ptr& ptr, float duration) { MWWorld::Ptr player = getPlayer(); if (ptr != player && ptr.getClass().isNpc()) { // get stats of witness CreatureStats& creatureStats = ptr.getClass().getCreatureStats(ptr); NpcStats& npcStats = ptr.getClass().getNpcStats(ptr); if (player.getClass().getNpcStats(player).isWerewolf()) return; if (ptr.getClass().isClass(ptr, "Guard") && creatureStats.getAiSequence().getTypeId() != AiPackageTypeId::Pursue && !creatureStats.getAiSequence().isInCombat() && creatureStats.getMagicEffects().get(ESM::MagicEffect::CalmHumanoid).getMagnitude() == 0) { const MWWorld::ESMStore& esmStore = MWBase::Environment::get().getWorld()->getStore(); static const int cutoff = esmStore.get().find("iCrimeThreshold")->mValue.getInteger(); // Force dialogue on sight if bounty is greater than the cutoff // In vanilla morrowind, the greeting dialogue is scripted to either arrest the player (< 5000 bounty) or attack (>= 5000 bounty) if ( player.getClass().getNpcStats(player).getBounty() >= cutoff // TODO: do not run these two every frame. keep an Aware state for each actor and update it every 0.2 s or so? && MWBase::Environment::get().getWorld()->getLOS(ptr, player) && MWBase::Environment::get().getMechanicsManager()->awarenessCheck(player, ptr)) { static const int iCrimeThresholdMultiplier = esmStore.get().find("iCrimeThresholdMultiplier")->mValue.getInteger(); /* Start of tes3mp change (major) Only attack players based on their high bounty if they haven't died since the last time an attempt was made to arrest them */ if (player.getClass().getNpcStats(player).getBounty() >= cutoff * iCrimeThresholdMultiplier && !mwmp::Main::get().getLocalPlayer()->diedSinceArrestAttempt) /* End of tes3mp change (major) */ { MWBase::Environment::get().getMechanicsManager()->startCombat(ptr, player); creatureStats.setHitAttemptActorId(player.getClass().getCreatureStats(player).getActorId()); // Stops the guard from quitting combat if player is unreachable } else creatureStats.getAiSequence().stack(AiPursue(player), ptr); creatureStats.setAlarmed(true); npcStats.setCrimeId(MWBase::Environment::get().getWorld()->getPlayer().getNewCrimeId()); } } // if I was a witness to a crime if (npcStats.getCrimeId() != -1) { // if you've paid for your crimes and I havent noticed if( npcStats.getCrimeId() <= MWBase::Environment::get().getWorld()->getPlayer().getCrimeId() ) { // Calm witness down if (ptr.getClass().isClass(ptr, "Guard")) creatureStats.getAiSequence().stopPursuit(); creatureStats.getAiSequence().stopCombat(); // Reset factors to attack creatureStats.setAttacked(false); creatureStats.setAlarmed(false); creatureStats.setAiSetting(CreatureStats::AI_Fight, ptr.getClass().getBaseFightRating(ptr)); // Update witness crime id npcStats.setCrimeId(-1); } /* Start of tes3mp addition If the player has died since their crime was committed, stop combat with them as though they have paid their bounty */ else if (mwmp::Main::get().getLocalPlayer()->diedSinceArrestAttempt && creatureStats.getAiSequence().isInCombat(player)) { if (difftime(mwmp::Main::get().getLocalPlayer()->deathTime, npcStats.getCrimeTime()) > 0) { creatureStats.getAiSequence().stopCombat(); creatureStats.setAttacked(false); creatureStats.setAlarmed(false); creatureStats.setAiSetting(CreatureStats::AI_Fight, ptr.getClass().getBaseFightRating(ptr)); npcStats.setCrimeId(-1); npcStats.setCrimeTime(time(0)); LOG_MESSAGE_SIMPLE(TimedLog::LOG_INFO, "NPC %s %i-%i has forgiven player's crimes after the player's death", ptr.getCellRef().getRefId().c_str(), ptr.getCellRef().getRefNum().mIndex, ptr.getCellRef().getMpNum()); } } /* End of tes3mp addition */ } } } Actors::Actors() : mSmoothMovement(Settings::Manager::getBool("smooth movement", "Game")) { mTimerDisposeSummonsCorpses = 0.2f; // We should add a delay between summoned creature death and its corpse despawning updateProcessingRange(); } Actors::~Actors() { clear(); } float Actors::getProcessingRange() const { return mActorsProcessingRange; } void Actors::updateProcessingRange() { /* Start of tes3mp change (major) Multiplayer needs AI processing to not have a distance limit, at least until a better authority system is implemented for LocalActors */ // 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 maxProcessingRange = 8192.f * 50; /* End of tes3mp change (major) */ static const float minProcessingRange = maxProcessingRange / 2.f; float actorsProcessingRange = Settings::Manager::getFloat("actors processing range", "Game"); actorsProcessingRange = std::min(actorsProcessingRange, maxProcessingRange); actorsProcessingRange = std::max(actorsProcessingRange, minProcessingRange); mActorsProcessingRange = actorsProcessingRange; } void Actors::addActor (const MWWorld::Ptr& ptr, bool updateImmediately) { removeActor(ptr); MWRender::Animation *anim = MWBase::Environment::get().getWorld()->getAnimation(ptr); if (!anim) return; mActors.insert(std::make_pair(ptr, new Actor(ptr, anim))); CharacterController* ctrl = mActors[ptr]->getCharacterController(); if (updateImmediately) ctrl->update(0); // We should initially hide actors outside of processing range. // Note: since we update player after other actors, distance will be incorrect during teleportation. // Do not update visibility if player was teleported, so actors will be visible during teleportation frame. if (MWBase::Environment::get().getWorld()->getPlayer().wasTeleported()) return; updateVisibility(ptr, ctrl); } void Actors::updateVisibility (const MWWorld::Ptr& ptr, CharacterController* ctrl) { MWWorld::Ptr player = MWMechanics::getPlayer(); if (ptr == player) return; const float dist = (player.getRefData().getPosition().asVec3() - ptr.getRefData().getPosition().asVec3()).length(); if (dist > mActorsProcessingRange) { ptr.getRefData().getBaseNode()->setNodeMask(0); return; } else ptr.getRefData().getBaseNode()->setNodeMask(MWRender::Mask_Actor); // Fade away actors on large distance (>90% of actor's processing distance) float visibilityRatio = 1.0; float fadeStartDistance = mActorsProcessingRange*0.9f; float fadeEndDistance = mActorsProcessingRange; float fadeRatio = (dist - fadeStartDistance)/(fadeEndDistance - fadeStartDistance); if (fadeRatio > 0) visibilityRatio -= std::max(0.f, fadeRatio); visibilityRatio = std::min(1.f, visibilityRatio); ctrl->setVisibility(visibilityRatio); } void Actors::removeActor (const MWWorld::Ptr& ptr) { PtrActorMap::iterator iter = mActors.find(ptr); if(iter != mActors.end()) { delete iter->second; mActors.erase(iter); } } void Actors::castSpell(const MWWorld::Ptr& ptr, const std::string spellId, bool manualSpell) { PtrActorMap::iterator iter = mActors.find(ptr); if(iter != mActors.end()) iter->second->getCharacterController()->castSpell(spellId, manualSpell); } bool Actors::isActorDetected(const MWWorld::Ptr& actor, const MWWorld::Ptr& observer) { if (!actor.getClass().isActor()) return false; // If an observer is NPC, check if he detected an actor if (!observer.isEmpty() && observer.getClass().isNpc()) { return MWBase::Environment::get().getWorld()->getLOS(observer, actor) && MWBase::Environment::get().getMechanicsManager()->awarenessCheck(actor, observer); } // Otherwise check if any actor in AI processing range sees the target actor std::vector neighbors; osg::Vec3f position (actor.getRefData().getPosition().asVec3()); getObjectsInRange(position, mActorsProcessingRange, neighbors); for (const MWWorld::Ptr &neighbor : neighbors) { if (neighbor == actor) continue; bool result = MWBase::Environment::get().getWorld()->getLOS(neighbor, actor) && MWBase::Environment::get().getMechanicsManager()->awarenessCheck(actor, neighbor); if (result) return true; } return false; } void Actors::updateActor(const MWWorld::Ptr &old, const MWWorld::Ptr &ptr) { PtrActorMap::iterator iter = mActors.find(old); if(iter != mActors.end()) { Actor *actor = iter->second; mActors.erase(iter); actor->updatePtr(ptr); mActors.insert(std::make_pair(ptr, actor)); } } void Actors::dropActors (const MWWorld::CellStore *cellStore, const MWWorld::Ptr& ignore) { PtrActorMap::iterator iter = mActors.begin(); while(iter != mActors.end()) { if((iter->first.isInCell() && iter->first.getCell()==cellStore) && iter->first != ignore) { delete iter->second; mActors.erase(iter++); } else ++iter; } } void Actors::updateCombatMusic () { MWWorld::Ptr player = getPlayer(); const osg::Vec3f playerPos = player.getRefData().getPosition().asVec3(); bool hasHostiles = false; // need to know this to play Battle music for(PtrActorMap::iterator iter(mActors.begin()); iter != mActors.end(); ++iter) { if (iter->first == player) continue; bool inProcessingRange = (playerPos - iter->first.getRefData().getPosition().asVec3()).length2() <= mActorsProcessingRange*mActorsProcessingRange; if (!inProcessingRange) continue; MWMechanics::CreatureStats& stats = iter->first.getClass().getCreatureStats(iter->first); if (!stats.isDead() && stats.getAiSequence().isInCombat()) { hasHostiles = true; break; } } // check if we still have any player enemies to switch music static int currentMusic = 0; if (currentMusic != 1 && !hasHostiles && !(player.getClass().getCreatureStats(player).isDead() && MWBase::Environment::get().getSoundManager()->isMusicPlaying())) { MWBase::Environment::get().getSoundManager()->playPlaylist(std::string("Explore")); currentMusic = 1; } else if (currentMusic != 2 && hasHostiles) { MWBase::Environment::get().getSoundManager()->playPlaylist(std::string("Battle")); currentMusic = 2; } } void Actors::predictAndAvoidCollisions(float duration) { if (!MWBase::Environment::get().getMechanicsManager()->isAIActive()) return; const float minGap = 10.f; const float maxDistForPartialAvoiding = 200.f; const float maxDistForStrictAvoiding = 100.f; const float maxTimeToCheck = 2.0f; static const bool giveWayWhenIdle = Settings::Manager::getBool("NPCs give way", "Game"); MWWorld::Ptr player = getPlayer(); MWBase::World* world = MWBase::Environment::get().getWorld(); for(PtrActorMap::iterator iter(mActors.begin()); iter != mActors.end(); ++iter) { const MWWorld::Ptr& ptr = iter->first; if (ptr == player) continue; // Don't interfere with player controls. float maxSpeed = ptr.getClass().getMaxSpeed(ptr); if (maxSpeed == 0.0) continue; // Can't move, so there is no sense to predict collisions. Movement& movement = ptr.getClass().getMovementSettings(ptr); osg::Vec2f origMovement(movement.mPosition[0], movement.mPosition[1]); bool isMoving = origMovement.length2() > 0.01; if (movement.mPosition[1] < 0) continue; // Actors can not see others when move backward. // Moving NPCs always should avoid collisions. // Standing NPCs give way to moving ones if they are not in combat (or pursue) mode and either // follow player or have a AIWander package with non-empty wander area. bool shouldAvoidCollision = isMoving; bool shouldGiveWay = false; bool shouldTurnToApproachingActor = !isMoving; MWWorld::Ptr currentTarget; // Combat or pursue target (NPCs should not avoid collision with their targets). const auto& aiSequence = ptr.getClass().getCreatureStats(ptr).getAiSequence(); for (const auto& package : aiSequence) { if (package->getTypeId() == AiPackageTypeId::Follow) shouldAvoidCollision = true; else if (package->getTypeId() == AiPackageTypeId::Wander && giveWayWhenIdle) { if (!static_cast(package.get())->isStationary()) shouldGiveWay = true; } else if (package->getTypeId() == AiPackageTypeId::Combat || package->getTypeId() == AiPackageTypeId::Pursue) { currentTarget = package->getTarget(); shouldAvoidCollision = isMoving; shouldTurnToApproachingActor = false; break; } } if (!shouldAvoidCollision && !shouldGiveWay) continue; osg::Vec2f baseSpeed = origMovement * maxSpeed; osg::Vec3f basePos = ptr.getRefData().getPosition().asVec3(); float baseRotZ = ptr.getRefData().getPosition().rot[2]; osg::Vec3f halfExtents = world->getHalfExtents(ptr); float maxDistToCheck = isMoving ? maxDistForPartialAvoiding : maxDistForStrictAvoiding; float timeToCheck = maxTimeToCheck; if (!shouldGiveWay && !aiSequence.isEmpty()) timeToCheck = std::min(timeToCheck, getTimeToDestination(**aiSequence.begin(), basePos, maxSpeed, duration, halfExtents)); float timeToCollision = timeToCheck; osg::Vec2f movementCorrection(0, 0); float angleToApproachingActor = 0; // Iterate through all other actors and predict collisions. for(PtrActorMap::iterator otherIter(mActors.begin()); otherIter != mActors.end(); ++otherIter) { const MWWorld::Ptr& otherPtr = otherIter->first; if (otherPtr == ptr || otherPtr == currentTarget) continue; osg::Vec3f otherHalfExtents = world->getHalfExtents(otherPtr); osg::Vec3f deltaPos = otherPtr.getRefData().getPosition().asVec3() - basePos; osg::Vec2f relPos = Misc::rotateVec2f(osg::Vec2f(deltaPos.x(), deltaPos.y()), baseRotZ); float dist = deltaPos.length(); // Ignore actors which are not close enough or come from behind. if (dist > maxDistToCheck || relPos.y() < 0) continue; // Don't check for a collision if vertical distance is greater then the actor's height. if (deltaPos.z() > halfExtents.z() * 2 || deltaPos.z() < -otherHalfExtents.z() * 2) continue; osg::Vec3f speed = otherPtr.getClass().getMovementSettings(otherPtr).asVec3() * otherPtr.getClass().getMaxSpeed(otherPtr); float rotZ = otherPtr.getRefData().getPosition().rot[2]; osg::Vec2f relSpeed = Misc::rotateVec2f(osg::Vec2f(speed.x(), speed.y()), baseRotZ - rotZ) - baseSpeed; float collisionDist = minGap + world->getHalfExtents(ptr).x() + world->getHalfExtents(otherPtr).x(); collisionDist = std::min(collisionDist, relPos.length()); // Find the earliest `t` when |relPos + relSpeed * t| == collisionDist. float vr = relPos.x() * relSpeed.x() + relPos.y() * relSpeed.y(); float v2 = relSpeed.length2(); float Dh = vr * vr - v2 * (relPos.length2() - collisionDist * collisionDist); if (Dh <= 0 || v2 == 0) continue; // No solution; distance is always >= collisionDist. float t = (-vr - std::sqrt(Dh)) / v2; if (t < 0 || t > timeToCollision) continue; // Check visibility and awareness last as it's expensive. if (!MWBase::Environment::get().getWorld()->getLOS(otherPtr, ptr)) continue; if (!MWBase::Environment::get().getMechanicsManager()->awarenessCheck(otherPtr, ptr)) continue; timeToCollision = t; 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); 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. movementCorrection.y() *= 0.5f; } if (timeToCollision < timeToCheck) { // Try to evade the nearest collision. osg::Vec2f newMovement = origMovement + movementCorrection; // Step to the side rather than backward. Otherwise player will be able to push the NPC far away from it's original location. newMovement.y() = std::max(newMovement.y(), 0.f); newMovement.normalize(); if (isMoving) newMovement *= origMovement.length(); // Keep the original speed. movement.mPosition[0] = newMovement.x(); movement.mPosition[1] = newMovement.y(); if (shouldTurnToApproachingActor) zTurn(ptr, angleToApproachingActor); } } } void Actors::update (float duration, bool paused) { if(!paused) { static float timerUpdateHeadTrack = 0; static float timerUpdateEquippedLight = 0; static float timerUpdateHello = 0; const float updateEquippedLightInterval = 1.0f; if (timerUpdateHeadTrack >= 0.3f) timerUpdateHeadTrack = 0; if (timerUpdateHello >= 0.25f) timerUpdateHello = 0; if (mTimerDisposeSummonsCorpses >= 0.2f) mTimerDisposeSummonsCorpses = 0; if (timerUpdateEquippedLight >= updateEquippedLightInterval) timerUpdateEquippedLight = 0; // show torches only when there are darkness and no precipitations MWBase::World* world = MWBase::Environment::get().getWorld(); bool showTorches = world->useTorches(); MWWorld::Ptr player = getPlayer(); const osg::Vec3f playerPos = player.getRefData().getPosition().asVec3(); /// \todo move update logic to Actor class where appropriate std::map > cachedAllies; // will be filled as engageCombat iterates bool aiActive = MWBase::Environment::get().getMechanicsManager()->isAIActive(); int attackedByPlayerId = player.getClass().getCreatureStats(player).getHitAttemptActorId(); if (attackedByPlayerId != -1) { const MWWorld::Ptr playerHitAttemptActor = world->searchPtrViaActorId(attackedByPlayerId); if (!playerHitAttemptActor.isInCell()) player.getClass().getCreatureStats(player).setHitAttemptActorId(-1); } bool godmode = MWBase::Environment::get().getWorld()->getGodModeState(); // AI and magic effects update for(PtrActorMap::iterator iter(mActors.begin()); iter != mActors.end(); ++iter) { bool isPlayer = iter->first == player; CharacterController* ctrl = iter->second->getCharacterController(); float distSqr = (playerPos - iter->first.getRefData().getPosition().asVec3()).length2(); // AI processing is only done within given distance to the player. bool inProcessingRange = distSqr <= mActorsProcessingRange*mActorsProcessingRange; /* Start of tes3mp change (minor) Instead of merely updating the player character's mAttackingOrSpell here, prepare an Attack packet for the LocalPlayer */ if (isPlayer) { bool state = MWBase::Environment::get().getWorld()->getPlayer().getAttackingOrSpell(); DrawState_ dstate = player.getClass().getNpcStats(player).getDrawState(); ctrl->setAttackingOrSpell(world->getPlayer().getAttackingOrSpell()); if (dstate == DrawState_Weapon) { mwmp::Attack *localAttack = MechanicsHelper::getLocalAttack(iter->first); if (localAttack->pressed != state) { MechanicsHelper::resetAttack(localAttack); localAttack->type = MechanicsHelper::isUsingRangedWeapon(player) ? mwmp::Attack::RANGED : mwmp::Attack::MELEE; localAttack->pressed = state; // Prepare this attack for sending as long as it's not a ranged attack that's being released, // because we need to get the final attackStrength for that from WeaponAnimation to have the // correct projectile speed if (localAttack->type == mwmp::Attack::MELEE || state) localAttack->shouldSend = true; } } } /* End of tes3mp change (minor) */ /* Start of tes3mp change (major) Allow this code to use the same logic for DedicatedPlayers as for LocalPlayers */ // If dead or no longer in combat, no longer store any actors who attempted to hit us. Also remove for the player. if (iter->first != player && !mwmp::PlayerList::isDedicatedPlayer(iter->first) &&(iter->first.getClass().getCreatureStats(iter->first).isDead() || !iter->first.getClass().getCreatureStats(iter->first).getAiSequence().isInCombat() || !inProcessingRange)) { iter->first.getClass().getCreatureStats(iter->first).setHitAttemptActorId(-1); if (player.getClass().getCreatureStats(player).getHitAttemptActorId() == iter->first.getClass().getCreatureStats(iter->first).getActorId()) player.getClass().getCreatureStats(player).setHitAttemptActorId(-1); mwmp::PlayerList::clearHitAttemptActorId(iter->first.getClass().getCreatureStats(iter->first).getActorId()); } /* End of tes3mp change (major) */ iter->first.getClass().getCreatureStats(iter->first).getActiveSpells().update(duration); const Misc::TimerStatus engageCombatTimerStatus = iter->second->updateEngageCombatTimer(duration); // For dead actors we need to update looping spell particles if (iter->first.getClass().getCreatureStats(iter->first).isDead()) { // They can be added during the death animation if (!iter->first.getClass().getCreatureStats(iter->first).isDeathAnimationFinished()) adjustMagicEffects(iter->first); ctrl->updateContinuousVfx(); } else { bool cellChanged = world->hasCellChanged(); MWWorld::Ptr actor = iter->first; // make a copy of the map key to avoid it being invalidated when the player teleports updateActor(actor, duration); // Looping magic VFX update // Note: we need to do this before any of the animations are updated. // Reaching the text keys may trigger Hit / Spellcast (and as such, particles), // so updating VFX immediately after that would just remove the particle effects instantly. // There needs to be a magic effect update in between. ctrl->updateContinuousVfx(); if (!cellChanged && world->hasCellChanged()) { return; // for now abort update of the old cell when cell changes by teleportation magic effect // a better solution might be to apply cell changes at the end of the frame } /* Start of tes3mp change (major) Allow AI processing for LocalActors and partially for DedicatedActors */ bool isLocalActor = mwmp::Main::get().getCellController()->isLocalActor(actor); bool isDedicatedActor = mwmp::Main::get().getCellController()->isDedicatedActor(actor); if (inProcessingRange && (aiActive || isLocalActor || isDedicatedActor)) { if (engageCombatTimerStatus == Misc::TimerStatus::Elapsed && (isLocalActor || aiActive)) { if (!isPlayer) adjustCommandedActor(iter->first); for(PtrActorMap::iterator it(mActors.begin()); it != mActors.end(); ++it) { if (it->first == iter->first || isPlayer) // player is not AI-controlled continue; engageCombat(iter->first, it->first, cachedAllies, it->first == player); } } if (timerUpdateHeadTrack == 0) { float sqrHeadTrackDistance = std::numeric_limits::max(); MWWorld::Ptr headTrackTarget; MWMechanics::CreatureStats& stats = iter->first.getClass().getCreatureStats(iter->first); bool firstPersonPlayer = isPlayer && world->isFirstPerson(); bool inCombatOrPursue = stats.getAiSequence().isInCombat() || stats.getAiSequence().hasPackage(AiPackageTypeId::Pursue); MWWorld::Ptr activePackageTarget; // 1. Unconsious actor can not track target // 2. Actors in combat and pursue mode do not bother to headtrack anyone except their target // 3. Player character does not use headtracking in the 1st-person view if (!stats.getKnockedDown() && !firstPersonPlayer) { if (inCombatOrPursue) activePackageTarget = stats.getAiSequence().getActivePackage().getTarget(); for (PtrActorMap::iterator it(mActors.begin()); it != mActors.end(); ++it) { if (it->first == iter->first) continue; if (inCombatOrPursue && it->first != activePackageTarget) continue; updateHeadTracking(iter->first, it->first, headTrackTarget, sqrHeadTrackDistance, inCombatOrPursue); } } ctrl->setHeadTrackTarget(headTrackTarget); } if (iter->first.getClass().isNpc() && iter->first != player && (isLocalActor || aiActive)) updateCrimePursuit(iter->first, duration); if (iter->first != player && (isLocalActor || aiActive)) { CreatureStats &stats = iter->first.getClass().getCreatureStats(iter->first); if (isConscious(iter->first)) { stats.getAiSequence().execute(iter->first, *ctrl, duration); updateGreetingState(iter->first, *iter->second, timerUpdateHello > 0); playIdleDialogue(iter->first); updateMovementSpeed(iter->first); } } } else if ((isLocalActor || aiActive) && iter->first != player && isConscious(iter->first)) { CreatureStats &stats = iter->first.getClass().getCreatureStats(iter->first); stats.getAiSequence().execute(iter->first, *ctrl, duration, /*outOfRange*/true); } /* End of tes3mp change (major) */ if(iter->first.getClass().isNpc()) { // We can not update drowning state for actors outside of AI distance - they can not resurface to breathe if (inProcessingRange) updateDrowning(iter->first, duration, ctrl->isKnockedOut(), isPlayer); calculateNpcStatModifiers(iter->first, duration); if (timerUpdateEquippedLight == 0) updateEquippedLight(iter->first, updateEquippedLightInterval, showTorches); } } } static const bool avoidCollisions = Settings::Manager::getBool("NPCs avoid collisions", "Game"); if (avoidCollisions) predictAndAvoidCollisions(duration); timerUpdateHeadTrack += duration; timerUpdateEquippedLight += duration; timerUpdateHello += duration; mTimerDisposeSummonsCorpses += duration; // Animation/movement update CharacterController* playerCharacter = nullptr; for(PtrActorMap::iterator iter(mActors.begin()); iter != mActors.end(); ++iter) { const float dist = (playerPos - iter->first.getRefData().getPosition().asVec3()).length(); bool isPlayer = iter->first == player; CreatureStats &stats = iter->first.getClass().getCreatureStats(iter->first); // Actors with active AI should be able to move. bool alwaysActive = false; if (!isPlayer && isConscious(iter->first) && !stats.isParalyzed()) { MWMechanics::AiSequence& seq = stats.getAiSequence(); alwaysActive = !seq.isEmpty() && seq.getActivePackage().alwaysActive(); } bool inRange = isPlayer || dist <= mActorsProcessingRange || alwaysActive; int activeFlag = 1; // Can be changed back to '2' to keep updating bounding boxes off screen (more accurate, but slower) if (isPlayer) activeFlag = 2; int active = inRange ? activeFlag : 0; CharacterController* ctrl = iter->second->getCharacterController(); ctrl->setActive(active); if (!inRange) { iter->first.getRefData().getBaseNode()->setNodeMask(0); world->setActorCollisionMode(iter->first, false, false); continue; } else if (!isPlayer) iter->first.getRefData().getBaseNode()->setNodeMask(MWRender::Mask_Actor); const bool isDead = iter->first.getClass().getCreatureStats(iter->first).isDead(); if (!isDead && (!godmode || !isPlayer) && iter->first.getClass().getCreatureStats(iter->first).isParalyzed()) ctrl->skipAnim(); // Handle player last, in case a cell transition occurs by casting a teleportation spell // (would invalidate the iterator) if (iter->first == getPlayer()) { playerCharacter = ctrl; continue; } world->setActorCollisionMode(iter->first, true, !iter->first.getClass().getCreatureStats(iter->first).isDeathAnimationFinished()); ctrl->update(duration); updateVisibility(iter->first, ctrl); } if (playerCharacter) { playerCharacter->update(duration); playerCharacter->setVisibility(1.f); } for(PtrActorMap::iterator iter(mActors.begin()); iter != mActors.end(); ++iter) { const MWWorld::Class &cls = iter->first.getClass(); CreatureStats &stats = cls.getCreatureStats(iter->first); //KnockedOutOneFrameLogic //Used for "OnKnockedOut" command //Put here to ensure that it's run for PRECISELY one frame. if (stats.getKnockedDown() && !stats.getKnockedDownOneFrame() && !stats.getKnockedDownOverOneFrame()) { //Start it for one frame if nessesary stats.setKnockedDownOneFrame(true); } else if (stats.getKnockedDownOneFrame() && !stats.getKnockedDownOverOneFrame()) { //Turn off KnockedOutOneframe stats.setKnockedDownOneFrame(false); stats.setKnockedDownOverOneFrame(true); } } killDeadActors(); updateSneaking(playerCharacter, duration); } updateCombatMusic(); } void Actors::notifyDied(const MWWorld::Ptr &actor) { actor.getClass().getCreatureStats(actor).notifyDied(); /* Start of tes3mp change (major) Don't increment the kill count and expect the server to send a packet to increment it for us instead */ //++mDeathCount[Misc::StringUtils::lowerCase(actor.getCellRef().getRefId())]; /* End of tes3mp change (major) */ } void Actors::resurrect(const MWWorld::Ptr &ptr) { PtrActorMap::iterator iter = mActors.find(ptr); if(iter != mActors.end()) { if(iter->second->getCharacterController()->isDead()) { // Actor has been resurrected. Notify the CharacterController and re-enable collision. MWBase::Environment::get().getWorld()->enableActorCollision(iter->first, true); iter->second->getCharacterController()->resurrect(); } } } void Actors::killDeadActors() { for(PtrActorMap::iterator iter(mActors.begin()); iter != mActors.end(); ++iter) { const MWWorld::Class &cls = iter->first.getClass(); CreatureStats &stats = cls.getCreatureStats(iter->first); if(!stats.isDead()) continue; MWBase::Environment::get().getWorld()->removeActorPath(iter->first); CharacterController::KillResult killResult = iter->second->getCharacterController()->kill(); if (killResult == CharacterController::Result_DeathAnimStarted) { // Play dying words // Note: It's not known whether the soundgen tags scream, roar, and moan are reliable // for NPCs since some of the npc death animation files are missing them. /* Start of tes3mp change (major) Don't play dying words for NPCs who have already been marked as having finished their death animations from elsewhere in the code */ if (!stats.isDeathAnimationFinished()) MWBase::Environment::get().getDialogueManager()->say(iter->first, "hit"); /* End of tes3mp change (major) */ // Apply soultrap if (iter->first.getTypeName() == typeid(ESM::Creature).name()) { SoulTrap soulTrap (iter->first); stats.getActiveSpells().visitEffectSources(soulTrap); } // Magic effects will be reset later, and the magic effect that could kill the actor // needs to be determined now calculateCreatureStatModifiers(iter->first, 0); if (cls.isEssential(iter->first)) MWBase::Environment::get().getWindowManager()->messageBox("#{sKilledEssential}"); } else if (killResult == CharacterController::Result_DeathAnimJustFinished) { bool isPlayer = iter->first == getPlayer(); notifyDied(iter->first); // Reset magic effects and recalculate derived effects // One case where we need this is to make sure bound items are removed upon death stats.modifyMagicEffects(MWMechanics::MagicEffects()); stats.getActiveSpells().clear(); // Make sure spell effects are removed purgeSpellEffects(stats.getActorId()); // Reset dynamic stats, attributes and skills calculateCreatureStatModifiers(iter->first, 0); if (iter->first.getClass().isNpc()) calculateNpcStatModifiers(iter->first, 0); if (isPlayer) { //player's death animation is over /* Start of tes3mp change (major) The main menu no longer opens when the local player dies, because of automatic respawning by default */ //MWBase::Environment::get().getStateManager()->askLoadRecent(); /* End of tes3mp change (major) */ } else { // NPC death animation is over, disable actor collision MWBase::Environment::get().getWorld()->enableActorCollision(iter->first, false); } // Play Death Music if it was the player dying if(iter->first == getPlayer()) MWBase::Environment::get().getSoundManager()->streamMusic("Special/MW_Death.mp3"); } } } void Actors::cleanupSummonedCreature (MWMechanics::CreatureStats& casterStats, int creatureActorId) { MWWorld::Ptr ptr = MWBase::Environment::get().getWorld()->searchPtrViaActorId(creatureActorId); /* Start of tes3mp change (major) Do a cleanup here and send an ID_OBJECT_DELETE packet every time a summoned creature despawns for the local player or for a local actor */ if (!ptr.isEmpty() && (casterStats.getActorId() == getPlayer().getClass().getCreatureStats(getPlayer()).getActorId() || mwmp::Main::get().getCellController()->hasLocalAuthority(*ptr.getCell()->getCell()))) { mwmp::ObjectList *objectList = mwmp::Main::get().getNetworking()->getObjectList(); objectList->reset(); objectList->packetOrigin = mwmp::CLIENT_GAMEPLAY; objectList->addObjectGeneric(ptr); objectList->sendObjectDelete(); /* End of tes3mp change (major) */ const ESM::Static* fx = MWBase::Environment::get().getWorld()->getStore().get() .search("VFX_Summon_End"); if (fx) MWBase::Environment::get().getWorld()->spawnEffect("meshes\\" + fx->mModel, "", ptr.getRefData().getPosition().asVec3()); // Remove the summoned creature's summoned creatures as well MWMechanics::CreatureStats& stats = ptr.getClass().getCreatureStats(ptr); std::map& creatureMap = stats.getSummonedCreatureMap(); for (const auto& creature : creatureMap) cleanupSummonedCreature(stats, creature.second); creatureMap.clear(); } else if (creatureActorId != -1) { // We didn't find the creature. It's probably in an inactive cell. // Add to graveyard so we can delete it when the cell becomes active. std::vector& graveyard = casterStats.getSummonedCreatureGraveyard(); graveyard.push_back(creatureActorId); } purgeSpellEffects(creatureActorId); } void Actors::purgeSpellEffects(int casterActorId) { for (PtrActorMap::iterator iter(mActors.begin());iter != mActors.end();++iter) { MWMechanics::ActiveSpells& spells = iter->first.getClass().getCreatureStats(iter->first).getActiveSpells(); spells.purge(casterActorId); } } void Actors::rest(double hours, bool sleep) { float duration = hours * 3600.f; float timeScale = MWBase::Environment::get().getWorld()->getTimeScaleFactor(); if (timeScale != 0.f) duration /= timeScale; const MWWorld::Ptr player = MWBase::Environment::get().getWorld()->getPlayerPtr(); const osg::Vec3f playerPos = player.getRefData().getPosition().asVec3(); for(PtrActorMap::iterator iter(mActors.begin()); iter != mActors.end(); ++iter) { if (iter->first.getClass().getCreatureStats(iter->first).isDead()) { iter->first.getClass().getCreatureStats(iter->first).getActiveSpells().update(duration); continue; } if (!sleep || iter->first == player) restoreDynamicStats(iter->first, hours, sleep); if ((!iter->first.getRefData().getBaseNode()) || (playerPos - iter->first.getRefData().getPosition().asVec3()).length2() > mActorsProcessingRange*mActorsProcessingRange) continue; adjustMagicEffects (iter->first); if (iter->first.getClass().getCreatureStats(iter->first).needToRecalcDynamicStats()) calculateDynamicStats (iter->first); calculateCreatureStatModifiers (iter->first, duration); if (iter->first.getClass().isNpc()) calculateNpcStatModifiers(iter->first, duration); iter->first.getClass().getCreatureStats(iter->first).getActiveSpells().update(duration); MWRender::Animation* animation = MWBase::Environment::get().getWorld()->getAnimation(iter->first); if (animation) { animation->removeEffects(); MWBase::Environment::get().getWorld()->applyLoopingParticles(iter->first); } } fastForwardAi(); } void Actors::updateSneaking(CharacterController* ctrl, float duration) { static float sneakTimer = 0.f; // Times update of sneak icon if (!ctrl) { MWBase::Environment::get().getWindowManager()->setSneakVisibility(false); return; } MWWorld::Ptr player = getPlayer(); if (!MWBase::Environment::get().getMechanicsManager()->isSneaking(player)) { MWBase::Environment::get().getWindowManager()->setSneakVisibility(false); return; } static float sneakSkillTimer = 0.f; // Times sneak skill progress from "avoid notice" MWBase::World* world = MWBase::Environment::get().getWorld(); const MWWorld::Store& gmst = world->getStore().get(); static const float fSneakUseDist = gmst.find("fSneakUseDist")->mValue.getFloat(); static const float fSneakUseDelay = gmst.find("fSneakUseDelay")->mValue.getFloat(); if (sneakTimer >= fSneakUseDelay) sneakTimer = 0.f; if (sneakTimer == 0.f) { // Set when an NPC is within line of sight and distance, but is still unaware. Used for skill progress. bool avoidedNotice = false; bool detected = false; std::vector observers; osg::Vec3f position(player.getRefData().getPosition().asVec3()); float radius = std::min(fSneakUseDist, mActorsProcessingRange); getObjectsInRange(position, radius, observers); for (const MWWorld::Ptr &observer : observers) { if (observer == player || observer.getClass().getCreatureStats(observer).isDead()) continue; /* Start of tes3mp addition Don't make allied players break each other's sneaking */ if (MechanicsHelper::isTeamMember(observer, player)) continue; /* End of tes3mp addition */ if (world->getLOS(player, observer)) { if (MWBase::Environment::get().getMechanicsManager()->awarenessCheck(player, observer)) { detected = true; avoidedNotice = false; MWBase::Environment::get().getWindowManager()->setSneakVisibility(false); break; } else { avoidedNotice = true; } } } if (sneakSkillTimer >= fSneakUseDelay) sneakSkillTimer = 0.f; if (avoidedNotice && sneakSkillTimer == 0.f) player.getClass().skillUsageSucceeded(player, ESM::Skill::Sneak, 0); if (!detected) MWBase::Environment::get().getWindowManager()->setSneakVisibility(true); } sneakTimer += duration; sneakSkillTimer += duration; } int Actors::getHoursToRest(const MWWorld::Ptr &ptr) const { float healthPerHour, magickaPerHour; getRestorationPerHourOfSleep(ptr, healthPerHour, magickaPerHour); CreatureStats& stats = ptr.getClass().getCreatureStats(ptr); bool stunted = stats.getMagicEffects ().get(ESM::MagicEffect::StuntedMagicka).getMagnitude() > 0; float healthHours = healthPerHour > 0 ? (stats.getHealth().getModified() - stats.getHealth().getCurrent()) / healthPerHour : 1.0f; float magickaHours = magickaPerHour > 0 && !stunted ? (stats.getMagicka().getModified() - stats.getMagicka().getCurrent()) / magickaPerHour : 1.0f; int autoHours = static_cast(std::ceil(std::max(1.f, std::max(healthHours, magickaHours)))); return autoHours; } int Actors::countDeaths (const std::string& id) const { std::map::const_iterator iter = mDeathCount.find(id); if(iter != mDeathCount.end()) return iter->second; return 0; } /* Start of tes3mp addition Make it possible to set the number of deaths for an actor with the given refId */ void Actors::setDeaths(const std::string& refId, int number) { mDeathCount[refId] = number; } /* End of tes3mp addition */ void Actors::forceStateUpdate(const MWWorld::Ptr & ptr) { PtrActorMap::iterator iter = mActors.find(ptr); if(iter != mActors.end()) iter->second->getCharacterController()->forceStateUpdate(); } bool Actors::playAnimationGroup(const MWWorld::Ptr& ptr, const std::string& groupName, int mode, int number, bool persist) { PtrActorMap::iterator iter = mActors.find(ptr); if(iter != mActors.end()) { return iter->second->getCharacterController()->playGroup(groupName, mode, number, persist); } else { Log(Debug::Warning) << "Warning: Actors::playAnimationGroup: Unable to find " << ptr.getCellRef().getRefId(); return false; } } void Actors::skipAnimation(const MWWorld::Ptr& ptr) { PtrActorMap::iterator iter = mActors.find(ptr); if(iter != mActors.end()) iter->second->getCharacterController()->skipAnim(); } bool Actors::checkAnimationPlaying(const MWWorld::Ptr& ptr, const std::string& groupName) { PtrActorMap::iterator iter = mActors.find(ptr); if(iter != mActors.end()) return iter->second->getCharacterController()->isAnimPlaying(groupName); return false; } void Actors::persistAnimationStates() { for (PtrActorMap::iterator iter = mActors.begin(); iter != mActors.end(); ++iter) iter->second->getCharacterController()->persistAnimationState(); } void Actors::getObjectsInRange(const osg::Vec3f& position, float radius, std::vector& out) { for (PtrActorMap::iterator iter = mActors.begin(); iter != mActors.end(); ++iter) { if ((iter->first.getRefData().getPosition().asVec3() - position).length2() <= radius*radius) out.push_back(iter->first); } } bool Actors::isAnyObjectInRange(const osg::Vec3f& position, float radius) { for (PtrActorMap::iterator iter = mActors.begin(); iter != mActors.end(); ++iter) { if ((iter->first.getRefData().getPosition().asVec3() - position).length2() <= radius*radius) return true; } return false; } std::list Actors::getActorsSidingWith(const MWWorld::Ptr& actor) { std::list list; for(PtrActorMap::iterator iter = mActors.begin(); iter != mActors.end(); ++iter) { const MWWorld::Ptr &iteratedActor = iter->first; if (iteratedActor == getPlayer()) continue; const bool sameActor = (iteratedActor == actor); const CreatureStats &stats = iteratedActor.getClass().getCreatureStats(iteratedActor); if (stats.isDead()) continue; /* Start of tes3mp addition If we're checking the LocalPlayer and the iteratedActor is a DedicatedPlayer belonging to this one's alliedPlayers, include the iteratedActor in the actors siding with the player Alternatively, if we're checking a DedicatedPlayer and the iteratedActor is a LocalPlayer or DedicatedPlayer belonging to their alliedPlayers, include the iteratedActor in the actors siding with them */ if (actor == getPlayer() && mwmp::PlayerList::isDedicatedPlayer(iteratedActor)) { if (Utils::vectorContains(mwmp::Main::get().getLocalPlayer()->alliedPlayers, mwmp::PlayerList::getPlayer(iteratedActor)->guid)) { list.push_back(iteratedActor); } } else if (mwmp::PlayerList::isDedicatedPlayer(actor)) { if (iteratedActor == getPlayer() && Utils::vectorContains(mwmp::PlayerList::getPlayer(actor)->alliedPlayers, mwmp::Main::get().getLocalPlayer()->guid)) { list.push_back(iteratedActor); } else if (mwmp::PlayerList::isDedicatedPlayer(iteratedActor) && Utils::vectorContains(mwmp::PlayerList::getPlayer(actor)->alliedPlayers, mwmp::PlayerList::getPlayer(iteratedActor)->guid)) { list.push_back(iteratedActor); } } /* End of tes3mp addition */ // 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 // Actors that are targeted by this actor's Follow or Escort packages also side with them for (const auto& package : stats.getAiSequence()) { if (package->sideWithTarget() && !package->getTarget().isEmpty()) { if (sameActor) { list.push_back(package->getTarget()); } else if (package->getTarget() == actor) { list.push_back(iteratedActor); } break; } else if (package->getTypeId() != AiPackageTypeId::Combat && package->getTypeId() != AiPackageTypeId::Wander) break; } } return list; } std::list Actors::getActorsFollowing(const MWWorld::Ptr& actor) { std::list list; forEachFollowingPackage(mActors, actor, getPlayer(), [&] (auto& iter, const std::unique_ptr& package) { if (package->followTargetThroughDoors() && package->getTarget() == actor) list.push_back(iter.first); else if (package->getTypeId() != AiPackageTypeId::Combat && package->getTypeId() != AiPackageTypeId::Wander) return false; return true; }); return list; } void Actors::getActorsFollowing(const MWWorld::Ptr &actor, std::set& out) { std::list followers = getActorsFollowing(actor); for(const MWWorld::Ptr &follower : followers) if (out.insert(follower).second) getActorsFollowing(follower, out); } void Actors::getActorsSidingWith(const MWWorld::Ptr &actor, std::set& out) { std::list followers = getActorsSidingWith(actor); for(const MWWorld::Ptr &follower : followers) if (out.insert(follower).second) getActorsSidingWith(follower, out); } void Actors::getActorsSidingWith(const MWWorld::Ptr &actor, std::set& out, std::map >& cachedAllies) { // If we have already found actor's allies, use the cache std::map >::const_iterator search = cachedAllies.find(actor); if (search != cachedAllies.end()) out.insert(search->second.begin(), search->second.end()); else { std::list followers = getActorsSidingWith(actor); for (const MWWorld::Ptr &follower : followers) if (out.insert(follower).second) getActorsSidingWith(follower, out, cachedAllies); // Cache ptrs and their sets of allies cachedAllies.insert(std::make_pair(actor, out)); for (const MWWorld::Ptr &iter : out) { search = cachedAllies.find(iter); if (search == cachedAllies.end()) cachedAllies.insert(std::make_pair(iter, out)); } } } std::list Actors::getActorsFollowingIndices(const MWWorld::Ptr &actor) { std::list list; forEachFollowingPackage(mActors, actor, getPlayer(), [&] (auto& iter, const std::unique_ptr& package) { if (package->followTargetThroughDoors() && package->getTarget() == actor) { list.push_back(static_cast(package.get())->getFollowIndex()); return false; } else if (package->getTypeId() != AiPackageTypeId::Combat && package->getTypeId() != AiPackageTypeId::Wander) return false; return true; }); return list; } std::map Actors::getActorsFollowingByIndex(const MWWorld::Ptr &actor) { std::map map; forEachFollowingPackage(mActors, actor, getPlayer(), [&] (auto& iter, const std::unique_ptr& package) { if (package->followTargetThroughDoors() && package->getTarget() == actor) { int index = static_cast(package.get())->getFollowIndex(); map[index] = iter.first; return false; } else if (package->getTypeId() != AiPackageTypeId::Combat && package->getTypeId() != AiPackageTypeId::Wander) return false; return true; }); return map; } std::list Actors::getActorsFighting(const MWWorld::Ptr& actor) { std::list list; std::vector neighbors; osg::Vec3f position (actor.getRefData().getPosition().asVec3()); getObjectsInRange(position, mActorsProcessingRange, neighbors); for(const MWWorld::Ptr& neighbor : neighbors) { if (neighbor == actor) continue; const CreatureStats &stats = neighbor.getClass().getCreatureStats(neighbor); if (stats.isDead()) continue; if (stats.getAiSequence().isInCombat(actor)) list.push_front(neighbor); } return list; } std::list Actors::getEnemiesNearby(const MWWorld::Ptr& actor) { std::list list; std::vector neighbors; osg::Vec3f position (actor.getRefData().getPosition().asVec3()); getObjectsInRange(position, mActorsProcessingRange, neighbors); std::set followers; getActorsFollowing(actor, followers); for (auto neighbor = neighbors.begin(); neighbor != neighbors.end(); ++neighbor) { const CreatureStats &stats = neighbor->getClass().getCreatureStats(*neighbor); if (stats.isDead() || *neighbor == actor || neighbor->getClass().isPureWaterCreature(*neighbor)) continue; const bool isFollower = followers.find(*neighbor) != followers.end(); if (stats.getAiSequence().isInCombat(actor) || (MWBase::Environment::get().getMechanicsManager()->isAggressive(*neighbor, actor) && !isFollower)) list.push_back(*neighbor); } return list; } void Actors::write (ESM::ESMWriter& writer, Loading::Listener& listener) const { writer.startRecord(ESM::REC_DCOU); for (std::map::const_iterator it = mDeathCount.begin(); it != mDeathCount.end(); ++it) { writer.writeHNString("ID__", it->first); writer.writeHNT ("COUN", it->second); } writer.endRecord(ESM::REC_DCOU); } void Actors::readRecord (ESM::ESMReader& reader, uint32_t type) { if (type == ESM::REC_DCOU) { while (reader.isNextSub("ID__")) { std::string id = reader.getHString(); int count; reader.getHNT(count, "COUN"); if (MWBase::Environment::get().getWorld()->getStore().find(id)) mDeathCount[id] = count; } } } void Actors::clear() { PtrActorMap::iterator it(mActors.begin()); for (; it != mActors.end(); ++it) { delete it->second; it->second = nullptr; } mActors.clear(); mDeathCount.clear(); } void Actors::updateMagicEffects(const MWWorld::Ptr &ptr) { adjustMagicEffects(ptr); calculateCreatureStatModifiers(ptr, 0.f); if (ptr.getClass().isNpc()) calculateNpcStatModifiers(ptr, 0.f); } bool Actors::isReadyToBlock(const MWWorld::Ptr &ptr) const { PtrActorMap::const_iterator it = mActors.find(ptr); if (it == mActors.end()) return false; return it->second->getCharacterController()->isReadyToBlock(); } bool Actors::isCastingSpell(const MWWorld::Ptr &ptr) const { PtrActorMap::const_iterator it = mActors.find(ptr); if (it == mActors.end()) return false; return it->second->getCharacterController()->isCastingSpell(); } bool Actors::isAttackingOrSpell(const MWWorld::Ptr& ptr) const { PtrActorMap::const_iterator it = mActors.find(ptr); if (it == mActors.end()) return false; CharacterController* ctrl = it->second->getCharacterController(); return ctrl->isAttackingOrSpell(); } /* Start of tes3mp addition Make it possible to set the attackingOrSpell state from elsewhere in the code */ void Actors::setAttackingOrSpell(const MWWorld::Ptr& ptr, bool state) const { PtrActorMap::const_iterator it = mActors.find(ptr); if (it == mActors.end()) return; CharacterController* ctrl = it->second->getCharacterController(); ctrl->setAttackingOrSpell(state); } /* End of tes3mp addition */ int Actors::getGreetingTimer(const MWWorld::Ptr& ptr) const { PtrActorMap::const_iterator it = mActors.find(ptr); if (it == mActors.end()) return 0; return it->second->getGreetingTimer(); } float Actors::getAngleToPlayer(const MWWorld::Ptr& ptr) const { PtrActorMap::const_iterator it = mActors.find(ptr); if (it == mActors.end()) return 0.f; return it->second->getAngleToPlayer(); } GreetingState Actors::getGreetingState(const MWWorld::Ptr& ptr) const { PtrActorMap::const_iterator it = mActors.find(ptr); if (it == mActors.end()) return Greet_None; return it->second->getGreetingState(); } bool Actors::isTurningToPlayer(const MWWorld::Ptr& ptr) const { PtrActorMap::const_iterator it = mActors.find(ptr); if (it == mActors.end()) return false; return it->second->isTurningToPlayer(); } void Actors::fastForwardAi() { if (!MWBase::Environment::get().getMechanicsManager()->isAIActive()) return; // making a copy since fast-forward could move actor to a different cell and invalidate the mActors iterator PtrActorMap map = mActors; for (PtrActorMap::iterator it = map.begin(); it != map.end(); ++it) { MWWorld::Ptr ptr = it->first; if (ptr == getPlayer() || !isConscious(ptr) || ptr.getClass().getCreatureStats(ptr).isParalyzed()) continue; MWMechanics::AiSequence& seq = ptr.getClass().getCreatureStats(ptr).getAiSequence(); seq.fastForward(ptr); } } } ================================================ FILE: apps/openmw/mwmechanics/actors.hpp ================================================ #ifndef GAME_MWMECHANICS_ACTORS_H #define GAME_MWMECHANICS_ACTORS_H #include #include #include #include #include #include "../mwmechanics/actorutil.hpp" namespace ESM { class ESMReader; class ESMWriter; } namespace osg { class Vec3f; } namespace Loading { class Listener; } namespace MWWorld { class Ptr; class CellStore; } namespace MWMechanics { class Actor; class CharacterController; class CreatureStats; class Actors { std::map mDeathCount; void addBoundItem (const std::string& itemId, const MWWorld::Ptr& actor); void removeBoundItem (const std::string& itemId, const MWWorld::Ptr& actor); void adjustMagicEffects (const MWWorld::Ptr& creature); void calculateDynamicStats (const MWWorld::Ptr& ptr); void calculateCreatureStatModifiers (const MWWorld::Ptr& ptr, float duration); void calculateNpcStatModifiers (const MWWorld::Ptr& ptr, float duration); void calculateRestoration (const MWWorld::Ptr& ptr, float duration); void updateDrowning (const MWWorld::Ptr& ptr, float duration, bool isKnockedOut, bool isPlayer); void updateEquippedLight (const MWWorld::Ptr& ptr, float duration, bool mayEquip); void updateCrimePursuit (const MWWorld::Ptr& ptr, float duration); void killDeadActors (); void purgeSpellEffects (int casterActorId); void predictAndAvoidCollisions(float duration); public: Actors(); ~Actors(); typedef std::map PtrActorMap; PtrActorMap::const_iterator begin() { return mActors.begin(); } PtrActorMap::const_iterator end() { return mActors.end(); } std::size_t size() const { return mActors.size(); } void notifyDied(const MWWorld::Ptr &actor); /// Check if the target actor was detected by an observer /// If the observer is a non-NPC, check all actors in AI processing distance as observers bool isActorDetected(const MWWorld::Ptr& actor, const MWWorld::Ptr& observer); /// Update magic effects for an actor. Usually done automatically once per frame, but if we're currently /// paused we may want to do it manually (after equipping permanent enchantment) void updateMagicEffects (const MWWorld::Ptr& ptr); void updateProcessingRange(); float getProcessingRange() const; void addActor (const MWWorld::Ptr& ptr, bool updateImmediately=false); ///< Register an actor for stats management /// /// \note Dead actors are ignored. void removeActor (const MWWorld::Ptr& ptr); ///< Deregister an actor for stats management /// /// \note Ignored, if \a ptr is not a registered actor. void resurrect (const MWWorld::Ptr& ptr); void castSpell(const MWWorld::Ptr& ptr, const std::string spellId, bool manualSpell=false); void updateActor(const MWWorld::Ptr &old, const MWWorld::Ptr& ptr); ///< Updates an actor with a new Ptr void dropActors (const MWWorld::CellStore *cellStore, const MWWorld::Ptr& ignore); ///< Deregister all actors (except for \a ignore) in the given cell. void updateCombatMusic(); ///< Update combat music state void update (float duration, bool paused); ///< Update actor stats and store desired velocity vectors in \a movement void updateActor (const MWWorld::Ptr& ptr, float duration); ///< This function is normally called automatically during the update process, but it can /// also be called explicitly at any time to force an update. /** 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. */ void engageCombat(const MWWorld::Ptr& actor1, const MWWorld::Ptr& actor2, std::map >& cachedAllies, bool againstPlayer); void playIdleDialogue(const MWWorld::Ptr& actor); void updateMovementSpeed(const MWWorld::Ptr& actor); void updateGreetingState(const MWWorld::Ptr& actor, Actor& actorState, bool turnOnly); void turnActorToFacePlayer(const MWWorld::Ptr& actor, Actor& actorState, const osg::Vec3f& dir); void updateHeadTracking(const MWWorld::Ptr& actor, const MWWorld::Ptr& targetActor, MWWorld::Ptr& headTrackTarget, float& sqrHeadTrackDistance, bool inCombatOrPursue); void rest(double hours, bool sleep); ///< Update actors while the player is waiting or sleeping. void updateSneaking(CharacterController* ctrl, float duration); ///< Update the sneaking indicator state according to the given player character controller. void restoreDynamicStats(const MWWorld::Ptr& actor, double hours, bool sleep); int getHoursToRest(const MWWorld::Ptr& ptr) const; ///< Calculate how many hours the given actor needs to rest in order to be fully healed void fastForwardAi(); ///< Simulate the passing of time int countDeaths (const std::string& id) const; ///< Return the number of deaths for actors with the given ID. /* Start of tes3mp addition Make it possible to set the number of deaths for an actor with the given refId */ void setDeaths(const std::string& refId, int number); /* End of tes3mp addition */ bool isAttackPreparing(const MWWorld::Ptr& ptr); bool isRunning(const MWWorld::Ptr& ptr); bool isSneaking(const MWWorld::Ptr& ptr); void forceStateUpdate(const MWWorld::Ptr &ptr); bool playAnimationGroup(const MWWorld::Ptr& ptr, const std::string& groupName, int mode, int number, bool persist=false); void skipAnimation(const MWWorld::Ptr& ptr); bool checkAnimationPlaying(const MWWorld::Ptr& ptr, const std::string& groupName); void persistAnimationStates(); void getObjectsInRange(const osg::Vec3f& position, float radius, std::vector& out); bool isAnyObjectInRange(const osg::Vec3f& position, float radius); void cleanupSummonedCreature (CreatureStats& casterStats, int creatureActorId); ///Returns the list of actors which are siding with the given actor in fights /**ie AiFollow or AiEscort is active and the target is the actor **/ std::list getActorsSidingWith(const MWWorld::Ptr& actor); std::list getActorsFollowing(const MWWorld::Ptr& actor); /// Recursive version of getActorsFollowing void getActorsFollowing(const MWWorld::Ptr &actor, std::set& out); /// Recursive version of getActorsSidingWith void getActorsSidingWith(const MWWorld::Ptr &actor, std::set& out); /// Recursive version of getActorsSidingWith that takes, adds to and returns a cache of actors mapped to their allies void getActorsSidingWith(const MWWorld::Ptr &actor, std::set& out, std::map >& cachedAllies); /// Get the list of AiFollow::mFollowIndex for all actors following this target std::list getActorsFollowingIndices(const MWWorld::Ptr& actor); std::map getActorsFollowingByIndex(const MWWorld::Ptr& actor); ///Returns the list of actors which are fighting the given actor /**ie AiCombat is active and the target is the actor **/ std::list getActorsFighting(const MWWorld::Ptr& actor); /// Unlike getActorsFighting, also returns actors that *would* fight the given actor if they saw him. std::list getEnemiesNearby(const MWWorld::Ptr& actor); void write (ESM::ESMWriter& writer, Loading::Listener& listener) const; void readRecord (ESM::ESMReader& reader, uint32_t type); void clear(); // Clear death counter bool isCastingSpell(const MWWorld::Ptr& ptr) const; bool isReadyToBlock(const MWWorld::Ptr& ptr) const; bool isAttackingOrSpell(const MWWorld::Ptr& ptr) const; /* Start of tes3mp addition Make it possible to set the attackingOrSpell state from elsewhere in the code */ void setAttackingOrSpell(const MWWorld::Ptr& ptr, bool state) const; /* End of tes3mp addition */ int getGreetingTimer(const MWWorld::Ptr& ptr) const; float getAngleToPlayer(const MWWorld::Ptr& ptr) const; GreetingState getGreetingState(const MWWorld::Ptr& ptr) const; bool isTurningToPlayer(const MWWorld::Ptr& ptr) const; private: void updateVisibility (const MWWorld::Ptr& ptr, CharacterController* ctrl); void applyCureEffects (const MWWorld::Ptr& actor); PtrActorMap mActors; float mTimerDisposeSummonsCorpses; float mActorsProcessingRange; bool mSmoothMovement; }; } #endif ================================================ FILE: apps/openmw/mwmechanics/actorutil.cpp ================================================ #include "actorutil.hpp" #include "../mwbase/world.hpp" #include "../mwbase/environment.hpp" #include "../mwworld/class.hpp" #include "../mwworld/player.hpp" namespace MWMechanics { MWWorld::Ptr getPlayer() { return MWBase::Environment::get().getWorld()->getPlayerPtr(); } bool isPlayerInCombat() { return MWBase::Environment::get().getWorld()->getPlayer().isInCombat(); } bool canActorMoveByZAxis(const MWWorld::Ptr& actor) { MWBase::World* world = MWBase::Environment::get().getWorld(); return (actor.getClass().canSwim(actor) && world->isSwimming(actor)) || world->isFlying(actor); } bool hasWaterWalking(const MWWorld::Ptr& actor) { const MWMechanics::MagicEffects& effects = actor.getClass().getCreatureStats(actor).getMagicEffects(); return effects.get(ESM::MagicEffect::WaterWalking).getMagnitude() > 0; } } ================================================ FILE: apps/openmw/mwmechanics/actorutil.hpp ================================================ #ifndef OPENMW_MWMECHANICS_ACTORUTIL_H #define OPENMW_MWMECHANICS_ACTORUTIL_H #include #include #include #include #include "../mwbase/environment.hpp" #include "../mwbase/world.hpp" #include "../mwworld/esmstore.hpp" #include "./creaturestats.hpp" namespace MWWorld { class Ptr; } namespace MWMechanics { enum GreetingState { Greet_None, Greet_InProgress, Greet_Done }; MWWorld::Ptr getPlayer(); bool isPlayerInCombat(); bool canActorMoveByZAxis(const MWWorld::Ptr& actor); bool hasWaterWalking(const MWWorld::Ptr& actor); template void setBaseAISetting(const std::string& id, MWMechanics::CreatureStats::AiSetting setting, int value) { T copy = *MWBase::Environment::get().getWorld()->getStore().get().find(id); switch(setting) { case MWMechanics::CreatureStats::AiSetting::AI_Hello: copy.mAiData.mHello = value; break; case MWMechanics::CreatureStats::AiSetting::AI_Fight: copy.mAiData.mFight = value; break; case MWMechanics::CreatureStats::AiSetting::AI_Flee: copy.mAiData.mFlee = value; break; case MWMechanics::CreatureStats::AiSetting::AI_Alarm: copy.mAiData.mAlarm = value; break; default: assert(0); } MWBase::Environment::get().getWorld()->createOverrideRecord(copy); } template void modifyBaseInventory(const std::string& actorId, const std::string& itemId, int amount) { T copy = *MWBase::Environment::get().getWorld()->getStore().get().find(actorId); for(auto& it : copy.mInventory.mList) { if(Misc::StringUtils::ciEqual(it.mItem, itemId)) { int sign = it.mCount < 1 ? -1 : 1; it.mCount = sign * std::max(it.mCount * sign + amount, 0); MWBase::Environment::get().getWorld()->createOverrideRecord(copy); return; } } if(amount > 0) { ESM::ContItem cont; cont.mItem = itemId; cont.mCount = amount; copy.mInventory.mList.push_back(cont); MWBase::Environment::get().getWorld()->createOverrideRecord(copy); } } template void setBaseAISetting(const std::string& id, MWMechanics::CreatureStats::AiSetting setting, int value); template void setBaseAISetting(const std::string& id, MWMechanics::CreatureStats::AiSetting setting, int value); template void modifyBaseInventory(const std::string& actorId, const std::string& itemId, int amount); template void modifyBaseInventory(const std::string& actorId, const std::string& itemId, int amount); template void modifyBaseInventory(const std::string& containerId, const std::string& itemId, int amount); } #endif ================================================ FILE: apps/openmw/mwmechanics/aiactivate.cpp ================================================ #include "aiactivate.hpp" #include /* Start of tes3mp addition Include additional headers for multiplayer purposes */ #include "../mwmp/Main.hpp" #include "../mwmp/Networking.hpp" #include "../mwmp/ObjectList.hpp" /* End of tes3mp addition */ #include "../mwbase/world.hpp" #include "../mwbase/environment.hpp" #include "../mwworld/class.hpp" #include "creaturestats.hpp" #include "movement.hpp" #include "steering.hpp" namespace MWMechanics { AiActivate::AiActivate(const std::string &objectId) : mObjectId(objectId) { } /* Start of tes3mp addition Allow AiActivate to be initialized using a Ptr instead of a refId */ AiActivate::AiActivate(MWWorld::Ptr object) : mObjectId("") { mObjectPtr = object; } /* End of tes3mp addition */ bool AiActivate::execute(const MWWorld::Ptr& actor, CharacterController& characterController, AiState& state, float duration) { /* Start of tes3mp change (major) Only search for an object based on its refId if we haven't provided a specific object already */ const MWWorld::Ptr target = mObjectId.empty() ? mObjectPtr : MWBase::Environment::get().getWorld()->searchPtr(mObjectId, false); /* End of tes3mp change (major) */ actor.getClass().getCreatureStats(actor).setDrawState(DrawState_Nothing); // Stop if the target doesn't exist // Really we should be checking whether the target is currently registered with the MechanicsManager if (target == MWWorld::Ptr() || !target.getRefData().getCount() || !target.getRefData().isEnabled()) return true; // Turn to target and move to it directly, without pathfinding. const osg::Vec3f targetDir = target.getRefData().getPosition().asVec3() - actor.getRefData().getPosition().asVec3(); zTurn(actor, std::atan2(targetDir.x(), targetDir.y()), 0.f); actor.getClass().getMovementSettings(actor).mPosition[1] = 1; actor.getClass().getMovementSettings(actor).mPosition[0] = 0; if (MWBase::Environment::get().getWorld()->getMaxActivationDistance() >= targetDir.length()) { /* Start of tes3mp addition Send an ID_OBJECT_ACTIVATE packet every time an object is activated here */ mwmp::ObjectList *objectList = mwmp::Main::get().getNetworking()->getObjectList(); objectList->reset(); objectList->packetOrigin = mwmp::CLIENT_GAMEPLAY; objectList->addObjectActivate(target, actor); objectList->sendObjectActivate(); /* End of tes3mp addition */ /* Start of tes3mp change (major) Disable unilateral activation on this client and expect the server's reply to our packet to do it instead Cancel the package to avoid an infinite activation loop, deviating from the behavior established in OpenMW in commit 48aba76ce904738d428e79f1ee24ce170f2a8309 */ //MWBase::Environment::get().getWorld()->activate(target, actor); return true; /* End of tes3mp change (major) */ } return false; } void AiActivate::writeState(ESM::AiSequence::AiSequence &sequence) const { std::unique_ptr activate(new ESM::AiSequence::AiActivate()); activate->mTargetId = mObjectId; ESM::AiSequence::AiPackageContainer package; package.mType = ESM::AiSequence::Ai_Activate; package.mPackage = activate.release(); sequence.mPackages.push_back(package); } AiActivate::AiActivate(const ESM::AiSequence::AiActivate *activate) : mObjectId(activate->mTargetId) { } } ================================================ FILE: apps/openmw/mwmechanics/aiactivate.hpp ================================================ #ifndef GAME_MWMECHANICS_AIACTIVATE_H #define GAME_MWMECHANICS_AIACTIVATE_H #include "typedaipackage.hpp" /* Start of tes3mp addition Include additional headers for multiplayer purposes */ #include "../mwworld/ptr.hpp" /* End of tes3mp addition */ #include #include "pathfinding.hpp" namespace ESM { namespace AiSequence { struct AiActivate; } } namespace MWMechanics { /// \brief Causes actor to walk to activatable object and activate it /** Will activate when close to object **/ class AiActivate final : public TypedAiPackage { public: /// Constructor /** \param objectId Reference to object to activate **/ explicit AiActivate(const std::string &objectId); /* Start of tes3mp addition Make it possible to initialize an AiActivate package with a specific Ptr as the target, allowing for more fine-tuned activation of objects */ AiActivate(MWWorld::Ptr object); /* End of tes3mp addition */ explicit AiActivate(const ESM::AiSequence::AiActivate* activate); bool execute (const MWWorld::Ptr& actor, CharacterController& characterController, AiState& state, float duration) override; static constexpr AiPackageTypeId getTypeId() { return AiPackageTypeId::Activate; } void writeState(ESM::AiSequence::AiSequence& sequence) const override; private: const std::string mObjectId; /* Start of tes3mp addition Track the object associated with this AiActivate package */ MWWorld::Ptr mObjectPtr; /* End of tes3mp addition */ }; } #endif // GAME_MWMECHANICS_AIACTIVATE_H ================================================ FILE: apps/openmw/mwmechanics/aiavoiddoor.cpp ================================================ #include "aiavoiddoor.hpp" #include #include "../mwbase/world.hpp" #include "../mwbase/environment.hpp" #include "../mwbase/mechanicsmanager.hpp" #include "../mwworld/class.hpp" #include "creaturestats.hpp" #include "movement.hpp" #include "actorutil.hpp" #include "steering.hpp" static const int MAX_DIRECTIONS = 4; MWMechanics::AiAvoidDoor::AiAvoidDoor(const MWWorld::ConstPtr& doorPtr) : mDuration(1), mDoorPtr(doorPtr), mDirection(0) { } bool MWMechanics::AiAvoidDoor::execute (const MWWorld::Ptr& actor, CharacterController& characterController, AiState& state, float duration) { ESM::Position pos = actor.getRefData().getPosition(); if(mDuration == 1) //If it just started, get the actor position as the stuck detection thing mLastPos = pos.asVec3(); mDuration -= duration; //Update timer if (mDuration < 0) { if (isStuck(pos.asVec3())) { adjustDirection(); mDuration = 1; //reset timer } else return true; // We have tried backing up for more than one second, we've probably cleared it } if (mDoorPtr.getClass().getDoorState(mDoorPtr) == MWWorld::DoorState::Idle) return true; //Door is no longer opening ESM::Position tPos = mDoorPtr.getRefData().getPosition(); //Position of the door float x = pos.pos[1] - tPos.pos[1]; float y = pos.pos[0] - tPos.pos[0]; actor.getClass().getCreatureStats(actor).setMovementFlag(CreatureStats::Flag_Run, true); // Turn away from the door and move when turn completed if (zTurn(actor, std::atan2(y,x) + getAdjustedAngle(), osg::DegreesToRadians(5.f))) actor.getClass().getMovementSettings(actor).mPosition[1] = 1; else actor.getClass().getMovementSettings(actor).mPosition[1] = 0; actor.getClass().getMovementSettings(actor).mPosition[0] = 0; // Make all nearby actors also avoid the door std::vector actors; MWBase::Environment::get().getMechanicsManager()->getActorsInRange(pos.asVec3(),100,actors); for(auto& neighbor : actors) { if (neighbor == getPlayer()) continue; MWMechanics::AiSequence& seq = neighbor.getClass().getCreatureStats(neighbor).getAiSequence(); if (seq.getTypeId() != MWMechanics::AiPackageTypeId::AvoidDoor) seq.stack(MWMechanics::AiAvoidDoor(mDoorPtr), neighbor); } return false; } bool MWMechanics::AiAvoidDoor::isStuck(const osg::Vec3f& actorPos) const { return (actorPos - mLastPos).length2() < 10 * 10; } void MWMechanics::AiAvoidDoor::adjustDirection() { mDirection = Misc::Rng::rollDice(MAX_DIRECTIONS); } float MWMechanics::AiAvoidDoor::getAdjustedAngle() const { return 2 * osg::PI / MAX_DIRECTIONS * mDirection; } ================================================ FILE: apps/openmw/mwmechanics/aiavoiddoor.hpp ================================================ #ifndef GAME_MWMECHANICS_AIAVOIDDOOR_H #define GAME_MWMECHANICS_AIAVOIDDOOR_H #include "typedaipackage.hpp" #include "../mwworld/class.hpp" #include "pathfinding.hpp" namespace MWMechanics { /// \brief AiPackage to have an actor avoid an opening door /** The AI will retreat from the door until it has finished opening, walked far away from it, or one second has passed, in an attempt to avoid it **/ class AiAvoidDoor final : public TypedAiPackage { public: /// Avoid door until the door is fully open explicit AiAvoidDoor(const MWWorld::ConstPtr& doorPtr); bool execute (const MWWorld::Ptr& actor, CharacterController& characterController, AiState& state, float duration) override; static constexpr AiPackageTypeId getTypeId() { return AiPackageTypeId::AvoidDoor; } static constexpr Options makeDefaultOptions() { AiPackage::Options options; options.mPriority = 2; options.mCanCancel = false; options.mShouldCancelPreviousAi = false; return options; } private: float mDuration; const MWWorld::ConstPtr mDoorPtr; osg::Vec3f mLastPos; int mDirection; bool isStuck(const osg::Vec3f& actorPos) const; void adjustDirection(); float getAdjustedAngle() const; }; } #endif ================================================ FILE: apps/openmw/mwmechanics/aibreathe.cpp ================================================ #include "aibreathe.hpp" #include "../mwbase/world.hpp" #include "../mwbase/environment.hpp" #include "../mwworld/class.hpp" #include "../mwworld/esmstore.hpp" #include "npcstats.hpp" #include "movement.hpp" #include "steering.hpp" bool MWMechanics::AiBreathe::execute (const MWWorld::Ptr& actor, CharacterController& characterController, AiState& state, float duration) { static const float fHoldBreathTime = MWBase::Environment::get().getWorld()->getStore().get().find("fHoldBreathTime")->mValue.getFloat(); const MWWorld::Class& actorClass = actor.getClass(); if (actorClass.isNpc()) { if (actorClass.getNpcStats(actor).getTimeToStartDrowning() < fHoldBreathTime / 2) { actorClass.getCreatureStats(actor).setMovementFlag(CreatureStats::Flag_Run, true); actorClass.getMovementSettings(actor).mPosition[1] = 1; smoothTurn(actor, static_cast(-osg::PI_2), 0); return false; } } return true; } ================================================ FILE: apps/openmw/mwmechanics/aibreathe.hpp ================================================ #ifndef GAME_MWMECHANICS_AIBREATHE_H #define GAME_MWMECHANICS_AIBREATHE_H #include "typedaipackage.hpp" namespace MWMechanics { /// \brief AiPackage to have an actor resurface to breathe // The AI will go up if lesser than half breath left class AiBreathe final : public TypedAiPackage { public: bool execute (const MWWorld::Ptr& actor, CharacterController& characterController, AiState& state, float duration) override; static constexpr AiPackageTypeId getTypeId() { return AiPackageTypeId::Breathe; } static constexpr Options makeDefaultOptions() { AiPackage::Options options; options.mPriority = 2; options.mCanCancel = false; options.mShouldCancelPreviousAi = false; return options; } }; } #endif ================================================ FILE: apps/openmw/mwmechanics/aicast.cpp ================================================ #include "aicast.hpp" #include #include "../mwbase/environment.hpp" #include "../mwbase/mechanicsmanager.hpp" #include "../mwbase/world.hpp" #include "../mwworld/class.hpp" #include "aicombataction.hpp" #include "creaturestats.hpp" #include "steering.hpp" namespace MWMechanics { namespace { float getInitialDistance(const std::string& spellId) { ActionSpell action = ActionSpell(spellId); bool isRanged; return action.getCombatRange(isRanged); } } } MWMechanics::AiCast::AiCast(const std::string& targetId, const std::string& spellId, bool manualSpell) : mTargetId(targetId), mSpellId(spellId), mCasting(false), mManual(manualSpell), mDistance(getInitialDistance(spellId)) { } bool MWMechanics::AiCast::execute(const MWWorld::Ptr& actor, MWMechanics::CharacterController& characterController, MWMechanics::AiState& state, float duration) { MWWorld::Ptr target; if (actor.getCellRef().getRefId() == mTargetId) { // If the target has the same ID as caster, consider that actor casts spell with Self range. target = actor; } else { target = getTarget(); if (!target) return true; if (!mManual && !pathTo(actor, target.getRefData().getPosition().asVec3(), duration, mDistance)) { return false; } } osg::Vec3f targetPos = target.getRefData().getPosition().asVec3(); // If the target of an on-target spell is an actor that is not the caster // the target position must be adjusted so that it's not casted at the actor's feet. if (target != actor && target.getClass().isActor()) { osg::Vec3f halfExtents = MWBase::Environment::get().getWorld()->getHalfExtents(target); targetPos.z() += halfExtents.z() * 2 * Constants::TorsoHeight; } osg::Vec3f actorPos = actor.getRefData().getPosition().asVec3(); osg::Vec3f halfExtents = MWBase::Environment::get().getWorld()->getHalfExtents(actor); actorPos.z() += halfExtents.z() * 2 * Constants::TorsoHeight; osg::Vec3f dir = targetPos - actorPos; bool turned = smoothTurn(actor, getZAngleToDir(dir), 2, osg::DegreesToRadians(3.f)); turned &= smoothTurn(actor, getXAngleToDir(dir), 0, osg::DegreesToRadians(3.f)); if (!turned) return false; // Check if the actor is already casting another spell bool isCasting = MWBase::Environment::get().getMechanicsManager()->isCastingSpell(actor); if (isCasting && !mCasting) return false; if (!mCasting) { MWBase::Environment::get().getMechanicsManager()->castSpell(actor, mSpellId, mManual); mCasting = true; return false; } // Finish package, if actor finished spellcasting return !isCasting; } MWWorld::Ptr MWMechanics::AiCast::getTarget() const { MWWorld::Ptr target = MWBase::Environment::get().getWorld()->searchPtr(mTargetId, false); return target; } ================================================ FILE: apps/openmw/mwmechanics/aicast.hpp ================================================ #ifndef GAME_MWMECHANICS_AICAST_H #define GAME_MWMECHANICS_AICAST_H #include "typedaipackage.hpp" namespace MWWorld { class Ptr; } namespace MWMechanics { /// AiPackage which makes an actor to cast given spell. class AiCast final : public TypedAiPackage { public: AiCast(const std::string& targetId, const std::string& spellId, bool manualSpell=false); bool execute (const MWWorld::Ptr& actor, CharacterController& characterController, AiState& state, float duration) override; static constexpr AiPackageTypeId getTypeId() { return AiPackageTypeId::Cast; } MWWorld::Ptr getTarget() const override; static constexpr Options makeDefaultOptions() { AiPackage::Options options; options.mPriority = 3; options.mCanCancel = false; options.mShouldCancelPreviousAi = false; return options; } private: const std::string mTargetId; const std::string mSpellId; bool mCasting; const bool mManual; const float mDistance; }; } #endif ================================================ FILE: apps/openmw/mwmechanics/aicombat.cpp ================================================ #include "aicombat.hpp" #include #include #include #include #include #include /* Start of tes3mp addition Include additional headers for multiplayer purposes */ #include #include "../mwmp/Main.hpp" #include "../mwmp/Networking.hpp" #include "../mwmp/ActorList.hpp" #include "../mwmp/MechanicsHelper.hpp" #include "../mwgui/windowmanagerimp.hpp" /* End of tes3mp addition */ #include "../mwphysics/collisiontype.hpp" #include "../mwworld/class.hpp" #include "../mwworld/esmstore.hpp" #include "../mwbase/environment.hpp" #include "../mwbase/dialoguemanager.hpp" #include "../mwbase/mechanicsmanager.hpp" #include "pathgrid.hpp" #include "creaturestats.hpp" #include "steering.hpp" #include "movement.hpp" #include "character.hpp" #include "aicombataction.hpp" #include "actorutil.hpp" namespace { //chooses an attack depending on probability to avoid uniformity std::string chooseBestAttack(const ESM::Weapon* weapon); osg::Vec3f AimDirToMovingTarget(const MWWorld::Ptr& actor, const MWWorld::Ptr& target, const osg::Vec3f& vLastTargetPos, float duration, int weapType, float strength); } namespace MWMechanics { AiCombat::AiCombat(const MWWorld::Ptr& actor) { mTargetActorId = actor.getClass().getCreatureStats(actor).getActorId(); } AiCombat::AiCombat(const ESM::AiSequence::AiCombat *combat) { mTargetActorId = combat->mTargetActorId; } void AiCombat::init() { } /* * Current AiCombat movement states (as of 0.29.0), ignoring the details of the * attack states such as CombatMove, Strike and ReadyToAttack: * * +----(within strike range)----->attack--(beyond strike range)-->follow * | | ^ | | * | | | | | * pursue<---(beyond follow range)-----+ +----(within strike range)---+ | * ^ | * | | * +-------------------------(beyond follow range)--------------------+ * * * Below diagram is high level only, the code detail is a little different * (but including those detail will just complicate the diagram w/o adding much) * * +----------(same)-------------->attack---------(same)---------->follow * | |^^ ||| * | ||| ||| * | +--(same)-----------------+|+----------(same)------------+|| * | | | || * | | | (in range) || * | <---+ (too far) | || * pursue<-------------------------[door open]<-----+ || * ^^^ | || * ||| | || * ||+----------evade-----+ | || * || | [closed door] | || * |+----> maybe stuck, check --------------> back up, check door || * | ^ | ^ | ^ || * | | | | | | || * | | +---+ +---+ || * | +-------------------------------------------------------+| * | | * +---------------------------(same)---------------------------------+ * * FIXME: * * The new scheme is way too complicated, should really be implemented as a * proper state machine. * * TODO: * * Use the observer pattern to coordinate attacks, provide intelligence on * whether the target was hit, etc. */ bool AiCombat::execute (const MWWorld::Ptr& actor, CharacterController& characterController, AiState& state, float duration) { // get or create temporary storage AiCombatStorage& storage = state.get(); //General description if (actor.getClass().getCreatureStats(actor).isDead()) return true; MWWorld::Ptr target = MWBase::Environment::get().getWorld()->searchPtrViaActorId(mTargetActorId); if (target.isEmpty()) return false; if(!target.getRefData().getCount() || !target.getRefData().isEnabled() // Really we should be checking whether the target is currently registered // with the MechanicsManager || target.getClass().getCreatureStats(target).isDead()) return true; if (actor == target) // This should never happen. return true; if (!storage.isFleeing()) { if (storage.mCurrentAction.get()) // need to wait to init action with its attack range { //Update every frame. UpdateLOS uses a timer, so the LOS check does not happen every frame. updateLOS(actor, target, duration, storage); const float targetReachedTolerance = storage.mLOS && !storage.mUseCustomDestination ? storage.mAttackRange : 0.0f; const osg::Vec3f destination = storage.mUseCustomDestination ? storage.mCustomDestination : target.getRefData().getPosition().asVec3(); const bool is_target_reached = pathTo(actor, destination, duration, targetReachedTolerance); if (is_target_reached) storage.mReadyToAttack = true; } storage.updateCombatMove(duration); if (storage.mReadyToAttack) updateActorsMovement(actor, duration, storage); storage.updateAttack(characterController); /* Start of tes3mp addition Record that this actor is updating an attack so that a packet will be sent about it */ mwmp::Attack *localAttack = MechanicsHelper::getLocalAttack(actor); if (localAttack && localAttack->pressed != storage.mAttack) { MechanicsHelper::resetAttack(localAttack); localAttack->pressed = storage.mAttack; localAttack->shouldSend = true; } /* End of tes3mp addition */ } else { updateFleeing(actor, target, duration, storage); } storage.mActionCooldown -= duration; if (storage.mReaction.update(duration) == Misc::TimerStatus::Waiting) return false; return attack(actor, target, storage, characterController); } bool AiCombat::attack(const MWWorld::Ptr& actor, const MWWorld::Ptr& target, AiCombatStorage& storage, CharacterController& characterController) { const MWWorld::CellStore*& currentCell = storage.mCell; bool cellChange = currentCell && (actor.getCell() != currentCell); if(!currentCell || cellChange) { currentCell = actor.getCell(); } /* Start of tes3mp addition Because multiplayer doesn't pause the world during dialogue, disallow attacks on a player engaged in dialogue */ if (target == MWBase::Environment::get().getWorld()->getPlayerPtr()) { if (MWBase::Environment::get().getWindowManager()->containsMode(MWGui::GM_Dialogue)) { storage.stopAttack(); return false; } } /* End of tes3mp addition */ bool forceFlee = false; if (!canFight(actor, target)) { storage.stopAttack(); characterController.setAttackingOrSpell(false); /* Start of tes3mp addition Record that this actor is stopping an attack so that a packet will be sent about it */ mwmp::Attack *localAttack = MechanicsHelper::getLocalAttack(actor); if (localAttack && localAttack->pressed != false) { MechanicsHelper::resetAttack(localAttack); localAttack->pressed = false; localAttack->shouldSend = true; } /* End of tes3mp addition */ storage.mActionCooldown = 0.f; // Continue combat if target is player or player follower/escorter and an attack has been attempted const std::list& playerFollowersAndEscorters = MWBase::Environment::get().getMechanicsManager()->getActorsSidingWith(MWMechanics::getPlayer()); bool targetSidesWithPlayer = (std::find(playerFollowersAndEscorters.begin(), playerFollowersAndEscorters.end(), target) != playerFollowersAndEscorters.end()); if ((target == MWMechanics::getPlayer() || targetSidesWithPlayer) && ((actor.getClass().getCreatureStats(actor).getHitAttemptActorId() == target.getClass().getCreatureStats(target).getActorId()) || (target.getClass().getCreatureStats(target).getHitAttemptActorId() == actor.getClass().getCreatureStats(actor).getActorId()))) forceFlee = true; else // Otherwise end combat return true; } const MWWorld::Class& actorClass = actor.getClass(); actorClass.getCreatureStats(actor).setMovementFlag(CreatureStats::Flag_Run, true); float& actionCooldown = storage.mActionCooldown; std::shared_ptr& currentAction = storage.mCurrentAction; if (!forceFlee) { if (actionCooldown > 0) return false; if (characterController.readyToPrepareAttack()) { currentAction = prepareNextAction(actor, target); actionCooldown = currentAction->getActionCooldown(); } } else { currentAction.reset(new ActionFlee()); actionCooldown = currentAction->getActionCooldown(); } if (!currentAction) return false; if (storage.isFleeing() != currentAction->isFleeing()) { if (currentAction->isFleeing()) { storage.startFleeing(); MWBase::Environment::get().getDialogueManager()->say(actor, "flee"); return false; } else storage.stopFleeing(); } bool isRangedCombat = false; float &rangeAttack = storage.mAttackRange; rangeAttack = currentAction->getCombatRange(isRangedCombat); // Get weapon characteristics const ESM::Weapon* weapon = currentAction->getWeapon(); ESM::Position pos = actor.getRefData().getPosition(); const osg::Vec3f vActorPos(pos.asVec3()); const osg::Vec3f vTargetPos(target.getRefData().getPosition().asVec3()); float distToTarget = MWBase::Environment::get().getWorld()->getHitDistance(actor, target); storage.mReadyToAttack = (currentAction->isAttackingOrSpell() && distToTarget <= rangeAttack && storage.mLOS); if (isRangedCombat) { // rotate actor taking into account target movement direction and projectile speed osg::Vec3f vAimDir = AimDirToMovingTarget(actor, target, storage.mLastTargetPos, AI_REACTION_TIME, (weapon ? weapon->mData.mType : 0), storage.mStrength); storage.mMovement.mRotation[0] = getXAngleToDir(vAimDir); storage.mMovement.mRotation[2] = getZAngleToDir(vAimDir); } else { osg::Vec3f vAimDir = MWBase::Environment::get().getWorld()->aimToTarget(actor, target, false); storage.mMovement.mRotation[0] = getXAngleToDir(vAimDir); storage.mMovement.mRotation[2] = getZAngleToDir((vTargetPos-vActorPos)); // using vAimDir results in spastic movements since the head is animated } storage.mLastTargetPos = vTargetPos; if (storage.mReadyToAttack) { storage.startCombatMove(isRangedCombat, distToTarget, rangeAttack, actor, target); // start new attack storage.startAttackIfReady(actor, characterController, weapon, isRangedCombat); } // If actor uses custom destination it has to try to rebuild path because environment can change // (door is opened between actor and target) or target position has changed and current custom destination // is not good enough to attack target. if (storage.mCurrentAction->isAttackingOrSpell() && ((!storage.mReadyToAttack && !mPathFinder.isPathConstructed()) || (storage.mUseCustomDestination && (storage.mCustomDestination - vTargetPos).length() > rangeAttack))) { const MWBase::World* world = MWBase::Environment::get().getWorld(); // Try to build path to the target. const auto halfExtents = world->getPathfindingHalfExtents(actor); const auto navigatorFlags = getNavigatorFlags(actor); const auto areaCosts = getAreaCosts(actor); const auto pathGridGraph = getPathGridGraph(actor.getCell()); mPathFinder.buildPath(actor, vActorPos, vTargetPos, actor.getCell(), pathGridGraph, halfExtents, navigatorFlags, areaCosts); if (!mPathFinder.isPathConstructed()) { // 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); if (hit.has_value() && (*hit - vTargetPos).length() <= rangeAttack) { // If the point is close enough, try to find a path to that point. mPathFinder.buildPath(actor, vActorPos, *hit, actor.getCell(), pathGridGraph, halfExtents, navigatorFlags, areaCosts); if (mPathFinder.isPathConstructed()) { // If path to that point is found use it as custom destination. storage.mCustomDestination = *hit; storage.mUseCustomDestination = true; } } if (!mPathFinder.isPathConstructed()) { storage.mUseCustomDestination = false; storage.stopAttack(); characterController.setAttackingOrSpell(false); currentAction.reset(new ActionFlee()); actionCooldown = currentAction->getActionCooldown(); storage.startFleeing(); MWBase::Environment::get().getDialogueManager()->say(actor, "flee"); } } else { storage.mUseCustomDestination = false; } } return false; } void MWMechanics::AiCombat::updateLOS(const MWWorld::Ptr& actor, const MWWorld::Ptr& target, float duration, MWMechanics::AiCombatStorage& storage) { static const float LOS_UPDATE_DURATION = 0.5f; if (storage.mUpdateLOSTimer <= 0.f) { storage.mLOS = MWBase::Environment::get().getWorld()->getLOS(actor, target); storage.mUpdateLOSTimer = LOS_UPDATE_DURATION; } else storage.mUpdateLOSTimer -= duration; } void MWMechanics::AiCombat::updateFleeing(const MWWorld::Ptr& actor, const MWWorld::Ptr& target, float duration, MWMechanics::AiCombatStorage& storage) { static const float BLIND_RUN_DURATION = 1.0f; updateLOS(actor, target, duration, storage); AiCombatStorage::FleeState& state = storage.mFleeState; switch (state) { case AiCombatStorage::FleeState_None: return; case AiCombatStorage::FleeState_Idle: { float triggerDist = getMaxAttackDistance(target); if (storage.mLOS && (triggerDist >= 1000 || getDistanceMinusHalfExtents(actor, target) <= triggerDist)) { const ESM::Pathgrid* pathgrid = MWBase::Environment::get().getWorld()->getStore().get().search(*storage.mCell->getCell()); bool runFallback = true; if (pathgrid != nullptr && !pathgrid->mPoints.empty() && !actor.getClass().isPureWaterCreature(actor)) { ESM::Pathgrid::PointList points; Misc::CoordinateConverter coords(storage.mCell->getCell()); osg::Vec3f localPos = actor.getRefData().getPosition().asVec3(); coords.toLocal(localPos); int closestPointIndex = PathFinder::getClosestPoint(pathgrid, localPos); for (int i = 0; i < static_cast(pathgrid->mPoints.size()); i++) { if (i != closestPointIndex && getPathGridGraph(storage.mCell).isPointConnected(closestPointIndex, i)) { points.push_back(pathgrid->mPoints[static_cast(i)]); } } if (!points.empty()) { ESM::Pathgrid::Point dest = points[Misc::Rng::rollDice(points.size())]; coords.toWorld(dest); state = AiCombatStorage::FleeState_RunToDestination; storage.mFleeDest = ESM::Pathgrid::Point(dest.mX, dest.mY, dest.mZ); runFallback = false; } } if (runFallback) { state = AiCombatStorage::FleeState_RunBlindly; storage.mFleeBlindRunTimer = 0.0f; } } } break; case AiCombatStorage::FleeState_RunBlindly: { // timer to prevent twitchy movement that can be observed in vanilla MW if (storage.mFleeBlindRunTimer < BLIND_RUN_DURATION) { storage.mFleeBlindRunTimer += duration; storage.mMovement.mRotation[0] = -actor.getRefData().getPosition().rot[0]; storage.mMovement.mRotation[2] = osg::PI + getZAngleToDir(target.getRefData().getPosition().asVec3()-actor.getRefData().getPosition().asVec3()); storage.mMovement.mPosition[1] = 1; updateActorsMovement(actor, duration, storage); } else state = AiCombatStorage::FleeState_Idle; } break; case AiCombatStorage::FleeState_RunToDestination: { static const float fFleeDistance = MWBase::Environment::get().getWorld()->getStore().get().find("fFleeDistance")->mValue.getFloat(); float dist = (actor.getRefData().getPosition().asVec3() - target.getRefData().getPosition().asVec3()).length(); if ((dist > fFleeDistance && !storage.mLOS) || pathTo(actor, PathFinder::makeOsgVec3(storage.mFleeDest), duration)) { state = AiCombatStorage::FleeState_Idle; } } break; }; } void AiCombat::updateActorsMovement(const MWWorld::Ptr& actor, float duration, AiCombatStorage& storage) { // apply combat movement float deltaAngle = storage.mMovement.mRotation[2] - actor.getRefData().getPosition().rot[2]; osg::Vec2f movement = Misc::rotateVec2f( osg::Vec2f(storage.mMovement.mPosition[0], storage.mMovement.mPosition[1]), -deltaAngle); MWMechanics::Movement& actorMovementSettings = actor.getClass().getMovementSettings(actor); actorMovementSettings.mPosition[0] = movement.x(); actorMovementSettings.mPosition[1] = movement.y(); actorMovementSettings.mPosition[2] = storage.mMovement.mPosition[2]; rotateActorOnAxis(actor, 2, actorMovementSettings, storage); rotateActorOnAxis(actor, 0, actorMovementSettings, storage); } void AiCombat::rotateActorOnAxis(const MWWorld::Ptr& actor, int axis, MWMechanics::Movement& actorMovementSettings, AiCombatStorage& storage) { actorMovementSettings.mRotation[axis] = 0; bool isRangedCombat = false; 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); } MWWorld::Ptr AiCombat::getTarget() const { return MWBase::Environment::get().getWorld()->searchPtrViaActorId(mTargetActorId); } void AiCombat::writeState(ESM::AiSequence::AiSequence &sequence) const { std::unique_ptr combat(new ESM::AiSequence::AiCombat()); combat->mTargetActorId = mTargetActorId; ESM::AiSequence::AiPackageContainer package; package.mType = ESM::AiSequence::Ai_Combat; package.mPackage = combat.release(); sequence.mPackages.push_back(package); } void AiCombatStorage::startCombatMove(bool isDistantCombat, float distToTarget, float rangeAttack, const MWWorld::Ptr& actor, const MWWorld::Ptr& target) { // get the range of the target's weapon MWWorld::Ptr targetWeapon = MWWorld::Ptr(); const MWWorld::Class& targetClass = target.getClass(); if (targetClass.hasInventoryStore(target)) { int weapType = ESM::Weapon::None; MWWorld::ContainerStoreIterator weaponSlot = MWMechanics::getActiveWeapon(target, &weapType); if (weapType > ESM::Weapon::None) targetWeapon = *weaponSlot; } bool targetUsesRanged = false; float rangeAttackOfTarget = ActionWeapon(targetWeapon).getCombatRange(targetUsesRanged); if (mMovement.mPosition[0] || mMovement.mPosition[1]) { mTimerCombatMove = 0.1f + 0.1f * Misc::Rng::rollClosedProbability(); mCombatMove = true; } else if (isDistantCombat) { // Backing up behaviour // Actor backs up slightly further away than opponent's weapon range // (in vanilla - only as far as oponent's weapon range), // or not at all if opponent is using a ranged weapon if (targetUsesRanged || distToTarget > rangeAttackOfTarget*1.5) // Don't back up if the target is wielding ranged weapon return; // actor should not back up into water if (MWBase::Environment::get().getWorld()->isUnderwater(MWWorld::ConstPtr(actor), 0.5f)) return; int mask = MWPhysics::CollisionType_World | MWPhysics::CollisionType_HeightMap | MWPhysics::CollisionType_Door; // Actor can not back up if there is no free space behind // Currently we take the 35% of actor's height from the ground as vector height. // This approach allows us to detect small obstacles (e.g. crates) and curved walls. osg::Vec3f halfExtents = MWBase::Environment::get().getWorld()->getHalfExtents(actor); osg::Vec3f pos = actor.getRefData().getPosition().asVec3(); osg::Vec3f source = pos + osg::Vec3f(0, 0, 0.75f * halfExtents.z()); osg::Vec3f fallbackDirection = actor.getRefData().getBaseNode()->getAttitude() * osg::Vec3f(0,-1,0); osg::Vec3f destination = source + fallbackDirection * (halfExtents.y() + 16); bool isObstacleDetected = MWBase::Environment::get().getWorld()->castRay(source.x(), source.y(), source.z(), destination.x(), destination.y(), destination.z(), mask); if (isObstacleDetected) return; // Check if there is nothing behind - probably actor is near cliff. // A current approach: cast ray 1.5-yard ray down in 1.5 yard behind actor from 35% of actor's height. // If we did not hit anything, there is a cliff behind actor. source = pos + osg::Vec3f(0, 0, 0.75f * halfExtents.z()) + fallbackDirection * (halfExtents.y() + 96); destination = source - osg::Vec3f(0, 0, 0.75f * halfExtents.z() + 96); bool isCliffDetected = !MWBase::Environment::get().getWorld()->castRay(source.x(), source.y(), source.z(), destination.x(), destination.y(), destination.z(), mask); if (isCliffDetected) return; mMovement.mPosition[1] = -1; } // dodge movements (for NPCs and bipedal creatures) // Note: do not use for ranged combat yet since in couple with back up behaviour can move actor out of cliff else if (actor.getClass().isBipedal(actor)) { float moveDuration = 0; float angleToTarget = Misc::normalizeAngle(mMovement.mRotation[2] - actor.getRefData().getPosition().rot[2]); // Apply a big side step if enemy tries to get around and come from behind. // Otherwise apply a random side step (kind of dodging) with some probability // if actor is within range of target's weapon. if (std::abs(angleToTarget) > osg::PI / 4) moveDuration = 0.2f; else if (distToTarget <= rangeAttackOfTarget && Misc::Rng::rollClosedProbability() < 0.25) moveDuration = 0.1f + 0.1f * Misc::Rng::rollClosedProbability(); if (moveDuration > 0) { mMovement.mPosition[0] = Misc::Rng::rollProbability() < 0.5 ? 1.0f : -1.0f; // to the left/right mTimerCombatMove = moveDuration; mCombatMove = true; } } } void AiCombatStorage::updateCombatMove(float duration) { if (mCombatMove) { mTimerCombatMove -= duration; if (mTimerCombatMove <= 0) { stopCombatMove(); } } } void AiCombatStorage::stopCombatMove() { mTimerCombatMove = 0; mMovement.mPosition[1] = mMovement.mPosition[0] = 0; mCombatMove = false; } void AiCombatStorage::startAttackIfReady(const MWWorld::Ptr& actor, CharacterController& characterController, const ESM::Weapon* weapon, bool distantCombat) { if (mReadyToAttack && characterController.readyToStartAttack()) { if (mAttackCooldown <= 0) { mAttack = true; // attack starts just now characterController.setAttackingOrSpell(true); if (!distantCombat) characterController.setAIAttackType(chooseBestAttack(weapon)); /* Start of tes3mp addition Record that this actor is starting an attack so that a packet will be sent about it */ mwmp::Attack *localAttack = MechanicsHelper::getLocalAttack(actor); if (localAttack && localAttack->pressed != true) { MechanicsHelper::resetAttack(localAttack); localAttack->type = distantCombat ? mwmp::Attack::RANGED : mwmp::Attack::MELEE; localAttack->attackAnimation = characterController.getAttackType(); localAttack->pressed = true; mwmp::ActorList *actorList = mwmp::Main::get().getNetworking()->getActorList(); actorList->reset(); actorList->cell = *actor.getCell()->getCell(); actorList->addAttackActor(actor, *localAttack); actorList->sendAttackActors(); } /* End of tes3mp addition */ mStrength = Misc::Rng::rollClosedProbability(); const MWWorld::ESMStore &store = MWBase::Environment::get().getWorld()->getStore(); float baseDelay = store.get().find("fCombatDelayCreature")->mValue.getFloat(); if (actor.getClass().isNpc()) { baseDelay = store.get().find("fCombatDelayNPC")->mValue.getFloat(); } // Say a provoking combat phrase const int iVoiceAttackOdds = store.get().find("iVoiceAttackOdds")->mValue.getInteger(); if (Misc::Rng::roll0to99() < iVoiceAttackOdds) { MWBase::Environment::get().getDialogueManager()->say(actor, "attack"); } mAttackCooldown = std::min(baseDelay + 0.01 * Misc::Rng::roll0to99(), baseDelay + 0.9); } else mAttackCooldown -= AI_REACTION_TIME; } } void AiCombatStorage::updateAttack(CharacterController& characterController) { if (mAttack && (characterController.getAttackStrength() >= mStrength || characterController.readyToPrepareAttack())) { mAttack = false; } characterController.setAttackingOrSpell(mAttack); } void AiCombatStorage::stopAttack() { mMovement.mPosition[0] = 0; mMovement.mPosition[1] = 0; mMovement.mPosition[2] = 0; mReadyToAttack = false; mAttack = false; } void AiCombatStorage::startFleeing() { stopFleeing(); mFleeState = FleeState_Idle; } void AiCombatStorage::stopFleeing() { mMovement.mPosition[0] = 0; mMovement.mPosition[1] = 0; mMovement.mPosition[2] = 0; mFleeState = FleeState_None; mFleeDest = ESM::Pathgrid::Point(0, 0, 0); } bool AiCombatStorage::isFleeing() { return mFleeState != FleeState_None; } } namespace { std::string chooseBestAttack(const ESM::Weapon* weapon) { std::string attackType; if (weapon != nullptr) { //the more damage attackType deals the more probability it has int slash = (weapon->mData.mSlash[0] + weapon->mData.mSlash[1])/2; int chop = (weapon->mData.mChop[0] + weapon->mData.mChop[1])/2; int thrust = (weapon->mData.mThrust[0] + weapon->mData.mThrust[1])/2; float roll = Misc::Rng::rollClosedProbability() * (slash + chop + thrust); if(roll <= slash) attackType = "slash"; else if(roll <= (slash + thrust)) attackType = "thrust"; else attackType = "chop"; } else MWMechanics::CharacterController::setAttackTypeRandomly(attackType); return attackType; } osg::Vec3f AimDirToMovingTarget(const MWWorld::Ptr& actor, const MWWorld::Ptr& target, const osg::Vec3f& vLastTargetPos, float duration, int weapType, float strength) { float projSpeed; const MWWorld::Store& gmst = MWBase::Environment::get().getWorld()->getStore().get(); // get projectile speed (depending on weapon type) if (MWMechanics::getWeaponType(weapType)->mWeaponClass == ESM::WeaponType::Thrown) { static float fThrownWeaponMinSpeed = gmst.find("fThrownWeaponMinSpeed")->mValue.getFloat(); static float fThrownWeaponMaxSpeed = gmst.find("fThrownWeaponMaxSpeed")->mValue.getFloat(); projSpeed = fThrownWeaponMinSpeed + (fThrownWeaponMaxSpeed - fThrownWeaponMinSpeed) * strength; } else if (weapType != 0) { static float fProjectileMinSpeed = gmst.find("fProjectileMinSpeed")->mValue.getFloat(); static float fProjectileMaxSpeed = gmst.find("fProjectileMaxSpeed")->mValue.getFloat(); projSpeed = fProjectileMinSpeed + (fProjectileMaxSpeed - fProjectileMinSpeed) * strength; } else // weapType is 0 ==> it's a target spell projectile { projSpeed = gmst.find("fTargetSpellMaxSpeed")->mValue.getFloat(); } // idea: perpendicular to dir to target speed components of target move vector and projectile vector should be the same osg::Vec3f vTargetPos = target.getRefData().getPosition().asVec3(); osg::Vec3f vDirToTarget = MWBase::Environment::get().getWorld()->aimToTarget(actor, target, true); float distToTarget = vDirToTarget.length(); osg::Vec3f vTargetMoveDir = vTargetPos - vLastTargetPos; vTargetMoveDir /= duration; // |vTargetMoveDir| is target real speed in units/sec now osg::Vec3f vPerpToDir = vDirToTarget ^ osg::Vec3f(0,0,1); // cross product vPerpToDir.normalize(); osg::Vec3f vDirToTargetNormalized = vDirToTarget; vDirToTargetNormalized.normalize(); // dot product float velPerp = vTargetMoveDir * vPerpToDir; float velDir = vTargetMoveDir * vDirToTargetNormalized; // time to collision between target and projectile float t_collision; float projVelDirSquared = projSpeed * projSpeed - velPerp * velPerp; if (projVelDirSquared > 0) { osg::Vec3f vTargetMoveDirNormalized = vTargetMoveDir; vTargetMoveDirNormalized.normalize(); float projDistDiff = vDirToTarget * vTargetMoveDirNormalized; // dot product projDistDiff = std::sqrt(distToTarget * distToTarget - projDistDiff * projDistDiff); t_collision = projDistDiff / (std::sqrt(projVelDirSquared) - velDir); } else t_collision = 0; // speed of projectile is not enough to reach moving target return vDirToTarget + vTargetMoveDir * t_collision; } } ================================================ FILE: apps/openmw/mwmechanics/aicombat.hpp ================================================ #ifndef GAME_MWMECHANICS_AICOMBAT_H #define GAME_MWMECHANICS_AICOMBAT_H #include "typedaipackage.hpp" #include "../mwworld/cellstore.hpp" // for Doors #include "../mwbase/world.hpp" #include "pathfinding.hpp" #include "movement.hpp" #include "aitimer.hpp" namespace ESM { namespace AiSequence { struct AiCombat; } } namespace MWMechanics { class Action; /// \brief This class holds the variables AiCombat needs which are deleted if the package becomes inactive. struct AiCombatStorage : AiTemporaryBase { float mAttackCooldown; AiReactionTimer mReaction; float mTimerCombatMove; bool mReadyToAttack; bool mAttack; float mAttackRange; bool mCombatMove; osg::Vec3f mLastTargetPos; const MWWorld::CellStore* mCell; std::shared_ptr mCurrentAction; float mActionCooldown; float mStrength; bool mForceNoShortcut; ESM::Position mShortcutFailPos; MWMechanics::Movement mMovement; enum FleeState { FleeState_None, FleeState_Idle, FleeState_RunBlindly, FleeState_RunToDestination }; FleeState mFleeState; bool mLOS; float mUpdateLOSTimer; float mFleeBlindRunTimer; ESM::Pathgrid::Point mFleeDest; bool mUseCustomDestination; osg::Vec3f mCustomDestination; AiCombatStorage(): mAttackCooldown(0.0f), mTimerCombatMove(0.0f), mReadyToAttack(false), mAttack(false), mAttackRange(0.0f), mCombatMove(false), mLastTargetPos(0,0,0), mCell(nullptr), mCurrentAction(), mActionCooldown(0.0f), mStrength(), mForceNoShortcut(false), mShortcutFailPos(), mMovement(), mFleeState(FleeState_None), mLOS(false), mUpdateLOSTimer(0.0f), mFleeBlindRunTimer(0.0f), mUseCustomDestination(false), mCustomDestination() {} void startCombatMove(bool isDistantCombat, float distToTarget, float rangeAttack, const MWWorld::Ptr& actor, const MWWorld::Ptr& target); void updateCombatMove(float duration); void stopCombatMove(); void startAttackIfReady(const MWWorld::Ptr& actor, CharacterController& characterController, const ESM::Weapon* weapon, bool distantCombat); void updateAttack(CharacterController& characterController); void stopAttack(); void startFleeing(); void stopFleeing(); bool isFleeing(); }; /// \brief Causes the actor to fight another actor class AiCombat final : public TypedAiPackage { public: ///Constructor /** \param actor Actor to fight **/ explicit AiCombat(const MWWorld::Ptr& actor); explicit AiCombat (const ESM::AiSequence::AiCombat* combat); void init(); bool execute (const MWWorld::Ptr& actor, CharacterController& characterController, AiState& state, float duration) override; static constexpr AiPackageTypeId getTypeId() { return AiPackageTypeId::Combat; } static constexpr Options makeDefaultOptions() { AiPackage::Options options; options.mPriority = 1; options.mCanCancel = false; options.mShouldCancelPreviousAi = false; return options; } ///Returns target ID MWWorld::Ptr getTarget() const override; void writeState(ESM::AiSequence::AiSequence &sequence) const override; private: /// Returns true if combat should end bool attack(const MWWorld::Ptr& actor, const MWWorld::Ptr& target, AiCombatStorage& storage, CharacterController& characterController); void updateLOS(const MWWorld::Ptr& actor, const MWWorld::Ptr& target, float duration, AiCombatStorage& storage); void updateFleeing(const MWWorld::Ptr& actor, const MWWorld::Ptr& target, float duration, AiCombatStorage& storage); /// Transfer desired movement (from AiCombatStorage) to Actor void updateActorsMovement(const MWWorld::Ptr& actor, float duration, AiCombatStorage& storage); void rotateActorOnAxis(const MWWorld::Ptr& actor, int axis, MWMechanics::Movement& actorMovementSettings, AiCombatStorage& storage); }; } #endif ================================================ FILE: apps/openmw/mwmechanics/aicombataction.cpp ================================================ #include "aicombataction.hpp" #include #include #include "../mwbase/environment.hpp" #include "../mwbase/world.hpp" #include "../mwbase/mechanicsmanager.hpp" #include "../mwworld/class.hpp" #include "../mwworld/esmstore.hpp" #include "../mwworld/inventorystore.hpp" #include "../mwworld/actionequip.hpp" #include "../mwworld/cellstore.hpp" #include "npcstats.hpp" #include "combat.hpp" #include "weaponpriority.hpp" #include "spellpriority.hpp" #include "weapontype.hpp" namespace MWMechanics { float suggestCombatRange(int rangeTypes) { static const float fCombatDistance = MWBase::Environment::get().getWorld()->getStore().get().find("fCombatDistance")->mValue.getFloat(); static float fHandToHandReach = MWBase::Environment::get().getWorld()->getStore().get().find("fHandToHandReach")->mValue.getFloat(); // This distance is a possible distance of melee attack static float distance = fCombatDistance * std::max(2.f, fHandToHandReach); if (rangeTypes & RangeTypes::Touch) { return fCombatDistance; } return distance * 4; } void ActionSpell::prepare(const MWWorld::Ptr &actor) { actor.getClass().getCreatureStats(actor).getSpells().setSelectedSpell(mSpellId); actor.getClass().getCreatureStats(actor).setDrawState(DrawState_Spell); if (actor.getClass().hasInventoryStore(actor)) { MWWorld::InventoryStore& inv = actor.getClass().getInventoryStore(actor); inv.setSelectedEnchantItem(inv.end()); } const ESM::Spell* spell = MWBase::Environment::get().getWorld()->getStore().get().find(mSpellId); MWBase::Environment::get().getWorld()->preloadEffects(&spell->mEffects); } float ActionSpell::getCombatRange (bool& isRanged) const { const ESM::Spell* spell = MWBase::Environment::get().getWorld()->getStore().get().find(mSpellId); int types = getRangeTypes(spell->mEffects); isRanged = (types & RangeTypes::Target) | (types & RangeTypes::Self); return suggestCombatRange(types); } void ActionEnchantedItem::prepare(const MWWorld::Ptr &actor) { actor.getClass().getCreatureStats(actor).getSpells().setSelectedSpell(std::string()); actor.getClass().getInventoryStore(actor).setSelectedEnchantItem(mItem); actor.getClass().getCreatureStats(actor).setDrawState(DrawState_Spell); } float ActionEnchantedItem::getCombatRange(bool& isRanged) const { const ESM::Enchantment* enchantment = MWBase::Environment::get().getWorld()->getStore().get().find(mItem->getClass().getEnchantment(*mItem)); int types = getRangeTypes(enchantment->mEffects); isRanged = (types & RangeTypes::Target) | (types & RangeTypes::Self); return suggestCombatRange(types); } float ActionPotion::getCombatRange(bool& isRanged) const { // Distance doesn't matter since this action has no animation // If we want to back away slightly to avoid enemy hits, we should set isRanged to "true" return 600.f; } void ActionPotion::prepare(const MWWorld::Ptr &actor) { actor.getClass().apply(actor, mPotion.getCellRef().getRefId(), actor); actor.getClass().getContainerStore(actor).remove(mPotion, 1, actor); } void ActionWeapon::prepare(const MWWorld::Ptr &actor) { if (actor.getClass().hasInventoryStore(actor)) { if (mWeapon.isEmpty()) actor.getClass().getInventoryStore(actor).unequipSlot(MWWorld::InventoryStore::Slot_CarriedRight, actor); else { MWWorld::ActionEquip equip(mWeapon); equip.execute(actor); } if (!mAmmunition.isEmpty()) { MWWorld::ActionEquip equip(mAmmunition); equip.execute(actor); } } actor.getClass().getCreatureStats(actor).setDrawState(DrawState_Weapon); } float ActionWeapon::getCombatRange(bool& isRanged) const { isRanged = false; static const float fCombatDistance = MWBase::Environment::get().getWorld()->getStore().get().find("fCombatDistance")->mValue.getFloat(); static const float fProjectileMaxSpeed = MWBase::Environment::get().getWorld()->getStore().get().find("fProjectileMaxSpeed")->mValue.getFloat(); if (mWeapon.isEmpty()) { static float fHandToHandReach = MWBase::Environment::get().getWorld()->getStore().get().find("fHandToHandReach")->mValue.getFloat(); return fHandToHandReach * fCombatDistance; } const ESM::Weapon* weapon = mWeapon.get()->mBase; if (MWMechanics::getWeaponType(weapon->mData.mType)->mWeaponClass != ESM::WeaponType::Melee) { isRanged = true; return fProjectileMaxSpeed; } else return weapon->mData.mReach * fCombatDistance; } const ESM::Weapon* ActionWeapon::getWeapon() const { if (mWeapon.isEmpty()) return nullptr; return mWeapon.get()->mBase; } std::shared_ptr prepareNextAction(const MWWorld::Ptr &actor, const MWWorld::Ptr &enemy) { Spells& spells = actor.getClass().getCreatureStats(actor).getSpells(); float bestActionRating = 0.f; float antiFleeRating = 0.f; // Default to hand-to-hand combat std::shared_ptr bestAction (new ActionWeapon(MWWorld::Ptr())); if (actor.getClass().isNpc() && actor.getClass().getNpcStats(actor).isWerewolf()) { bestAction->prepare(actor); return bestAction; } if (actor.getClass().hasInventoryStore(actor)) { MWWorld::InventoryStore& store = actor.getClass().getInventoryStore(actor); for (MWWorld::ContainerStoreIterator it = store.begin(); it != store.end(); ++it) { float rating = ratePotion(*it, actor); if (rating > bestActionRating) { bestActionRating = rating; bestAction.reset(new ActionPotion(*it)); antiFleeRating = std::numeric_limits::max(); } } for (MWWorld::ContainerStoreIterator it = store.begin(); it != store.end(); ++it) { float rating = rateMagicItem(*it, actor, enemy); if (rating > bestActionRating) { bestActionRating = rating; bestAction.reset(new ActionEnchantedItem(it)); antiFleeRating = std::numeric_limits::max(); } } MWWorld::Ptr bestArrow; float bestArrowRating = rateAmmo(actor, enemy, bestArrow, ESM::Weapon::Arrow); MWWorld::Ptr bestBolt; float bestBoltRating = rateAmmo(actor, enemy, bestBolt, ESM::Weapon::Bolt); for (MWWorld::ContainerStoreIterator it = store.begin(); it != store.end(); ++it) { float rating = rateWeapon(*it, actor, enemy, -1, bestArrowRating, bestBoltRating); if (rating > bestActionRating) { const ESM::Weapon* weapon = it->get()->mBase; int ammotype = getWeaponType(weapon->mData.mType)->mAmmoType; MWWorld::Ptr ammo; if (ammotype == ESM::Weapon::Arrow) ammo = bestArrow; else if (ammotype == ESM::Weapon::Bolt) ammo = bestBolt; bestActionRating = rating; bestAction.reset(new ActionWeapon(*it, ammo)); antiFleeRating = vanillaRateWeaponAndAmmo(*it, ammo, actor, enemy); } } } for (Spells::TIterator it = spells.begin(); it != spells.end(); ++it) { float rating = rateSpell(it->first, actor, enemy); if (rating > bestActionRating) { bestActionRating = rating; bestAction.reset(new ActionSpell(it->first->mId)); antiFleeRating = vanillaRateSpell(it->first, actor, enemy); } } if (makeFleeDecision(actor, enemy, antiFleeRating)) bestAction.reset(new ActionFlee()); if (bestAction.get()) bestAction->prepare(actor); return bestAction; } float getBestActionRating(const MWWorld::Ptr &actor, const MWWorld::Ptr &enemy) { Spells& spells = actor.getClass().getCreatureStats(actor).getSpells(); float bestActionRating = 0.f; // Default to hand-to-hand combat if (actor.getClass().isNpc() && actor.getClass().getNpcStats(actor).isWerewolf()) { return bestActionRating; } if (actor.getClass().hasInventoryStore(actor)) { MWWorld::InventoryStore& store = actor.getClass().getInventoryStore(actor); for (MWWorld::ContainerStoreIterator it = store.begin(); it != store.end(); ++it) { float rating = rateMagicItem(*it, actor, enemy); if (rating > bestActionRating) { bestActionRating = rating; } } float bestArrowRating = rateAmmo(actor, enemy, ESM::Weapon::Arrow); float bestBoltRating = rateAmmo(actor, enemy, ESM::Weapon::Bolt); for (MWWorld::ContainerStoreIterator it = store.begin(); it != store.end(); ++it) { float rating = rateWeapon(*it, actor, enemy, -1, bestArrowRating, bestBoltRating); if (rating > bestActionRating) { bestActionRating = rating; } } } for (Spells::TIterator it = spells.begin(); it != spells.end(); ++it) { float rating = rateSpell(it->first, actor, enemy); if (rating > bestActionRating) { bestActionRating = rating; } } return bestActionRating; } float getDistanceMinusHalfExtents(const MWWorld::Ptr& actor1, const MWWorld::Ptr& actor2, bool minusZDist) { osg::Vec3f actor1Pos = actor1.getRefData().getPosition().asVec3(); osg::Vec3f actor2Pos = actor2.getRefData().getPosition().asVec3(); float dist = (actor1Pos - actor2Pos).length(); if (minusZDist) dist -= std::abs(actor1Pos.z() - actor2Pos.z()); return (dist - MWBase::Environment::get().getWorld()->getHalfExtents(actor1).y() - MWBase::Environment::get().getWorld()->getHalfExtents(actor2).y()); } float getMaxAttackDistance(const MWWorld::Ptr& actor) { const CreatureStats& stats = actor.getClass().getCreatureStats(actor); const MWWorld::Store& gmst = MWBase::Environment::get().getWorld()->getStore().get(); std::string selectedSpellId = stats.getSpells().getSelectedSpell(); MWWorld::Ptr selectedEnchItem; MWWorld::Ptr activeWeapon, activeAmmo; if (actor.getClass().hasInventoryStore(actor)) { MWWorld::InventoryStore& invStore = actor.getClass().getInventoryStore(actor); MWWorld::ContainerStoreIterator item = invStore.getSlot(MWWorld::InventoryStore::Slot_CarriedRight); if (item != invStore.end() && item.getType() == MWWorld::ContainerStore::Type_Weapon) activeWeapon = *item; item = invStore.getSlot(MWWorld::InventoryStore::Slot_Ammunition); if (item != invStore.end() && item.getType() == MWWorld::ContainerStore::Type_Weapon) activeAmmo = *item; if (invStore.getSelectedEnchantItem() != invStore.end()) selectedEnchItem = *invStore.getSelectedEnchantItem(); } float dist = 1.0f; if (activeWeapon.isEmpty() && !selectedSpellId.empty() && !selectedEnchItem.isEmpty()) { static const float fHandToHandReach = gmst.find("fHandToHandReach")->mValue.getFloat(); dist = fHandToHandReach; } else if (stats.getDrawState() == MWMechanics::DrawState_Spell) { dist = 1.0f; if (!selectedSpellId.empty()) { const ESM::Spell* spell = MWBase::Environment::get().getWorld()->getStore().get().find(selectedSpellId); for (std::vector::const_iterator effectIt = spell->mEffects.mList.begin(); effectIt != spell->mEffects.mList.end(); ++effectIt) { if (effectIt->mRange == ESM::RT_Target) { const ESM::MagicEffect* effect = MWBase::Environment::get().getWorld()->getStore().get().find(effectIt->mEffectID); dist = effect->mData.mSpeed; break; } } } else if (!selectedEnchItem.isEmpty()) { std::string enchId = selectedEnchItem.getClass().getEnchantment(selectedEnchItem); if (!enchId.empty()) { const ESM::Enchantment* ench = MWBase::Environment::get().getWorld()->getStore().get().find(enchId); for (std::vector::const_iterator effectIt = ench->mEffects.mList.begin(); effectIt != ench->mEffects.mList.end(); ++effectIt) { if (effectIt->mRange == ESM::RT_Target) { const ESM::MagicEffect* effect = MWBase::Environment::get().getWorld()->getStore().get().find(effectIt->mEffectID); dist = effect->mData.mSpeed; break; } } } } static const float fTargetSpellMaxSpeed = gmst.find("fTargetSpellMaxSpeed")->mValue.getFloat(); dist *= std::max(1000.0f, fTargetSpellMaxSpeed); } else if (!activeWeapon.isEmpty()) { const ESM::Weapon* esmWeap = activeWeapon.get()->mBase; if (MWMechanics::getWeaponType(esmWeap->mData.mType)->mWeaponClass != ESM::WeaponType::Melee) { static const float fTargetSpellMaxSpeed = gmst.find("fProjectileMaxSpeed")->mValue.getFloat(); dist = fTargetSpellMaxSpeed; if (!activeAmmo.isEmpty()) { const ESM::Weapon* esmAmmo = activeAmmo.get()->mBase; dist *= esmAmmo->mData.mSpeed; } } else if (esmWeap->mData.mReach > 1) { dist = esmWeap->mData.mReach; } } dist = (dist > 0.f) ? dist : 1.0f; static const float fCombatDistance = gmst.find("fCombatDistance")->mValue.getFloat(); static const float fCombatDistanceWerewolfMod = gmst.find("fCombatDistanceWerewolfMod")->mValue.getFloat(); float combatDistance = fCombatDistance; if (actor.getClass().isNpc() && actor.getClass().getNpcStats(actor).isWerewolf()) combatDistance *= (fCombatDistanceWerewolfMod + 1.0f); if (dist < combatDistance) dist *= combatDistance; return dist; } bool canFight(const MWWorld::Ptr& actor, const MWWorld::Ptr& enemy) { ESM::Position actorPos = actor.getRefData().getPosition(); ESM::Position enemyPos = enemy.getRefData().getPosition(); if (isTargetMagicallyHidden(enemy) && !MWBase::Environment::get().getMechanicsManager()->awarenessCheck(enemy, actor)) { return false; } if (actor.getClass().isPureWaterCreature(actor)) { if (!MWBase::Environment::get().getWorld()->isWading(enemy)) return false; } float atDist = getMaxAttackDistance(actor); if (atDist > getDistanceMinusHalfExtents(actor, enemy) && atDist > std::abs(actorPos.pos[2] - enemyPos.pos[2])) { if (MWBase::Environment::get().getWorld()->getLOS(actor, enemy)) return true; } if (actor.getClass().isPureLandCreature(actor) && MWBase::Environment::get().getWorld()->isWalkingOnWater(enemy)) { return false; } if (actor.getClass().isPureFlyingCreature(actor) || actor.getClass().isPureLandCreature(actor)) { if (MWBase::Environment::get().getWorld()->isSwimming(enemy)) return false; } if (actor.getClass().isBipedal(actor) || !actor.getClass().canFly(actor)) { if (enemy.getClass().getCreatureStats(enemy).getMagicEffects().get(ESM::MagicEffect::Levitate).getMagnitude() > 0) { float attackDistance = getMaxAttackDistance(actor); if ((attackDistance + actorPos.pos[2]) < enemyPos.pos[2]) { if (enemy.getCell()->isExterior()) { if (attackDistance < (enemyPos.pos[2] - MWBase::Environment::get().getWorld()->getTerrainHeightAt(enemyPos.asVec3()))) return false; } } } } if (!actor.getClass().canWalk(actor) && !actor.getClass().isBipedal(actor)) return true; if (actor.getClass().getCreatureStats(actor).getMagicEffects().get(ESM::MagicEffect::Levitate).getMagnitude() > 0) return true; if (MWBase::Environment::get().getWorld()->isSwimming(actor)) return true; if (getDistanceMinusHalfExtents(actor, enemy, true) <= 0.0f) return false; return true; } float vanillaRateFlee(const MWWorld::Ptr& actor, const MWWorld::Ptr& enemy) { const CreatureStats& stats = actor.getClass().getCreatureStats(actor); const MWWorld::Store& gmst = MWBase::Environment::get().getWorld()->getStore().get(); int flee = stats.getAiSetting(CreatureStats::AI_Flee).getModified(); if (flee >= 100) return flee; static const float fAIFleeHealthMult = gmst.find("fAIFleeHealthMult")->mValue.getFloat(); static const float fAIFleeFleeMult = gmst.find("fAIFleeFleeMult")->mValue.getFloat(); float healthPercentage = (stats.getHealth().getModified() == 0.0f) ? 1.0f : stats.getHealth().getCurrent() / stats.getHealth().getModified(); float rating = (1.0f - healthPercentage) * fAIFleeHealthMult + flee * fAIFleeFleeMult; static const int iWereWolfLevelToAttack = gmst.find("iWereWolfLevelToAttack")->mValue.getInteger(); if (actor.getClass().isNpc() && enemy.getClass().isNpc()) { if (enemy.getClass().getNpcStats(enemy).isWerewolf() && stats.getLevel() < iWereWolfLevelToAttack) { static const int iWereWolfFleeMod = gmst.find("iWereWolfFleeMod")->mValue.getInteger(); rating = iWereWolfFleeMod; } } if (rating != 0.0f) rating += getFightDistanceBias(actor, enemy); return rating; } bool makeFleeDecision(const MWWorld::Ptr& actor, const MWWorld::Ptr& enemy, float antiFleeRating) { float fleeRating = vanillaRateFlee(actor, enemy); if (fleeRating < 100.0f) fleeRating = 0.0f; if (fleeRating > antiFleeRating) return true; // Run away after summoning a creature if we have nothing to use but fists. if (antiFleeRating == 0.0f && !actor.getClass().getCreatureStats(actor).getSummonedCreatureMap().empty()) return true; return false; } } ================================================ FILE: apps/openmw/mwmechanics/aicombataction.hpp ================================================ #ifndef OPENMW_AICOMBAT_ACTION_H #define OPENMW_AICOMBAT_ACTION_H #include #include "../mwworld/ptr.hpp" #include "../mwworld/containerstore.hpp" namespace MWMechanics { class Action { public: virtual ~Action() {} virtual void prepare(const MWWorld::Ptr& actor) = 0; virtual float getCombatRange (bool& isRanged) const = 0; virtual float getActionCooldown() { return 0.f; } virtual const ESM::Weapon* getWeapon() const { return nullptr; } virtual bool isAttackingOrSpell() const { return true; } virtual bool isFleeing() const { return false; } }; class ActionFlee : public Action { public: ActionFlee() {} void prepare(const MWWorld::Ptr& actor) override {} float getCombatRange (bool& isRanged) const override { return 0.0f; } float getActionCooldown() override { return 3.0f; } bool isAttackingOrSpell() const override { return false; } bool isFleeing() const override { return true; } }; class ActionSpell : public Action { public: ActionSpell(const std::string& spellId) : mSpellId(spellId) {} std::string mSpellId; /// Sets the given spell as selected on the actor's spell list. void prepare(const MWWorld::Ptr& actor) override; float getCombatRange (bool& isRanged) const override; }; class ActionEnchantedItem : public Action { public: ActionEnchantedItem(const MWWorld::ContainerStoreIterator& item) : mItem(item) {} MWWorld::ContainerStoreIterator mItem; /// Sets the given item as selected enchanted item in the actor's InventoryStore. void prepare(const MWWorld::Ptr& actor) override; float getCombatRange (bool& isRanged) const override; /// Since this action has no animation, apply a small cool down for using it float getActionCooldown() override { return 0.75f; } }; class ActionPotion : public Action { public: ActionPotion(const MWWorld::Ptr& potion) : mPotion(potion) {} MWWorld::Ptr mPotion; /// Drinks the given potion. void prepare(const MWWorld::Ptr& actor) override; float getCombatRange (bool& isRanged) const override; bool isAttackingOrSpell() const override { return false; } /// Since this action has no animation, apply a small cool down for using it float getActionCooldown() override { return 0.75f; } }; class ActionWeapon : public Action { private: MWWorld::Ptr mAmmunition; MWWorld::Ptr mWeapon; public: /// \a weapon may be empty for hand-to-hand combat ActionWeapon(const MWWorld::Ptr& weapon, const MWWorld::Ptr& ammo = MWWorld::Ptr()) : mAmmunition(ammo), mWeapon(weapon) {} /// Equips the given weapon. void prepare(const MWWorld::Ptr& actor) override; float getCombatRange (bool& isRanged) const override; const ESM::Weapon* getWeapon() const override; }; std::shared_ptr prepareNextAction (const MWWorld::Ptr& actor, const MWWorld::Ptr& enemy); float getBestActionRating(const MWWorld::Ptr &actor, const MWWorld::Ptr &enemy); float getDistanceMinusHalfExtents(const MWWorld::Ptr& actor, const MWWorld::Ptr& enemy, bool minusZDist=false); float getMaxAttackDistance(const MWWorld::Ptr& actor); bool canFight(const MWWorld::Ptr& actor, const MWWorld::Ptr& enemy); float vanillaRateFlee(const MWWorld::Ptr& actor, const MWWorld::Ptr& enemy); bool makeFleeDecision(const MWWorld::Ptr& actor, const MWWorld::Ptr& enemy, float antiFleeRating); } #endif ================================================ FILE: apps/openmw/mwmechanics/aiescort.cpp ================================================ #include "aiescort.hpp" #include #include #include "../mwbase/world.hpp" #include "../mwbase/environment.hpp" #include "../mwbase/mechanicsmanager.hpp" #include "../mwworld/class.hpp" #include "../mwworld/cellstore.hpp" #include "creaturestats.hpp" #include "movement.hpp" /* TODO: Different behavior for AIEscort a d x y z and AIEscortCell a c d x y z. TODO: Take account for actors being in different cells. */ 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(duration)) , mCellX(std::numeric_limits::max()) , mCellY(std::numeric_limits::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(duration)) , mCellX(std::numeric_limits::max()) , mCellY(std::numeric_limits::max()) { mTargetActorRefId = actorId; } 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) , mRemainingDuration(escort->mRemainingDuration) , mCellX(std::numeric_limits::max()) , mCellY(std::numeric_limits::max()) { mTargetActorRefId = escort->mTargetId; mTargetActorId = escort->mTargetActorId; } bool AiEscort::execute (const MWWorld::Ptr& actor, CharacterController& characterController, AiState& state, float duration) { // If AiEscort has ran for as long or longer then the duration specified // and the duration is not infinite, the package is complete. if (mDuration > 0) { mRemainingDuration -= ((duration*MWBase::Environment::get().getWorld()->getTimeScaleFactor()) / 3600); if (mRemainingDuration <= 0) { mRemainingDuration = mDuration; return true; } } if (!mCellId.empty() && mCellId != actor.getCell()->getCell()->getCellId().mWorldspace) return false; // Not in the correct cell, pause and rely on the player to go back through a teleport door actor.getClass().getCreatureStats(actor).setDrawState(DrawState_Nothing); actor.getClass().getCreatureStats(actor).setMovementFlag(CreatureStats::Flag_Run, false); const MWWorld::Ptr follower = MWBase::Environment::get().getWorld()->getPtr(mTargetActorRefId, false); const osg::Vec3f leaderPos = actor.getRefData().getPosition().asVec3(); const osg::Vec3f followerPos = follower.getRefData().getPosition().asVec3(); const osg::Vec3f halfExtents = MWBase::Environment::get().getWorld()->getHalfExtents(actor); const float maxHalfExtent = std::max(halfExtents.x(), std::max(halfExtents.y(), halfExtents.z())); if ((leaderPos - followerPos).length2() <= mMaxDist * mMaxDist) { const osg::Vec3f dest(mX, mY, mZ); if (pathTo(actor, dest, duration, maxHalfExtent)) //Returns true on path complete { mRemainingDuration = mDuration; return true; } mMaxDist = maxHalfExtent + 450.0f; } else { // Stop moving if the player is too far away MWBase::Environment::get().getMechanicsManager()->playAnimationGroup(actor, "idle3", 0, 1); actor.getClass().getMovementSettings(actor).mPosition[1] = 0; mMaxDist = maxHalfExtent + 250.0f; } return false; } void AiEscort::writeState(ESM::AiSequence::AiSequence &sequence) const { std::unique_ptr escort(new ESM::AiSequence::AiEscort()); escort->mData.mX = mX; escort->mData.mY = mY; escort->mData.mZ = mZ; escort->mTargetId = mTargetActorRefId; escort->mTargetActorId = mTargetActorId; escort->mRemainingDuration = mRemainingDuration; escort->mCellId = mCellId; ESM::AiSequence::AiPackageContainer package; package.mType = ESM::AiSequence::Ai_Escort; package.mPackage = escort.release(); sequence.mPackages.push_back(package); } void AiEscort::fastForward(const MWWorld::Ptr& actor, AiState &state) { // Update duration counter if this package has a duration if (mDuration > 0) mRemainingDuration--; } } ================================================ FILE: apps/openmw/mwmechanics/aiescort.hpp ================================================ #ifndef GAME_MWMECHANICS_AIESCORT_H #define GAME_MWMECHANICS_AIESCORT_H #include "typedaipackage.hpp" #include namespace ESM { namespace AiSequence { struct AiEscort; } } namespace MWMechanics { /// \brief AI Package to have an NPC lead the player to a specific point class AiEscort final : public TypedAiPackage { public: /// 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); /// 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 ESM::AiSequence::AiEscort* escort); bool execute (const MWWorld::Ptr& actor, CharacterController& characterController, AiState& state, float duration) override; static constexpr AiPackageTypeId getTypeId() { return AiPackageTypeId::Escort; } static constexpr Options makeDefaultOptions() { AiPackage::Options options; options.mUseVariableSpeed = true; options.mSideWithTarget = true; return options; } void writeState(ESM::AiSequence::AiSequence &sequence) const override; void fastForward(const MWWorld::Ptr& actor, AiState& state) override; osg::Vec3f getDestination() const override { return osg::Vec3f(mX, mY, mZ); } private: const std::string mCellId; const float mX; const float mY; const float mZ; float mMaxDist = 450; const float mDuration; // In hours float mRemainingDuration; // In hours const int mCellX; const int mCellY; }; } #endif ================================================ FILE: apps/openmw/mwmechanics/aiface.cpp ================================================ #include "aiface.hpp" #include "../mwworld/ptr.hpp" #include "steering.hpp" MWMechanics::AiFace::AiFace(float targetX, float targetY) : mTargetX(targetX), mTargetY(targetY) { } bool MWMechanics::AiFace::execute(const MWWorld::Ptr& actor, MWMechanics::CharacterController& /*characterController*/, MWMechanics::AiState& /*state*/, float /*duration*/) { osg::Vec3f dir = osg::Vec3f(mTargetX, mTargetY, 0) - actor.getRefData().getPosition().asVec3(); return zTurn(actor, std::atan2(dir.x(), dir.y()), osg::DegreesToRadians(3.f)); } ================================================ FILE: apps/openmw/mwmechanics/aiface.hpp ================================================ #ifndef GAME_MWMECHANICS_AIFACE_H #define GAME_MWMECHANICS_AIFACE_H #include "typedaipackage.hpp" namespace MWMechanics { /// AiPackage which makes an actor face a certain direction. class AiFace final : public TypedAiPackage { public: AiFace(float targetX, float targetY); bool execute (const MWWorld::Ptr& actor, CharacterController& characterController, AiState& state, float duration) override; static constexpr AiPackageTypeId getTypeId() { return AiPackageTypeId::Face; } static constexpr Options makeDefaultOptions() { AiPackage::Options options; options.mPriority = 2; options.mCanCancel = false; options.mShouldCancelPreviousAi = false; return options; } private: const float mTargetX; const float mTargetY; }; } #endif ================================================ FILE: apps/openmw/mwmechanics/aifollow.cpp ================================================ #include "aifollow.hpp" #include #include #include "../mwbase/world.hpp" #include "../mwbase/environment.hpp" #include "../mwbase/mechanicsmanager.hpp" #include "../mwworld/class.hpp" #include "../mwworld/cellstore.hpp" #include "creaturestats.hpp" #include "movement.hpp" #include "steering.hpp" namespace { osg::Vec3f::value_type getHalfExtents(const MWWorld::ConstPtr& actor) { if(actor.getClass().isNpc()) return 64; return MWBase::Environment::get().getWorld()->getHalfExtents(actor).y(); } } 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) , 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) , 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(makeDefaultOptions().withShouldCancelPreviousAi(!commanded)) , mAlwaysFollow(true), mDuration(0), mRemainingDuration(0), mX(0), mY(0), mZ(0) , mCellId(""), mActive(false), mFollowIndex(mFollowIndexCounter++) { mTargetActorRefId = actor.getCellRef().getRefId(); mTargetActorId = actor.getClass().getCreatureStats(actor).getActorId(); } AiFollow::AiFollow(const ESM::AiSequence::AiFollow *follow) : TypedAiPackage(makeDefaultOptions().withShouldCancelPreviousAi(!follow->mCommanded)) , 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) , mRemainingDuration(follow->mRemainingDuration) , mX(follow->mData.mX), mY(follow->mData.mY), mZ(follow->mData.mZ) , mCellId(follow->mCellId), mActive(follow->mActive), mFollowIndex(mFollowIndexCounter++) { mTargetActorRefId = follow->mTargetId; mTargetActorId = follow->mTargetActorId; } bool AiFollow::execute (const MWWorld::Ptr& actor, CharacterController& characterController, AiState& state, float duration) { const MWWorld::Ptr target = getTarget(); // Target is not here right now, wait for it to return // Really we should be checking whether the target is currently registered with the MechanicsManager if (target == MWWorld::Ptr() || !target.getRefData().getCount() || !target.getRefData().isEnabled()) return false; actor.getClass().getCreatureStats(actor).setDrawState(DrawState_Nothing); AiFollowStorage& storage = state.get(); bool& rotate = storage.mTurnActorToTarget; if (rotate) { if (zTurn(actor, storage.mTargetAngleRadians)) rotate = false; return false; } /* Start of tes3mp addition If this follow package is set to allow for any distance, skip the checks below */ if (mIgnoreDistance) mActive = true; /* End of tes3mp addition */ const osg::Vec3f actorPos(actor.getRefData().getPosition().asVec3()); const osg::Vec3f targetPos(target.getRefData().getPosition().asVec3()); const osg::Vec3f targetDir = targetPos - actorPos; // AiFollow requires the target to be in range and within sight for the initial activation if (!mActive) { storage.mTimer -= duration; if (storage.mTimer < 0) { if (targetDir.length2() < 500*500 && MWBase::Environment::get().getWorld()->getLOS(actor, target)) mActive = true; storage.mTimer = 0.5f; } } if (!mActive) return false; // In the original engine the first follower stays closer to the player than any subsequent followers. // Followers beyond the first usually attempt to stand inside each other. osg::Vec3f::value_type floatingDistance = 0; auto followers = MWBase::Environment::get().getMechanicsManager()->getActorsFollowingByIndex(target); if (followers.size() >= 2 && followers.cbegin()->first != mFollowIndex) { for(auto& follower : followers) { auto halfExtent = getHalfExtents(follower.second); if(halfExtent > floatingDistance) floatingDistance = halfExtent; } floatingDistance += 128; } floatingDistance += getHalfExtents(target) + 64; floatingDistance += getHalfExtents(actor) * 2; short followDistance = static_cast(floatingDistance); if (!mAlwaysFollow) //Update if you only follow for a bit { //Check if we've run out of time if (mDuration > 0) { mRemainingDuration -= ((duration*MWBase::Environment::get().getWorld()->getTimeScaleFactor()) / 3600); if (mRemainingDuration <= 0) { mRemainingDuration = mDuration; return true; } } osg::Vec3f finalPos(mX, mY, mZ); if ((actorPos-finalPos).length2() < followDistance*followDistance) //Close-ish to final position { if (actor.getCell()->isExterior()) //Outside? { if (mCellId == "") //No cell to travel to return true; } else { if (mCellId == actor.getCell()->getCell()->mName) //Cell to travel to return true; } } } short baseFollowDistance = followDistance; short threshold = 30; // to avoid constant switching between moving/stopping if (storage.mMoving) followDistance -= threshold; else followDistance += threshold; if (targetDir.length2() <= followDistance * followDistance) { float faceAngleRadians = std::atan2(targetDir.x(), targetDir.y()); if (!zTurn(actor, faceAngleRadians, osg::DegreesToRadians(45.f))) { storage.mTargetAngleRadians = faceAngleRadians; storage.mTurnActorToTarget = true; } return false; } storage.mMoving = !pathTo(actor, targetPos, duration, baseFollowDistance); // Go to the destination if (storage.mMoving) { //Check if you're far away if (targetDir.length2() > 450 * 450) actor.getClass().getCreatureStats(actor).setMovementFlag(MWMechanics::CreatureStats::Flag_Run, true); //Make NPC run else if (targetDir.length2() < 325 * 325) //Have a bit of a dead zone, otherwise npc will constantly flip between running and not when right on the edge of the running threshold actor.getClass().getCreatureStats(actor).setMovementFlag(MWMechanics::CreatureStats::Flag_Run, false); //make NPC walk } return false; } std::string AiFollow::getFollowedActor() { return mTargetActorRefId; } bool AiFollow::isCommanded() const { return !mOptions.mShouldCancelPreviousAi; } void AiFollow::writeState(ESM::AiSequence::AiSequence &sequence) const { std::unique_ptr follow(new ESM::AiSequence::AiFollow()); follow->mData.mX = mX; follow->mData.mY = mY; follow->mData.mZ = mZ; follow->mTargetId = mTargetActorRefId; follow->mTargetActorId = mTargetActorId; follow->mRemainingDuration = mRemainingDuration; follow->mCellId = mCellId; follow->mAlwaysFollow = mAlwaysFollow; follow->mCommanded = isCommanded(); follow->mActive = mActive; ESM::AiSequence::AiPackageContainer package; package.mType = ESM::AiSequence::Ai_Follow; package.mPackage = follow.release(); sequence.mPackages.push_back(package); } int AiFollow::getFollowIndex() const { return mFollowIndex; } void AiFollow::fastForward(const MWWorld::Ptr& actor, AiState &state) { // Update duration counter if this package has a duration if (mDuration > 0) mRemainingDuration--; } /* Start of tes3mp addition Make it possible to allow following from any distance */ void AiFollow::allowAnyDistance(bool state) { mIgnoreDistance = state; } /* End of tes3mp addition */ } ================================================ FILE: apps/openmw/mwmechanics/aifollow.hpp ================================================ #ifndef GAME_MWMECHANICS_AIFOLLOW_H #define GAME_MWMECHANICS_AIFOLLOW_H #include "typedaipackage.hpp" #include #include #include "../mwworld/ptr.hpp" namespace ESM { namespace AiSequence { struct AiFollow; } } namespace MWMechanics { struct AiFollowStorage : AiTemporaryBase { float mTimer; bool mMoving; float mTargetAngleRadians; bool mTurnActorToTarget; AiFollowStorage() : mTimer(0.f), mMoving(false), mTargetAngleRadians(0.f), mTurnActorToTarget(false) {} }; /// \brief AiPackage for an actor to follow another actor/the PC /** The AI will follow the target until a condition (time, or position) are set. Both can be disabled to cause the actor to follow the other indefinitely **/ class AiFollow final : public TypedAiPackage { 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); /// 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); /// Follow Actor indefinitively AiFollow(const MWWorld::Ptr& actor, bool commanded=false); AiFollow(const ESM::AiSequence::AiFollow* follow); bool execute (const MWWorld::Ptr& actor, CharacterController& characterController, AiState& state, float duration) override; static constexpr AiPackageTypeId getTypeId() { return AiPackageTypeId::Follow; } static constexpr Options makeDefaultOptions() { AiPackage::Options options; options.mUseVariableSpeed = true; options.mSideWithTarget = true; options.mFollowTargetThroughDoors = true; return options; } /// Returns the actor being followed std::string getFollowedActor(); void writeState (ESM::AiSequence::AiSequence& sequence) const override; bool isCommanded() const; int getFollowIndex() const; void fastForward(const MWWorld::Ptr& actor, AiState& state) override; osg::Vec3f getDestination() const override { MWWorld::Ptr target = getTarget(); if (target.isEmpty()) return osg::Vec3f(0, 0, 0); return target.getRefData().getPosition().asVec3(); } /* Start of tes3mp addition Make it possible to allow following from any distance */ void allowAnyDistance(bool state); /* End of tes3mp addition */ private: /// This will make the actor always follow. /** Thus ignoring mDuration and mX,mY,mZ (used for summoned creatures). **/ const bool mAlwaysFollow; const float mDuration; // Hours float mRemainingDuration; // Hours const float mX; const float mY; const float mZ; const std::string mCellId; bool mActive; // have we spotted the target? const int mFollowIndex; static int mFollowIndexCounter; /* Start of tes3mp addition Track whether this package allows following to start from any distance */ bool mIgnoreDistance = false; /* End of tes3mp addition */ }; } #endif ================================================ FILE: apps/openmw/mwmechanics/aipackage.cpp ================================================ #include "aipackage.hpp" #include #include #include #include #include #include "../mwbase/world.hpp" #include "../mwbase/environment.hpp" #include "../mwworld/action.hpp" #include "../mwworld/class.hpp" #include "../mwworld/cellstore.hpp" #include "../mwworld/inventorystore.hpp" #include "pathgrid.hpp" #include "creaturestats.hpp" #include "movement.hpp" #include "steering.hpp" #include "actorutil.hpp" #include namespace { float divOrMax(float dividend, float divisor) { return divisor == 0 ? std::numeric_limits::max() * std::numeric_limits::epsilon() : dividend / divisor; } float getPointTolerance(float speed, float duration, const osg::Vec3f& halfExtents) { const float actorTolerance = 2 * speed * duration + 1.2 * std::max(halfExtents.x(), halfExtents.y()); return std::max(MWMechanics::MIN_TOLERANCE, actorTolerance); } } MWMechanics::AiPackage::AiPackage(AiPackageTypeId typeId, const Options& options) : mTypeId(typeId), mOptions(options), mTargetActorRefId(""), mTargetActorId(-1), mRotateOnTheRunChecks(0), mIsShortcutting(false), mShortcutProhibited(false), mShortcutFailPos() { } MWWorld::Ptr MWMechanics::AiPackage::getTarget() const { if (mTargetActorId == -2) return MWWorld::Ptr(); if (mTargetActorId == -1) { if (mTargetActorRefId.empty()) { mTargetActorId = -2; return MWWorld::Ptr(); } MWWorld::Ptr target = MWBase::Environment::get().getWorld()->searchPtr(mTargetActorRefId, false); if (target.isEmpty()) { mTargetActorId = -2; return target; } else mTargetActorId = target.getClass().getCreatureStats(target).getActorId(); } if (mTargetActorId != -1) return MWBase::Environment::get().getWorld()->searchPtrViaActorId(mTargetActorId); else return MWWorld::Ptr(); } void MWMechanics::AiPackage::reset() { // reset all members mReaction.reset(); mIsShortcutting = false; mShortcutProhibited = false; mShortcutFailPos = osg::Vec3f(); mPathFinder.clearPath(); mObstacleCheck.clear(); } bool MWMechanics::AiPackage::pathTo(const MWWorld::Ptr& actor, const osg::Vec3f& dest, float duration, float destTolerance) { const Misc::TimerStatus timerStatus = mReaction.update(duration); const osg::Vec3f position = actor.getRefData().getPosition().asVec3(); //position of the actor MWBase::World* world = MWBase::Environment::get().getWorld(); const osg::Vec3f halfExtents = world->getHalfExtents(actor); /// Stops the actor when it gets too close to a unloaded cell //... At current time, this test is unnecessary. AI shuts down when actor is more than "actors processing range" setting value //... units from player, and exterior cells are 8192 units long and wide. //... But AI processing distance may increase in the future. if (isNearInactiveCell(position)) { actor.getClass().getMovementSettings(actor).mPosition[0] = 0; actor.getClass().getMovementSettings(actor).mPosition[1] = 0; world->updateActorPath(actor, mPathFinder.getPath(), halfExtents, position, dest); return false; } mLastDestinationTolerance = destTolerance; const float distToTarget = distance(position, dest); const bool isDestReached = (distToTarget <= destTolerance); const bool actorCanMoveByZ = canActorMoveByZAxis(actor); if (!isDestReached && timerStatus == Misc::TimerStatus::Elapsed) { if (actor.getClass().isBipedal(actor)) openDoors(actor); const bool wasShortcutting = mIsShortcutting; bool destInLOS = false; // Prohibit shortcuts for AiWander, if the actor can not move in 3 dimensions. mIsShortcutting = actorCanMoveByZ && shortcutPath(position, dest, actor, &destInLOS, actorCanMoveByZ); // try to shortcut first if (!mIsShortcutting) { if (wasShortcutting || doesPathNeedRecalc(dest, actor)) // if need to rebuild path { const auto pathfindingHalfExtents = world->getPathfindingHalfExtents(actor); mPathFinder.buildLimitedPath(actor, position, dest, actor.getCell(), getPathGridGraph(actor.getCell()), pathfindingHalfExtents, getNavigatorFlags(actor), getAreaCosts(actor)); mRotateOnTheRunChecks = 3; // give priority to go directly on target if there is minimal opportunity if (destInLOS && mPathFinder.getPath().size() > 1) { // get point just before dest auto pPointBeforeDest = mPathFinder.getPath().rbegin() + 1; // if start point is closer to the target then last point of path (excluding target itself) then go straight on the target if (distance(position, dest) <= distance(dest, *pPointBeforeDest)) { mPathFinder.clearPath(); mPathFinder.addPointToPath(dest); } } } if (!mPathFinder.getPath().empty()) //Path has points in it { const osg::Vec3f& lastPos = mPathFinder.getPath().back(); //Get the end of the proposed path if(distance(dest, lastPos) > 100) //End of the path is far from the destination mPathFinder.addPointToPath(dest); //Adds the final destination to the path, to try to get to where you want to go } } } const float pointTolerance = getPointTolerance(actor.getClass().getMaxSpeed(actor), duration, halfExtents); static const bool smoothMovement = Settings::Manager::getBool("smooth movement", "Game"); mPathFinder.update(position, pointTolerance, DEFAULT_TOLERANCE, /*shortenIfAlmostStraight=*/smoothMovement, actorCanMoveByZ, halfExtents, getNavigatorFlags(actor)); if (isDestReached || mPathFinder.checkPathCompleted()) // if path is finished { // turn to destination point zTurn(actor, getZAngleToPoint(position, dest)); smoothTurn(actor, getXAngleToPoint(position, dest), 0); world->removeActorPath(actor); return true; } else if (mPathFinder.getPath().empty()) return false; world->updateActorPath(actor, mPathFinder.getPath(), halfExtents, position, dest); if (mRotateOnTheRunChecks == 0 || isReachableRotatingOnTheRun(actor, *mPathFinder.getPath().begin())) // to prevent circling around a path point { actor.getClass().getMovementSettings(actor).mPosition[1] = 1; // move to the target if (mRotateOnTheRunChecks > 0) mRotateOnTheRunChecks--; } // turn to next path point by X,Z axes float zAngleToNext = mPathFinder.getZAngleToNext(position.x(), position.y()); zTurn(actor, zAngleToNext); smoothTurn(actor, mPathFinder.getXAngleToNext(position.x(), position.y(), position.z()), 0); const auto destination = getNextPathPoint(dest); mObstacleCheck.update(actor, destination, duration); if (smoothMovement) { const float smoothTurnReservedDist = 150; auto& movement = actor.getClass().getMovementSettings(actor); float distToNextSqr = osg::Vec2f(destination.x() - position.x(), destination.y() - position.y()).length2(); float diffAngle = zAngleToNext - actor.getRefData().getPosition().rot[2]; if (std::cos(diffAngle) < -0.1) movement.mPosition[0] = movement.mPosition[1] = 0; else if (distToNextSqr > smoothTurnReservedDist * smoothTurnReservedDist) { // Go forward (and slowly turn towards the next path point) movement.mPosition[0] = 0; movement.mPosition[1] = 1; } else { // Next path point is near, so use diagonal movement to follow the path precisely. movement.mPosition[0] = std::sin(diffAngle); movement.mPosition[1] = std::max(std::cos(diffAngle), 0.f); } } // handle obstacles on the way evadeObstacles(actor); return false; } void MWMechanics::AiPackage::evadeObstacles(const MWWorld::Ptr& actor) { // check if stuck due to obstacles if (!mObstacleCheck.isEvading()) return; // first check if obstacle is a door static float distance = MWBase::Environment::get().getWorld()->getMaxActivationDistance(); const MWWorld::Ptr door = getNearbyDoor(actor, distance); if (!door.isEmpty() && actor.getClass().isBipedal(actor)) { openDoors(actor); } else { mObstacleCheck.takeEvasiveAction(actor.getClass().getMovementSettings(actor)); } } namespace { bool isDoorOnTheWay(const MWWorld::Ptr& actor, const MWWorld::Ptr& door, const osg::Vec3f& nextPathPoint) { const auto world = MWBase::Environment::get().getWorld(); const auto halfExtents = world->getHalfExtents(actor); const auto position = actor.getRefData().getPosition().asVec3() + osg::Vec3f(0, 0, halfExtents.z()); const auto destination = nextPathPoint + osg::Vec3f(0, 0, halfExtents.z()); return world->hasCollisionWithDoor(door, position, destination); } } void MWMechanics::AiPackage::openDoors(const MWWorld::Ptr& actor) { // note: AiWander currently does not open doors if (getTypeId() == AiPackageTypeId::Wander) return; if (mPathFinder.getPathSize() == 0) return; MWBase::World* world = MWBase::Environment::get().getWorld(); static float distance = world->getMaxActivationDistance(); const MWWorld::Ptr door = getNearbyDoor(actor, distance); if (door == MWWorld::Ptr()) return; if (!door.getCellRef().getTeleport() && door.getClass().getDoorState(door) == MWWorld::DoorState::Idle) { if (!isDoorOnTheWay(actor, door, mPathFinder.getPath().front())) return; if ((door.getCellRef().getTrap().empty() && door.getCellRef().getLockLevel() <= 0 )) { world->activate(door, actor); return; } const std::string keyId = door.getCellRef().getKey(); if (keyId.empty()) return; MWWorld::ContainerStore &invStore = actor.getClass().getContainerStore(actor); MWWorld::Ptr keyPtr = invStore.search(keyId); if (!keyPtr.isEmpty()) world->activate(door, actor); } } const MWMechanics::PathgridGraph& MWMechanics::AiPackage::getPathGridGraph(const MWWorld::CellStore *cell) { const ESM::CellId& id = cell->getCell()->getCellId(); // static cache is OK for now, pathgrids can never change during runtime typedef std::map > CacheMap; static CacheMap cache; CacheMap::iterator found = cache.find(id); if (found == cache.end()) { cache.insert(std::make_pair(id, std::make_unique(MWMechanics::PathgridGraph(cell)))); } return *cache[id].get(); } bool MWMechanics::AiPackage::shortcutPath(const osg::Vec3f& startPoint, const osg::Vec3f& endPoint, const MWWorld::Ptr& actor, bool *destInLOS, bool isPathClear) { if (!mShortcutProhibited || (mShortcutFailPos - startPoint).length() >= PATHFIND_SHORTCUT_RETRY_DIST) { // check if target is clearly visible isPathClear = !MWBase::Environment::get().getWorld()->castRay( startPoint.x(), startPoint.y(), startPoint.z(), endPoint.x(), endPoint.y(), endPoint.z()); if (destInLOS != nullptr) *destInLOS = isPathClear; if (!isPathClear) return false; // check if an actor can move along the shortcut path isPathClear = checkWayIsClearForActor(startPoint, endPoint, actor); } if (isPathClear) // can shortcut the path { mPathFinder.clearPath(); mPathFinder.addPointToPath(endPoint); return true; } return false; } bool MWMechanics::AiPackage::checkWayIsClearForActor(const osg::Vec3f& startPoint, const osg::Vec3f& endPoint, const MWWorld::Ptr& actor) { if (canActorMoveByZAxis(actor)) return true; const float actorSpeed = actor.getClass().getMaxSpeed(actor); const float maxAvoidDist = AI_REACTION_TIME * actorSpeed + actorSpeed / getAngularVelocity(actorSpeed) * 2; // *2 - for reliability const float distToTarget = osg::Vec2f(endPoint.x(), endPoint.y()).length(); const float offsetXY = distToTarget > maxAvoidDist*1.5? maxAvoidDist : maxAvoidDist/2; // update shortcut prohibit state if (checkWayIsClear(startPoint, endPoint, offsetXY)) { if (mShortcutProhibited) { mShortcutProhibited = false; mShortcutFailPos = osg::Vec3f(); } return true; } else { if (mShortcutFailPos == osg::Vec3f()) { mShortcutProhibited = true; mShortcutFailPos = startPoint; } } return false; } bool MWMechanics::AiPackage::doesPathNeedRecalc(const osg::Vec3f& newDest, const MWWorld::Ptr& actor) const { return mPathFinder.getPath().empty() || getPathDistance(actor, mPathFinder.getPath().back(), newDest) > 10 || mPathFinder.getPathCell() != actor.getCell(); } bool MWMechanics::AiPackage::isNearInactiveCell(osg::Vec3f position) { const ESM::Cell* playerCell(getPlayer().getCell()->getCell()); if (playerCell->isExterior()) { // get actor's distance from origin of center cell Misc::CoordinateConverter(playerCell).toLocal(position); // currently assumes 3 x 3 grid for exterior cells, with player at center cell. // AI shuts down actors before they reach edges of 3 x 3 grid. const float distanceFromEdge = 200.0; float minThreshold = (-1.0f * ESM::Land::REAL_SIZE) + distanceFromEdge; float maxThreshold = (2.0f * ESM::Land::REAL_SIZE) - distanceFromEdge; return (position.x() < minThreshold) || (maxThreshold < position.x()) || (position.y() < minThreshold) || (maxThreshold < position.y()); } else { return false; } } bool MWMechanics::AiPackage::isReachableRotatingOnTheRun(const MWWorld::Ptr& actor, const osg::Vec3f& dest) { // get actor's shortest radius for moving in circle float speed = actor.getClass().getMaxSpeed(actor); speed += speed * 0.1f; // 10% real speed inaccuracy float radius = speed / getAngularVelocity(speed); // get radius direction to the center const float* rot = actor.getRefData().getPosition().rot; osg::Quat quatRot(rot[0], -osg::X_AXIS, rot[1], -osg::Y_AXIS, rot[2], -osg::Z_AXIS); osg::Vec3f dir = quatRot * osg::Y_AXIS; // actor's orientation direction is a tangent to circle osg::Vec3f radiusDir = dir ^ osg::Z_AXIS; // radius is perpendicular to a tangent radiusDir.normalize(); radiusDir *= radius; // pick up the nearest center candidate osg::Vec3f pos = actor.getRefData().getPosition().asVec3(); osg::Vec3f center1 = pos - radiusDir; osg::Vec3f center2 = pos + radiusDir; osg::Vec3f center = (center1 - dest).length2() < (center2 - dest).length2() ? center1 : center2; float distToDest = (center - dest).length(); // if pathpoint is reachable for the actor rotating on the run: // no points of actor's circle should be farther from the center than destination point return (radius <= distToDest); } DetourNavigator::Flags MWMechanics::AiPackage::getNavigatorFlags(const MWWorld::Ptr& actor) const { static const bool allowToFollowOverWaterSurface = Settings::Manager::getBool("allow actors to follow over water surface", "Game"); const MWWorld::Class& actorClass = actor.getClass(); DetourNavigator::Flags result = DetourNavigator::Flag_none; if ((actorClass.isPureWaterCreature(actor) || (getTypeId() != AiPackageTypeId::Wander && ((allowToFollowOverWaterSurface && getTypeId() == AiPackageTypeId::Follow) || actorClass.canSwim(actor) || hasWaterWalking(actor))) ) && actorClass.getSwimSpeed(actor) > 0) result |= DetourNavigator::Flag_swim; if (actorClass.canWalk(actor) && actor.getClass().getWalkSpeed(actor) > 0) result |= DetourNavigator::Flag_walk; if (actorClass.isBipedal(actor) && getTypeId() != AiPackageTypeId::Wander) result |= DetourNavigator::Flag_openDoor; return result; } DetourNavigator::AreaCosts MWMechanics::AiPackage::getAreaCosts(const MWWorld::Ptr& actor) const { DetourNavigator::AreaCosts costs; const DetourNavigator::Flags flags = getNavigatorFlags(actor); const MWWorld::Class& actorClass = actor.getClass(); if (flags & DetourNavigator::Flag_swim) costs.mWater = divOrMax(costs.mWater, actorClass.getSwimSpeed(actor)); if (flags & DetourNavigator::Flag_walk) { float walkCost; if (getTypeId() == AiPackageTypeId::Wander) walkCost = divOrMax(1.0, actorClass.getWalkSpeed(actor)); else walkCost = divOrMax(1.0, actorClass.getRunSpeed(actor)); costs.mDoor = costs.mDoor * walkCost; costs.mPathgrid = costs.mPathgrid * walkCost; costs.mGround = costs.mGround * walkCost; } return costs; } osg::Vec3f MWMechanics::AiPackage::getNextPathPoint(const osg::Vec3f& destination) const { return mPathFinder.getPath().empty() ? destination : mPathFinder.getPath().front(); } float MWMechanics::AiPackage::getNextPathPointTolerance(float speed, float duration, const osg::Vec3f& halfExtents) const { if (mPathFinder.getPathSize() <= 1) return std::max(DEFAULT_TOLERANCE, mLastDestinationTolerance); return getPointTolerance(speed, duration, halfExtents); } ================================================ FILE: apps/openmw/mwmechanics/aipackage.hpp ================================================ #ifndef GAME_MWMECHANICS_AIPACKAGE_H #define GAME_MWMECHANICS_AIPACKAGE_H #include #include #include "pathfinding.hpp" #include "obstacle.hpp" #include "aistate.hpp" #include "aipackagetypeid.hpp" #include "aitimer.hpp" namespace MWWorld { class Ptr; } namespace ESM { struct Cell; namespace AiSequence { struct AiSequence; } } namespace MWMechanics { class CharacterController; class PathgridGraph; /// \brief Base class for AI packages class AiPackage { public: struct Options { unsigned int mPriority = 0; bool mUseVariableSpeed = false; bool mSideWithTarget = false; bool mFollowTargetThroughDoors = false; bool mCanCancel = true; bool mShouldCancelPreviousAi = true; bool mRepeat = false; bool mAlwaysActive = false; constexpr Options withRepeat(bool value) { mRepeat = value; return *this; } constexpr Options withShouldCancelPreviousAi(bool value) { mShouldCancelPreviousAi = value; return *this; } }; AiPackage(AiPackageTypeId typeId, const Options& options); virtual ~AiPackage() = default; static constexpr Options makeDefaultOptions() { return Options{}; } ///Clones the package virtual std::unique_ptr clone() const = 0; /// Updates and runs the package (Should run every frame) /// \return Package completed? virtual bool execute (const MWWorld::Ptr& actor, CharacterController& characterController, AiState& state, float duration) = 0; /// Returns the TypeID of the AiPackage /// \see enum TypeId AiPackageTypeId getTypeId() const { return mTypeId; } /// Higher number is higher priority (0 being the lowest) unsigned int getPriority() const { return mOptions.mPriority; } /// Check if package use movement with variable speed bool useVariableSpeed() const { return mOptions.mUseVariableSpeed; } virtual void writeState (ESM::AiSequence::AiSequence& sequence) const {} /// Simulates the passing of time virtual void fastForward(const MWWorld::Ptr& actor, AiState& state) {} /// Get the target actor the AI is targeted at (not applicable to all AI packages, default return empty Ptr) virtual MWWorld::Ptr getTarget() const; /// Get the destination point of the AI package (not applicable to all AI packages, default return (0, 0, 0)) virtual osg::Vec3f getDestination(const MWWorld::Ptr& actor) const { return osg::Vec3f(0, 0, 0); }; /// Return true if having this AiPackage makes the actor side with the target in fights (default false) bool sideWithTarget() const { return mOptions.mSideWithTarget; } /// Return true if the actor should follow the target through teleport doors (default false) bool followTargetThroughDoors() const { return mOptions.mFollowTargetThroughDoors; } /// Can this Ai package be canceled? (default true) bool canCancel() const { return mOptions.mCanCancel; } /// 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. bool getRepeat() const { return mOptions.mRepeat; } virtual osg::Vec3f getDestination() const { return osg::Vec3f(0, 0, 0); } /// Return true if any loaded actor with this AI package must be active. bool alwaysActive() const { return mOptions.mAlwaysActive; } /// Reset pathfinding state void reset(); /// Return if actor's rotation speed is sufficient to rotate to the destination pathpoint on the run. Otherwise actor should rotate while standing. static bool isReachableRotatingOnTheRun(const MWWorld::Ptr& actor, const osg::Vec3f& dest); osg::Vec3f getNextPathPoint(const osg::Vec3f& destination) const; float getNextPathPointTolerance(float speed, float duration, const osg::Vec3f& halfExtents) const; protected: /// Handles path building and shortcutting with obstacles avoiding /** \return If the actor has arrived at his destination **/ bool pathTo(const MWWorld::Ptr& actor, const osg::Vec3f& dest, float duration, float destTolerance = 0.0f); /// Check if there aren't any obstacles along the path to make shortcut possible /// If a shortcut is possible then path will be cleared and filled with the destination point. /// \param destInLOS If not nullptr function will return ray cast check result /// \return If can shortcut the path bool shortcutPath(const osg::Vec3f& startPoint, const osg::Vec3f& endPoint, const MWWorld::Ptr& actor, bool *destInLOS, bool isPathClear); /// Check if the way to the destination is clear, taking into account actor speed bool checkWayIsClearForActor(const osg::Vec3f& startPoint, const osg::Vec3f& endPoint, const MWWorld::Ptr& actor); bool doesPathNeedRecalc(const osg::Vec3f& newDest, const MWWorld::Ptr& actor) const; void evadeObstacles(const MWWorld::Ptr& actor); void openDoors(const MWWorld::Ptr& actor); const PathgridGraph& getPathGridGraph(const MWWorld::CellStore* cell); DetourNavigator::Flags getNavigatorFlags(const MWWorld::Ptr& actor) const; DetourNavigator::AreaCosts getAreaCosts(const MWWorld::Ptr& actor) const; const AiPackageTypeId mTypeId; const Options mOptions; // TODO: all this does not belong here, move into temporary storage PathFinder mPathFinder; ObstacleCheck mObstacleCheck; AiReactionTimer mReaction; std::string mTargetActorRefId; mutable int mTargetActorId; short mRotateOnTheRunChecks; // attempts to check rotation to the pathpoint on the run possibility bool mIsShortcutting; // if shortcutting at the moment bool mShortcutProhibited; // shortcutting may be prohibited after unsuccessful attempt osg::Vec3f mShortcutFailPos; // position of last shortcut fail float mLastDestinationTolerance = 0; private: bool isNearInactiveCell(osg::Vec3f position); }; } #endif ================================================ FILE: apps/openmw/mwmechanics/aipackagetypeid.hpp ================================================ #ifndef GAME_MWMECHANICS_AIPACKAGETYPEID_H #define GAME_MWMECHANICS_AIPACKAGETYPEID_H namespace MWMechanics { ///Enumerates the various AITypes available enum class AiPackageTypeId { None = -1, Wander = 0, Travel = 1, Escort = 2, Follow = 3, Activate = 4, // These 5 are not really handled as Ai Packages in the MW engine // For compatibility do *not* return these in the getCurrentAiPackage script function.. Combat = 5, Pursue = 6, AvoidDoor = 7, Face = 8, Breathe = 9, InternalTravel = 10, Cast = 11 }; } #endif ================================================ FILE: apps/openmw/mwmechanics/aipursue.cpp ================================================ #include "aipursue.hpp" #include #include "../mwbase/environment.hpp" #include "../mwbase/mechanicsmanager.hpp" #include "../mwbase/windowmanager.hpp" #include "../mwbase/world.hpp" #include "../mwworld/class.hpp" /* Start of tes3mp addition Include additional headers for multiplayer purposes */ #include #include "../mwgui/windowmanagerimp.hpp" #include "../mwmp/Main.hpp" #include "../mwmp/LocalPlayer.hpp" /* End of tes3mp addition */ #include "movement.hpp" #include "creaturestats.hpp" #include "combat.hpp" namespace MWMechanics { AiPursue::AiPursue(const MWWorld::Ptr& actor) { mTargetActorId = actor.getClass().getCreatureStats(actor).getActorId(); } AiPursue::AiPursue(const ESM::AiSequence::AiPursue *pursue) { mTargetActorId = pursue->mTargetActorId; } bool AiPursue::execute (const MWWorld::Ptr& actor, CharacterController& characterController, AiState& state, float duration) { if(actor.getClass().getCreatureStats(actor).isDead()) return true; const MWWorld::Ptr target = MWBase::Environment::get().getWorld()->searchPtrViaActorId(mTargetActorId); //The target to follow // Stop if the target doesn't exist // Really we should be checking whether the target is currently registered with the MechanicsManager if (target == MWWorld::Ptr() || !target.getRefData().getCount() || !target.getRefData().isEnabled()) return true; if (isTargetMagicallyHidden(target) && !MWBase::Environment::get().getMechanicsManager()->awarenessCheck(target, actor)) return false; if (target.getClass().getCreatureStats(target).isDead()) return true; /* Start of tes3mp addition Because multiplayer does not pause the game, prevent infinite arrest loops by ignoring players already engaged in dialogue while retaining the AiPursue package Additionally, do not arrest players who are currently jailed */ if (target == MWBase::Environment::get().getWorld()->getPlayerPtr()) { if (MWBase::Environment::get().getWindowManager()->containsMode(MWGui::GM_Dialogue) || MWBase::Environment::get().getWindowManager()->containsMode(MWGui::GM_Jail)) { return false; } } /* End of tes3mp addition */ actor.getClass().getCreatureStats(actor).setDrawState(DrawState_Nothing); //Set the target destination const osg::Vec3f dest = target.getRefData().getPosition().asVec3(); const osg::Vec3f actorPos = actor.getRefData().getPosition().asVec3(); const float pathTolerance = 100.f; // check the true distance in case the target is far away in Z-direction bool reached = pathTo(actor, dest, duration, pathTolerance) && std::abs(dest.z() - actorPos.z()) < pathTolerance; if (reached) { if (!MWBase::Environment::get().getWorld()->getLOS(target, actor)) return false; /* Start of tes3mp addition Record that the player has not died since the last attempt to arrest them Close the player's inventory or open container and cancel any drag and drops */ LOG_MESSAGE_SIMPLE(TimedLog::LOG_INFO, "After being pursued by %s, diedSinceArrestAttempt is now false", actor.getCellRef().getRefId().c_str()); mwmp::Main::get().getLocalPlayer()->diedSinceArrestAttempt = false; mwmp::Main::get().getLocalPlayer()->closeInventoryWindows(); /* End of tes3mp addition */ MWBase::Environment::get().getWindowManager()->pushGuiMode(MWGui::GM_Dialogue, actor); //Arrest player when reached return true; } actor.getClass().getCreatureStats(actor).setMovementFlag(MWMechanics::CreatureStats::Flag_Run, true); //Make NPC run return false; } MWWorld::Ptr AiPursue::getTarget() const { return MWBase::Environment::get().getWorld()->searchPtrViaActorId(mTargetActorId); } void AiPursue::writeState(ESM::AiSequence::AiSequence &sequence) const { std::unique_ptr pursue(new ESM::AiSequence::AiPursue()); pursue->mTargetActorId = mTargetActorId; ESM::AiSequence::AiPackageContainer package; package.mType = ESM::AiSequence::Ai_Pursue; package.mPackage = pursue.release(); sequence.mPackages.push_back(package); } } // namespace MWMechanics ================================================ FILE: apps/openmw/mwmechanics/aipursue.hpp ================================================ #ifndef GAME_MWMECHANICS_AIPURSUE_H #define GAME_MWMECHANICS_AIPURSUE_H #include "typedaipackage.hpp" namespace ESM { namespace AiSequence { struct AiPursue; } } namespace MWMechanics { /// \brief Makes the actor very closely follow the actor /** Used for arresting players. Causes the actor to run to the pursued actor and activate them, to arrest them. Note that while very similar to AiActivate, it will ONLY activate when evry close to target (Not also when the path is completed). **/ class AiPursue final : public TypedAiPackage { public: ///Constructor /** \param actor Actor to pursue **/ AiPursue(const MWWorld::Ptr& actor); AiPursue(const ESM::AiSequence::AiPursue* pursue); bool execute (const MWWorld::Ptr& actor, CharacterController& characterController, AiState& state, float duration) override; static constexpr AiPackageTypeId getTypeId() { return AiPackageTypeId::Pursue; } static constexpr Options makeDefaultOptions() { AiPackage::Options options; options.mCanCancel = false; options.mShouldCancelPreviousAi = false; return options; } MWWorld::Ptr getTarget() const override; void writeState (ESM::AiSequence::AiSequence& sequence) const override; }; } #endif ================================================ FILE: apps/openmw/mwmechanics/aisequence.cpp ================================================ #include "aisequence.hpp" #include #include #include #include "aipackage.hpp" #include "aistate.hpp" #include "aiwander.hpp" #include "aiescort.hpp" #include "aitravel.hpp" #include "aifollow.hpp" #include "aiactivate.hpp" #include "aicombat.hpp" #include "aicombataction.hpp" #include "aipursue.hpp" #include "actorutil.hpp" #include "../mwworld/class.hpp" namespace MWMechanics { void AiSequence::copy (const AiSequence& sequence) { for (const auto& package : sequence.mPackages) mPackages.push_back(package->clone()); // We need to keep an AiWander storage, if present - it has a state machine. // Not sure about another temporary storages sequence.mAiState.copy(mAiState); } AiSequence::AiSequence() : mDone (false), mRepeat(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) { if (this!=&sequence) { clear(); copy (sequence); mDone = sequence.mDone; mLastAiPackage = sequence.mLastAiPackage; } return *this; } AiSequence::~AiSequence() { clear(); } AiPackageTypeId AiSequence::getTypeId() const { if (mPackages.empty()) return AiPackageTypeId::None; return mPackages.front()->getTypeId(); } bool AiSequence::getCombatTarget(MWWorld::Ptr &targetActor) const { if (getTypeId() != AiPackageTypeId::Combat) return false; targetActor = mPackages.front()->getTarget(); return !targetActor.isEmpty(); } bool AiSequence::getCombatTargets(std::vector &targetActors) const { for (auto it = mPackages.begin(); it != mPackages.end(); ++it) { if ((*it)->getTypeId() == MWMechanics::AiPackageTypeId::Combat) targetActors.push_back((*it)->getTarget()); } return !targetActors.empty(); } std::list>::const_iterator AiSequence::begin() const { return mPackages.begin(); } std::list>::const_iterator AiSequence::end() const { return mPackages.end(); } void AiSequence::erase(std::list>::const_iterator package) { // Not sure if manually terminated packages should trigger mDone, probably not? for(auto it = mPackages.begin(); it != mPackages.end(); ++it) { if (package == it) { mPackages.erase(it); return; } } throw std::runtime_error("can't find package to erase"); } bool AiSequence::isInCombat() const { for (auto it = mPackages.begin(); it != mPackages.end(); ++it) { if ((*it)->getTypeId() == AiPackageTypeId::Combat) return true; } return false; } bool AiSequence::isEngagedWithActor() const { for (auto it = mPackages.begin(); it != mPackages.end(); ++it) { if ((*it)->getTypeId() == AiPackageTypeId::Combat) { MWWorld::Ptr target2 = (*it)->getTarget(); if (!target2.isEmpty() && target2.getClass().isNpc()) return true; } } return false; } bool AiSequence::hasPackage(AiPackageTypeId typeId) const { for (auto it = mPackages.begin(); it != mPackages.end(); ++it) { if ((*it)->getTypeId() == typeId) return true; } return false; } bool AiSequence::isInCombat(const MWWorld::Ptr &actor) const { for (auto it = mPackages.begin(); it != mPackages.end(); ++it) { if ((*it)->getTypeId() == AiPackageTypeId::Combat) { if ((*it)->getTarget() == actor) return true; } } return false; } void AiSequence::stopCombat() { for(auto it = mPackages.begin(); it != mPackages.end(); ) { if ((*it)->getTypeId() == AiPackageTypeId::Combat) { it = mPackages.erase(it); } else ++it; } } void AiSequence::stopPursuit() { for(auto it = mPackages.begin(); it != mPackages.end(); ) { if ((*it)->getTypeId() == AiPackageTypeId::Pursue) { it = mPackages.erase(it); } else ++it; } } bool AiSequence::isPackageDone() const { return mDone; } namespace { bool isActualAiPackage(AiPackageTypeId packageTypeId) { return (packageTypeId >= AiPackageTypeId::Wander && packageTypeId <= AiPackageTypeId::Activate); } } void AiSequence::execute (const MWWorld::Ptr& actor, CharacterController& characterController, float duration, bool outOfRange) { if(actor != getPlayer()) { if (mPackages.empty()) { mLastAiPackage = AiPackageTypeId::None; return; } auto packageIt = mPackages.begin(); MWMechanics::AiPackage* package = packageIt->get(); if (!package->alwaysActive() && outOfRange) return; auto packageTypeId = package->getTypeId(); // workaround ai packages not being handled as in the vanilla engine if (isActualAiPackage(packageTypeId)) mLastAiPackage = packageTypeId; // if active package is combat one, choose nearest target if (packageTypeId == AiPackageTypeId::Combat) { auto itActualCombat = mPackages.end(); float nearestDist = std::numeric_limits::max(); osg::Vec3f vActorPos = actor.getRefData().getPosition().asVec3(); float bestRating = 0.f; for (auto it = mPackages.begin(); it != mPackages.end();) { if ((*it)->getTypeId() != AiPackageTypeId::Combat) break; MWWorld::Ptr target = (*it)->getTarget(); // target disappeared (e.g. summoned creatures) if (target.isEmpty()) { it = mPackages.erase(it); } else { float rating = MWMechanics::getBestActionRating(actor, target); const ESM::Position &targetPos = target.getRefData().getPosition(); float distTo = (targetPos.asVec3() - vActorPos).length2(); // Small threshold for changing target if (it == mPackages.begin()) distTo = std::max(0.f, distTo - 2500.f); // if a target has higher priority than current target or has same priority but closer if (rating > bestRating || ((distTo < nearestDist) && rating == bestRating)) { nearestDist = distTo; itActualCombat = it; bestRating = rating; } ++it; } } assert(!mPackages.empty()); if (nearestDist < std::numeric_limits::max() && mPackages.begin() != itActualCombat) { assert(itActualCombat != mPackages.end()); // move combat package with nearest target to the front mPackages.splice(mPackages.begin(), mPackages, itActualCombat); } packageIt = mPackages.begin(); package = packageIt->get(); packageTypeId = package->getTypeId(); } try { 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())) { package->reset(); mPackages.push_back(package->clone()); } // To account for the rare case where AiPackage::execute() queued another AI package // (e.g. AiPursue executing a dialogue script that uses startCombat) mPackages.erase(packageIt); if (isActualAiPackage(packageTypeId)) mDone = true; } else { mDone = false; } } catch (std::exception& e) { Log(Debug::Error) << "Error during AiSequence::execute: " << e.what(); } } } void AiSequence::clear() { mPackages.clear(); } void AiSequence::stack (const AiPackage& package, const MWWorld::Ptr& actor, bool cancelOther) { if (actor == getPlayer()) throw std::runtime_error("Can't add AI packages to player"); // Stop combat when a non-combat AI package is added if (isActualAiPackage(package.getTypeId())) stopCombat(); // We should return a wandering actor back after combat, casting or pursuit. // The same thing for actors without AI packages. // Also there is no point to stack return packages. const auto currentTypeId = getTypeId(); const auto newTypeId = package.getTypeId(); if (currentTypeId <= MWMechanics::AiPackageTypeId::Wander && !hasPackage(MWMechanics::AiPackageTypeId::InternalTravel) && (newTypeId <= MWMechanics::AiPackageTypeId::Combat || newTypeId == MWMechanics::AiPackageTypeId::Pursue || newTypeId == MWMechanics::AiPackageTypeId::Cast)) { osg::Vec3f dest; if (currentTypeId == MWMechanics::AiPackageTypeId::Wander) { dest = getActivePackage().getDestination(actor); } else { dest = actor.getRefData().getPosition().asVec3(); } MWMechanics::AiInternalTravel travelPackage(dest.x(), dest.y(), dest.z()); stack(travelPackage, actor, false); } // remove previous packages if required if (cancelOther && package.shouldCancelPreviousAi()) { for (auto it = mPackages.begin(); it != mPackages.end();) { if((*it)->canCancel()) { it = mPackages.erase(it); } else ++it; } mRepeat=false; } // insert new package in correct place depending on priority for (auto it = mPackages.begin(); it != mPackages.end(); ++it) { // We should keep current AiCast package, if we try to add a new one. if ((*it)->getTypeId() == MWMechanics::AiPackageTypeId::Cast && package.getTypeId() == MWMechanics::AiPackageTypeId::Cast) { continue; } if((*it)->getPriority() <= package.getPriority()) { mPackages.insert(it, package.clone()); return; } } mPackages.push_back(package.clone()); // Make sure that temporary storage is empty if (cancelOther) { mAiState.moveIn(new AiCombatStorage()); mAiState.moveIn(new AiFollowStorage()); mAiState.moveIn(new AiWanderStorage()); } } bool MWMechanics::AiSequence::isEmpty() const { return mPackages.empty(); } const AiPackage& MWMechanics::AiSequence::getActivePackage() { if(mPackages.empty()) throw std::runtime_error(std::string("No AI Package!")); return *mPackages.front(); } 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 package; if (esmPackage.mType == ESM::AI_Wander) { ESM::AIWander data = esmPackage.mWander; std::vector idles; idles.reserve(8); for (int i=0; i<8; ++i) idles.push_back(data.mIdle[i]); package = std::make_unique(data.mDistance, data.mDuration, data.mTimeOfDay, idles, data.mShouldRepeat != 0); } else if (esmPackage.mType == ESM::AI_Escort) { ESM::AITarget data = esmPackage.mTarget; package = std::make_unique(data.mId.toString(), data.mDuration, data.mX, data.mY, data.mZ); } else if (esmPackage.mType == ESM::AI_Travel) { ESM::AITravel data = esmPackage.mTravel; package = std::make_unique(data.mX, data.mY, data.mZ); } else if (esmPackage.mType == ESM::AI_Activate) { ESM::AIActivate data = esmPackage.mActivate; package = std::make_unique(data.mName.toString()); } else //if (esmPackage.mType == ESM::AI_Follow) { ESM::AITarget data = esmPackage.mTarget; package = std::make_unique(data.mId.toString(), data.mDuration, data.mX, data.mY, data.mZ); } mPackages.push_back(std::move(package)); } } void AiSequence::writeState(ESM::AiSequence::AiSequence &sequence) const { for (const auto& package : mPackages) package->writeState(sequence); sequence.mLastAiPackage = static_cast(mLastAiPackage); } 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) { std::unique_ptr package; switch (container.mType) { case ESM::AiSequence::Ai_Wander: { package.reset(new AiWander(static_cast(container.mPackage))); break; } case ESM::AiSequence::Ai_Travel: { const auto source = static_cast(container.mPackage); if (source->mHidden) package.reset(new AiInternalTravel(source)); else package.reset(new AiTravel(source)); break; } case ESM::AiSequence::Ai_Escort: { package.reset(new AiEscort(static_cast(container.mPackage))); break; } case ESM::AiSequence::Ai_Follow: { package.reset(new AiFollow(static_cast(container.mPackage))); break; } case ESM::AiSequence::Ai_Activate: { package.reset(new AiActivate(static_cast(container.mPackage))); break; } case ESM::AiSequence::Ai_Combat: { package.reset(new AiCombat(static_cast(container.mPackage))); break; } case ESM::AiSequence::Ai_Pursue: { package.reset(new AiPursue(static_cast(container.mPackage))); break; } default: break; } if (!package.get()) continue; mPackages.push_back(std::move(package)); } mLastAiPackage = static_cast(sequence.mLastAiPackage); } void AiSequence::fastForward(const MWWorld::Ptr& actor) { if (!mPackages.empty()) { mPackages.front()->fastForward(actor, mAiState); } } } // namespace MWMechanics ================================================ FILE: apps/openmw/mwmechanics/aisequence.hpp ================================================ #ifndef GAME_MWMECHANICS_AISEQUENCE_H #define GAME_MWMECHANICS_AISEQUENCE_H #include #include #include "aistate.hpp" #include "aipackagetypeid.hpp" #include namespace MWWorld { class Ptr; } namespace ESM { namespace AiSequence { struct AiSequence; } } namespace MWMechanics { class AiPackage; class CharacterController; template< class Base > class DerivedClassStorage; struct AiTemporaryBase; typedef DerivedClassStorage AiState; /// \brief Sequence of AI-packages for a single actor /** The top-most AI package is run each frame. When completed, it is removed from the stack. **/ class AiSequence { ///AiPackages to run though std::list> mPackages; ///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); /// The type of AI package that ran last AiPackageTypeId mLastAiPackage; AiState mAiState; public: ///Default constructor AiSequence(); /// Copy Constructor AiSequence (const AiSequence& sequence); /// Assignment operator AiSequence& operator= (const AiSequence& sequence); virtual ~AiSequence(); /// Iterator may be invalidated by any function calls other than begin() or end(). std::list>::const_iterator begin() const; std::list>::const_iterator end() const; void erase(std::list>::const_iterator package); /// Returns currently executing AiPackage type /** \see enum class AiPackageTypeId **/ AiPackageTypeId getTypeId() const; /// Get the typeid of the Ai package that ran last /** NOT the currently "active" Ai package that will be run in the next frame. This difference is important when an Ai package has just finished and been removed. \see enum class AiPackageTypeId **/ AiPackageTypeId getLastRunTypeId() const { return mLastAiPackage; } /// Return true and assign target if combat package is currently active, return false otherwise bool getCombatTarget (MWWorld::Ptr &targetActor) const; /// Return true and assign targets for all combat packages, or return false if there are no combat packages bool getCombatTargets(std::vector &targetActors) const; /// Is there any combat package? bool isInCombat () const; /// Are we in combat with any other actor, who's also engaging us? bool isEngagedWithActor () const; /// Does this AI sequence have the given package type? bool hasPackage(AiPackageTypeId typeId) const; /// Are we in combat with this particular actor? bool isInCombat (const MWWorld::Ptr& actor) const; bool canAddTarget(const ESM::Position& actorPos, float distToTarget) const; ///< Function assumes that actor can have only 1 target apart player /// Removes all combat packages until first non-combat or stack empty. void stopCombat(); /// Has a package been completed during the last update? bool isPackageDone() const; /// Removes all pursue packages until first non-pursue or stack empty. void stopPursuit(); /// Execute current package, switching if needed. void execute (const MWWorld::Ptr& actor, CharacterController& characterController, float duration, bool outOfRange=false); /// Simulate the passing of time using the currently active AI package void fastForward(const MWWorld::Ptr &actor); /// Remove all packages. void clear(); ///< Add \a package to the front of the sequence /** Suspends current package @param actor The actor that owns this AiSequence **/ void stack (const AiPackage& package, const MWWorld::Ptr& actor, bool cancelOther=true); /// Return the current active package. /** If there is no active package, it will throw an exception **/ const AiPackage& getActivePackage(); /// Fills the AiSequence with packages /** Typically used for loading from the ESM \see ESM::AIPackageList **/ void fill (const ESM::AIPackageList& list); bool isEmpty() const; void writeState (ESM::AiSequence::AiSequence& sequence) const; void readState (const ESM::AiSequence::AiSequence& sequence); }; } #endif ================================================ FILE: apps/openmw/mwmechanics/aistate.hpp ================================================ #ifndef AISTATE_H #define AISTATE_H #include namespace MWMechanics { /** \brief stores one object of any class derived from Base. * Requesting a certain derived class via get() either returns * the stored object if it has the correct type or otherwise replaces * it with an object of the requested type. */ template< class Base > class DerivedClassStorage { private: Base* mStorage; //if needed you have to provide a clone member function DerivedClassStorage( const DerivedClassStorage& other ); DerivedClassStorage& operator=( const DerivedClassStorage& ); public: /// \brief returns reference to stored object or deletes it and creates a fitting template< class Derived > Derived& get() { Derived* result = dynamic_cast(mStorage); if(!result) { if(mStorage) delete mStorage; mStorage = result = new Derived(); } //return a reference to the (new allocated) object return *result; } template< class Derived > void copy(DerivedClassStorage& destination) const { Derived* result = dynamic_cast(mStorage); if (result != nullptr) destination.store(*result); } template< class Derived > void store( const Derived& payload ) { if(mStorage) delete mStorage; mStorage = new Derived(payload); } /// \brief takes ownership of the passed object template< class Derived > void moveIn( Derived* p ) { if(mStorage) delete mStorage; mStorage = p; } bool empty() const { return mStorage == nullptr; } const std::type_info& getType() const { return typeid(mStorage); } DerivedClassStorage():mStorage(nullptr){} ~DerivedClassStorage() { if(mStorage) delete mStorage; } }; /// \brief base class for the temporary storage of AiPackages. /** * Each AI package with temporary values needs a AiPackageStorage class * which is derived from AiTemporaryBase. The Actor holds a container * AiState where one of these storages can be stored at a time. * The execute(...) member function takes this container as an argument. * */ struct AiTemporaryBase { virtual ~AiTemporaryBase(){} }; /// \brief Container for AI package status. typedef DerivedClassStorage AiState; } #endif // AISTATE_H ================================================ FILE: apps/openmw/mwmechanics/aitimer.hpp ================================================ #ifndef OPENMW_MECHANICS_AITIMER_H #define OPENMW_MECHANICS_AITIMER_H #include #include namespace MWMechanics { constexpr float AI_REACTION_TIME = 0.25f; class AiReactionTimer { public: static constexpr float sDeviation = 0.1f; Misc::TimerStatus update(float duration) { return mImpl.update(duration); } void reset() { mImpl.reset(Misc::Rng::deviate(0, sDeviation)); } private: Misc::DeviatingPeriodicTimer mImpl {AI_REACTION_TIME, sDeviation, Misc::Rng::deviate(0, sDeviation)}; }; } #endif ================================================ FILE: apps/openmw/mwmechanics/aitravel.cpp ================================================ #include "aitravel.hpp" #include #include "../mwbase/environment.hpp" #include "../mwbase/mechanicsmanager.hpp" #include "../mwbase/world.hpp" #include "../mwworld/class.hpp" #include "../mwworld/cellstore.hpp" #include "movement.hpp" #include "creaturestats.hpp" namespace { bool isWithinMaxRange(const osg::Vec3f& pos1, const osg::Vec3f& pos2) { // Maximum travel distance for vanilla compatibility. // Was likely meant to prevent NPCs walking into non-loaded exterior cells, but for some reason is used in interior cells as well. // We can make this configurable at some point, but the default *must* be the below value. Anything else will break shoddily-written content (*cough* MW *cough*) in bizarre ways. return (pos1 - pos2).length2() <= 7168*7168; } } 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, AiInternalTravel* derived) : TypedAiPackage(derived), mX(x), mY(y), mZ(z), mHidden(true) { } AiTravel::AiTravel(float x, float y, float z) : AiTravel(x, y, z, this) { } AiTravel::AiTravel(const ESM::AiSequence::AiTravel *travel) : 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); } bool AiTravel::execute (const MWWorld::Ptr& actor, CharacterController& characterController, AiState& state, float duration) { MWBase::MechanicsManager* mechMgr = MWBase::Environment::get().getMechanicsManager(); auto& stats = actor.getClass().getCreatureStats(actor); if (!stats.getMovementFlag(CreatureStats::Flag_ForceJump) && !stats.getMovementFlag(CreatureStats::Flag_ForceSneak) && (mechMgr->isTurningToPlayer(actor) || mechMgr->getGreetingState(actor) == Greet_InProgress)) return false; const osg::Vec3f actorPos(actor.getRefData().getPosition().asVec3()); const osg::Vec3f targetPos(mX, mY, mZ); stats.setMovementFlag(CreatureStats::Flag_Run, false); stats.setDrawState(DrawState_Nothing); // Note: we should cancel internal "return after combat" package, if original location is too far away if (!isWithinMaxRange(targetPos, actorPos)) return mHidden; // Unfortunately, with vanilla assets destination is sometimes blocked by other actor. // If we got close to target, check for actors nearby. If they are, finish AI package. int destinationTolerance = 64; if (distance(actorPos, targetPos) <= destinationTolerance) { std::vector targetActors; std::pair result = MWBase::Environment::get().getWorld()->getHitContact(actor, destinationTolerance, targetActors); if (!result.first.isEmpty()) { actor.getClass().getMovementSettings(actor).mPosition[1] = 0; return true; } } if (pathTo(actor, targetPos, duration)) { actor.getClass().getMovementSettings(actor).mPosition[1] = 0; return true; } return false; } void AiTravel::fastForward(const MWWorld::Ptr& actor, AiState& state) { if (!isWithinMaxRange(osg::Vec3f(mX, mY, mZ), actor.getRefData().getPosition().asVec3())) return; // does not do any validation on the travel target (whether it's in air, inside collision geometry, etc), // that is the user's responsibility MWBase::Environment::get().getWorld()->moveObject(actor, mX, mY, mZ); actor.getClass().adjustPosition(actor, false); reset(); } void AiTravel::writeState(ESM::AiSequence::AiSequence &sequence) const { std::unique_ptr travel(new ESM::AiSequence::AiTravel()); travel->mData.mX = mX; travel->mData.mY = mY; travel->mData.mZ = mZ; travel->mHidden = mHidden; ESM::AiSequence::AiPackageContainer package; package.mType = ESM::AiSequence::Ai_Travel; package.mPackage = travel.release(); sequence.mPackages.push_back(package); } AiInternalTravel::AiInternalTravel(float x, float y, float z) : AiTravel(x, y, z, this) { } AiInternalTravel::AiInternalTravel(const ESM::AiSequence::AiTravel* travel) : AiTravel(travel->mData.mX, travel->mData.mY, travel->mData.mZ, this) { } std::unique_ptr AiInternalTravel::clone() const { return std::make_unique(*this); } } ================================================ FILE: apps/openmw/mwmechanics/aitravel.hpp ================================================ #ifndef GAME_MWMECHANICS_AITRAVEL_H #define GAME_MWMECHANICS_AITRAVEL_H #include "typedaipackage.hpp" namespace ESM { namespace AiSequence { struct AiTravel; } } namespace MWMechanics { struct AiInternalTravel; /// \brief Causes the AI to travel to the specified point class AiTravel : public TypedAiPackage { public: AiTravel(float x, float y, float z, AiTravel* derived); AiTravel(float x, float y, float z, AiInternalTravel* derived); AiTravel(float x, float y, float z); explicit AiTravel(const ESM::AiSequence::AiTravel* travel); /// Simulates the passing of time void fastForward(const MWWorld::Ptr& actor, AiState& state) override; void writeState(ESM::AiSequence::AiSequence &sequence) const override; bool execute (const MWWorld::Ptr& actor, CharacterController& characterController, AiState& state, float duration) override; static constexpr AiPackageTypeId getTypeId() { return AiPackageTypeId::Travel; } static constexpr Options makeDefaultOptions() { AiPackage::Options options; options.mUseVariableSpeed = true; options.mAlwaysActive = true; return options; } osg::Vec3f getDestination() const override { return osg::Vec3f(mX, mY, mZ); } private: const float mX; const float mY; const float mZ; const bool mHidden; }; struct AiInternalTravel final : public AiTravel { AiInternalTravel(float x, float y, float z); explicit AiInternalTravel(const ESM::AiSequence::AiTravel* travel); static constexpr AiPackageTypeId getTypeId() { return AiPackageTypeId::InternalTravel; } std::unique_ptr clone() const override; }; } #endif ================================================ FILE: apps/openmw/mwmechanics/aiwander.cpp ================================================ #include "aiwander.hpp" #include #include #include #include #include #include #include "../mwbase/world.hpp" #include "../mwbase/environment.hpp" #include "../mwbase/mechanicsmanager.hpp" #include "../mwworld/class.hpp" #include "../mwworld/esmstore.hpp" #include "../mwworld/cellstore.hpp" #include "../mwphysics/collisiontype.hpp" #include "pathgrid.hpp" #include "creaturestats.hpp" #include "movement.hpp" #include "actorutil.hpp" namespace MWMechanics { static const int COUNT_BEFORE_RESET = 10; static const float IDLE_POSITION_CHECK_INTERVAL = 1.5f; // to prevent overcrowding static const int DESTINATION_TOLERANCE = 64; // distance must be long enough that NPC will need to move to get there. static const int MINIMUM_WANDER_DISTANCE = DESTINATION_TOLERANCE * 2; static const std::size_t MAX_IDLE_SIZE = 8; const std::string AiWander::sIdleSelectToGroupName[GroupIndex_MaxIdle - GroupIndex_MinIdle + 1] = { std::string("idle2"), std::string("idle3"), std::string("idle4"), std::string("idle5"), std::string("idle6"), std::string("idle7"), std::string("idle8"), std::string("idle9"), }; namespace { inline int getCountBeforeReset(const MWWorld::ConstPtr& actor) { if (actor.getClass().isPureWaterCreature(actor) || actor.getClass().isPureFlyingCreature(actor)) return 1; return COUNT_BEFORE_RESET; } osg::Vec3f getRandomPointAround(const osg::Vec3f& position, const float distance) { const float randomDirection = Misc::Rng::rollClosedProbability() * 2.0f * osg::PI; osg::Matrixf rotation; rotation.makeRotate(randomDirection, osg::Vec3f(0.0, 0.0, 1.0)); return position + osg::Vec3f(distance, 0.0, 0.0) * rotation; } bool isDestinationHidden(const MWWorld::ConstPtr &actor, const osg::Vec3f& destination) { const auto position = actor.getRefData().getPosition().asVec3(); const bool isWaterCreature = actor.getClass().isPureWaterCreature(actor); const bool isFlyingCreature = actor.getClass().isPureFlyingCreature(actor); const osg::Vec3f halfExtents = MWBase::Environment::get().getWorld()->getPathfindingHalfExtents(actor); osg::Vec3f direction = destination - position; direction.normalize(); const auto visibleDestination = ( isWaterCreature || isFlyingCreature ? destination : destination + osg::Vec3f(0, 0, halfExtents.z()) ) + direction * std::max(halfExtents.x(), std::max(halfExtents.y(), halfExtents.z())); const int mask = MWPhysics::CollisionType_World | MWPhysics::CollisionType_HeightMap | MWPhysics::CollisionType_Door | MWPhysics::CollisionType_Actor; return MWBase::Environment::get().getWorld()->castRay(position, visibleDestination, mask, actor); } bool isAreaOccupiedByOtherActor(const MWWorld::ConstPtr &actor, const osg::Vec3f& destination) { const auto world = MWBase::Environment::get().getWorld(); const osg::Vec3f halfExtents = world->getPathfindingHalfExtents(actor); const auto maxHalfExtent = std::max(halfExtents.x(), std::max(halfExtents.y(), halfExtents.z())); return world->isAreaOccupiedByOtherActor(destination, 2 * maxHalfExtent, actor); } void stopMovement(const MWWorld::Ptr& actor) { actor.getClass().getMovementSettings(actor).mPosition[0] = 0; actor.getClass().getMovementSettings(actor).mPosition[1] = 0; } std::vector getInitialIdle(const std::vector& idle) { std::vector result(MAX_IDLE_SIZE, 0); std::copy_n(idle.begin(), std::min(MAX_IDLE_SIZE, idle.size()), result.begin()); return result; } std::vector getInitialIdle(const unsigned char (&idle)[MAX_IDLE_SIZE]) { return std::vector(std::begin(idle), std::end(idle)); } } AiWander::AiWander(int distance, int duration, int timeOfDay, const std::vector& idle, bool repeat): TypedAiPackage(makeDefaultOptions().withRepeat(repeat)), mDistance(std::max(0, distance)), mDuration(std::max(0, duration)), mRemainingDuration(duration), mTimeOfDay(timeOfDay), mIdle(getInitialIdle(idle)), mStoredInitialActorPosition(false), mInitialActorPosition(osg::Vec3f(0, 0, 0)), mHasDestination(false), mDestination(osg::Vec3f(0, 0, 0)), mUsePathgrid(false) { } /* * AiWander high level states (0.29.0). Not entirely accurate in some cases * e.g. non-NPC actors do not greet and some creatures may be moving even in * the IdleNow state. * * [select node, * build path] * +---------->MoveNow----------->Walking * | | * [allowed | | * nodes] | [hello if near] | * start--->ChooseAction----->IdleNow | * ^ ^ | | * | | | | * | +-----------+ | * | | * +----------------------------------+ * * * New high level states. Not exactly as per vanilla (e.g. door stuff) * but the differences are required because our physics does not work like * vanilla and therefore have to compensate/work around. * * [select node, [if stuck evade * build path] or remove nodes if near door] * +---------->MoveNow<---------->Walking * | ^ | | * | |(near door) | | * [allowed | | | | * nodes] | [hello if near] | | * start--->ChooseAction----->IdleNow | | * ^ ^ | ^ | | * | | | | (stuck near | | * | +-----------+ +---------------+ | * | player) | * +----------------------------------+ * * NOTE: non-time critical operations are run once every 250ms or so. * * TODO: It would be great if door opening/closing can be detected and pathgrid * links dynamically updated. Currently (0.29.0) AiWander allows choosing a * destination beyond closed doors which sometimes makes the actors stuck at the * door and impossible for the player to open the door. * * For now detect being stuck at the door and simply delete the nodes from the * allowed set. The issue is when the door opens the allowed set is not * re-calculated. However this would not be an issue in most cases since hostile * actors will enter combat (i.e. no longer wandering) and different pathfinding * will kick in. */ bool AiWander::execute (const MWWorld::Ptr& actor, CharacterController& /*characterController*/, AiState& state, float duration) { MWMechanics::CreatureStats& cStats = actor.getClass().getCreatureStats(actor); if (cStats.isDead() || cStats.getHealth().getCurrent() <= 0) return true; // Don't bother with dead actors // get or create temporary storage AiWanderStorage& storage = state.get(); mRemainingDuration -= ((duration*MWBase::Environment::get().getWorld()->getTimeScaleFactor()) / 3600); cStats.setDrawState(DrawState_Nothing); cStats.setMovementFlag(CreatureStats::Flag_Run, false); ESM::Position pos = actor.getRefData().getPosition(); // If there is already a destination due to the package having been interrupted by a combat or pursue package, // rebuild a path to it if (!mPathFinder.isPathConstructed() && mHasDestination) { if (mUsePathgrid) { mPathFinder.buildPathByPathgrid(pos.asVec3(), mDestination, actor.getCell(), getPathGridGraph(actor.getCell())); } else { const osg::Vec3f halfExtents = MWBase::Environment::get().getWorld()->getPathfindingHalfExtents(actor); mPathFinder.buildPath(actor, pos.asVec3(), mDestination, actor.getCell(), getPathGridGraph(actor.getCell()), halfExtents, getNavigatorFlags(actor), getAreaCosts(actor)); } if (mPathFinder.isPathConstructed()) storage.setState(AiWanderStorage::Wander_Walking); } if(!cStats.getMovementFlag(CreatureStats::Flag_ForceJump) && !cStats.getMovementFlag(CreatureStats::Flag_ForceSneak)) { GreetingState greetingState = MWBase::Environment::get().getMechanicsManager()->getGreetingState(actor); if (greetingState == Greet_InProgress) { if (storage.mState == AiWanderStorage::Wander_Walking) { stopMovement(actor); mObstacleCheck.clear(); storage.setState(AiWanderStorage::Wander_IdleNow); } } } doPerFrameActionsForState(actor, duration, storage); if (storage.mReaction.update(duration) == Misc::TimerStatus::Waiting) return false; return reactionTimeActions(actor, storage, pos); } bool AiWander::reactionTimeActions(const MWWorld::Ptr& actor, AiWanderStorage& storage, ESM::Position& pos) { if (mDistance <= 0) storage.mCanWanderAlongPathGrid = false; if (isPackageCompleted()) { stopWalking(actor); // Reset package so it can be used again mRemainingDuration=mDuration; return true; } if (!mStoredInitialActorPosition) { mInitialActorPosition = actor.getRefData().getPosition().asVec3(); mStoredInitialActorPosition = true; } // Initialization to discover & store allowed node points for this actor. if (storage.mPopulateAvailableNodes) { getAllowedNodes(actor, actor.getCell()->getCell(), storage); } if (canActorMoveByZAxis(actor) && mDistance > 0) { // Typically want to idle for a short time before the next wander if (Misc::Rng::rollDice(100) >= 92 && storage.mState != AiWanderStorage::Wander_Walking) { wanderNearStart(actor, storage, mDistance); } storage.mCanWanderAlongPathGrid = false; } // If the package has a wander distance but no pathgrid is available, // randomly idle or wander near spawn point else if(storage.mAllowedNodes.empty() && mDistance > 0 && !storage.mIsWanderingManually) { // Typically want to idle for a short time before the next wander if (Misc::Rng::rollDice(100) >= 96) { wanderNearStart(actor, storage, mDistance); } else { storage.setState(AiWanderStorage::Wander_IdleNow); } } else if (storage.mAllowedNodes.empty() && !storage.mIsWanderingManually) { storage.mCanWanderAlongPathGrid = false; } // If Wandering manually and hit an obstacle, stop if (storage.mIsWanderingManually && mObstacleCheck.isEvading()) { completeManualWalking(actor, storage); } if (storage.mState == AiWanderStorage::Wander_MoveNow && storage.mCanWanderAlongPathGrid) { // Construct a new path if there isn't one if(!mPathFinder.isPathConstructed()) { if (!storage.mAllowedNodes.empty()) { setPathToAnAllowedNode(actor, storage, pos); } } } else if (storage.mIsWanderingManually && mPathFinder.checkPathCompleted()) { completeManualWalking(actor, storage); } if (storage.mIsWanderingManually && storage.mState == AiWanderStorage::Wander_Walking && (mPathFinder.getPathSize() == 0 || isDestinationHidden(actor, mPathFinder.getPath().back()) || isAreaOccupiedByOtherActor(actor, mPathFinder.getPath().back()))) completeManualWalking(actor, storage); return false; // AiWander package not yet completed } osg::Vec3f AiWander::getDestination(const MWWorld::Ptr& actor) const { if (mHasDestination) return mDestination; return actor.getRefData().getPosition().asVec3(); } bool AiWander::isPackageCompleted() const { // End package if duration is complete return mDuration && mRemainingDuration <= 0; } /* * Commands actor to walk to a random location near original spawn location. */ void AiWander::wanderNearStart(const MWWorld::Ptr &actor, AiWanderStorage &storage, int wanderDistance) { const auto currentPosition = actor.getRefData().getPosition().asVec3(); std::size_t attempts = 10; // If a unit can't wander out of water, don't want to hang here const bool isWaterCreature = actor.getClass().isPureWaterCreature(actor); const bool isFlyingCreature = actor.getClass().isPureFlyingCreature(actor); const auto world = MWBase::Environment::get().getWorld(); const auto halfExtents = world->getPathfindingHalfExtents(actor); const auto navigator = world->getNavigator(); const auto navigatorFlags = getNavigatorFlags(actor); const auto areaCosts = getAreaCosts(actor); do { // Determine a random location within radius of original position const float wanderRadius = (0.2f + Misc::Rng::rollClosedProbability() * 0.8f) * wanderDistance; 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)) mDestination = *destination; else mDestination = getRandomPointAround(mInitialActorPosition, wanderRadius); } else mDestination = getRandomPointAround(mInitialActorPosition, wanderRadius); // Check if land creature will walk onto water or if water creature will swim onto land if (!isWaterCreature && destinationIsAtWater(actor, mDestination)) continue; if (isDestinationHidden(actor, mDestination)) continue; if (isAreaOccupiedByOtherActor(actor, mDestination)) continue; if (isWaterCreature || isFlyingCreature) mPathFinder.buildStraightPath(mDestination); else mPathFinder.buildPathByNavMesh(actor, currentPosition, mDestination, halfExtents, navigatorFlags, areaCosts); if (mPathFinder.isPathConstructed()) { storage.setState(AiWanderStorage::Wander_Walking, true); mHasDestination = true; mUsePathgrid = false; } break; } while (--attempts); } /* * Returns true if the position provided is above water. */ bool AiWander::destinationIsAtWater(const MWWorld::Ptr &actor, const osg::Vec3f& destination) { float heightToGroundOrWater = MWBase::Environment::get().getWorld()->getDistToNearestRayHit(destination, osg::Vec3f(0,0,-1), 1000.0, true); osg::Vec3f positionBelowSurface = destination; positionBelowSurface[2] = positionBelowSurface[2] - heightToGroundOrWater - 1.0f; return MWBase::Environment::get().getWorld()->isUnderwater(actor.getCell(), positionBelowSurface); } void AiWander::completeManualWalking(const MWWorld::Ptr &actor, AiWanderStorage &storage) { stopWalking(actor); mObstacleCheck.clear(); storage.setState(AiWanderStorage::Wander_IdleNow); } void AiWander::doPerFrameActionsForState(const MWWorld::Ptr& actor, float duration, AiWanderStorage& storage) { switch (storage.mState) { case AiWanderStorage::Wander_IdleNow: onIdleStatePerFrameActions(actor, duration, storage); break; case AiWanderStorage::Wander_Walking: onWalkingStatePerFrameActions(actor, duration, storage); break; case AiWanderStorage::Wander_ChooseAction: onChooseActionStatePerFrameActions(actor, storage); break; case AiWanderStorage::Wander_MoveNow: break; // nothing to do default: // should never get here assert(false); break; } } void AiWander::onIdleStatePerFrameActions(const MWWorld::Ptr& actor, float duration, AiWanderStorage& storage) { // Check if an idle actor is too far from all allowed nodes or too close to a door - if so start walking. storage.mCheckIdlePositionTimer += duration; if (storage.mCheckIdlePositionTimer >= IDLE_POSITION_CHECK_INTERVAL && !isStationary()) { storage.mCheckIdlePositionTimer = 0; // restart timer static float distance = MWBase::Environment::get().getWorld()->getMaxActivationDistance() * 1.6f; if (proximityToDoor(actor, distance) || !isNearAllowedNode(actor, storage, distance)) { storage.setState(AiWanderStorage::Wander_MoveNow); storage.mTrimCurrentNode = false; // just in case return; } } // Check if idle animation finished GreetingState greetingState = MWBase::Environment::get().getMechanicsManager()->getGreetingState(actor); if (!checkIdle(actor, storage.mIdleAnimation) && (greetingState == Greet_Done || greetingState == Greet_None)) { if (mPathFinder.isPathConstructed()) storage.setState(AiWanderStorage::Wander_Walking); else storage.setState(AiWanderStorage::Wander_ChooseAction); } } bool AiWander::isNearAllowedNode(const MWWorld::Ptr& actor, const AiWanderStorage& storage, float distance) const { const osg::Vec3f actorPos = actor.getRefData().getPosition().asVec3(); auto cell = actor.getCell()->getCell(); for (const ESM::Pathgrid::Point& node : storage.mAllowedNodes) { osg::Vec3f point(node.mX, node.mY, node.mZ); Misc::CoordinateConverter(cell).toWorld(point); if ((actorPos - point).length2() < distance * distance) return true; } return false; } void AiWander::onWalkingStatePerFrameActions(const MWWorld::Ptr& actor, float duration, AiWanderStorage& storage) { // Is there no destination or are we there yet? if ((!mPathFinder.isPathConstructed()) || pathTo(actor, osg::Vec3f(mPathFinder.getPath().back()), duration, DESTINATION_TOLERANCE)) { stopWalking(actor); storage.setState(AiWanderStorage::Wander_ChooseAction); } else { // have not yet reached the destination evadeObstacles(actor, storage); } } void AiWander::onChooseActionStatePerFrameActions(const MWWorld::Ptr& actor, AiWanderStorage& storage) { // Wait while fully stop before starting idle animation (important if "smooth movement" is enabled). if (actor.getClass().getCurrentSpeed(actor) > 0) return; unsigned short idleAnimation = getRandomIdle(); storage.mIdleAnimation = idleAnimation; if (!idleAnimation && mDistance) { storage.setState(AiWanderStorage::Wander_MoveNow); return; } if(idleAnimation) { if(std::find(storage.mBadIdles.begin(), storage.mBadIdles.end(), idleAnimation)==storage.mBadIdles.end()) { if(!playIdle(actor, idleAnimation)) { storage.mBadIdles.push_back(idleAnimation); storage.setState(AiWanderStorage::Wander_ChooseAction); return; } } } storage.setState(AiWanderStorage::Wander_IdleNow); } void AiWander::evadeObstacles(const MWWorld::Ptr& actor, AiWanderStorage& storage) { if (mUsePathgrid) { const auto halfExtents = MWBase::Environment::get().getWorld()->getHalfExtents(actor); mPathFinder.buildPathByNavMeshToNextPoint(actor, halfExtents, getNavigatorFlags(actor), getAreaCosts(actor)); } if (mObstacleCheck.isEvading()) { // first check if we're walking into a door static float distance = MWBase::Environment::get().getWorld()->getMaxActivationDistance(); if (proximityToDoor(actor, distance)) { // remove allowed points then select another random destination storage.mTrimCurrentNode = true; trimAllowedNodes(storage.mAllowedNodes, mPathFinder); mObstacleCheck.clear(); stopWalking(actor); storage.setState(AiWanderStorage::Wander_MoveNow); } storage.mStuckCount++; // TODO: maybe no longer needed } // if stuck for sufficiently long, act like current location was the destination if (storage.mStuckCount >= getCountBeforeReset(actor)) // something has gone wrong, reset { mObstacleCheck.clear(); stopWalking(actor); storage.setState(AiWanderStorage::Wander_ChooseAction); storage.mStuckCount = 0; } } void AiWander::setPathToAnAllowedNode(const MWWorld::Ptr& actor, AiWanderStorage& storage, const ESM::Position& actorPos) { unsigned int randNode = Misc::Rng::rollDice(storage.mAllowedNodes.size()); ESM::Pathgrid::Point dest(storage.mAllowedNodes[randNode]); ToWorldCoordinates(dest, actor.getCell()->getCell()); // actor position is already in world coordinates const osg::Vec3f start = actorPos.asVec3(); // don't take shortcuts for wandering const osg::Vec3f destVec3f = PathFinder::makeOsgVec3(dest); mPathFinder.buildPathByPathgrid(start, destVec3f, actor.getCell(), getPathGridGraph(actor.getCell())); if (mPathFinder.isPathConstructed()) { mDestination = destVec3f; mHasDestination = true; mUsePathgrid = true; // Remove this node as an option and add back the previously used node (stops NPC from picking the same node): ESM::Pathgrid::Point temp = storage.mAllowedNodes[randNode]; storage.mAllowedNodes.erase(storage.mAllowedNodes.begin() + randNode); // check if mCurrentNode was taken out of mAllowedNodes if (storage.mTrimCurrentNode && storage.mAllowedNodes.size() > 1) storage.mTrimCurrentNode = false; else storage.mAllowedNodes.push_back(storage.mCurrentNode); storage.mCurrentNode = temp; storage.setState(AiWanderStorage::Wander_Walking); } // Choose a different node and delete this one from possible nodes because it is uncreachable: else storage.mAllowedNodes.erase(storage.mAllowedNodes.begin() + randNode); } void AiWander::ToWorldCoordinates(ESM::Pathgrid::Point& point, const ESM::Cell * cell) { Misc::CoordinateConverter(cell).toWorld(point); } void AiWander::trimAllowedNodes(std::vector& nodes, const PathFinder& pathfinder) { // TODO: how to add these back in once the door opens? // Idea: keep a list of detected closed doors (see aicombat.cpp) // Every now and then check whether one of the doors is opened. (maybe // at the end of playing idle?) If the door is opened then re-calculate // allowed nodes starting from the spawn point. auto paths = pathfinder.getPath(); while(paths.size() >= 2) { const auto pt = paths.back(); for(unsigned int j = 0; j < nodes.size(); j++) { // FIXME: doesn't handle a door with the same X/Y // coordinates but with a different Z if (std::abs(nodes[j].mX - pt.x()) <= 0.5 && std::abs(nodes[j].mY - pt.y()) <= 0.5) { nodes.erase(nodes.begin() + j); break; } } paths.pop_back(); } } void AiWander::stopWalking(const MWWorld::Ptr& actor) { mPathFinder.clearPath(); mHasDestination = false; stopMovement(actor); } bool AiWander::playIdle(const MWWorld::Ptr& actor, unsigned short idleSelect) { if ((GroupIndex_MinIdle <= idleSelect) && (idleSelect <= GroupIndex_MaxIdle)) { const std::string& groupName = sIdleSelectToGroupName[idleSelect - GroupIndex_MinIdle]; return MWBase::Environment::get().getMechanicsManager()->playAnimationGroup(actor, groupName, 0, 1); } else { Log(Debug::Verbose) << "Attempted to play out of range idle animation \"" << idleSelect << "\" for " << actor.getCellRef().getRefId(); return false; } } bool AiWander::checkIdle(const MWWorld::Ptr& actor, unsigned short idleSelect) { if ((GroupIndex_MinIdle <= idleSelect) && (idleSelect <= GroupIndex_MaxIdle)) { const std::string& groupName = sIdleSelectToGroupName[idleSelect - GroupIndex_MinIdle]; return MWBase::Environment::get().getMechanicsManager()->checkAnimationPlaying(actor, groupName); } else { return false; } } short unsigned AiWander::getRandomIdle() { unsigned short idleRoll = 0; short unsigned selectedAnimation = 0; for(unsigned int counter = 0; counter < mIdle.size(); counter++) { static float fIdleChanceMultiplier = MWBase::Environment::get().getWorld()->getStore() .get().find("fIdleChanceMultiplier")->mValue.getFloat(); unsigned short idleChance = static_cast(fIdleChanceMultiplier * mIdle[counter]); unsigned short randSelect = (int)(Misc::Rng::rollProbability() * int(100 / fIdleChanceMultiplier)); if(randSelect < idleChance && randSelect > idleRoll) { selectedAnimation = counter + GroupIndex_MinIdle; idleRoll = randSelect; } } return selectedAnimation; } void AiWander::fastForward(const MWWorld::Ptr& actor, AiState &state) { // Update duration counter mRemainingDuration--; if (mDistance == 0) return; AiWanderStorage& storage = state.get(); if (storage.mPopulateAvailableNodes) getAllowedNodes(actor, actor.getCell()->getCell(), storage); if (storage.mAllowedNodes.empty()) return; int index = Misc::Rng::rollDice(storage.mAllowedNodes.size()); ESM::Pathgrid::Point dest = storage.mAllowedNodes[index]; ESM::Pathgrid::Point worldDest = dest; ToWorldCoordinates(worldDest, actor.getCell()->getCell()); bool isPathGridOccupied = MWBase::Environment::get().getMechanicsManager()->isAnyActorInRange(PathFinder::makeOsgVec3(worldDest), 60); // add offset only if the selected pathgrid is occupied by another actor if (isPathGridOccupied) { ESM::Pathgrid::PointList points; getNeighbouringNodes(dest, actor.getCell(), points); // there are no neighbouring nodes, nowhere to move if (points.empty()) return; int initialSize = points.size(); bool isOccupied = false; // AI will try to move the NPC towards every neighboring node until suitable place will be found for (int i = 0; i < initialSize; i++) { int randomIndex = Misc::Rng::rollDice(points.size()); ESM::Pathgrid::Point connDest = points[randomIndex]; // add an offset towards random neighboring node osg::Vec3f dir = PathFinder::makeOsgVec3(connDest) - PathFinder::makeOsgVec3(dest); float length = dir.length(); dir.normalize(); for (int j = 1; j <= 3; j++) { // move for 5-15% towards random neighboring node dest = PathFinder::makePathgridPoint(PathFinder::makeOsgVec3(dest) + dir * (j * 5 * length / 100.f)); worldDest = dest; ToWorldCoordinates(worldDest, actor.getCell()->getCell()); isOccupied = MWBase::Environment::get().getMechanicsManager()->isAnyActorInRange(PathFinder::makeOsgVec3(worldDest), 60); if (!isOccupied) break; } if (!isOccupied) break; // Will try an another neighboring node points.erase(points.begin()+randomIndex); } // there is no free space, nowhere to move if (isOccupied) return; } // place above to prevent moving inside objects, e.g. stairs, because a vector between pathgrids can be underground. // Adding 20 in adjustPosition() is not enough. dest.mZ += 60; ToWorldCoordinates(dest, actor.getCell()->getCell()); state.moveIn(new AiWanderStorage()); MWBase::Environment::get().getWorld()->moveObject(actor, static_cast(dest.mX), static_cast(dest.mY), static_cast(dest.mZ)); actor.getClass().adjustPosition(actor, false); } void AiWander::getNeighbouringNodes(ESM::Pathgrid::Point dest, const MWWorld::CellStore* currentCell, ESM::Pathgrid::PointList& points) { const ESM::Pathgrid *pathgrid = MWBase::Environment::get().getWorld()->getStore().get().search(*currentCell->getCell()); if (pathgrid == nullptr || pathgrid->mPoints.empty()) return; int index = PathFinder::getClosestPoint(pathgrid, PathFinder::makeOsgVec3(dest)); getPathGridGraph(currentCell).getNeighbouringPoints(index, points); } void AiWander::getAllowedNodes(const MWWorld::Ptr& actor, const ESM::Cell* cell, AiWanderStorage& storage) { // infrequently used, therefore no benefit in caching it as a member const ESM::Pathgrid * pathgrid = MWBase::Environment::get().getWorld()->getStore().get().search(*cell); const MWWorld::CellStore* cellStore = actor.getCell(); storage.mAllowedNodes.clear(); // If there is no path this actor doesn't go anywhere. See: // https://forum.openmw.org/viewtopic.php?t=1556 // http://www.fliggerty.com/phpBB3/viewtopic.php?f=30&t=5833 // Note: In order to wander, need at least two points. if(!pathgrid || (pathgrid->mPoints.size() < 2)) storage.mCanWanderAlongPathGrid = false; // A distance value passed into the constructor indicates how far the // actor can wander from the spawn position. AiWander assumes that // pathgrid points are available, and uses them to randomly select wander // destinations within the allowed set of pathgrid points (nodes). // ... pathgrids don't usually include water, so swimmers ignore them if (mDistance && storage.mCanWanderAlongPathGrid && !actor.getClass().isPureWaterCreature(actor)) { // get NPC's position in local (i.e. cell) coordinates osg::Vec3f npcPos(mInitialActorPosition); Misc::CoordinateConverter(cell).toLocal(npcPos); // Find closest pathgrid point int closestPointIndex = PathFinder::getClosestPoint(pathgrid, npcPos); // mAllowedNodes for this actor with pathgrid point indexes based on mDistance // and if the point is connected to the closest current point // NOTE: mPoints and mAllowedNodes are in local coordinates int pointIndex = 0; for(unsigned int counter = 0; counter < pathgrid->mPoints.size(); counter++) { osg::Vec3f nodePos(PathFinder::makeOsgVec3(pathgrid->mPoints[counter])); if((npcPos - nodePos).length2() <= mDistance * mDistance && getPathGridGraph(cellStore).isPointConnected(closestPointIndex, counter)) { storage.mAllowedNodes.push_back(pathgrid->mPoints[counter]); pointIndex = counter; } } if (storage.mAllowedNodes.size() == 1) { AddNonPathGridAllowedPoints(npcPos, pathgrid, pointIndex, storage); } if(!storage.mAllowedNodes.empty()) { SetCurrentNodeToClosestAllowedNode(npcPos, storage); } } storage.mPopulateAvailableNodes = false; } // When only one path grid point in wander distance, // additional points for NPC to wander to are: // 1. NPC's initial location // 2. Partway along the path between the point and its connected points. void AiWander::AddNonPathGridAllowedPoints(osg::Vec3f npcPos, const ESM::Pathgrid * pathGrid, int pointIndex, AiWanderStorage& storage) { storage.mAllowedNodes.push_back(PathFinder::makePathgridPoint(npcPos)); for (auto& edge : pathGrid->mEdges) { if (edge.mV0 == pointIndex) { AddPointBetweenPathGridPoints(pathGrid->mPoints[edge.mV0], pathGrid->mPoints[edge.mV1], storage); } } } void AiWander::AddPointBetweenPathGridPoints(const ESM::Pathgrid::Point& start, const ESM::Pathgrid::Point& end, AiWanderStorage& storage) { osg::Vec3f vectorStart = PathFinder::makeOsgVec3(start); osg::Vec3f delta = PathFinder::makeOsgVec3(end) - vectorStart; float length = delta.length(); delta.normalize(); int distance = std::max(mDistance / 2, MINIMUM_WANDER_DISTANCE); // must not travel longer than distance between waypoints or NPC goes past waypoint distance = std::min(distance, static_cast(length)); delta *= distance; storage.mAllowedNodes.push_back(PathFinder::makePathgridPoint(vectorStart + delta)); } void AiWander::SetCurrentNodeToClosestAllowedNode(const osg::Vec3f& npcPos, AiWanderStorage& storage) { float distanceToClosestNode = std::numeric_limits::max(); unsigned int index = 0; for (unsigned int counterThree = 0; counterThree < storage.mAllowedNodes.size(); counterThree++) { osg::Vec3f nodePos(PathFinder::makeOsgVec3(storage.mAllowedNodes[counterThree])); float tempDist = (npcPos - nodePos).length2(); if (tempDist < distanceToClosestNode) { index = counterThree; distanceToClosestNode = tempDist; } } storage.mCurrentNode = storage.mAllowedNodes[index]; storage.mAllowedNodes.erase(storage.mAllowedNodes.begin() + index); } void AiWander::writeState(ESM::AiSequence::AiSequence &sequence) const { float remainingDuration; if (mRemainingDuration > 0 && mRemainingDuration < 24) remainingDuration = mRemainingDuration; else remainingDuration = mDuration; std::unique_ptr wander(new ESM::AiSequence::AiWander()); wander->mData.mDistance = mDistance; wander->mData.mDuration = mDuration; wander->mData.mTimeOfDay = mTimeOfDay; wander->mDurationData.mRemainingDuration = remainingDuration; assert (mIdle.size() == 8); for (int i=0; i<8; ++i) wander->mData.mIdle[i] = mIdle[i]; wander->mData.mShouldRepeat = mOptions.mRepeat; wander->mStoredInitialActorPosition = mStoredInitialActorPosition; if (mStoredInitialActorPosition) wander->mInitialActorPosition = mInitialActorPosition; ESM::AiSequence::AiPackageContainer package; package.mType = ESM::AiSequence::Ai_Wander; package.mPackage = wander.release(); sequence.mPackages.push_back(package); } AiWander::AiWander (const ESM::AiSequence::AiWander* wander) : TypedAiPackage(makeDefaultOptions().withRepeat(wander->mData.mShouldRepeat != 0)) , mDistance(std::max(static_cast(0), wander->mData.mDistance)) , mDuration(std::max(static_cast(0), wander->mData.mDuration)) , mRemainingDuration(wander->mDurationData.mRemainingDuration) , mTimeOfDay(wander->mData.mTimeOfDay) , mIdle(getInitialIdle(wander->mData.mIdle)) , mStoredInitialActorPosition(wander->mStoredInitialActorPosition) , mHasDestination(false) , mDestination(osg::Vec3f(0, 0, 0)) , mUsePathgrid(false) { if (mStoredInitialActorPosition) mInitialActorPosition = wander->mInitialActorPosition; if (mRemainingDuration <= 0 || mRemainingDuration >= 24) mRemainingDuration = mDuration; } } ================================================ FILE: apps/openmw/mwmechanics/aiwander.hpp ================================================ #ifndef GAME_MWMECHANICS_AIWANDER_H #define GAME_MWMECHANICS_AIWANDER_H #include "typedaipackage.hpp" #include #include "pathfinding.hpp" #include "obstacle.hpp" #include "aistate.hpp" #include "aitimer.hpp" namespace ESM { struct Cell; namespace AiSequence { struct AiWander; } } namespace MWMechanics { /// \brief This class holds the variables AiWander needs which are deleted if the package becomes inactive. struct AiWanderStorage : AiTemporaryBase { AiReactionTimer mReaction; // AiWander states enum WanderState { Wander_ChooseAction, Wander_IdleNow, Wander_MoveNow, Wander_Walking }; WanderState mState; bool mIsWanderingManually; bool mCanWanderAlongPathGrid; unsigned short mIdleAnimation; std::vector mBadIdles; // Idle animations that when called cause errors // do we need to calculate allowed nodes based on mDistance bool mPopulateAvailableNodes; // allowed pathgrid nodes based on mDistance from the spawn point // in local coordinates of mCell std::vector mAllowedNodes; ESM::Pathgrid::Point mCurrentNode; bool mTrimCurrentNode; float mCheckIdlePositionTimer; int mStuckCount; AiWanderStorage(): mState(Wander_ChooseAction), mIsWanderingManually(false), mCanWanderAlongPathGrid(true), mIdleAnimation(0), mBadIdles(), mPopulateAvailableNodes(true), mAllowedNodes(), mTrimCurrentNode(false), mCheckIdlePositionTimer(0), mStuckCount(0) {}; void setState(const WanderState wanderState, const bool isManualWander = false) { mState = wanderState; mIsWanderingManually = isManualWander; } }; /// \brief Causes the Actor to wander within a specified range class AiWander final : public TypedAiPackage { public: /// Constructor /** \param distance Max distance the ACtor will wander \param duration Time, in hours, that this package will be preformed \param timeOfDay Currently unimplemented. Not functional in the original engine. \param idle Chances of each idle to play (9 in total) \param repeat Repeat wander or not **/ AiWander(int distance, int duration, int timeOfDay, const std::vector& idle, bool repeat); explicit AiWander (const ESM::AiSequence::AiWander* wander); bool execute(const MWWorld::Ptr& actor, CharacterController& characterController, AiState& state, float duration) override; static constexpr AiPackageTypeId getTypeId() { return AiPackageTypeId::Wander; } static constexpr Options makeDefaultOptions() { AiPackage::Options options; options.mUseVariableSpeed = true; return options; } void writeState(ESM::AiSequence::AiSequence &sequence) const override; void fastForward(const MWWorld::Ptr& actor, AiState& state) override; osg::Vec3f getDestination(const MWWorld::Ptr& actor) const override; osg::Vec3f getDestination() const override { if (!mHasDestination) return osg::Vec3f(0, 0, 0); return mDestination; } bool isStationary() const { return mDistance == 0; } private: void stopWalking(const MWWorld::Ptr& actor); /// Have the given actor play an idle animation /// @return Success or error bool playIdle(const MWWorld::Ptr& actor, unsigned short idleSelect); bool checkIdle(const MWWorld::Ptr& actor, unsigned short idleSelect); short unsigned getRandomIdle(); void setPathToAnAllowedNode(const MWWorld::Ptr& actor, AiWanderStorage& storage, const ESM::Position& actorPos); void evadeObstacles(const MWWorld::Ptr& actor, AiWanderStorage& storage); void doPerFrameActionsForState(const MWWorld::Ptr& actor, float duration, AiWanderStorage& storage); void onIdleStatePerFrameActions(const MWWorld::Ptr& actor, float duration, AiWanderStorage& storage); void onWalkingStatePerFrameActions(const MWWorld::Ptr& actor, float duration, AiWanderStorage& storage); void onChooseActionStatePerFrameActions(const MWWorld::Ptr& actor, AiWanderStorage& storage); bool reactionTimeActions(const MWWorld::Ptr& actor, AiWanderStorage& storage, ESM::Position& pos); inline bool isPackageCompleted() const; void wanderNearStart(const MWWorld::Ptr &actor, AiWanderStorage &storage, int wanderDistance); bool destinationIsAtWater(const MWWorld::Ptr &actor, const osg::Vec3f& destination); void completeManualWalking(const MWWorld::Ptr &actor, AiWanderStorage &storage); bool isNearAllowedNode(const MWWorld::Ptr &actor, const AiWanderStorage& storage, float distance) const; const int mDistance; // how far the actor can wander from the spawn point const int mDuration; float mRemainingDuration; const int mTimeOfDay; const std::vector mIdle; bool mStoredInitialActorPosition; osg::Vec3f mInitialActorPosition; // Note: an original engine does not reset coordinates even when actor changes a cell bool mHasDestination; osg::Vec3f mDestination; bool mUsePathgrid; void getNeighbouringNodes(ESM::Pathgrid::Point dest, const MWWorld::CellStore* currentCell, ESM::Pathgrid::PointList& points); void getAllowedNodes(const MWWorld::Ptr& actor, const ESM::Cell* cell, AiWanderStorage& storage); void trimAllowedNodes(std::vector& nodes, const PathFinder& pathfinder); // constants for converting idleSelect values into groupNames enum GroupIndex { GroupIndex_MinIdle = 2, GroupIndex_MaxIdle = 9 }; /// convert point from local (i.e. cell) to world coordinates void ToWorldCoordinates(ESM::Pathgrid::Point& point, const ESM::Cell * cell); void SetCurrentNodeToClosestAllowedNode(const osg::Vec3f& npcPos, AiWanderStorage& storage); void AddNonPathGridAllowedPoints(osg::Vec3f npcPos, const ESM::Pathgrid * pathGrid, int pointIndex, AiWanderStorage& storage); void AddPointBetweenPathGridPoints(const ESM::Pathgrid::Point& start, const ESM::Pathgrid::Point& end, AiWanderStorage& storage); /// lookup table for converting idleSelect value to groupName static const std::string sIdleSelectToGroupName[GroupIndex_MaxIdle - GroupIndex_MinIdle + 1]; static int OffsetToPreventOvercrowding(); }; } #endif ================================================ FILE: apps/openmw/mwmechanics/alchemy.cpp ================================================ #include "alchemy.hpp" #include #include #include #include #include #include #include #include #include #include /* Start of tes3mp addition Include additional headers for multiplayer purposes */ #include #include "../mwmp/Main.hpp" #include "../mwmp/Networking.hpp" #include "../mwmp/LocalPlayer.hpp" #include "../mwmp/Worldstate.hpp" /* End of tes3mp addition */ #include "../mwbase/environment.hpp" #include "../mwbase/world.hpp" #include "../mwworld/esmstore.hpp" #include "../mwworld/containerstore.hpp" #include "../mwworld/class.hpp" #include "../mwworld/cellstore.hpp" #include "magiceffects.hpp" #include "creaturestats.hpp" MWMechanics::Alchemy::Alchemy() : mValue(0) , mPotionName("") { } std::set MWMechanics::Alchemy::listEffects() const { std::map effects; for (TIngredientsIterator iter (mIngredients.begin()); iter!=mIngredients.end(); ++iter) { if (!iter->isEmpty()) { const MWWorld::LiveCellRef *ingredient = iter->get(); std::set seenEffects; for (int i=0; i<4; ++i) if (ingredient->mBase->mData.mEffectID[i]!=-1) { EffectKey key ( ingredient->mBase->mData.mEffectID[i], ingredient->mBase->mData.mSkills[i]!=-1 ? ingredient->mBase->mData.mSkills[i] : ingredient->mBase->mData.mAttributes[i]); if (seenEffects.insert(key).second) ++effects[key]; } } } std::set effects2; for (std::map::const_iterator iter (effects.begin()); iter!=effects.end(); ++iter) if (iter->second>1) effects2.insert (iter->first); return effects2; } void MWMechanics::Alchemy::applyTools (int flags, float& value) const { bool magnitude = !(flags & ESM::MagicEffect::NoMagnitude); bool duration = !(flags & ESM::MagicEffect::NoDuration); bool negative = (flags & ESM::MagicEffect::Harmful) != 0; int tool = negative ? ESM::Apparatus::Alembic : ESM::Apparatus::Retort; int setup = 0; if (!mTools[tool].isEmpty() && !mTools[ESM::Apparatus::Calcinator].isEmpty()) setup = 1; else if (!mTools[tool].isEmpty()) setup = 2; else if (!mTools[ESM::Apparatus::Calcinator].isEmpty()) setup = 3; else return; float toolQuality = setup==1 || setup==2 ? mTools[tool].get()->mBase->mData.mQuality : 0; float calcinatorQuality = setup==1 || setup==3 ? mTools[ESM::Apparatus::Calcinator].get()->mBase->mData.mQuality : 0; float quality = 1; switch (setup) { case 1: quality = negative ? 2 * toolQuality + 3 * calcinatorQuality : (magnitude && duration ? 2 * toolQuality + calcinatorQuality : 2/3.0f * (toolQuality + calcinatorQuality) + 0.5f); break; case 2: quality = negative ? 1+toolQuality : (magnitude && duration ? toolQuality : toolQuality + 0.5f); break; case 3: quality = magnitude && duration ? calcinatorQuality : calcinatorQuality + 0.5f; break; } if (setup==3 || !negative) { value += quality; } else { if (quality==0) throw std::runtime_error ("invalid derived alchemy apparatus quality"); value /= quality; } } void MWMechanics::Alchemy::updateEffects() { mEffects.clear(); mValue = 0; if (countIngredients()<2 || mAlchemist.isEmpty() || mTools[ESM::Apparatus::MortarPestle].isEmpty()) return; // find effects std::set effects (listEffects()); // general alchemy factor float x = getAlchemyFactor(); x *= mTools[ESM::Apparatus::MortarPestle].get()->mBase->mData.mQuality; x *= MWBase::Environment::get().getWorld()->getStore().get().find ("fPotionStrengthMult")->mValue.getFloat(); // value mValue = static_cast ( x * MWBase::Environment::get().getWorld()->getStore().get().find ("iAlchemyMod")->mValue.getFloat()); // build quantified effect list for (std::set::const_iterator iter (effects.begin()); iter!=effects.end(); ++iter) { const ESM::MagicEffect *magicEffect = MWBase::Environment::get().getWorld()->getStore().get().find (iter->mId); if (magicEffect->mData.mBaseCost<=0) { const std::string os = "invalid base cost for magic effect " + std::to_string(iter->mId); throw std::runtime_error (os); } float fPotionT1MagMul = MWBase::Environment::get().getWorld()->getStore().get().find ("fPotionT1MagMult")->mValue.getFloat(); if (fPotionT1MagMul<=0) throw std::runtime_error ("invalid gmst: fPotionT1MagMul"); float fPotionT1DurMult = MWBase::Environment::get().getWorld()->getStore().get().find ("fPotionT1DurMult")->mValue.getFloat(); if (fPotionT1DurMult<=0) throw std::runtime_error ("invalid gmst: fPotionT1DurMult"); float magnitude = (magicEffect->mData.mFlags & ESM::MagicEffect::NoMagnitude) ? 1.0f : (x / fPotionT1MagMul) / magicEffect->mData.mBaseCost; float duration = (magicEffect->mData.mFlags & ESM::MagicEffect::NoDuration) ? 1.0f : (x / fPotionT1DurMult) / magicEffect->mData.mBaseCost; if (!(magicEffect->mData.mFlags & ESM::MagicEffect::NoMagnitude)) applyTools (magicEffect->mData.mFlags, magnitude); if (!(magicEffect->mData.mFlags & ESM::MagicEffect::NoDuration)) applyTools (magicEffect->mData.mFlags, duration); duration = roundf(duration); magnitude = roundf(magnitude); if (magnitude>0 && duration>0) { ESM::ENAMstruct effect; effect.mEffectID = iter->mId; effect.mAttribute = -1; effect.mSkill = -1; if (magicEffect->mData.mFlags & ESM::MagicEffect::TargetSkill) effect.mSkill = iter->mArg; else if (magicEffect->mData.mFlags & ESM::MagicEffect::TargetAttribute) effect.mAttribute = iter->mArg; effect.mRange = 0; effect.mArea = 0; effect.mDuration = static_cast(duration); effect.mMagnMin = effect.mMagnMax = static_cast(magnitude); mEffects.push_back (effect); } } } const ESM::Potion *MWMechanics::Alchemy::getRecord(const ESM::Potion& toFind) const { const MWWorld::Store &potions = MWBase::Environment::get().getWorld()->getStore().get(); MWWorld::Store::iterator iter = potions.begin(); for (; iter != potions.end(); ++iter) { if (iter->mEffects.mList.size() != mEffects.size()) continue; if (iter->mName != toFind.mName || iter->mScript != toFind.mScript || iter->mData.mWeight != toFind.mData.mWeight || iter->mData.mValue != toFind.mData.mValue || iter->mData.mAutoCalc != toFind.mData.mAutoCalc) continue; // Don't choose an ID that came from the content files, would have unintended side effects // where alchemy can be used to produce quest-relevant items if (!potions.isDynamic(iter->mId)) continue; bool mismatch = false; for (int i=0; i (iter->mEffects.mList.size()); ++i) { const ESM::ENAMstruct& first = iter->mEffects.mList[i]; const ESM::ENAMstruct& second = mEffects[i]; if (first.mEffectID!=second.mEffectID || first.mArea!=second.mArea || first.mRange!=second.mRange || first.mSkill!=second.mSkill || first.mAttribute!=second.mAttribute || first.mMagnMin!=second.mMagnMin || first.mMagnMax!=second.mMagnMax || first.mDuration!=second.mDuration) { mismatch = true; break; } } if (!mismatch) return &(*iter); } return nullptr; } void MWMechanics::Alchemy::removeIngredients() { for (TIngredientsContainer::iterator iter (mIngredients.begin()); iter!=mIngredients.end(); ++iter) if (!iter->isEmpty()) { iter->getContainerStore()->remove(*iter, 1, mAlchemist); /* Start of tes3mp addition Store this item removal for later sending to avoid packet spam */ mwmp::Main::get().getLocalPlayer()->storeItemRemoval(iter->getCellRef().getRefId(), 1); /* End of tes3mp addition */ if (iter->getRefData().getCount()<1) *iter = MWWorld::Ptr(); } updateEffects(); } void MWMechanics::Alchemy::addPotion (const std::string& name) { ESM::Potion newRecord; newRecord.mData.mWeight = 0; for (TIngredientsIterator iter (beginIngredients()); iter!=endIngredients(); ++iter) if (!iter->isEmpty()) newRecord.mData.mWeight += iter->get()->mBase->mData.mWeight; if (countIngredients() > 0) newRecord.mData.mWeight /= countIngredients(); newRecord.mData.mValue = mValue; newRecord.mData.mAutoCalc = 0; newRecord.mName = name; int index = Misc::Rng::rollDice(6); assert (index>=0 && index<6); static const char *meshes[] = { "standard", "bargain", "cheap", "fresh", "exclusive", "quality" }; newRecord.mModel = "m\\misc_potion_" + std::string (meshes[index]) + "_01.nif"; newRecord.mIcon = "m\\tx_potion_" + std::string (meshes[index]) + "_01.dds"; newRecord.mEffects.mList = mEffects; /* Start of tes3mp change (major) Don't create a record and don't add the potion to the player's inventory; instead just store its record in preparation for sending it to the server and expect the server to add it to the player's inventory */ /* const ESM::Potion* record = getRecord(newRecord); if (!record) { record = MWBase::Environment::get().getWorld()->createRecord(newRecord); } mAlchemist.getClass().getContainerStore (mAlchemist).add (record->mId, 1, mAlchemist); */ mStoredPotion = newRecord; /* End of tes3mp change (major) */ } void MWMechanics::Alchemy::increaseSkill() { mAlchemist.getClass().skillUsageSucceeded (mAlchemist, ESM::Skill::Alchemy, 0); } float MWMechanics::Alchemy::getAlchemyFactor() const { const CreatureStats& creatureStats = mAlchemist.getClass().getCreatureStats (mAlchemist); return (mAlchemist.getClass().getSkill(mAlchemist, ESM::Skill::Alchemy) + 0.1f * creatureStats.getAttribute (ESM::Attribute::Intelligence).getModified() + 0.1f * creatureStats.getAttribute (ESM::Attribute::Luck).getModified()); } int MWMechanics::Alchemy::countIngredients() const { int ingredients = 0; for (TIngredientsIterator iter (beginIngredients()); iter!=endIngredients(); ++iter) if (!iter->isEmpty()) ++ingredients; return ingredients; } int MWMechanics::Alchemy::countPotionsToBrew() const { Result readyStatus = getReadyStatus(); if (readyStatus != Result_Success) return 0; int toBrew = -1; for (TIngredientsIterator iter (beginIngredients()); iter!=endIngredients(); ++iter) if (!iter->isEmpty()) { int count = iter->getRefData().getCount(); if ((count > 0 && count < toBrew) || toBrew < 0) toBrew = count; } return toBrew; } void MWMechanics::Alchemy::setAlchemist (const MWWorld::Ptr& npc) { mAlchemist = npc; mIngredients.resize (4); std::fill (mIngredients.begin(), mIngredients.end(), MWWorld::Ptr()); mTools.resize (4); std::fill (mTools.begin(), mTools.end(), MWWorld::Ptr()); mEffects.clear(); MWWorld::ContainerStore& store = npc.getClass().getContainerStore (npc); for (MWWorld::ContainerStoreIterator iter (store.begin (MWWorld::ContainerStore::Type_Apparatus)); iter!=store.end(); ++iter) { MWWorld::LiveCellRef* ref = iter->get(); int type = ref->mBase->mData.mType; if (type<0 || type>=static_cast (mTools.size())) throw std::runtime_error ("invalid apparatus type"); if (!mTools[type].isEmpty()) if (ref->mBase->mData.mQuality<=mTools[type].get()->mBase->mData.mQuality) continue; mTools[type] = *iter; } } MWMechanics::Alchemy::TToolsIterator MWMechanics::Alchemy::beginTools() const { return mTools.begin(); } MWMechanics::Alchemy::TToolsIterator MWMechanics::Alchemy::endTools() const { return mTools.end(); } MWMechanics::Alchemy::TIngredientsIterator MWMechanics::Alchemy::beginIngredients() const { return mIngredients.begin(); } MWMechanics::Alchemy::TIngredientsIterator MWMechanics::Alchemy::endIngredients() const { return mIngredients.end(); } void MWMechanics::Alchemy::clear() { mAlchemist = MWWorld::Ptr(); mTools.clear(); mIngredients.clear(); mEffects.clear(); setPotionName(""); } void MWMechanics::Alchemy::setPotionName(const std::string& name) { mPotionName = name; } int MWMechanics::Alchemy::addIngredient (const MWWorld::Ptr& ingredient) { // find a free slot int slot = -1; for (int i=0; i (mIngredients.size()); ++i) if (mIngredients[i].isEmpty()) { slot = i; break; } if (slot==-1) return -1; for (TIngredientsIterator iter (mIngredients.begin()); iter!=mIngredients.end(); ++iter) if (!iter->isEmpty() && Misc::StringUtils::ciEqual(ingredient.getCellRef().getRefId(), iter->getCellRef().getRefId())) return -1; mIngredients[slot] = ingredient; updateEffects(); return slot; } void MWMechanics::Alchemy::removeIngredient (int index) { if (index>=0 && index (mIngredients.size())) { mIngredients[index] = MWWorld::Ptr(); updateEffects(); } } MWMechanics::Alchemy::TEffectsIterator MWMechanics::Alchemy::beginEffects() const { return mEffects.begin(); } MWMechanics::Alchemy::TEffectsIterator MWMechanics::Alchemy::endEffects() const { return mEffects.end(); } bool MWMechanics::Alchemy::knownEffect(unsigned int potionEffectIndex, const MWWorld::Ptr &npc) { float alchemySkill = npc.getClass().getSkill (npc, ESM::Skill::Alchemy); static const float fWortChanceValue = MWBase::Environment::get().getWorld()->getStore().get().find("fWortChanceValue")->mValue.getFloat(); return (potionEffectIndex <= 1 && alchemySkill >= fWortChanceValue) || (potionEffectIndex <= 3 && alchemySkill >= fWortChanceValue*2) || (potionEffectIndex <= 5 && alchemySkill >= fWortChanceValue*3) || (potionEffectIndex <= 7 && alchemySkill >= fWortChanceValue*4); } MWMechanics::Alchemy::Result MWMechanics::Alchemy::getReadyStatus() const { if (mTools[ESM::Apparatus::MortarPestle].isEmpty()) return Result_NoMortarAndPestle; if (countIngredients()<2) return Result_LessThanTwoIngredients; if (mPotionName.empty()) return Result_NoName; if (listEffects().empty()) return Result_NoEffects; return Result_Success; } MWMechanics::Alchemy::Result MWMechanics::Alchemy::create (const std::string& name, int& count) { /* Start of tes3mp addition Instead of sending an ID_PLAYER_INVENTORY packet for every ingredient removal in ContainerStore::remove(), as that would get very spammy when many potions are created at the same time, just avoid sending packets here and store the item removals so they can be sent in a single packet when all the potions have been created */ mwmp::Main::get().getLocalPlayer()->avoidSendingInventoryPackets = true; /* End of tes3mp addition */ setPotionName(name); Result readyStatus = getReadyStatus(); if (readyStatus == Result_NoEffects) removeIngredients(); /* Start of tes3mp change (minor) Set avoidSendingInventoryPackets to false again if this has not been a successful potion creation */ if (readyStatus != Result_Success) { mwmp::Main::get().getLocalPlayer()->avoidSendingInventoryPackets = false; return readyStatus; } /* End of tes3mp change (major) */ Result result = Result_RandomFailure; int brewedCount = 0; for (int i = 0; i < count; ++i) { if (createSingle() == Result_Success) { result = Result_Success; brewedCount++; } } count = brewedCount; /* Start of tes3mp addition Send an ID_RECORD_DYNAMIC packet with the potion we've been creating now that we know its quantity Stop avoiding the sending of ID_PLAYER_INVENTORY packets and send all item removals stored so far */ mwmp::Main::get().getNetworking()->getWorldstate()->sendPotionRecord(&mStoredPotion, brewedCount); mwmp::Main::get().getLocalPlayer()->avoidSendingInventoryPackets = false; mwmp::Main::get().getLocalPlayer()->sendStoredItemRemovals(); /* End of tes3mp addition */ return result; } MWMechanics::Alchemy::Result MWMechanics::Alchemy::createSingle () { if (beginEffects() == endEffects()) { // all effects were nullified due to insufficient skill removeIngredients(); return Result_RandomFailure; } if (getAlchemyFactor() < Misc::Rng::roll0to99()) { removeIngredients(); return Result_RandomFailure; } addPotion(mPotionName); removeIngredients(); increaseSkill(); return Result_Success; } std::string MWMechanics::Alchemy::suggestPotionName() { std::set effects = listEffects(); if (effects.empty()) return ""; int effectId = effects.begin()->mId; return MWBase::Environment::get().getWorld()->getStore().get().find( ESM::MagicEffect::effectIdToString(effectId))->mValue.getString(); } std::vector MWMechanics::Alchemy::effectsDescription (const MWWorld::ConstPtr &ptr, const int alchemySkill) { std::vector effects; const auto& item = ptr.get()->mBase; const auto& gmst = MWBase::Environment::get().getWorld()->getStore().get(); const static auto fWortChanceValue = gmst.find("fWortChanceValue")->mValue.getFloat(); const auto& data = item->mData; for (auto i = 0; i < 4; ++i) { const auto effectID = data.mEffectID[i]; const auto skillID = data.mSkills[i]; const auto attributeID = data.mAttributes[i]; if (alchemySkill < fWortChanceValue * (i + 1)) break; if (effectID != -1) { std::string effect = gmst.find(ESM::MagicEffect::effectIdToString(effectID))->mValue.getString(); if (skillID != -1) effect += " " + gmst.find(ESM::Skill::sSkillNameIds[skillID])->mValue.getString(); else if (attributeID != -1) effect += " " + gmst.find(ESM::Attribute::sGmstAttributeIds[attributeID])->mValue.getString(); effects.push_back(effect); } } return effects; } ================================================ FILE: apps/openmw/mwmechanics/alchemy.hpp ================================================ #ifndef GAME_MWMECHANICS_ALCHEMY_H #define GAME_MWMECHANICS_ALCHEMY_H #include #include /* Start of tes3mp addition Include additional headers for multiplayer purposes */ #include /* End of tes3mp addition */ #include #include "../mwworld/ptr.hpp" namespace ESM { struct Potion; } namespace MWMechanics { struct EffectKey; /// \brief Potion creation via alchemy skill class Alchemy { public: Alchemy(); typedef std::vector TToolsContainer; typedef TToolsContainer::const_iterator TToolsIterator; typedef std::vector TIngredientsContainer; typedef TIngredientsContainer::const_iterator TIngredientsIterator; typedef std::vector TEffectsContainer; typedef TEffectsContainer::const_iterator TEffectsIterator; enum Result { Result_Success, Result_NoMortarAndPestle, Result_LessThanTwoIngredients, Result_NoName, Result_NoEffects, Result_RandomFailure }; private: MWWorld::Ptr mAlchemist; TToolsContainer mTools; TIngredientsContainer mIngredients; TEffectsContainer mEffects; int mValue; std::string mPotionName; /* Start of tes3mp addition Keep a copy of the last created potion record so it can be sent to the server once we have determined its brewedCount */ ESM::Potion mStoredPotion; /* End of tes3mp addition */ void applyTools (int flags, float& value) const; void updateEffects(); Result getReadyStatus() const; const ESM::Potion *getRecord(const ESM::Potion& toFind) const; ///< Try to find a potion record similar to \a toFind in the record store, or return 0 if not found /// \note Does not account for record ID, model or icon void removeIngredients(); ///< Remove selected ingredients from alchemist's inventory, cleanup selected ingredients and /// update effect list accordingly. void addPotion (const std::string& name); ///< Add a potion to the alchemist's inventory. void increaseSkill(); ///< Increase alchemist's skill. Result createSingle (); ///< Try to create a potion from the ingredients, place it in the inventory of the alchemist and /// adjust the skills of the alchemist accordingly. float getAlchemyFactor() const; int countIngredients() const; TEffectsIterator beginEffects() const; TEffectsIterator endEffects() const; public: int countPotionsToBrew() const; ///< calculates maximum amount of potions, which you can make from selected ingredients static bool knownEffect (unsigned int potionEffectIndex, const MWWorld::Ptr& npc); ///< Does npc have sufficient alchemy skill to know about this potion effect? void setAlchemist (const MWWorld::Ptr& npc); ///< Set alchemist and configure alchemy setup accordingly. \a npc may be empty to indicate that /// there is no alchemist (alchemy session has ended). TToolsIterator beginTools() const; ///< \attention Iterates over tool slots, not over tools. Some of the slots may be empty. TToolsIterator endTools() const; TIngredientsIterator beginIngredients() const; ///< \attention Iterates over ingredient slots, not over ingredients. Some of the slots may be empty. TIngredientsIterator endIngredients() const; void clear(); ///< Remove alchemist, tools and ingredients. void setPotionName(const std::string& name); ///< Set name of potion to create std::set listEffects() const; ///< List all effects shared by at least two ingredients. int addIngredient (const MWWorld::Ptr& ingredient); ///< Add ingredient into the next free slot. /// /// \return Slot index or -1, if adding failed because of no free slot or the ingredient type being /// listed already. void removeIngredient (int index); ///< Remove ingredient from slot (calling this function on an empty slot is a no-op). std::string suggestPotionName (); ///< Suggest a name for the potion, based on the current effects Result create (const std::string& name, int& count); ///< Try to create potions from the ingredients, place them in the inventory of the alchemist and /// adjust the skills of the alchemist accordingly. /// \param name must not be an empty string, or Result_NoName is returned static std::vector effectsDescription (const MWWorld::ConstPtr &ptr, const int alchemySKill); }; } #endif ================================================ FILE: apps/openmw/mwmechanics/autocalcspell.cpp ================================================ #include "autocalcspell.hpp" #include #include "../mwworld/esmstore.hpp" #include "../mwbase/world.hpp" #include "../mwbase/environment.hpp" #include "spellutil.hpp" namespace MWMechanics { struct SchoolCaps { int mCount; int mLimit; bool mReachedLimit; int mMinCost; std::string mWeakestSpell; }; std::vector autoCalcNpcSpells(const int *actorSkills, const int *actorAttributes, const ESM::Race* race) { const MWWorld::Store& gmst = MWBase::Environment::get().getWorld()->getStore().get(); static const float fNPCbaseMagickaMult = gmst.find("fNPCbaseMagickaMult")->mValue.getFloat(); float baseMagicka = fNPCbaseMagickaMult * actorAttributes[ESM::Attribute::Intelligence]; static const std::string schools[] = { "alteration", "conjuration", "destruction", "illusion", "mysticism", "restoration" }; static int iAutoSpellSchoolMax[6]; static bool init = false; if (!init) { for (int i=0; i<6; ++i) { const std::string& gmstName = "iAutoSpell" + schools[i] + "Max"; iAutoSpellSchoolMax[i] = gmst.find(gmstName)->mValue.getInteger(); } init = true; } std::map schoolCaps; for (int i=0; i<6; ++i) { SchoolCaps caps; caps.mCount = 0; caps.mLimit = iAutoSpellSchoolMax[i]; caps.mReachedLimit = iAutoSpellSchoolMax[i] <= 0; caps.mMinCost = std::numeric_limits::max(); caps.mWeakestSpell.clear(); schoolCaps[i] = caps; } std::vector selectedSpells; const MWWorld::Store &spells = MWBase::Environment::get().getWorld()->getStore().get(); // Note: the algorithm heavily depends on the traversal order of the spells. For vanilla-compatible results the // Store must preserve the record ordering as it was in the content files. for (const ESM::Spell& spell : spells) { if (spell.mData.mType != ESM::Spell::ST_Spell) continue; if (!(spell.mData.mFlags & ESM::Spell::F_Autocalc)) continue; static const int iAutoSpellTimesCanCast = gmst.find("iAutoSpellTimesCanCast")->mValue.getInteger(); if (baseMagicka < iAutoSpellTimesCanCast * spell.mData.mCost) continue; if (race && race->mPowers.exists(spell.mId)) continue; if (!attrSkillCheck(&spell, actorSkills, actorAttributes)) continue; int school; float skillTerm; calcWeakestSchool(&spell, actorSkills, school, skillTerm); assert(school >= 0 && school < 6); SchoolCaps& cap = schoolCaps[school]; if (cap.mReachedLimit && spell.mData.mCost <= cap.mMinCost) continue; static const float fAutoSpellChance = gmst.find("fAutoSpellChance")->mValue.getFloat(); if (calcAutoCastChance(&spell, actorSkills, actorAttributes, school) < fAutoSpellChance) continue; selectedSpells.push_back(spell.mId); if (cap.mReachedLimit) { std::vector::iterator found = std::find(selectedSpells.begin(), selectedSpells.end(), cap.mWeakestSpell); if (found != selectedSpells.end()) selectedSpells.erase(found); cap.mMinCost = std::numeric_limits::max(); for (const std::string& testSpellName : selectedSpells) { const ESM::Spell* testSpell = spells.find(testSpellName); //int testSchool; //float dummySkillTerm; //calcWeakestSchool(testSpell, actorSkills, testSchool, dummySkillTerm); // Note: if there are multiple spells with the same cost, we pick the first one we found. // So the algorithm depends on the iteration order of the outer loop. if ( // There is a huge bug here. It is not checked that weakestSpell is of the correct school. // As result multiple SchoolCaps could have the same mWeakestSpell. Erasing the weakest spell would then fail if another school // already erased it, and so the number of spells would often exceed the sum of limits. // This bug cannot be fixed without significantly changing the results of the spell autocalc, which will not have been playtested. //testSchool == school && testSpell->mData.mCost < cap.mMinCost) { cap.mMinCost = testSpell->mData.mCost; cap.mWeakestSpell = testSpell->mId; } } } else { cap.mCount += 1; if (cap.mCount == cap.mLimit) cap.mReachedLimit = true; if (spell.mData.mCost < cap.mMinCost) { cap.mWeakestSpell = spell.mId; cap.mMinCost = spell.mData.mCost; } } } return selectedSpells; } std::vector autoCalcPlayerSpells(const int* actorSkills, const int* actorAttributes, const ESM::Race* race) { const MWWorld::ESMStore& esmStore = MWBase::Environment::get().getWorld()->getStore(); static const float fPCbaseMagickaMult = esmStore.get().find("fPCbaseMagickaMult")->mValue.getFloat(); float baseMagicka = fPCbaseMagickaMult * actorAttributes[ESM::Attribute::Intelligence]; bool reachedLimit = false; const ESM::Spell* weakestSpell = nullptr; int minCost = std::numeric_limits::max(); std::vector selectedSpells; const MWWorld::Store &spells = esmStore.get(); for (const ESM::Spell& spell : spells) { if (spell.mData.mType != ESM::Spell::ST_Spell) continue; if (!(spell.mData.mFlags & ESM::Spell::F_PCStart)) continue; if (reachedLimit && spell.mData.mCost <= minCost) continue; if (race && std::find(race->mPowers.mList.begin(), race->mPowers.mList.end(), spell.mId) != race->mPowers.mList.end()) continue; if (baseMagicka < spell.mData.mCost) continue; static const float fAutoPCSpellChance = esmStore.get().find("fAutoPCSpellChance")->mValue.getFloat(); if (calcAutoCastChance(&spell, actorSkills, actorAttributes, -1) < fAutoPCSpellChance) continue; if (!attrSkillCheck(&spell, actorSkills, actorAttributes)) continue; selectedSpells.push_back(spell.mId); if (reachedLimit) { std::vector::iterator it = std::find(selectedSpells.begin(), selectedSpells.end(), weakestSpell->mId); if (it != selectedSpells.end()) selectedSpells.erase(it); minCost = std::numeric_limits::max(); for (const std::string& testSpellName : selectedSpells) { const ESM::Spell* testSpell = esmStore.get().find(testSpellName); if (testSpell->mData.mCost < minCost) { minCost = testSpell->mData.mCost; weakestSpell = testSpell; } } } else { if (spell.mData.mCost < minCost) { weakestSpell = &spell; minCost = weakestSpell->mData.mCost; } static const unsigned int iAutoPCSpellMax = esmStore.get().find("iAutoPCSpellMax")->mValue.getInteger(); if (selectedSpells.size() == iAutoPCSpellMax) reachedLimit = true; } } return selectedSpells; } bool attrSkillCheck (const ESM::Spell* spell, const int* actorSkills, const int* actorAttributes) { for (const auto& spellEffect : spell->mEffects.mList) { const ESM::MagicEffect* magicEffect = MWBase::Environment::get().getWorld()->getStore().get().find(spellEffect.mEffectID); static const int iAutoSpellAttSkillMin = MWBase::Environment::get().getWorld()->getStore().get().find("iAutoSpellAttSkillMin")->mValue.getInteger(); if ((magicEffect->mData.mFlags & ESM::MagicEffect::TargetSkill)) { assert (spellEffect.mSkill >= 0 && spellEffect.mSkill < ESM::Skill::Length); if (actorSkills[spellEffect.mSkill] < iAutoSpellAttSkillMin) return false; } if ((magicEffect->mData.mFlags & ESM::MagicEffect::TargetAttribute)) { assert (spellEffect.mAttribute >= 0 && spellEffect.mAttribute < ESM::Attribute::Length); if (actorAttributes[spellEffect.mAttribute] < iAutoSpellAttSkillMin) return false; } } return true; } void calcWeakestSchool (const ESM::Spell* spell, const int* actorSkills, int& effectiveSchool, float& skillTerm) { // Morrowind for some reason uses a formula slightly different from magicka cost calculation float minChance = std::numeric_limits::max(); for (const ESM::ENAMstruct& effect : spell->mEffects.mList) { const ESM::MagicEffect* magicEffect = MWBase::Environment::get().getWorld()->getStore().get().find(effect.mEffectID); int minMagn = 1; int maxMagn = 1; if (!(magicEffect->mData.mFlags & ESM::MagicEffect::NoMagnitude)) { minMagn = effect.mMagnMin; maxMagn = effect.mMagnMax; } int duration = 0; if (!(magicEffect->mData.mFlags & ESM::MagicEffect::NoDuration)) duration = effect.mDuration; if (!(magicEffect->mData.mFlags & ESM::MagicEffect::AppliedOnce)) duration = std::max(1, duration); static const float fEffectCostMult = MWBase::Environment::get().getWorld()->getStore() .get().find("fEffectCostMult")->mValue.getFloat(); float x = 0.5 * (std::max(1, minMagn) + std::max(1, maxMagn)); x *= 0.1 * magicEffect->mData.mBaseCost; x *= 1 + duration; x += 0.05 * std::max(1, effect.mArea) * magicEffect->mData.mBaseCost; x *= fEffectCostMult; if (effect.mRange == ESM::RT_Target) x *= 1.5f; float s = 2.f * actorSkills[spellSchoolToSkill(magicEffect->mData.mSchool)]; if (s - x < minChance) { minChance = s - x; effectiveSchool = magicEffect->mData.mSchool; skillTerm = s; } } } float calcAutoCastChance(const ESM::Spell *spell, const int *actorSkills, const int *actorAttributes, int effectiveSchool) { if (spell->mData.mType != ESM::Spell::ST_Spell) return 100.f; if (spell->mData.mFlags & ESM::Spell::F_Always) return 100.f; float skillTerm = 0; if (effectiveSchool != -1) skillTerm = 2.f * actorSkills[spellSchoolToSkill(effectiveSchool)]; else calcWeakestSchool(spell, actorSkills, effectiveSchool, skillTerm); // Note effectiveSchool is unused after this float castChance = skillTerm - spell->mData.mCost + 0.2f * actorAttributes[ESM::Attribute::Willpower] + 0.1f * actorAttributes[ESM::Attribute::Luck]; return castChance; } } ================================================ FILE: apps/openmw/mwmechanics/autocalcspell.hpp ================================================ #ifndef OPENMW_AUTOCALCSPELL_H #define OPENMW_AUTOCALCSPELL_H #include #include namespace ESM { struct Spell; struct Race; } namespace MWMechanics { /// Contains algorithm for calculating an NPC's spells based on stats /// @note We might want to move this code to a component later, so the editor can use it for preview purposes std::vector autoCalcNpcSpells(const int* actorSkills, const int* actorAttributes, const ESM::Race* race); std::vector autoCalcPlayerSpells(const int* actorSkills, const int* actorAttributes, const ESM::Race* race); // Helpers bool attrSkillCheck (const ESM::Spell* spell, const int* actorSkills, const int* actorAttributes); void calcWeakestSchool(const ESM::Spell* spell, const int* actorSkills, int& effectiveSchool, float& skillTerm); float calcAutoCastChance(const ESM::Spell* spell, const int* actorSkills, const int* actorAttributes, int effectiveSchool); } #endif ================================================ FILE: apps/openmw/mwmechanics/character.cpp ================================================ /* * OpenMW - The completely unofficial reimplementation of Morrowind * * This file (character.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 * https://www.gnu.org/licenses/ . */ #include "character.hpp" #include #include #include #include #include /* Start of tes3mp addition Include additional headers for multiplayer purposes */ #include #include "../mwmp/Main.hpp" #include "../mwmp/LocalPlayer.hpp" #include "../mwmp/LocalActor.hpp" #include "../mwmp/PlayerList.hpp" #include "../mwmp/DedicatedPlayer.hpp" #include "../mwmp/CellController.hpp" #include "../mwmp/MechanicsHelper.hpp" /* End of tes3mp addition */ #include "../mwrender/animation.hpp" #include "../mwbase/environment.hpp" #include "../mwbase/mechanicsmanager.hpp" #include "../mwbase/world.hpp" #include "../mwbase/soundmanager.hpp" #include "../mwbase/windowmanager.hpp" #include "../mwworld/class.hpp" #include "../mwworld/inventorystore.hpp" #include "../mwworld/esmstore.hpp" #include "../mwworld/player.hpp" #include "aicombataction.hpp" #include "movement.hpp" #include "npcstats.hpp" #include "creaturestats.hpp" #include "security.hpp" #include "actorutil.hpp" #include "spellcasting.hpp" namespace { std::string getBestAttack (const ESM::Weapon* weapon) { int slash = (weapon->mData.mSlash[0] + weapon->mData.mSlash[1])/2; int chop = (weapon->mData.mChop[0] + weapon->mData.mChop[1])/2; int thrust = (weapon->mData.mThrust[0] + weapon->mData.mThrust[1])/2; if (slash == chop && slash == thrust) return "slash"; else if (thrust >= chop && thrust >= slash) return "thrust"; else if (slash >= chop && slash >= thrust) return "slash"; else return "chop"; } // Converts a movement Run state to its equivalent Walk state. MWMechanics::CharacterState runStateToWalkState (MWMechanics::CharacterState state) { using namespace MWMechanics; CharacterState ret = state; switch (state) { case CharState_RunForward: ret = CharState_WalkForward; break; case CharState_RunBack: ret = CharState_WalkBack; break; case CharState_RunLeft: ret = CharState_WalkLeft; break; case CharState_RunRight: ret = CharState_WalkRight; break; case CharState_SwimRunForward: ret = CharState_SwimWalkForward; break; case CharState_SwimRunBack: ret = CharState_SwimWalkBack; break; case CharState_SwimRunLeft: ret = CharState_SwimWalkLeft; break; case CharState_SwimRunRight: ret = CharState_SwimWalkRight; break; default: break; } return ret; } float getFallDamage(const MWWorld::Ptr& ptr, float fallHeight) { MWBase::World *world = MWBase::Environment::get().getWorld(); const MWWorld::Store &store = world->getStore().get(); const float fallDistanceMin = store.find("fFallDamageDistanceMin")->mValue.getFloat(); if (fallHeight >= fallDistanceMin) { const float acrobaticsSkill = static_cast(ptr.getClass().getSkill(ptr, ESM::Skill::Acrobatics)); const float jumpSpellBonus = ptr.getClass().getCreatureStats(ptr).getMagicEffects().get(ESM::MagicEffect::Jump).getMagnitude(); const float fallAcroBase = store.find("fFallAcroBase")->mValue.getFloat(); const float fallAcroMult = store.find("fFallAcroMult")->mValue.getFloat(); const float fallDistanceBase = store.find("fFallDistanceBase")->mValue.getFloat(); const float fallDistanceMult = store.find("fFallDistanceMult")->mValue.getFloat(); float x = fallHeight - fallDistanceMin; x -= (1.5f * acrobaticsSkill) + jumpSpellBonus; x = std::max(0.0f, x); float a = fallAcroBase + fallAcroMult * (100 - acrobaticsSkill); x = fallDistanceBase + fallDistanceMult * x; x *= a; return x; } return 0.f; } } namespace MWMechanics { struct StateInfo { CharacterState state; const char groupname[32]; }; static const StateInfo sMovementList[] = { { CharState_WalkForward, "walkforward" }, { CharState_WalkBack, "walkback" }, { CharState_WalkLeft, "walkleft" }, { CharState_WalkRight, "walkright" }, { CharState_SwimWalkForward, "swimwalkforward" }, { CharState_SwimWalkBack, "swimwalkback" }, { CharState_SwimWalkLeft, "swimwalkleft" }, { CharState_SwimWalkRight, "swimwalkright" }, { CharState_RunForward, "runforward" }, { CharState_RunBack, "runback" }, { CharState_RunLeft, "runleft" }, { CharState_RunRight, "runright" }, { CharState_SwimRunForward, "swimrunforward" }, { CharState_SwimRunBack, "swimrunback" }, { CharState_SwimRunLeft, "swimrunleft" }, { CharState_SwimRunRight, "swimrunright" }, { CharState_SneakForward, "sneakforward" }, { CharState_SneakBack, "sneakback" }, { CharState_SneakLeft, "sneakleft" }, { CharState_SneakRight, "sneakright" }, { CharState_Jump, "jump" }, { CharState_TurnLeft, "turnleft" }, { CharState_TurnRight, "turnright" }, { CharState_SwimTurnLeft, "swimturnleft" }, { CharState_SwimTurnRight, "swimturnright" }, }; static const StateInfo *sMovementListEnd = &sMovementList[sizeof(sMovementList)/sizeof(sMovementList[0])]; class FindCharState { CharacterState state; public: FindCharState(CharacterState _state) : state(_state) { } bool operator()(const StateInfo &info) const { return info.state == state; } }; std::string CharacterController::chooseRandomGroup (const std::string& prefix, int* num) const { int numAnims=0; while (mAnimation->hasAnimation(prefix + std::to_string(numAnims+1))) ++numAnims; int roll = Misc::Rng::rollDice(numAnims) + 1; // [1, numAnims] if (num) *num = roll; return prefix + std::to_string(roll); } void CharacterController::refreshHitRecoilAnims(CharacterState& idle) { bool recovery = mPtr.getClass().getCreatureStats(mPtr).getHitRecovery(); bool knockdown = mPtr.getClass().getCreatureStats(mPtr).getKnockedDown(); bool block = mPtr.getClass().getCreatureStats(mPtr).getBlock(); bool isSwimming = MWBase::Environment::get().getWorld()->isSwimming(mPtr); if(mHitState == CharState_None) { if ((mPtr.getClass().getCreatureStats(mPtr).getFatigue().getCurrent() < 0 || mPtr.getClass().getCreatureStats(mPtr).getFatigue().getBase() == 0)) { mTimeUntilWake = Misc::Rng::rollClosedProbability() * 2 + 1; // Wake up after 1 to 3 seconds if (isSwimming && mAnimation->hasAnimation("swimknockout")) { mHitState = CharState_SwimKnockOut; mCurrentHit = "swimknockout"; mAnimation->play(mCurrentHit, Priority_Knockdown, MWRender::Animation::BlendMask_All, false, 1, "start", "stop", 0.0f, ~0ul); } else if (!isSwimming && mAnimation->hasAnimation("knockout")) { mHitState = CharState_KnockOut; mCurrentHit = "knockout"; mAnimation->play(mCurrentHit, Priority_Knockdown, MWRender::Animation::BlendMask_All, false, 1, "start", "stop", 0.0f, ~0ul); } else { // Knockout animations are missing. Fall back to idle animation, so target actor still can be killed via HtH. mCurrentHit.erase(); } mPtr.getClass().getCreatureStats(mPtr).setKnockedDown(true); } else if (knockdown) { if (isSwimming && mAnimation->hasAnimation("swimknockdown")) { mHitState = CharState_SwimKnockDown; mCurrentHit = "swimknockdown"; mAnimation->play(mCurrentHit, Priority_Knockdown, MWRender::Animation::BlendMask_All, true, 1, "start", "stop", 0.0f, 0); } else if (!isSwimming && mAnimation->hasAnimation("knockdown")) { mHitState = CharState_KnockDown; mCurrentHit = "knockdown"; mAnimation->play(mCurrentHit, Priority_Knockdown, MWRender::Animation::BlendMask_All, true, 1, "start", "stop", 0.0f, 0); } else { // Knockdown animation is missing. Cancel knockdown state. mPtr.getClass().getCreatureStats(mPtr).setKnockedDown(false); } } else if (recovery) { std::string anim = chooseRandomGroup("swimhit"); if (isSwimming && mAnimation->hasAnimation(anim)) { mHitState = CharState_SwimHit; mCurrentHit = anim; mAnimation->play(mCurrentHit, Priority_Hit, MWRender::Animation::BlendMask_All, true, 1, "start", "stop", 0.0f, 0); } else { anim = chooseRandomGroup("hit"); if (mAnimation->hasAnimation(anim)) { mHitState = CharState_Hit; mCurrentHit = anim; mAnimation->play(mCurrentHit, Priority_Hit, MWRender::Animation::BlendMask_All, true, 1, "start", "stop", 0.0f, 0); } } } else if (block && mAnimation->hasAnimation("shield")) { mHitState = CharState_Block; mCurrentHit = "shield"; MWRender::Animation::AnimPriority priorityBlock (Priority_Hit); priorityBlock[MWRender::Animation::BoneGroup_LeftArm] = Priority_Block; priorityBlock[MWRender::Animation::BoneGroup_LowerBody] = Priority_WeaponLowerBody; mAnimation->play(mCurrentHit, priorityBlock, MWRender::Animation::BlendMask_All, true, 1, "block start", "block stop", 0.0f, 0); } // Cancel upper body animations if (isKnockedOut() || isKnockedDown()) { if (mUpperBodyState > UpperCharState_WeapEquiped) { mAnimation->disable(mCurrentWeapon); mUpperBodyState = UpperCharState_WeapEquiped; if (mWeaponType > ESM::Weapon::None) mAnimation->showWeapons(true); } else if (mUpperBodyState > UpperCharState_Nothing && mUpperBodyState < UpperCharState_WeapEquiped) { mAnimation->disable(mCurrentWeapon); mUpperBodyState = UpperCharState_Nothing; } } if (mHitState != CharState_None) idle = CharState_None; } else if(!mAnimation->isPlaying(mCurrentHit)) { mCurrentHit.erase(); if (knockdown) mPtr.getClass().getCreatureStats(mPtr).setKnockedDown(false); if (recovery) mPtr.getClass().getCreatureStats(mPtr).setHitRecovery(false); if (block) mPtr.getClass().getCreatureStats(mPtr).setBlock(false); mHitState = CharState_None; } else if (isKnockedOut() && mPtr.getClass().getCreatureStats(mPtr).getFatigue().getCurrent() > 0 && mTimeUntilWake <= 0) { mHitState = isSwimming ? CharState_SwimKnockDown : CharState_KnockDown; mAnimation->disable(mCurrentHit); mAnimation->play(mCurrentHit, Priority_Knockdown, MWRender::Animation::BlendMask_All, true, 1, "loop stop", "stop", 0.0f, 0); } } void CharacterController::refreshJumpAnims(const std::string& weapShortGroup, JumpingState jump, CharacterState& idle, bool force) { if (!force && jump == mJumpState && idle == CharState_None) return; std::string jumpAnimName; MWRender::Animation::BlendMask jumpmask = MWRender::Animation::BlendMask_All; if (jump != JumpState_None) { jumpAnimName = "jump"; if(!weapShortGroup.empty()) { jumpAnimName += weapShortGroup; if(!mAnimation->hasAnimation(jumpAnimName)) { jumpAnimName = fallbackShortWeaponGroup("jump", &jumpmask); // If we apply jump only for lower body, do not reset idle animations. // For upper body there will be idle animation. if (jumpmask == MWRender::Animation::BlendMask_LowerBody && idle == CharState_None) idle = CharState_Idle; } } } if (!force && jump == mJumpState) return; bool startAtLoop = (jump == mJumpState); mJumpState = jump; if (!mCurrentJump.empty()) { mAnimation->disable(mCurrentJump); mCurrentJump.clear(); } if(mJumpState == JumpState_InAir) { if (mAnimation->hasAnimation(jumpAnimName)) { mAnimation->play(jumpAnimName, Priority_Jump, jumpmask, false, 1.0f, startAtLoop ? "loop start" : "start", "stop", 0.f, ~0ul); mCurrentJump = jumpAnimName; } } else if (mJumpState == JumpState_Landing) { if (mAnimation->hasAnimation(jumpAnimName)) { mAnimation->play(jumpAnimName, Priority_Jump, jumpmask, true, 1.0f, "loop stop", "stop", 0.0f, 0); mCurrentJump = jumpAnimName; } } } bool CharacterController::onOpen() { if (mPtr.getTypeName() == typeid(ESM::Container).name()) { if (!mAnimation->hasAnimation("containeropen")) return true; if (mAnimation->isPlaying("containeropen")) return false; if (mAnimation->isPlaying("containerclose")) return false; mAnimation->play("containeropen", Priority_Persistent, MWRender::Animation::BlendMask_All, false, 1.0f, "start", "stop", 0.f, 0); if (mAnimation->isPlaying("containeropen")) return false; } return true; } void CharacterController::onClose() { if (mPtr.getTypeName() == typeid(ESM::Container).name()) { if (!mAnimation->hasAnimation("containerclose")) return; float complete, startPoint = 0.f; bool animPlaying = mAnimation->getInfo("containeropen", &complete); if (animPlaying) startPoint = 1.f - complete; mAnimation->play("containerclose", Priority_Persistent, MWRender::Animation::BlendMask_All, false, 1.0f, "start", "stop", startPoint, 0); } } std::string CharacterController::getWeaponAnimation(int weaponType) const { std::string weaponGroup = getWeaponType(weaponType)->mLongGroup; bool isRealWeapon = weaponType != ESM::Weapon::HandToHand && weaponType != ESM::Weapon::Spell && weaponType != ESM::Weapon::None; if (isRealWeapon && !mAnimation->hasAnimation(weaponGroup)) { static const std::string oneHandFallback = getWeaponType(ESM::Weapon::LongBladeOneHand)->mLongGroup; static const std::string twoHandFallback = getWeaponType(ESM::Weapon::LongBladeTwoHand)->mLongGroup; const ESM::WeaponType* weapInfo = getWeaponType(weaponType); // For real two-handed melee weapons use 2h swords animations as fallback, otherwise use the 1h ones if (weapInfo->mFlags & ESM::WeaponType::TwoHanded && weapInfo->mWeaponClass == ESM::WeaponType::Melee) weaponGroup = twoHandFallback; else if (isRealWeapon) weaponGroup = oneHandFallback; } return weaponGroup; } std::string CharacterController::fallbackShortWeaponGroup(const std::string& baseGroupName, MWRender::Animation::BlendMask* blendMask) { bool isRealWeapon = mWeaponType != ESM::Weapon::HandToHand && mWeaponType != ESM::Weapon::Spell && mWeaponType != ESM::Weapon::None; if (!isRealWeapon) { if (blendMask != nullptr) *blendMask = MWRender::Animation::BlendMask_LowerBody; return baseGroupName; } static const std::string oneHandFallback = getWeaponType(ESM::Weapon::LongBladeOneHand)->mShortGroup; static const std::string twoHandFallback = getWeaponType(ESM::Weapon::LongBladeTwoHand)->mShortGroup; std::string groupName = baseGroupName; const ESM::WeaponType* weapInfo = getWeaponType(mWeaponType); // For real two-handed melee weapons use 2h swords animations as fallback, otherwise use the 1h ones if (weapInfo->mFlags & ESM::WeaponType::TwoHanded && weapInfo->mWeaponClass == ESM::WeaponType::Melee) groupName += twoHandFallback; else groupName += oneHandFallback; // Special case for crossbows - we shouls apply 1h animations a fallback only for lower body if (mWeaponType == ESM::Weapon::MarksmanCrossbow && blendMask != nullptr) *blendMask = MWRender::Animation::BlendMask_LowerBody; if (!mAnimation->hasAnimation(groupName)) { groupName = baseGroupName; if (blendMask != nullptr) *blendMask = MWRender::Animation::BlendMask_LowerBody; } return groupName; } void CharacterController::refreshMovementAnims(const std::string& weapShortGroup, CharacterState movement, CharacterState& idle, bool force) { if (movement == mMovementState && idle == mIdleState && !force) return; // Reset idle if we actually play movement animations excepts of these cases: // 1. When we play turning animations // 2. When we use a fallback animation for lower body since movement animation for given weapon is missing (e.g. for crossbows and spellcasting) bool resetIdle = (movement != CharState_None && !isTurning()); std::string movementAnimName; MWRender::Animation::BlendMask movemask; const StateInfo *movestate; movemask = MWRender::Animation::BlendMask_All; movestate = std::find_if(sMovementList, sMovementListEnd, FindCharState(movement)); if(movestate != sMovementListEnd) { movementAnimName = movestate->groupname; if(!weapShortGroup.empty()) { std::string::size_type swimpos = movementAnimName.find("swim"); if (swimpos == std::string::npos) { if (mWeaponType == ESM::Weapon::Spell && (movement == CharState_TurnLeft || movement == CharState_TurnRight)) // Spellcasting stance turning is a special case movementAnimName = weapShortGroup + movementAnimName; else movementAnimName += weapShortGroup; } if(!mAnimation->hasAnimation(movementAnimName)) { movementAnimName = movestate->groupname; if (swimpos == std::string::npos) { movementAnimName = fallbackShortWeaponGroup(movementAnimName, &movemask); // If we apply movement only for lower body, do not reset idle animations. // For upper body there will be idle animation. if (movemask == MWRender::Animation::BlendMask_LowerBody && idle == CharState_None) idle = CharState_Idle; if (movemask == MWRender::Animation::BlendMask_LowerBody) resetIdle = false; } } } } if(force || movement != mMovementState) { mMovementState = movement; if(movestate != sMovementListEnd) { if(!mAnimation->hasAnimation(movementAnimName)) { std::string::size_type swimpos = movementAnimName.find("swim"); if (swimpos != std::string::npos) { movementAnimName.erase(swimpos, 4); if (!weapShortGroup.empty()) { std::string weapMovementAnimName = movementAnimName + weapShortGroup; if(mAnimation->hasAnimation(weapMovementAnimName)) movementAnimName = weapMovementAnimName; else { movementAnimName = fallbackShortWeaponGroup(movementAnimName, &movemask); if (movemask == MWRender::Animation::BlendMask_LowerBody) resetIdle = false; } } } if (swimpos == std::string::npos || !mAnimation->hasAnimation(movementAnimName)) { std::string::size_type runpos = movementAnimName.find("run"); if (runpos != std::string::npos) { movementAnimName.replace(runpos, runpos+3, "walk"); if (!mAnimation->hasAnimation(movementAnimName)) movementAnimName.clear(); } else movementAnimName.clear(); } } } // If we're playing the same animation, start it from the point it ended float startpoint = 0.f; if (!mCurrentMovement.empty() && movementAnimName == mCurrentMovement) mAnimation->getInfo(mCurrentMovement, &startpoint); mMovementAnimationControlled = true; mAnimation->disable(mCurrentMovement); if (!mAnimation->hasAnimation(movementAnimName)) movementAnimName.clear(); mCurrentMovement = movementAnimName; if(!mCurrentMovement.empty()) { if (resetIdle) { mAnimation->disable(mCurrentIdle); mIdleState = CharState_None; idle = CharState_None; } // For non-flying creatures, MW uses the Walk animation to calculate the animation velocity // even if we are running. This must be replicated, otherwise the observed speed would differ drastically. std::string anim = mCurrentMovement; mAdjustMovementAnimSpeed = true; if (mPtr.getClass().getTypeName() == typeid(ESM::Creature).name() && !(mPtr.get()->mBase->mFlags & ESM::Creature::Flies)) { CharacterState walkState = runStateToWalkState(mMovementState); const StateInfo *stateinfo = std::find_if(sMovementList, sMovementListEnd, FindCharState(walkState)); anim = stateinfo->groupname; mMovementAnimSpeed = mAnimation->getVelocity(anim); if (mMovementAnimSpeed <= 1.0f) { // Another bug: when using a fallback animation (e.g. RunForward as fallback to SwimRunForward), // then the equivalent Walk animation will not use a fallback, and if that animation doesn't exist // we will play without any scaling. // Makes the speed attribute of most water creatures totally useless. // And again, this can not be fixed without patching game data. mAdjustMovementAnimSpeed = false; mMovementAnimSpeed = 1.f; } } else { mMovementAnimSpeed = mAnimation->getVelocity(anim); if (mMovementAnimSpeed <= 1.0f) { // The first person anims don't have any velocity to calculate a speed multiplier from. // We use the third person velocities instead. // FIXME: should be pulled from the actual animation, but it is not presently loaded. bool sneaking = mMovementState == CharState_SneakForward || mMovementState == CharState_SneakBack || mMovementState == CharState_SneakLeft || mMovementState == CharState_SneakRight; mMovementAnimSpeed = (sneaking ? 33.5452f : (isRunning() ? 222.857f : 154.064f)); mMovementAnimationControlled = false; } } mAnimation->play(mCurrentMovement, Priority_Movement, movemask, false, 1.f, "start", "stop", startpoint, ~0ul, true); } else mMovementState = CharState_None; } } void CharacterController::refreshIdleAnims(const std::string& weapShortGroup, CharacterState idle, bool force) { // FIXME: if one of the below states is close to their last animation frame (i.e. will be disabled in the coming update), // the idle animation should be displayed if (((mUpperBodyState != UpperCharState_Nothing && mUpperBodyState != UpperCharState_WeapEquiped) || (mMovementState != CharState_None && !isTurning()) || mHitState != CharState_None) && !mPtr.getClass().isBipedal(mPtr)) idle = CharState_None; if(force || idle != mIdleState || (!mAnimation->isPlaying(mCurrentIdle) && mAnimQueue.empty())) { mIdleState = idle; size_t numLoops = ~0ul; std::string idleGroup; MWRender::Animation::AnimPriority idlePriority (Priority_Default); // Only play "idleswim" or "idlesneak" if they exist. Otherwise, fallback to // "idle"+weapon or "idle". if(mIdleState == CharState_IdleSwim && mAnimation->hasAnimation("idleswim")) { idleGroup = "idleswim"; idlePriority = Priority_SwimIdle; } else if(mIdleState == CharState_IdleSneak && mAnimation->hasAnimation("idlesneak")) { idleGroup = "idlesneak"; idlePriority[MWRender::Animation::BoneGroup_LowerBody] = Priority_SneakIdleLowerBody; } else if(mIdleState != CharState_None) { idleGroup = "idle"; if(!weapShortGroup.empty()) { idleGroup += weapShortGroup; if(!mAnimation->hasAnimation(idleGroup)) { idleGroup = fallbackShortWeaponGroup("idle"); } // play until the Loop Stop key 2 to 5 times, then play until the Stop key // this replicates original engine behavior for the "Idle1h" 1st-person animation numLoops = 1 + Misc::Rng::rollDice(4); } } // There is no need to restart anim if the new and old anims are the same. // Just update a number of loops. float startPoint = 0; if (!mCurrentIdle.empty() && mCurrentIdle == idleGroup) { mAnimation->getInfo(mCurrentIdle, &startPoint); } if(!mCurrentIdle.empty()) mAnimation->disable(mCurrentIdle); mCurrentIdle = idleGroup; if(!mCurrentIdle.empty()) mAnimation->play(mCurrentIdle, idlePriority, MWRender::Animation::BlendMask_All, false, 1.0f, "start", "stop", startPoint, numLoops, true); } } void CharacterController::refreshCurrentAnims(CharacterState idle, CharacterState movement, JumpingState jump, bool force) { // If the current animation is persistent, do not touch it if (isPersistentAnimPlaying()) return; if (mPtr.getClass().isActor()) refreshHitRecoilAnims(idle); std::string weap; if (mPtr.getClass().hasInventoryStore(mPtr)) weap = getWeaponType(mWeaponType)->mShortGroup; refreshJumpAnims(weap, jump, idle, force); refreshMovementAnims(weap, movement, idle, force); // idle handled last as it can depend on the other states refreshIdleAnims(weap, idle, force); } void CharacterController::playDeath(float startpoint, CharacterState death) { // Make sure the character was swimming upon death for forward-compatibility const bool wasSwimming = MWBase::Environment::get().getWorld()->isSwimming(mPtr); switch (death) { case CharState_SwimDeath: mCurrentDeath = "swimdeath"; break; case CharState_SwimDeathKnockDown: mCurrentDeath = (wasSwimming ? "swimdeathknockdown" : "deathknockdown"); break; case CharState_SwimDeathKnockOut: mCurrentDeath = (wasSwimming ? "swimdeathknockout" : "deathknockout"); break; case CharState_DeathKnockDown: mCurrentDeath = "deathknockdown"; break; case CharState_DeathKnockOut: mCurrentDeath = "deathknockout"; break; default: mCurrentDeath = "death" + std::to_string(death - CharState_Death1 + 1); } mDeathState = death; mPtr.getClass().getCreatureStats(mPtr).setDeathAnimation(mDeathState - CharState_Death1); // For dead actors, refreshCurrentAnims is no longer called, so we need to disable the movement state manually. // Note that these animations wouldn't actually be visible (due to the Death animation's priority being higher). // However, they could still trigger text keys, such as Hit events, or sounds. mMovementState = CharState_None; mAnimation->disable(mCurrentMovement); mCurrentMovement = ""; mUpperBodyState = UpperCharState_Nothing; mAnimation->disable(mCurrentWeapon); mCurrentWeapon = ""; mHitState = CharState_None; mAnimation->disable(mCurrentHit); mCurrentHit = ""; mIdleState = CharState_None; mAnimation->disable(mCurrentIdle); mCurrentIdle = ""; mJumpState = JumpState_None; mAnimation->disable(mCurrentJump); mCurrentJump = ""; mMovementAnimationControlled = true; mAnimation->play(mCurrentDeath, Priority_Death, MWRender::Animation::BlendMask_All, false, 1.0f, "start", "stop", startpoint, 0); } CharacterState CharacterController::chooseRandomDeathState() const { int selected=0; chooseRandomGroup("death", &selected); return static_cast(CharState_Death1 + (selected-1)); } void CharacterController::playRandomDeath(float startpoint) { /* Start of tes3mp addition If this is a LocalActor or DedicatedActor whose death animation is supposed to be finished, set the startpoint to the animation's end */ if (mPtr.getClass().getCreatureStats(mPtr).isDeathAnimationFinished() && (mwmp::Main::get().getCellController()->isLocalActor(mPtr) || mwmp::Main::get().getCellController()->isDedicatedActor(mPtr))) { startpoint = 1.F; } /* End of tes3mp addition */ if (mPtr == getPlayer()) { // The first-person animations do not include death, so we need to // force-switch to third person before playing the death animation. MWBase::Environment::get().getWorld()->useDeathCamera(); } /* Start tes3mp change (major) If this is a DedicatedPlayer, use the deathState received from their PlayerDeath packet If this is a DedicatedActor, use the deathState from their ActorDeath packet */ if (mwmp::PlayerList::isDedicatedPlayer(mPtr)) { mDeathState = static_cast(mwmp::PlayerList::getPlayer(mPtr)->deathState); } else if (mwmp::Main::get().getCellController()->hasQueuedDeathState(mPtr)) { mDeathState = static_cast(mwmp::Main::get().getCellController()->getQueuedDeathState(mPtr)); mwmp::Main::get().getCellController()->clearQueuedDeathState(mPtr); } else if(mHitState == CharState_SwimKnockDown && mAnimation->hasAnimation("swimdeathknockdown")) /* End of tes3mp change (major) */ { mDeathState = CharState_SwimDeathKnockDown; } else if(mHitState == CharState_SwimKnockOut && mAnimation->hasAnimation("swimdeathknockout")) { mDeathState = CharState_SwimDeathKnockOut; } else if(MWBase::Environment::get().getWorld()->isSwimming(mPtr) && mAnimation->hasAnimation("swimdeath")) { mDeathState = CharState_SwimDeath; } else if (mHitState == CharState_KnockDown && mAnimation->hasAnimation("deathknockdown")) { mDeathState = CharState_DeathKnockDown; } else if (mHitState == CharState_KnockOut && mAnimation->hasAnimation("deathknockout")) { mDeathState = CharState_DeathKnockOut; } else { mDeathState = chooseRandomDeathState(); } /* Start of tes3mp addition If this is the local player, send a PlayerDeath packet with the decided-upon death animation If this is a local actor, send an ActorDeath packet with the animation */ if (mPtr == getPlayer()) { mwmp::Main::get().getLocalPlayer()->sendDeath(mDeathState); } else if (!mPtr.getClass().getCreatureStats(mPtr).isDeathAnimationFinished() && mwmp::Main::get().getCellController()->isLocalActor(mPtr)) { mwmp::Main::get().getCellController()->getLocalActor(mPtr)->sendDeath(mDeathState); } /* End of tes3mp addition */ // Do not interrupt scripted animation by death if (isPersistentAnimPlaying()) return; playDeath(startpoint, mDeathState); } std::string CharacterController::chooseRandomAttackAnimation() const { std::string result; bool isSwimming = MWBase::Environment::get().getWorld()->isSwimming(mPtr); if (isSwimming) result = chooseRandomGroup("swimattack"); if (!isSwimming || !mAnimation->hasAnimation(result)) result = chooseRandomGroup("attack"); return result; } CharacterController::CharacterController(const MWWorld::Ptr &ptr, MWRender::Animation *anim) : mPtr(ptr) , mWeapon(MWWorld::Ptr()) , mAnimation(anim) , mIdleState(CharState_None) , mMovementState(CharState_None) , mMovementAnimSpeed(0.f) , mAdjustMovementAnimSpeed(false) , mHasMovedInXY(false) , mMovementAnimationControlled(true) , mDeathState(CharState_None) , mFloatToSurface(true) , mHitState(CharState_None) , mUpperBodyState(UpperCharState_Nothing) , mJumpState(JumpState_None) , mWeaponType(ESM::Weapon::None) , mAttackStrength(0.f) , mSkipAnim(false) , mSecondsOfSwimming(0) , mSecondsOfRunning(0) , mTurnAnimationThreshold(0) , mAttackingOrSpell(false) , mCastingManualSpell(false) , mTimeUntilWake(0.f) , mIsMovingBackward(false) { if(!mAnimation) return; mAnimation->setTextKeyListener(this); const MWWorld::Class &cls = mPtr.getClass(); if(cls.isActor()) { /* Accumulate along X/Y only for now, until we can figure out how we should * handle knockout and death which moves the character down. */ mAnimation->setAccumulation(osg::Vec3f(1.0f, 1.0f, 0.0f)); if (cls.hasInventoryStore(mPtr)) { getActiveWeapon(mPtr, &mWeaponType); if (mWeaponType != ESM::Weapon::None) { mUpperBodyState = UpperCharState_WeapEquiped; mCurrentWeapon = getWeaponAnimation(mWeaponType); } if(mWeaponType != ESM::Weapon::None && mWeaponType != ESM::Weapon::Spell && mWeaponType != ESM::Weapon::HandToHand) { mAnimation->showWeapons(true); // Note: controllers for ranged weapon should use time for beginning of animation to play shooting properly, // for other weapons they should use absolute time. Some mods rely on this behaviour (to rotate throwing projectiles, for example) ESM::WeaponType::Class weaponClass = getWeaponType(mWeaponType)->mWeaponClass; bool useRelativeDuration = weaponClass == ESM::WeaponType::Ranged; mAnimation->setWeaponGroup(mCurrentWeapon, useRelativeDuration); } mAnimation->showCarriedLeft(updateCarriedLeftVisible(mWeaponType)); } if(!cls.getCreatureStats(mPtr).isDead()) { mIdleState = CharState_Idle; if (cls.getCreatureStats(mPtr).getFallHeight() > 0) mJumpState = JumpState_InAir; } else { const MWMechanics::CreatureStats& cStats = mPtr.getClass().getCreatureStats(mPtr); if (cStats.isDeathAnimationFinished()) { // Set the death state, but don't play it yet // We will play it in the first frame, but only if no script set the skipAnim flag signed char deathanim = cStats.getDeathAnimation(); if (deathanim == -1) mDeathState = chooseRandomDeathState(); else mDeathState = static_cast(CharState_Death1 + deathanim); mFloatToSurface = false; } // else: nothing to do, will detect death in the next frame and start playing death animation } } else { /* Don't accumulate with non-actors. */ mAnimation->setAccumulation(osg::Vec3f(0.f, 0.f, 0.f)); mIdleState = CharState_Idle; } // Do not update animation status for dead actors if(mDeathState == CharState_None && (!cls.isActor() || !cls.getCreatureStats(mPtr).isDead())) refreshCurrentAnims(mIdleState, mMovementState, mJumpState, true); mAnimation->runAnimation(0.f); unpersistAnimationState(); } CharacterController::~CharacterController() { if (mAnimation) { persistAnimationState(); mAnimation->setTextKeyListener(nullptr); } } void split(const std::string &s, char delim, std::vector &elems) { std::stringstream ss(s); std::string item; while (std::getline(ss, item, delim)) { elems.push_back(item); } } void CharacterController::handleTextKey(const std::string &groupname, SceneUtil::TextKeyMap::ConstIterator key, const SceneUtil::TextKeyMap& map) { const std::string &evt = key->second; if(evt.compare(0, 7, "sound: ") == 0) { MWBase::SoundManager *sndMgr = MWBase::Environment::get().getSoundManager(); sndMgr->playSound3D(mPtr, evt.substr(7), 1.0f, 1.0f); return; } if(evt.compare(0, 10, "soundgen: ") == 0) { std::string soundgen = evt.substr(10); // The event can optionally contain volume and pitch modifiers float volume=1.f, pitch=1.f; if (soundgen.find(' ') != std::string::npos) { std::vector tokens; split(soundgen, ' ', tokens); soundgen = tokens[0]; if (tokens.size() >= 2) { std::stringstream stream; stream << tokens[1]; stream >> volume; } if (tokens.size() >= 3) { std::stringstream stream; stream << tokens[2]; stream >> pitch; } } std::string sound = mPtr.getClass().getSoundIdFromSndGen(mPtr, soundgen); if(!sound.empty()) { MWBase::SoundManager *sndMgr = MWBase::Environment::get().getSoundManager(); // NB: landing sound is not played for NPCs here if(soundgen == "left" || soundgen == "right" || soundgen == "land") { sndMgr->playSound3D(mPtr, sound, volume, pitch, MWSound::Type::Foot, MWSound::PlayMode::NoPlayerLocal); } else { sndMgr->playSound3D(mPtr, sound, volume, pitch); } } return; } if(evt.compare(0, groupname.size(), groupname) != 0 || evt.compare(groupname.size(), 2, ": ") != 0) { // Not ours, skip it return; } size_t off = groupname.size()+2; size_t len = evt.size() - off; if(groupname == "shield" && evt.compare(off, len, "equip attach") == 0) mAnimation->showCarriedLeft(true); else if(groupname == "shield" && evt.compare(off, len, "unequip detach") == 0) mAnimation->showCarriedLeft(false); else if(evt.compare(off, len, "equip attach") == 0) mAnimation->showWeapons(true); else if(evt.compare(off, len, "unequip detach") == 0) mAnimation->showWeapons(false); else if(evt.compare(off, len, "chop hit") == 0) mPtr.getClass().hit(mPtr, mAttackStrength, ESM::Weapon::AT_Chop); else if(evt.compare(off, len, "slash hit") == 0) mPtr.getClass().hit(mPtr, mAttackStrength, ESM::Weapon::AT_Slash); else if(evt.compare(off, len, "thrust hit") == 0) mPtr.getClass().hit(mPtr, mAttackStrength, ESM::Weapon::AT_Thrust); else if(evt.compare(off, len, "hit") == 0) { if (groupname == "attack1" || groupname == "swimattack1") mPtr.getClass().hit(mPtr, mAttackStrength, ESM::Weapon::AT_Chop); else if (groupname == "attack2" || groupname == "swimattack2") mPtr.getClass().hit(mPtr, mAttackStrength, ESM::Weapon::AT_Slash); else if (groupname == "attack3" || groupname == "swimattack3") mPtr.getClass().hit(mPtr, mAttackStrength, ESM::Weapon::AT_Thrust); else mPtr.getClass().hit(mPtr, mAttackStrength); } else if (!groupname.empty() && (groupname.compare(0, groupname.size()-1, "attack") == 0 || groupname.compare(0, groupname.size()-1, "swimattack") == 0) && evt.compare(off, len, "start") == 0) { std::multimap::const_iterator hitKey = key; // Not all animations have a hit key defined. If there is none, the hit happens with the start key. bool hasHitKey = false; while (hitKey != map.end()) { if (hitKey->second == groupname + ": hit") { hasHitKey = true; break; } if (hitKey->second == groupname + ": stop") break; ++hitKey; } if (!hasHitKey) { if (groupname == "attack1" || groupname == "swimattack1") mPtr.getClass().hit(mPtr, mAttackStrength, ESM::Weapon::AT_Chop); else if (groupname == "attack2" || groupname == "swimattack2") mPtr.getClass().hit(mPtr, mAttackStrength, ESM::Weapon::AT_Slash); else if (groupname == "attack3" || groupname == "swimattack3") mPtr.getClass().hit(mPtr, mAttackStrength, ESM::Weapon::AT_Thrust); } } else if (evt.compare(off, len, "shoot attach") == 0) mAnimation->attachArrow(); else if (evt.compare(off, len, "shoot release") == 0) mAnimation->releaseArrow(mAttackStrength); else if (evt.compare(off, len, "shoot follow attach") == 0) mAnimation->attachArrow(); else if (groupname == "spellcast" && evt.substr(evt.size()-7, 7) == "release" // Make sure this key is actually for the RangeType we are casting. The flame atronach has // the same animation for all range types, so there are 3 "release" keys on the same time, one for each range type. && evt.compare(off, len, mAttackType + " release") == 0) { /* Start of tes3mp change (major) Make the completion of the spellcast animation actually cast spells only for the local player and local actors, relying on Cast packets to cause spells to be cast for dedicated players and actors */ if (mPtr == getPlayer() || mwmp::Main::get().getCellController()->isLocalActor(mPtr)) { MWBase::Environment::get().getWorld()->castSpell(mPtr, mCastingManualSpell); mCastingManualSpell = false; } /* End of tes3mp change (major) */ } else if (groupname == "shield" && evt.compare(off, len, "block hit") == 0) mPtr.getClass().block(mPtr); else if (groupname == "containeropen" && evt.compare(off, len, "loot") == 0) MWBase::Environment::get().getWindowManager()->pushGuiMode(MWGui::GM_Container, mPtr); } void CharacterController::updatePtr(const MWWorld::Ptr &ptr) { mPtr = ptr; } void CharacterController::updateIdleStormState(bool inwater) { if (!mAnimation->hasAnimation("idlestorm") || mUpperBodyState != UpperCharState_Nothing || inwater) { mAnimation->disable("idlestorm"); return; } if (MWBase::Environment::get().getWorld()->isInStorm()) { osg::Vec3f stormDirection = MWBase::Environment::get().getWorld()->getStormDirection(); osg::Vec3f characterDirection = mPtr.getRefData().getBaseNode()->getAttitude() * osg::Vec3f(0,1,0); stormDirection.normalize(); characterDirection.normalize(); if (stormDirection * characterDirection < -0.5f) { if (!mAnimation->isPlaying("idlestorm")) { int mask = MWRender::Animation::BlendMask_Torso | MWRender::Animation::BlendMask_RightArm; mAnimation->play("idlestorm", Priority_Storm, mask, true, 1.0f, "start", "stop", 0.0f, ~0ul); } else { mAnimation->setLoopingEnabled("idlestorm", true); } return; } } if (mAnimation->isPlaying("idlestorm")) { mAnimation->setLoopingEnabled("idlestorm", false); } } bool CharacterController::updateCreatureState() { const MWWorld::Class &cls = mPtr.getClass(); CreatureStats &stats = cls.getCreatureStats(mPtr); int weapType = ESM::Weapon::None; if(stats.getDrawState() == DrawState_Weapon) weapType = ESM::Weapon::HandToHand; else if (stats.getDrawState() == DrawState_Spell) weapType = ESM::Weapon::Spell; if (weapType != mWeaponType) { mWeaponType = weapType; if (mAnimation->isPlaying(mCurrentWeapon)) mAnimation->disable(mCurrentWeapon); } if(mAttackingOrSpell) { if(mUpperBodyState == UpperCharState_Nothing && mHitState == CharState_None) { MWBase::Environment::get().getWorld()->breakInvisibility(mPtr); std::string startKey = "start"; std::string stopKey = "stop"; if (weapType == ESM::Weapon::Spell) { const std::string spellid = stats.getSpells().getSelectedSpell(); bool canCast = mCastingManualSpell || MWBase::Environment::get().getWorld()->startSpellCast(mPtr); if (!spellid.empty() && canCast) { /* Start of tes3mp addition If this mPtr belongs to a LocalPlayer or LocalActor, get their Attack and prepare it for sending */ mwmp::Cast *localCast = MechanicsHelper::getLocalCast(mPtr); if (localCast) { MechanicsHelper::resetCast(localCast); localCast->type = mwmp::Cast::REGULAR; localCast->spellId = spellid; localCast->pressed = true; localCast->shouldSend = true; // Mark the attack as instant if there is no spellcast animation if (!mAnimation->hasAnimation("spellcast")) localCast->instant = true; } /* End of tes3mp addition */ MWMechanics::CastSpell cast(mPtr, nullptr, false, mCastingManualSpell); cast.playSpellCastingEffects(spellid, false); if (!mAnimation->hasAnimation("spellcast")) { MWBase::Environment::get().getWorld()->castSpell(mPtr, mCastingManualSpell); // No "release" text key to use, so cast immediately mCastingManualSpell = false; } else { const ESM::Spell *spell = MWBase::Environment::get().getWorld()->getStore().get().find(spellid); const ESM::ENAMstruct &effectentry = spell->mEffects.mList.at(0); switch(effectentry.mRange) { case 0: mAttackType = "self"; break; case 1: mAttackType = "touch"; break; case 2: mAttackType = "target"; break; } startKey = mAttackType + " " + startKey; stopKey = mAttackType + " " + stopKey; mCurrentWeapon = "spellcast"; } } else mCurrentWeapon = ""; } if (weapType != ESM::Weapon::Spell || !mAnimation->hasAnimation("spellcast")) // Not all creatures have a dedicated spellcast animation { mCurrentWeapon = chooseRandomAttackAnimation(); } if (!mCurrentWeapon.empty()) { mAnimation->play(mCurrentWeapon, Priority_Weapon, MWRender::Animation::BlendMask_All, true, 1, startKey, stopKey, 0.0f, 0); mUpperBodyState = UpperCharState_StartToMinAttack; mAttackStrength = std::min(1.f, 0.1f + Misc::Rng::rollClosedProbability()); if (weapType == ESM::Weapon::HandToHand) playSwishSound(0.0f); } } mAttackingOrSpell = false; } bool animPlaying = mAnimation->getInfo(mCurrentWeapon); if (!animPlaying) mUpperBodyState = UpperCharState_Nothing; return false; } bool CharacterController::updateCarriedLeftVisible(const int weaptype) const { // Shields/torches shouldn't be visible during any operation involving two hands // There seems to be no text keys for this purpose, except maybe for "[un]equip start/stop", // but they are also present in weapon drawing animation. return mAnimation->updateCarriedLeftVisible(weaptype); } bool CharacterController::updateWeaponState(CharacterState& idle) { const MWWorld::Class &cls = mPtr.getClass(); CreatureStats &stats = cls.getCreatureStats(mPtr); int weaptype = ESM::Weapon::None; if(stats.getDrawState() == DrawState_Weapon) weaptype = ESM::Weapon::HandToHand; else if (stats.getDrawState() == DrawState_Spell) weaptype = ESM::Weapon::Spell; const bool isWerewolf = cls.isNpc() && cls.getNpcStats(mPtr).isWerewolf(); std::string upSoundId; std::string downSoundId; bool weaponChanged = false; if (mPtr.getClass().hasInventoryStore(mPtr)) { MWWorld::InventoryStore &inv = cls.getInventoryStore(mPtr); MWWorld::ContainerStoreIterator weapon = getActiveWeapon(mPtr, &weaptype); if(stats.getDrawState() == DrawState_Spell) weapon = inv.getSlot(MWWorld::InventoryStore::Slot_CarriedRight); if(weapon != inv.end() && mWeaponType != ESM::Weapon::HandToHand && weaptype != ESM::Weapon::HandToHand && weaptype != ESM::Weapon::Spell && weaptype != ESM::Weapon::None) upSoundId = weapon->getClass().getUpSoundId(*weapon); if(weapon != inv.end() && mWeaponType != ESM::Weapon::HandToHand && mWeaponType != ESM::Weapon::Spell && mWeaponType != ESM::Weapon::None) downSoundId = weapon->getClass().getDownSoundId(*weapon); // weapon->HtH switch: weapon is empty already, so we need to take sound from previous weapon if(weapon == inv.end() && !mWeapon.isEmpty() && weaptype == ESM::Weapon::HandToHand && mWeaponType != ESM::Weapon::Spell) downSoundId = mWeapon.getClass().getDownSoundId(mWeapon); MWWorld::Ptr newWeapon = weapon != inv.end() ? *weapon : MWWorld::Ptr(); if (mWeapon != newWeapon) { mWeapon = newWeapon; weaponChanged = true; } } // For biped actors, blend weapon animations with lower body animations with higher priority MWRender::Animation::AnimPriority priorityWeapon(Priority_Weapon); if (mPtr.getClass().isBipedal(mPtr)) priorityWeapon[MWRender::Animation::BoneGroup_LowerBody] = Priority_WeaponLowerBody; bool forcestateupdate = false; // We should not play equipping animation and sound during weapon->weapon transition bool isStillWeapon = weaptype != ESM::Weapon::HandToHand && weaptype != ESM::Weapon::Spell && weaptype != ESM::Weapon::None && mWeaponType != ESM::Weapon::HandToHand && mWeaponType != ESM::Weapon::Spell && mWeaponType != ESM::Weapon::None; // If the current weapon type was changed in the middle of attack (e.g. by Equip console command or when bound spell expires), // we should force actor to the "weapon equipped" state, interrupt attack and update animations. if (isStillWeapon && mWeaponType != weaptype && mUpperBodyState > UpperCharState_WeapEquiped) { forcestateupdate = true; mUpperBodyState = UpperCharState_WeapEquiped; mAttackingOrSpell = false; mAnimation->disable(mCurrentWeapon); mAnimation->showWeapons(true); if (mPtr == getPlayer()) MWBase::Environment::get().getWorld()->getPlayer().setAttackingOrSpell(false); } if(!isKnockedOut() && !isKnockedDown() && !isRecovery()) { std::string weapgroup; if ((!isWerewolf || mWeaponType != ESM::Weapon::Spell) && weaptype != mWeaponType && mUpperBodyState != UpperCharState_UnEquipingWeap && !isStillWeapon) { // We can not play un-equip animation if weapon changed since last update if (!weaponChanged) { // Note: we do not disable unequipping animation automatically to avoid body desync weapgroup = getWeaponAnimation(mWeaponType); int unequipMask = MWRender::Animation::BlendMask_All; bool useShieldAnims = mAnimation->useShieldAnimations(); if (useShieldAnims && mWeaponType != ESM::Weapon::HandToHand && mWeaponType != ESM::Weapon::Spell && !(mWeaponType == ESM::Weapon::None && weaptype == ESM::Weapon::Spell)) { unequipMask = unequipMask |~MWRender::Animation::BlendMask_LeftArm; mAnimation->play("shield", Priority_Block, MWRender::Animation::BlendMask_LeftArm, true, 1.0f, "unequip start", "unequip stop", 0.0f, 0); } else if (mWeaponType == ESM::Weapon::HandToHand) mAnimation->showCarriedLeft(false); mAnimation->play(weapgroup, priorityWeapon, unequipMask, false, 1.0f, "unequip start", "unequip stop", 0.0f, 0); mUpperBodyState = UpperCharState_UnEquipingWeap; mAnimation->detachArrow(); // If we do not have the "unequip detach" key, hide weapon manually. if (mAnimation->getTextKeyTime(weapgroup+": unequip detach") < 0) mAnimation->showWeapons(false); } if(!downSoundId.empty()) { MWBase::SoundManager *sndMgr = MWBase::Environment::get().getSoundManager(); sndMgr->playSound3D(mPtr, downSoundId, 1.0f, 1.0f); } } float complete; bool animPlaying = mAnimation->getInfo(mCurrentWeapon, &complete); if (!animPlaying || complete >= 1.0f) { // Weapon is changed, no current animation (e.g. unequipping or attack). // Start equipping animation now. if (weaptype != mWeaponType) { forcestateupdate = true; bool useShieldAnims = mAnimation->useShieldAnimations(); if (!useShieldAnims) mAnimation->showCarriedLeft(updateCarriedLeftVisible(weaptype)); weapgroup = getWeaponAnimation(weaptype); // Note: controllers for ranged weapon should use time for beginning of animation to play shooting properly, // for other weapons they should use absolute time. Some mods rely on this behaviour (to rotate throwing projectiles, for example) ESM::WeaponType::Class weaponClass = getWeaponType(weaptype)->mWeaponClass; bool useRelativeDuration = weaponClass == ESM::WeaponType::Ranged; mAnimation->setWeaponGroup(weapgroup, useRelativeDuration); if (!isStillWeapon) { mAnimation->disable(mCurrentWeapon); if (weaptype != ESM::Weapon::None) { mAnimation->showWeapons(false); int equipMask = MWRender::Animation::BlendMask_All; if (useShieldAnims && weaptype != ESM::Weapon::Spell) { equipMask = equipMask |~MWRender::Animation::BlendMask_LeftArm; mAnimation->play("shield", Priority_Block, MWRender::Animation::BlendMask_LeftArm, true, 1.0f, "equip start", "equip stop", 0.0f, 0); } mAnimation->play(weapgroup, priorityWeapon, equipMask, true, 1.0f, "equip start", "equip stop", 0.0f, 0); mUpperBodyState = UpperCharState_EquipingWeap; // If we do not have the "equip attach" key, show weapon manually. if (weaptype != ESM::Weapon::Spell) { if (mAnimation->getTextKeyTime(weapgroup+": equip attach") < 0) mAnimation->showWeapons(true); } } } if(isWerewolf) { const MWWorld::ESMStore &store = MWBase::Environment::get().getWorld()->getStore(); const ESM::Sound *sound = store.get().searchRandom("WolfEquip"); if(sound) { MWBase::SoundManager *sndMgr = MWBase::Environment::get().getSoundManager(); sndMgr->playSound3D(mPtr, sound->mId, 1.0f, 1.0f); } } mWeaponType = weaptype; mCurrentWeapon = getWeaponAnimation(mWeaponType); if(!upSoundId.empty() && !isStillWeapon) { MWBase::SoundManager *sndMgr = MWBase::Environment::get().getSoundManager(); sndMgr->playSound3D(mPtr, upSoundId, 1.0f, 1.0f); } } // Make sure that we disabled unequipping animation if (mUpperBodyState == UpperCharState_UnEquipingWeap) { mUpperBodyState = UpperCharState_Nothing; mAnimation->disable(mCurrentWeapon); mWeaponType = ESM::Weapon::None; mCurrentWeapon = getWeaponAnimation(mWeaponType); } } } if(isWerewolf) { MWBase::SoundManager *sndMgr = MWBase::Environment::get().getSoundManager(); if(cls.getCreatureStats(mPtr).getStance(MWMechanics::CreatureStats::Stance_Run) && mHasMovedInXY && !MWBase::Environment::get().getWorld()->isSwimming(mPtr) && mWeaponType == ESM::Weapon::None) { if(!sndMgr->getSoundPlaying(mPtr, "WolfRun")) sndMgr->playSound3D(mPtr, "WolfRun", 1.0f, 1.0f, MWSound::Type::Sfx, MWSound::PlayMode::Loop); } else sndMgr->stopSound3D(mPtr, "WolfRun"); } // Cancel attack if we no longer have ammunition bool ammunition = true; bool isWeapon = false; float weapSpeed = 1.f; if (mPtr.getClass().hasInventoryStore(mPtr)) { MWWorld::InventoryStore &inv = cls.getInventoryStore(mPtr); MWWorld::ConstContainerStoreIterator weapon = getActiveWeapon(mPtr, &weaptype); isWeapon = (weapon != inv.end() && weapon->getTypeName() == typeid(ESM::Weapon).name()); if (isWeapon) { weapSpeed = weapon->get()->mBase->mData.mSpeed; MWWorld::ConstContainerStoreIterator ammo = inv.getSlot(MWWorld::InventoryStore::Slot_Ammunition); int ammotype = getWeaponType(weapon->get()->mBase->mData.mType)->mAmmoType; if (ammotype != ESM::Weapon::None && (ammo == inv.end() || ammo->get()->mBase->mData.mType != ammotype)) ammunition = false; } if (!ammunition && mUpperBodyState > UpperCharState_WeapEquiped) { mAnimation->disable(mCurrentWeapon); mUpperBodyState = UpperCharState_WeapEquiped; } } // Combat for actors with persistent animations obviously will be buggy if (isPersistentAnimPlaying()) return forcestateupdate; float complete; bool animPlaying; ESM::WeaponType::Class weapclass = getWeaponType(mWeaponType)->mWeaponClass; if(mAttackingOrSpell) { MWWorld::Ptr player = getPlayer(); bool resetIdle = ammunition; if(mUpperBodyState == UpperCharState_WeapEquiped && (mHitState == CharState_None || mHitState == CharState_Block)) { MWBase::Environment::get().getWorld()->breakInvisibility(mPtr); mAttackStrength = 0; // Randomize attacks for non-bipedal creatures with Weapon flag if (mPtr.getClass().getTypeName() == typeid(ESM::Creature).name() && !mPtr.getClass().isBipedal(mPtr) && (!mAnimation->hasAnimation(mCurrentWeapon) || isRandomAttackAnimation(mCurrentWeapon))) { mCurrentWeapon = chooseRandomAttackAnimation(); } if(mWeaponType == ESM::Weapon::Spell) { // Unset casting flag, otherwise pressing the mouse button down would // continue casting every frame if there is no animation mAttackingOrSpell = false; if (mPtr == player) { MWBase::Environment::get().getWorld()->getPlayer().setAttackingOrSpell(false); // For the player, set the spell we want to cast // This has to be done at the start of the casting animation, // *not* when selecting a spell in the GUI (otherwise you could change the spell mid-animation) std::string selectedSpell = MWBase::Environment::get().getWindowManager()->getSelectedSpell(); stats.getSpells().setSelectedSpell(selectedSpell); } std::string spellid = stats.getSpells().getSelectedSpell(); bool isMagicItem = false; bool canCast = mCastingManualSpell || MWBase::Environment::get().getWorld()->startSpellCast(mPtr); if (spellid.empty()) { if (mPtr.getClass().hasInventoryStore(mPtr)) { MWWorld::InventoryStore& inv = mPtr.getClass().getInventoryStore(mPtr); if (inv.getSelectedEnchantItem() != inv.end()) { const MWWorld::Ptr& enchantItem = *inv.getSelectedEnchantItem(); spellid = enchantItem.getClass().getEnchantment(enchantItem); isMagicItem = true; } } } static const bool useCastingAnimations = Settings::Manager::getBool("use magic item animations", "Game"); if (isMagicItem && !useCastingAnimations) { // Enchanted items by default do not use casting animations MWBase::Environment::get().getWorld()->castSpell(mPtr); resetIdle = false; } else if(!spellid.empty() && canCast) { /* Start of tes3mp addition If this mPtr belongs to a LocalPlayer or LocalActor, get their Cast and prepare it for sending */ mwmp::Cast *localCast = MechanicsHelper::getLocalCast(mPtr); if (localCast) { MechanicsHelper::resetCast(localCast); localCast->type = mwmp::Cast::REGULAR; localCast->spellId = spellid; localCast->pressed = true; localCast->shouldSend = true; } /* End of tes3mp addition */ MWMechanics::CastSpell cast(mPtr, nullptr, false, mCastingManualSpell); cast.playSpellCastingEffects(spellid, isMagicItem); std::vector effects; const MWWorld::ESMStore &store = MWBase::Environment::get().getWorld()->getStore(); if (isMagicItem) { const ESM::Enchantment *enchantment = store.get().find(spellid); effects = enchantment->mEffects.mList; } else { const ESM::Spell *spell = store.get().find(spellid); effects = spell->mEffects.mList; } const ESM::MagicEffect *effect = store.get().find(effects.back().mEffectID); // use last effect of list for color of VFX_Hands const ESM::Static* castStatic = MWBase::Environment::get().getWorld()->getStore().get().find ("VFX_Hands"); for (size_t iter = 0; iter < effects.size(); ++iter) // play hands vfx for each effect { if (mAnimation->getNode("Bip01 L Hand")) mAnimation->addEffect("meshes\\" + castStatic->mModel, -1, false, "Bip01 L Hand", effect->mParticle); if (mAnimation->getNode("Bip01 R Hand")) mAnimation->addEffect("meshes\\" + castStatic->mModel, -1, false, "Bip01 R Hand", effect->mParticle); } const ESM::ENAMstruct &firstEffect = effects.at(0); // first effect used for casting animation std::string startKey; std::string stopKey; if (isRandomAttackAnimation(mCurrentWeapon)) { startKey = "start"; stopKey = "stop"; MWBase::Environment::get().getWorld()->castSpell(mPtr, mCastingManualSpell); // No "release" text key to use, so cast immediately mCastingManualSpell = false; } else { switch(firstEffect.mRange) { case 0: mAttackType = "self"; break; case 1: mAttackType = "touch"; break; case 2: mAttackType = "target"; break; } startKey = mAttackType+" start"; stopKey = mAttackType+" stop"; } mAnimation->play(mCurrentWeapon, priorityWeapon, MWRender::Animation::BlendMask_All, true, 1, startKey, stopKey, 0.0f, 0); mUpperBodyState = UpperCharState_CastingSpell; } else { resetIdle = false; } } else if(mWeaponType == ESM::Weapon::PickProbe) { MWWorld::ContainerStoreIterator weapon = mPtr.getClass().getInventoryStore(mPtr).getSlot(MWWorld::InventoryStore::Slot_CarriedRight); MWWorld::Ptr item = *weapon; // TODO: this will only work for the player, and needs to be fixed if NPCs should ever use lockpicks/probes. MWWorld::Ptr target = MWBase::Environment::get().getWorld()->getFacedObject(); std::string resultMessage, resultSound; if(!target.isEmpty()) { if(item.getTypeName() == typeid(ESM::Lockpick).name()) Security(mPtr).pickLock(target, item, resultMessage, resultSound); else if(item.getTypeName() == typeid(ESM::Probe).name()) Security(mPtr).probeTrap(target, item, resultMessage, resultSound); } mAnimation->play(mCurrentWeapon, priorityWeapon, MWRender::Animation::BlendMask_All, true, 1.0f, "start", "stop", 0.0, 0); mUpperBodyState = UpperCharState_FollowStartToFollowStop; if(!resultMessage.empty()) MWBase::Environment::get().getWindowManager()->messageBox(resultMessage); if(!resultSound.empty()) MWBase::Environment::get().getSoundManager()->playSound3D(target, resultSound, 1.0f, 1.0f); } else if (ammunition) { std::string startKey; std::string stopKey; if(weapclass == ESM::WeaponType::Ranged || weapclass == ESM::WeaponType::Thrown) { mAttackType = "shoot"; startKey = mAttackType+" start"; stopKey = mAttackType+" min attack"; } else if (isRandomAttackAnimation(mCurrentWeapon)) { startKey = "start"; stopKey = "stop"; } else { if(mPtr == getPlayer()) { if (Settings::Manager::getBool("best attack", "Game")) { if (isWeapon) { MWWorld::ConstContainerStoreIterator weapon = mPtr.getClass().getInventoryStore(mPtr).getSlot(MWWorld::InventoryStore::Slot_CarriedRight); mAttackType = getBestAttack(weapon->get()->mBase); } else { // There is no "best attack" for Hand-to-Hand setAttackTypeRandomly(mAttackType); } } else { setAttackTypeBasedOnMovement(); } /* Start of tes3mp addition Record the attack animation chosen so we can send it in the next PlayerAttack packet */ mwmp::Attack *localAttack = MechanicsHelper::getLocalAttack(mPtr); if (localAttack) localAttack->attackAnimation = mAttackType; /* End of tes3mp addition */ } /* Start of tes3mp addition If this is a DedicatedPlayer or DedicatedActor, use the attack animation received in the latest Attack packet about them */ else { mwmp::Attack *dedicatedAttack = MechanicsHelper::getDedicatedAttack(mPtr); if (dedicatedAttack) mAttackType = dedicatedAttack->attackAnimation; } /* End of tes3mp addition */ // else if (mPtr != getPlayer()) use mAttackType set by AiCombat startKey = mAttackType+" start"; stopKey = mAttackType+" min attack"; } mAnimation->play(mCurrentWeapon, priorityWeapon, MWRender::Animation::BlendMask_All, false, weapSpeed, startKey, stopKey, 0.0f, 0); mUpperBodyState = UpperCharState_StartToMinAttack; } } // We should not break swim and sneak animations if (resetIdle && idle != CharState_IdleSneak && idle != CharState_IdleSwim && mIdleState != CharState_IdleSneak && mIdleState != CharState_IdleSwim) { mAnimation->disable(mCurrentIdle); mIdleState = CharState_None; } animPlaying = mAnimation->getInfo(mCurrentWeapon, &complete); if(mUpperBodyState == UpperCharState_MinAttackToMaxAttack && !isKnockedDown()) mAttackStrength = complete; } else { animPlaying = mAnimation->getInfo(mCurrentWeapon, &complete); if(mUpperBodyState == UpperCharState_MinAttackToMaxAttack && !isKnockedDown()) { float attackStrength = complete; float minAttackTime = mAnimation->getTextKeyTime(mCurrentWeapon+": "+mAttackType+" "+"min attack"); float maxAttackTime = mAnimation->getTextKeyTime(mCurrentWeapon+": "+mAttackType+" "+"max attack"); if (minAttackTime == maxAttackTime) { // most creatures don't actually have an attack wind-up animation, so use a uniform random value // (even some creatures that can use weapons don't have a wind-up animation either, e.g. Rieklings) // Note: vanilla MW uses a random value for *all* non-player actors, but we probably don't need to go that far. attackStrength = std::min(1.f, 0.1f + Misc::Rng::rollClosedProbability()); } if(weapclass != ESM::WeaponType::Ranged && weapclass != ESM::WeaponType::Thrown) { MWBase::SoundManager *sndMgr = MWBase::Environment::get().getSoundManager(); if(isWerewolf) { const MWWorld::ESMStore &store = MWBase::Environment::get().getWorld()->getStore(); const ESM::Sound *sound = store.get().searchRandom("WolfSwing"); if(sound) sndMgr->playSound3D(mPtr, sound->mId, 1.0f, 1.0f); } else { playSwishSound(attackStrength); } } mAttackStrength = attackStrength; mAnimation->disable(mCurrentWeapon); mAnimation->play(mCurrentWeapon, priorityWeapon, MWRender::Animation::BlendMask_All, false, weapSpeed, mAttackType+" max attack", mAttackType+" min hit", 1.0f-complete, 0); complete = 0.f; mUpperBodyState = UpperCharState_MaxAttackToMinHit; } else if (isKnockedDown()) { if (mUpperBodyState > UpperCharState_WeapEquiped) { mUpperBodyState = UpperCharState_WeapEquiped; if (mWeaponType > ESM::Weapon::None) mAnimation->showWeapons(true); } mAnimation->disable(mCurrentWeapon); } } mAnimation->setPitchFactor(0.f); if (weapclass == ESM::WeaponType::Ranged || weapclass == ESM::WeaponType::Thrown) { switch (mUpperBodyState) { case UpperCharState_StartToMinAttack: mAnimation->setPitchFactor(complete); break; case UpperCharState_MinAttackToMaxAttack: case UpperCharState_MaxAttackToMinHit: case UpperCharState_MinHitToHit: mAnimation->setPitchFactor(1.f); break; case UpperCharState_FollowStartToFollowStop: if (animPlaying) { // technically we do not need a pitch for crossbow reload animation, // but we should avoid abrupt repositioning if (mWeaponType == ESM::Weapon::MarksmanCrossbow) mAnimation->setPitchFactor(std::max(0.f, 1.f-complete*10.f)); else mAnimation->setPitchFactor(1.f-complete); } break; default: break; } } if(!animPlaying) { if(mUpperBodyState == UpperCharState_EquipingWeap || mUpperBodyState == UpperCharState_FollowStartToFollowStop || mUpperBodyState == UpperCharState_CastingSpell) { if (ammunition && mWeaponType == ESM::Weapon::MarksmanCrossbow) mAnimation->attachArrow(); mUpperBodyState = UpperCharState_WeapEquiped; } else if(mUpperBodyState == UpperCharState_UnEquipingWeap) mUpperBodyState = UpperCharState_Nothing; } else if(complete >= 1.0f && !isRandomAttackAnimation(mCurrentWeapon)) { std::string start, stop; switch(mUpperBodyState) { case UpperCharState_MinAttackToMaxAttack: //hack to avoid body pos desync when jumping/sneaking in 'max attack' state if(!mAnimation->isPlaying(mCurrentWeapon)) mAnimation->play(mCurrentWeapon, priorityWeapon, MWRender::Animation::BlendMask_All, false, 0, mAttackType+" min attack", mAttackType+" max attack", 0.999f, 0); break; case UpperCharState_StartToMinAttack: case UpperCharState_MaxAttackToMinHit: { if (mUpperBodyState == UpperCharState_StartToMinAttack) { // If actor is already stopped preparing attack, do not play the "min attack -> max attack" part. // Happens if the player did not hold the attack button. // Note: if the "min attack"->"max attack" is a stub, "play" it anyway. Attack strength will be random. float minAttackTime = mAnimation->getTextKeyTime(mCurrentWeapon+": "+mAttackType+" "+"min attack"); float maxAttackTime = mAnimation->getTextKeyTime(mCurrentWeapon+": "+mAttackType+" "+"max attack"); if (mAttackingOrSpell || minAttackTime == maxAttackTime) { start = mAttackType+" min attack"; stop = mAttackType+" max attack"; mUpperBodyState = UpperCharState_MinAttackToMaxAttack; break; } if(weapclass != ESM::WeaponType::Ranged && weapclass != ESM::WeaponType::Thrown) playSwishSound(0.0f); } if(mAttackType == "shoot") { start = mAttackType+" min hit"; stop = mAttackType+" release"; } else { start = mAttackType+" min hit"; stop = mAttackType+" hit"; } mUpperBodyState = UpperCharState_MinHitToHit; break; } case UpperCharState_MinHitToHit: if(mAttackType == "shoot") { start = mAttackType+" follow start"; stop = mAttackType+" follow stop"; } else { float str = mAttackStrength; start = mAttackType+((str < 0.5f) ? " small follow start" : (str < 1.0f) ? " medium follow start" : " large follow start"); stop = mAttackType+((str < 0.5f) ? " small follow stop" : (str < 1.0f) ? " medium follow stop" : " large follow stop"); } mUpperBodyState = UpperCharState_FollowStartToFollowStop; break; default: break; } // Note: apply crossbow reload animation only for upper body // since blending with movement animations can give weird result. if(!start.empty()) { int mask = MWRender::Animation::BlendMask_All; if (mWeaponType == ESM::Weapon::MarksmanCrossbow) mask = MWRender::Animation::BlendMask_UpperBody; mAnimation->disable(mCurrentWeapon); if (mUpperBodyState == UpperCharState_FollowStartToFollowStop) mAnimation->play(mCurrentWeapon, priorityWeapon, mask, true, weapSpeed, start, stop, 0.0f, 0); else mAnimation->play(mCurrentWeapon, priorityWeapon, mask, false, weapSpeed, start, stop, 0.0f, 0); } } else if(complete >= 1.0f && isRandomAttackAnimation(mCurrentWeapon)) { mAnimation->disable(mCurrentWeapon); mUpperBodyState = UpperCharState_WeapEquiped; } if (mPtr.getClass().hasInventoryStore(mPtr)) { const MWWorld::InventoryStore& inv = mPtr.getClass().getInventoryStore(mPtr); MWWorld::ConstContainerStoreIterator torch = inv.getSlot(MWWorld::InventoryStore::Slot_CarriedLeft); if(torch != inv.end() && torch->getTypeName() == typeid(ESM::Light).name() && updateCarriedLeftVisible(mWeaponType)) { if (mAnimation->isPlaying("shield")) mAnimation->disable("shield"); mAnimation->play("torch", Priority_Torch, MWRender::Animation::BlendMask_LeftArm, false, 1.0f, "start", "stop", 0.0f, (~(size_t)0), true); } else if (mAnimation->isPlaying("torch")) { mAnimation->disable("torch"); } } mAnimation->setAccurateAiming(mUpperBodyState > UpperCharState_WeapEquiped); return forcestateupdate; } void CharacterController::updateAnimQueue() { if(mAnimQueue.size() > 1) { if(mAnimation->isPlaying(mAnimQueue.front().mGroup) == false) { mAnimation->disable(mAnimQueue.front().mGroup); mAnimQueue.pop_front(); bool loopfallback = (mAnimQueue.front().mGroup.compare(0,4,"idle") == 0); mAnimation->play(mAnimQueue.front().mGroup, Priority_Default, MWRender::Animation::BlendMask_All, false, 1.0f, "start", "stop", 0.0f, mAnimQueue.front().mLoopCount, loopfallback); } } if(!mAnimQueue.empty()) mAnimation->setLoopingEnabled(mAnimQueue.front().mGroup, mAnimQueue.size() <= 1); } void CharacterController::update(float duration) { MWBase::World *world = MWBase::Environment::get().getWorld(); const MWWorld::Class &cls = mPtr.getClass(); osg::Vec3f movement(0.f, 0.f, 0.f); float speed = 0.f; updateMagicEffects(); if (isKnockedOut()) mTimeUntilWake -= duration; bool isPlayer = mPtr == MWMechanics::getPlayer(); bool isFirstPersonPlayer = isPlayer && MWBase::Environment::get().getWorld()->isFirstPerson(); bool godmode = isPlayer && MWBase::Environment::get().getWorld()->getGodModeState(); float scale = mPtr.getCellRef().getScale(); static const bool normalizeSpeed = Settings::Manager::getBool("normalise race speed", "Game"); if (!normalizeSpeed && mPtr.getClass().isNpc()) { const ESM::NPC* npc = mPtr.get()->mBase; const ESM::Race* race = world->getStore().get().find(npc->mRace); float weight = npc->isMale() ? race->mData.mWeight.mMale : race->mData.mWeight.mFemale; scale *= weight; } if(!cls.isActor()) updateAnimQueue(); else if(!cls.getCreatureStats(mPtr).isDead()) { bool onground = world->isOnGround(mPtr); bool incapacitated = ((!godmode && cls.getCreatureStats(mPtr).isParalyzed()) || cls.getCreatureStats(mPtr).getKnockedDown()); bool inwater = world->isSwimming(mPtr); 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 isrunning = cls.getCreatureStats(mPtr).getStance(MWMechanics::CreatureStats::Stance_Run) && !flying; CreatureStats &stats = cls.getCreatureStats(mPtr); Movement& movementSettings = cls.getMovementSettings(mPtr); //Force Jump Logic bool isMoving = (std::abs(movementSettings.mPosition[0]) > .5 || std::abs(movementSettings.mPosition[1]) > .5); if(!inwater && !flying && solid) { //Force Jump if(stats.getMovementFlag(MWMechanics::CreatureStats::Flag_ForceJump)) movementSettings.mPosition[2] = onground ? 1 : 0; //Force Move Jump, only jump if they're otherwise moving if(stats.getMovementFlag(MWMechanics::CreatureStats::Flag_ForceMoveJump) && isMoving) movementSettings.mPosition[2] = onground ? 1 : 0; } /* Start of tes3mp addition Character movement setting rotations get reset here, so we have to assign movement settings to the LocalPlayer or a LocalActor now */ if (world->getPlayerPtr() == mPtr) { mwmp::LocalPlayer *localPlayer = mwmp::Main::get().getLocalPlayer(); MWMechanics::Movement &movementSettings = cls.getMovementSettings(mPtr); localPlayer->direction.pos[0] = movementSettings.mPosition[0]; localPlayer->direction.pos[1] = movementSettings.mPosition[1]; localPlayer->direction.pos[2] = movementSettings.mPosition[2]; localPlayer->direction.rot[0] = movementSettings.mRotation[0]; localPlayer->direction.rot[1] = movementSettings.mRotation[1]; localPlayer->direction.rot[2] = movementSettings.mRotation[2]; } else if (mwmp::Main::get().getCellController()->isLocalActor(mPtr)) { mwmp::LocalActor *localActor = mwmp::Main::get().getCellController()->getLocalActor(mPtr); MWMechanics::Movement &movementSettings = cls.getMovementSettings(mPtr); localActor->direction.pos[0] = movementSettings.mPosition[0]; localActor->direction.pos[1] = movementSettings.mPosition[1]; localActor->direction.pos[2] = movementSettings.mPosition[2]; localActor->direction.rot[0] = movementSettings.mRotation[0]; localActor->direction.rot[1] = movementSettings.mRotation[1]; localActor->direction.rot[2] = movementSettings.mRotation[2]; } /* End of tes3mp addition */ osg::Vec3f rot = cls.getRotationVector(mPtr); osg::Vec3f vec(movementSettings.asVec3()); movementSettings.mSpeedFactor = std::min(vec.length(), 1.f); vec.normalize(); // TODO: Move this check to mwinput. // Joystick analogue movement. // Due to the half way split between walking/running, we multiply speed by 2 while walking, unless a keyboard was used. if (isPlayer && !isrunning && !sneak && !flying && movementSettings.mSpeedFactor <= 0.5f) movementSettings.mSpeedFactor *= 2.f; static const bool smoothMovement = Settings::Manager::getBool("smooth movement", "Game"); if (smoothMovement) { static const float playerTurningCoef = 1.0 / std::max(0.01f, Settings::Manager::getFloat("smooth movement player turning delay", "Game")); float angle = mPtr.getRefData().getPosition().rot[2]; osg::Vec2f targetSpeed = Misc::rotateVec2f(osg::Vec2f(vec.x(), vec.y()), -angle) * movementSettings.mSpeedFactor; osg::Vec2f delta = targetSpeed - mSmoothedSpeed; float speedDelta = movementSettings.mSpeedFactor - mSmoothedSpeed.length(); float deltaLen = delta.length(); float maxDelta; if (isFirstPersonPlayer) maxDelta = 1; else if (std::abs(speedDelta) < deltaLen / 2) // Turning is smooth for player and less smooth for NPCs (otherwise NPC can miss a path point). maxDelta = duration * (isPlayer ? playerTurningCoef : 6.f); else if (isPlayer && speedDelta < -deltaLen / 2) // As soon as controls are released, mwinput switches player from running to walking. // So stopping should be instant for player, otherwise it causes a small twitch. maxDelta = 1; else // In all other cases speeding up and stopping are smooth. maxDelta = duration * 3.f; if (deltaLen > maxDelta) delta *= maxDelta / deltaLen; mSmoothedSpeed += delta; osg::Vec2f newSpeed = Misc::rotateVec2f(mSmoothedSpeed, angle); movementSettings.mSpeedFactor = newSpeed.normalize(); vec.x() = newSpeed.x(); vec.y() = newSpeed.y(); const float eps = 0.001f; if (movementSettings.mSpeedFactor < eps) { movementSettings.mSpeedFactor = 0; vec.x() = 0; vec.y() = 1; } else if ((vec.y() < 0) != mIsMovingBackward) { if (targetSpeed.length() < eps || (movementSettings.mPosition[1] < 0) == mIsMovingBackward) vec.y() = mIsMovingBackward ? -eps : eps; } vec.normalize(); } float effectiveRotation = rot.z(); bool canMove = cls.getMaxSpeed(mPtr) > 0; static const bool turnToMovementDirection = Settings::Manager::getBool("turn to movement direction", "Game"); if (!turnToMovementDirection || isFirstPersonPlayer) { movementSettings.mIsStrafing = std::abs(vec.x()) > std::abs(vec.y()) * 2; stats.setSideMovementAngle(0); } else if (canMove) { float targetMovementAngle = vec.y() >= 0 ? std::atan2(-vec.x(), vec.y()) : std::atan2(vec.x(), -vec.y()); movementSettings.mIsStrafing = (stats.getDrawState() != MWMechanics::DrawState_Nothing || inwater) && std::abs(targetMovementAngle) > osg::DegreesToRadians(60.0f); if (movementSettings.mIsStrafing) targetMovementAngle = 0; float delta = targetMovementAngle - stats.getSideMovementAngle(); float cosDelta = cosf(delta); if ((vec.y() < 0) == mIsMovingBackward) movementSettings.mSpeedFactor *= std::min(std::max(cosDelta, 0.f) + 0.3f, 1.f); // slow down when turn if (std::abs(delta) < osg::DegreesToRadians(20.0f)) mIsMovingBackward = vec.y() < 0; float maxDelta = osg::PI * duration * (2.5f - cosDelta); delta = osg::clampBetween(delta, -maxDelta, maxDelta); stats.setSideMovementAngle(stats.getSideMovementAngle() + delta); effectiveRotation += delta; } mAnimation->setLegsYawRadians(stats.getSideMovementAngle()); if (stats.getDrawState() == MWMechanics::DrawState_Nothing || inwater) mAnimation->setUpperBodyYawRadians(stats.getSideMovementAngle() / 2); else mAnimation->setUpperBodyYawRadians(stats.getSideMovementAngle() / 4); if (smoothMovement && !isPlayer && !inwater) mAnimation->setUpperBodyYawRadians(mAnimation->getUpperBodyYawRadians() + mAnimation->getHeadYaw() / 2); speed = cls.getCurrentSpeed(mPtr); vec.x() *= speed; vec.y() *= speed; if(mHitState != CharState_None && mJumpState == JumpState_None) vec = osg::Vec3f(); CharacterState movestate = CharState_None; CharacterState idlestate = CharState_SpecialIdle; JumpingState jumpstate = JumpState_None; bool forcestateupdate = false; mHasMovedInXY = std::abs(vec.x())+std::abs(vec.y()) > 0.0f; isrunning = isrunning && mHasMovedInXY; // advance athletics if(mHasMovedInXY && isPlayer) { if(inwater) { mSecondsOfSwimming += duration; while(mSecondsOfSwimming > 1) { cls.skillUsageSucceeded(mPtr, ESM::Skill::Athletics, 1); mSecondsOfSwimming -= 1; } } else if(isrunning && !sneak) { mSecondsOfRunning += duration; while(mSecondsOfRunning > 1) { cls.skillUsageSucceeded(mPtr, ESM::Skill::Athletics, 0); mSecondsOfRunning -= 1; } } } // reduce fatigue const MWWorld::Store &gmst = world->getStore().get(); float fatigueLoss = 0; static const float fFatigueRunBase = gmst.find("fFatigueRunBase")->mValue.getFloat(); static const float fFatigueRunMult = gmst.find("fFatigueRunMult")->mValue.getFloat(); static const float fFatigueSwimWalkBase = gmst.find("fFatigueSwimWalkBase")->mValue.getFloat(); static const float fFatigueSwimRunBase = gmst.find("fFatigueSwimRunBase")->mValue.getFloat(); static const float fFatigueSwimWalkMult = gmst.find("fFatigueSwimWalkMult")->mValue.getFloat(); static const float fFatigueSwimRunMult = gmst.find("fFatigueSwimRunMult")->mValue.getFloat(); static const float fFatigueSneakBase = gmst.find("fFatigueSneakBase")->mValue.getFloat(); static const float fFatigueSneakMult = gmst.find("fFatigueSneakMult")->mValue.getFloat(); if (cls.getEncumbrance(mPtr) <= cls.getCapacity(mPtr)) { const float encumbrance = cls.getNormalizedEncumbrance(mPtr); if (sneak) fatigueLoss = fFatigueSneakBase + encumbrance * fFatigueSneakMult; else { if (inwater) { if (!isrunning) fatigueLoss = fFatigueSwimWalkBase + encumbrance * fFatigueSwimWalkMult; else fatigueLoss = fFatigueSwimRunBase + encumbrance * fFatigueSwimRunMult; } else if (isrunning) fatigueLoss = fFatigueRunBase + encumbrance * fFatigueRunMult; } } fatigueLoss *= duration; fatigueLoss *= movementSettings.mSpeedFactor; DynamicStat fatigue = cls.getCreatureStats(mPtr).getFatigue(); if (!godmode) { fatigue.setCurrent(fatigue.getCurrent() - fatigueLoss, fatigue.getCurrent() < 0); cls.getCreatureStats(mPtr).setFatigue(fatigue); } float z = cls.getJump(mPtr); if(sneak || inwater || flying || incapacitated || !solid || z <= 0) vec.z() = 0.0f; bool inJump = true; bool playLandingSound = false; if(!onground && !flying && !inwater && solid) { // In the air (either getting up —ascending part of jump— or falling). forcestateupdate = (mJumpState != JumpState_InAir); jumpstate = JumpState_InAir; static const float fJumpMoveBase = gmst.find("fJumpMoveBase")->mValue.getFloat(); static const float fJumpMoveMult = gmst.find("fJumpMoveMult")->mValue.getFloat(); float factor = fJumpMoveBase + fJumpMoveMult * mPtr.getClass().getSkill(mPtr, ESM::Skill::Acrobatics)/100.f; factor = std::min(1.f, factor); vec.x() *= factor; vec.y() *= factor; vec.z() = 0.0f; } else if(vec.z() > 0.0f && mJumpState != JumpState_InAir) { // Started a jump. if (z > 0) { if(vec.x() == 0 && vec.y() == 0) vec = osg::Vec3f(0.0f, 0.0f, z); else { osg::Vec3f lat (vec.x(), vec.y(), 0.0f); lat.normalize(); vec = osg::Vec3f(lat.x(), lat.y(), 1.0f) * z * 0.707f; } } } else if(mJumpState == JumpState_InAir && !inwater && !flying && solid) { forcestateupdate = true; jumpstate = JumpState_Landing; vec.z() = 0.0f; // We should reset idle animation during landing mAnimation->disable(mCurrentIdle); float height = cls.getCreatureStats(mPtr).land(isPlayer); float healthLost = getFallDamage(mPtr, height); if (healthLost > 0.0f) { const float fatigueTerm = cls.getCreatureStats(mPtr).getFatigueTerm(); // inflict fall damages if (!godmode) { float realHealthLost = static_cast(healthLost * (1.0f - 0.25f * fatigueTerm)); cls.onHit(mPtr, realHealthLost, true, MWWorld::Ptr(), MWWorld::Ptr(), osg::Vec3f(), true); } const float acrobaticsSkill = cls.getSkill(mPtr, ESM::Skill::Acrobatics); if (healthLost > (acrobaticsSkill * fatigueTerm)) { if (!godmode) cls.getCreatureStats(mPtr).setKnockedDown(true); } else { // report acrobatics progression if (isPlayer) cls.skillUsageSucceeded(mPtr, ESM::Skill::Acrobatics, 1); } } if (mPtr.getClass().isNpc()) playLandingSound = true; } else { if(mPtr.getClass().isNpc() && mJumpState == JumpState_InAir && !flying && solid) playLandingSound = true; jumpstate = mAnimation->isPlaying(mCurrentJump) ? JumpState_Landing : JumpState_None; vec.x() *= scale; vec.y() *= scale; vec.z() = 0.0f; inJump = false; if (movementSettings.mIsStrafing) { if(vec.x() > 0.0f) movestate = (inwater ? (isrunning ? CharState_SwimRunRight : CharState_SwimWalkRight) : (sneak ? CharState_SneakRight : (isrunning ? CharState_RunRight : CharState_WalkRight))); else if(vec.x() < 0.0f) movestate = (inwater ? (isrunning ? CharState_SwimRunLeft : CharState_SwimWalkLeft) : (sneak ? CharState_SneakLeft : (isrunning ? CharState_RunLeft : CharState_WalkLeft))); } else if (vec.length2() > 0.0f) { if (vec.y() >= 0.0f) movestate = (inwater ? (isrunning ? CharState_SwimRunForward : CharState_SwimWalkForward) : (sneak ? CharState_SneakForward : (isrunning ? CharState_RunForward : CharState_WalkForward))); else movestate = (inwater ? (isrunning ? CharState_SwimRunBack : CharState_SwimWalkBack) : (sneak ? CharState_SneakBack : (isrunning ? CharState_RunBack : CharState_WalkBack))); } else { // Do not play turning animation for player if rotation speed is very slow. // Actual threshold should take framerate in account. float rotationThreshold = (isPlayer ? 0.015f : 0.001f) * 60 * duration; // It seems only bipedal actors use turning animations. // Also do not use turning animations in the first-person view and when sneaking. if (!sneak && jumpstate == JumpState_None && !isFirstPersonPlayer && mPtr.getClass().isBipedal(mPtr)) { if(effectiveRotation > rotationThreshold) movestate = inwater ? CharState_SwimTurnRight : CharState_TurnRight; else if(effectiveRotation < -rotationThreshold) movestate = inwater ? CharState_SwimTurnLeft : CharState_TurnLeft; } } } if (playLandingSound) { MWBase::SoundManager *sndMgr = MWBase::Environment::get().getSoundManager(); std::string sound; osg::Vec3f pos(mPtr.getRefData().getPosition().asVec3()); if (world->isUnderwater(mPtr.getCell(), pos) || world->isWalkingOnWater(mPtr)) sound = "DefaultLandWater"; else if (onground) sound = "DefaultLand"; if (!sound.empty()) sndMgr->playSound3D(mPtr, sound, 1.f, 1.f, MWSound::Type::Foot, MWSound::PlayMode::NoPlayerLocal); } if (turnToMovementDirection && !isFirstPersonPlayer && (movestate == CharState_SwimRunForward || movestate == CharState_SwimWalkForward || movestate == CharState_SwimRunBack || movestate == CharState_SwimWalkBack)) { float swimmingPitch = mAnimation->getBodyPitchRadians(); float targetSwimmingPitch = -mPtr.getRefData().getPosition().rot[0]; float maxSwimPitchDelta = 3.0f * duration; swimmingPitch += osg::clampBetween(targetSwimmingPitch - swimmingPitch, -maxSwimPitchDelta, maxSwimPitchDelta); mAnimation->setBodyPitchRadians(swimmingPitch); } else mAnimation->setBodyPitchRadians(0); static const bool swimUpwardCorrection = Settings::Manager::getBool("swim upward correction", "Game"); if (inwater && isPlayer && !isFirstPersonPlayer && swimUpwardCorrection) { static const float swimUpwardCoef = Settings::Manager::getFloat("swim upward coef", "Game"); static const float swimForwardCoef = sqrtf(1.0f - swimUpwardCoef * swimUpwardCoef); vec.z() = std::abs(vec.y()) * swimUpwardCoef; vec.y() *= swimForwardCoef; } // Player can not use smooth turning as NPCs, so we play turning animation a bit to avoid jittering if (isPlayer) { float threshold = mCurrentMovement.find("swim") == std::string::npos ? 0.4f : 0.8f; float complete; bool animPlaying = mAnimation->getInfo(mCurrentMovement, &complete); if (movestate == CharState_None && jumpstate == JumpState_None && isTurning()) { if (animPlaying && complete < threshold) movestate = mMovementState; } } else { if (mPtr.getClass().isBipedal(mPtr)) { if (mTurnAnimationThreshold > 0) mTurnAnimationThreshold -= duration; if (movestate == CharState_TurnRight || movestate == CharState_TurnLeft || movestate == CharState_SwimTurnRight || movestate == CharState_SwimTurnLeft) { mTurnAnimationThreshold = 0.05f; } else if (movestate == CharState_None && isTurning() && mTurnAnimationThreshold > 0) { movestate = mMovementState; } } } if(movestate != CharState_None && !isTurning()) clearAnimQueue(); if(mAnimQueue.empty() || inwater || (sneak && mIdleState != CharState_SpecialIdle)) { if (inwater) idlestate = CharState_IdleSwim; else if (sneak && !inJump) idlestate = CharState_IdleSneak; else idlestate = CharState_Idle; } else updateAnimQueue(); if (!mSkipAnim) { // bipedal means hand-to-hand could be used (which is handled in updateWeaponState). an existing InventoryStore means an actual weapon could be used. if(cls.isBipedal(mPtr) || cls.hasInventoryStore(mPtr)) forcestateupdate = updateWeaponState(idlestate) || forcestateupdate; else forcestateupdate = updateCreatureState() || forcestateupdate; refreshCurrentAnims(idlestate, movestate, jumpstate, forcestateupdate); updateIdleStormState(inwater); } if (inJump) mMovementAnimationControlled = false; if (isTurning()) { // Adjust animation speed from 1.0 to 1.5 multiplier if (duration > 0) { float turnSpeed = std::min(1.5f, std::abs(rot.z()) / duration / static_cast(osg::PI)); mAnimation->adjustSpeedMult(mCurrentMovement, std::max(turnSpeed, 1.0f)); } } else if (mMovementState != CharState_None && mAdjustMovementAnimSpeed) { // Vanilla caps the played animation speed. const float maxSpeedMult = 10.f; const float speedMult = speed / mMovementAnimSpeed; mAnimation->adjustSpeedMult(mCurrentMovement, std::min(maxSpeedMult, speedMult)); // Make sure the actual speed is the "expected" speed even though the animation is slower scale *= std::max(1.f, speedMult / maxSpeedMult); } if (!mSkipAnim) { if(!isKnockedDown() && !isKnockedOut()) { if (rot != osg::Vec3f()) world->rotateObject(mPtr, rot.x(), rot.y(), rot.z(), true); } else //avoid z-rotating for knockdown { if (rot.x() != 0 && rot.y() != 0) world->rotateObject(mPtr, rot.x(), rot.y(), 0.0f, true); } if (!mMovementAnimationControlled) world->queueMovement(mPtr, vec); } movement = vec; movementSettings.mPosition[0] = movementSettings.mPosition[1] = 0; if (movement.z() == 0.f) movementSettings.mPosition[2] = 0; // Can't reset jump state (mPosition[2]) here in full; we don't know for sure whether the PhysicSystem will actually handle it in this frame // due to the fixed minimum timestep used for the physics update. It will be reset in PhysicSystem::move once the jump is handled. if (!mSkipAnim) updateHeadTracking(duration); } else if(cls.getCreatureStats(mPtr).isDead()) { // initial start of death animation for actors that started the game as dead // not done in constructor since we need to give scripts a chance to set the mSkipAnim flag if (!mSkipAnim && mDeathState != CharState_None && mCurrentDeath.empty()) { // Fast-forward death animation to end for persisting corpses or corpses after end of death animation if (cls.isPersistent(mPtr) || cls.getCreatureStats(mPtr).isDeathAnimationFinished()) playDeath(1.f, mDeathState); } } bool isPersist = isPersistentAnimPlaying(); osg::Vec3f moved = mAnimation->runAnimation(mSkipAnim && !isPersist ? 0.f : duration); if(duration > 0.0f) moved /= duration; else moved = osg::Vec3f(0.f, 0.f, 0.f); moved.x() *= scale; moved.y() *= scale; // Ensure we're moving in generally the right direction... if (speed > 0.f && moved != osg::Vec3f()) { float l = moved.length(); if (std::abs(movement.x() - moved.x()) > std::abs(moved.x()) / 2 || std::abs(movement.y() - moved.y()) > std::abs(moved.y()) / 2 || std::abs(movement.z() - moved.z()) > std::abs(moved.z()) / 2) { moved = movement; // For some creatures getSpeed doesn't work, so we adjust speed to the animation. // TODO: Fix Creature::getSpeed. float newLength = moved.length(); if (newLength > 0 && !cls.isNpc()) moved *= (l / newLength); } } if (mFloatToSurface && cls.isActor()) { if (cls.getCreatureStats(mPtr).isDead() || (!godmode && cls.getCreatureStats(mPtr).getMagicEffects().get(ESM::MagicEffect::Paralyze).getModifier() > 0)) { moved.z() = 1.0; } } // Update movement if(mMovementAnimationControlled && mPtr.getClass().isActor()) world->queueMovement(mPtr, moved); mSkipAnim = false; mAnimation->enableHeadAnimation(cls.isActor() && !cls.getCreatureStats(mPtr).isDead()); } void CharacterController::persistAnimationState() { ESM::AnimationState& state = mPtr.getRefData().getAnimationState(); state.mScriptedAnims.clear(); for (AnimationQueue::const_iterator iter = mAnimQueue.begin(); iter != mAnimQueue.end(); ++iter) { if (!iter->mPersist) continue; ESM::AnimationState::ScriptedAnimation anim; anim.mGroup = iter->mGroup; if (iter == mAnimQueue.begin()) { anim.mLoopCount = mAnimation->getCurrentLoopCount(anim.mGroup); float complete; mAnimation->getInfo(anim.mGroup, &complete, nullptr); anim.mTime = complete; } else { anim.mLoopCount = iter->mLoopCount; anim.mTime = 0.f; } state.mScriptedAnims.push_back(anim); } } void CharacterController::unpersistAnimationState() { const ESM::AnimationState& state = mPtr.getRefData().getAnimationState(); if (!state.mScriptedAnims.empty()) { clearAnimQueue(); for (ESM::AnimationState::ScriptedAnimations::const_iterator iter = state.mScriptedAnims.begin(); iter != state.mScriptedAnims.end(); ++iter) { AnimationQueueEntry entry; entry.mGroup = iter->mGroup; entry.mLoopCount = iter->mLoopCount; entry.mPersist = true; mAnimQueue.push_back(entry); } const ESM::AnimationState::ScriptedAnimation& anim = state.mScriptedAnims.front(); float complete = anim.mTime; if (anim.mAbsolute) { float start = mAnimation->getTextKeyTime(anim.mGroup+": start"); float stop = mAnimation->getTextKeyTime(anim.mGroup+": stop"); float time = std::max(start, std::min(stop, anim.mTime)); complete = (time - start) / (stop - start); } mAnimation->disable(mCurrentIdle); mCurrentIdle.clear(); mIdleState = CharState_SpecialIdle; bool loopfallback = (mAnimQueue.front().mGroup.compare(0,4,"idle") == 0); mAnimation->play(anim.mGroup, Priority_Persistent, MWRender::Animation::BlendMask_All, false, 1.0f, "start", "stop", complete, anim.mLoopCount, loopfallback); } } bool CharacterController::playGroup(const std::string &groupname, int mode, int count, bool persist) { if(!mAnimation || !mAnimation->hasAnimation(groupname)) return false; // We should not interrupt persistent animations by non-persistent ones if (isPersistentAnimPlaying() && !persist) return false; // If this animation is a looped animation (has a "loop start" key) that is already playing // and has not yet reached the end of the loop, allow it to continue animating with its existing loop count // and remove any other animations that were queued. // This emulates observed behavior from the original allows the script "OutsideBanner" to animate banners correctly. if (!mAnimQueue.empty() && mAnimQueue.front().mGroup == groupname && mAnimation->getTextKeyTime(mAnimQueue.front().mGroup + ": loop start") >= 0 && mAnimation->isPlaying(groupname)) { float endOfLoop = mAnimation->getTextKeyTime(mAnimQueue.front().mGroup+": loop stop"); if (endOfLoop < 0) // if no Loop Stop key was found, use the Stop key endOfLoop = mAnimation->getTextKeyTime(mAnimQueue.front().mGroup+": stop"); if (endOfLoop > 0 && (mAnimation->getCurrentTime(mAnimQueue.front().mGroup) < endOfLoop)) { mAnimQueue.resize(1); return true; } } count = std::max(count, 1); AnimationQueueEntry entry; entry.mGroup = groupname; entry.mLoopCount = count-1; entry.mPersist = persist; if(mode != 0 || mAnimQueue.empty() || !isAnimPlaying(mAnimQueue.front().mGroup)) { clearAnimQueue(persist); mAnimation->disable(mCurrentIdle); mCurrentIdle.clear(); mIdleState = CharState_SpecialIdle; bool loopfallback = (entry.mGroup.compare(0,4,"idle") == 0); mAnimation->play(groupname, persist && groupname != "idle" ? Priority_Persistent : Priority_Default, MWRender::Animation::BlendMask_All, false, 1.0f, ((mode==2) ? "loop start" : "start"), "stop", 0.0f, count-1, loopfallback); /* Start of tes3mp addition If we are the cell authority over this actor, we need to record this new animation for it */ if (mwmp::Main::get().getCellController()->isLocalActor(mPtr)) { mwmp::LocalActor *actor = mwmp::Main::get().getCellController()->getLocalActor(mPtr); actor->animation.groupname = groupname; actor->animation.mode = mode; actor->animation.count = count; actor->animation.persist = persist; } /* End of tes3mp addition */ } else { mAnimQueue.resize(1); } // "PlayGroup idle" is a special case, used to remove to stop scripted animations playing if (groupname == "idle") entry.mPersist = false; mAnimQueue.push_back(entry); return true; } void CharacterController::skipAnim() { mSkipAnim = true; } bool CharacterController::isPersistentAnimPlaying() { if (!mAnimQueue.empty()) { AnimationQueueEntry& first = mAnimQueue.front(); return first.mPersist && isAnimPlaying(first.mGroup); } return false; } bool CharacterController::isAnimPlaying(const std::string &groupName) { if(mAnimation == nullptr) return false; return mAnimation->isPlaying(groupName); } void CharacterController::clearAnimQueue(bool clearPersistAnims) { // Do not interrupt scripted animations, if we want to keep them if ((!isPersistentAnimPlaying() || clearPersistAnims) && !mAnimQueue.empty()) mAnimation->disable(mAnimQueue.front().mGroup); for (AnimationQueue::iterator it = mAnimQueue.begin(); it != mAnimQueue.end();) { if (clearPersistAnims || !it->mPersist) it = mAnimQueue.erase(it); else ++it; } } void CharacterController::forceStateUpdate() { if(!mAnimation) return; clearAnimQueue(); // Make sure we canceled the current attack or spellcasting, // because we disabled attack animations anyway. mCastingManualSpell = false; mAttackingOrSpell = false; if (mUpperBodyState != UpperCharState_Nothing) mUpperBodyState = UpperCharState_WeapEquiped; refreshCurrentAnims(mIdleState, mMovementState, mJumpState, true); if(mDeathState != CharState_None) { playRandomDeath(); } mAnimation->runAnimation(0.f); } CharacterController::KillResult CharacterController::kill() { if (mDeathState == CharState_None) { playRandomDeath(); mAnimation->disable(mCurrentIdle); mIdleState = CharState_None; mCurrentIdle.clear(); return Result_DeathAnimStarted; } MWMechanics::CreatureStats& cStats = mPtr.getClass().getCreatureStats(mPtr); if (isAnimPlaying(mCurrentDeath)) return Result_DeathAnimPlaying; if (!cStats.isDeathAnimationFinished()) { /* Start of tes3mp addition */ if (mwmp::Main::get().getCellController()->isLocalActor(mPtr)) { mwmp::Main::get().getCellController()->getLocalActor(mPtr)->creatureStats.mDeathAnimationFinished = true; } /* End of tes3mp addition */ cStats.setDeathAnimationFinished(true); return Result_DeathAnimJustFinished; } return Result_DeathAnimFinished; } void CharacterController::resurrect() { if(mDeathState == CharState_None) return; if(mAnimation) mAnimation->disable(mCurrentDeath); mCurrentDeath.clear(); mDeathState = CharState_None; mWeaponType = ESM::Weapon::None; } void CharacterController::updateContinuousVfx() { // Keeping track of when to stop a continuous VFX seems to be very difficult to do inside the spells code, // as it's extremely spread out (ActiveSpells, Spells, InventoryStore effects, etc...) so we do it here. // Stop any effects that are no longer active std::vector effects; mAnimation->getLoopingEffects(effects); for (int effectId : effects) { if (mPtr.getClass().getCreatureStats(mPtr).isDeathAnimationFinished() || mPtr.getClass().getCreatureStats(mPtr).getMagicEffects().get(MWMechanics::EffectKey(effectId)).getMagnitude() <= 0) mAnimation->removeEffect(effectId); } } void CharacterController::updateMagicEffects() { if (!mPtr.getClass().isActor()) return; float light = mPtr.getClass().getCreatureStats(mPtr).getMagicEffects().get(ESM::MagicEffect::Light).getMagnitude(); mAnimation->setLightEffect(light); // If you're dead you don't care about whether you've started/stopped being a vampire or not if (mPtr.getClass().getCreatureStats(mPtr).isDead()) return; bool vampire = mPtr.getClass().getCreatureStats(mPtr).getMagicEffects().get(ESM::MagicEffect::Vampirism).getMagnitude() > 0.0f; mAnimation->setVampire(vampire); } void CharacterController::setVisibility(float visibility) { // We should take actor's invisibility in account if (mPtr.getClass().isActor()) { float alpha = 1.f; if (mPtr.getClass().getCreatureStats(mPtr).getMagicEffects().get(ESM::MagicEffect::Invisibility).getModifier()) // Ignore base magnitude (see bug #3555). { if (mPtr == getPlayer()) alpha = 0.25f; else alpha = 0.05f; } 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)); } visibility = std::min(visibility, alpha); } // TODO: implement a dithering shader rather than just change object transparency. mAnimation->setAlpha(visibility); } void CharacterController::setAttackTypeBasedOnMovement() { float *move = mPtr.getClass().getMovementSettings(mPtr).mPosition; if (std::abs(move[1]) > std::abs(move[0]) + 0.2f) // forward-backward mAttackType = "thrust"; else if (std::abs(move[0]) > std::abs(move[1]) + 0.2f) // sideway mAttackType = "slash"; else mAttackType = "chop"; } bool CharacterController::isRandomAttackAnimation(const std::string& group) const { return (group == "attack1" || group == "swimattack1" || group == "attack2" || group == "swimattack2" || group == "attack3" || group == "swimattack3"); } bool CharacterController::isAttackPreparing() const { return mUpperBodyState == UpperCharState_StartToMinAttack || mUpperBodyState == UpperCharState_MinAttackToMaxAttack; } bool CharacterController::isCastingSpell() const { return mCastingManualSpell || mUpperBodyState == UpperCharState_CastingSpell; } bool CharacterController::isReadyToBlock() const { return updateCarriedLeftVisible(mWeaponType); } bool CharacterController::isKnockedDown() const { return mHitState == CharState_KnockDown || mHitState == CharState_SwimKnockDown; } bool CharacterController::isKnockedOut() const { return mHitState == CharState_KnockOut || mHitState == CharState_SwimKnockOut; } bool CharacterController::isTurning() const { return mMovementState == CharState_TurnLeft || mMovementState == CharState_TurnRight || mMovementState == CharState_SwimTurnLeft || mMovementState == CharState_SwimTurnRight; } bool CharacterController::isRecovery() const { return mHitState == CharState_Hit || mHitState == CharState_SwimHit; } bool CharacterController::isAttackingOrSpell() const { return mUpperBodyState != UpperCharState_Nothing && mUpperBodyState != UpperCharState_WeapEquiped; } bool CharacterController::isSneaking() const { return mIdleState == CharState_IdleSneak || mMovementState == CharState_SneakForward || mMovementState == CharState_SneakBack || mMovementState == CharState_SneakLeft || mMovementState == CharState_SneakRight; } bool CharacterController::isRunning() const { return mMovementState == CharState_RunForward || mMovementState == CharState_RunBack || mMovementState == CharState_RunLeft || mMovementState == CharState_RunRight || mMovementState == CharState_SwimRunForward || mMovementState == CharState_SwimRunBack || mMovementState == CharState_SwimRunLeft || mMovementState == CharState_SwimRunRight; } void CharacterController::setAttackingOrSpell(bool attackingOrSpell) { mAttackingOrSpell = attackingOrSpell; } void CharacterController::castSpell(const std::string spellId, bool manualSpell) { mAttackingOrSpell = true; mCastingManualSpell = manualSpell; ActionSpell action = ActionSpell(spellId); action.prepare(mPtr); } void CharacterController::setAIAttackType(const std::string& attackType) { mAttackType = attackType; } void CharacterController::setAttackTypeRandomly(std::string& attackType) { float random = Misc::Rng::rollProbability(); if (random >= 2/3.f) attackType = "thrust"; else if (random >= 1/3.f) attackType = "slash"; else attackType = "chop"; } bool CharacterController::readyToPrepareAttack() const { return (mHitState == CharState_None || mHitState == CharState_Block) && mUpperBodyState <= UpperCharState_WeapEquiped; } bool CharacterController::readyToStartAttack() const { if (mHitState != CharState_None && mHitState != CharState_Block) return false; if (mPtr.getClass().hasInventoryStore(mPtr) || mPtr.getClass().isBipedal(mPtr)) return mUpperBodyState == UpperCharState_WeapEquiped; else return mUpperBodyState == UpperCharState_Nothing; } float CharacterController::getAttackStrength() const { return mAttackStrength; } /* Start of tes3mp addition Make it possible to get the current attack type from elsewhere in the code */ std::string CharacterController::getAttackType() const { return mAttackType; } /* End of tes3mp addition */ void CharacterController::setActive(int active) { mAnimation->setActive(active); } void CharacterController::setHeadTrackTarget(const MWWorld::ConstPtr &target) { mHeadTrackTarget = target; } void CharacterController::playSwishSound(float attackStrength) { MWBase::SoundManager *sndMgr = MWBase::Environment::get().getSoundManager(); std::string sound = "Weapon Swish"; if(attackStrength < 0.5f) sndMgr->playSound3D(mPtr, sound, 1.0f, 0.8f); //Weak attack else if(attackStrength < 1.0f) sndMgr->playSound3D(mPtr, sound, 1.0f, 1.0f); //Medium attack else sndMgr->playSound3D(mPtr, sound, 1.0f, 1.2f); //Strong attack } void CharacterController::updateHeadTracking(float duration) { const osg::Node* head = mAnimation->getNode("Bip01 Head"); if (!head) return; double zAngleRadians = 0.f; double xAngleRadians = 0.f; if (!mHeadTrackTarget.isEmpty()) { osg::NodePathList nodepaths = head->getParentalNodePaths(); if (nodepaths.empty()) return; osg::Matrixf mat = osg::computeLocalToWorld(nodepaths[0]); osg::Vec3f headPos = mat.getTrans(); osg::Vec3f direction; if (const MWRender::Animation* anim = MWBase::Environment::get().getWorld()->getAnimation(mHeadTrackTarget)) { const osg::Node* node = anim->getNode("Head"); if (node == nullptr) node = anim->getNode("Bip01 Head"); if (node != nullptr) { nodepaths = node->getParentalNodePaths(); if (!nodepaths.empty()) direction = osg::computeLocalToWorld(nodepaths[0]).getTrans() - headPos; } else // no head node to look at, fall back to look at center of collision box direction = MWBase::Environment::get().getWorld()->aimToTarget(mPtr, mHeadTrackTarget, false); } direction.normalize(); if (!mPtr.getRefData().getBaseNode()) return; const osg::Vec3f actorDirection = mPtr.getRefData().getBaseNode()->getAttitude() * osg::Vec3f(0,1,0); zAngleRadians = std::atan2(actorDirection.x(), actorDirection.y()) - std::atan2(direction.x(), direction.y()); zAngleRadians = Misc::normalizeAngle(zAngleRadians - mAnimation->getHeadYaw()) + mAnimation->getHeadYaw(); zAngleRadians *= (1 - direction.z() * direction.z()); xAngleRadians = std::asin(direction.z()); } 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); float factor = duration*5; factor = std::min(factor, 1.f); xAngleRadians = (1.f-factor) * mAnimation->getHeadPitch() + factor * xAngleRadians; zAngleRadians = (1.f-factor) * mAnimation->getHeadYaw() + factor * zAngleRadians; mAnimation->setHeadPitch(xAngleRadians); mAnimation->setHeadYaw(zAngleRadians); } } ================================================ FILE: apps/openmw/mwmechanics/character.hpp ================================================ #ifndef GAME_MWMECHANICS_CHARACTER_HPP #define GAME_MWMECHANICS_CHARACTER_HPP #include #include "../mwworld/ptr.hpp" #include "../mwworld/containerstore.hpp" #include "../mwrender/animation.hpp" #include "weapontype.hpp" namespace MWWorld { class InventoryStore; } namespace MWRender { class Animation; } namespace MWMechanics { struct Movement; class CreatureStats; enum Priority { Priority_Default, Priority_WeaponLowerBody, Priority_SneakIdleLowerBody, Priority_SwimIdle, Priority_Jump, Priority_Movement, Priority_Hit, Priority_Weapon, Priority_Block, Priority_Knockdown, Priority_Torch, Priority_Storm, Priority_Death, Priority_Persistent, Num_Priorities }; enum CharacterState { CharState_None, CharState_SpecialIdle, CharState_Idle, CharState_Idle2, CharState_Idle3, CharState_Idle4, CharState_Idle5, CharState_Idle6, CharState_Idle7, CharState_Idle8, CharState_Idle9, CharState_IdleSwim, CharState_IdleSneak, CharState_WalkForward, CharState_WalkBack, CharState_WalkLeft, CharState_WalkRight, CharState_SwimWalkForward, CharState_SwimWalkBack, CharState_SwimWalkLeft, CharState_SwimWalkRight, CharState_RunForward, CharState_RunBack, CharState_RunLeft, CharState_RunRight, CharState_SwimRunForward, CharState_SwimRunBack, CharState_SwimRunLeft, CharState_SwimRunRight, CharState_SneakForward, CharState_SneakBack, CharState_SneakLeft, CharState_SneakRight, CharState_TurnLeft, CharState_TurnRight, CharState_SwimTurnLeft, CharState_SwimTurnRight, CharState_Jump, CharState_Death1, CharState_Death2, CharState_Death3, CharState_Death4, CharState_Death5, CharState_SwimDeath, CharState_SwimDeathKnockDown, CharState_SwimDeathKnockOut, CharState_DeathKnockDown, CharState_DeathKnockOut, CharState_Hit, CharState_SwimHit, CharState_KnockDown, CharState_KnockOut, CharState_SwimKnockDown, CharState_SwimKnockOut, CharState_Block }; enum UpperBodyCharacterState { UpperCharState_Nothing, UpperCharState_EquipingWeap, UpperCharState_UnEquipingWeap, UpperCharState_WeapEquiped, UpperCharState_StartToMinAttack, UpperCharState_MinAttackToMaxAttack, UpperCharState_MaxAttackToMinHit, UpperCharState_MinHitToHit, UpperCharState_FollowStartToFollowStop, UpperCharState_CastingSpell }; enum JumpingState { JumpState_None, JumpState_InAir, JumpState_Landing }; struct WeaponInfo; class CharacterController : public MWRender::Animation::TextKeyListener { MWWorld::Ptr mPtr; MWWorld::Ptr mWeapon; MWRender::Animation *mAnimation; struct AnimationQueueEntry { std::string mGroup; size_t mLoopCount; bool mPersist; }; typedef std::deque AnimationQueue; AnimationQueue mAnimQueue; CharacterState mIdleState; std::string mCurrentIdle; CharacterState mMovementState; std::string mCurrentMovement; float mMovementAnimSpeed; bool mAdjustMovementAnimSpeed; bool mHasMovedInXY; bool mMovementAnimationControlled; CharacterState mDeathState; std::string mCurrentDeath; bool mFloatToSurface; CharacterState mHitState; std::string mCurrentHit; UpperBodyCharacterState mUpperBodyState; JumpingState mJumpState; std::string mCurrentJump; int mWeaponType; std::string mCurrentWeapon; float mAttackStrength; bool mSkipAnim; // counted for skill increase float mSecondsOfSwimming; float mSecondsOfRunning; MWWorld::ConstPtr mHeadTrackTarget; float mTurnAnimationThreshold; // how long to continue playing turning animation after actor stopped turning std::string mAttackType; // slash, chop or thrust bool mAttackingOrSpell; bool mCastingManualSpell; float mTimeUntilWake; bool mIsMovingBackward; osg::Vec2f mSmoothedSpeed; void setAttackTypeBasedOnMovement(); void refreshCurrentAnims(CharacterState idle, CharacterState movement, JumpingState jump, bool force=false); void refreshHitRecoilAnims(CharacterState& idle); void refreshJumpAnims(const std::string& weapShortGroup, JumpingState jump, CharacterState& idle, bool force=false); void refreshMovementAnims(const std::string& weapShortGroup, CharacterState movement, CharacterState& idle, bool force=false); void refreshIdleAnims(const std::string& weapShortGroup, CharacterState idle, bool force=false); void clearAnimQueue(bool clearPersistAnims = false); bool updateWeaponState(CharacterState& idle); bool updateCreatureState(); void updateIdleStormState(bool inwater); std::string chooseRandomAttackAnimation() const; bool isRandomAttackAnimation(const std::string& group) const; bool isPersistentAnimPlaying(); void updateAnimQueue(); void updateHeadTracking(float duration); void updateMagicEffects(); void playDeath(float startpoint, CharacterState death); CharacterState chooseRandomDeathState() const; void playRandomDeath(float startpoint = 0.0f); /// choose a random animation group with \a prefix and numeric suffix /// @param num if non-nullptr, the chosen animation number will be written here std::string chooseRandomGroup (const std::string& prefix, int* num = nullptr) const; bool updateCarriedLeftVisible(int weaptype) const; std::string fallbackShortWeaponGroup(const std::string& baseGroupName, MWRender::Animation::BlendMask* blendMask = nullptr); std::string getWeaponAnimation(int weaponType) const; public: CharacterController(const MWWorld::Ptr &ptr, MWRender::Animation *anim); virtual ~CharacterController(); void handleTextKey(const std::string &groupname, SceneUtil::TextKeyMap::ConstIterator key, const SceneUtil::TextKeyMap& map) override; // Be careful when to call this, see comment in Actors void updateContinuousVfx(); void updatePtr(const MWWorld::Ptr &ptr); void update(float duration); bool onOpen(); void onClose(); void persistAnimationState(); void unpersistAnimationState(); bool playGroup(const std::string &groupname, int mode, int count, bool persist=false); void skipAnim(); bool isAnimPlaying(const std::string &groupName); enum KillResult { Result_DeathAnimStarted, Result_DeathAnimPlaying, Result_DeathAnimJustFinished, Result_DeathAnimFinished }; KillResult kill(); void resurrect(); bool isDead() const { return mDeathState != CharState_None; } void forceStateUpdate(); bool isAttackPreparing() const; bool isCastingSpell() const; bool isReadyToBlock() const; bool isKnockedDown() const; bool isKnockedOut() const; bool isRecovery() const; bool isSneaking() const; bool isRunning() const; bool isTurning() const; bool isAttackingOrSpell() const; void setVisibility(float visibility); void setAttackingOrSpell(bool attackingOrSpell); void castSpell(const std::string spellId, bool manualSpell=false); void setAIAttackType(const std::string& attackType); static void setAttackTypeRandomly(std::string& attackType); bool readyToPrepareAttack() const; bool readyToStartAttack() const; float getAttackStrength() const; /* Start of tes3mp addition Make it possible to get the current attack type from elsewhere in the code */ std::string getAttackType() const; /* End of tes3mp addition */ /// @see Animation::setActive void setActive(int active); /// Make this character turn its head towards \a target. To turn off head tracking, pass an empty Ptr. void setHeadTrackTarget(const MWWorld::ConstPtr& target); void playSwishSound(float attackStrength); }; } #endif /* GAME_MWMECHANICS_CHARACTER_HPP */ ================================================ FILE: apps/openmw/mwmechanics/combat.cpp ================================================ #include "combat.hpp" #include #include #include /* Start of tes3mp addition Include additional headers for multiplayer purposes */ #include #include "../mwmp/Main.hpp" #include "../mwmp/LocalPlayer.hpp" #include "../mwmp/PlayerList.hpp" #include "../mwmp/CellController.hpp" #include "../mwmp/MechanicsHelper.hpp" /* End of tes3mp addition */ #include "../mwbase/environment.hpp" #include "../mwbase/world.hpp" #include "../mwbase/mechanicsmanager.hpp" #include "../mwbase/soundmanager.hpp" #include "../mwbase/windowmanager.hpp" #include "../mwworld/class.hpp" #include "../mwworld/inventorystore.hpp" #include "../mwworld/esmstore.hpp" #include "npcstats.hpp" #include "movement.hpp" #include "spellcasting.hpp" #include "spellresistance.hpp" #include "difficultyscaling.hpp" #include "actorutil.hpp" #include "pathfinding.hpp" namespace { float signedAngleRadians (const osg::Vec3f& v1, const osg::Vec3f& v2, const osg::Vec3f& normal) { return std::atan2((normal * (v1 ^ v2)), (v1 * v2)); } } namespace MWMechanics { bool applyOnStrikeEnchantment(const MWWorld::Ptr& attacker, const MWWorld::Ptr& victim, const MWWorld::Ptr& object, const osg::Vec3f& hitPosition, const bool fromProjectile) { std::string enchantmentName = !object.isEmpty() ? object.getClass().getEnchantment(object) : ""; if (!enchantmentName.empty()) { const ESM::Enchantment* enchantment = MWBase::Environment::get().getWorld()->getStore().get().find( enchantmentName); if (enchantment->mData.mType == ESM::Enchantment::WhenStrikes) { MWMechanics::CastSpell cast(attacker, victim, fromProjectile); cast.mHitPosition = hitPosition; cast.cast(object, false); return true; } } return false; } bool blockMeleeAttack(const MWWorld::Ptr &attacker, const MWWorld::Ptr &blocker, const MWWorld::Ptr &weapon, float damage, float attackStrength) { if (!blocker.getClass().hasInventoryStore(blocker)) return false; MWMechanics::CreatureStats& blockerStats = blocker.getClass().getCreatureStats(blocker); if (blockerStats.getKnockedDown() // Used for both knockout or knockdown || blockerStats.getHitRecovery() || blockerStats.isParalyzed()) return false; if (!MWBase::Environment::get().getMechanicsManager()->isReadyToBlock(blocker)) return false; MWWorld::InventoryStore& inv = blocker.getClass().getInventoryStore(blocker); MWWorld::ContainerStoreIterator shield = inv.getSlot(MWWorld::InventoryStore::Slot_CarriedLeft); if (shield == inv.end() || shield->getTypeName() != typeid(ESM::Armor).name()) return false; if (!blocker.getRefData().getBaseNode()) return false; // shouldn't happen float angleDegrees = osg::RadiansToDegrees( signedAngleRadians ( (attacker.getRefData().getPosition().asVec3() - blocker.getRefData().getPosition().asVec3()), blocker.getRefData().getBaseNode()->getAttitude() * osg::Vec3f(0,1,0), osg::Vec3f(0,0,1))); const MWWorld::Store& gmst = MWBase::Environment::get().getWorld()->getStore().get(); if (angleDegrees < gmst.find("fCombatBlockLeftAngle")->mValue.getFloat()) return false; if (angleDegrees > gmst.find("fCombatBlockRightAngle")->mValue.getFloat()) return false; MWMechanics::CreatureStats& attackerStats = attacker.getClass().getCreatureStats(attacker); float blockTerm = blocker.getClass().getSkill(blocker, ESM::Skill::Block) + 0.2f * blockerStats.getAttribute(ESM::Attribute::Agility).getModified() + 0.1f * blockerStats.getAttribute(ESM::Attribute::Luck).getModified(); float enemySwing = attackStrength; float swingTerm = enemySwing * gmst.find("fSwingBlockMult")->mValue.getFloat() + gmst.find("fSwingBlockBase")->mValue.getFloat(); float blockerTerm = blockTerm * swingTerm; if (blocker.getClass().getMovementSettings(blocker).mPosition[1] <= 0) blockerTerm *= gmst.find("fBlockStillBonus")->mValue.getFloat(); blockerTerm *= blockerStats.getFatigueTerm(); float attackerSkill = 0; if (weapon.isEmpty()) attackerSkill = attacker.getClass().getSkill(attacker, ESM::Skill::HandToHand); else attackerSkill = attacker.getClass().getSkill(attacker, weapon.getClass().getEquipmentSkill(weapon)); float attackerTerm = attackerSkill + 0.2f * attackerStats.getAttribute(ESM::Attribute::Agility).getModified() + 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)); /* Start of tes3mp change (major) Only calculate block chance for LocalPlayers and LocalActors; otherwise, get the block state from the relevant DedicatedPlayer or DedicatedActor */ mwmp::Attack *localAttack = MechanicsHelper::getLocalAttack(attacker); if (localAttack) { localAttack->block = false; } mwmp::Attack *dedicatedAttack = MechanicsHelper::getDedicatedAttack(blocker); if ((dedicatedAttack && dedicatedAttack->block == true) || Misc::Rng::roll0to99() < x) { if (localAttack) { localAttack->block = true; } /* End of tes3mp change (major) */ // Reduce shield durability by incoming damage int shieldhealth = shield->getClass().getItemHealth(*shield); shieldhealth -= std::min(shieldhealth, int(damage)); shield->getCellRef().setCharge(shieldhealth); if (shieldhealth == 0) inv.unequipItem(*shield, blocker); // Reduce blocker fatigue const float fFatigueBlockBase = gmst.find("fFatigueBlockBase")->mValue.getFloat(); const float fFatigueBlockMult = gmst.find("fFatigueBlockMult")->mValue.getFloat(); const float fWeaponFatigueBlockMult = gmst.find("fWeaponFatigueBlockMult")->mValue.getFloat(); MWMechanics::DynamicStat fatigue = blockerStats.getFatigue(); float normalizedEncumbrance = blocker.getClass().getNormalizedEncumbrance(blocker); normalizedEncumbrance = std::min(1.f, normalizedEncumbrance); float fatigueLoss = fFatigueBlockBase + normalizedEncumbrance * fFatigueBlockMult; if (!weapon.isEmpty()) fatigueLoss += weapon.getClass().getWeight(weapon) * attackStrength * fWeaponFatigueBlockMult; fatigue.setCurrent(fatigue.getCurrent() - fatigueLoss); blockerStats.setFatigue(fatigue); blockerStats.setBlock(true); if (blocker == getPlayer()) blocker.getClass().skillUsageSucceeded(blocker, ESM::Skill::Block, 0); return true; } return false; } bool isNormalWeapon(const MWWorld::Ptr &weapon) { if (weapon.isEmpty()) return false; const int flags = weapon.get()->mBase->mData.mFlags; bool isSilver = flags & ESM::Weapon::Silver; bool isMagical = flags & ESM::Weapon::Magical; bool isEnchanted = !weapon.getClass().getEnchantment(weapon).empty(); return !isSilver && !isMagical && (!isEnchanted || !Settings::Manager::getBool("enchanted weapons are magical", "Game")); } void resistNormalWeapon(const MWWorld::Ptr &actor, const MWWorld::Ptr& attacker, const MWWorld::Ptr &weapon, float &damage) { if (damage == 0 || weapon.isEmpty() || !isNormalWeapon(weapon)) return; const MWMechanics::MagicEffects& effects = actor.getClass().getCreatureStats(actor).getMagicEffects(); const float resistance = effects.get(ESM::MagicEffect::ResistNormalWeapons).getMagnitude() / 100.f; const float weakness = effects.get(ESM::MagicEffect::WeaknessToNormalWeapons).getMagnitude() / 100.f; damage *= 1.f - std::min(1.f, resistance-weakness); if (damage == 0 && attacker == getPlayer()) MWBase::Environment::get().getWindowManager()->messageBox("#{sMagicTargetResistsWeapons}"); } void applyWerewolfDamageMult(const MWWorld::Ptr &actor, const MWWorld::Ptr &weapon, float &damage) { if (damage == 0 || weapon.isEmpty() || !actor.getClass().isNpc()) return; const int flags = weapon.get()->mBase->mData.mFlags; bool isSilver = flags & ESM::Weapon::Silver; if (isSilver && actor.getClass().getNpcStats(actor).isWerewolf()) { const MWWorld::ESMStore& store = MWBase::Environment::get().getWorld()->getStore(); damage *= store.get().find("fWereWolfSilverWeaponDamageMult")->mValue.getFloat(); } } void projectileHit(const MWWorld::Ptr& attacker, const MWWorld::Ptr& victim, MWWorld::Ptr weapon, const MWWorld::Ptr& projectile, const osg::Vec3f& hitPosition, float attackStrength) { /* Start of tes3mp addition Ignore projectiles fired by DedicatedPlayers and DedicatedActors If fired by LocalPlayers and LocalActors, get the associated LocalAttack and set its type to RANGED while also marking it as a hit */ if (mwmp::PlayerList::isDedicatedPlayer(attacker) || mwmp::Main::get().getCellController()->isDedicatedActor(attacker)) return; mwmp::Attack *localAttack = MechanicsHelper::getLocalAttack(attacker); if (localAttack) { localAttack->type = mwmp::Attack::RANGED; localAttack->isHit = true; } /* End of tes3mp addition */ MWBase::World *world = MWBase::Environment::get().getWorld(); const MWWorld::Store &gmst = world->getStore().get(); bool validVictim = !victim.isEmpty() && victim.getClass().isActor(); float damage = 0.f; if (validVictim) { if (attacker == getPlayer()) MWBase::Environment::get().getWindowManager()->setEnemy(victim); int weaponSkill = ESM::Skill::Marksman; if (!weapon.isEmpty()) weaponSkill = weapon.getClass().getEquipmentSkill(weapon); int skillValue = attacker.getClass().getSkill(attacker, weapon.getClass().getEquipmentSkill(weapon)); /* Start of tes3mp addition Mark this as a successful attack for the associated LocalAttack unless proven otherwise */ if (localAttack) localAttack->success = true; /* End of tes3mp addition */ if (Misc::Rng::roll0to99() >= getHitChance(attacker, victim, skillValue)) { /* Start of tes3mp addition Mark this as a failed LocalAttack now that the hit roll has failed */ if (localAttack) localAttack->success = false; /* End of tes3mp addition */ victim.getClass().onHit(victim, damage, false, projectile, attacker, osg::Vec3f(), false); MWMechanics::reduceWeaponCondition(damage, false, weapon, attacker); return; } const unsigned char* attack = weapon.get()->mBase->mData.mChop; damage = attack[0] + ((attack[1] - attack[0]) * attackStrength); // Bow/crossbow damage // Arrow/bolt damage // NB in case of thrown weapons, we are applying the damage twice since projectile == weapon attack = projectile.get()->mBase->mData.mChop; damage += attack[0] + ((attack[1] - attack[0]) * attackStrength); adjustWeaponDamage(damage, weapon, attacker); if (weapon == projectile || Settings::Manager::getBool("only appropriate ammunition bypasses resistance", "Game") || isNormalWeapon(weapon)) resistNormalWeapon(victim, attacker, projectile, damage); applyWerewolfDamageMult(victim, projectile, damage); if (attacker == getPlayer()) attacker.getClass().skillUsageSucceeded(attacker, weaponSkill, 0); const MWMechanics::AiSequence& sequence = victim.getClass().getCreatureStats(victim).getAiSequence(); bool unaware = attacker == getPlayer() && !sequence.isInCombat() && !MWBase::Environment::get().getMechanicsManager()->awarenessCheck(attacker, victim); bool knockedDown = victim.getClass().getCreatureStats(victim).getKnockedDown(); if (knockedDown || unaware) { damage *= gmst.find("fCombatKODamageMult")->mValue.getFloat(); if (!knockedDown) MWBase::Environment::get().getSoundManager()->playSound3D(victim, "critical damage", 1.0f, 1.0f); } } reduceWeaponCondition(damage, validVictim, weapon, attacker); // Apply "On hit" effect of the projectile bool appliedEnchantment = applyOnStrikeEnchantment(attacker, victim, projectile, hitPosition, true); /* Start of tes3mp change (minor) Track whether the strike enchantment is successful for attacks by the LocalPlayer or LocalActors for their projectile */ if (localAttack) localAttack->applyAmmoEnchantment = appliedEnchantment; /* End of tes3mp change (minor) */ if (validVictim) { // Non-enchanted arrows shot at enemies have a chance to turn up in their inventory if (victim != getPlayer() && !appliedEnchantment) { float fProjectileThrownStoreChance = gmst.find("fProjectileThrownStoreChance")->mValue.getFloat(); if (Misc::Rng::rollProbability() < fProjectileThrownStoreChance / 100.f) victim.getClass().getContainerStore(victim).add(projectile, 1, victim); } victim.getClass().onHit(victim, damage, true, projectile, attacker, hitPosition, true); } /* Start of tes3mp addition If this is a local attack that had no victim, send a packet for it here */ else if (localAttack) { localAttack->hitPosition = MechanicsHelper::getPositionFromVector(hitPosition); localAttack->shouldSend = true; } /* End of tes3mp addition */ } float getHitChance(const MWWorld::Ptr &attacker, const MWWorld::Ptr &victim, int skillValue) { MWMechanics::CreatureStats &stats = attacker.getClass().getCreatureStats(attacker); const MWMechanics::MagicEffects &mageffects = stats.getMagicEffects(); MWBase::World *world = MWBase::Environment::get().getWorld(); const MWWorld::Store &gmst = world->getStore().get(); float defenseTerm = 0; MWMechanics::CreatureStats& victimStats = victim.getClass().getCreatureStats(victim); if (victimStats.getFatigue().getCurrent() >= 0) { // Maybe we should keep an aware state for actors updated every so often instead of testing every time bool unaware = (!victimStats.getAiSequence().isInCombat()) && (attacker == getPlayer()) && (!MWBase::Environment::get().getMechanicsManager()->awarenessCheck(attacker, victim)); if (!(victimStats.getKnockedDown() || victimStats.isParalyzed() || unaware )) { defenseTerm = victimStats.getEvasion(); } defenseTerm += std::min(100.f, gmst.find("fCombatInvisoMult")->mValue.getFloat() * victimStats.getMagicEffects().get(ESM::MagicEffect::Chameleon).getMagnitude()); defenseTerm += std::min(100.f, gmst.find("fCombatInvisoMult")->mValue.getFloat() * victimStats.getMagicEffects().get(ESM::MagicEffect::Invisibility).getMagnitude()); } float attackTerm = skillValue + (stats.getAttribute(ESM::Attribute::Agility).getModified() / 5.0f) + (stats.getAttribute(ESM::Attribute::Luck).getModified() / 10.0f); attackTerm *= stats.getFatigueTerm(); attackTerm += mageffects.get(ESM::MagicEffect::FortifyAttack).getMagnitude() - mageffects.get(ESM::MagicEffect::Blind).getMagnitude(); return round(attackTerm - defenseTerm); } void applyElementalShields(const MWWorld::Ptr &attacker, const MWWorld::Ptr &victim) { // Don't let elemental shields harm the player in god mode. bool godmode = attacker == getPlayer() && MWBase::Environment::get().getWorld()->getGodModeState(); if (godmode) return; for (int i=0; i<3; ++i) { float magnitude = victim.getClass().getCreatureStats(victim).getMagicEffects().get(ESM::MagicEffect::FireShield+i).getMagnitude(); if (!magnitude) continue; CreatureStats& attackerStats = attacker.getClass().getCreatureStats(attacker); float saveTerm = attacker.getClass().getSkill(attacker, ESM::Skill::Destruction) + 0.2f * attackerStats.getAttribute(ESM::Attribute::Willpower).getModified() + 0.1f * attackerStats.getAttribute(ESM::Attribute::Luck).getModified(); float fatigueMax = attackerStats.getFatigue().getModified(); float fatigueCurrent = attackerStats.getFatigue().getCurrent(); float normalisedFatigue = floor(fatigueMax)==0 ? 1 : std::max (0.0f, (fatigueCurrent/fatigueMax)); saveTerm *= 1.25f * normalisedFatigue; float x = std::max(0.f, saveTerm - Misc::Rng::roll0to99()); int element = ESM::MagicEffect::FireDamage; if (i == 1) element = ESM::MagicEffect::ShockDamage; if (i == 2) element = ESM::MagicEffect::FrostDamage; float elementResistance = MWMechanics::getEffectResistanceAttribute(element, &attackerStats.getMagicEffects()); x = std::min(100.f, x + elementResistance); static const float fElementalShieldMult = MWBase::Environment::get().getWorld()->getStore().get().find("fElementalShieldMult")->mValue.getFloat(); x = fElementalShieldMult * magnitude * (1.f - 0.01f * x); // Note swapped victim and attacker, since the attacker takes the damage here. x = scaleDamage(x, victim, attacker); MWMechanics::DynamicStat health = attackerStats.getHealth(); health.setCurrent(health.getCurrent() - x); attackerStats.setHealth(health); MWBase::Environment::get().getSoundManager()->playSound3D(attacker, "Health Damage", 1.0f, 1.0f); } } void reduceWeaponCondition(float damage, bool hit, MWWorld::Ptr &weapon, const MWWorld::Ptr &attacker) { if (weapon.isEmpty()) return; if (!hit) damage = 0.f; const bool weaphashealth = weapon.getClass().hasItemHealth(weapon); if(weaphashealth) { int weaphealth = weapon.getClass().getItemHealth(weapon); bool godmode = attacker == MWMechanics::getPlayer() && MWBase::Environment::get().getWorld()->getGodModeState(); // weapon condition does not degrade when godmode is on if (!godmode) { const float fWeaponDamageMult = MWBase::Environment::get().getWorld()->getStore().get().find("fWeaponDamageMult")->mValue.getFloat(); float x = std::max(1.f, fWeaponDamageMult * damage); weaphealth -= std::min(int(x), weaphealth); weapon.getCellRef().setCharge(weaphealth); } // Weapon broken? unequip it if (weaphealth == 0) weapon = *attacker.getClass().getInventoryStore(attacker).unequipItem(weapon, attacker); } } void adjustWeaponDamage(float &damage, const MWWorld::Ptr &weapon, const MWWorld::Ptr& attacker) { if (weapon.isEmpty()) return; const bool weaphashealth = weapon.getClass().hasItemHealth(weapon); if (weaphashealth) { damage *= weapon.getClass().getItemNormalizedHealth(weapon); } static const float fDamageStrengthBase = MWBase::Environment::get().getWorld()->getStore().get() .find("fDamageStrengthBase")->mValue.getFloat(); static const float fDamageStrengthMult = MWBase::Environment::get().getWorld()->getStore().get() .find("fDamageStrengthMult")->mValue.getFloat(); damage *= fDamageStrengthBase + (attacker.getClass().getCreatureStats(attacker).getAttribute(ESM::Attribute::Strength).getModified() * fDamageStrengthMult * 0.1f); } void getHandToHandDamage(const MWWorld::Ptr &attacker, const MWWorld::Ptr &victim, float &damage, bool &healthdmg, float attackStrength) { const MWWorld::ESMStore& store = MWBase::Environment::get().getWorld()->getStore(); float minstrike = store.get().find("fMinHandToHandMult")->mValue.getFloat(); float maxstrike = store.get().find("fMaxHandToHandMult")->mValue.getFloat(); damage = static_cast(attacker.getClass().getSkill(attacker, ESM::Skill::HandToHand)); damage *= minstrike + ((maxstrike-minstrike)*attackStrength); MWMechanics::CreatureStats& otherstats = victim.getClass().getCreatureStats(victim); healthdmg = otherstats.isParalyzed() || otherstats.getKnockedDown(); bool isWerewolf = (attacker.getClass().isNpc() && attacker.getClass().getNpcStats(attacker).isWerewolf()); // Options in the launcher's combo box: unarmedFactorsStrengthComboBox // 0 = Do not factor strength into hand-to-hand combat. // 1 = Factor into werewolf hand-to-hand combat. // 2 = Ignore werewolves. int factorStrength = Settings::Manager::getInt("strength influences hand to hand", "Game"); if (factorStrength == 1 || (factorStrength == 2 && !isWerewolf)) { damage *= attacker.getClass().getCreatureStats(attacker).getAttribute(ESM::Attribute::Strength).getModified() / 40.0f; } if(isWerewolf) { healthdmg = true; // GLOB instead of GMST because it gets updated during a quest damage *= MWBase::Environment::get().getWorld()->getGlobalFloat("werewolfclawmult"); } if(healthdmg) damage *= store.get().find("fHandtoHandHealthPer")->mValue.getFloat(); MWBase::SoundManager *sndMgr = MWBase::Environment::get().getSoundManager(); if(isWerewolf) { const ESM::Sound *sound = store.get().searchRandom("WolfHit"); if(sound) sndMgr->playSound3D(victim, sound->mId, 1.0f, 1.0f); } else if (!healthdmg) sndMgr->playSound3D(victim, "Hand To Hand Hit", 1.0f, 1.0f); } void applyFatigueLoss(const MWWorld::Ptr &attacker, const MWWorld::Ptr &weapon, float attackStrength) { // somewhat of a guess, but using the weapon weight makes sense const MWWorld::Store& store = MWBase::Environment::get().getWorld()->getStore().get(); const float fFatigueAttackBase = store.find("fFatigueAttackBase")->mValue.getFloat(); const float fFatigueAttackMult = store.find("fFatigueAttackMult")->mValue.getFloat(); const float fWeaponFatigueMult = store.find("fWeaponFatigueMult")->mValue.getFloat(); CreatureStats& stats = attacker.getClass().getCreatureStats(attacker); MWMechanics::DynamicStat fatigue = stats.getFatigue(); const float normalizedEncumbrance = attacker.getClass().getNormalizedEncumbrance(attacker); bool godmode = attacker == MWMechanics::getPlayer() && MWBase::Environment::get().getWorld()->getGodModeState(); if (!godmode) { float fatigueLoss = fFatigueAttackBase + normalizedEncumbrance * fFatigueAttackMult; if (!weapon.isEmpty()) fatigueLoss += weapon.getClass().getWeight(weapon) * attackStrength * fWeaponFatigueMult; fatigue.setCurrent(fatigue.getCurrent() - fatigueLoss); stats.setFatigue(fatigue); } } float getFightDistanceBias(const MWWorld::Ptr& actor1, const MWWorld::Ptr& actor2) { osg::Vec3f pos1 (actor1.getRefData().getPosition().asVec3()); osg::Vec3f pos2 (actor2.getRefData().getPosition().asVec3()); float d = getAggroDistance(actor1, pos1, pos2); static const int iFightDistanceBase = MWBase::Environment::get().getWorld()->getStore().get().find( "iFightDistanceBase")->mValue.getInteger(); static const float fFightDistanceMultiplier = MWBase::Environment::get().getWorld()->getStore().get().find( "fFightDistanceMultiplier")->mValue.getFloat(); return (iFightDistanceBase - fFightDistanceMultiplier * d); } bool isTargetMagicallyHidden(const MWWorld::Ptr& target) { const MagicEffects& magicEffects = target.getClass().getCreatureStats(target).getMagicEffects(); return (magicEffects.get(ESM::MagicEffect::Invisibility).getMagnitude() > 0) || (magicEffects.get(ESM::MagicEffect::Chameleon).getMagnitude() > 75); } float getAggroDistance(const MWWorld::Ptr& actor, const osg::Vec3f& lhs, const osg::Vec3f& rhs) { if (canActorMoveByZAxis(actor)) return distanceIgnoreZ(lhs, rhs); return distance(lhs, rhs); } } ================================================ FILE: apps/openmw/mwmechanics/combat.hpp ================================================ #ifndef OPENMW_MECHANICS_COMBAT_H #define OPENMW_MECHANICS_COMBAT_H namespace osg { class Vec3f; } namespace MWWorld { class Ptr; } namespace MWMechanics { bool applyOnStrikeEnchantment(const MWWorld::Ptr& attacker, const MWWorld::Ptr& victim, const MWWorld::Ptr& object, const osg::Vec3f& hitPosition, const bool fromProjectile=false); /// @return can we block the attack? bool blockMeleeAttack (const MWWorld::Ptr& attacker, const MWWorld::Ptr& blocker, const MWWorld::Ptr& weapon, float damage, float attackStrength); /// @return does normal weapon resistance and weakness apply to the weapon? bool isNormalWeapon (const MWWorld::Ptr& weapon); void resistNormalWeapon (const MWWorld::Ptr& actor, const MWWorld::Ptr& attacker, const MWWorld::Ptr& weapon, float& damage); void applyWerewolfDamageMult (const MWWorld::Ptr& actor, const MWWorld::Ptr& weapon, float &damage); /// @note for a thrown weapon, \a weapon == \a projectile, for bows/crossbows, \a projectile is the arrow/bolt /// @note \a victim may be empty (e.g. for a hit on terrain), a non-actor (environment objects) or an actor void projectileHit (const MWWorld::Ptr& attacker, const MWWorld::Ptr& victim, MWWorld::Ptr weapon, const MWWorld::Ptr& projectile, const osg::Vec3f& hitPosition, float attackStrength); /// Get the chance (in percent) for \a attacker to successfully hit \a victim with a given weapon skill value float getHitChance (const MWWorld::Ptr& attacker, const MWWorld::Ptr& victim, int skillValue); /// Applies damage to attacker based on the victim's elemental shields. void applyElementalShields(const MWWorld::Ptr& attacker, const MWWorld::Ptr& victim); /// @param damage Unmitigated weapon damage of the attack /// @param hit Was the attack successful? /// @param weapon The weapon used. /// @note if the weapon is unequipped as result of condition damage, a new Ptr will be assigned to \a weapon. void reduceWeaponCondition (float damage, bool hit, MWWorld::Ptr& weapon, const MWWorld::Ptr& attacker); /// Adjust weapon damage based on its condition. A used weapon will be less effective. void adjustWeaponDamage (float& damage, const MWWorld::Ptr& weapon, const MWWorld::Ptr& attacker); void getHandToHandDamage (const MWWorld::Ptr& attacker, const MWWorld::Ptr& victim, float& damage, bool& healthdmg, float attackStrength); /// Apply the fatigue loss incurred by attacking with the given weapon (weapon may be empty = hand-to-hand) void applyFatigueLoss(const MWWorld::Ptr& attacker, const MWWorld::Ptr& weapon, float attackStrength); float getFightDistanceBias(const MWWorld::Ptr& actor1, const MWWorld::Ptr& actor2); bool isTargetMagicallyHidden(const MWWorld::Ptr& target); float getAggroDistance(const MWWorld::Ptr& actor, const osg::Vec3f& lhs, const osg::Vec3f& rhs); } #endif ================================================ FILE: apps/openmw/mwmechanics/creaturestats.cpp ================================================ #include "creaturestats.hpp" #include /* Start of tes3mp addition Include additional headers for multiplayer purposes */ #include #include "summoning.hpp" /* End of tes3mp addition */ #include #include #include #include "../mwworld/esmstore.hpp" #include "../mwworld/player.hpp" #include "../mwbase/environment.hpp" #include "../mwbase/world.hpp" namespace MWMechanics { int CreatureStats::sActorId = 0; CreatureStats::CreatureStats() : mDrawState (DrawState_Nothing), mDead (false), mDeathAnimationFinished(false), mDied (false), mMurdered(false), mFriendlyHits (0), mTalkedTo (false), mAlarmed (false), mAttacked (false), mKnockdown(false), mKnockdownOneFrame(false), mKnockdownOverOneFrame(false), mHitRecovery(false), mBlock(false), mMovementFlags(0), mFallHeight(0), mRecalcMagicka(false), mLastRestock(0,0), mGoldPool(0), mActorId(-1), mHitAttemptActorId(-1), mDeathAnimation(-1), mTimeOfDeath(), mSideMovementAngle(0), mLevel (0) { for (int i=0; i<4; ++i) mAiSettings[i] = 0; } const AiSequence& CreatureStats::getAiSequence() const { return mAiSequence; } AiSequence& CreatureStats::getAiSequence() { return mAiSequence; } float CreatureStats::getFatigueTerm() const { float max = getFatigue().getModified(); float current = getFatigue().getCurrent(); float normalised = floor(max) == 0 ? 1 : std::max (0.0f, current / max); const MWWorld::Store &gmst = MWBase::Environment::get().getWorld()->getStore().get(); static const float fFatigueBase = gmst.find("fFatigueBase")->mValue.getFloat(); static const float fFatigueMult = gmst.find("fFatigueMult")->mValue.getFloat(); return fFatigueBase - fFatigueMult * (1-normalised); } const AttributeValue &CreatureStats::getAttribute(int index) const { if (index < 0 || index > 7) { throw std::runtime_error("attribute index is out of range"); } return mAttributes[index]; } const DynamicStat &CreatureStats::getHealth() const { return mDynamic[0]; } const DynamicStat &CreatureStats::getMagicka() const { return mDynamic[1]; } const DynamicStat &CreatureStats::getFatigue() const { return mDynamic[2]; } const Spells &CreatureStats::getSpells() const { return mSpells; } const ActiveSpells &CreatureStats::getActiveSpells() const { return mActiveSpells; } const MagicEffects &CreatureStats::getMagicEffects() const { return mMagicEffects; } int CreatureStats::getLevel() const { return mLevel; } Stat CreatureStats::getAiSetting (AiSetting index) const { return mAiSettings[index]; } const DynamicStat &CreatureStats::getDynamic(int index) const { if (index < 0 || index > 2) { throw std::runtime_error("dynamic stat index is out of range"); } return mDynamic[index]; } Spells &CreatureStats::getSpells() { return mSpells; } ActiveSpells &CreatureStats::getActiveSpells() { /* Start of tes3mp addition Set the actorId associated with these ActiveSpells so it can be used inside them */ mActiveSpells.setActorId(getActorId()); /* End of tes3mp addition */ return mActiveSpells; } MagicEffects &CreatureStats::getMagicEffects() { return mMagicEffects; } void CreatureStats::setAttribute(int index, float base) { AttributeValue current = getAttribute(index); current.setBase(base); setAttribute(index, current); } void CreatureStats::setAttribute(int index, const AttributeValue &value) { if (index < 0 || index > 7) { throw std::runtime_error("attribute index is out of range"); } const AttributeValue& currentValue = mAttributes[index]; if (value != currentValue) { mAttributes[index] = value; if (index == ESM::Attribute::Intelligence) mRecalcMagicka = true; else if (index == ESM::Attribute::Strength || index == ESM::Attribute::Willpower || index == ESM::Attribute::Agility || index == ESM::Attribute::Endurance) { float strength = getAttribute(ESM::Attribute::Strength).getModified(); float willpower = getAttribute(ESM::Attribute::Willpower).getModified(); float agility = getAttribute(ESM::Attribute::Agility).getModified(); float endurance = getAttribute(ESM::Attribute::Endurance).getModified(); DynamicStat fatigue = getFatigue(); float diff = (strength+willpower+agility+endurance) - fatigue.getBase(); float currentToBaseRatio = fatigue.getBase() > 0 ? (fatigue.getCurrent() / fatigue.getBase()) : 0; fatigue.setModified(fatigue.getModified() + diff, 0); fatigue.setCurrent(fatigue.getBase() * currentToBaseRatio); setFatigue(fatigue); } } } void CreatureStats::setHealth(const DynamicStat &value) { setDynamic (0, value); } void CreatureStats::setMagicka(const DynamicStat &value) { setDynamic (1, value); } void CreatureStats::setFatigue(const DynamicStat &value) { setDynamic (2, value); } void CreatureStats::setDynamic (int index, const DynamicStat &value) { if (index < 0 || index > 2) throw std::runtime_error("dynamic stat index is out of range"); mDynamic[index] = value; if (index==0 && mDynamic[index].getCurrent()<1) { if (!mDead) mTimeOfDeath = MWBase::Environment::get().getWorld()->getTimeStamp(); mDead = true; mDynamic[index].setModifier(0); mDynamic[index].setCurrentModifier(0); mDynamic[index].setCurrent(0); } } void CreatureStats::setLevel(int level) { mLevel = level; } void CreatureStats::modifyMagicEffects(const MagicEffects &effects) { if (effects.get(ESM::MagicEffect::FortifyMaximumMagicka).getModifier() != mMagicEffects.get(ESM::MagicEffect::FortifyMaximumMagicka).getModifier()) mRecalcMagicka = true; mMagicEffects.setModifiers(effects); } void CreatureStats::setAiSetting (AiSetting index, Stat value) { mAiSettings[index] = value; } void CreatureStats::setAiSetting (AiSetting index, int base) { Stat stat = getAiSetting(index); stat.setBase(base); setAiSetting(index, stat); } bool CreatureStats::isParalyzed() const { return mMagicEffects.get(ESM::MagicEffect::Paralyze).getMagnitude() > 0; } bool CreatureStats::isDead() const { return mDead; } bool CreatureStats::isDeathAnimationFinished() const { return mDeathAnimationFinished; } void CreatureStats::setDeathAnimationFinished(bool finished) { mDeathAnimationFinished = finished; } void CreatureStats::notifyDied() { mDied = true; } bool CreatureStats::hasDied() const { return mDied; } void CreatureStats::clearHasDied() { mDied = false; } bool CreatureStats::hasBeenMurdered() const { return mMurdered; } void CreatureStats::notifyMurder() { mMurdered = true; } void CreatureStats::clearHasBeenMurdered() { mMurdered = false; } void CreatureStats::resurrect() { if (mDead) { if (mDynamic[0].getModified() < 1) mDynamic[0].setModified(1, 0); mDynamic[0].setCurrent(mDynamic[0].getModified()); mDead = false; mDeathAnimationFinished = false; } } bool CreatureStats::hasCommonDisease() const { return mSpells.hasCommonDisease(); } bool CreatureStats::hasBlightDisease() const { return mSpells.hasBlightDisease(); } int CreatureStats::getFriendlyHits() const { return mFriendlyHits; } /* Start of tes3mp addition Make it possible to set the number of friendly hits from elsewhere */ void CreatureStats::setFriendlyHits(int hits) { mFriendlyHits = hits; } /* End of tes3mp addition */ void CreatureStats::friendlyHit() { ++mFriendlyHits; } bool CreatureStats::hasTalkedToPlayer() const { return mTalkedTo; } void CreatureStats::talkedToPlayer() { mTalkedTo = true; } bool CreatureStats::isAlarmed() const { return mAlarmed; } void CreatureStats::setAlarmed (bool alarmed) { mAlarmed = alarmed; } bool CreatureStats::getAttacked() const { return mAttacked; } void CreatureStats::setAttacked (bool attacked) { mAttacked = attacked; } float CreatureStats::getEvasion() const { float evasion = (getAttribute(ESM::Attribute::Agility).getModified() / 5.0f) + (getAttribute(ESM::Attribute::Luck).getModified() / 10.0f); evasion *= getFatigueTerm(); evasion += std::min(100.f, mMagicEffects.get(ESM::MagicEffect::Sanctuary).getMagnitude()); return evasion; } void CreatureStats::setLastHitObject(const std::string& objectid) { mLastHitObject = objectid; } const std::string &CreatureStats::getLastHitObject() const { return mLastHitObject; } void CreatureStats::setLastHitAttemptObject(const std::string& objectid) { mLastHitAttemptObject = objectid; } const std::string &CreatureStats::getLastHitAttemptObject() const { return mLastHitAttemptObject; } void CreatureStats::setHitAttemptActorId(int actorId) { mHitAttemptActorId = actorId; } int CreatureStats::getHitAttemptActorId() const { return mHitAttemptActorId; } void CreatureStats::addToFallHeight(float height) { mFallHeight += height; } float CreatureStats::getFallHeight() const { return mFallHeight; } float CreatureStats::land(bool isPlayer) { if (isPlayer) MWBase::Environment::get().getWorld()->getPlayer().setJumping(false); float height = mFallHeight; mFallHeight = 0; return height; } bool CreatureStats::needToRecalcDynamicStats() { if (mRecalcMagicka) { mRecalcMagicka = false; return true; } return false; } void CreatureStats::setNeedRecalcDynamicStats(bool val) { mRecalcMagicka = val; } void CreatureStats::setKnockedDown(bool value) { mKnockdown = value; if(!value) //Resets the "OverOneFrame" flag setKnockedDownOverOneFrame(false); } bool CreatureStats::getKnockedDown() const { return mKnockdown; } void CreatureStats::setKnockedDownOneFrame(bool value) { mKnockdownOneFrame = value; } bool CreatureStats::getKnockedDownOneFrame() const { return mKnockdownOneFrame; } void CreatureStats::setKnockedDownOverOneFrame(bool value) { mKnockdownOverOneFrame = value; } bool CreatureStats::getKnockedDownOverOneFrame() const { return mKnockdownOverOneFrame; } void CreatureStats::setHitRecovery(bool value) { mHitRecovery = value; } bool CreatureStats::getHitRecovery() const { return mHitRecovery; } void CreatureStats::setBlock(bool value) { mBlock = value; } bool CreatureStats::getBlock() const { return mBlock; } bool CreatureStats::getMovementFlag (Flag flag) const { return (mMovementFlags & flag) != 0; } void CreatureStats::setMovementFlag (Flag flag, bool state) { if (state) mMovementFlags |= flag; else mMovementFlags &= ~flag; } bool CreatureStats::getStance(Stance flag) const { switch (flag) { case Stance_Run: return getMovementFlag (Flag_Run) || getMovementFlag (Flag_ForceRun); case Stance_Sneak: return getMovementFlag (Flag_Sneak) || getMovementFlag (Flag_ForceSneak); default: return false; } } DrawState_ CreatureStats::getDrawState() const { return mDrawState; } void CreatureStats::setDrawState(DrawState_ state) { mDrawState = state; } void CreatureStats::writeState (ESM::CreatureStats& state) const { for (int i=0; ifirst].mWorsenings[i] = mCorprusSpells.at(it->first).mWorsenings[i]; state.mCorprusSpells[it->first].mNextWorsening = mCorprusSpells.at(it->first).mNextWorsening.toEsm(); } } void CreatureStats::readState (const ESM::CreatureStats& state) { for (int i=0; i 0) mBoundItems.insert(effectId); else { // Check active spell effects // We can't use mActiveSpells::getMagicEffects here because it doesn't include expired effects auto spell = std::find_if(mActiveSpells.begin(), mActiveSpells.end(), [&] (const auto& spell) { const auto& effects = spell.second.mEffects; return std::find_if(effects.begin(), effects.end(), [&] (const auto& effect) { return effect.mEffectId == effectId; }) != effects.end(); }); if(spell != mActiveSpells.end()) mBoundItems.insert(effectId); } } mSummonedCreatures = state.mSummonedCreatureMap; mSummonGraveyard = state.mSummonGraveyard; if (state.mHasAiSettings) for (int i=0; i<4; ++i) mAiSettings[i].readState(state.mAiSettings[i]); mCorprusSpells.clear(); for (auto it = state.mCorprusSpells.begin(); it != state.mCorprusSpells.end(); ++it) { for (int i=0; ifirst].mWorsenings[i] = state.mCorprusSpells.at(it->first).mWorsenings[i]; mCorprusSpells[it->first].mNextWorsening = MWWorld::TimeStamp(state.mCorprusSpells.at(it->first).mNextWorsening); } } void CreatureStats::setLastRestockTime(MWWorld::TimeStamp tradeTime) { mLastRestock = tradeTime; } MWWorld::TimeStamp CreatureStats::getLastRestockTime() const { return mLastRestock; } void CreatureStats::setGoldPool(int pool) { mGoldPool = pool; } int CreatureStats::getGoldPool() const { return mGoldPool; } int CreatureStats::getActorId() { if (mActorId==-1) mActorId = sActorId++; return mActorId; } bool CreatureStats::matchesActorId (int id) const { return mActorId!=-1 && id==mActorId; } void CreatureStats::cleanup() { sActorId = 0; } void CreatureStats::writeActorIdCounter (ESM::ESMWriter& esm) { esm.startRecord(ESM::REC_ACTC); esm.writeHNT("COUN", sActorId); esm.endRecord(ESM::REC_ACTC); } void CreatureStats::readActorIdCounter (ESM::ESMReader& esm) { esm.getHNT(sActorId, "COUN"); } signed char CreatureStats::getDeathAnimation() const { return mDeathAnimation; } void CreatureStats::setDeathAnimation(signed char index) { mDeathAnimation = index; } MWWorld::TimeStamp CreatureStats::getTimeOfDeath() const { return mTimeOfDeath; } std::map& CreatureStats::getSummonedCreatureMap() { return mSummonedCreatures; } std::vector& CreatureStats::getSummonedCreatureGraveyard() { return mSummonGraveyard; } /* Start of tes3mp addition Make it possible to set a new actorId for summoned creatures, necessary for properly initializing them after syncing them across players */ void CreatureStats::setSummonedCreatureActorId(std::string refId, int actorId) { for (std::map::iterator it = mSummonedCreatures.begin(); it != mSummonedCreatures.end(); ) { if (Misc::StringUtils::ciEqual(getSummonedCreature(it->first.mEffectId), refId) && it->second == -1) { it->second = actorId; break; } else ++it; } } /* End of tes3mp addition */ std::map &CreatureStats::getCorprusSpells() { return mCorprusSpells; } void CreatureStats::addCorprusSpell(const std::string& sourceId, CorprusStats& stats) { mCorprusSpells[sourceId] = stats; } void CreatureStats::removeCorprusSpell(const std::string& sourceId) { auto corprusIt = mCorprusSpells.find(sourceId); if (corprusIt != mCorprusSpells.end()) { mCorprusSpells.erase(corprusIt); } } } ================================================ FILE: apps/openmw/mwmechanics/creaturestats.hpp ================================================ #ifndef GAME_MWMECHANICS_CREATURESTATS_H #define GAME_MWMECHANICS_CREATURESTATS_H #include #include #include #include "stat.hpp" #include "magiceffects.hpp" #include "spells.hpp" #include "activespells.hpp" #include "aisequence.hpp" #include "drawstate.hpp" #include #include namespace ESM { struct CreatureStats; } namespace MWMechanics { struct CorprusStats { static constexpr int sWorseningPeriod = 24; int mWorsenings[ESM::Attribute::Length]; MWWorld::TimeStamp mNextWorsening; }; /// \brief Common creature stats /// /// class CreatureStats { static int sActorId; DrawState_ mDrawState; AttributeValue mAttributes[ESM::Attribute::Length]; DynamicStat mDynamic[3]; // health, magicka, fatigue Spells mSpells; ActiveSpells mActiveSpells; MagicEffects mMagicEffects; Stat mAiSettings[4]; AiSequence mAiSequence; bool mDead; bool mDeathAnimationFinished; bool mDied; // flag for OnDeath script function bool mMurdered; int mFriendlyHits; bool mTalkedTo; bool mAlarmed; bool mAttacked; bool mKnockdown; bool mKnockdownOneFrame; bool mKnockdownOverOneFrame; bool mHitRecovery; bool mBlock; unsigned int mMovementFlags; float mFallHeight; std::string mLastHitObject; // The last object to hit this actor std::string mLastHitAttemptObject; // The last object to attempt to hit this actor bool mRecalcMagicka; // For merchants: the last time items were restocked and gold pool refilled. MWWorld::TimeStamp mLastRestock; // The pool of merchant gold (not in inventory) int mGoldPool; int mActorId; int mHitAttemptActorId; // Stores an actor that attacked this actor. Only one is stored at a time, // and it is not changed if a different actor attacks. It is cleared when combat ends. // The index of the death animation that was played, or -1 if none played signed char mDeathAnimation; MWWorld::TimeStamp mTimeOfDeath; // The difference between view direction and lower body direction. float mSideMovementAngle; private: std::map mSummonedCreatures; // // Contains ActorIds of summoned creatures with an expired lifetime that have not been deleted yet. // This may be necessary when the creature is in an inactive cell. std::vector mSummonGraveyard; std::map mCorprusSpells; protected: int mLevel; public: CreatureStats(); DrawState_ getDrawState() const; void setDrawState(DrawState_ state); bool needToRecalcDynamicStats(); void setNeedRecalcDynamicStats(bool val); float getFallHeight() const; void addToFallHeight(float height); /// Reset the fall height /// @return total fall height float land(bool isPlayer=false); const AttributeValue & getAttribute(int index) const; const DynamicStat & getHealth() const; const DynamicStat & getMagicka() const; const DynamicStat & getFatigue() const; const DynamicStat & getDynamic (int index) const; const Spells & getSpells() const; const ActiveSpells & getActiveSpells() const; const MagicEffects & getMagicEffects() const; bool getAttackingOrSpell() const; int getLevel() const; Spells & getSpells(); ActiveSpells & getActiveSpells(); MagicEffects & getMagicEffects(); void setAttribute(int index, const AttributeValue &value); // Shortcut to set only the base void setAttribute(int index, float base); void setHealth(const DynamicStat &value); void setMagicka(const DynamicStat &value); void setFatigue(const DynamicStat &value); void setDynamic (int index, const DynamicStat &value); /// Set Modifier for each magic effect according to \a effects. Does not touch Base values. void modifyMagicEffects(const MagicEffects &effects); void setAttackingOrSpell(bool attackingOrSpell); void setLevel(int level); enum AiSetting { AI_Hello = 0, AI_Fight = 1, AI_Flee = 2, AI_Alarm = 3 }; void setAiSetting (AiSetting index, Stat value); void setAiSetting (AiSetting index, int base); Stat getAiSetting (AiSetting index) const; const AiSequence& getAiSequence() const; AiSequence& getAiSequence(); float getFatigueTerm() const; ///< Return effective fatigue bool isParalyzed() const; bool isDead() const; bool isDeathAnimationFinished() const; void setDeathAnimationFinished(bool finished); void notifyDied(); bool hasDied() const; void clearHasDied(); bool hasBeenMurdered() const; void clearHasBeenMurdered(); void notifyMurder(); void resurrect(); bool hasCommonDisease() const; bool hasBlightDisease() const; int getFriendlyHits() const; ///< Number of friendly hits received. /* Start of tes3mp addition Make it possible to set the number of friendly hits from elsewhere */ void setFriendlyHits(int hits); /* End of tes3mp addition */ void friendlyHit(); ///< Increase number of friendly hits by one. bool hasTalkedToPlayer() const; ///< Has this creature talked with the player before? void talkedToPlayer(); bool isAlarmed() const; void setAlarmed (bool alarmed); bool getAttacked() const; void setAttacked (bool attacked); float getEvasion() const; void setKnockedDown(bool value); /// Returns true for the entire duration of the actor being knocked down or knocked out, /// including transition animations (falling down & standing up) bool getKnockedDown() const; void setKnockedDownOneFrame(bool value); ///Returns true only for the first frame of the actor being knocked out; used for "onKnockedOut" command bool getKnockedDownOneFrame() const; void setKnockedDownOverOneFrame(bool value); ///Returns true for all but the first frame of being knocked out; used to know to not reset mKnockedDownOneFrame bool getKnockedDownOverOneFrame() const; void setHitRecovery(bool value); bool getHitRecovery() const; void setBlock(bool value); bool getBlock() const; std::map& getSummonedCreatureMap(); // std::vector& getSummonedCreatureGraveyard(); // ActorIds /* Start of tes3mp addition Make it possible to set a new actorId for summoned creatures, necessary for properly initializing them after syncing them across players */ void setSummonedCreatureActorId(std::string refId, int actorId); /* End of tes3mp addition */ enum Flag { Flag_ForceRun = 1, Flag_ForceSneak = 2, Flag_Run = 4, Flag_Sneak = 8, Flag_ForceJump = 16, Flag_ForceMoveJump = 32 }; enum Stance { Stance_Run, Stance_Sneak }; bool getMovementFlag (Flag flag) const; void setMovementFlag (Flag flag, bool state); /// Like getMovementFlag, but also takes into account if the flag is Forced bool getStance (Stance flag) const; void setLastHitObject(const std::string &objectid); const std::string &getLastHitObject() const; void setLastHitAttemptObject(const std::string &objectid); const std::string &getLastHitAttemptObject() const; void setHitAttemptActorId(const int actorId); int getHitAttemptActorId() const; // Note, this is just a cache to avoid checking the whole container store every frame. We don't need to store it in saves. // TODO: Put it somewhere else? std::set mBoundItems; void writeState (ESM::CreatureStats& state) const; void readState (const ESM::CreatureStats& state); static void writeActorIdCounter (ESM::ESMWriter& esm); static void readActorIdCounter (ESM::ESMReader& esm); void setLastRestockTime(MWWorld::TimeStamp tradeTime); MWWorld::TimeStamp getLastRestockTime() const; void setGoldPool(int pool); int getGoldPool() const; signed char getDeathAnimation() const; // -1 means not decided void setDeathAnimation(signed char index); MWWorld::TimeStamp getTimeOfDeath() const; int getActorId(); ///< Will generate an actor ID, if the actor does not have one yet. bool matchesActorId (int id) const; ///< Check if \a id matches the actor ID of *this (if the actor does not have an ID /// assigned this function will return false). static void cleanup(); std::map & getCorprusSpells(); void addCorprusSpell(const std::string& sourceId, CorprusStats& stats); void removeCorprusSpell(const std::string& sourceId); float getSideMovementAngle() const { return mSideMovementAngle; } void setSideMovementAngle(float angle) { mSideMovementAngle = angle; } }; } #endif ================================================ FILE: apps/openmw/mwmechanics/difficultyscaling.cpp ================================================ #include "difficultyscaling.hpp" #include /* Start of tes3mp addition Include additional headers for multiplayer purposes */ #include "../mwmp/Main.hpp" #include "../mwmp/LocalPlayer.hpp" /* End of tes3mp addition */ #include "../mwbase/world.hpp" #include "../mwbase/environment.hpp" #include "../mwworld/esmstore.hpp" #include "actorutil.hpp" float scaleDamage(float damage, const MWWorld::Ptr& attacker, const MWWorld::Ptr& victim) { 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); /* Start of tes3mp change (major) Use difficulty setting received from server instead of basing it on client settings */ difficultySetting = mwmp::Main::get().getLocalPlayer()->difficulty; /* End of tes3mp change (major) */ static const float fDifficultyMult = MWBase::Environment::get().getWorld()->getStore().get().find("fDifficultyMult")->mValue.getFloat(); float difficultyTerm = 0.01f * difficultySetting; float x = 0; if (victim == player) { if (difficultyTerm > 0) x = fDifficultyMult * difficultyTerm; else x = difficultyTerm / fDifficultyMult; } else if (attacker == player) { if (difficultyTerm > 0) x = -difficultyTerm / fDifficultyMult; else x = fDifficultyMult * (-difficultyTerm); } damage *= 1 + x; return damage; } ================================================ FILE: apps/openmw/mwmechanics/difficultyscaling.hpp ================================================ #ifndef OPENMW_MWMECHANICS_DIFFICULTYSCALING_H #define OPENMW_MWMECHANICS_DIFFICULTYSCALING_H namespace MWWorld { class Ptr; } /// Scales damage dealt to an actor based on difficulty setting float scaleDamage(float damage, const MWWorld::Ptr& attacker, const MWWorld::Ptr& victim); #endif ================================================ FILE: apps/openmw/mwmechanics/disease.hpp ================================================ #ifndef OPENMW_MECHANICS_DISEASE_H #define OPENMW_MECHANICS_DISEASE_H #include /* Start of tes3mp addition Include additional headers for multiplayer purposes */ #include "../mwmp/Main.hpp" #include "../mwmp/LocalPlayer.hpp" /* End of tes3mp addition */ #include "../mwbase/windowmanager.hpp" #include "../mwbase/environment.hpp" #include "../mwbase/world.hpp" #include "../mwworld/ptr.hpp" #include "../mwworld/class.hpp" #include "../mwworld/esmstore.hpp" #include "spells.hpp" #include "creaturestats.hpp" #include "actorutil.hpp" namespace MWMechanics { /// Call when \a actor has got in contact with \a carrier (e.g. hit by him, or loots him) /// @param actor The actor that will potentially catch diseases. Currently only the player can catch diseases. /// @param carrier The disease carrier. inline void diseaseContact (MWWorld::Ptr actor, MWWorld::Ptr carrier) { if (!carrier.getClass().isActor() || actor != getPlayer()) return; float fDiseaseXferChance = MWBase::Environment::get().getWorld()->getStore().get().find( "fDiseaseXferChance")->mValue.getFloat(); MagicEffects& actorEffects = actor.getClass().getCreatureStats(actor).getMagicEffects(); Spells& spells = carrier.getClass().getCreatureStats(carrier).getSpells(); for (Spells::TIterator it = spells.begin(); it != spells.end(); ++it) { const ESM::Spell* spell = it->first; if (actor.getClass().getCreatureStats(actor).getSpells().hasSpell(spell->mId)) continue; float resist = 0.f; if (Spells::hasCorprusEffect(spell)) resist = 1.f - 0.01f * (actorEffects.get(ESM::MagicEffect::ResistCorprusDisease).getMagnitude() - actorEffects.get(ESM::MagicEffect::WeaknessToCorprusDisease).getMagnitude()); else if (spell->mData.mType == ESM::Spell::ST_Disease) resist = 1.f - 0.01f * (actorEffects.get(ESM::MagicEffect::ResistCommonDisease).getMagnitude() - actorEffects.get(ESM::MagicEffect::WeaknessToCommonDisease).getMagnitude()); else if (spell->mData.mType == ESM::Spell::ST_Blight) resist = 1.f - 0.01f * (actorEffects.get(ESM::MagicEffect::ResistBlightDisease).getMagnitude() - actorEffects.get(ESM::MagicEffect::WeaknessToBlightDisease).getMagnitude()); else continue; int x = static_cast(fDiseaseXferChance * 100 * resist); if (Misc::Rng::rollDice(10000) < x) { // Contracted disease! actor.getClass().getCreatureStats(actor).getSpells().add(it->first); MWBase::Environment::get().getWorld()->applyLoopingParticles(actor); /* Start of tes3mp addition Send an ID_PLAYER_SPELLBOOK packet every time a player gains a disease */ mwmp::Main::get().getLocalPlayer()->sendSpellChange(it->first->mId, mwmp::SpellbookChanges::ADD); /* End of tes3mp addition */ std::string msg = "sMagicContractDisease"; msg = MWBase::Environment::get().getWorld()->getStore().get().find(msg)->mValue.getString(); msg = Misc::StringUtils::format(msg, spell->mName); MWBase::Environment::get().getWindowManager()->messageBox(msg); } } } } #endif ================================================ FILE: apps/openmw/mwmechanics/drawstate.hpp ================================================ #ifndef GAME_MWMECHANICS_DRAWSTATE_H #define GAME_MWMECHANICS_DRAWSTATE_H namespace MWMechanics { /// \note The _ suffix is required to avoid a collision with a Windoze macro. Die, Microsoft! Die! enum DrawState_ { DrawState_Nothing = 0, DrawState_Weapon = 1, DrawState_Spell = 2 }; } #endif ================================================ FILE: apps/openmw/mwmechanics/enchanting.cpp ================================================ #include "enchanting.hpp" #include #include /* Start of tes3mp addition Include additional headers for multiplayer purposes */ #include #include "../mwmp/Main.hpp" #include "../mwmp/Networking.hpp" #include "../mwmp/LocalPlayer.hpp" #include "../mwmp/Worldstate.hpp" /* End of tes3mp addition */ #include "../mwworld/manualref.hpp" #include "../mwworld/class.hpp" #include "../mwworld/containerstore.hpp" #include "../mwworld/esmstore.hpp" #include "../mwbase/world.hpp" #include "../mwbase/environment.hpp" #include "../mwbase/mechanicsmanager.hpp" #include "creaturestats.hpp" #include "spellutil.hpp" #include "actorutil.hpp" #include "weapontype.hpp" namespace MWMechanics { Enchanting::Enchanting() : mCastStyle(ESM::Enchantment::CastOnce) , mSelfEnchanting(false) , mWeaponType(-1) {} void Enchanting::setOldItem(const MWWorld::Ptr& oldItem) { mOldItemPtr=oldItem; mWeaponType = -1; mObjectType.clear(); if(!itemEmpty()) { mObjectType = mOldItemPtr.getTypeName(); if (mObjectType == typeid(ESM::Weapon).name()) mWeaponType = mOldItemPtr.get()->mBase->mData.mType; } } void Enchanting::setNewItemName(const std::string& s) { mNewItemName=s; } void Enchanting::setEffect(const ESM::EffectList& effectList) { mEffectList=effectList; } int Enchanting::getCastStyle() const { return mCastStyle; } void Enchanting::setSoulGem(const MWWorld::Ptr& soulGem) { mSoulGemPtr=soulGem; } bool Enchanting::create() { const MWWorld::Ptr& player = getPlayer(); MWWorld::ContainerStore& store = player.getClass().getContainerStore(player); ESM::Enchantment enchantment; enchantment.mData.mFlags = 0; enchantment.mData.mType = mCastStyle; enchantment.mData.mCost = getBaseCastCost(); store.remove(mSoulGemPtr, 1, player); //Exception for Azura Star, new one will be added after enchanting if(Misc::StringUtils::ciEqual(mSoulGemPtr.get()->mBase->mId, "Misc_SoulGem_Azura")) store.add("Misc_SoulGem_Azura", 1, player); if(mSelfEnchanting) { if(getEnchantChance() <= (Misc::Rng::roll0to99())) return false; mEnchanter.getClass().skillUsageSucceeded (mEnchanter, ESM::Skill::Enchant, 2); } enchantment.mEffects = mEffectList; int count = getEnchantItemsCount(); if(mCastStyle==ESM::Enchantment::ConstantEffect) enchantment.mData.mCharge = 0; else enchantment.mData.mCharge = getGemCharge() / count; // Try to find a dynamic enchantment with the same stats, create a new one if not found. const ESM::Enchantment* enchantmentPtr = getRecord(enchantment); if (enchantmentPtr == nullptr) enchantmentPtr = MWBase::Environment::get().getWorld()->createRecord (enchantment); // Apply the enchantment /* Start of tes3mp change (major) Send the enchantment's record to the server Don't add the new item to the player's inventory and instead expect the server to add it Store the quantity used for the enchantment so it can be retrieved in applyEnchantment() when applicable The applyEnchantment() method is where the record of the newly enchanted item will be sent to the server, causing the server to send back the player's inventory with the new item included */ mwmp::Main::get().getNetworking()->getWorldstate()->sendEnchantmentRecord(enchantmentPtr); store.remove(mOldItemPtr, count, player); if(!mSelfEnchanting) payForEnchantment(); mwmp::Main::get().getLocalPlayer()->storeLastEnchantmentQuantity(count); std::string newItemId = mOldItemPtr.getClass().applyEnchantment(mOldItemPtr, enchantmentPtr->mId, getGemCharge(), mNewItemName); /* End of tes3mp change (major) */ return true; } void Enchanting::nextCastStyle() { if (itemEmpty()) return; const bool powerfulSoul = getGemCharge() >= \ MWBase::Environment::get().getWorld()->getStore().get().find ("iSoulAmountForConstantEffect")->mValue.getInteger(); if ((mObjectType == typeid(ESM::Armor).name()) || (mObjectType == typeid(ESM::Clothing).name())) { // Armor or Clothing switch(mCastStyle) { case ESM::Enchantment::WhenUsed: if (powerfulSoul) mCastStyle = ESM::Enchantment::ConstantEffect; return; default: // takes care of Constant effect too mCastStyle = ESM::Enchantment::WhenUsed; return; } } else if (mWeaponType != -1) { // Weapon ESM::WeaponType::Class weapclass = MWMechanics::getWeaponType(mWeaponType)->mWeaponClass; switch(mCastStyle) { case ESM::Enchantment::WhenStrikes: if (weapclass == ESM::WeaponType::Melee || weapclass == ESM::WeaponType::Ranged) mCastStyle = ESM::Enchantment::WhenUsed; return; case ESM::Enchantment::WhenUsed: if (powerfulSoul && weapclass != ESM::WeaponType::Ammo && weapclass != ESM::WeaponType::Thrown) mCastStyle = ESM::Enchantment::ConstantEffect; else if (weapclass != ESM::WeaponType::Ranged) mCastStyle = ESM::Enchantment::WhenStrikes; return; default: // takes care of Constant effect too mCastStyle = ESM::Enchantment::WhenUsed; if (weapclass != ESM::WeaponType::Ranged) mCastStyle = ESM::Enchantment::WhenStrikes; return; } } else if(mObjectType == typeid(ESM::Book).name()) { // Scroll or Book mCastStyle = ESM::Enchantment::CastOnce; return; } // Fail case mCastStyle = ESM::Enchantment::CastOnce; } /* * Vanilla enchant cost formula: * * Touch/Self: (min + max) * baseCost * 0.025 * duration + area * baseCost * 0.025 * Target: 1.5 * ((min + max) * baseCost * 0.025 * duration + area * baseCost * 0.025) * Constant eff: (min + max) * baseCost * 2.5 + area * baseCost * 0.025 * * For multiple effects - cost of each effect is multiplied by number of effects that follows +1. * * Note: Minimal value inside formula for 'min' and 'max' is 1. So in vanilla: * (0 + 0) == (1 + 0) == (1 + 1) => 2 or (2 + 0) == (1 + 2) => 3 * * Formula on UESPWiki is not entirely correct. */ float Enchanting::getEnchantPoints(bool precise) const { if (mEffectList.mList.empty()) // No effects added, cost = 0 return 0; const MWWorld::ESMStore &store = MWBase::Environment::get().getWorld()->getStore(); const float fEffectCostMult = store.get().find("fEffectCostMult")->mValue.getFloat(); const float fEnchantmentConstantDurationMult = store.get().find("fEnchantmentConstantDurationMult")->mValue.getFloat(); float enchantmentCost = 0.f; float cost = 0.f; for (const ESM::ENAMstruct& effect : mEffectList.mList) { float baseCost = (store.get().find(effect.mEffectID))->mData.mBaseCost; int magMin = std::max(1, effect.mMagnMin); int magMax = std::max(1, effect.mMagnMax); int area = std::max(1, effect.mArea); float duration = static_cast(effect.mDuration); if (mCastStyle == ESM::Enchantment::ConstantEffect) duration = fEnchantmentConstantDurationMult; cost += ((magMin + magMax) * duration + area) * baseCost * fEffectCostMult * 0.05f; cost = std::max(1.f, cost); if (effect.mRange == ESM::RT_Target) cost *= 1.5f; enchantmentCost += precise ? cost : std::floor(cost); } return enchantmentCost; } const ESM::Enchantment* Enchanting::getRecord(const ESM::Enchantment& toFind) const { const MWWorld::Store& enchantments = MWBase::Environment::get().getWorld()->getStore().get(); MWWorld::Store::iterator iter (enchantments.begin()); iter += (enchantments.getSize() - enchantments.getDynamicSize()); for (; iter != enchantments.end(); ++iter) { if (iter->mEffects.mList.size() != toFind.mEffects.mList.size()) continue; if (iter->mData.mFlags != toFind.mData.mFlags || iter->mData.mType != toFind.mData.mType || iter->mData.mCost != toFind.mData.mCost || iter->mData.mCharge != toFind.mData.mCharge) continue; // Don't choose an ID that came from the content files, would have unintended side effects if (!enchantments.isDynamic(iter->mId)) continue; bool mismatch = false; for (int i=0; i (iter->mEffects.mList.size()); ++i) { const ESM::ENAMstruct& first = iter->mEffects.mList[i]; const ESM::ENAMstruct& second = toFind.mEffects.mList[i]; if (first.mEffectID!=second.mEffectID || first.mArea!=second.mArea || first.mRange!=second.mRange || first.mSkill!=second.mSkill || first.mAttribute!=second.mAttribute || first.mMagnMin!=second.mMagnMin || first.mMagnMax!=second.mMagnMax || first.mDuration!=second.mDuration) { mismatch = true; break; } } if (!mismatch) return &(*iter); } return nullptr; } int Enchanting::getBaseCastCost() const { if (mCastStyle == ESM::Enchantment::ConstantEffect) return 0; return static_cast(getEnchantPoints(false)); } int Enchanting::getEffectiveCastCost() const { int baseCost = getBaseCastCost(); MWWorld::Ptr player = getPlayer(); return getEffectiveEnchantmentCastCost(static_cast(baseCost), player); } int Enchanting::getEnchantPrice() const { if(mEnchanter.isEmpty()) return 0; float priceMultipler = MWBase::Environment::get().getWorld()->getStore().get().find ("fEnchantmentValueMult")->mValue.getFloat(); int price = MWBase::Environment::get().getMechanicsManager()->getBarterOffer(mEnchanter, static_cast(getEnchantPoints() * priceMultipler), true); price *= getEnchantItemsCount() * getTypeMultiplier(); return std::max(1, price); } int Enchanting::getGemCharge() const { const MWWorld::ESMStore &store = MWBase::Environment::get().getWorld()->getStore(); if(soulEmpty()) return 0; if(mSoulGemPtr.getCellRef().getSoul()=="") return 0; const ESM::Creature* soul = store.get().search(mSoulGemPtr.getCellRef().getSoul()); if(soul) return soul->mData.mSoul; else return 0; } int Enchanting::getMaxEnchantValue() const { if (itemEmpty()) return 0; const MWWorld::ESMStore &store = MWBase::Environment::get().getWorld()->getStore(); return static_cast(mOldItemPtr.getClass().getEnchantmentPoints(mOldItemPtr) * store.get().find("fEnchantmentMult")->mValue.getFloat()); } bool Enchanting::soulEmpty() const { return mSoulGemPtr.isEmpty(); } bool Enchanting::itemEmpty() const { return mOldItemPtr.isEmpty(); } void Enchanting::setSelfEnchanting(bool selfEnchanting) { mSelfEnchanting = selfEnchanting; } void Enchanting::setEnchanter(const MWWorld::Ptr& enchanter) { mEnchanter = enchanter; // Reset cast style mCastStyle = ESM::Enchantment::CastOnce; } int Enchanting::getEnchantChance() const { const CreatureStats& stats = mEnchanter.getClass().getCreatureStats(mEnchanter); const MWWorld::Store& gmst = MWBase::Environment::get().getWorld()->getStore().get(); const float a = static_cast(mEnchanter.getClass().getSkill(mEnchanter, ESM::Skill::Enchant)); const float b = static_cast(stats.getAttribute (ESM::Attribute::Intelligence).getModified()); const float c = static_cast(stats.getAttribute (ESM::Attribute::Luck).getModified()); const float fEnchantmentChanceMult = gmst.find("fEnchantmentChanceMult")->mValue.getFloat(); const float fEnchantmentConstantChanceMult = gmst.find("fEnchantmentConstantChanceMult")->mValue.getFloat(); float x = (a - getEnchantPoints() * fEnchantmentChanceMult * getTypeMultiplier() * getEnchantItemsCount() + 0.2f * b + 0.1f * c) * stats.getFatigueTerm(); if (mCastStyle == ESM::Enchantment::ConstantEffect) x *= fEnchantmentConstantChanceMult; return static_cast(x); } int Enchanting::getEnchantItemsCount() const { int count = 1; float enchantPoints = getEnchantPoints(); if (mWeaponType != -1 && enchantPoints > 0) { 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"))); 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))); } } return count; } float Enchanting::getTypeMultiplier() const { static const bool useMultiplier = Settings::Manager::getFloat("projectiles enchant multiplier", "Game") > 0; if (useMultiplier && mWeaponType != -1 && getEnchantPoints() > 0) { ESM::WeaponType::Class weapclass = MWMechanics::getWeaponType(mWeaponType)->mWeaponClass; if (weapclass == ESM::WeaponType::Thrown || weapclass == ESM::WeaponType::Ammo) return 0.125f; } return 1.f; } void Enchanting::payForEnchantment() const { const MWWorld::Ptr& player = getPlayer(); MWWorld::ContainerStore& store = player.getClass().getContainerStore(player); store.remove(MWWorld::ContainerStore::sGoldId, getEnchantPrice(), player); // add gold to NPC trading gold pool CreatureStats& enchanterStats = mEnchanter.getClass().getCreatureStats(mEnchanter); enchanterStats.setGoldPool(enchanterStats.getGoldPool() + getEnchantPrice()); } } ================================================ FILE: apps/openmw/mwmechanics/enchanting.hpp ================================================ #ifndef GAME_MWMECHANICS_ENCHANTING_H #define GAME_MWMECHANICS_ENCHANTING_H #include #include #include #include "../mwworld/ptr.hpp" namespace MWMechanics { class Enchanting { MWWorld::Ptr mOldItemPtr; MWWorld::Ptr mSoulGemPtr; MWWorld::Ptr mEnchanter; int mCastStyle; bool mSelfEnchanting; ESM::EffectList mEffectList; std::string mNewItemName; std::string mObjectType; int mWeaponType; const ESM::Enchantment* getRecord(const ESM::Enchantment& newEnchantment) const; public: Enchanting(); void setEnchanter(const MWWorld::Ptr& enchanter); void setSelfEnchanting(bool selfEnchanting); void setOldItem(const MWWorld::Ptr& oldItem); MWWorld::Ptr getOldItem() { return mOldItemPtr; } MWWorld::Ptr getGem() { return mSoulGemPtr; } void setNewItemName(const std::string& s); void setEffect(const ESM::EffectList& effectList); void setSoulGem(const MWWorld::Ptr& soulGem); bool create(); //Return true if created, false if failed. void nextCastStyle(); //Set enchant type to next possible type (for mOldItemPtr object) int getCastStyle() const; float getEnchantPoints(bool precise = true) const; int getBaseCastCost() const; // To be saved in the enchantment's record int getEffectiveCastCost() const; // Effective cost taking player Enchant skill into account, used for preview purposes in the UI int getEnchantPrice() const; int getMaxEnchantValue() const; int getGemCharge() const; int getEnchantChance() const; int getEnchantItemsCount() const; float getTypeMultiplier() const; bool soulEmpty() const; //Return true if empty bool itemEmpty() const; //Return true if empty void payForEnchantment() const; }; } #endif ================================================ FILE: apps/openmw/mwmechanics/levelledlist.hpp ================================================ #ifndef OPENMW_MECHANICS_LEVELLEDLIST_H #define OPENMW_MECHANICS_LEVELLEDLIST_H #include #include #include "../mwworld/ptr.hpp" #include "../mwworld/esmstore.hpp" #include "../mwworld/manualref.hpp" #include "../mwworld/class.hpp" #include "../mwbase/world.hpp" #include "../mwbase/environment.hpp" #include "creaturestats.hpp" #include "actorutil.hpp" namespace MWMechanics { /// @return ID of resulting item, or empty if none inline std::string getLevelledItem (const ESM::LevelledListBase* levItem, bool creature, Misc::Rng::Seed& seed = Misc::Rng::getSeed()) { const std::vector& items = levItem->mList; const MWWorld::Ptr& player = getPlayer(); int playerLevel = player.getClass().getCreatureStats(player).getLevel(); if (Misc::Rng::roll0to99(seed) < levItem->mChanceNone) return std::string(); std::vector candidates; int highestLevel = 0; for (const auto& levelledItem : items) { if (levelledItem.mLevel > highestLevel && levelledItem.mLevel <= playerLevel) highestLevel = levelledItem.mLevel; } // For levelled creatures, the flags are swapped. This file format just makes so much sense. bool allLevels = (levItem->mFlags & ESM::ItemLevList::AllLevels) != 0; if (creature) allLevels = levItem->mFlags & ESM::CreatureLevList::AllLevels; std::pair highest = std::make_pair(-1, ""); for (const auto& levelledItem : items) { if (playerLevel >= levelledItem.mLevel && (allLevels || levelledItem.mLevel == highestLevel)) { candidates.push_back(levelledItem.mId); if (levelledItem.mLevel >= highest.first) highest = std::make_pair(levelledItem.mLevel, levelledItem.mId); } } if (candidates.empty()) return std::string(); std::string item = candidates[Misc::Rng::rollDice(candidates.size(), seed)]; // Vanilla doesn't fail on nonexistent items in levelled lists if (!MWBase::Environment::get().getWorld()->getStore().find(Misc::StringUtils::lowerCase(item))) { Log(Debug::Warning) << "Warning: ignoring nonexistent item '" << item << "' in levelled list '" << levItem->mId << "'"; return std::string(); } // Is this another levelled item or a real item? MWWorld::ManualRef ref (MWBase::Environment::get().getWorld()->getStore(), item, 1); if (ref.getPtr().getTypeName() != typeid(ESM::ItemLevList).name() && ref.getPtr().getTypeName() != typeid(ESM::CreatureLevList).name()) { return item; } else { if (ref.getPtr().getTypeName() == typeid(ESM::ItemLevList).name()) return getLevelledItem(ref.getPtr().get()->mBase, false, seed); else return getLevelledItem(ref.getPtr().get()->mBase, true, seed); } } } #endif ================================================ FILE: apps/openmw/mwmechanics/linkedeffects.cpp ================================================ #include "linkedeffects.hpp" #include #include #include "../mwbase/environment.hpp" #include "../mwbase/world.hpp" #include "../mwrender/animation.hpp" #include "../mwworld/class.hpp" #include "../mwworld/esmstore.hpp" #include "creaturestats.hpp" namespace MWMechanics { bool reflectEffect(const ESM::ENAMstruct& effect, const ESM::MagicEffect* magicEffect, const MWWorld::Ptr& caster, const MWWorld::Ptr& target, ESM::EffectList& reflectedEffects) { if (caster.isEmpty() || caster == target || !target.getClass().isActor()) return false; bool isHarmful = magicEffect->mData.mFlags & ESM::MagicEffect::Harmful; bool isUnreflectable = magicEffect->mData.mFlags & ESM::MagicEffect::Unreflectable; if (!isHarmful || isUnreflectable) return false; float reflect = target.getClass().getCreatureStats(target).getMagicEffects().get(ESM::MagicEffect::Reflect).getMagnitude(); if (Misc::Rng::roll0to99() >= reflect) return false; const ESM::Static* reflectStatic = MWBase::Environment::get().getWorld()->getStore().get().find ("VFX_Reflect"); MWRender::Animation* animation = MWBase::Environment::get().getWorld()->getAnimation(target); if (animation && !reflectStatic->mModel.empty()) animation->addEffect("meshes\\" + reflectStatic->mModel, ESM::MagicEffect::Reflect, false, std::string()); reflectedEffects.mList.emplace_back(effect); return true; } void absorbStat(const ESM::ENAMstruct& effect, const ESM::ActiveEffect& appliedEffect, const MWWorld::Ptr& caster, const MWWorld::Ptr& target, bool reflected, const std::string& source) { if (caster.isEmpty() || caster == target) return; if (!target.getClass().isActor() || !caster.getClass().isActor()) return; // Make sure callers don't do something weird if (effect.mEffectID < ESM::MagicEffect::AbsorbAttribute || effect.mEffectID > ESM::MagicEffect::AbsorbSkill) throw std::runtime_error("invalid absorb stat effect"); if (appliedEffect.mMagnitude == 0) return; std::vector absorbEffects; ActiveSpells::ActiveEffect absorbEffect = appliedEffect; absorbEffect.mMagnitude *= -1; absorbEffect.mEffectIndex = appliedEffect.mEffectIndex; absorbEffects.emplace_back(absorbEffect); // Morrowind negates reflected Absorb spells so the original caster won't be harmed. if (reflected && Settings::Manager::getBool("classic reflected absorb spells behavior", "Game")) { target.getClass().getCreatureStats(target).getActiveSpells().addSpell(std::string(), true, absorbEffects, source, caster.getClass().getCreatureStats(caster).getActorId()); return; } caster.getClass().getCreatureStats(caster).getActiveSpells().addSpell(std::string(), true, absorbEffects, source, target.getClass().getCreatureStats(target).getActorId()); } } ================================================ FILE: apps/openmw/mwmechanics/linkedeffects.hpp ================================================ #ifndef MWMECHANICS_LINKEDEFFECTS_H #define MWMECHANICS_LINKEDEFFECTS_H #include namespace ESM { struct ActiveEffect; struct EffectList; struct ENAMstruct; struct MagicEffect; struct Spell; } namespace MWWorld { class Ptr; } namespace MWMechanics { // Try to reflect a spell effect. If it's reflected, it's also put into the passed reflected effects list. bool reflectEffect(const ESM::ENAMstruct& effect, const ESM::MagicEffect* magicEffect, const MWWorld::Ptr& caster, const MWWorld::Ptr& target, ESM::EffectList& reflectedEffects); // Try to absorb a stat (skill, attribute, etc.) from the target and transfer it to the caster. void absorbStat(const ESM::ENAMstruct& effect, const ESM::ActiveEffect& appliedEffect, const MWWorld::Ptr& caster, const MWWorld::Ptr& target, bool reflected, const std::string& source); } #endif ================================================ FILE: apps/openmw/mwmechanics/magiceffects.cpp ================================================ #include "magiceffects.hpp" #include #include #include namespace MWMechanics { EffectKey::EffectKey() : mId (0), mArg (-1) {} EffectKey::EffectKey (const ESM::ENAMstruct& effect) { mId = effect.mEffectID; mArg = -1; if (effect.mSkill!=-1) mArg = effect.mSkill; if (effect.mAttribute!=-1) { if (mArg!=-1) throw std::runtime_error ( "magic effect can't have both a skill and an attribute argument"); mArg = effect.mAttribute; } } bool operator< (const EffectKey& left, const EffectKey& right) { if (left.mIdright.mId) return false; return left.mArgsecond += param; } } void MagicEffects::modifyBase(const EffectKey &key, int diff) { mCollection[key].modifyBase(diff); } void MagicEffects::setModifiers(const MagicEffects &effects) { for (Collection::iterator it = mCollection.begin(); it != mCollection.end(); ++it) { it->second.setModifier(effects.get(it->first).getModifier()); } for (Collection::const_iterator it = effects.begin(); it != effects.end(); ++it) { mCollection[it->first].setModifier(it->second.getModifier()); } } 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); if (iter==mCollection.end()) { return EffectParam(); } else { return iter->second; } } MagicEffects MagicEffects::diff (const MagicEffects& prev, const MagicEffects& now) { MagicEffects result; // adding/changing for (Collection::const_iterator iter (now.begin()); iter!=now.end(); ++iter) { Collection::const_iterator other = prev.mCollection.find (iter->first); if (other==prev.end()) { // adding result.add (iter->first, iter->second); } else { // changing result.add (iter->first, iter->second - other->second); } } // removing for (Collection::const_iterator iter (prev.begin()); iter!=prev.end(); ++iter) { Collection::const_iterator other = now.mCollection.find (iter->first); if (other==now.end()) { result.add (iter->first, EffectParam() - iter->second); } } return result; } void MagicEffects::writeState(ESM::MagicEffects &state) const { // Don't need to save Modifiers, they are recalculated every frame anyway. for (Collection::const_iterator iter (begin()); iter!=end(); ++iter) { if (iter->second.getBase() != 0) { // Don't worry about mArg, never used by magic effect script instructions state.mEffects.insert(std::make_pair(iter->first.mId, iter->second.getBase())); } } } void MagicEffects::readState(const ESM::MagicEffects &state) { for (std::map::const_iterator it = state.mEffects.begin(); it != state.mEffects.end(); ++it) { mCollection[EffectKey(it->first)].setBase(it->second); } } } ================================================ FILE: apps/openmw/mwmechanics/magiceffects.hpp ================================================ #ifndef GAME_MWMECHANICS_MAGICEFFECTS_H #define GAME_MWMECHANICS_MAGICEFFECTS_H #include #include namespace ESM { struct ENAMstruct; struct EffectList; struct MagicEffects; } namespace MWMechanics { struct EffectKey { int mId; int mArg; // skill or ability EffectKey(); EffectKey (int id, int arg = -1) : mId (id), mArg (arg) {} EffectKey (const ESM::ENAMstruct& effect); }; bool operator< (const EffectKey& left, const EffectKey& right); struct EffectParam { private: // Note usually this would be int, but applying partial resistance might introduce a decimal point. float mModifier; int mBase; public: /// Get the total magnitude including base and modifier. float getMagnitude() const; void setModifier(float mod); float getModifier() const; /// Change mBase by \a diff void modifyBase(int diff); void setBase(int base); int getBase() const; EffectParam(); EffectParam(float magnitude) : mModifier(magnitude), mBase(0) {} EffectParam& operator+= (const EffectParam& param); EffectParam& operator-= (const EffectParam& param); }; inline EffectParam operator+ (const EffectParam& left, const EffectParam& right) { EffectParam param (left); return param += right; } inline EffectParam operator- (const EffectParam& left, const EffectParam& right) { EffectParam param (left); return param -= right; } // Used by effect management classes (ActiveSpells, InventoryStore, Spells) to list active effect sources for GUI display struct EffectSourceVisitor { virtual ~EffectSourceVisitor() { } virtual void visit (EffectKey key, int effectIndex, const std::string& sourceName, const std::string& sourceId, int casterActorId, float magnitude, float remainingTime = -1, float totalTime = -1) = 0; }; /// \brief Effects currently affecting a NPC or creature class MagicEffects { public: typedef std::map Collection; private: Collection mCollection; public: Collection::const_iterator begin() const { return mCollection.begin(); } Collection::const_iterator end() const { return mCollection.end(); } void readState (const ESM::MagicEffects& state); void writeState (ESM::MagicEffects& state) const; void add (const EffectKey& key, const EffectParam& param); void remove (const EffectKey& key); void modifyBase (const EffectKey& key, int diff); /// 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. static MagicEffects diff (const MagicEffects& prev, const MagicEffects& now); ///< Return changes from \a prev to \a now. }; } #endif ================================================ FILE: apps/openmw/mwmechanics/mechanicsmanagerimp.cpp ================================================ #include "mechanicsmanagerimp.hpp" #include #include #include #include #include #include /* Start of tes3mp addition Include additional headers for multiplayer purposes */ #include #include "../mwmp/Main.hpp" #include "../mwmp/LocalPlayer.hpp" #include "../mwmp/PlayerList.hpp" #include "../mwmp/CellController.hpp" /* End of tes3mp addition */ #include "../mwworld/esmstore.hpp" #include "../mwworld/inventorystore.hpp" #include "../mwworld/class.hpp" #include "../mwworld/player.hpp" #include "../mwworld/ptr.hpp" #include "../mwbase/environment.hpp" #include "../mwbase/statemanager.hpp" #include "../mwbase/world.hpp" #include "../mwbase/windowmanager.hpp" #include "../mwbase/dialoguemanager.hpp" #include "aicombat.hpp" #include "aipursue.hpp" #include "spellutil.hpp" #include "autocalcspell.hpp" #include "npcstats.hpp" #include "actorutil.hpp" #include "combat.hpp" namespace { float getFightDispositionBias(float disposition) { static const float fFightDispMult = MWBase::Environment::get().getWorld()->getStore().get().find( "fFightDispMult")->mValue.getFloat(); return ((50.f - disposition) * fFightDispMult); } void getPersuasionRatings(const MWMechanics::NpcStats& stats, float& rating1, float& rating2, float& rating3, bool player) { const MWWorld::Store &gmst = MWBase::Environment::get().getWorld()->getStore().get(); float persTerm = stats.getAttribute(ESM::Attribute::Personality).getModified() / gmst.find("fPersonalityMod")->mValue.getFloat(); float luckTerm = stats.getAttribute(ESM::Attribute::Luck).getModified() / gmst.find("fLuckMod")->mValue.getFloat(); float repTerm = stats.getReputation() * gmst.find("fReputationMod")->mValue.getFloat(); float fatigueTerm = stats.getFatigueTerm(); float levelTerm = stats.getLevel() * gmst.find("fLevelMod")->mValue.getFloat(); rating1 = (repTerm + luckTerm + persTerm + stats.getSkill(ESM::Skill::Speechcraft).getModified()) * fatigueTerm; if (player) { rating2 = rating1 + levelTerm; rating3 = (stats.getSkill(ESM::Skill::Mercantile).getModified() + luckTerm + persTerm) * fatigueTerm; } else { rating2 = (levelTerm + repTerm + luckTerm + persTerm + stats.getSkill(ESM::Skill::Speechcraft).getModified()) * fatigueTerm; rating3 = (stats.getSkill(ESM::Skill::Mercantile).getModified() + repTerm + luckTerm + persTerm) * fatigueTerm; } } } namespace MWMechanics { void MechanicsManager::buildPlayer() { MWWorld::Ptr ptr = getPlayer(); MWMechanics::CreatureStats& creatureStats = ptr.getClass().getCreatureStats (ptr); MWMechanics::NpcStats& npcStats = ptr.getClass().getNpcStats (ptr); npcStats.setNeedRecalcDynamicStats(true); const ESM::NPC *player = ptr.get()->mBase; // reset creatureStats.setLevel(player->mNpdt.mLevel); creatureStats.getSpells().clear(true); creatureStats.modifyMagicEffects(MagicEffects()); for (int i=0; i<27; ++i) npcStats.getSkill (i).setBase (player->mNpdt.mSkills[i]); creatureStats.setAttribute(ESM::Attribute::Strength, player->mNpdt.mStrength); creatureStats.setAttribute(ESM::Attribute::Intelligence, player->mNpdt.mIntelligence); creatureStats.setAttribute(ESM::Attribute::Willpower, player->mNpdt.mWillpower); creatureStats.setAttribute(ESM::Attribute::Agility, player->mNpdt.mAgility); creatureStats.setAttribute(ESM::Attribute::Speed, player->mNpdt.mSpeed); creatureStats.setAttribute(ESM::Attribute::Endurance, player->mNpdt.mEndurance); creatureStats.setAttribute(ESM::Attribute::Personality, player->mNpdt.mPersonality); creatureStats.setAttribute(ESM::Attribute::Luck, player->mNpdt.mLuck); const MWWorld::ESMStore &esmStore = MWBase::Environment::get().getWorld()->getStore(); // race if (mRaceSelected) { const ESM::Race *race = esmStore.get().find(player->mRace); bool male = (player->mFlags & ESM::NPC::Female) == 0; for (int i=0; i<8; ++i) { const ESM::Race::MaleFemale& attribute = race->mData.mAttributeValues[i]; creatureStats.setAttribute(i, male ? attribute.mMale : attribute.mFemale); } for (int i=0; i<27; ++i) { int bonus = 0; for (int i2=0; i2<7; ++i2) if (race->mData.mBonus[i2].mSkill==i) { bonus = race->mData.mBonus[i2].mBonus; break; } npcStats.getSkill (i).setBase (5 + bonus); } for (const std::string &power : race->mPowers.mList) { creatureStats.getSpells().add(power); } } // birthsign const std::string &signId = MWBase::Environment::get().getWorld()->getPlayer().getBirthSign(); if (!signId.empty()) { const ESM::BirthSign *sign = esmStore.get().find(signId); for (const std::string &power : sign->mPowers.mList) { creatureStats.getSpells().add(power); } } // class if (mClassSelected) { const ESM::Class *class_ = esmStore.get().find(player->mClass); for (int i=0; i<2; ++i) { int attribute = class_->mData.mAttribute[i]; if (attribute>=0 && attribute<8) { creatureStats.setAttribute(attribute, creatureStats.getAttribute(attribute).getBase() + 10); } } for (int i=0; i<2; ++i) { int bonus = i==0 ? 10 : 25; for (int i2=0; i2<5; ++i2) { int index = class_->mData.mSkills[i2][i]; if (index>=0 && index<27) { npcStats.getSkill (index).setBase ( npcStats.getSkill (index).getBase() + bonus); } } } const MWWorld::Store &skills = esmStore.get(); MWWorld::Store::iterator iter = skills.begin(); for (; iter != skills.end(); ++iter) { if (iter->second.mData.mSpecialization==class_->mData.mSpecialization) { int index = iter->first; if (index>=0 && index<27) { npcStats.getSkill (index).setBase ( npcStats.getSkill (index).getBase() + 5); } } } } // F_PCStart spells const ESM::Race* race = nullptr; if (mRaceSelected) race = esmStore.get().find(player->mRace); int skills[ESM::Skill::Length]; for (int i=0; i selectedSpells = autoCalcPlayerSpells(skills, attributes, race); for (const std::string &spell : selectedSpells) creatureStats.getSpells().add(spell); // forced update and current value adjustments mActors.updateActor (ptr, 0); for (int i=0; i<3; ++i) { DynamicStat stat = creatureStats.getDynamic (i); stat.setCurrent (stat.getModified()); creatureStats.setDynamic (i, stat); } // auto-equip again. we need this for when the race is changed to a beast race and shoes are no longer equippable MWWorld::InventoryStore& invStore = ptr.getClass().getInventoryStore(ptr); for (int i=0; igetWatchedActor()) MWBase::Environment::get().getWindowManager()->watchActor(MWWorld::Ptr()); mActors.removeActor(ptr); mObjects.removeObject(ptr); } void MechanicsManager::updateCell(const MWWorld::Ptr &old, const MWWorld::Ptr &ptr) { if(old == MWBase::Environment::get().getWindowManager()->getWatchedActor()) MWBase::Environment::get().getWindowManager()->watchActor(ptr); if(ptr.getClass().isActor()) mActors.updateActor(old, ptr); else mObjects.updateObject(old, ptr); } void MechanicsManager::drop(const MWWorld::CellStore *cellStore) { mActors.dropActors(cellStore, getPlayer()); mObjects.dropObjects(cellStore); } void MechanicsManager::restoreStatsAfterCorprus(const MWWorld::Ptr& actor, const std::string& sourceId) { auto& stats = actor.getClass().getCreatureStats (actor); auto& corprusSpells = stats.getCorprusSpells(); auto corprusIt = corprusSpells.find(sourceId); if (corprusIt != corprusSpells.end()) { for (int i = 0; i < ESM::Attribute::Length; ++i) { MWMechanics::AttributeValue attr = stats.getAttribute(i); attr.restore(corprusIt->second.mWorsenings[i]); actor.getClass().getCreatureStats(actor).setAttribute(i, attr); } } } void MechanicsManager::update(float duration, bool paused) { // Note: we should do it here since game mechanics and world updates use these values 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(); if (enchantItem != inv.end()) winMgr->setSelectedEnchantItem(*enchantItem); else { const std::string& spell = winMgr->getSelectedSpell(); if (!spell.empty()) winMgr->setSelectedSpell(spell, int(MWMechanics::getSpellSuccessChance(spell, ptr))); else winMgr->unsetSelectedSpell(); } if (mUpdatePlayer) { mUpdatePlayer = false; // HACK? The player has been changed, so a new Animation object may // have been made for them. Make sure they're properly updated. mActors.removeActor(ptr); mActors.addActor(ptr, true); } mActors.update(duration, paused); mObjects.update(duration, paused); } void MechanicsManager::processChangedSettings(const Settings::CategorySettingVector &changed) { for (Settings::CategorySettingVector::const_iterator it = changed.begin(); it != changed.end(); ++it) { if (it->first == "Game" && it->second == "actors processing range") { int state = MWBase::Environment::get().getStateManager()->getState(); if (state != MWBase::StateManager::State_Running) continue; mActors.updateProcessingRange(); // Update mechanics for new processing range immediately update(0.f, false); } } } void MechanicsManager::notifyDied(const MWWorld::Ptr& actor) { mActors.notifyDied(actor); } float MechanicsManager::getActorsProcessingRange() const { return mActors.getProcessingRange(); } bool MechanicsManager::isActorDetected(const MWWorld::Ptr& actor, const MWWorld::Ptr& observer) { return mActors.isActorDetected(actor, observer); } bool MechanicsManager::isAttackPreparing(const MWWorld::Ptr& ptr) { return mActors.isAttackPreparing(ptr); } bool MechanicsManager::isRunning(const MWWorld::Ptr& ptr) { return mActors.isRunning(ptr); } bool MechanicsManager::isSneaking(const MWWorld::Ptr& ptr) { CreatureStats& stats = ptr.getClass().getCreatureStats(ptr); MWBase::World* world = MWBase::Environment::get().getWorld(); bool animActive = mActors.isSneaking(ptr); bool stanceOn = stats.getStance(MWMechanics::CreatureStats::Stance_Sneak); bool inair = !world->isOnGround(ptr) && !world->isSwimming(ptr) && !world->isFlying(ptr); return stanceOn && (animActive || inair); } void MechanicsManager::rest(double hours, bool sleep) { if (sleep) MWBase::Environment::get().getWorld()->rest(hours); mActors.rest(hours, sleep); } void MechanicsManager::restoreDynamicStats(MWWorld::Ptr actor, double hours, bool sleep) { mActors.restoreDynamicStats(actor, hours, sleep); } int MechanicsManager::getHoursToRest() const { return mActors.getHoursToRest(getPlayer()); } void MechanicsManager::setPlayerName (const std::string& name) { MWBase::World *world = MWBase::Environment::get().getWorld(); ESM::NPC player = *world->getPlayerPtr().get()->mBase; player.mName = name; world->createRecord(player); mUpdatePlayer = true; } void MechanicsManager::setPlayerRace (const std::string& race, bool male, const std::string &head, const std::string &hair) { MWBase::World *world = MWBase::Environment::get().getWorld(); ESM::NPC player = *world->getPlayerPtr().get()->mBase; player.mRace = race; player.mHead = head; player.mHair = hair; player.setIsMale(male); world->createRecord(player); mRaceSelected = true; buildPlayer(); mUpdatePlayer = true; } void MechanicsManager::setPlayerBirthsign (const std::string& id) { MWBase::Environment::get().getWorld()->getPlayer().setBirthSign(id); buildPlayer(); mUpdatePlayer = true; } void MechanicsManager::setPlayerClass (const std::string& id) { MWBase::World *world = MWBase::Environment::get().getWorld(); ESM::NPC player = *world->getPlayerPtr().get()->mBase; player.mClass = id; world->createRecord(player); mClassSelected = true; buildPlayer(); mUpdatePlayer = true; } void MechanicsManager::setPlayerClass (const ESM::Class &cls) { MWBase::World *world = MWBase::Environment::get().getWorld(); const ESM::Class *ptr = world->createRecord(cls); ESM::NPC player = *world->getPlayerPtr().get()->mBase; player.mClass = ptr->mId; world->createRecord(player); mClassSelected = true; buildPlayer(); mUpdatePlayer = true; } int MechanicsManager::getDerivedDisposition(const MWWorld::Ptr& ptr, bool addTemporaryDispositionChange) { const MWMechanics::NpcStats& npcSkill = ptr.getClass().getNpcStats(ptr); float x = static_cast(npcSkill.getBaseDisposition()); MWWorld::LiveCellRef* npc = ptr.get(); MWWorld::Ptr playerPtr = getPlayer(); MWWorld::LiveCellRef* player = playerPtr.get(); const MWMechanics::NpcStats &playerStats = playerPtr.getClass().getNpcStats(playerPtr); const MWWorld::Store& gmst = MWBase::Environment::get().getWorld()->getStore().get(); static const float fDispRaceMod = gmst.find("fDispRaceMod")->mValue.getFloat(); if (Misc::StringUtils::ciEqual(npc->mBase->mRace, player->mBase->mRace)) x += fDispRaceMod; static const float fDispPersonalityMult = gmst.find("fDispPersonalityMult")->mValue.getFloat(); static const float fDispPersonalityBase = gmst.find("fDispPersonalityBase")->mValue.getFloat(); x += fDispPersonalityMult * (playerStats.getAttribute(ESM::Attribute::Personality).getModified() - fDispPersonalityBase); float reaction = 0; int rank = 0; std::string npcFaction = ptr.getClass().getPrimaryFaction(ptr); Misc::StringUtils::lowerCaseInPlace(npcFaction); if (playerStats.getFactionRanks().find(npcFaction) != playerStats.getFactionRanks().end()) { if (!playerStats.getExpelled(npcFaction)) { // faction reaction towards itself. yes, that exists reaction = static_cast(MWBase::Environment::get().getDialogueManager()->getFactionReaction(npcFaction, npcFaction)); rank = playerStats.getFactionRanks().find(npcFaction)->second; } } else if (!npcFaction.empty()) { std::map::const_iterator playerFactionIt = playerStats.getFactionRanks().begin(); for (; playerFactionIt != playerStats.getFactionRanks().end(); ++playerFactionIt) { std::string itFaction = playerFactionIt->first; // Ignore the faction, if a player was expelled from it. if (playerStats.getExpelled(itFaction)) continue; int itReaction = MWBase::Environment::get().getDialogueManager()->getFactionReaction(npcFaction, itFaction); if (playerFactionIt == playerStats.getFactionRanks().begin() || itReaction < reaction) { reaction = static_cast(itReaction); rank = playerFactionIt->second; } } } else { reaction = 0; rank = 0; } static const float fDispFactionRankMult = gmst.find("fDispFactionRankMult")->mValue.getFloat(); static const float fDispFactionRankBase = gmst.find("fDispFactionRankBase")->mValue.getFloat(); static const float fDispFactionMod = gmst.find("fDispFactionMod")->mValue.getFloat(); x += (fDispFactionRankMult * rank + fDispFactionRankBase) * fDispFactionMod * reaction; static const float fDispCrimeMod = gmst.find("fDispCrimeMod")->mValue.getFloat(); static const float fDispDiseaseMod = gmst.find("fDispDiseaseMod")->mValue.getFloat(); x -= fDispCrimeMod * playerStats.getBounty(); if (playerStats.hasCommonDisease() || playerStats.hasBlightDisease()) x += fDispDiseaseMod; static const float fDispWeaponDrawn = gmst.find("fDispWeaponDrawn")->mValue.getFloat(); if (playerStats.getDrawState() == MWMechanics::DrawState_Weapon) x += fDispWeaponDrawn; x += ptr.getClass().getCreatureStats(ptr).getMagicEffects().get(ESM::MagicEffect::Charm).getMagnitude(); if(addTemporaryDispositionChange) x += MWBase::Environment::get().getDialogueManager()->getTemporaryDispositionChange(); int effective_disposition = std::max(0,std::min(int(x),100));//, normally clamped to [0..100] when used return effective_disposition; } int MechanicsManager::getBarterOffer(const MWWorld::Ptr& ptr,int basePrice, bool buying) { // Make sure zero base price items/services can't be bought/sold for 1 gold // and return the intended base price for creature merchants if (basePrice == 0 || ptr.getTypeName() == typeid(ESM::Creature).name()) return basePrice; const MWMechanics::NpcStats &sellerStats = ptr.getClass().getNpcStats(ptr); MWWorld::Ptr playerPtr = getPlayer(); const MWMechanics::NpcStats &playerStats = playerPtr.getClass().getNpcStats(playerPtr); // I suppose the temporary disposition change (second param to getDerivedDisposition()) _has_ to be considered here, // otherwise one would get different prices when exiting and re-entering the dialogue window... int clampedDisposition = getDerivedDisposition(ptr); float a = std::min(playerPtr.getClass().getSkill(playerPtr, ESM::Skill::Mercantile), 100.f); float b = std::min(0.1f * playerStats.getAttribute(ESM::Attribute::Luck).getModified(), 10.f); float c = std::min(0.2f * playerStats.getAttribute(ESM::Attribute::Personality).getModified(), 10.f); float d = std::min(ptr.getClass().getSkill(ptr, ESM::Skill::Mercantile), 100.f); float e = std::min(0.1f * sellerStats.getAttribute(ESM::Attribute::Luck).getModified(), 10.f); float f = std::min(0.2f * sellerStats.getAttribute(ESM::Attribute::Personality).getModified(), 10.f); float pcTerm = (clampedDisposition - 50 + a + b + c) * playerStats.getFatigueTerm(); float npcTerm = (d + e + f) * sellerStats.getFatigueTerm(); float buyTerm = 0.01f * (100 - 0.5f * (pcTerm - npcTerm)); float sellTerm = 0.01f * (50 - 0.5f * (npcTerm - pcTerm)); int offerPrice = int(basePrice * (buying ? buyTerm : sellTerm)); return std::max(1, offerPrice); } int MechanicsManager::countDeaths (const std::string& id) const { return mActors.countDeaths (id); } /* Start of tes3mp addition Make it possible to set the number of deaths for an actor with the given refId */ void MechanicsManager::setDeaths(const std::string& refId, int number) { mActors.setDeaths(refId, number); } /* End of tes3mp addition */ void MechanicsManager::getPersuasionDispositionChange (const MWWorld::Ptr& npc, PersuasionType type, bool& success, float& tempChange, float& permChange) { const MWWorld::Store &gmst = MWBase::Environment::get().getWorld()->getStore().get(); MWMechanics::NpcStats& npcStats = npc.getClass().getNpcStats(npc); MWWorld::Ptr playerPtr = getPlayer(); const MWMechanics::NpcStats &playerStats = playerPtr.getClass().getNpcStats(playerPtr); float npcRating1, npcRating2, npcRating3; getPersuasionRatings(npcStats, npcRating1, npcRating2, npcRating3, false); float playerRating1, playerRating2, playerRating3; getPersuasionRatings(playerStats, playerRating1, playerRating2, playerRating3, true); int currentDisposition = getDerivedDisposition(npc); float d = 1 - 0.02f * abs(currentDisposition - 50); float target1 = d * (playerRating1 - npcRating1 + 50); float target2 = d * (playerRating2 - npcRating2 + 50); float bribeMod; if (type == PT_Bribe10) bribeMod = gmst.find("fBribe10Mod")->mValue.getFloat(); else if (type == PT_Bribe100) bribeMod = gmst.find("fBribe100Mod")->mValue.getFloat(); else bribeMod = gmst.find("fBribe1000Mod")->mValue.getFloat(); float target3 = d * (playerRating3 - npcRating3 + 50) + bribeMod; float iPerMinChance = floor(gmst.find("iPerMinChance")->mValue.getFloat()); float iPerMinChange = floor(gmst.find("iPerMinChange")->mValue.getFloat()); float fPerDieRollMult = gmst.find("fPerDieRollMult")->mValue.getFloat(); float fPerTempMult = gmst.find("fPerTempMult")->mValue.getFloat(); float x = 0; float y = 0; int roll = Misc::Rng::roll0to99(); if (type == PT_Admire) { target1 = std::max(iPerMinChance, target1); success = (roll <= target1); float c = floor(fPerDieRollMult * (target1 - roll)); x = success ? std::max(iPerMinChange, c) : c; } else if (type == PT_Intimidate) { target2 = std::max(iPerMinChance, target2); success = (roll <= target2); float r; if (roll != target2) r = floor(target2 - roll); else r = 1; if (roll <= target2) { float s = floor(r * fPerDieRollMult * fPerTempMult); 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))))); npcStats.setAiSetting (MWMechanics::CreatureStats::AI_Fight, std::max(0, std::min(100, fight + int(std::min(-iPerMinChange, -s))))); } float c = -std::abs(floor(r * fPerDieRollMult)); if (success) { if (std::abs(c) < iPerMinChange) { // Deviating from Morrowind here: it doesn't increase disposition on marginal wins, // which seems to be a bug (MCP fixes it too). // Original logic: x = 0, y = -iPerMinChange x = iPerMinChange; y = x; // This goes unused. } else { x = -floor(c * fPerTempMult); y = c; } } else { x = floor(c * fPerTempMult); y = c; } } else if (type == PT_Taunt) { target1 = std::max(iPerMinChance, target1); success = (roll <= target1); float c = std::abs(floor(target1 - roll)); if (success) { 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))))); } x = floor(-c * fPerDieRollMult); if (success && std::abs(x) < iPerMinChange) x = -iPerMinChange; } else // Bribe { target3 = std::max(iPerMinChance, target3); success = (roll <= target3); float c = floor((target3 - roll) * fPerDieRollMult); x = success ? std::max(iPerMinChange, c) : c; } tempChange = type == PT_Intimidate ? x : int(x * fPerTempMult); float cappedDispositionChange = tempChange; if (currentDisposition + tempChange > 100.f) cappedDispositionChange = static_cast(100 - currentDisposition); if (currentDisposition + tempChange < 0.f) cappedDispositionChange = static_cast(-currentDisposition); permChange = floor(cappedDispositionChange / fPerTempMult); if (type == PT_Intimidate) { permChange = success ? -int(cappedDispositionChange/ fPerTempMult) : y; } } void MechanicsManager::forceStateUpdate(const MWWorld::Ptr &ptr) { if(ptr.getClass().isActor()) mActors.forceStateUpdate(ptr); } bool MechanicsManager::playAnimationGroup(const MWWorld::Ptr& ptr, const std::string& groupName, int mode, int number, bool persist) { if(ptr.getClass().isActor()) return mActors.playAnimationGroup(ptr, groupName, mode, number, persist); else return mObjects.playAnimationGroup(ptr, groupName, mode, number, persist); } void MechanicsManager::skipAnimation(const MWWorld::Ptr& ptr) { if(ptr.getClass().isActor()) mActors.skipAnimation(ptr); else mObjects.skipAnimation(ptr); } bool MechanicsManager::checkAnimationPlaying(const MWWorld::Ptr& ptr, const std::string &groupName) { if(ptr.getClass().isActor()) return mActors.checkAnimationPlaying(ptr, groupName); else return false; } bool MechanicsManager::onOpen(const MWWorld::Ptr& ptr) { if(ptr.getClass().isActor()) return true; else return mObjects.onOpen(ptr); } void MechanicsManager::onClose(const MWWorld::Ptr& ptr) { if(!ptr.getClass().isActor()) mObjects.onClose(ptr); } void MechanicsManager::persistAnimationStates() { mActors.persistAnimationStates(); mObjects.persistAnimationStates(); } void MechanicsManager::updateMagicEffects(const MWWorld::Ptr &ptr) { mActors.updateMagicEffects(ptr); } bool MechanicsManager::toggleAI() { mAI = !mAI; MWBase::World* world = MWBase::Environment::get().getWorld(); world->getNavigator()->setUpdatesEnabled(mAI); if (mAI) world->getNavigator()->update(world->getPlayerPtr().getRefData().getPosition().asVec3()); return mAI; } bool MechanicsManager::isAIActive() { return mAI; } void MechanicsManager::playerLoaded() { mUpdatePlayer = true; mClassSelected = true; mRaceSelected = true; /* Start of tes3mp change (major) Avoid enabling AI in multiplayer */ mAI = false; /* End of tes3mp change (major) */ } /* Start of tes3mp change (major) Move boundItemIDCache outside of the original isBoundItem(const MWWorld::Ptr& item) method so it can be reused in the new isBoundItem(std::string itemId) method */ std::set boundItemIDCache; bool MechanicsManager::isBoundItem(const MWWorld::Ptr& item) { /* End of tes3mp change (major) */ // If this is empty then we haven't executed the GMST cache logic yet; or there isn't any sMagicBound* GMST's for some reason if (boundItemIDCache.empty()) { // Build a list of known bound item ID's const MWWorld::Store &gameSettings = MWBase::Environment::get().getWorld()->getStore().get(); for (const ESM::GameSetting ¤tSetting : gameSettings) { std::string currentGMSTID = currentSetting.mId; Misc::StringUtils::lowerCaseInPlace(currentGMSTID); // Don't bother checking this GMST if it's not a sMagicBound* one. const std::string& toFind = "smagicbound"; if (currentGMSTID.compare(0, toFind.length(), toFind) != 0) continue; // All sMagicBound* GMST's should be of type string std::string currentGMSTValue = currentSetting.mValue.getString(); Misc::StringUtils::lowerCaseInPlace(currentGMSTValue); boundItemIDCache.insert(currentGMSTValue); } } // Perform bound item check and assign the Flag_Bound bit if it passes std::string tempItemID = item.getCellRef().getRefId(); Misc::StringUtils::lowerCaseInPlace(tempItemID); if (boundItemIDCache.count(tempItemID) != 0) return true; return false; } /* Start of tes3mp addition Make it possible to check if an itemId corresponds to a bound item */ bool MechanicsManager::isBoundItem(std::string itemId) { Misc::StringUtils::lowerCaseInPlace(itemId); if (boundItemIDCache.count(itemId) != 0) return true; return false; } /* End of tes3mp addition */ bool MechanicsManager::isAllowedToUse (const MWWorld::Ptr& ptr, const MWWorld::Ptr& target, MWWorld::Ptr& victim) { if (target.isEmpty()) return true; const MWWorld::CellRef& cellref = target.getCellRef(); // there is no harm to use unlocked doors int lockLevel = cellref.getLockLevel(); if (target.getClass().isDoor() && (lockLevel <= 0 || lockLevel == ESM::UnbreakableLock) && ptr.getCellRef().getTrap().empty()) { return true; } if (!target.getClass().hasToolTip(target)) return true; // TODO: implement a better check to check if target is owned bed if (target.getClass().isActivator() && target.getClass().getScript(target).compare(0, 3, "Bed") != 0) return true; if (target.getClass().isNpc()) { if (target.getClass().getCreatureStats(target).isDead()) return true; if (target.getClass().getCreatureStats(target).getAiSequence().isInCombat()) return true; // check if a player tries to pickpocket a target NPC if (target.getClass().getCreatureStats(target).getKnockedDown() || isSneaking(ptr)) return false; 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& factions = ptr.getClass().getNpcStats(ptr).getFactionRanks(); std::map::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")) return false; return (!isOwned && !isFactionOwned); } bool MechanicsManager::sleepInBed(const MWWorld::Ptr &ptr, const MWWorld::Ptr &bed) { if (ptr.getClass().getNpcStats(ptr).isWerewolf()) { MWBase::Environment::get().getWindowManager()->messageBox("#{sWerewolfRefusal}"); return true; } if(MWBase::Environment::get().getWorld()->getPlayer().enemiesNearby()) { MWBase::Environment::get().getWindowManager()->messageBox("#{sNotifyMessage2}"); return true; } MWWorld::Ptr victim; if (isAllowedToUse(ptr, bed, victim)) return false; if(commitCrime(ptr, victim, OT_SleepingInOwnedBed, bed.getCellRef().getFaction())) { MWBase::Environment::get().getWindowManager()->messageBox("#{sNotifyMessage64}"); return true; } else return false; } 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()); } std::vector > MechanicsManager::getStolenItemOwners(const std::string& itemid) { std::vector > result; StolenItemsMap::const_iterator it = mStolenItems.find(Misc::StringUtils::lowerCase(itemid)); if (it == mStolenItems.end()) return result; else { const OwnerMap& owners = it->second; for (OwnerMap::const_iterator ownerIt = owners.begin(); ownerIt != owners.end(); ++ownerIt) result.emplace_back(ownerIt->first.first, ownerIt->second); return result; } } bool MechanicsManager::isItemStolenFrom(const std::string &itemid, const MWWorld::Ptr& ptr) { StolenItemsMap::const_iterator it = mStolenItems.find(Misc::StringUtils::lowerCase(itemid)); if (it == mStolenItems.end()) return false; const OwnerMap& owners = it->second; const std::string ownerid = ptr.getCellRef().getRefId(); OwnerMap::const_iterator ownerFound = owners.find(std::make_pair(Misc::StringUtils::lowerCase(ownerid), false)); if (ownerFound != owners.end()) return true; const std::string factionid = ptr.getClass().getPrimaryFaction(ptr); if (!factionid.empty()) { OwnerMap::const_iterator factionOwnerFound = owners.find(std::make_pair(Misc::StringUtils::lowerCase(factionid), true)); return factionOwnerFound != owners.end(); } return false; } void MechanicsManager::confiscateStolenItemToOwner(const MWWorld::Ptr &player, const MWWorld::Ptr &item, const MWWorld::Ptr& victim, int count) { if (player != getPlayer()) return; const std::string itemId = Misc::StringUtils::lowerCase(item.getCellRef().getRefId()); StolenItemsMap::iterator stolenIt = mStolenItems.find(itemId); if (stolenIt == mStolenItems.end()) return; Owner owner; owner.first = victim.getCellRef().getRefId(); owner.second = false; const std::string victimFaction = victim.getClass().getPrimaryFaction(victim); if (!victimFaction.empty() && Misc::StringUtils::ciEqual(item.getCellRef().getFaction(), victimFaction)) // Is the item faction-owned? { owner.first = victimFaction; owner.second = true; } Misc::StringUtils::lowerCaseInPlace(owner.first); // decrease count of stolen items int toRemove = std::min(count, mStolenItems[itemId][owner]); mStolenItems[itemId][owner] -= toRemove; if (mStolenItems[itemId][owner] == 0) { // erase owner from stolen items owners OwnerMap& owners = stolenIt->second; OwnerMap::iterator ownersIt = owners.find(owner); if (ownersIt != owners.end()) owners.erase(ownersIt); } MWWorld::ContainerStore& store = player.getClass().getContainerStore(player); // move items from player to owner and report about theft victim.getClass().getContainerStore(victim).add(item, toRemove, victim); store.remove(item, toRemove, player); commitCrime(player, victim, OT_Theft, item.getCellRef().getFaction(), item.getClass().getValue(item) * toRemove); } void MechanicsManager::confiscateStolenItems(const MWWorld::Ptr &player, const MWWorld::Ptr &targetContainer) { MWWorld::ContainerStore& store = player.getClass().getContainerStore(player); MWWorld::ContainerStore& containerStore = targetContainer.getClass().getContainerStore(targetContainer); for (MWWorld::ContainerStoreIterator it = store.begin(); it != store.end(); ++it) { StolenItemsMap::iterator stolenIt = mStolenItems.find(Misc::StringUtils::lowerCase(it->getCellRef().getRefId())); if (stolenIt == mStolenItems.end()) continue; OwnerMap& owners = stolenIt->second; int itemCount = it->getRefData().getCount(); for (OwnerMap::iterator ownerIt = owners.begin(); ownerIt != owners.end();) { int toRemove = std::min(itemCount, ownerIt->second); itemCount -= toRemove; ownerIt->second -= toRemove; if (ownerIt->second == 0) owners.erase(ownerIt++); else ++ownerIt; } int toMove = it->getRefData().getCount() - itemCount; containerStore.add(*it, toMove, targetContainer); store.remove(*it, toMove, player); } // TODO: unhardcode the locklevel targetContainer.getCellRef().lock(50); } void MechanicsManager::itemTaken(const MWWorld::Ptr &ptr, const MWWorld::Ptr &item, const MWWorld::Ptr& container, int count, bool alarm) { if (ptr != getPlayer()) return; MWWorld::Ptr victim; bool isAllowed = true; const MWWorld::CellRef* ownerCellRef = &item.getCellRef(); if (!container.isEmpty()) { // Inherit the owner of the container ownerCellRef = &container.getCellRef(); isAllowed = isAllowedToUse(ptr, container, victim); } else { isAllowed = isAllowedToUse(ptr, item, victim); if (!item.getCellRef().hasContentFile()) { // this is a manually placed item, which means it was already stolen return; } } if (isAllowed) return; Owner owner; owner.second = false; if (!container.isEmpty() && container.getClass().isActor()) { // "container" is an actor inventory, so just take actor's ID owner.first = ownerCellRef->getRefId(); } else { owner.first = ownerCellRef->getOwner(); if (owner.first.empty()) { owner.first = ownerCellRef->getFaction(); owner.second = true; } } Misc::StringUtils::lowerCaseInPlace(owner.first); if (!Misc::StringUtils::ciEqual(item.getCellRef().getRefId(), MWWorld::ContainerStore::sGoldId)) { if (victim.isEmpty() || (victim.getClass().isActor() && victim.getRefData().getCount() > 0 && !victim.getClass().getCreatureStats(victim).isDead())) mStolenItems[Misc::StringUtils::lowerCase(item.getCellRef().getRefId())][owner] += count; } if (alarm) commitCrime(ptr, victim, OT_Theft, ownerCellRef->getFaction(), item.getClass().getValue(item) * count); } bool MechanicsManager::commitCrime(const MWWorld::Ptr &player, const MWWorld::Ptr &victim, OffenseType type, const std::string& factionId, int arg, bool victimAware) { // NOTE: victim may be empty // Only player can commit crime if (player != getPlayer()) return false; // Find all the actors within the alarm radius std::vector neighbors; osg::Vec3f from (player.getRefData().getPosition().asVec3()); const MWWorld::ESMStore& esmStore = MWBase::Environment::get().getWorld()->getStore(); float radius = esmStore.get().find("fAlarmRadius")->mValue.getFloat(); mActors.getObjectsInRange(from, radius, neighbors); // victim should be considered even beyond alarm radius if (!victim.isEmpty() && (from - victim.getRefData().getPosition().asVec3()).length2() > radius*radius) neighbors.push_back(victim); // get the player's followers / allies (works recursively) that will not report crimes std::set playerFollowers; getActorsSidingWith(player, playerFollowers); // Did anyone see it? bool crimeSeen = false; for (const MWWorld::Ptr &neighbor : neighbors) { if (!canReportCrime(neighbor, victim, playerFollowers)) continue; if ((neighbor == victim && victimAware) // Murder crime can be reported even if no one saw it (hearing is enough, I guess). // TODO: Add mod support for stealth executions! || (type == OT_Murder && neighbor != victim) || (MWBase::Environment::get().getWorld()->getLOS(player, neighbor) && awarenessCheck(player, neighbor))) { /* Start of tes3mp addition We need player-controlled NPCs to not report crimes committed by other players */ if (mwmp::PlayerList::isDedicatedPlayer(neighbor)) continue; /* End of tes3mp addition */ // NPC will complain about theft even if he will do nothing about it if (type == OT_Theft || type == OT_Pickpocket) MWBase::Environment::get().getDialogueManager()->say(neighbor, "thief"); crimeSeen = true; } } if (crimeSeen) reportCrime(player, victim, type, factionId, arg); else if (type == OT_Assault && !victim.isEmpty()) { bool reported = false; if (victim.getClass().isClass(victim, "guard") && !victim.getClass().getCreatureStats(victim).getAiSequence().hasPackage(AiPackageTypeId::Pursue)) reported = reportCrime(player, victim, type, std::string(), arg); if (!reported) startCombat(victim, player); // TODO: combat should be started with an "unaware" flag, which makes the victim flee? } return crimeSeen; } bool MechanicsManager::canReportCrime(const MWWorld::Ptr &actor, const MWWorld::Ptr &victim, std::set &playerFollowers) { if (actor == getPlayer() || !actor.getClass().isNpc() || actor.getClass().getCreatureStats(actor).isDead()) return false; if (actor.getClass().getCreatureStats(actor).getAiSequence().isInCombat(victim)) return false; // Unconsious actor can not report about crime and should not become hostile if (actor.getClass().getCreatureStats(actor).getKnockedDown()) return false; // Player's followers should not attack player, or try to arrest him if (actor.getClass().getCreatureStats(actor).getAiSequence().hasPackage(AiPackageTypeId::Follow)) { if (playerFollowers.find(actor) != playerFollowers.end()) return false; } return true; } bool MechanicsManager::reportCrime(const MWWorld::Ptr &player, const MWWorld::Ptr &victim, OffenseType type, const std::string& factionId, int arg) { const MWWorld::Store& store = MWBase::Environment::get().getWorld()->getStore().get(); if (type == OT_Murder && !victim.isEmpty()) victim.getClass().getCreatureStats(victim).notifyMurder(); // Bounty and disposition penalty for each type of crime float disp = 0.f, dispVictim = 0.f; if (type == OT_Trespassing || type == OT_SleepingInOwnedBed) { arg = store.find("iCrimeTresspass")->mValue.getInteger(); disp = dispVictim = store.find("iDispTresspass")->mValue.getFloat(); } else if (type == OT_Pickpocket) { arg = store.find("iCrimePickPocket")->mValue.getInteger(); disp = dispVictim = store.find("fDispPickPocketMod")->mValue.getFloat(); } else if (type == OT_Assault) { arg = store.find("iCrimeAttack")->mValue.getInteger(); disp = store.find("iDispAttackMod")->mValue.getFloat(); dispVictim = store.find("fDispAttacking")->mValue.getFloat(); } else if (type == OT_Murder) { arg = store.find("iCrimeKilling")->mValue.getInteger(); disp = dispVictim = store.find("iDispKilling")->mValue.getFloat(); } else if (type == OT_Theft) { disp = dispVictim = store.find("fDispStealing")->mValue.getFloat() * arg; arg = static_cast(arg * store.find("fCrimeStealing")->mValue.getFloat()); arg = std::max(1, arg); // Minimum bounty of 1, in case items with zero value are stolen } // Make surrounding actors within alarm distance respond to the crime std::vector neighbors; const MWWorld::ESMStore& esmStore = MWBase::Environment::get().getWorld()->getStore(); osg::Vec3f from (player.getRefData().getPosition().asVec3()); float radius = esmStore.get().find("fAlarmRadius")->mValue.getFloat(); mActors.getObjectsInRange(from, radius, neighbors); // victim should be considered even beyond alarm radius if (!victim.isEmpty() && (from - victim.getRefData().getPosition().asVec3()).length2() > radius*radius) neighbors.push_back(victim); int id = MWBase::Environment::get().getWorld()->getPlayer().getNewCrimeId(); // What amount of provocation did this crime generate? // Controls whether witnesses will engage combat with the criminal. int fight = 0, fightVictim = 0; if (type == OT_Trespassing || type == OT_SleepingInOwnedBed) fight = fightVictim = esmStore.get().find("iFightTrespass")->mValue.getInteger(); else if (type == OT_Pickpocket) { fight = esmStore.get().find("iFightPickpocket")->mValue.getInteger(); fightVictim = esmStore.get().find("iFightPickpocket")->mValue.getInteger() * 4; // *4 according to research wiki } else if (type == OT_Assault) { fight = esmStore.get().find("iFightAttacking")->mValue.getInteger(); fightVictim = esmStore.get().find("iFightAttack")->mValue.getInteger(); } else if (type == OT_Murder) fight = fightVictim = esmStore.get().find("iFightKilling")->mValue.getInteger(); else if (type == OT_Theft) fight = fightVictim = esmStore.get().find("fFightStealing")->mValue.getInteger(); bool reported = false; std::set playerFollowers; getActorsSidingWith(player, playerFollowers); // Tell everyone (including the original reporter) in alarm range for (const MWWorld::Ptr &actor : neighbors) { if (!canReportCrime(actor, victim, playerFollowers)) continue; // Will the witness report the crime? if (actor.getClass().getCreatureStats(actor).getAiSetting(CreatureStats::AI_Alarm).getBase() >= 100) { reported = true; if (type == OT_Trespassing) MWBase::Environment::get().getDialogueManager()->say(actor, "intruder"); } } for (const MWWorld::Ptr &actor : neighbors) { if (!canReportCrime(actor, victim, playerFollowers)) continue; if (reported && actor.getClass().isClass(actor, "guard")) { // Mark as Alarmed for dialogue actor.getClass().getCreatureStats(actor).setAlarmed(true); // Set the crime ID, which we will use to calm down participants // once the bounty has been paid. actor.getClass().getNpcStats(actor).setCrimeId(id); if (!actor.getClass().getCreatureStats(actor).getAiSequence().hasPackage(AiPackageTypeId::Pursue)) { actor.getClass().getCreatureStats(actor).getAiSequence().stack(AiPursue(player), actor); } } else { float dispTerm = (actor == victim) ? dispVictim : disp; float alarmTerm = 0.01f * actor.getClass().getCreatureStats(actor).getAiSetting(CreatureStats::AI_Alarm).getBase(); if (type == OT_Pickpocket && alarmTerm <= 0) alarmTerm = 1.0; if (actor != victim) dispTerm *= alarmTerm; float fightTerm = static_cast((actor == victim) ? fightVictim : fight); fightTerm += getFightDispositionBias(dispTerm); fightTerm += getFightDistanceBias(actor, player); fightTerm *= alarmTerm; int observerFightRating = actor.getClass().getCreatureStats(actor).getAiSetting(CreatureStats::AI_Fight).getBase(); if (observerFightRating + fightTerm > 100) fightTerm = static_cast(100 - observerFightRating); fightTerm = std::max(0.f, fightTerm); if (observerFightRating + fightTerm >= 100) { startCombat(actor, player); NpcStats& observerStats = actor.getClass().getNpcStats(actor); // Apply aggression value to the base Fight rating, so that the actor can continue fighting // after a Calm spell wears off observerStats.setAiSetting(CreatureStats::AI_Fight, observerFightRating + static_cast(fightTerm)); observerStats.setBaseDisposition(observerStats.getBaseDisposition() + static_cast(dispTerm)); // Set the crime ID, which we will use to calm down participants // once the bounty has been paid. observerStats.setCrimeId(id); // Mark as Alarmed for dialogue observerStats.setAlarmed(true); } } } if (reported) { player.getClass().getNpcStats(player).setBounty(player.getClass().getNpcStats(player).getBounty() + arg); // If committing a crime against a faction member, expell from the faction if (!victim.isEmpty() && victim.getClass().isNpc()) { std::string factionID = victim.getClass().getPrimaryFaction(victim); const std::map& playerRanks = player.getClass().getNpcStats(player).getFactionRanks(); if (playerRanks.find(Misc::StringUtils::lowerCase(factionID)) != playerRanks.end()) { /* Start of tes3mp addition Send an ID_PLAYER_FACTION packet every time a player is expelled from a faction */ mwmp::Main::get().getLocalPlayer()->sendFactionExpulsionState(Misc::StringUtils::lowerCase(factionID), true); /* End of tes3mp addition */ player.getClass().getNpcStats(player).expell(factionID); } } else if (!factionId.empty()) { const std::map& playerRanks = player.getClass().getNpcStats(player).getFactionRanks(); if (playerRanks.find(Misc::StringUtils::lowerCase(factionId)) != playerRanks.end()) { player.getClass().getNpcStats(player).expell(factionId); } } if (type == OT_Assault && !victim.isEmpty() && !victim.getClass().getCreatureStats(victim).getAiSequence().isInCombat(player) && victim.getClass().isNpc()) { // Attacker is in combat with us, but we are not in combat with the attacker yet. Time to fight back. // Note: accidental or collateral damage attacks are ignored. if (!victim.getClass().getCreatureStats(victim).getAiSequence().hasPackage(AiPackageTypeId::Pursue)) startCombat(victim, player); // Set the crime ID, which we will use to calm down participants // once the bounty has been paid. victim.getClass().getNpcStats(victim).setCrimeId(id); } } return reported; } bool MechanicsManager::actorAttacked(const MWWorld::Ptr &target, const MWWorld::Ptr &attacker) { const MWWorld::Ptr& player = getPlayer(); if (target == player || !attacker.getClass().isActor()) return false; /* Start of tes3mp change (major) Don't set DedicatedPlayers as being in combat with the attacker, to prevent AI actors from deciding to reciprocate by also starting combat */ if (mwmp::PlayerList::isDedicatedPlayer(target)) return false; /* End of tes3mp change (major) */ MWMechanics::CreatureStats& statsTarget = target.getClass().getCreatureStats(target); /* Start of tes3mp change (major) Allow collateral damage from dedicated players as well */ if (attacker == player || mwmp::PlayerList::isDedicatedPlayer(attacker)) /* End of tes3mp change (major) */ { std::set followersAttacker; getActorsSidingWith(attacker, followersAttacker); /* Start of tes3mp change (major) Check not only whether the target is on the same side as the attacker, but also whether the attacker is on the same side as the target, thus allowing for NPC companions of one player to forgive another player when those players are allied */ std::set followersTarget; getActorsSidingWith(target, followersTarget); if (followersAttacker.find(target) != followersAttacker.end() || followersTarget.find(attacker) != followersTarget.end()) /* End of tes3mp change (major) */ { statsTarget.friendlyHit(); /* Start of tes3mp change (major) Due to a greater propensity for collateral damage in multiplayer, allow more friendly hits TODO: Allow the server to change the count of the friendly hits */ if (statsTarget.getFriendlyHits() < 8) /* End of tes3mp change (major) */ { MWBase::Environment::get().getDialogueManager()->say(target, "hit"); return false; } } } if (canCommitCrimeAgainst(target, attacker)) commitCrime(attacker, target, MWBase::MechanicsManager::OT_Assault); AiSequence& seq = statsTarget.getAiSequence(); /* Start of tes3mp change (major) Make it possible to start combat with DedicatedPlayers and DedicatedActors by adding additional conditions for them */ if (!attacker.isEmpty() && (attacker.getClass().getCreatureStats(attacker).getAiSequence().isInCombat(target) || attacker == player || mwmp::PlayerList::isDedicatedPlayer(attacker) || mwmp::Main::get().getCellController()->isDedicatedActor(attacker)) && !seq.isInCombat(attacker)) /* End of tes3mp change (major) */ { // Attacker is in combat with us, but we are not in combat with the attacker yet. Time to fight back. // Note: accidental or collateral damage attacks are ignored. if (!target.getClass().getCreatureStats(target).getAiSequence().hasPackage(AiPackageTypeId::Pursue)) { // If an actor has OnPCHitMe declared in his script, his Fight = 0 and the attacker is player, // he will attack the player only if we will force him (e.g. via StartCombat console command) bool peaceful = false; std::string script = target.getClass().getScript(target); if (!script.empty() && target.getRefData().getLocals().hasVar(script, "onpchitme") && attacker == player) { int fight = std::max(0, target.getClass().getCreatureStats(target).getAiSetting(CreatureStats::AI_Fight).getModified()); peaceful = (fight == 0); } if (!peaceful) startCombat(target, attacker); } } return true; } bool MechanicsManager::canCommitCrimeAgainst(const MWWorld::Ptr &target, const MWWorld::Ptr &attacker) { const MWMechanics::AiSequence& seq = target.getClass().getCreatureStats(target).getAiSequence(); return target.getClass().isNpc() && !attacker.isEmpty() && !seq.isInCombat(attacker) && !isAggressive(target, attacker) && !seq.isEngagedWithActor() && !target.getClass().getCreatureStats(target).getAiSequence().hasPackage(AiPackageTypeId::Pursue); } void MechanicsManager::actorKilled(const MWWorld::Ptr &victim, const MWWorld::Ptr &attacker) { if (attacker.isEmpty() || victim.isEmpty()) return; if (victim == attacker) return; // known to happen if (!victim.getClass().isNpc()) return; // TODO: implement animal rights const MWMechanics::NpcStats& victimStats = victim.getClass().getNpcStats(victim); const MWWorld::Ptr &player = getPlayer(); bool canCommit = attacker == player && canCommitCrimeAgainst(victim, attacker); // For now we report only about crimes of player and player's followers if (attacker != player) { std::set playerFollowers; getActorsSidingWith(player, playerFollowers); if (playerFollowers.find(attacker) == playerFollowers.end()) return; } if (!canCommit && victimStats.getCrimeId() == -1) return; // Simple check for who attacked first: if the player attacked first, a crimeId should be set // Doesn't handle possible edge case where no one reported the assault, but in such a case, // for bystanders it is not possible to tell who attacked first, anyway. commitCrime(player, victim, MWBase::MechanicsManager::OT_Murder); } bool MechanicsManager::awarenessCheck(const MWWorld::Ptr &ptr, const MWWorld::Ptr &observer) { if (observer.getClass().getCreatureStats(observer).isDead() || !observer.getRefData().isEnabled()) return false; const MWWorld::Store& store = MWBase::Environment::get().getWorld()->getStore().get(); CreatureStats& stats = ptr.getClass().getCreatureStats(ptr); float invisibility = stats.getMagicEffects().get(ESM::MagicEffect::Invisibility).getMagnitude(); if (invisibility > 0) return false; float sneakTerm = 0; if (isSneaking(ptr)) { static float fSneakSkillMult = store.find("fSneakSkillMult")->mValue.getFloat(); static float fSneakBootMult = store.find("fSneakBootMult")->mValue.getFloat(); float sneak = static_cast(ptr.getClass().getSkill(ptr, ESM::Skill::Sneak)); float agility = stats.getAttribute(ESM::Attribute::Agility).getModified(); float luck = stats.getAttribute(ESM::Attribute::Luck).getModified(); float bootWeight = 0; if (ptr.getClass().isNpc() && MWBase::Environment::get().getWorld()->isOnGround(ptr)) { const MWWorld::InventoryStore& inv = ptr.getClass().getInventoryStore(ptr); MWWorld::ConstContainerStoreIterator it = inv.getSlot(MWWorld::InventoryStore::Slot_Boots); if (it != inv.end()) bootWeight = it->getClass().getWeight(*it); } sneakTerm = fSneakSkillMult * sneak + 0.2f * agility + 0.1f * luck + bootWeight * fSneakBootMult; } static float fSneakDistBase = store.find("fSneakDistanceBase")->mValue.getFloat(); static float fSneakDistMult = store.find("fSneakDistanceMultiplier")->mValue.getFloat(); osg::Vec3f pos1 (ptr.getRefData().getPosition().asVec3()); osg::Vec3f pos2 (observer.getRefData().getPosition().asVec3()); float distTerm = fSneakDistBase + fSneakDistMult * (pos1 - pos2).length(); float chameleon = stats.getMagicEffects().get(ESM::MagicEffect::Chameleon).getMagnitude(); float x = sneakTerm * distTerm * stats.getFatigueTerm() + chameleon + invisibility; CreatureStats& observerStats = observer.getClass().getCreatureStats(observer); float obsAgility = observerStats.getAttribute(ESM::Attribute::Agility).getModified(); float obsLuck = observerStats.getAttribute(ESM::Attribute::Luck).getModified(); float obsBlind = observerStats.getMagicEffects().get(ESM::MagicEffect::Blind).getMagnitude(); float obsSneak = observer.getClass().getSkill(observer, ESM::Skill::Sneak); float obsTerm = obsSneak + 0.2f * obsAgility + 0.1f * obsLuck - obsBlind; // is ptr behind the observer? static float fSneakNoViewMult = store.find("fSneakNoViewMult")->mValue.getFloat(); static float fSneakViewMult = store.find("fSneakViewMult")->mValue.getFloat(); float y = 0; osg::Vec3f vec = pos1 - pos2; if (observer.getRefData().getBaseNode()) { osg::Vec3f observerDir = (observer.getRefData().getBaseNode()->getAttitude() * osg::Vec3f(0,1,0)); float angleRadians = std::acos(observerDir * vec / (observerDir.length() * vec.length())); if (angleRadians > osg::DegreesToRadians(90.f)) y = obsTerm * observerStats.getFatigueTerm() * fSneakNoViewMult; else y = obsTerm * observerStats.getFatigueTerm() * fSneakViewMult; } float target = x - y; return (Misc::Rng::roll0to99() >= target); } void MechanicsManager::startCombat(const MWWorld::Ptr &ptr, const MWWorld::Ptr &target) { CreatureStats& stats = ptr.getClass().getCreatureStats(ptr); // Don't add duplicate packages nor add packages to dead actors. if (stats.isDead() || stats.getAiSequence().isInCombat(target)) return; // The target is somehow the same as the actor. Early-out. if (ptr == target) { // We don't care about dialogue filters since the target is invalid. // We still want to play the combat taunt. MWBase::Environment::get().getDialogueManager()->say(ptr, "attack"); return; } stats.getAiSequence().stack(MWMechanics::AiCombat(target), ptr); if (target == getPlayer()) { // if guard starts combat with player, guards pursuing player should do the same if (ptr.getClass().isClass(ptr, "Guard")) { stats.setHitAttemptActorId(target.getClass().getCreatureStats(target).getActorId()); // Stops guard from ending combat if player is unreachable for (Actors::PtrActorMap::const_iterator iter = mActors.begin(); iter != mActors.end(); ++iter) { if (iter->first.getClass().isClass(iter->first, "Guard")) { MWMechanics::AiSequence& aiSeq = iter->first.getClass().getCreatureStats(iter->first).getAiSequence(); if (aiSeq.getTypeId() == MWMechanics::AiPackageTypeId::Pursue) { aiSeq.stopPursuit(); aiSeq.stack(MWMechanics::AiCombat(target), ptr); iter->first.getClass().getCreatureStats(iter->first).setHitAttemptActorId(target.getClass().getCreatureStats(target).getActorId()); // Stops guard from ending combat if player is unreachable } } } } } // Must be done after the target is set up, so that CreatureTargetted dialogue filter works properly MWBase::Environment::get().getDialogueManager()->say(ptr, "attack"); } void MechanicsManager::getObjectsInRange(const osg::Vec3f &position, float radius, std::vector &objects) { mActors.getObjectsInRange(position, radius, objects); mObjects.getObjectsInRange(position, radius, objects); } void MechanicsManager::getActorsInRange(const osg::Vec3f &position, float radius, std::vector &objects) { mActors.getObjectsInRange(position, radius, objects); } bool MechanicsManager::isAnyActorInRange(const osg::Vec3f &position, float radius) { return mActors.isAnyObjectInRange(position, radius); } std::list MechanicsManager::getActorsSidingWith(const MWWorld::Ptr& actor) { return mActors.getActorsSidingWith(actor); } std::list MechanicsManager::getActorsFollowing(const MWWorld::Ptr& actor) { return mActors.getActorsFollowing(actor); } std::list MechanicsManager::getActorsFollowingIndices(const MWWorld::Ptr& actor) { return mActors.getActorsFollowingIndices(actor); } std::map MechanicsManager::getActorsFollowingByIndex(const MWWorld::Ptr& actor) { return mActors.getActorsFollowingByIndex(actor); } std::list MechanicsManager::getActorsFighting(const MWWorld::Ptr& actor) { return mActors.getActorsFighting(actor); } std::list MechanicsManager::getEnemiesNearby(const MWWorld::Ptr& actor) { return mActors.getEnemiesNearby(actor); } void MechanicsManager::getActorsFollowing(const MWWorld::Ptr& actor, std::set& out) { mActors.getActorsFollowing(actor, out); } void MechanicsManager::getActorsSidingWith(const MWWorld::Ptr& actor, std::set& out) { mActors.getActorsSidingWith(actor, out); } int MechanicsManager::countSavedGameRecords() const { return 1 // Death counter +1; // Stolen items } void MechanicsManager::write(ESM::ESMWriter &writer, Loading::Listener &listener) const { mActors.write(writer, listener); ESM::StolenItems items; items.mStolenItems = mStolenItems; writer.startRecord(ESM::REC_STLN); items.write(writer); writer.endRecord(ESM::REC_STLN); } void MechanicsManager::readRecord(ESM::ESMReader &reader, uint32_t type) { if (type == ESM::REC_STLN) { ESM::StolenItems items; items.load(reader); mStolenItems = items.mStolenItems; } else mActors.readRecord(reader, type); } void MechanicsManager::clear() { mActors.clear(); mStolenItems.clear(); mClassSelected = false; mRaceSelected = false; } bool MechanicsManager::isAggressive(const MWWorld::Ptr &ptr, const MWWorld::Ptr &target) { // Don't become aggressive if a calm effect is active, since it would cause combat to cycle on/off as // combat is activated here and then canceled by the calm effect if ((ptr.getClass().isNpc() && ptr.getClass().getCreatureStats(ptr).getMagicEffects().get(ESM::MagicEffect::CalmHumanoid).getMagnitude() > 0) || (!ptr.getClass().isNpc() && ptr.getClass().getCreatureStats(ptr).getMagicEffects().get(ESM::MagicEffect::CalmCreature).getMagnitude() > 0)) return false; int disposition = 50; if (ptr.getClass().isNpc()) disposition = getDerivedDisposition(ptr, true); int fight = ptr.getClass().getCreatureStats(ptr).getAiSetting(CreatureStats::AI_Fight).getModified() + static_cast(getFightDistanceBias(ptr, target) + getFightDispositionBias(static_cast(disposition))); if (ptr.getClass().isNpc() && target.getClass().isNpc()) { if (target.getClass().getNpcStats(target).isWerewolf() || (target == getPlayer() && MWBase::Environment::get().getWorld()->getGlobalInt("pcknownwerewolf"))) { const ESM::GameSetting * iWerewolfFightMod = MWBase::Environment::get().getWorld()->getStore().get().find("iWerewolfFightMod"); fight += iWerewolfFightMod->mValue.getInteger(); } } return (fight >= 100); } void MechanicsManager::resurrect(const MWWorld::Ptr &ptr) { CreatureStats& stats = ptr.getClass().getCreatureStats(ptr); if (stats.isDead()) { stats.resurrect(); mActors.resurrect(ptr); } } bool MechanicsManager::isCastingSpell(const MWWorld::Ptr &ptr) const { return mActors.isCastingSpell(ptr); } bool MechanicsManager::isReadyToBlock(const MWWorld::Ptr &ptr) const { return mActors.isReadyToBlock(ptr); } bool MechanicsManager::isAttackingOrSpell(const MWWorld::Ptr &ptr) const { return mActors.isAttackingOrSpell(ptr); } /* Start of tes3mp addition Make it possible to set the attackingOrSpell state from elsewhere in the code */ void MechanicsManager::setAttackingOrSpell(const MWWorld::Ptr &ptr, bool state) const { return mActors.setAttackingOrSpell(ptr, state); } /* End of tes3mp addition */ void MechanicsManager::setWerewolf(const MWWorld::Ptr& actor, bool werewolf) { MWMechanics::NpcStats& npcStats = actor.getClass().getNpcStats(actor); // The actor does not have to change state if (npcStats.isWerewolf() == werewolf) return; MWWorld::Player* player = &MWBase::Environment::get().getWorld()->getPlayer(); if (actor == player->getPlayer()) { if (werewolf) { player->saveStats(); player->setWerewolfStats(); } else player->restoreStats(); } // Werewolfs can not cast spells, so we need to unset the prepared spell if there is one. if (npcStats.getDrawState() == MWMechanics::DrawState_Spell) npcStats.setDrawState(MWMechanics::DrawState_Nothing); npcStats.setWerewolf(werewolf); MWWorld::InventoryStore &inv = actor.getClass().getInventoryStore(actor); if(werewolf) { inv.unequipAll(actor); inv.equip(MWWorld::InventoryStore::Slot_Robe, inv.ContainerStore::add("werewolfrobe", 1, actor), actor); } else { inv.unequipSlot(MWWorld::InventoryStore::Slot_Robe, actor); inv.ContainerStore::remove("werewolfrobe", 1, actor); } if(actor == player->getPlayer()) { MWBase::Environment::get().getWorld()->reattachPlayerCamera(); // Update the GUI only when called on the player MWBase::WindowManager* windowManager = MWBase::Environment::get().getWindowManager(); if (werewolf) { windowManager->forceHide(MWGui::GW_Inventory); windowManager->forceHide(MWGui::GW_Magic); } else { windowManager->unsetForceHide(MWGui::GW_Inventory); windowManager->unsetForceHide(MWGui::GW_Magic); } windowManager->setWerewolfOverlay(werewolf); // Witnesses of the player's transformation will make them a globally known werewolf std::vector neighbors; const MWWorld::Store& gmst = MWBase::Environment::get().getWorld()->getStore().get(); getActorsInRange(actor.getRefData().getPosition().asVec3(), gmst.find("fAlarmRadius")->mValue.getFloat(), neighbors); bool detected = false, reported = false; for (const MWWorld::Ptr& neighbor : neighbors) { if (neighbor == actor || !neighbor.getClass().isNpc()) continue; if (MWBase::Environment::get().getWorld()->getLOS(neighbor, actor) && awarenessCheck(actor, neighbor)) { detected = true; if (neighbor.getClass().getCreatureStats(neighbor).getAiSetting(MWMechanics::CreatureStats::AI_Alarm).getModified() > 0) { reported = true; break; } } } if (detected) { windowManager->messageBox("#{sWerewolfAlarmMessage}"); MWBase::Environment::get().getWorld()->setGlobalInt("pcknownwerewolf", 1); if (reported) { npcStats.setBounty(npcStats.getBounty()+ gmst.find("iWereWolfBounty")->mValue.getInteger()); } } } } void MechanicsManager::applyWerewolfAcrobatics(const MWWorld::Ptr &actor) { const MWWorld::Store& gmst = MWBase::Environment::get().getWorld()->getStore().get(); MWMechanics::NpcStats &stats = actor.getClass().getNpcStats(actor); stats.getSkill(ESM::Skill::Acrobatics).setBase(gmst.find("fWerewolfAcrobatics")->mValue.getInteger()); } void MechanicsManager::cleanupSummonedCreature(const MWWorld::Ptr &caster, int creatureActorId) { mActors.cleanupSummonedCreature(caster.getClass().getCreatureStats(caster), creatureActorId); } void MechanicsManager::reportStats(unsigned int frameNumber, osg::Stats& stats) const { stats.setAttribute(frameNumber, "Mechanics Actors", mActors.size()); stats.setAttribute(frameNumber, "Mechanics Objects", mObjects.size()); } int MechanicsManager::getGreetingTimer(const MWWorld::Ptr &ptr) const { return mActors.getGreetingTimer(ptr); } float MechanicsManager::getAngleToPlayer(const MWWorld::Ptr &ptr) const { return mActors.getAngleToPlayer(ptr); } GreetingState MechanicsManager::getGreetingState(const MWWorld::Ptr &ptr) const { return mActors.getGreetingState(ptr); } bool MechanicsManager::isTurningToPlayer(const MWWorld::Ptr &ptr) const { return mActors.isTurningToPlayer(ptr); } } ================================================ FILE: apps/openmw/mwmechanics/mechanicsmanagerimp.hpp ================================================ #ifndef GAME_MWMECHANICS_MECHANICSMANAGERIMP_H #define GAME_MWMECHANICS_MECHANICSMANAGERIMP_H #include #include "../mwbase/mechanicsmanager.hpp" #include "../mwworld/ptr.hpp" #include "creaturestats.hpp" #include "npcstats.hpp" #include "objects.hpp" #include "actors.hpp" namespace MWWorld { class CellStore; } namespace MWMechanics { class MechanicsManager : public MWBase::MechanicsManager { bool mUpdatePlayer; bool mClassSelected; bool mRaceSelected; bool mAI;///< is AI active? Objects mObjects; Actors mActors; typedef std::pair Owner; // < Owner id, bool isFaction > typedef std::map OwnerMap; // < Owner, number of stolen items with this id from this owner > typedef std::map StolenItemsMap; StolenItemsMap mStolenItems; public: void buildPlayer(); ///< build player according to stored class/race/birthsign information. Will /// default to the values of the ESM::NPC object, if no explicit information is given. MechanicsManager(); void add (const MWWorld::Ptr& ptr) override; ///< Register an object for management void remove (const MWWorld::Ptr& ptr) override; ///< Deregister an object for management void updateCell(const MWWorld::Ptr &old, const MWWorld::Ptr &ptr) override; ///< Moves an object to a new cell void drop(const MWWorld::CellStore *cellStore) override; ///< Deregister all objects in the given cell. void update (float duration, bool paused) override; ///< Update objects /// /// \param paused In game type does not currently advance (this usually means some GUI /// component is up). void setPlayerName (const std::string& name) override; ///< Set player name. void setPlayerRace (const std::string& id, bool male, const std::string &head, const std::string &hair) override; ///< Set player race. void setPlayerBirthsign (const std::string& id) override; ///< Set player birthsign. void setPlayerClass (const std::string& id) override; ///< Set player class to stock class. void setPlayerClass (const ESM::Class& class_) override; ///< Set player class to custom class. void restoreDynamicStats(MWWorld::Ptr actor, double hours, bool sleep) override; void rest(double hours, bool sleep) override; ///< If the player is sleeping or waiting, this should be called every hour. /// @param sleep is the player sleeping or waiting? int getHoursToRest() const override; ///< Calculate how many hours the player needs to rest in order to be fully healed int getBarterOffer(const MWWorld::Ptr& ptr,int basePrice, bool buying) override; ///< This is used by every service to determine the price of objects given the trading skills of the player and NPC. int getDerivedDisposition(const MWWorld::Ptr& ptr, bool addTemporaryDispositionChange = true) override; ///< Calculate the diposition of an NPC toward the player. int countDeaths (const std::string& id) const override; ///< Return the number of deaths for actors with the given ID. /* Start of tes3mp addition Make it possible to set the number of deaths for an actor with the given refId */ virtual void setDeaths(const std::string& refId, int number); /* End of tes3mp addition */ void getPersuasionDispositionChange(const MWWorld::Ptr& npc, PersuasionType type, bool& success, float& tempChange, float& permChange) override; ///< Perform a persuasion action on NPC /// Check if \a observer is potentially aware of \a ptr. Does not do a line of sight check! bool awarenessCheck (const MWWorld::Ptr& ptr, const MWWorld::Ptr& observer) override; /// Makes \a ptr fight \a target. Also shouts a combat taunt. void startCombat (const MWWorld::Ptr& ptr, const MWWorld::Ptr& target) override; /** * @note victim may be empty * @param arg Depends on \a type, e.g. for Theft, the value of the item that was stolen. * @param victimAware Is the victim already aware of the crime? * If this parameter is false, it will be determined by a line-of-sight and awareness check. * @return was the crime seen? */ bool commitCrime (const MWWorld::Ptr& ptr, const MWWorld::Ptr& victim, OffenseType type, const std::string& factionId="", int arg=0, bool victimAware=false) override; /// @return false if the attack was considered a "friendly hit" and forgiven bool actorAttacked (const MWWorld::Ptr& victim, const MWWorld::Ptr& attacker) override; /// Notify that actor was killed, add a murder bounty if applicable /// @note No-op for non-player attackers void actorKilled (const MWWorld::Ptr& victim, const MWWorld::Ptr& attacker) override; /// Utility to check if taking this item is illegal and calling commitCrime if so /// @param container The container the item is in; may be empty for an item in the world void itemTaken (const MWWorld::Ptr& ptr, const MWWorld::Ptr& item, const MWWorld::Ptr& container, int count, bool alarm = true) override; /// Utility to check if unlocking this object is illegal and calling commitCrime if so void unlockAttempted (const MWWorld::Ptr& ptr, const MWWorld::Ptr& item) override; /// Attempt sleeping in a bed. If this is illegal, call commitCrime. /// @return was it illegal, and someone saw you doing it? Also returns fail when enemies are nearby bool sleepInBed (const MWWorld::Ptr& ptr, const MWWorld::Ptr& bed) override; void forceStateUpdate(const MWWorld::Ptr &ptr) override; /// Attempt to play an animation group /// @return Success or error bool playAnimationGroup(const MWWorld::Ptr& ptr, const std::string& groupName, int mode, int number, bool persist=false) override; void skipAnimation(const MWWorld::Ptr& ptr) override; bool checkAnimationPlaying(const MWWorld::Ptr& ptr, const std::string &groupName) override; void persistAnimationStates() override; /// Update magic effects for an actor. Usually done automatically once per frame, but if we're currently /// paused we may want to do it manually (after equipping permanent enchantment) void updateMagicEffects (const MWWorld::Ptr& ptr) override; void getObjectsInRange (const osg::Vec3f& position, float radius, std::vector& objects) override; void getActorsInRange(const osg::Vec3f &position, float radius, std::vector &objects) override; /// Check if there are actors in selected range bool isAnyActorInRange(const osg::Vec3f &position, float radius) override; std::list getActorsSidingWith(const MWWorld::Ptr& actor) override; std::list getActorsFollowing(const MWWorld::Ptr& actor) override; std::list getActorsFollowingIndices(const MWWorld::Ptr& actor) override; std::map getActorsFollowingByIndex(const MWWorld::Ptr& actor) override; std::list getActorsFighting(const MWWorld::Ptr& actor) override; std::list getEnemiesNearby(const MWWorld::Ptr& actor) override; /// Recursive version of getActorsFollowing void getActorsFollowing(const MWWorld::Ptr& actor, std::set& out) override; /// Recursive version of getActorsSidingWith void getActorsSidingWith(const MWWorld::Ptr& actor, std::set& out) override; bool toggleAI() override; bool isAIActive() override; void playerLoaded() override; bool onOpen(const MWWorld::Ptr& ptr) override; void onClose(const MWWorld::Ptr& ptr) override; int countSavedGameRecords() const override; void write (ESM::ESMWriter& writer, Loading::Listener& listener) const override; void readRecord (ESM::ESMReader& reader, uint32_t type) override; void clear() override; bool isAggressive (const MWWorld::Ptr& ptr, const MWWorld::Ptr& target) override; void resurrect(const MWWorld::Ptr& ptr) override; bool isCastingSpell (const MWWorld::Ptr& ptr) const override; bool isReadyToBlock (const MWWorld::Ptr& ptr) const override; /// Is \a ptr casting spell or using weapon now? bool isAttackingOrSpell(const MWWorld::Ptr &ptr) const override; /* Start of tes3mp addition Make it possible to set the attackingOrSpell state from elsewhere in the code */ virtual void setAttackingOrSpell(const MWWorld::Ptr &ptr, bool state) const override; /* End of tes3mp addition */ void castSpell(const MWWorld::Ptr& ptr, const std::string spellId, bool manualSpell=false) override; void processChangedSettings(const Settings::CategorySettingVector& settings) override; float getActorsProcessingRange() const override; void notifyDied(const MWWorld::Ptr& actor) override; /// Check if the target actor was detected by an observer /// If the observer is a non-NPC, check all actors in AI processing distance as observers bool isActorDetected(const MWWorld::Ptr& actor, const MWWorld::Ptr& observer) override; void confiscateStolenItems (const MWWorld::Ptr& player, const MWWorld::Ptr& targetContainer) override; /// List the owners that the player has stolen this item from (the owner can be an NPC or a faction). /// std::vector > getStolenItemOwners(const std::string& itemid) override; /// Has the player stolen this item from the given owner? bool isItemStolenFrom(const std::string& itemid, const MWWorld::Ptr& ptr) override; bool isBoundItem(const MWWorld::Ptr& item) override; /* Start of tes3mp addition Make it possible to check if an itemId corresponds to a bound item */ virtual bool isBoundItem(std::string itemId); /* End of tes3mp addition */ /// @return is \a ptr allowed to take/use \a target or is it a crime? bool isAllowedToUse (const MWWorld::Ptr& ptr, const MWWorld::Ptr& target, MWWorld::Ptr& victim) override; void setWerewolf(const MWWorld::Ptr& actor, bool werewolf) override; void applyWerewolfAcrobatics(const MWWorld::Ptr& actor) override; void cleanupSummonedCreature(const MWWorld::Ptr& caster, int creatureActorId) override; void confiscateStolenItemToOwner(const MWWorld::Ptr &player, const MWWorld::Ptr &item, const MWWorld::Ptr& victim, int count) override; bool isAttackPreparing(const MWWorld::Ptr& ptr) override; bool isRunning(const MWWorld::Ptr& ptr) override; bool isSneaking(const MWWorld::Ptr& ptr) override; void reportStats(unsigned int frameNumber, osg::Stats& stats) const override; int getGreetingTimer(const MWWorld::Ptr& ptr) const override; float getAngleToPlayer(const MWWorld::Ptr& ptr) const override; GreetingState getGreetingState(const MWWorld::Ptr& ptr) const override; bool isTurningToPlayer(const MWWorld::Ptr& ptr) const override; void restoreStatsAfterCorprus(const MWWorld::Ptr& actor, const std::string& sourceId) override; private: bool canCommitCrimeAgainst(const MWWorld::Ptr& victim, const MWWorld::Ptr& attacker); bool canReportCrime(const MWWorld::Ptr &actor, const MWWorld::Ptr &victim, std::set &playerFollowers); bool reportCrime (const MWWorld::Ptr& ptr, const MWWorld::Ptr& victim, OffenseType type, const std::string& factionId, int arg=0); }; } #endif ================================================ FILE: apps/openmw/mwmechanics/movement.hpp ================================================ #ifndef GAME_MWMECHANICS_MOVEMENT_H #define GAME_MWMECHANICS_MOVEMENT_H #include namespace MWMechanics { /// Desired movement for an actor struct Movement { // Desired movement. Direction is relative to the current orientation. // Length of the vector controls desired speed. 0 - stay, 0.5 - half-speed, 1.0 - max speed. float mPosition[3]; // Desired rotation delta (euler angles). float mRotation[3]; // Controlled by CharacterController, should not be changed from other places. // These fields can not be private fields in CharacterController, because Actor::getCurrentSpeed uses it. float mSpeedFactor; bool mIsStrafing; Movement() { mPosition[0] = mPosition[1] = mPosition[2] = 0.0f; mRotation[0] = mRotation[1] = mRotation[2] = 0.0f; mSpeedFactor = 1.f; mIsStrafing = false; } osg::Vec3f asVec3() { return osg::Vec3f(mPosition[0], mPosition[1], mPosition[2]); } }; } #endif ================================================ FILE: apps/openmw/mwmechanics/npcstats.cpp ================================================ #include "npcstats.hpp" #include #include #include #include #include #include "../mwworld/esmstore.hpp" #include "../mwbase/environment.hpp" #include "../mwbase/world.hpp" #include "../mwbase/windowmanager.hpp" MWMechanics::NpcStats::NpcStats() : mDisposition (0) , mReputation(0) , mCrimeId(-1) , mBounty(0) , mWerewolfKills (0) , mLevelProgress(0) , mTimeToStartDrowning(-1.0) // set breath to special value, it will be replaced during actor update , mIsWerewolf(false) { mSkillIncreases.resize (ESM::Attribute::Length, 0); mSpecIncreases.resize(3, 0); } int MWMechanics::NpcStats::getBaseDisposition() const { return mDisposition; } void MWMechanics::NpcStats::setBaseDisposition(int disposition) { mDisposition = disposition; } const MWMechanics::SkillValue& MWMechanics::NpcStats::getSkill (int index) const { if (index<0 || index>=ESM::Skill::Length) throw std::runtime_error ("skill index out of range"); return mSkill[index]; } MWMechanics::SkillValue& MWMechanics::NpcStats::getSkill (int index) { if (index<0 || index>=ESM::Skill::Length) throw std::runtime_error ("skill index out of range"); return mSkill[index]; } void MWMechanics::NpcStats::setSkill(int index, const MWMechanics::SkillValue &value) { if (index<0 || index>=ESM::Skill::Length) throw std::runtime_error ("skill index out of range"); mSkill[index] = value; } const std::map& MWMechanics::NpcStats::getFactionRanks() const { return mFactionRank; } int MWMechanics::NpcStats::getFactionRank(const std::string &faction) const { const std::string lower = Misc::StringUtils::lowerCase(faction); std::map::const_iterator it = mFactionRank.find(lower); if (it != mFactionRank.end()) return it->second; return -1; } void MWMechanics::NpcStats::raiseRank(const std::string &faction) { const std::string lower = Misc::StringUtils::lowerCase(faction); std::map::iterator it = mFactionRank.find(lower); if (it != mFactionRank.end()) { // Does the next rank exist? const ESM::Faction* factionPtr = MWBase::Environment::get().getWorld()->getStore().get().find(lower); if (it->second+1 < 10 && !factionPtr->mRanks[it->second+1].empty()) it->second += 1; } } void MWMechanics::NpcStats::lowerRank(const std::string &faction) { const std::string lower = Misc::StringUtils::lowerCase(faction); std::map::iterator it = mFactionRank.find(lower); if (it != mFactionRank.end()) { it->second = it->second-1; if (it->second < 0) { mFactionRank.erase(it); mExpelled.erase(lower); } } } void MWMechanics::NpcStats::joinFaction(const std::string& faction) { const std::string lower = Misc::StringUtils::lowerCase(faction); std::map::iterator it = mFactionRank.find(lower); if (it == mFactionRank.end()) mFactionRank[lower] = 0; } bool MWMechanics::NpcStats::getExpelled(const std::string& factionID) const { return mExpelled.find(Misc::StringUtils::lowerCase(factionID)) != mExpelled.end(); } void MWMechanics::NpcStats::expell(const std::string& factionID) { std::string lower = Misc::StringUtils::lowerCase(factionID); if (mExpelled.find(lower) == mExpelled.end()) { std::string message = "#{sExpelledMessage}"; message += MWBase::Environment::get().getWorld()->getStore().get().find(factionID)->mName; MWBase::Environment::get().getWindowManager()->messageBox(message); mExpelled.insert(lower); } } void MWMechanics::NpcStats::clearExpelled(const std::string& factionID) { mExpelled.erase(Misc::StringUtils::lowerCase(factionID)); } bool MWMechanics::NpcStats::isInFaction (const std::string& faction) const { return (mFactionRank.find(Misc::StringUtils::lowerCase(faction)) != mFactionRank.end()); } int MWMechanics::NpcStats::getFactionReputation (const std::string& faction) const { std::map::const_iterator iter = mFactionReputation.find (Misc::StringUtils::lowerCase(faction)); if (iter==mFactionReputation.end()) return 0; return iter->second; } void MWMechanics::NpcStats::setFactionReputation (const std::string& faction, int value) { mFactionReputation[Misc::StringUtils::lowerCase(faction)] = value; } float MWMechanics::NpcStats::getSkillProgressRequirement (int skillIndex, const ESM::Class& class_) const { float progressRequirement = static_cast(1 + getSkill(skillIndex).getBase()); const MWWorld::Store &gmst = MWBase::Environment::get().getWorld()->getStore().get(); float typeFactor = gmst.find ("fMiscSkillBonus")->mValue.getFloat(); for (int i=0; i<5; ++i) { if (class_.mData.mSkills[i][0]==skillIndex) { typeFactor = gmst.find ("fMinorSkillBonus")->mValue.getFloat(); break; } else if (class_.mData.mSkills[i][1]==skillIndex) { typeFactor = gmst.find ("fMajorSkillBonus")->mValue.getFloat(); break; } } progressRequirement *= typeFactor; if (typeFactor<=0) throw std::runtime_error ("invalid skill type factor"); float specialisationFactor = 1; const ESM::Skill *skill = MWBase::Environment::get().getWorld()->getStore().get().find (skillIndex); if (skill->mData.mSpecialization==class_.mData.mSpecialization) { specialisationFactor = gmst.find ("fSpecialSkillBonus")->mValue.getFloat(); if (specialisationFactor<=0) throw std::runtime_error ("invalid skill specialisation factor"); } progressRequirement *= specialisationFactor; return progressRequirement; } void MWMechanics::NpcStats::useSkill (int skillIndex, const ESM::Class& class_, int usageType, float extraFactor) { const ESM::Skill *skill = MWBase::Environment::get().getWorld()->getStore().get().find (skillIndex); float skillGain = 1; if (usageType>=4) throw std::runtime_error ("skill usage type out of range"); if (usageType>=0) { skillGain = skill->mData.mUseValue[usageType]; if (skillGain<0) throw std::runtime_error ("invalid skill gain factor"); } skillGain *= extraFactor; MWMechanics::SkillValue& value = getSkill (skillIndex); value.setProgress(value.getProgress() + skillGain); if (int(value.getProgress())>=int(getSkillProgressRequirement(skillIndex, class_))) { // skill levelled up increaseSkill(skillIndex, class_, false); } } void MWMechanics::NpcStats::increaseSkill(int skillIndex, const ESM::Class &class_, bool preserveProgress, bool readBook) { float base = getSkill (skillIndex).getBase(); if (base >= 100.f) return; base += 1; const MWWorld::Store &gmst = MWBase::Environment::get().getWorld()->getStore().get(); // is this a minor or major skill? int increase = gmst.find("iLevelupMiscMultAttriubte")->mValue.getInteger(); // Note: GMST has a typo for (int k=0; k<5; ++k) { if (class_.mData.mSkills[k][0] == skillIndex) { mLevelProgress += gmst.find("iLevelUpMinorMult")->mValue.getInteger(); increase = gmst.find("iLevelUpMinorMultAttribute")->mValue.getInteger(); break; } else if (class_.mData.mSkills[k][1] == skillIndex) { mLevelProgress += gmst.find("iLevelUpMajorMult")->mValue.getInteger(); increase = gmst.find("iLevelUpMajorMultAttribute")->mValue.getInteger(); break; } } const ESM::Skill* skill = MWBase::Environment::get().getWorld ()->getStore ().get().find(skillIndex); mSkillIncreases[skill->mData.mAttribute] += increase; mSpecIncreases[skill->mData.mSpecialization] += gmst.find("iLevelupSpecialization")->mValue.getInteger(); // Play sound & skill progress notification /// \todo check if character is the player, if levelling is ever implemented for NPCs MWBase::Environment::get().getWindowManager()->playSound("skillraise"); std::string message = MWBase::Environment::get().getWindowManager ()->getGameSettingString ("sNotifyMessage39", ""); message = Misc::StringUtils::format(message, ("#{" + ESM::Skill::sSkillNameIds[skillIndex] + "}"), static_cast(base)); if (readBook) message = "#{sBookSkillMessage}\n" + message; MWBase::Environment::get().getWindowManager ()->messageBox(message, MWGui::ShowInDialogueMode_Never); if (mLevelProgress >= gmst.find("iLevelUpTotal")->mValue.getInteger()) { // levelup is possible now MWBase::Environment::get().getWindowManager ()->messageBox ("#{sLevelUpMsg}", MWGui::ShowInDialogueMode_Never); } getSkill(skillIndex).setBase (base); if (!preserveProgress) getSkill(skillIndex).setProgress(0); } int MWMechanics::NpcStats::getLevelProgress () const { return mLevelProgress; } /* Start of tes3mp addition Make it possible to set a player's level progress directly instead of going through other methods */ void MWMechanics::NpcStats::setLevelProgress(int value) { mLevelProgress = value; } /* End of tes3mp addition */ /* Start of tes3mp addition Make it possible to get a player's skill increases for an attribute directly instead of going through other methods */ int MWMechanics::NpcStats::getSkillIncrease(int attribute) const { return mSkillIncreases[attribute]; } /* End of tes3mp addition */ /* Start of tes3mp addition Make it possible to set a player's skill increases for an attribute directly instead of going through other methods */ void MWMechanics::NpcStats::setSkillIncrease(int attribute, int value) { mSkillIncreases[attribute] = value; } /* End of tes3mp addition */ /* Start of tes3mp addition Make it possible to get and set the time of the last crime witnessed by the NPC, used to stop combat with a player after that player dies and is resurrected */ std::time_t MWMechanics::NpcStats::getCrimeTime() { return mCrimeTime; } void MWMechanics::NpcStats::setCrimeTime(std::time_t crimeTime) { mCrimeTime = crimeTime; } /* End of tes3mp addition */ void MWMechanics::NpcStats::levelUp() { const MWWorld::Store &gmst = MWBase::Environment::get().getWorld()->getStore().get(); mLevelProgress -= gmst.find("iLevelUpTotal")->mValue.getInteger(); mLevelProgress = std::max(0, mLevelProgress); // might be necessary when levelup was invoked via console for (int i=0; imValue.getFloat(); MWMechanics::DynamicStat health(getHealth()); health.setBase(getHealth().getBase() + healthGain); health.setCurrent(std::max(1.f, getHealth().getCurrent() + healthGain)); setHealth(health); setLevel(getLevel()+1); } void MWMechanics::NpcStats::updateHealth() { const float endurance = getAttribute(ESM::Attribute::Endurance).getBase(); const float strength = getAttribute(ESM::Attribute::Strength).getBase(); setHealth(floor(0.5f * (strength + endurance))); } int MWMechanics::NpcStats::getLevelupAttributeMultiplier(int attribute) const { int num = mSkillIncreases[attribute]; if (num == 0) return 1; num = std::min(10, num); // iLevelUp01Mult - iLevelUp10Mult std::stringstream gmst; gmst << "iLevelUp" << std::setfill('0') << std::setw(2) << num << "Mult"; return MWBase::Environment::get().getWorld()->getStore().get().find(gmst.str())->mValue.getInteger(); } int MWMechanics::NpcStats::getSkillIncreasesForSpecialization(int spec) const { return mSpecIncreases[spec]; } void MWMechanics::NpcStats::flagAsUsed (const std::string& id) { mUsedIds.insert (id); } bool MWMechanics::NpcStats::hasBeenUsed (const std::string& id) const { return mUsedIds.find (id)!=mUsedIds.end(); } int MWMechanics::NpcStats::getBounty() const { return mBounty; } void MWMechanics::NpcStats::setBounty (int bounty) { mBounty = bounty; } int MWMechanics::NpcStats::getReputation() const { return mReputation; } void MWMechanics::NpcStats::setReputation(int reputation) { // Reputation is capped in original engine mReputation = std::min(255, std::max(0, reputation)); } int MWMechanics::NpcStats::getCrimeId() const { return mCrimeId; } void MWMechanics::NpcStats::setCrimeId(int id) { mCrimeId = id; /* Start of tes3mp addition Record this as the time of the last crime witnessed by this NPC */ if (id != -1) setCrimeTime(time(0)); /* End of tes3mp addition */ } bool MWMechanics::NpcStats::hasSkillsForRank (const std::string& factionId, int rank) const { if (rank<0 || rank>=10) throw std::runtime_error ("rank index out of range"); const ESM::Faction& faction = *MWBase::Environment::get().getWorld()->getStore().get().find (factionId); std::vector skills; for (int i=0; i<7; ++i) { if (faction.mData.mSkills[i] != -1) skills.push_back (static_cast (getSkill (faction.mData.mSkills[i]).getBase())); } if (skills.empty()) return true; std::sort (skills.begin(), skills.end()); std::vector::const_reverse_iterator iter = skills.rbegin(); const ESM::RankData& rankData = faction.mData.mRankData[rank]; if (*iter::const_iterator iter (mFactionRank.begin()); iter!=mFactionRank.end(); ++iter) state.mFactions[iter->first].mRank = iter->second; state.mDisposition = mDisposition; for (int i=0; i::const_iterator iter (mExpelled.begin()); iter!=mExpelled.end(); ++iter) state.mFactions[*iter].mExpelled = true; for (std::map::const_iterator iter (mFactionReputation.begin()); iter!=mFactionReputation.end(); ++iter) state.mFactions[iter->first].mReputation = iter->second; state.mReputation = mReputation; state.mWerewolfKills = mWerewolfKills; state.mLevelProgress = mLevelProgress; for (int i=0; igetStore(); for (std::map::const_iterator iter (state.mFactions.begin()); iter!=state.mFactions.end(); ++iter) if (store.get().search (iter->first)) { if (iter->second.mExpelled) mExpelled.insert (iter->first); if (iter->second.mRank >= 0) mFactionRank[iter->first] = iter->second.mRank; if (iter->second.mReputation) mFactionReputation[Misc::StringUtils::lowerCase(iter->first)] = iter->second.mReputation; } mDisposition = state.mDisposition; for (int i=0; i::const_iterator iter (state.mUsedIds.begin()); iter!=state.mUsedIds.end(); ++iter) if (store.find (*iter)) mUsedIds.insert (*iter); mTimeToStartDrowning = state.mTimeToStartDrowning; } ================================================ FILE: apps/openmw/mwmechanics/npcstats.hpp ================================================ #ifndef GAME_MWMECHANICS_NPCSTATS_H #define GAME_MWMECHANICS_NPCSTATS_H #include #include #include #include /* Start of tes3mp addition Include additional headers for multiplayer purposes */ #include /* End of tes3mp addition */ #include "creaturestats.hpp" namespace ESM { struct Class; struct NpcStats; } namespace MWMechanics { /// \brief Additional stats for NPCs class NpcStats : public CreatureStats { int mDisposition; SkillValue mSkill[ESM::Skill::Length]; // SkillValue.mProgress used by the player only int mReputation; int mCrimeId; /* Start of tes3mp addition Add a variable used to track the time of the most recent crime by a player */ time_t mCrimeTime = time(0); /* End of tes3mp addition */ // ----- used by the player only, maybe should be moved at some point ------- int mBounty; int mWerewolfKills; /// Used only for the player and for NPC's with ranks, modified by scripts; other NPCs have maximum one faction defined in their NPC record std::map mFactionRank; std::set mExpelled; std::map mFactionReputation; int mLevelProgress; // 0-10 std::vector mSkillIncreases; // number of skill increases for each attribute (resets after leveling up) std::vector mSpecIncreases; // number of skill increases for each specialization (accumulates throughout the entire game) std::set mUsedIds; // --------------------------------------------------------------------------- /// Countdown to getting damage while underwater float mTimeToStartDrowning; bool mIsWerewolf; public: NpcStats(); int getBaseDisposition() const; void setBaseDisposition(int disposition); int getReputation() const; void setReputation(int reputation); int getCrimeId() const; void setCrimeId(int id); const SkillValue& getSkill (int index) const; SkillValue& getSkill (int index); void setSkill(int index, const SkillValue& value); int getFactionRank(const std::string &faction) const; const std::map& getFactionRanks() const; /// Increase the rank in this faction by 1, if such a rank exists. void raiseRank(const std::string& faction); /// Lower the rank in this faction by 1, if such a rank exists. void lowerRank(const std::string& faction); /// Join this faction, setting the initial rank to 0. void joinFaction(const std::string& faction); const std::set& getExpelled() const { return mExpelled; } bool getExpelled(const std::string& factionID) const; void expell(const std::string& factionID); void clearExpelled(const std::string& factionID); bool isInFaction (const std::string& faction) const; float getSkillProgressRequirement (int skillIndex, const ESM::Class& class_) const; void useSkill (int skillIndex, const ESM::Class& class_, int usageType = -1, float extraFactor=1.f); ///< Increase skill by usage. void increaseSkill (int skillIndex, const ESM::Class& class_, bool preserveProgress, bool readBook = false); int getLevelProgress() const; /* Start of tes3mp addition Useful methods for setting player stats */ void setLevelProgress(int value); int getSkillIncrease(int attribute) const; void setSkillIncrease(int attribute, int value); /* End of tes3mp addition */ /* Start of tes3mp addition Make it possible to get and set the time of the last crime witnessed by the NPC, used to stop combat with a player after that player dies and is resurrected */ time_t getCrimeTime(); void setCrimeTime(time_t crimeTime); /* End of tes3mp addition */ int getLevelupAttributeMultiplier(int attribute) const; int getSkillIncreasesForSpecialization(int spec) const; void levelUp(); void updateHealth(); ///< Calculate health based on endurance and strength. /// Called at character creation. void flagAsUsed (const std::string& id); ///< @note Id must be lower-case bool hasBeenUsed (const std::string& id) const; ///< @note Id must be lower-case int getBounty() const; void setBounty (int bounty); int getFactionReputation (const std::string& faction) const; void setFactionReputation (const std::string& faction, int value); bool hasSkillsForRank (const std::string& factionId, int rank) const; bool isWerewolf() const; void setWerewolf(bool set); int getWerewolfKills() const; /// Increments mWerewolfKills by 1. void addWerewolfKill(); float getTimeToStartDrowning() const; /// Sets time left for the creature to drown if it stays underwater. /// @param time value from [0,20] void setTimeToStartDrowning(float time); void writeState (ESM::CreatureStats& state) const; void writeState (ESM::NpcStats& state) const; void readState (const ESM::CreatureStats& state); void readState (const ESM::NpcStats& state); }; } #endif ================================================ FILE: apps/openmw/mwmechanics/objects.cpp ================================================ #include "objects.hpp" #include #include #include "../mwbase/environment.hpp" #include "../mwbase/windowmanager.hpp" #include "../mwbase/world.hpp" #include "character.hpp" #include "movement.hpp" namespace MWMechanics { Objects::Objects() { } Objects::~Objects() { for(auto& object : mObjects) { delete object.second; object.second = nullptr; } } void Objects::addObject(const MWWorld::Ptr& ptr) { removeObject(ptr); MWRender::Animation *anim = MWBase::Environment::get().getWorld()->getAnimation(ptr); if(anim) mObjects.insert(std::make_pair(ptr, new CharacterController(ptr, anim))); } void Objects::removeObject(const MWWorld::Ptr& ptr) { PtrControllerMap::iterator iter = mObjects.find(ptr); if(iter != mObjects.end()) { delete iter->second; mObjects.erase(iter); } } void Objects::updateObject(const MWWorld::Ptr &old, const MWWorld::Ptr &ptr) { PtrControllerMap::iterator iter = mObjects.find(old); if(iter != mObjects.end()) { CharacterController *ctrl = iter->second; mObjects.erase(iter); ctrl->updatePtr(ptr); mObjects.insert(std::make_pair(ptr, ctrl)); } } void Objects::dropObjects (const MWWorld::CellStore *cellStore) { PtrControllerMap::iterator iter = mObjects.begin(); while(iter != mObjects.end()) { if(iter->first.getCell()==cellStore) { delete iter->second; mObjects.erase(iter++); } else ++iter; } } void Objects::update(float duration, bool paused) { if(!paused) { for(auto& object : mObjects) object.second->update(duration); } else { // We still should play container opening animation in the Container GUI mode. MWGui::GuiMode mode = MWBase::Environment::get().getWindowManager()->getMode(); if(mode != MWGui::GM_Container) return; for(auto& object : mObjects) { if (object.first.getTypeName() != typeid(ESM::Container).name()) continue; if (object.second->isAnimPlaying("containeropen")) { object.second->update(duration); MWBase::Environment::get().getWorld()->updateAnimatedCollisionShape(object.first); } } } } bool Objects::onOpen(const MWWorld::Ptr& ptr) { PtrControllerMap::iterator iter = mObjects.find(ptr); if(iter != mObjects.end()) return iter->second->onOpen(); return true; } void Objects::onClose(const MWWorld::Ptr& ptr) { PtrControllerMap::iterator iter = mObjects.find(ptr); if(iter != mObjects.end()) iter->second->onClose(); } bool Objects::playAnimationGroup(const MWWorld::Ptr& ptr, const std::string& groupName, int mode, int number, bool persist) { PtrControllerMap::iterator iter = mObjects.find(ptr); if(iter != mObjects.end()) { return iter->second->playGroup(groupName, mode, number, persist); } else { Log(Debug::Warning) << "Warning: Objects::playAnimationGroup: Unable to find " << ptr.getCellRef().getRefId(); return false; } } void Objects::skipAnimation(const MWWorld::Ptr& ptr) { PtrControllerMap::iterator iter = mObjects.find(ptr); if(iter != mObjects.end()) iter->second->skipAnim(); } void Objects::persistAnimationStates() { for (PtrControllerMap::iterator iter = mObjects.begin(); iter != mObjects.end(); ++iter) iter->second->persistAnimationState(); } void Objects::getObjectsInRange(const osg::Vec3f& position, float radius, std::vector& out) { for (PtrControllerMap::iterator iter = mObjects.begin(); iter != mObjects.end(); ++iter) { if ((position - iter->first.getRefData().getPosition().asVec3()).length2() <= radius*radius) out.push_back(iter->first); } } } ================================================ FILE: apps/openmw/mwmechanics/objects.hpp ================================================ #ifndef GAME_MWMECHANICS_ACTIVATORS_H #define GAME_MWMECHANICS_ACTIVATORS_H #include #include #include namespace osg { class Vec3f; } namespace MWWorld { class Ptr; class CellStore; } namespace MWMechanics { class CharacterController; class Objects { typedef std::map PtrControllerMap; PtrControllerMap mObjects; public: Objects(); ~Objects(); void addObject (const MWWorld::Ptr& ptr); ///< Register an animated object void removeObject (const MWWorld::Ptr& ptr); ///< Deregister an object void updateObject(const MWWorld::Ptr &old, const MWWorld::Ptr& ptr); ///< Updates an object with a new Ptr void dropObjects(const MWWorld::CellStore *cellStore); ///< Deregister all objects in the given cell. void update(float duration, bool paused); ///< Update object animations bool onOpen(const MWWorld::Ptr& ptr); void onClose(const MWWorld::Ptr& ptr); bool playAnimationGroup(const MWWorld::Ptr& ptr, const std::string& groupName, int mode, int number, bool persist=false); void skipAnimation(const MWWorld::Ptr& ptr); void persistAnimationStates(); void getObjectsInRange (const osg::Vec3f& position, float radius, std::vector& out); std::size_t size() const { return mObjects.size(); } }; } #endif ================================================ FILE: apps/openmw/mwmechanics/obstacle.cpp ================================================ #include "obstacle.hpp" #include #include "../mwworld/class.hpp" #include "../mwworld/cellstore.hpp" #include "movement.hpp" namespace MWMechanics { // NOTE: determined empirically but probably need further tweaking static const float DIST_SAME_SPOT = 0.5f; static const float DURATION_SAME_SPOT = 1.5f; static const float DURATION_TO_EVADE = 0.4f; const float ObstacleCheck::evadeDirections[NUM_EVADE_DIRECTIONS][2] = { { 1.0f, 0.0f }, // move to side { 1.0f, -1.0f }, // move to side and backwards { -1.0f, 0.0f }, // move to other side { -1.0f, -1.0f } // move to side and backwards }; bool proximityToDoor(const MWWorld::Ptr& actor, float minDist) { if(getNearbyDoor(actor, minDist).isEmpty()) return false; else return true; } const MWWorld::Ptr getNearbyDoor(const MWWorld::Ptr& actor, float minDist) { MWWorld::CellStore *cell = actor.getCell(); // Check all the doors in this cell const MWWorld::CellRefList& doors = cell->getReadOnlyDoors(); osg::Vec3f pos(actor.getRefData().getPosition().asVec3()); pos.z() = 0; osg::Vec3f actorDir = (actor.getRefData().getBaseNode()->getAttitude() * osg::Vec3f(0,1,0)); for (const auto& ref : doors.mList) { osg::Vec3f doorPos(ref.mData.getPosition().asVec3()); // FIXME: cast const MWWorld::Ptr doorPtr = MWWorld::Ptr(&const_cast &>(ref), actor.getCell()); const auto doorState = doorPtr.getClass().getDoorState(doorPtr); float doorRot = ref.mData.getPosition().rot[2] - doorPtr.getCellRef().getPosition().rot[2]; if (doorState != MWWorld::DoorState::Idle || doorRot != 0) continue; // the door is already opened/opening doorPos.z() = 0; float angle = std::acos(actorDir * (doorPos - pos) / (actorDir.length() * (doorPos - pos).length())); // Allow 60 degrees angle between actor and door if (angle < -osg::PI / 3 || angle > osg::PI / 3) continue; // Door is not close enough if ((pos - doorPos).length2() > minDist*minDist) continue; return doorPtr; // found, stop searching } return MWWorld::Ptr(); // none found } ObstacleCheck::ObstacleCheck() : mWalkState(WalkState::Initial) , mStateDuration(0) , mEvadeDirectionIndex(0) { } void ObstacleCheck::clear() { mWalkState = WalkState::Initial; } bool ObstacleCheck::isEvading() const { return mWalkState == WalkState::Evade; } /* * input - actor, duration (time since last check) * output - true if evasive action needs to be taken * * Walking state transitions (player greeting check not shown): * * Initial ----> Norm <--------> CheckStuck -------> Evade ---+ * ^ ^ | f ^ | t ^ | | * | | | | | | | | * | +-+ +---+ +---+ | u * | any < t < u | * +---------------------------------------------+ * * f = one reaction time * t = how long before considered stuck * u = how long to move sideways * */ void ObstacleCheck::update(const MWWorld::Ptr& actor, const osg::Vec3f& destination, float duration) { const auto position = actor.getRefData().getPosition().asVec3(); if (mWalkState == WalkState::Initial) { mWalkState = WalkState::Norm; mStateDuration = 0; mPrev = position; mInitialDistance = (destination - position).length(); return; } if (mWalkState != WalkState::Evade) { const float distSameSpot = DIST_SAME_SPOT * actor.getClass().getCurrentSpeed(actor) * duration; const float prevDistance = (destination - mPrev).length(); const float currentDistance = (destination - position).length(); const float movedDistance = prevDistance - currentDistance; const float movedFromInitialDistance = mInitialDistance - currentDistance; mPrev = position; if (movedDistance >= distSameSpot && movedFromInitialDistance >= distSameSpot) { mWalkState = WalkState::Norm; mStateDuration = 0; return; } if (mWalkState == WalkState::Norm) { mWalkState = WalkState::CheckStuck; mStateDuration = duration; mInitialDistance = (destination - position).length(); return; } mStateDuration += duration; if (mStateDuration < DURATION_SAME_SPOT) { return; } mWalkState = WalkState::Evade; mStateDuration = 0; chooseEvasionDirection(); return; } mStateDuration += duration; if(mStateDuration >= DURATION_TO_EVADE) { // tried to evade, assume all is ok and start again mWalkState = WalkState::Norm; mStateDuration = 0; mPrev = position; } } void ObstacleCheck::takeEvasiveAction(MWMechanics::Movement& actorMovement) const { actorMovement.mPosition[0] = evadeDirections[mEvadeDirectionIndex][0]; actorMovement.mPosition[1] = evadeDirections[mEvadeDirectionIndex][1]; } void ObstacleCheck::chooseEvasionDirection() { // change direction if attempt didn't work ++mEvadeDirectionIndex; if (mEvadeDirectionIndex == NUM_EVADE_DIRECTIONS) { mEvadeDirectionIndex = 0; } } } ================================================ FILE: apps/openmw/mwmechanics/obstacle.hpp ================================================ #ifndef OPENMW_MECHANICS_OBSTACLE_H #define OPENMW_MECHANICS_OBSTACLE_H #include namespace MWWorld { class Ptr; } namespace MWMechanics { struct Movement; static constexpr int NUM_EVADE_DIRECTIONS = 4; /// tests actor's proximity to a closed door by default bool proximityToDoor(const MWWorld::Ptr& actor, float minDist); /// Returns door pointer within range. No guarantee is given as to which one /** \return Pointer to the door, or empty pointer if none exists **/ const MWWorld::Ptr getNearbyDoor(const MWWorld::Ptr& actor, float minDist); class ObstacleCheck { public: ObstacleCheck(); // Clear the timers and set the state machine to default void clear(); bool isEvading() const; // Updates internal state, call each frame for moving actor void update(const MWWorld::Ptr& actor, const osg::Vec3f& destination, float duration); // change direction to try to fix "stuck" actor void takeEvasiveAction(MWMechanics::Movement& actorMovement) const; private: osg::Vec3f mPrev; // directions to try moving in when get stuck static const float evadeDirections[NUM_EVADE_DIRECTIONS][2]; enum class WalkState { Initial, Norm, CheckStuck, Evade }; WalkState mWalkState; float mStateDuration; int mEvadeDirectionIndex; float mInitialDistance = 0; void chooseEvasionDirection(); }; } #endif ================================================ FILE: apps/openmw/mwmechanics/pathfinding.cpp ================================================ #include "pathfinding.hpp" #include #include #include #include #include #include #include "../mwbase/world.hpp" #include "../mwbase/environment.hpp" #include "../mwphysics/collisiontype.hpp" #include "../mwworld/cellstore.hpp" #include "../mwworld/class.hpp" #include "pathgrid.hpp" #include "actorutil.hpp" namespace { // Chooses a reachable end pathgrid point. start is assumed reachable. std::pair getClosestReachablePoint(const ESM::Pathgrid* grid, const MWMechanics::PathgridGraph *graph, const osg::Vec3f& pos, int start) { assert(grid && !grid->mPoints.empty()); float closestDistanceBetween = std::numeric_limits::max(); float closestDistanceReachable = std::numeric_limits::max(); int closestIndex = 0; int closestReachableIndex = 0; // TODO: if this full scan causes performance problems mapping pathgrid // points to a quadtree may help for(unsigned int counter = 0; counter < grid->mPoints.size(); counter++) { float potentialDistBetween = MWMechanics::PathFinder::distanceSquared(grid->mPoints[counter], pos); if (potentialDistBetween < closestDistanceReachable) { // found a closer one if (graph->isPointConnected(start, counter)) { closestDistanceReachable = potentialDistBetween; closestReachableIndex = counter; } if (potentialDistBetween < closestDistanceBetween) { closestDistanceBetween = potentialDistBetween; closestIndex = counter; } } } // post-condition: start and endpoint must be connected assert(graph->isPointConnected(start, closestReachableIndex)); // AiWander has logic that depends on whether a path was created, deleting // allowed nodes if not. Hence a path needs to be created even if the start // and the end points are the same. return std::pair (closestReachableIndex, closestReachableIndex == closestIndex); } float sqrDistance(const osg::Vec2f& lhs, const osg::Vec2f& rhs) { return (lhs - rhs).length2(); } float sqrDistanceIgnoreZ(const osg::Vec3f& lhs, const osg::Vec3f& rhs) { return sqrDistance(osg::Vec2f(lhs.x(), lhs.y()), osg::Vec2f(rhs.x(), rhs.y())); } float getPathStepSize(const MWWorld::ConstPtr& actor) { const auto world = MWBase::Environment::get().getWorld(); const auto realHalfExtents = world->getHalfExtents(actor); return 2 * std::max(realHalfExtents.x(), realHalfExtents.y()); } float getHeight(const MWWorld::ConstPtr& actor) { const auto world = MWBase::Environment::get().getWorld(); const auto halfExtents = world->getHalfExtents(actor); return 2.0 * halfExtents.z(); } // Returns true if turn in `p2` is less than 10 degrees and all the 3 points are almost on one line. bool isAlmostStraight(const osg::Vec3f& p1, const osg::Vec3f& p2, const osg::Vec3f& p3, float pointTolerance) { osg::Vec3f v1 = p1 - p2; osg::Vec3f v3 = p3 - p2; v1.z() = v3.z() = 0; float dotProduct = v1.x() * v3.x() + v1.y() * v3.y(); float crossProduct = v1.x() * v3.y() - v1.y() * v3.x(); // Check that the angle between v1 and v3 is less or equal than 5 degrees. static const float cos175 = std::cos(osg::PI * (175.0 / 180)); bool checkAngle = dotProduct <= cos175 * v1.length() * v3.length(); // Check that distance from p2 to the line (p1, p3) is less or equal than `pointTolerance`. bool checkDist = std::abs(crossProduct) <= pointTolerance * (p3 - p1).length(); return checkAngle && checkDist; } struct IsValidShortcut { const DetourNavigator::Navigator* mNavigator; const osg::Vec3f mHalfExtents; const DetourNavigator::Flags mFlags; bool operator()(const osg::Vec3f& start, const osg::Vec3f& end) const { const auto position = mNavigator->raycast(mHalfExtents, start, end, mFlags); return position.has_value() && std::abs((position.value() - start).length2() - (end - start).length2()) <= 1; } }; } namespace MWMechanics { float getPathDistance(const MWWorld::Ptr& actor, const osg::Vec3f& lhs, const osg::Vec3f& rhs) { if (std::abs(lhs.z() - rhs.z()) > getHeight(actor) || canActorMoveByZAxis(actor)) return distance(lhs, rhs); return distanceIgnoreZ(lhs, rhs); } bool checkWayIsClear(const osg::Vec3f& from, const osg::Vec3f& to, float offsetXY) { osg::Vec3f dir = to - from; dir.z() = 0; dir.normalize(); float verticalOffset = 200; // instead of '200' here we want the height of the actor osg::Vec3f _from = from + dir*offsetXY + osg::Z_AXIS * verticalOffset; // cast up-down ray and find height of hit in world space float h = _from.z() - MWBase::Environment::get().getWorld()->getDistToNearestRayHit(_from, -osg::Z_AXIS, verticalOffset + PATHFIND_Z_REACH + 1); return (std::abs(from.z() - h) <= PATHFIND_Z_REACH); } /* * NOTE: This method may fail to find a path. The caller must check the * result before using it. If there is no path the AI routies need to * implement some other heuristics to reach the target. * * NOTE: It may be desirable to simply go directly to the endPoint if for * example there are no pathgrids in this cell. * * NOTE: startPoint & endPoint are in world coordinates * * Updates mPath using aStarSearch() or ray test (if shortcut allowed). * mPath consists of pathgrid points, except the last element which is * endPoint. This may be useful where the endPoint is not on a pathgrid * point (e.g. combat). However, if the caller has already chosen a * pathgrid point (e.g. wander) then it may be worth while to call * pop_back() to remove the redundant entry. * * NOTE: coordinates must be converted prior to calling getClosestPoint() * * | * | cell * | +-----------+ * | | | * | | | * | | @ | * | i | j | * |<--->|<---->| | * | +-----------+ * | k * |<---------->| world * +----------------------------- * * i = x value of cell itself (multiply by ESM::Land::REAL_SIZE to convert) * j = @.x in local coordinates (i.e. within the cell) * k = @.x in world coordinates */ void PathFinder::buildPathByPathgridImpl(const osg::Vec3f& startPoint, const osg::Vec3f& endPoint, const PathgridGraph& pathgridGraph, std::back_insert_iterator> out) { const auto pathgrid = pathgridGraph.getPathgrid(); // Refer to AiWander reseach topic on openmw forums for some background. // Maybe there is no pathgrid for this cell. Just go to destination and let // physics take care of any blockages. if(!pathgrid || pathgrid->mPoints.empty()) return; // NOTE: getClosestPoint expects local coordinates Misc::CoordinateConverter converter(mCell->getCell()); // NOTE: It is possible that getClosestPoint returns a pathgrind point index // that is unreachable in some situations. e.g. actor is standing // outside an area enclosed by walls, but there is a pathgrid // point right behind the wall that is closer than any pathgrid // point outside the wall osg::Vec3f startPointInLocalCoords(converter.toLocalVec3(startPoint)); int startNode = getClosestPoint(pathgrid, startPointInLocalCoords); osg::Vec3f endPointInLocalCoords(converter.toLocalVec3(endPoint)); std::pair endNode = getClosestReachablePoint(pathgrid, &pathgridGraph, endPointInLocalCoords, startNode); // if it's shorter for actor to travel from start to end, than to travel from either // start or end to nearest pathgrid point, just travel from start to end. float startToEndLength2 = (endPointInLocalCoords - startPointInLocalCoords).length2(); float endTolastNodeLength2 = distanceSquared(pathgrid->mPoints[endNode.first], endPointInLocalCoords); float startTo1stNodeLength2 = distanceSquared(pathgrid->mPoints[startNode], startPointInLocalCoords); if ((startToEndLength2 < startTo1stNodeLength2) || (startToEndLength2 < endTolastNodeLength2)) { *out++ = endPoint; return; } // AiWander has logic that depends on whether a path was created, // deleting allowed nodes if not. Hence a path needs to be created // even if the start and the end points are the same. // NOTE: aStarSearch will return an empty path if the start and end // nodes are the same if(startNode == endNode.first) { ESM::Pathgrid::Point temp(pathgrid->mPoints[startNode]); converter.toWorld(temp); *out++ = makeOsgVec3(temp); } else { auto path = pathgridGraph.aStarSearch(startNode, endNode.first); // If nearest path node is in opposite direction from second, remove it from path. // Especially useful for wandering actors, if the nearest node is blocked for some reason. if (path.size() > 1) { ESM::Pathgrid::Point secondNode = *(++path.begin()); osg::Vec3f firstNodeVec3f = makeOsgVec3(pathgrid->mPoints[startNode]); osg::Vec3f secondNodeVec3f = makeOsgVec3(secondNode); osg::Vec3f toSecondNodeVec3f = secondNodeVec3f - firstNodeVec3f; osg::Vec3f toStartPointVec3f = startPointInLocalCoords - firstNodeVec3f; if (toSecondNodeVec3f * toStartPointVec3f > 0) { ESM::Pathgrid::Point temp(secondNode); converter.toWorld(temp); // Add Z offset since path node can overlap with other objects. // Also ignore doors in raytesting. const int mask = MWPhysics::CollisionType_World; bool isPathClear = !MWBase::Environment::get().getWorld()->castRay( startPoint.x(), startPoint.y(), startPoint.z() + 16, temp.mX, temp.mY, temp.mZ + 16, mask); if (isPathClear) path.pop_front(); } } // convert supplied path to world coordinates std::transform(path.begin(), path.end(), out, [&] (ESM::Pathgrid::Point& point) { converter.toWorld(point); return makeOsgVec3(point); }); } // If endNode found is NOT the closest PathGrid point to the endPoint, // assume endPoint is not reachable from endNode. In which case, // path ends at endNode. // // So only add the destination (which may be different to the closest // pathgrid point) when endNode was the closest point to endPoint. // // This logic can fail in the opposite situate, e.g. endPoint may // have been reachable but happened to be very close to an // unreachable pathgrid point. // // The AI routines will have to deal with such situations. if (endNode.second) *out++ = endPoint; } float PathFinder::getZAngleToNext(float x, float y) const { // This should never happen (programmers should have an if statement checking // isPathConstructed that prevents this call if otherwise). if(mPath.empty()) return 0.; const auto& nextPoint = mPath.front(); const float directionX = nextPoint.x() - x; const float directionY = nextPoint.y() - y; return std::atan2(directionX, directionY); } float PathFinder::getXAngleToNext(float x, float y, float z) const { // This should never happen (programmers should have an if statement checking // isPathConstructed that prevents this call if otherwise). if(mPath.empty()) return 0.; const osg::Vec3f dir = mPath.front() - osg::Vec3f(x, y, z); return getXAngleToDir(dir); } void PathFinder::update(const osg::Vec3f& position, float pointTolerance, float destinationTolerance, bool shortenIfAlmostStraight, bool canMoveByZ, const osg::Vec3f& halfExtents, const DetourNavigator::Flags flags) { if (mPath.empty()) return; while (mPath.size() > 1 && sqrDistanceIgnoreZ(mPath.front(), position) < pointTolerance * pointTolerance) mPath.pop_front(); if (shortenIfAlmostStraight) { const IsValidShortcut isValidShortcut { MWBase::Environment::get().getWorld()->getNavigator(), halfExtents, flags }; while (mPath.size() > 2 && isAlmostStraight(mPath[0], mPath[1], mPath[2], pointTolerance) && isValidShortcut(mPath[0], mPath[2])) mPath.erase(mPath.begin() + 1); if (mPath.size() > 1 && isAlmostStraight(position, mPath[0], mPath[1], pointTolerance) && isValidShortcut(position, mPath[1])) mPath.pop_front(); } if (mPath.size() == 1) { float distSqr; if (canMoveByZ) distSqr = (mPath.front() - position).length2(); else distSqr = sqrDistanceIgnoreZ(mPath.front(), position); if (distSqr < destinationTolerance * destinationTolerance) mPath.pop_front(); } } void PathFinder::buildStraightPath(const osg::Vec3f& endPoint) { mPath.clear(); mPath.push_back(endPoint); mConstructed = true; } void PathFinder::buildPathByPathgrid(const osg::Vec3f& startPoint, const osg::Vec3f& endPoint, const MWWorld::CellStore* cell, const PathgridGraph& pathgridGraph) { mPath.clear(); mCell = cell; buildPathByPathgridImpl(startPoint, endPoint, pathgridGraph, std::back_inserter(mPath)); mConstructed = !mPath.empty(); } void PathFinder::buildPathByNavMesh(const MWWorld::ConstPtr& actor, const osg::Vec3f& startPoint, const osg::Vec3f& endPoint, const osg::Vec3f& halfExtents, const DetourNavigator::Flags flags, const DetourNavigator::AreaCosts& areaCosts) { mPath.clear(); // If it's not possible to build path over navmesh due to disabled navmesh generation fallback to straight path DetourNavigator::Status status = buildPathByNavigatorImpl(actor, startPoint, endPoint, halfExtents, flags, areaCosts, std::back_inserter(mPath)); if (status != DetourNavigator::Status::Success) mPath.clear(); if (status == DetourNavigator::Status::NavMeshNotFound) mPath.push_back(endPoint); mConstructed = !mPath.empty(); } void PathFinder::buildPath(const MWWorld::ConstPtr& actor, const osg::Vec3f& startPoint, const osg::Vec3f& endPoint, const MWWorld::CellStore* cell, const PathgridGraph& pathgridGraph, const osg::Vec3f& halfExtents, const DetourNavigator::Flags flags, const DetourNavigator::AreaCosts& areaCosts) { mPath.clear(); mCell = cell; DetourNavigator::Status status = DetourNavigator::Status::NavMeshNotFound; if (!actor.getClass().isPureWaterCreature(actor) && !actor.getClass().isPureFlyingCreature(actor)) { status = buildPathByNavigatorImpl(actor, startPoint, endPoint, halfExtents, flags, areaCosts, std::back_inserter(mPath)); if (status != DetourNavigator::Status::Success) mPath.clear(); } if (status != DetourNavigator::Status::NavMeshNotFound && mPath.empty()) { status = buildPathByNavigatorImpl(actor, startPoint, endPoint, halfExtents, flags | DetourNavigator::Flag_usePathgrid, areaCosts, std::back_inserter(mPath)); if (status != DetourNavigator::Status::Success) mPath.clear(); } if (mPath.empty()) buildPathByPathgridImpl(startPoint, endPoint, pathgridGraph, std::back_inserter(mPath)); if (status == DetourNavigator::Status::NavMeshNotFound && mPath.empty()) mPath.push_back(endPoint); mConstructed = !mPath.empty(); } DetourNavigator::Status PathFinder::buildPathByNavigatorImpl(const MWWorld::ConstPtr& actor, const osg::Vec3f& startPoint, const osg::Vec3f& endPoint, const osg::Vec3f& halfExtents, const DetourNavigator::Flags flags, const DetourNavigator::AreaCosts& areaCosts, std::back_insert_iterator> out) { 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, out); if (status != DetourNavigator::Status::Success) { Log(Debug::Debug) << "Build path by navigator error: \"" << DetourNavigator::getMessage(status) << "\" for \"" << actor.getClass().getName(actor) << "\" (" << actor.getBase() << ") from " << startPoint << " to " << endPoint << " with flags (" << DetourNavigator::WriteFlags {flags} << ")"; } return status; } void PathFinder::buildPathByNavMeshToNextPoint(const MWWorld::ConstPtr& actor, const osg::Vec3f& halfExtents, const DetourNavigator::Flags flags, const DetourNavigator::AreaCosts& areaCosts) { if (mPath.empty()) return; const auto stepSize = getPathStepSize(actor); const auto startPoint = actor.getRefData().getPosition().asVec3(); if (sqrDistanceIgnoreZ(mPath.front(), startPoint) <= 4 * stepSize * stepSize) return; const auto navigator = MWBase::Environment::get().getWorld()->getNavigator(); std::deque prePath; auto prePathInserter = std::back_inserter(prePath); const auto status = navigator->findPath(halfExtents, stepSize, startPoint, mPath.front(), flags, areaCosts, prePathInserter); if (status == DetourNavigator::Status::NavMeshNotFound) return; if (status != DetourNavigator::Status::Success) { Log(Debug::Debug) << "Build path by navigator error: \"" << DetourNavigator::getMessage(status) << "\" for \"" << actor.getClass().getName(actor) << "\" (" << actor.getBase() << ") from " << startPoint << " to " << mPath.front() << " with flags (" << DetourNavigator::WriteFlags {flags} << ")"; return; } while (!prePath.empty() && sqrDistanceIgnoreZ(prePath.front(), startPoint) < stepSize * stepSize) prePath.pop_front(); while (!prePath.empty() && sqrDistanceIgnoreZ(prePath.back(), mPath.front()) < stepSize * stepSize) prePath.pop_back(); std::copy(prePath.rbegin(), prePath.rend(), std::front_inserter(mPath)); } void PathFinder::buildLimitedPath(const MWWorld::ConstPtr& actor, const osg::Vec3f& startPoint, const osg::Vec3f& endPoint, const MWWorld::CellStore* cell, const PathgridGraph& pathgridGraph, const osg::Vec3f& halfExtents, const DetourNavigator::Flags flags, const DetourNavigator::AreaCosts& areaCosts) { const auto navigator = MWBase::Environment::get().getWorld()->getNavigator(); const auto maxDistance = std::min( navigator->getMaxNavmeshAreaRealRadius(), static_cast(Constants::CellSizeInUnits) ); const auto startToEnd = endPoint - startPoint; const auto distance = startToEnd.length(); if (distance <= maxDistance) return buildPath(actor, startPoint, endPoint, cell, pathgridGraph, halfExtents, flags, areaCosts); const auto end = startPoint + startToEnd * maxDistance / distance; buildPath(actor, startPoint, end, cell, pathgridGraph, halfExtents, flags, areaCosts); } } ================================================ FILE: apps/openmw/mwmechanics/pathfinding.hpp ================================================ #ifndef GAME_MWMECHANICS_PATHFINDING_H #define GAME_MWMECHANICS_PATHFINDING_H #include #include #include #include #include #include #include #include namespace MWWorld { class CellStore; class ConstPtr; class Ptr; } namespace MWMechanics { class PathgridGraph; template inline float distance(const T& lhs, const T& rhs) { static_assert(std::is_same::value || std::is_same::value, "T is not a position"); return (lhs - rhs).length(); } inline float distanceIgnoreZ(const osg::Vec3f& lhs, const osg::Vec3f& rhs) { return distance(osg::Vec2f(lhs.x(), lhs.y()), osg::Vec2f(rhs.x(), rhs.y())); } float getPathDistance(const MWWorld::Ptr& actor, const osg::Vec3f& lhs, const osg::Vec3f& rhs); inline float getZAngleToDir(const osg::Vec3f& dir) { return std::atan2(dir.x(), dir.y()); } inline float getXAngleToDir(const osg::Vec3f& dir) { float dirLen = dir.length(); return (dirLen != 0) ? -std::asin(dir.z() / dirLen) : 0; } inline float getZAngleToPoint(const osg::Vec3f& origin, const osg::Vec3f& dest) { return getZAngleToDir(dest - origin); } inline float getXAngleToPoint(const osg::Vec3f& origin, const osg::Vec3f& dest) { return getXAngleToDir(dest - origin); } const float PATHFIND_Z_REACH = 50.0f; // distance after which actor (failed previously to shortcut) will try again const float PATHFIND_SHORTCUT_RETRY_DIST = 300.0f; const float MIN_TOLERANCE = 1.0f; const float DEFAULT_TOLERANCE = 32.0f; // cast up-down ray with some offset from actor position to check for pits/obstacles on the way to target; // magnitude of pits/obstacles is defined by PATHFIND_Z_REACH bool checkWayIsClear(const osg::Vec3f& from, const osg::Vec3f& to, float offsetXY); class PathFinder { public: PathFinder() : mConstructed(false) , mCell(nullptr) { } void clearPath() { mConstructed = false; mPath.clear(); mCell = nullptr; } void buildStraightPath(const osg::Vec3f& endPoint); void buildPathByPathgrid(const osg::Vec3f& startPoint, const osg::Vec3f& endPoint, const MWWorld::CellStore* cell, const PathgridGraph& pathgridGraph); void buildPathByNavMesh(const MWWorld::ConstPtr& actor, const osg::Vec3f& startPoint, const osg::Vec3f& endPoint, const osg::Vec3f& halfExtents, const DetourNavigator::Flags flags, const DetourNavigator::AreaCosts& areaCosts); void buildPath(const MWWorld::ConstPtr& actor, const osg::Vec3f& startPoint, const osg::Vec3f& endPoint, const MWWorld::CellStore* cell, const PathgridGraph& pathgridGraph, const osg::Vec3f& halfExtents, const DetourNavigator::Flags flags, const DetourNavigator::AreaCosts& areaCosts); void buildPathByNavMeshToNextPoint(const MWWorld::ConstPtr& actor, const osg::Vec3f& halfExtents, const DetourNavigator::Flags flags, const DetourNavigator::AreaCosts& areaCosts); void buildLimitedPath(const MWWorld::ConstPtr& actor, const osg::Vec3f& startPoint, const osg::Vec3f& endPoint, const MWWorld::CellStore* cell, const PathgridGraph& pathgridGraph, const osg::Vec3f& halfExtents, const DetourNavigator::Flags flags, const DetourNavigator::AreaCosts& areaCosts); /// Remove front point if exist and within tolerance void update(const osg::Vec3f& position, float pointTolerance, float destinationTolerance, bool shortenIfAlmostStraight, bool canMoveByZ, const osg::Vec3f& halfExtents, const DetourNavigator::Flags flags); bool checkPathCompleted() const { return mConstructed && mPath.empty(); } /// In radians float getZAngleToNext(float x, float y) const; float getXAngleToNext(float x, float y, float z) const; bool isPathConstructed() const { return mConstructed && !mPath.empty(); } std::size_t getPathSize() const { return mPath.size(); } const std::deque& getPath() const { return mPath; } const MWWorld::CellStore* getPathCell() const { return mCell; } void addPointToPath(const osg::Vec3f& point) { mConstructed = true; mPath.push_back(point); } /// utility function to convert a osg::Vec3f to a Pathgrid::Point static ESM::Pathgrid::Point makePathgridPoint(const osg::Vec3f& v) { return ESM::Pathgrid::Point(static_cast(v[0]), static_cast(v[1]), static_cast(v[2])); } /// utility function to convert an ESM::Position to a Pathgrid::Point static ESM::Pathgrid::Point makePathgridPoint(const ESM::Position& p) { return ESM::Pathgrid::Point(static_cast(p.pos[0]), static_cast(p.pos[1]), static_cast(p.pos[2])); } static osg::Vec3f makeOsgVec3(const ESM::Pathgrid::Point& p) { return osg::Vec3f(static_cast(p.mX), static_cast(p.mY), static_cast(p.mZ)); } // Slightly cheaper version for comparisons. // Caller needs to be careful for very short distances (i.e. less than 1) // or when accumuating the results i.e. (a + b)^2 != a^2 + b^2 // static float distanceSquared(ESM::Pathgrid::Point point, const osg::Vec3f& pos) { return (MWMechanics::PathFinder::makeOsgVec3(point) - pos).length2(); } // Return the closest pathgrid point index from the specified position // coordinates. NOTE: Does not check if there is a sensible way to get there // (e.g. a cliff in front). // // NOTE: pos is expected to be in local coordinates, as is grid->mPoints // static int getClosestPoint(const ESM::Pathgrid* grid, const osg::Vec3f& pos) { assert(grid && !grid->mPoints.empty()); float distanceBetween = distanceSquared(grid->mPoints[0], pos); int closestIndex = 0; // TODO: if this full scan causes performance problems mapping pathgrid // points to a quadtree may help for(unsigned int counter = 1; counter < grid->mPoints.size(); counter++) { float potentialDistBetween = distanceSquared(grid->mPoints[counter], pos); if(potentialDistBetween < distanceBetween) { distanceBetween = potentialDistBetween; closestIndex = counter; } } return closestIndex; } private: bool mConstructed; std::deque mPath; const MWWorld::CellStore* mCell; void buildPathByPathgridImpl(const osg::Vec3f& startPoint, const osg::Vec3f& endPoint, const PathgridGraph& pathgridGraph, std::back_insert_iterator> out); [[nodiscard]] DetourNavigator::Status buildPathByNavigatorImpl(const MWWorld::ConstPtr& actor, const osg::Vec3f& startPoint, const osg::Vec3f& endPoint, const osg::Vec3f& halfExtents, const DetourNavigator::Flags flags, const DetourNavigator::AreaCosts& areaCosts, std::back_insert_iterator> out); }; } #endif ================================================ FILE: apps/openmw/mwmechanics/pathgrid.cpp ================================================ #include "pathgrid.hpp" #include "../mwbase/world.hpp" #include "../mwbase/environment.hpp" #include "../mwworld/cellstore.hpp" #include "../mwworld/esmstore.hpp" namespace { // See https://theory.stanford.edu/~amitp/GameProgramming/Heuristics.html // // One of the smallest cost in Seyda Neen is between points 77 & 78: // pt x y // 77 = 8026, 4480 // 78 = 7986, 4218 // // Euclidean distance is about 262 (ignoring z) and Manhattan distance is 300 // (again ignoring z). Using a value of about 300 for D seems like a reasonable // starting point for experiments. If in doubt, just use value 1. // // The distance between 3 & 4 are pretty small, too. // 3 = 5435, 223 // 4 = 5948, 193 // // Approx. 514 Euclidean distance and 533 Manhattan distance. // float manhattan(const ESM::Pathgrid::Point& a, const ESM::Pathgrid::Point& b) { return 300.0f * (abs(a.mX - b.mX) + abs(a.mY - b.mY) + abs(a.mZ - b.mZ)); } // Choose a heuristics - Note that these may not be the best for directed // graphs with non-uniform edge costs. // // distance: // - sqrt((curr.x - goal.x)^2 + (curr.y - goal.y)^2 + (curr.z - goal.z)^2) // - slower but more accurate // // Manhattan: // - |curr.x - goal.x| + |curr.y - goal.y| + |curr.z - goal.z| // - faster but not the shortest path float costAStar(const ESM::Pathgrid::Point& a, const ESM::Pathgrid::Point& b) { //return distance(a, b); return manhattan(a, b); } } namespace MWMechanics { PathgridGraph::PathgridGraph(const MWWorld::CellStore *cell) : mCell(nullptr) , mPathgrid(nullptr) , mGraph(0) , mIsGraphConstructed(false) , mSCCId(0) , mSCCIndex(0) { load(cell); } /* * mGraph is populated with the cost of each allowed edge. * * The data structure is based on the code in buildPath2() but modified. * Please check git history if interested. * * mGraph[v].edges[i].index = w * * v = point index of location "from" * i = index of edges from point v * w = point index of location "to" * * * Example: (notice from p(0) to p(2) is not allowed in this example) * * mGraph[0].edges[0].index = 1 * .edges[1].index = 3 * * mGraph[1].edges[0].index = 0 * .edges[1].index = 2 * .edges[2].index = 3 * * mGraph[2].edges[0].index = 1 * * (etc, etc) * * * low * cost * p(0) <---> p(1) <------------> p(2) * ^ ^ * | | * | +-----> p(3) * +----------------> * high cost */ bool PathgridGraph::load(const MWWorld::CellStore *cell) { if(!cell) return false; if(mIsGraphConstructed) return true; mCell = cell->getCell(); mPathgrid = MWBase::Environment::get().getWorld()->getStore().get().search(*cell->getCell()); if(!mPathgrid) return false; mGraph.resize(mPathgrid->mPoints.size()); for(int i = 0; i < static_cast (mPathgrid->mEdges.size()); i++) { ConnectedPoint neighbour; neighbour.cost = costAStar(mPathgrid->mPoints[mPathgrid->mEdges[i].mV0], mPathgrid->mPoints[mPathgrid->mEdges[i].mV1]); // forward path of the edge neighbour.index = mPathgrid->mEdges[i].mV1; mGraph[mPathgrid->mEdges[i].mV0].edges.push_back(neighbour); // reverse path of the edge // NOTE: These are redundant, ESM already contains the required reverse paths //neighbour.index = mPathgrid->mEdges[i].mV0; //mGraph[mPathgrid->mEdges[i].mV1].edges.push_back(neighbour); } buildConnectedPoints(); mIsGraphConstructed = true; return true; } const ESM::Pathgrid *PathgridGraph::getPathgrid() const { return mPathgrid; } // v is the pathgrid point index (some call them vertices) void PathgridGraph::recursiveStrongConnect(int v) { mSCCPoint[v].first = mSCCIndex; // index mSCCPoint[v].second = mSCCIndex; // lowlink mSCCIndex++; mSCCStack.push_back(v); int w; for(int i = 0; i < static_cast (mGraph[v].edges.size()); i++) { w = mGraph[v].edges[i].index; if(mSCCPoint[w].first == -1) // not visited { recursiveStrongConnect(w); // recurse mSCCPoint[v].second = std::min(mSCCPoint[v].second, mSCCPoint[w].second); } else { if(find(mSCCStack.begin(), mSCCStack.end(), w) != mSCCStack.end()) mSCCPoint[v].second = std::min(mSCCPoint[v].second, mSCCPoint[w].first); } } if(mSCCPoint[v].second == mSCCPoint[v].first) { // new component do { w = mSCCStack.back(); mSCCStack.pop_back(); mGraph[w].componentId = mSCCId; } while(w != v); mSCCId++; } return; } /* * mGraph contains the strongly connected component group id's along * with pre-calculated edge costs. * * A cell can have disjointed pathgrids, e.g. Seyda Neen has 3 * * mGraph for Seyda Neen will therefore have 3 different values. When * selecting a random pathgrid point for AiWander, mGraph can be checked * for quickly finding whether the destination is reachable. * * Otherwise, buildPath can automatically select a closest reachable end * pathgrid point (reachable from the closest start point). * * Using Tarjan's algorithm: * * mGraph | graph G | * mSCCPoint | V | derived from mPoints * mGraph[v].edges | E (for v) | * mSCCIndex | index | tracking smallest unused index * mSCCStack | S | * mGraph[v].edges[i].index | w | * */ void PathgridGraph::buildConnectedPoints() { // both of these are set to zero in the constructor //mSCCId = 0; // how many strongly connected components in this cell //mSCCIndex = 0; int pointsSize = static_cast (mPathgrid->mPoints.size()); mSCCPoint.resize(pointsSize, std::pair (-1, -1)); mSCCStack.reserve(pointsSize); for(int v = 0; v < pointsSize; v++) { if(mSCCPoint[v].first == -1) // undefined (haven't visited) recursiveStrongConnect(v); } } bool PathgridGraph::isPointConnected(const int start, const int end) const { return (mGraph[start].componentId == mGraph[end].componentId); } void PathgridGraph::getNeighbouringPoints(const int index, ESM::Pathgrid::PointList &nodes) const { for(int i = 0; i < static_cast (mGraph[index].edges.size()); i++) { int neighbourIndex = mGraph[index].edges[i].index; if (neighbourIndex != index) nodes.push_back(mPathgrid->mPoints[neighbourIndex]); } } /* * NOTE: Based on buildPath2(), please check git history if interested * Should consider using a 3rd party library version (e.g. boost) * * Find the shortest path to the target goal using a well known algorithm. * Uses mGraph which has pre-computed costs for allowed edges. It is assumed * that mGraph is already constructed. * * Should be possible to make this MT safe. * * Returns path which may be empty. path contains pathgrid points in local * cell coordinates (indoors) or world coordinates (external). * * Input params: * start, goal - pathgrid point indexes (for this cell) * * Variables: * openset - point indexes to be traversed, lowest cost at the front * closedset - point indexes already traversed * gScore - past accumulated costs vector indexed by point index * fScore - future estimated costs vector indexed by point index * * TODO: An intersting exercise might be to cache the paths created for a * start/goal pair. To cache the results the paths need to be in * pathgrid points form (currently they are converted to world * coordinates). Essentially trading speed w/ memory. */ std::deque PathgridGraph::aStarSearch(const int start, const int goal) const { std::deque path; if(!isPointConnected(start, goal)) { return path; // there is no path, return an empty path } int graphSize = static_cast (mGraph.size()); std::vector gScore (graphSize, -1); std::vector fScore (graphSize, -1); std::vector graphParent (graphSize, -1); // gScore & fScore keep costs for each pathgrid point in mPoints gScore[start] = 0; fScore[start] = costAStar(mPathgrid->mPoints[start], mPathgrid->mPoints[goal]); std::list openset; std::list closedset; openset.push_back(start); int current = -1; while(!openset.empty()) { current = openset.front(); // front has the lowest cost openset.pop_front(); if(current == goal) break; closedset.push_back(current); // remember we've been here // check all edges for the current point index for(int j = 0; j < static_cast (mGraph[current].edges.size()); j++) { if(std::find(closedset.begin(), closedset.end(), mGraph[current].edges[j].index) == closedset.end()) { // not in closedset - i.e. have not traversed this edge destination int dest = mGraph[current].edges[j].index; float tentative_g = gScore[current] + mGraph[current].edges[j].cost; bool isInOpenSet = std::find(openset.begin(), openset.end(), dest) != openset.end(); if(!isInOpenSet || tentative_g < gScore[dest]) { graphParent[dest] = current; gScore[dest] = tentative_g; fScore[dest] = tentative_g + costAStar(mPathgrid->mPoints[dest], mPathgrid->mPoints[goal]); if(!isInOpenSet) { // add this edge to openset, lowest cost goes to the front // TODO: if this causes performance problems a hash table may help std::list::iterator it = openset.begin(); for(it = openset.begin(); it!= openset.end(); ++it) { if(fScore[*it] > fScore[dest]) break; } openset.insert(it, dest); } } } // if in closedset, i.e. traversed this edge already, try the next edge } } if(current != goal) return path; // for some reason couldn't build a path // reconstruct path to return, using local coordinates while(graphParent[current] != -1) { path.push_front(mPathgrid->mPoints[current]); current = graphParent[current]; } // add first node to path explicitly path.push_front(mPathgrid->mPoints[start]); return path; } } ================================================ FILE: apps/openmw/mwmechanics/pathgrid.hpp ================================================ #ifndef GAME_MWMECHANICS_PATHGRID_H #define GAME_MWMECHANICS_PATHGRID_H #include #include namespace ESM { struct Cell; } namespace MWWorld { class CellStore; } namespace MWMechanics { class PathgridGraph { public: PathgridGraph(const MWWorld::CellStore* cell); bool load(const MWWorld::CellStore *cell); const ESM::Pathgrid* getPathgrid() const; // returns true if end point is strongly connected (i.e. reachable // from start point) both start and end are pathgrid point indexes bool isPointConnected(const int start, const int end) const; // get neighbouring nodes for index node and put them to "nodes" vector void getNeighbouringPoints(const int index, ESM::Pathgrid::PointList &nodes) const; // the input parameters are pathgrid point indexes // the output list is in local (internal cells) or world (external // cells) coordinates // // NOTE: if start equals end an empty path is returned std::deque aStarSearch(const int start, const int end) const; private: const ESM::Cell *mCell; const ESM::Pathgrid *mPathgrid; struct ConnectedPoint // edge { int index; // pathgrid point index of neighbour float cost; }; struct Node // point { int componentId; std::vector edges; // neighbours }; // componentId is an integer indicating the groups of connected // pathgrid points (all connected points will have the same value) // // In Seyda Neen there are 3: // // 52, 53 and 54 are one set (enclosed yard) // 48, 49, 50, 51, 84, 85, 86, 87, 88, 89, 90 (ship & office) // all other pathgrid points are the third set // std::vector mGraph; bool mIsGraphConstructed; // variables used to calculate connected components int mSCCId; int mSCCIndex; std::vector mSCCStack; typedef std::pair VPair; // first is index, second is lowlink std::vector mSCCPoint; // methods used to calculate connected components void recursiveStrongConnect(int v); void buildConnectedPoints(); }; } #endif ================================================ FILE: apps/openmw/mwmechanics/pickpocket.cpp ================================================ #include "pickpocket.hpp" #include #include "../mwworld/class.hpp" #include "../mwworld/esmstore.hpp" #include "../mwbase/world.hpp" #include "../mwbase/environment.hpp" #include "npcstats.hpp" namespace MWMechanics { Pickpocket::Pickpocket(const MWWorld::Ptr &thief, const MWWorld::Ptr &victim) : mThief(thief) , mVictim(victim) { } float Pickpocket::getChanceModifier(const MWWorld::Ptr &ptr, float add) { NpcStats& stats = ptr.getClass().getNpcStats(ptr); float agility = stats.getAttribute(ESM::Attribute::Agility).getModified(); float luck = stats.getAttribute(ESM::Attribute::Luck).getModified(); float sneak = static_cast(ptr.getClass().getSkill(ptr, ESM::Skill::Sneak)); return (add + 0.2f * agility + 0.1f * luck + sneak) * stats.getFatigueTerm(); } bool Pickpocket::getDetected(float valueTerm) { float x = getChanceModifier(mThief); float y = getChanceModifier(mVictim, valueTerm); float t = 2*x - y; float pcSneak = static_cast(mThief.getClass().getSkill(mThief, ESM::Skill::Sneak)); int iPickMinChance = MWBase::Environment::get().getWorld()->getStore().get() .find("iPickMinChance")->mValue.getInteger(); int iPickMaxChance = MWBase::Environment::get().getWorld()->getStore().get() .find("iPickMaxChance")->mValue.getInteger(); int roll = Misc::Rng::roll0to99(); if (t < pcSneak / iPickMinChance) { return (roll > int(pcSneak / iPickMinChance)); } else { t = std::min(float(iPickMaxChance), t); return (roll > int(t)); } } bool Pickpocket::pick(MWWorld::Ptr item, int count) { float stackValue = static_cast(item.getClass().getValue(item) * count); float fPickPocketMod = MWBase::Environment::get().getWorld()->getStore().get() .find("fPickPocketMod")->mValue.getFloat(); float valueTerm = 10 * fPickPocketMod * stackValue; return getDetected(valueTerm); } bool Pickpocket::finish() { return getDetected(0.f); } } ================================================ FILE: apps/openmw/mwmechanics/pickpocket.hpp ================================================ #ifndef OPENMW_MECHANICS_PICKPOCKET_H #define OPENMW_MECHANICS_PICKPOCKET_H #include "../mwworld/ptr.hpp" namespace MWMechanics { class Pickpocket { public: Pickpocket (const MWWorld::Ptr& thief, const MWWorld::Ptr& victim); /// Steal some items /// @return Was the thief detected? bool pick (MWWorld::Ptr item, int count); /// End the pickpocketing process /// @return Was the thief detected? bool finish (); private: bool getDetected(float valueTerm); float getChanceModifier(const MWWorld::Ptr& ptr, float add=0); MWWorld::Ptr mThief; MWWorld::Ptr mVictim; }; } #endif ================================================ FILE: apps/openmw/mwmechanics/recharge.cpp ================================================ #include "recharge.hpp" #include /* Start of tes3mp addition Include additional headers for multiplayer purposes */ #include "../mwmp/Main.hpp" #include "../mwmp/Networking.hpp" #include "../mwmp/LocalPlayer.hpp" #include "../mwmp/MechanicsHelper.hpp" /* End of tes3mp addition */ #include "../mwbase/world.hpp" #include "../mwbase/environment.hpp" #include "../mwbase/windowmanager.hpp" #include "../mwworld/containerstore.hpp" #include "../mwworld/class.hpp" #include "../mwworld/esmstore.hpp" #include "creaturestats.hpp" #include "actorutil.hpp" namespace MWMechanics { bool rechargeItem(const MWWorld::Ptr &item, const float maxCharge, const float duration) { float charge = item.getCellRef().getEnchantmentCharge(); if (charge == -1 || charge == maxCharge) return false; static const float fMagicItemRechargePerSecond = MWBase::Environment::get().getWorld()->getStore().get().find( "fMagicItemRechargePerSecond")->mValue.getFloat(); item.getCellRef().setEnchantmentCharge(std::min(charge + fMagicItemRechargePerSecond * duration, maxCharge)); return true; } bool rechargeItem(const MWWorld::Ptr &item, const MWWorld::Ptr &gem) { if (!gem.getRefData().getCount()) return false; MWWorld::Ptr player = MWMechanics::getPlayer(); MWMechanics::CreatureStats& stats = player.getClass().getCreatureStats(player); float luckTerm = 0.1f * stats.getAttribute(ESM::Attribute::Luck).getModified(); if (luckTerm < 1 || luckTerm > 10) luckTerm = 1; float intelligenceTerm = 0.2f * stats.getAttribute(ESM::Attribute::Intelligence).getModified(); if (intelligenceTerm > 20) intelligenceTerm = 20; if (intelligenceTerm < 1) intelligenceTerm = 1; float x = (player.getClass().getSkill(player, ESM::Skill::Enchant) + intelligenceTerm + luckTerm) * stats.getFatigueTerm(); int roll = Misc::Rng::roll0to99(); if (roll < x) { std::string soul = gem.getCellRef().getSoul(); const ESM::Creature *creature = MWBase::Environment::get().getWorld()->getStore().get().find(soul); float restored = creature->mData.mSoul * (roll / x); const ESM::Enchantment* enchantment = MWBase::Environment::get().getWorld()->getStore().get().find( item.getClass().getEnchantment(item)); item.getCellRef().setEnchantmentCharge( std::min(item.getCellRef().getEnchantmentCharge() + restored, static_cast(enchantment->mData.mCharge))); /* Start of tes3mp change (minor) Send PlayerInventory packets that replace the original item with the new one */ mwmp::LocalPlayer *localPlayer = mwmp::Main::get().getLocalPlayer(); mwmp::Item removedItem = MechanicsHelper::getItem(item, 1); item.getCellRef().setEnchantmentCharge( std::min(item.getCellRef().getEnchantmentCharge() + restored, static_cast(enchantment->mData.mCharge))); mwmp::Item addedItem = MechanicsHelper::getItem(item, 1); localPlayer->sendItemChange(addedItem, mwmp::InventoryChanges::ADD); localPlayer->sendItemChange(removedItem, mwmp::InventoryChanges::REMOVE); /* End of tes3mp change (minor) */ MWBase::Environment::get().getWindowManager()->playSound("Enchant Success"); player.getClass().getContainerStore(player).restack(item); /* Start of tes3mp addition Send an ID_OBJECT_SOUND packet every time the player makes a sound here */ mwmp::ObjectList *objectList = mwmp::Main::get().getNetworking()->getObjectList(); objectList->reset(); objectList->packetOrigin = mwmp::CLIENT_GAMEPLAY; objectList->addObjectSound(MWMechanics::getPlayer(), "Enchant Success", 1.0, 1.0); objectList->sendObjectSound(); /* End of tes3mp addition */ } else { MWBase::Environment::get().getWindowManager()->playSound("Enchant Fail"); /* Start of tes3mp addition Send an ID_OBJECT_SOUND packet every time the player makes a sound here */ mwmp::ObjectList *objectList = mwmp::Main::get().getNetworking()->getObjectList(); objectList->reset(); objectList->packetOrigin = mwmp::CLIENT_GAMEPLAY; objectList->addObjectSound(MWMechanics::getPlayer(), "Enchant Fail", 1.0, 1.0); objectList->sendObjectSound(); /* End of tes3mp addition */ } player.getClass().skillUsageSucceeded (player, ESM::Skill::Enchant, 0); gem.getContainerStore()->remove(gem, 1, player); if (gem.getRefData().getCount() == 0) { std::string message = MWBase::Environment::get().getWorld()->getStore().get().find("sNotifyMessage51")->mValue.getString(); message = Misc::StringUtils::format(message, gem.getClass().getName(gem)); MWBase::Environment::get().getWindowManager()->messageBox(message); // special case: readd Azura's Star if (Misc::StringUtils::ciEqual(gem.get()->mBase->mId, "Misc_SoulGem_Azura")) player.getClass().getContainerStore(player).add("Misc_SoulGem_Azura", 1, player); } return true; } } ================================================ FILE: apps/openmw/mwmechanics/recharge.hpp ================================================ #ifndef MWMECHANICS_RECHARGE_H #define MWMECHANICS_RECHARGE_H #include "../mwworld/ptr.hpp" namespace MWMechanics { bool rechargeItem(const MWWorld::Ptr &item, const float maxCharge, const float duration); bool rechargeItem(const MWWorld::Ptr &item, const MWWorld::Ptr &gem); } #endif ================================================ FILE: apps/openmw/mwmechanics/repair.cpp ================================================ #include "repair.hpp" #include /* Start of tes3mp addition Include additional headers for multiplayer purposes */ #include "../mwmp/Main.hpp" #include "../mwmp/Networking.hpp" #include "../mwmp/LocalPlayer.hpp" #include "../mwmp/MechanicsHelper.hpp" /* End of tes3mp addition */ #include "../mwbase/world.hpp" #include "../mwbase/environment.hpp" #include "../mwbase/windowmanager.hpp" #include "../mwworld/containerstore.hpp" #include "../mwworld/class.hpp" #include "../mwworld/esmstore.hpp" #include "creaturestats.hpp" #include "actorutil.hpp" namespace MWMechanics { void Repair::repair(const MWWorld::Ptr &itemToRepair) { MWWorld::Ptr player = getPlayer(); MWWorld::LiveCellRef *ref = mTool.get(); // unstack tool if required player.getClass().getContainerStore(player).unstack(mTool, player); // reduce number of uses left int uses = mTool.getClass().getItemHealth(mTool); uses -= std::min(uses, 1); mTool.getCellRef().setCharge(uses); MWMechanics::CreatureStats& stats = player.getClass().getCreatureStats(player); float fatigueTerm = stats.getFatigueTerm(); float pcStrength = stats.getAttribute(ESM::Attribute::Strength).getModified(); float pcLuck = stats.getAttribute(ESM::Attribute::Luck).getModified(); float armorerSkill = player.getClass().getSkill(player, ESM::Skill::Armorer); float fRepairAmountMult = MWBase::Environment::get().getWorld()->getStore().get() .find("fRepairAmountMult")->mValue.getFloat(); float toolQuality = ref->mBase->mData.mQuality; float x = (0.1f * pcStrength + 0.1f * pcLuck + armorerSkill) * fatigueTerm; int roll = Misc::Rng::roll0to99(); if (roll <= x) { int y = static_cast(fRepairAmountMult * toolQuality * roll); y = std::max(1, y); // repair by 'y' points int charge = itemToRepair.getClass().getItemHealth(itemToRepair); charge = std::min(charge + y, itemToRepair.getClass().getItemMaxHealth(itemToRepair)); /* Start of tes3mp change (minor) Send PlayerInventory packets that replace the original item with the new one */ mwmp::LocalPlayer *localPlayer = mwmp::Main::get().getLocalPlayer(); mwmp::Item removedItem = MechanicsHelper::getItem(itemToRepair, 1); itemToRepair.getCellRef().setCharge(charge); mwmp::Item addedItem = MechanicsHelper::getItem(itemToRepair, 1); localPlayer->sendItemChange(addedItem, mwmp::InventoryChanges::ADD); localPlayer->sendItemChange(removedItem, mwmp::InventoryChanges::REMOVE); /* End of tes3mp change (minor) */ // attempt to re-stack item, in case it was fully repaired MWWorld::ContainerStoreIterator stacked = player.getClass().getContainerStore(player).restack(itemToRepair); // set the OnPCRepair variable on the item's script std::string script = stacked->getClass().getScript(itemToRepair); if(script != "") stacked->getRefData().getLocals().setVarByInt(script, "onpcrepair", 1); // increase skill player.getClass().skillUsageSucceeded(player, ESM::Skill::Armorer, 0); MWBase::Environment::get().getWindowManager()->playSound("Repair"); MWBase::Environment::get().getWindowManager()->messageBox("#{sRepairSuccess}"); /* Start of tes3mp addition Send an ID_OBJECT_SOUND packet every time the player makes a sound here */ mwmp::ObjectList *objectList = mwmp::Main::get().getNetworking()->getObjectList(); objectList->reset(); objectList->packetOrigin = mwmp::CLIENT_GAMEPLAY; objectList->addObjectSound(MWMechanics::getPlayer(), "Repair", 1.0, 1.0); objectList->sendObjectSound(); /* End of tes3mp addition */ } else { MWBase::Environment::get().getWindowManager()->playSound("Repair Fail"); MWBase::Environment::get().getWindowManager()->messageBox("#{sRepairFailed}"); /* Start of tes3mp addition Send an ID_OBJECT_SOUND packet every time the player makes a sound here */ mwmp::ObjectList *objectList = mwmp::Main::get().getNetworking()->getObjectList(); objectList->reset(); objectList->packetOrigin = mwmp::CLIENT_GAMEPLAY; objectList->addObjectSound(MWMechanics::getPlayer(), "Repair Fail", 1.0, 1.0); objectList->sendObjectSound(); /* End of tes3mp addition */ } // tool used up? if (mTool.getCellRef().getCharge() == 0) { MWWorld::ContainerStore& store = player.getClass().getContainerStore(player); store.remove(mTool, 1, player); std::string message = MWBase::Environment::get().getWorld()->getStore().get() .find("sNotifyMessage51")->mValue.getString(); message = Misc::StringUtils::format(message, mTool.getClass().getName(mTool)); MWBase::Environment::get().getWindowManager()->messageBox(message); // try to find a new tool of the same ID for (MWWorld::ContainerStoreIterator iter (store.begin()); iter!=store.end(); ++iter) { if (Misc::StringUtils::ciEqual(iter->getCellRef().getRefId(), mTool.getCellRef().getRefId())) { mTool = *iter; MWBase::Environment::get().getWindowManager()->playSound("Item Repair Up"); break; } } } } } ================================================ FILE: apps/openmw/mwmechanics/repair.hpp ================================================ #ifndef OPENMW_MWMECHANICS_REPAIR_H #define OPENMW_MWMECHANICS_REPAIR_H #include "../mwworld/ptr.hpp" namespace MWMechanics { class Repair { public: void setTool (const MWWorld::Ptr& tool) { mTool = tool; } MWWorld::Ptr getTool() { return mTool; } void repair (const MWWorld::Ptr& itemToRepair); private: MWWorld::Ptr mTool; }; } #endif ================================================ FILE: apps/openmw/mwmechanics/security.cpp ================================================ #include "security.hpp" /* Start of tes3mp addition Include additional headers for multiplayer purposes */ #include "../mwmp/Main.hpp" #include "../mwmp/Networking.hpp" #include "../mwmp/ObjectList.hpp" /* End of tes3mp addition */ #include "../mwworld/cellstore.hpp" #include #include "../mwworld/class.hpp" #include "../mwworld/containerstore.hpp" #include "../mwworld/esmstore.hpp" #include "../mwbase/world.hpp" #include "../mwbase/environment.hpp" #include "../mwbase/mechanicsmanager.hpp" #include "creaturestats.hpp" namespace MWMechanics { Security::Security(const MWWorld::Ptr &actor) : mActor(actor) { CreatureStats& creatureStats = actor.getClass().getCreatureStats(actor); mAgility = creatureStats.getAttribute(ESM::Attribute::Agility).getModified(); mLuck = creatureStats.getAttribute(ESM::Attribute::Luck).getModified(); mSecuritySkill = static_cast(actor.getClass().getSkill(actor, ESM::Skill::Security)); mFatigueTerm = creatureStats.getFatigueTerm(); } void Security::pickLock(const MWWorld::Ptr &lock, const MWWorld::Ptr &lockpick, std::string& resultMessage, std::string& resultSound) { if (lock.getCellRef().getLockLevel() <= 0 || lock.getCellRef().getLockLevel() == ESM::UnbreakableLock || !lock.getClass().hasToolTip(lock)) //If it's unlocked or can not be unlocked back out immediately return; int uses = lockpick.getClass().getItemHealth(lockpick); if (uses == 0) return; int lockStrength = lock.getCellRef().getLockLevel(); float pickQuality = lockpick.get()->mBase->mData.mQuality; float fPickLockMult = MWBase::Environment::get().getWorld()->getStore().get().find("fPickLockMult")->mValue.getFloat(); float x = 0.2f * mAgility + 0.1f * mLuck + mSecuritySkill; x *= pickQuality * mFatigueTerm; x += fPickLockMult * lockStrength; MWBase::Environment::get().getMechanicsManager()->unlockAttempted(mActor, lock); resultSound = "Open Lock Fail"; if (x <= 0) resultMessage = "#{sLockImpossible}"; else { if (Misc::Rng::roll0to99() <= x) { /* Start of tes3mp change (major) Disable unilateral locking on this client and expect the server's reply to our packet to do it instead */ //lock.getCellRef().unlock(); /* End of tes3mp change (major) */ /* Start of tes3mp addition Send an ID_OBJECT_LOCK packet every time an object is unlocked here */ mwmp::ObjectList *objectList = mwmp::Main::get().getNetworking()->getObjectList(); objectList->reset(); objectList->packetOrigin = mwmp::CLIENT_GAMEPLAY; objectList->addObjectLock(lock, 0); objectList->sendObjectLock(); /* End of tes3mp addition */ resultMessage = "#{sLockSuccess}"; resultSound = "Open Lock"; mActor.getClass().skillUsageSucceeded(mActor, ESM::Skill::Security, 1); } else resultMessage = "#{sLockFail}"; } lockpick.getCellRef().setCharge(--uses); if (!uses) lockpick.getContainerStore()->remove(lockpick, 1, mActor); } void Security::probeTrap(const MWWorld::Ptr &trap, const MWWorld::Ptr &probe, std::string& resultMessage, std::string& resultSound) { if (trap.getCellRef().getTrap().empty()) return; int uses = probe.getClass().getItemHealth(probe); if (uses == 0) return; float probeQuality = probe.get()->mBase->mData.mQuality; const ESM::Spell* trapSpell = MWBase::Environment::get().getWorld()->getStore().get().find(trap.getCellRef().getTrap()); int trapSpellPoints = trapSpell->mData.mCost; float fTrapCostMult = MWBase::Environment::get().getWorld()->getStore().get().find("fTrapCostMult")->mValue.getFloat(); float x = 0.2f * mAgility + 0.1f * mLuck + mSecuritySkill; x += fTrapCostMult * trapSpellPoints; x *= probeQuality * mFatigueTerm; MWBase::Environment::get().getMechanicsManager()->unlockAttempted(mActor, trap); resultSound = "Disarm Trap Fail"; if (x <= 0) resultMessage = "#{sTrapImpossible}"; else { if (Misc::Rng::roll0to99() <= x) { /* Start of tes3mp change (major) Disable unilateral trap disarming on this client and expect the server's reply to our packet to do it instead */ //trap.getCellRef().setTrap(""); /* End of tes3mp change (major) */ resultSound = "Disarm Trap"; resultMessage = "#{sTrapSuccess}"; mActor.getClass().skillUsageSucceeded(mActor, ESM::Skill::Security, 0); /* Start of tes3mp addition Send an ID_OBJECT_TRAP packet every time a trap is disarmed */ mwmp::ObjectList *objectList = mwmp::Main::get().getNetworking()->getObjectList(); objectList->reset(); objectList->packetOrigin = mwmp::CLIENT_GAMEPLAY; objectList->addObjectTrap(trap, trap.getRefData().getPosition(), true); objectList->sendObjectTrap(); /* End of tes3mp addition */ } else resultMessage = "#{sTrapFail}"; } probe.getCellRef().setCharge(--uses); if (!uses) probe.getContainerStore()->remove(probe, 1, mActor); } } ================================================ FILE: apps/openmw/mwmechanics/security.hpp ================================================ #ifndef MWMECHANICS_SECURITY_H #define MWMECHANICS_SECURITY_H #include "../mwworld/ptr.hpp" namespace MWMechanics { /// @brief implementation of Security skill class Security { public: Security (const MWWorld::Ptr& actor); void pickLock (const MWWorld::Ptr& lock, const MWWorld::Ptr& lockpick, std::string& resultMessage, std::string& resultSound); void probeTrap (const MWWorld::Ptr& trap, const MWWorld::Ptr& probe, std::string& resultMessage, std::string& resultSound); private: float mAgility, mLuck, mSecuritySkill, mFatigueTerm; MWWorld::Ptr mActor; }; } #endif ================================================ FILE: apps/openmw/mwmechanics/spellabsorption.cpp ================================================ #include "spellabsorption.hpp" #include #include "../mwbase/environment.hpp" #include "../mwbase/world.hpp" #include "../mwrender/animation.hpp" #include "../mwworld/class.hpp" #include "../mwworld/esmstore.hpp" #include "../mwworld/inventorystore.hpp" #include "creaturestats.hpp" #include "spellutil.hpp" namespace MWMechanics { class GetAbsorptionProbability : public MWMechanics::EffectSourceVisitor { public: float mProbability{0.f}; GetAbsorptionProbability() = default; void visit (MWMechanics::EffectKey key, int /*effectIndex*/, const std::string& /*sourceName*/, const std::string& /*sourceId*/, int /*casterActorId*/, float magnitude, float /*remainingTime*/, float /*totalTime*/) override { if (key.mId == ESM::MagicEffect::SpellAbsorption) { if (mProbability == 0.f) mProbability = magnitude / 100; else { // If there are different sources of SpellAbsorption effect, multiply failing probability for all effects. // Real absorption probability will be the (1 - total fail chance) in this case. float failProbability = 1.f - mProbability; failProbability *= 1.f - magnitude / 100; mProbability = 1.f - failProbability; } } } }; int getAbsorbChance(const MWWorld::Ptr& caster, const MWWorld::Ptr& target) { if(target.isEmpty() || caster == target || !target.getClass().isActor()) return 0; CreatureStats& stats = target.getClass().getCreatureStats(target); if (stats.getMagicEffects().get(ESM::MagicEffect::SpellAbsorption).getMagnitude() <= 0.f) return 0; GetAbsorptionProbability check; stats.getActiveSpells().visitEffectSources(check); stats.getSpells().visitEffectSources(check); if (target.getClass().hasInventoryStore(target)) target.getClass().getInventoryStore(target).visitEffectSources(check); return check.mProbability * 100; } void absorbSpell (const std::string& spellId, const MWWorld::Ptr& caster, const MWWorld::Ptr& target) { CreatureStats& stats = target.getClass().getCreatureStats(target); const auto& esmStore = MWBase::Environment::get().getWorld()->getStore(); const ESM::Static* absorbStatic = esmStore.get().find("VFX_Absorb"); MWRender::Animation* animation = MWBase::Environment::get().getWorld()->getAnimation(target); if (animation && !absorbStatic->mModel.empty()) animation->addEffect( "meshes\\" + absorbStatic->mModel, ESM::MagicEffect::SpellAbsorption, false, std::string()); const ESM::Spell* spell = esmStore.get().search(spellId); int spellCost = 0; if (spell) { spellCost = spell->mData.mCost; } else { const ESM::Enchantment* enchantment = esmStore.get().search(spellId); if (enchantment) spellCost = getEffectiveEnchantmentCastCost(static_cast(enchantment->mData.mCost), caster); } // Magicka is increased by the cost of the spell DynamicStat magicka = stats.getMagicka(); magicka.setCurrent(magicka.getCurrent() + spellCost); stats.setMagicka(magicka); } } ================================================ FILE: apps/openmw/mwmechanics/spellabsorption.hpp ================================================ #ifndef MWMECHANICS_SPELLABSORPTION_H #define MWMECHANICS_SPELLABSORPTION_H #include namespace MWWorld { class Ptr; } namespace MWMechanics { void absorbSpell(const std::string& spellId, const MWWorld::Ptr& caster, const MWWorld::Ptr& target); // Calculate the chance to absorb a spell based on the magnitude of every Spell Absorption effect source on the target. int getAbsorbChance(const MWWorld::Ptr& caster, const MWWorld::Ptr& target); } #endif ================================================ FILE: apps/openmw/mwmechanics/spellcasting.cpp ================================================ #include "spellcasting.hpp" #include #include /* Start of tes3mp addition Include additional headers for multiplayer purposes */ #include #include "../mwmp/Main.hpp" #include "../mwmp/Networking.hpp" #include "../mwmp/PlayerList.hpp" #include "../mwmp/LocalPlayer.hpp" #include "../mwmp/ObjectList.hpp" #include "../mwmp/CellController.hpp" #include "../mwmp/MechanicsHelper.hpp" /* End of tes3mp addition */ #include "../mwbase/windowmanager.hpp" #include "../mwbase/soundmanager.hpp" #include "../mwbase/mechanicsmanager.hpp" #include "../mwbase/environment.hpp" #include "../mwbase/world.hpp" #include "../mwworld/containerstore.hpp" #include "../mwworld/actionteleport.hpp" #include "../mwworld/player.hpp" #include "../mwworld/class.hpp" #include "../mwworld/cellstore.hpp" #include "../mwworld/esmstore.hpp" #include "../mwworld/inventorystore.hpp" #include "../mwrender/animation.hpp" #include "actorutil.hpp" #include "aifollow.hpp" #include "creaturestats.hpp" #include "linkedeffects.hpp" #include "spellabsorption.hpp" #include "spellresistance.hpp" #include "spellutil.hpp" #include "summoning.hpp" #include "tickableeffects.hpp" #include "weapontype.hpp" namespace MWMechanics { CastSpell::CastSpell(const MWWorld::Ptr &caster, const MWWorld::Ptr &target, const bool fromProjectile, const bool manualSpell) : mCaster(caster) , mTarget(target) , mFromProjectile(fromProjectile) , mManualSpell(manualSpell) { } void CastSpell::launchMagicBolt () { osg::Vec3f fallbackDirection(0, 1, 0); osg::Vec3f offset(0, 0, 0); if (!mTarget.isEmpty() && mTarget.getClass().isActor()) offset.z() = MWBase::Environment::get().getWorld()->getHalfExtents(mTarget).z(); // Fall back to a "caster to target" direction if we have no other means of determining it // (e.g. when cast by a non-actor) if (!mTarget.isEmpty()) fallbackDirection = (mTarget.getRefData().getPosition().asVec3() + offset) - (mCaster.getRefData().getPosition().asVec3()); MWBase::Environment::get().getWorld()->launchMagicBolt(mId, mCaster, fallbackDirection); } void CastSpell::inflict(const MWWorld::Ptr &target, const MWWorld::Ptr &caster, const ESM::EffectList &effects, ESM::RangeType range, bool reflected, bool exploded) { const bool targetIsActor = !target.isEmpty() && target.getClass().isActor(); if (targetIsActor) { // Early-out for characters that have departed. const auto& stats = target.getClass().getCreatureStats(target); if (stats.isDead() && stats.isDeathAnimationFinished()) return; } // If none of the effects need to apply, we can early-out bool found = false; for (const ESM::ENAMstruct& effect : effects.mList) { if (effect.mRange == range) { found = true; break; } } if (!found) return; const ESM::Spell* spell = MWBase::Environment::get().getWorld()->getStore().get().search (mId); if (spell && targetIsActor && (spell->mData.mType == ESM::Spell::ST_Disease || spell->mData.mType == ESM::Spell::ST_Blight)) { int requiredResistance = (spell->mData.mType == ESM::Spell::ST_Disease) ? ESM::MagicEffect::ResistCommonDisease : ESM::MagicEffect::ResistBlightDisease; float x = target.getClass().getCreatureStats(target).getMagicEffects().get(requiredResistance).getMagnitude(); if (Misc::Rng::roll0to99() <= x) { // Fully resisted, show message if (target == getPlayer()) MWBase::Environment::get().getWindowManager()->messageBox("#{sMagicPCResisted}"); return; } } ESM::EffectList reflectedEffects; std::vector appliedLastingEffects; // HACK: cache target's magic effects here, and add any applied effects to it. Use the cached effects for determining resistance. // This is required for Weakness effects in a spell to apply to any subsequent effects in the spell. // Otherwise, they'd only apply after the whole spell was added. MagicEffects targetEffects; if (targetIsActor) targetEffects += target.getClass().getCreatureStats(target).getMagicEffects(); bool castByPlayer = (!caster.isEmpty() && caster == getPlayer()); ActiveSpells targetSpells; if (targetIsActor) targetSpells = target.getClass().getCreatureStats(target).getActiveSpells(); bool canCastAnEffect = false; // For bound equipment.If this remains false // throughout the iteration of this spell's // effects, we display a "can't re-cast" message int absorbChance = getAbsorbChance(caster, target); int currentEffectIndex = 0; for (std::vector::const_iterator effectIt (effects.mList.begin()); !target.isEmpty() && effectIt != effects.mList.end(); ++effectIt, ++currentEffectIndex) { if (effectIt->mRange != range) continue; const ESM::MagicEffect *magicEffect = MWBase::Environment::get().getWorld()->getStore().get().find ( effectIt->mEffectID); // Re-casting a bound equipment effect has no effect if the spell is still active if (magicEffect->mData.mFlags & ESM::MagicEffect::NonRecastable && targetSpells.isSpellActive(mId)) { if (effectIt == (effects.mList.end() - 1) && !canCastAnEffect && castByPlayer) MWBase::Environment::get().getWindowManager()->messageBox("#{sMagicCannotRecast}"); continue; } canCastAnEffect = true; // Try absorbing the effect if(absorbChance && Misc::Rng::roll0to99() < absorbChance) { absorbSpell(mId, caster, target); continue; } 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())) continue; // Notify the target actor they've been hit bool isHarmful = magicEffect->mData.mFlags & ESM::MagicEffect::Harmful; if (target.getClass().isActor() && target != caster && !caster.isEmpty() && isHarmful) target.getClass().onHit(target, 0.0f, true, MWWorld::Ptr(), caster, osg::Vec3f(), true); // Reflect harmful effects if (!reflected && reflectEffect(*effectIt, magicEffect, caster, target, reflectedEffects)) continue; /* Start of tes3mp addition Now that reflected effects have been handled, don't unilaterally process effects further for dedicated players and actors on this client and instead expect their effects to be applied correctly through the SpellsActive packets received */ if (mwmp::PlayerList::isDedicatedPlayer(target) || mwmp::Main::get().getCellController()->isDedicatedActor(target)) continue; /* End of tes3mp addition */ // Try resisting. float magnitudeMult = getEffectMultiplier(effectIt->mEffectID, target, caster, spell, &targetEffects); if (magnitudeMult == 0) { // Fully resisted, show message if (target == getPlayer()) MWBase::Environment::get().getWindowManager()->messageBox("#{sMagicPCResisted}"); else if (castByPlayer) MWBase::Environment::get().getWindowManager()->messageBox("#{sMagicTargetResisted}"); } else { float magnitude = effectIt->mMagnMin + Misc::Rng::rollDice(effectIt->mMagnMax - effectIt->mMagnMin + 1); magnitude *= magnitudeMult; if (!target.getClass().isActor()) { // non-actor objects have no list of active magic effects, so have to apply instantly if (!applyInstantEffect(target, caster, EffectKey(*effectIt), magnitude)) continue; } else // target.getClass().isActor() == true { ActiveSpells::ActiveEffect effect; effect.mEffectId = effectIt->mEffectID; effect.mArg = MWMechanics::EffectKey(*effectIt).mArg; effect.mMagnitude = magnitude; effect.mTimeLeft = 0.f; effect.mEffectIndex = currentEffectIndex; // Avoid applying absorb effects if the caster is the target // We still need the spell to be added if (caster == target && effectIt->mEffectID >= ESM::MagicEffect::AbsorbAttribute && effectIt->mEffectID <= ESM::MagicEffect::AbsorbSkill) { effect.mMagnitude = 0; } // Avoid applying harmful effects to the player in god mode if (target == getPlayer() && MWBase::Environment::get().getWorld()->getGodModeState() && isHarmful) { effect.mMagnitude = 0; } bool effectAffectsHealth = isHarmful || effectIt->mEffectID == ESM::MagicEffect::RestoreHealth; if (castByPlayer && target != caster && !target.getClass().getCreatureStats(target).isDead() && effectAffectsHealth) { // If player is attempting to cast a harmful spell on or is healing a living target, show the target's HP bar. MWBase::Environment::get().getWindowManager()->setEnemy(target); } bool hasDuration = !(magicEffect->mData.mFlags & ESM::MagicEffect::NoDuration); effect.mDuration = hasDuration ? static_cast(effectIt->mDuration) : 1.f; bool appliedOnce = magicEffect->mData.mFlags & ESM::MagicEffect::AppliedOnce; if (!appliedOnce) effect.mDuration = std::max(1.f, effect.mDuration); if (effect.mDuration == 0) { // We still should add effect to list to allow GetSpellEffects to detect this spell appliedLastingEffects.push_back(effect); // duration 0 means apply full magnitude instantly bool wasDead = target.getClass().getCreatureStats(target).isDead(); effectTick(target.getClass().getCreatureStats(target), target, EffectKey(*effectIt), effect.mMagnitude); bool isDead = target.getClass().getCreatureStats(target).isDead(); /* Start of tes3mp addition If the target was a LocalPlayer or LocalActor who died, record the caster as the killer */ if (!wasDead && isDead) { bool isSuicide = target == caster || caster.isEmpty(); if (target == MWMechanics::getPlayer()) { mwmp::Main::get().getLocalPlayer()->killer = isSuicide ? MechanicsHelper::getTarget(target) : MechanicsHelper::getTarget(caster); } else if (mwmp::Main::get().getCellController()->isLocalActor(target)) { mwmp::Main::get().getCellController()->getLocalActor(target)->killer = isSuicide ? MechanicsHelper::getTarget(target) : MechanicsHelper::getTarget(caster); } } /* End of tes3mp addition */ if (!wasDead && isDead) MWBase::Environment::get().getMechanicsManager()->actorKilled(target, caster); } else { effect.mTimeLeft = effect.mDuration; targetEffects.add(MWMechanics::EffectKey(*effectIt), MWMechanics::EffectParam(effect.mMagnitude)); // add to list of active effects, to apply in next frame appliedLastingEffects.push_back(effect); // Unequip all items, if a spell with the ExtraSpell effect was casted if (effectIt->mEffectID == ESM::MagicEffect::ExtraSpell && target.getClass().hasInventoryStore(target)) { MWWorld::InventoryStore& store = target.getClass().getInventoryStore(target); store.unequipAll(target); } // Command spells should have their effect, including taking the target out of combat, each time the spell successfully affects the target if (((effectIt->mEffectID == ESM::MagicEffect::CommandHumanoid && target.getClass().isNpc()) || (effectIt->mEffectID == ESM::MagicEffect::CommandCreature && target.getTypeName() == typeid(ESM::Creature).name())) && !caster.isEmpty() && caster.getClass().isActor() && target != getPlayer() && effect.mMagnitude >= target.getClass().getCreatureStats(target).getLevel()) { MWMechanics::AiFollow package(caster, true); target.getClass().getCreatureStats(target).getAiSequence().stack(package, target); } // For absorb effects, also apply the effect to the caster - but with a negative // magnitude, since we're transferring stats from the target to the caster if (effectIt->mEffectID >= ESM::MagicEffect::AbsorbAttribute && effectIt->mEffectID <= ESM::MagicEffect::AbsorbSkill) absorbStat(*effectIt, effect, caster, target, reflected, mSourceName); } } // Re-casting a summon effect will remove the creature from previous castings of that effect. if (isSummoningEffect(effectIt->mEffectID) && targetIsActor) { CreatureStats& targetStats = target.getClass().getCreatureStats(target); ESM::SummonKey key(effectIt->mEffectID, mId, currentEffectIndex); auto findCreature = targetStats.getSummonedCreatureMap().find(key); if (findCreature != targetStats.getSummonedCreatureMap().end()) { /* Start of tes3mp change (major) Don't clean up placeholder summoned creatures still awaiting a spawn packet from the server, because that would make the packet create permanent spawns instead */ if (findCreature->second != -1) { MWBase::Environment::get().getMechanicsManager()->cleanupSummonedCreature(target, findCreature->second); targetStats.getSummonedCreatureMap().erase(findCreature); } /* End of tes3mp change (major) */ } } if (target.getClass().isActor() || magicEffect->mData.mFlags & ESM::MagicEffect::NoDuration) { static const std::string schools[] = { "alteration", "conjuration", "destruction", "illusion", "mysticism", "restoration" }; MWBase::SoundManager *sndMgr = MWBase::Environment::get().getSoundManager(); if(!magicEffect->mHitSound.empty()) sndMgr->playSound3D(target, magicEffect->mHitSound, 1.0f, 1.0f); else sndMgr->playSound3D(target, schools[magicEffect->mData.mSchool]+" hit", 1.0f, 1.0f); /* Start of tes3mp addition Send an ID_OBJECT_SOUND packet every time a sound is made here */ mwmp::ObjectList* objectList = mwmp::Main::get().getNetworking()->getObjectList(); objectList->reset(); objectList->packetOrigin = mwmp::CLIENT_GAMEPLAY; objectList->addObjectSound(target, magicEffect->mHitSound.empty() ? schools[magicEffect->mData.mSchool] + " hit" : magicEffect->mHitSound, 1.0f, 1.0f); objectList->sendObjectSound(); /* End of tes3mp addition */ // Add VFX const ESM::Static* castStatic; if (!magicEffect->mHit.empty()) castStatic = MWBase::Environment::get().getWorld()->getStore().get().find (magicEffect->mHit); else castStatic = MWBase::Environment::get().getWorld()->getStore().get().find ("VFX_DefaultHit"); bool loop = (magicEffect->mData.mFlags & ESM::MagicEffect::ContinuousVfx) != 0; // Note: in case of non actor, a free effect should be fine as well MWRender::Animation* anim = MWBase::Environment::get().getWorld()->getAnimation(target); if (anim && !castStatic->mModel.empty()) anim->addEffect("meshes\\" + castStatic->mModel, magicEffect->mIndex, loop, "", magicEffect->mParticle); } } } if (!exploded) MWBase::Environment::get().getWorld()->explodeSpell(mHitPosition, effects, caster, target, range, mId, mSourceName, mFromProjectile); if (!target.isEmpty()) { if (!reflectedEffects.mList.empty()) inflict(caster, target, reflectedEffects, range, true, exploded); if (!appliedLastingEffects.empty()) { int casterActorId = -1; if (!caster.isEmpty() && caster.getClass().isActor()) casterActorId = caster.getClass().getCreatureStats(caster).getActorId(); target.getClass().getCreatureStats(target).getActiveSpells().addSpell(mId, mStack, appliedLastingEffects, mSourceName, casterActorId); } } } bool CastSpell::applyInstantEffect(const MWWorld::Ptr &target, const MWWorld::Ptr &caster, const MWMechanics::EffectKey& effect, float magnitude) { short effectId = effect.mId; if (target.getClass().canLock(target)) { if (effectId == ESM::MagicEffect::Lock) { const MWWorld::ESMStore& store = MWBase::Environment::get().getWorld()->getStore(); const ESM::MagicEffect *magiceffect = store.get().find(effectId); MWRender::Animation* animation = MWBase::Environment::get().getWorld()->getAnimation(target); if (animation) animation->addSpellCastGlow(magiceffect); if (target.getCellRef().getLockLevel() < magnitude) //If the door is not already locked to a higher value, lock it to spell magnitude { if (caster == getPlayer()) MWBase::Environment::get().getWindowManager()->messageBox("#{sMagicLockSuccess}"); /* Start of tes3mp change (major) Disable unilateral locking on this client and expect the server's reply to our packet to do it instead */ //target.getCellRef().lock(static_cast(magnitude)); /* End of tes3mp change (major) */ /* Start of tes3mp addition Send an ID_OBJECT_LOCK packet every time an object is locked here */ mwmp::ObjectList *objectList = mwmp::Main::get().getNetworking()->getObjectList(); objectList->reset(); objectList->packetOrigin = mwmp::CLIENT_GAMEPLAY; objectList->addObjectLock(target, static_cast(magnitude)); objectList->sendObjectLock(); /* End of tes3mp addition */ } return true; } else if (effectId == ESM::MagicEffect::Open) { if (!caster.isEmpty()) { MWBase::Environment::get().getMechanicsManager()->unlockAttempted(getPlayer(), target); // Use the player instead of the caster for vanilla crime compatibility } const MWWorld::ESMStore& store = MWBase::Environment::get().getWorld()->getStore(); const ESM::MagicEffect *magiceffect = store.get().find(effectId); MWRender::Animation* animation = MWBase::Environment::get().getWorld()->getAnimation(target); if (animation) animation->addSpellCastGlow(magiceffect); if (target.getCellRef().getLockLevel() <= magnitude) { if (target.getCellRef().getLockLevel() > 0) { MWBase::Environment::get().getSoundManager()->playSound3D(target, "Open Lock", 1.f, 1.f); if (caster == getPlayer()) MWBase::Environment::get().getWindowManager()->messageBox("#{sMagicOpenSuccess}"); } /* Start of tes3mp change (major) Disable unilateral locking on this client and expect the server's reply to our packet to do it instead */ //target.getCellRef().unlock(); /* End of tes3mp change (major) */ /* Start of tes3mp addition Send an ID_OBJECT_LOCK packet every time an object is unlocked here */ mwmp::ObjectList *objectList = mwmp::Main::get().getNetworking()->getObjectList(); objectList->reset(); objectList->packetOrigin = mwmp::CLIENT_GAMEPLAY; objectList->addObjectLock(target, 0); objectList->sendObjectLock(); /* End of tes3mp addition */ } else { MWBase::Environment::get().getSoundManager()->playSound3D(target, "Open Lock Fail", 1.f, 1.f); } return true; } } else if (target.getClass().isActor() && effectId == ESM::MagicEffect::Dispel) { target.getClass().getCreatureStats(target).getActiveSpells().purgeAll(magnitude, true); return true; } else if (target.getClass().isActor() && target == getPlayer()) { MWRender::Animation* anim = MWBase::Environment::get().getWorld()->getAnimation(mCaster); bool teleportingEnabled = MWBase::Environment::get().getWorld()->isTeleportingEnabled(); if (effectId == ESM::MagicEffect::DivineIntervention || effectId == ESM::MagicEffect::AlmsiviIntervention) { if (!teleportingEnabled) { if (caster == getPlayer()) MWBase::Environment::get().getWindowManager()->messageBox("#{sTeleportDisabled}"); return true; } std::string marker = (effectId == ESM::MagicEffect::DivineIntervention) ? "divinemarker" : "templemarker"; MWBase::Environment::get().getWorld()->teleportToClosestMarker(target, marker); anim->removeEffect(effectId); const ESM::Static* fx = MWBase::Environment::get().getWorld()->getStore().get() .search("VFX_Summon_end"); if (fx) anim->addEffect("meshes\\" + fx->mModel, -1); return true; } else if (effectId == ESM::MagicEffect::Mark) { if (teleportingEnabled) { MWBase::Environment::get().getWorld()->getPlayer().markPosition( target.getCell(), target.getRefData().getPosition()); } else if (caster == getPlayer()) { MWBase::Environment::get().getWindowManager()->messageBox("#{sTeleportDisabled}"); } /* Start of tes3mp addition Send a PlayerMiscellaneous packet with the player's new mark location */ mwmp::Main::get().getLocalPlayer()->sendMarkLocation(*target.getCell()->getCell(), target.getRefData().getPosition()); /* End of tes3mp addition */ return true; } else if (effectId == ESM::MagicEffect::Recall) { if (!teleportingEnabled) { if (caster == getPlayer()) MWBase::Environment::get().getWindowManager()->messageBox("#{sTeleportDisabled}"); return true; } MWWorld::CellStore* markedCell = nullptr; ESM::Position markedPosition; MWBase::Environment::get().getWorld()->getPlayer().getMarkedPosition(markedCell, markedPosition); if (markedCell) { MWWorld::ActionTeleport action(markedCell->isExterior() ? "" : markedCell->getCell()->mName, markedPosition, false); action.execute(target); anim->removeEffect(effectId); } return true; } } return false; } bool CastSpell::cast(const std::string &id) { const MWWorld::ESMStore& store = MWBase::Environment::get().getWorld()->getStore(); if (const auto spell = store.get().search(id)) return cast(spell); if (const auto potion = store.get().search(id)) return cast(potion); if (const auto ingredient = store.get().search(id)) return cast(ingredient); throw std::runtime_error("ID type cannot be casted"); } bool CastSpell::cast(const MWWorld::Ptr &item, bool launchProjectile) { std::string enchantmentName = item.getClass().getEnchantment(item); if (enchantmentName.empty()) throw std::runtime_error("can't cast an item without an enchantment"); mSourceName = item.getClass().getName(item); mId = item.getCellRef().getRefId(); const ESM::Enchantment* enchantment = MWBase::Environment::get().getWorld()->getStore().get().find(enchantmentName); mStack = false; bool godmode = mCaster == MWMechanics::getPlayer() && MWBase::Environment::get().getWorld()->getGodModeState(); bool isProjectile = false; if (item.getTypeName() == typeid(ESM::Weapon).name()) { int type = item.get()->mBase->mData.mType; ESM::WeaponType::Class weapclass = MWMechanics::getWeaponType(type)->mWeaponClass; isProjectile = (weapclass == ESM::WeaponType::Thrown || weapclass == ESM::WeaponType::Ammo); } int type = enchantment->mData.mType; // Check if there's enough charge left if (!godmode && (type == ESM::Enchantment::WhenUsed || (!isProjectile && type == ESM::Enchantment::WhenStrikes))) { int castCost = getEffectiveEnchantmentCastCost(static_cast(enchantment->mData.mCost), mCaster); if (item.getCellRef().getEnchantmentCharge() == -1) item.getCellRef().setEnchantmentCharge(static_cast(enchantment->mData.mCharge)); if (item.getCellRef().getEnchantmentCharge() < castCost) { if (mCaster == getPlayer()) { MWBase::Environment::get().getWindowManager()->messageBox("#{sMagicInsufficientCharge}"); // Failure sound int school = 0; if (!enchantment->mEffects.mList.empty()) { short effectId = enchantment->mEffects.mList.front().mEffectID; const ESM::MagicEffect* magicEffect = MWBase::Environment::get().getWorld()->getStore().get().find(effectId); school = magicEffect->mData.mSchool; } static const std::string schools[] = { "alteration", "conjuration", "destruction", "illusion", "mysticism", "restoration" }; MWBase::SoundManager *sndMgr = MWBase::Environment::get().getSoundManager(); sndMgr->playSound3D(mCaster, "Spell Failure " + schools[school], 1.0f, 1.0f); /* Start of tes3mp addition Send an ID_OBJECT_SOUND packet every time a sound is made here */ mwmp::ObjectList* objectList = mwmp::Main::get().getNetworking()->getObjectList(); objectList->reset(); objectList->packetOrigin = mwmp::CLIENT_GAMEPLAY; objectList->addObjectSound(mCaster, "Spell Failure " + schools[school], 1.0f, 1.0f); objectList->sendObjectSound(); /* End of tes3mp addition */ } return false; } // Reduce charge item.getCellRef().setEnchantmentCharge(item.getCellRef().getEnchantmentCharge() - castCost); } if (type == ESM::Enchantment::WhenUsed) { if (mCaster == getPlayer()) mCaster.getClass().skillUsageSucceeded (mCaster, ESM::Skill::Enchant, 1); } else if (type == ESM::Enchantment::CastOnce) { if (!godmode) item.getContainerStore()->remove(item, 1, mCaster); } else if (type == ESM::Enchantment::WhenStrikes) { if (mCaster == getPlayer()) mCaster.getClass().skillUsageSucceeded (mCaster, ESM::Skill::Enchant, 3); } inflict(mCaster, mCaster, enchantment->mEffects, ESM::RT_Self); if (isProjectile || !mTarget.isEmpty()) inflict(mTarget, mCaster, enchantment->mEffects, ESM::RT_Touch); if (launchProjectile) launchMagicBolt(); else if (isProjectile || !mTarget.isEmpty()) inflict(mTarget, mCaster, enchantment->mEffects, ESM::RT_Target); return true; } bool CastSpell::cast(const ESM::Potion* potion) { mSourceName = potion->mName; mId = potion->mId; mStack = true; inflict(mCaster, mCaster, potion->mEffects, ESM::RT_Self); return true; } bool CastSpell::cast(const ESM::Spell* spell) { mSourceName = spell->mName; mId = spell->mId; mStack = false; const MWWorld::ESMStore& store = MWBase::Environment::get().getWorld()->getStore(); int school = 0; bool godmode = mCaster == MWMechanics::getPlayer() && MWBase::Environment::get().getWorld()->getGodModeState(); if (mCaster.getClass().isActor() && !mAlwaysSucceed && !mManualSpell) { school = getSpellSchool(spell, mCaster); CreatureStats& stats = mCaster.getClass().getCreatureStats(mCaster); if (!godmode) { // Reduce fatigue (note that in the vanilla game, both GMSTs are 0, and there's no fatigue loss) static const float fFatigueSpellBase = store.get().find("fFatigueSpellBase")->mValue.getFloat(); static const float fFatigueSpellMult = store.get().find("fFatigueSpellMult")->mValue.getFloat(); DynamicStat fatigue = stats.getFatigue(); const float normalizedEncumbrance = mCaster.getClass().getNormalizedEncumbrance(mCaster); float fatigueLoss = spell->mData.mCost * (fFatigueSpellBase + normalizedEncumbrance * fFatigueSpellMult); fatigue.setCurrent(fatigue.getCurrent() - fatigueLoss); stats.setFatigue(fatigue); bool fail = false; /* Start of tes3mp change (major) Make spell casting fail based on the casting success rated determined in MechanicsHelper::getSpellSuccess() */ mwmp::Cast *localCast = NULL; mwmp::Cast *dedicatedCast = MechanicsHelper::getDedicatedCast(mCaster); if (dedicatedCast) dedicatedCast->pressed = false; else { localCast = MechanicsHelper::getLocalCast(mCaster); localCast->success = MechanicsHelper::getSpellSuccess(mId, mCaster); localCast->pressed = false; localCast->shouldSend = true; } // Check success if ((localCast && localCast->success == false) || (dedicatedCast && dedicatedCast->success == false)) { if (mCaster == getPlayer()) MWBase::Environment::get().getWindowManager()->messageBox("#{sMagicSkillFail}"); fail = true; } /* End of tes3mp change (major) */ if (fail) { // Failure sound static const std::string schools[] = { "alteration", "conjuration", "destruction", "illusion", "mysticism", "restoration" }; MWBase::SoundManager *sndMgr = MWBase::Environment::get().getSoundManager(); sndMgr->playSound3D(mCaster, "Spell Failure " + schools[school], 1.0f, 1.0f); /* Start of tes3mp addition Send an ID_OBJECT_SOUND packet every time a sound is made here */ mwmp::ObjectList* objectList = mwmp::Main::get().getNetworking()->getObjectList(); objectList->reset(); objectList->packetOrigin = mwmp::CLIENT_GAMEPLAY; objectList->addObjectSound(mCaster, "Spell Failure " + schools[school], 1.0f, 1.0f); objectList->sendObjectSound(); /* End of tes3mp addition */ return false; } } // A power can be used once per 24h if (spell->mData.mType == ESM::Spell::ST_Power) stats.getSpells().usePower(spell); } if (!mManualSpell && mCaster == getPlayer() && spellIncreasesSkill(spell)) mCaster.getClass().skillUsageSucceeded(mCaster, spellSchoolToSkill(school), 0); // A non-actor doesn't play its spell cast effects from a character controller, so play them here if (!mCaster.getClass().isActor()) playSpellCastingEffects(spell->mEffects.mList); inflict(mCaster, mCaster, spell->mEffects, ESM::RT_Self); if (!mTarget.isEmpty()) inflict(mTarget, mCaster, spell->mEffects, ESM::RT_Touch); launchMagicBolt(); return true; } bool CastSpell::cast (const ESM::Ingredient* ingredient) { mId = ingredient->mId; mStack = true; mSourceName = ingredient->mName; ESM::ENAMstruct effect; effect.mEffectID = ingredient->mData.mEffectID[0]; effect.mSkill = ingredient->mData.mSkills[0]; effect.mAttribute = ingredient->mData.mAttributes[0]; effect.mRange = ESM::RT_Self; effect.mArea = 0; const MWWorld::ESMStore& store = MWBase::Environment::get().getWorld()->getStore(); const auto magicEffect = store.get().find(effect.mEffectID); const MWMechanics::CreatureStats& creatureStats = mCaster.getClass().getCreatureStats(mCaster); float x = (mCaster.getClass().getSkill(mCaster, ESM::Skill::Alchemy) + 0.2f * creatureStats.getAttribute (ESM::Attribute::Intelligence).getModified() + 0.1f * creatureStats.getAttribute (ESM::Attribute::Luck).getModified()) * creatureStats.getFatigueTerm(); int roll = Misc::Rng::roll0to99(); if (roll > x) { // "X has no effect on you" std::string message = store.get().find("sNotifyMessage50")->mValue.getString(); message = Misc::StringUtils::format(message, ingredient->mName); MWBase::Environment::get().getWindowManager()->messageBox(message); return false; } float magnitude = 0; float y = roll / std::min(x, 100.f); y *= 0.25f * x; if (magicEffect->mData.mFlags & ESM::MagicEffect::NoDuration) effect.mDuration = 1; else effect.mDuration = static_cast(y); if (!(magicEffect->mData.mFlags & ESM::MagicEffect::NoMagnitude)) { if (!(magicEffect->mData.mFlags & ESM::MagicEffect::NoDuration)) magnitude = floor((0.05f * y) / (0.1f * magicEffect->mData.mBaseCost)); else magnitude = floor(y / (0.1f * magicEffect->mData.mBaseCost)); magnitude = std::max(1.f, magnitude); } else magnitude = 1; effect.mMagnMax = static_cast(magnitude); effect.mMagnMin = static_cast(magnitude); ESM::EffectList effects; effects.mList.push_back(effect); inflict(mCaster, mCaster, effects, ESM::RT_Self); return true; } void CastSpell::playSpellCastingEffects(const std::string &spellid, bool enchantment) { const MWWorld::ESMStore& store = MWBase::Environment::get().getWorld()->getStore(); if (enchantment) { if (const auto spell = store.get().search(spellid)) playSpellCastingEffects(spell->mEffects.mList); } else { if (const auto spell = store.get().search(spellid)) playSpellCastingEffects(spell->mEffects.mList); } } void CastSpell::playSpellCastingEffects(const std::vector& effects) { const MWWorld::ESMStore& store = MWBase::Environment::get().getWorld()->getStore(); std::vector addedEffects; for (const ESM::ENAMstruct& effectData : effects) { const auto effect = store.get().find(effectData.mEffectID); const ESM::Static* castStatic; if (!effect->mCasting.empty()) castStatic = store.get().find (effect->mCasting); else castStatic = store.get().find ("VFX_DefaultCast"); // check if the effect was already added if (std::find(addedEffects.begin(), addedEffects.end(), "meshes\\" + castStatic->mModel) != addedEffects.end()) continue; MWRender::Animation* animation = MWBase::Environment::get().getWorld()->getAnimation(mCaster); if (animation) { animation->addEffect("meshes\\" + castStatic->mModel, effect->mIndex, false, "", effect->mParticle); } else { // If the caster has no animation, add the effect directly to the effectManager // We should scale it manually osg::Vec3f bounds (MWBase::Environment::get().getWorld()->getHalfExtents(mCaster) * 2.f / Constants::UnitsPerFoot); float scale = std::max({ bounds.x()/3.f, bounds.y()/3.f, bounds.z()/6.f }); float meshScale = !mCaster.getClass().isActor() ? mCaster.getCellRef().getScale() : 1.0f; osg::Vec3f pos (mCaster.getRefData().getPosition().asVec3()); MWBase::Environment::get().getWorld()->spawnEffect("meshes\\" + castStatic->mModel, effect->mParticle, pos, scale * meshScale); } if (animation && !mCaster.getClass().isActor()) animation->addSpellCastGlow(effect); static const std::string schools[] = { "alteration", "conjuration", "destruction", "illusion", "mysticism", "restoration" }; addedEffects.push_back("meshes\\" + castStatic->mModel); MWBase::SoundManager *sndMgr = MWBase::Environment::get().getSoundManager(); if(!effect->mCastSound.empty()) sndMgr->playSound3D(mCaster, effect->mCastSound, 1.0f, 1.0f); else sndMgr->playSound3D(mCaster, schools[effect->mData.mSchool]+" cast", 1.0f, 1.0f); } } } ================================================ FILE: apps/openmw/mwmechanics/spellcasting.hpp ================================================ #ifndef MWMECHANICS_SPELLCASTING_H #define MWMECHANICS_SPELLCASTING_H #include #include "../mwworld/ptr.hpp" namespace ESM { struct Spell; struct Ingredient; struct Potion; struct EffectList; } namespace MWMechanics { struct EffectKey; class CastSpell { private: MWWorld::Ptr mCaster; // May be empty MWWorld::Ptr mTarget; // May be empty void playSpellCastingEffects(const std::vector& effects); public: bool mStack{false}; std::string mId; // ID of spell, potion, item etc std::string mSourceName; // Display name for spell, potion, etc osg::Vec3f mHitPosition{0,0,0}; // Used for spawning area orb bool mAlwaysSucceed{false}; // Always succeed spells casted by NPCs/creatures regardless of their chance (default: false) bool mFromProjectile; // True if spell is cast by enchantment of some projectile (arrow, bolt or thrown weapon) bool mManualSpell; // True if spell is casted from script and ignores some checks (mana level, success chance, etc.) public: CastSpell(const MWWorld::Ptr& caster, const MWWorld::Ptr& target, const bool fromProjectile=false, const bool manualSpell=false); bool cast (const ESM::Spell* spell); /// @note mCaster must be an actor /// @param launchProjectile If set to false, "on target" effects are directly applied instead of being launched as projectile originating from the caster. bool cast (const MWWorld::Ptr& item, bool launchProjectile=true); /// @note mCaster must be an NPC bool cast (const ESM::Ingredient* ingredient); bool cast (const ESM::Potion* potion); /// @note Auto detects if spell, ingredient or potion bool cast (const std::string& id); void playSpellCastingEffects(const std::string &spellid, bool enchantment); /// Launch a bolt with the given effects. void launchMagicBolt (); /// @note \a target can be any type of object, not just actors. /// @note \a caster can be any type of object, or even an empty object. void inflict (const MWWorld::Ptr& target, const MWWorld::Ptr& caster, const ESM::EffectList& effects, ESM::RangeType range, bool reflected=false, bool exploded=false); /// @note \a caster can be any type of object, or even an empty object. /// @return was the target suitable for the effect? bool applyInstantEffect (const MWWorld::Ptr& target, const MWWorld::Ptr& caster, const MWMechanics::EffectKey& effect, float magnitude); }; } #endif ================================================ FILE: apps/openmw/mwmechanics/spelllist.cpp ================================================ #include "spelllist.hpp" #include #include #include "spells.hpp" #include "../mwbase/environment.hpp" #include "../mwbase/world.hpp" #include "../mwworld/esmstore.hpp" namespace { template const std::vector getSpellList(const std::string& id) { return MWBase::Environment::get().getWorld()->getStore().get().find(id)->mSpells.mList; } template bool withBaseRecord(const std::string& id, const std::function&)>& function) { T copy = *MWBase::Environment::get().getWorld()->getStore().get().find(id); bool changed = function(copy.mSpells.mList); if(changed) MWBase::Environment::get().getWorld()->createOverrideRecord(copy); return changed; } } namespace MWMechanics { SpellList::SpellList(const std::string& id, int type) : mId(id), mType(type) {} bool SpellList::withBaseRecord(const std::function&)>& function) { switch(mType) { case ESM::REC_CREA: return ::withBaseRecord(mId, function); case ESM::REC_NPC_: return ::withBaseRecord(mId, function); default: throw std::logic_error("failed to update base record for " + mId); } } const std::vector SpellList::getSpells() const { switch(mType) { case ESM::REC_CREA: return getSpellList(mId); case ESM::REC_NPC_: return getSpellList(mId); default: throw std::logic_error("failed to get spell list for " + mId); } } const ESM::Spell* SpellList::getSpell(const std::string& id) { return MWBase::Environment::get().getWorld()->getStore().get().find(id); } void SpellList::add (const ESM::Spell* spell) { auto& id = spell->mId; bool changed = withBaseRecord([&] (auto& spells) { for(const auto& it : spells) { if(Misc::StringUtils::ciEqual(id, it)) return false; } spells.push_back(id); return true; }); if(changed) { for(auto listener : mListeners) listener->addSpell(spell); } } void SpellList::remove (const ESM::Spell* spell) { auto& id = spell->mId; bool changed = withBaseRecord([&] (auto& spells) { for(auto it = spells.begin(); it != spells.end(); it++) { if(Misc::StringUtils::ciEqual(id, *it)) { spells.erase(it); return true; } } return false; }); if(changed) { for(auto listener : mListeners) listener->removeSpell(spell); } } void SpellList::removeAll (const std::vector& ids) { bool changed = withBaseRecord([&] (auto& spells) { const auto it = std::remove_if(spells.begin(), spells.end(), [&] (const auto& spell) { const auto isSpell = [&] (const auto& id) { return Misc::StringUtils::ciEqual(spell, id); }; return ids.end() != std::find_if(ids.begin(), ids.end(), isSpell); }); if (it == spells.end()) return false; spells.erase(it, spells.end()); return true; }); if(changed) { for(auto listener : mListeners) { for(auto& id : ids) { const auto spell = getSpell(id); listener->removeSpell(spell); } } } } void SpellList::clear() { bool changed = withBaseRecord([] (auto& spells) { if(spells.empty()) return false; spells.clear(); return true; }); if(changed) { for(auto listener : mListeners) listener->removeAllSpells(); } } void SpellList::addListener(Spells* spells) { if (std::find(mListeners.begin(), mListeners.end(), spells) != mListeners.end()) return; mListeners.push_back(spells); } void SpellList::removeListener(Spells* spells) { const auto it = std::find(mListeners.begin(), mListeners.end(), spells); if (it != mListeners.end()) mListeners.erase(it); } void SpellList::updateListener(Spells* before, Spells* after) { const auto it = std::find(mListeners.begin(), mListeners.end(), before); if (it == mListeners.end()) return mListeners.push_back(after); *it = after; } } ================================================ FILE: apps/openmw/mwmechanics/spelllist.hpp ================================================ #ifndef GAME_MWMECHANICS_SPELLLIST_H #define GAME_MWMECHANICS_SPELLLIST_H #include #include #include #include #include #include namespace ESM { struct SpellState; } namespace MWMechanics { struct SpellParams { std::map mEffectRands; // std::set mPurgedEffects; // indices of purged effects }; class Spells; /// Multiple instances of the same actor share the same spell list in Morrowind. /// The most obvious result of this is that adding a spell or ability to one instance adds it to all instances. /// @note The original game will only update visual effects associated with any added abilities for the originally targeted actor, /// changing cells applies the update to all actors. /// Aside from sharing the same active spell list, changes made to this list are also written to the actor's base record. /// Interestingly, it is not just scripted changes that are persisted to the base record. Curing one instance's disease will cure all instances. /// @note The original game is inconsistent in persisting this example; /// saving and loading the game might reapply the cured disease depending on which instance was cured. class SpellList { const std::string mId; const int mType; std::vector mListeners; bool withBaseRecord(const std::function&)>& function); public: SpellList(const std::string& id, int type); /// Get spell from ID, throws exception if not found static const ESM::Spell* getSpell(const std::string& id); void add (const ESM::Spell* spell); ///< Adding a spell that is already listed in *this is a no-op. void remove (const ESM::Spell* spell); void removeAll(const std::vector& spells); void clear(); ///< Remove all spells of all types. void addListener(Spells* spells); void removeListener(Spells* spells); void updateListener(Spells* before, Spells* after); const std::vector getSpells() const; }; } #endif ================================================ FILE: apps/openmw/mwmechanics/spellpriority.cpp ================================================ #include "spellpriority.hpp" #include "weaponpriority.hpp" #include #include #include #include "../mwbase/environment.hpp" #include "../mwbase/world.hpp" #include "../mwbase/mechanicsmanager.hpp" #include "../mwworld/class.hpp" #include "../mwworld/esmstore.hpp" #include "../mwworld/inventorystore.hpp" #include "../mwworld/cellstore.hpp" #include "creaturestats.hpp" #include "spellresistance.hpp" #include "weapontype.hpp" #include "summoning.hpp" #include "spellutil.hpp" namespace { int numEffectsToDispel (const MWWorld::Ptr& actor, int effectFilter=-1, bool negative = true) { int toCure=0; const MWMechanics::ActiveSpells& activeSpells = actor.getClass().getCreatureStats(actor).getActiveSpells(); for (MWMechanics::ActiveSpells::TIterator it = activeSpells.begin(); it != activeSpells.end(); ++it) { // if the effect filter is not specified, take in account only spells effects. Leave potions, enchanted items etc. if (effectFilter == -1) { const ESM::Spell* spell = MWBase::Environment::get().getWorld()->getStore().get().search(it->first); if (!spell || spell->mData.mType != ESM::Spell::ST_Spell) continue; } const MWMechanics::ActiveSpells::ActiveSpellParams& params = it->second; for (std::vector::const_iterator effectIt = params.mEffects.begin(); effectIt != params.mEffects.end(); ++effectIt) { int effectId = effectIt->mEffectId; if (effectFilter != -1 && effectId != effectFilter) continue; const ESM::MagicEffect* magicEffect = MWBase::Environment::get().getWorld()->getStore().get().find(effectId); if (effectIt->mDuration <= 3) // Don't attempt to dispel if effect runs out shortly anyway continue; if (negative && magicEffect->mData.mFlags & ESM::MagicEffect::Harmful) ++toCure; if (!negative && !(magicEffect->mData.mFlags & ESM::MagicEffect::Harmful)) ++toCure; } } return toCure; } float getSpellDuration (const MWWorld::Ptr& actor, const std::string& spellId) { float duration = 0; const MWMechanics::ActiveSpells& activeSpells = actor.getClass().getCreatureStats(actor).getActiveSpells(); for (MWMechanics::ActiveSpells::TIterator it = activeSpells.begin(); it != activeSpells.end(); ++it) { if (it->first != spellId) continue; const MWMechanics::ActiveSpells::ActiveSpellParams& params = it->second; for (std::vector::const_iterator effectIt = params.mEffects.begin(); effectIt != params.mEffects.end(); ++effectIt) { if (effectIt->mDuration > duration) duration = effectIt->mDuration; } } return duration; } } namespace MWMechanics { int getRangeTypes (const ESM::EffectList& effects) { int types = 0; for (std::vector::const_iterator it = effects.mList.begin(); it != effects.mList.end(); ++it) { if (it->mRange == ESM::RT_Self) types |= RangeTypes::Self; else if (it->mRange == ESM::RT_Touch) types |= RangeTypes::Touch; else if (it->mRange == ESM::RT_Target) types |= RangeTypes::Target; } return types; } float ratePotion (const MWWorld::Ptr &item, const MWWorld::Ptr& actor) { if (item.getTypeName() != typeid(ESM::Potion).name()) return 0.f; const ESM::Potion* potion = item.get()->mBase; return rateEffects(potion->mEffects, actor, MWWorld::Ptr()); } 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; if (spell->mData.mType != ESM::Spell::ST_Spell) return 0.f; // Don't make use of racial bonus spells, like MW. Can be made optional later if (actor.getClass().isNpc()) { std::string raceid = actor.get()->mBase->mRace; const ESM::Race* race = MWBase::Environment::get().getWorld()->getStore().get().find(raceid); if (race->mPowers.exists(spell->mId)) return 0.f; } // 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)) return 0.f; if ( ((types & Touch) || (types & Target)) && enemy.getClass().getCreatureStats(enemy).getActiveSpells().isSpellActive(spell->mId)) return 0.f; return rateEffects(spell->mEffects, actor, enemy) * (successChance / 100.f); } float rateMagicItem(const MWWorld::Ptr &ptr, const MWWorld::Ptr &actor, const MWWorld::Ptr& enemy) { if (ptr.getClass().getEnchantment(ptr).empty()) return 0.f; const ESM::Enchantment* enchantment = MWBase::Environment::get().getWorld()->getStore().get().find(ptr.getClass().getEnchantment(ptr)); // Spells don't stack, so early out if the spell is still active on the target int types = getRangeTypes(enchantment->mEffects); if ((types & Self) && actor.getClass().getCreatureStats(actor).getActiveSpells().isSpellActive(ptr.getCellRef().getRefId())) return 0.f; if (types & (Touch|Target) && getSpellDuration(enemy, ptr.getCellRef().getRefId()) > 3) return 0.f; if (enchantment->mData.mType == ESM::Enchantment::CastOnce) { return rateEffects(enchantment->mEffects, actor, enemy); } else if (enchantment->mData.mType == ESM::Enchantment::WhenUsed) { MWWorld::InventoryStore& store = actor.getClass().getInventoryStore(actor); // Creatures can not wear armor/clothing, so allow creatures to use non-equipped items, if (actor.getClass().isNpc() && !store.isEquipped(ptr)) return 0.f; int castCost = getEffectiveEnchantmentCastCost(static_cast(enchantment->mData.mCost), actor); if (ptr.getCellRef().getEnchantmentCharge() != -1 && ptr.getCellRef().getEnchantmentCharge() < castCost) return 0.f; float rating = rateEffects(enchantment->mEffects, actor, enemy); rating *= 1.25f; // prefer rechargable magic items over spells return rating; } return 0.f; } float rateEffect(const ESM::ENAMstruct &effect, const MWWorld::Ptr &actor, const MWWorld::Ptr &enemy) { // NOTE: enemy may be empty float rating = 1; switch (effect.mEffectID) { case ESM::MagicEffect::Soultrap: case ESM::MagicEffect::AlmsiviIntervention: case ESM::MagicEffect::DivineIntervention: case ESM::MagicEffect::CalmHumanoid: case ESM::MagicEffect::CalmCreature: case ESM::MagicEffect::FrenzyHumanoid: case ESM::MagicEffect::FrenzyCreature: case ESM::MagicEffect::DemoralizeHumanoid: case ESM::MagicEffect::DemoralizeCreature: case ESM::MagicEffect::RallyHumanoid: case ESM::MagicEffect::RallyCreature: case ESM::MagicEffect::Charm: case ESM::MagicEffect::DetectAnimal: case ESM::MagicEffect::DetectEnchantment: case ESM::MagicEffect::DetectKey: case ESM::MagicEffect::Telekinesis: case ESM::MagicEffect::Mark: case ESM::MagicEffect::Recall: case ESM::MagicEffect::Jump: case ESM::MagicEffect::WaterBreathing: case ESM::MagicEffect::SwiftSwim: case ESM::MagicEffect::WaterWalking: case ESM::MagicEffect::SlowFall: case ESM::MagicEffect::Light: case ESM::MagicEffect::Lock: case ESM::MagicEffect::Open: case ESM::MagicEffect::TurnUndead: case ESM::MagicEffect::WeaknessToCommonDisease: case ESM::MagicEffect::WeaknessToBlightDisease: case ESM::MagicEffect::WeaknessToCorprusDisease: case ESM::MagicEffect::CureCommonDisease: case ESM::MagicEffect::CureBlightDisease: case ESM::MagicEffect::CureCorprusDisease: case ESM::MagicEffect::ResistBlightDisease: case ESM::MagicEffect::ResistCommonDisease: case ESM::MagicEffect::ResistCorprusDisease: case ESM::MagicEffect::Invisibility: case ESM::MagicEffect::Chameleon: case ESM::MagicEffect::NightEye: case ESM::MagicEffect::Vampirism: case ESM::MagicEffect::StuntedMagicka: case ESM::MagicEffect::ExtraSpell: case ESM::MagicEffect::RemoveCurse: case ESM::MagicEffect::CommandCreature: case ESM::MagicEffect::CommandHumanoid: return 0.f; case ESM::MagicEffect::Blind: { if (enemy.isEmpty()) return 0.f; const CreatureStats& stats = enemy.getClass().getCreatureStats(enemy); // Enemy can't attack if (stats.isParalyzed() || stats.getKnockedDown()) return 0.f; // Enemy doesn't attack if (stats.getDrawState() != MWMechanics::DrawState_Weapon) return 0.f; break; } case ESM::MagicEffect::Sound: { if (enemy.isEmpty()) return 0.f; const CreatureStats& stats = enemy.getClass().getCreatureStats(enemy); // Enemy can't cast spells if (stats.getMagicEffects().get(ESM::MagicEffect::Silence).getMagnitude() > 0) return 0.f; if (stats.isParalyzed() || stats.getKnockedDown()) return 0.f; // Enemy doesn't cast spells if (stats.getDrawState() != MWMechanics::DrawState_Spell) return 0.f; break; } case ESM::MagicEffect::Silence: { if (enemy.isEmpty()) return 0.f; const CreatureStats& stats = enemy.getClass().getCreatureStats(enemy); // Enemy can't cast spells if (stats.isParalyzed() || stats.getKnockedDown()) return 0.f; // Enemy doesn't cast spells if (stats.getDrawState() != MWMechanics::DrawState_Spell) return 0.f; break; } case ESM::MagicEffect::RestoreAttribute: return 0.f; // TODO: implement based on attribute damage case ESM::MagicEffect::RestoreSkill: return 0.f; // TODO: implement based on skill damage case ESM::MagicEffect::ResistFire: case ESM::MagicEffect::ResistFrost: case ESM::MagicEffect::ResistMagicka: case ESM::MagicEffect::ResistNormalWeapons: case ESM::MagicEffect::ResistParalysis: case ESM::MagicEffect::ResistPoison: case ESM::MagicEffect::ResistShock: case ESM::MagicEffect::SpellAbsorption: case ESM::MagicEffect::Reflect: return 0.f; // probably useless since we don't know in advance what the enemy will cast // don't cast these for now as they would make the NPC cast the same effect over and over again, especially when they have potions case ESM::MagicEffect::FortifyAttribute: case ESM::MagicEffect::FortifyHealth: case ESM::MagicEffect::FortifyMagicka: case ESM::MagicEffect::FortifyFatigue: case ESM::MagicEffect::FortifySkill: case ESM::MagicEffect::FortifyMaximumMagicka: case ESM::MagicEffect::FortifyAttack: return 0.f; case ESM::MagicEffect::Burden: { if (enemy.isEmpty()) return 0.f; // Ignore enemy without inventory if (!enemy.getClass().hasInventoryStore(enemy)) return 0.f; // burden makes sense only to overburden an enemy float burden = enemy.getClass().getEncumbrance(enemy) - enemy.getClass().getCapacity(enemy); if (burden > 0) return 0.f; if ((effect.mMagnMin + effect.mMagnMax)/2.f > -burden) rating *= 3; else return 0.f; break; } case ESM::MagicEffect::Feather: { // Ignore actors without inventory if (!actor.getClass().hasInventoryStore(actor)) return 0.f; // feather makes sense only for overburden actors float burden = actor.getClass().getEncumbrance(actor) - actor.getClass().getCapacity(actor); if (burden <= 0) return 0.f; if ((effect.mMagnMin + effect.mMagnMax)/2.f >= burden) rating *= 3; else return 0.f; break; } case ESM::MagicEffect::Levitate: return 0.f; // AI isn't designed to take advantage of this, and could be perceived as unfair anyway case ESM::MagicEffect::BoundBoots: case ESM::MagicEffect::BoundHelm: if (actor.getClass().isNpc()) { // Beast races can't wear helmets or boots std::string raceid = actor.get()->mBase->mRace; const ESM::Race* race = MWBase::Environment::get().getWorld()->getStore().get().find(raceid); if (race->mData.mFlags & ESM::Race::Beast) return 0.f; } else return 0.f; break; // Creatures can not wear armor case ESM::MagicEffect::BoundCuirass: case ESM::MagicEffect::BoundGloves: if (!actor.getClass().isNpc()) return 0.f; break; case ESM::MagicEffect::BoundLongbow: // AI should not summon the bow if there is no suitable ammo. if (rateAmmo(actor, enemy, getWeaponType(ESM::Weapon::MarksmanBow)->mAmmoType) <= 0.f) return 0.f; break; case ESM::MagicEffect::RestoreHealth: case ESM::MagicEffect::RestoreMagicka: case ESM::MagicEffect::RestoreFatigue: if (effect.mRange == ESM::RT_Self) { int priority = 1; if (effect.mEffectID == ESM::MagicEffect::RestoreHealth) priority = 10; const MWMechanics::CreatureStats& stats = actor.getClass().getCreatureStats(actor); const DynamicStat& current = stats.getDynamic(effect.mEffectID - ESM::MagicEffect::RestoreHealth); // NB: this currently assumes the hardcoded magic effect flags are used const float magnitude = (effect.mMagnMin + effect.mMagnMax)/2.f; const float toHeal = magnitude * std::max(1, effect.mDuration); // Effect doesn't heal more than we need, *or* we are below 1/2 health if (current.getModified() - current.getCurrent() > toHeal || current.getCurrent() < current.getModified()*0.5) { return 10000.f * priority - (toHeal - (current.getModified()-current.getCurrent())); // prefer the most fitting potion } else return -10000.f * priority; // Save for later } break; case ESM::MagicEffect::Dispel: { int numPositive = 0; int numNegative = 0; int diff = 0; if (effect.mRange == ESM::RT_Self) { numPositive = numEffectsToDispel(actor, -1, false); numNegative = numEffectsToDispel(actor); diff = numNegative - numPositive; } else { if (enemy.isEmpty()) return 0.f; numPositive = numEffectsToDispel(enemy, -1, false); numNegative = numEffectsToDispel(enemy); diff = numPositive - numNegative; // if rating < 0 here, the spell will be considered as negative later rating *= -1; } if (diff <= 0) return 0.f; rating *= (diff) / 5.f; break; } // Prefer Cure effects over Dispel, because Dispel also removes positive effects case ESM::MagicEffect::CureParalyzation: return 1001.f * numEffectsToDispel(actor, ESM::MagicEffect::Paralyze); case ESM::MagicEffect::CurePoison: return 1001.f * numEffectsToDispel(actor, ESM::MagicEffect::Poison); case ESM::MagicEffect::DisintegrateArmor: { if (enemy.isEmpty()) return 0.f; // Ignore enemy without inventory if (!enemy.getClass().hasInventoryStore(enemy)) return 0.f; MWWorld::InventoryStore& inv = enemy.getClass().getInventoryStore(enemy); // According to UESP static const int armorSlots[] = { MWWorld::InventoryStore::Slot_CarriedLeft, MWWorld::InventoryStore::Slot_Cuirass, MWWorld::InventoryStore::Slot_LeftPauldron, MWWorld::InventoryStore::Slot_RightPauldron, MWWorld::InventoryStore::Slot_LeftGauntlet, MWWorld::InventoryStore::Slot_RightGauntlet, MWWorld::InventoryStore::Slot_Helmet, MWWorld::InventoryStore::Slot_Greaves, MWWorld::InventoryStore::Slot_Boots }; bool enemyHasArmor = false; // Ignore enemy without armor for (unsigned int i=0; i= 0 && effect.mAttribute < ESM::Attribute::Length) { const float attributePriorities[ESM::Attribute::Length] = { 1.0f, // Strength 0.5f, // Intelligence 0.6f, // Willpower 0.7f, // Agility 0.5f, // Speed 0.8f, // Endurance 0.7f, // Personality 0.3f // Luck }; rating *= attributePriorities[effect.mAttribute]; } } break; case ESM::MagicEffect::DamageSkill: case ESM::MagicEffect::DrainSkill: if (enemy.isEmpty() || !enemy.getClass().isNpc()) return 0.f; if (enemy.getClass().getSkill(enemy, effect.mSkill) <= 0) return 0.f; break; default: break; } // Allow only one summoned creature at time if (isSummoningEffect(effect.mEffectID)) { MWMechanics::CreatureStats& creatureStats = actor.getClass().getCreatureStats(actor); if (!creatureStats.getSummonedCreatureMap().empty()) return 0.f; } // Underwater casting not possible if (effect.mRange == ESM::RT_Target) { if (MWBase::Environment::get().getWorld()->isUnderwater(MWWorld::ConstPtr(actor), 0.75f)) return 0.f; if (enemy.isEmpty()) return 0.f; if (MWBase::Environment::get().getWorld()->isUnderwater(MWWorld::ConstPtr(enemy), 0.75f)) return 0.f; } const ESM::MagicEffect* magicEffect = MWBase::Environment::get().getWorld()->getStore().get().find(effect.mEffectID); if (magicEffect->mData.mFlags & ESM::MagicEffect::Harmful) { rating *= -1.f; if (enemy.isEmpty()) return 0.f; // Check resistance for harmful effects CreatureStats& stats = enemy.getClass().getCreatureStats(enemy); float resistance = MWMechanics::getEffectResistanceAttribute(effect.mEffectID, &stats.getMagicEffects()); rating *= (1.f - std::min(resistance, 100.f) / 100.f); } // for harmful no-magnitude effects (e.g. silence) check if enemy is already has them // for non-harmful no-magnitude effects (e.g. bound items) check if actor is already has them if (magicEffect->mData.mFlags & ESM::MagicEffect::NoMagnitude) { if (magicEffect->mData.mFlags & ESM::MagicEffect::Harmful) { CreatureStats& stats = enemy.getClass().getCreatureStats(enemy); if (stats.getMagicEffects().get(effect.mEffectID).getMagnitude() > 0) return 0.f; } else { CreatureStats& stats = actor.getClass().getCreatureStats(actor); if (stats.getMagicEffects().get(effect.mEffectID).getMagnitude() > 0) return 0.f; } } rating *= calcEffectCost(effect, magicEffect); // Currently treating all "on target" or "on touch" effects to target the enemy actor. // Combat AI is egoistic, so doesn't consider applying positive effects to friendly actors. if (effect.mRange != ESM::RT_Self) rating *= -1.f; return rating; } float rateEffects(const ESM::EffectList &list, const MWWorld::Ptr& actor, const MWWorld::Ptr& enemy) { // NOTE: enemy may be empty float rating = 0.f; float ratingMult = 1.f; // NB: this multiplier is applied to the effect rating, not the final rating const MWWorld::Store& gmst = MWBase::Environment::get().getWorld()->getStore().get(); static const float fAIMagicSpellMult = gmst.find("fAIMagicSpellMult")->mValue.getFloat(); static const float fAIRangeMagicSpellMult = gmst.find("fAIRangeMagicSpellMult")->mValue.getFloat(); for (std::vector::const_iterator it = list.mList.begin(); it != list.mList.end(); ++it) { ratingMult = (it->mRange == ESM::RT_Target) ? fAIRangeMagicSpellMult : fAIMagicSpellMult; rating += rateEffect(*it, actor, enemy) * ratingMult; } return rating; } float vanillaRateSpell(const ESM::Spell* spell, const MWWorld::Ptr& actor, const MWWorld::Ptr& enemy) { const MWWorld::Store& gmst = MWBase::Environment::get().getWorld()->getStore().get(); static const float fAIMagicSpellMult = gmst.find("fAIMagicSpellMult")->mValue.getFloat(); static const float fAIRangeMagicSpellMult = gmst.find("fAIRangeMagicSpellMult")->mValue.getFloat(); float mult = fAIMagicSpellMult; for (std::vector::const_iterator effectIt = spell->mEffects.mList.begin(); effectIt != spell->mEffects.mList.end(); ++effectIt) { if (effectIt->mRange == ESM::RT_Target) { if (!MWBase::Environment::get().getWorld()->isSwimming(enemy)) mult = fAIRangeMagicSpellMult; else mult = 0.0f; break; } } return MWMechanics::getSpellSuccessChance(spell, actor) * mult; } } ================================================ FILE: apps/openmw/mwmechanics/spellpriority.hpp ================================================ #ifndef OPENMW_SPELL_PRIORITY_H #define OPENMW_SPELL_PRIORITY_H namespace ESM { struct Spell; struct EffectList; struct ENAMstruct; } namespace MWWorld { class Ptr; } namespace MWMechanics { // RangeTypes using bitflags to allow multiple range types, as can be the case with spells having multiple effects. enum RangeTypes { Self = 0x1, Touch = 0x10, Target = 0x100 }; int getRangeTypes (const ESM::EffectList& effects); float rateSpell (const ESM::Spell* spell, const MWWorld::Ptr& actor, const MWWorld::Ptr& enemy); float rateMagicItem (const MWWorld::Ptr& ptr, const MWWorld::Ptr& actor, const MWWorld::Ptr& enemy); float ratePotion (const MWWorld::Ptr& item, const MWWorld::Ptr &actor); /// @note target may be empty float rateEffect (const ESM::ENAMstruct& effect, const MWWorld::Ptr& actor, const MWWorld::Ptr& enemy); /// @note target may be empty float rateEffects (const ESM::EffectList& list, const MWWorld::Ptr& actor, const MWWorld::Ptr& enemy); float vanillaRateSpell(const ESM::Spell* spell, const MWWorld::Ptr& actor, const MWWorld::Ptr& enemy); } #endif ================================================ FILE: apps/openmw/mwmechanics/spellresistance.cpp ================================================ #include "spellresistance.hpp" #include #include "../mwbase/environment.hpp" #include "../mwbase/world.hpp" #include "../mwworld/class.hpp" #include "../mwworld/esmstore.hpp" #include "creaturestats.hpp" #include "spellutil.hpp" namespace MWMechanics { float getEffectMultiplier(short effectId, const MWWorld::Ptr& actor, const MWWorld::Ptr& caster, const ESM::Spell* spell, const MagicEffects* effects) { if (!actor.getClass().isActor()) return 1; float resistance = getEffectResistance(effectId, actor, caster, spell, effects); return 1 - resistance / 100.f; } float getEffectResistance (short effectId, const MWWorld::Ptr& actor, const MWWorld::Ptr& caster, const ESM::Spell* spell, const MagicEffects* effects) { // Effects with no resistance attribute belonging to them can not be resisted if (ESM::MagicEffect::getResistanceEffect(effectId) == -1) return 0.f; const auto magicEffect = MWBase::Environment::get().getWorld()->getStore().get().find(effectId); const MWMechanics::CreatureStats& stats = actor.getClass().getCreatureStats(actor); const MWMechanics::MagicEffects* magicEffects = &stats.getMagicEffects(); if (effects) magicEffects = effects; float resistance = getEffectResistanceAttribute(effectId, magicEffects); float willpower = stats.getAttribute(ESM::Attribute::Willpower).getModified(); float luck = stats.getAttribute(ESM::Attribute::Luck).getModified(); float x = (willpower + 0.1f * luck) * stats.getFatigueTerm(); // This makes spells that are easy to cast harder to resist and vice versa float castChance = 100.f; if (spell != nullptr && !caster.isEmpty() && caster.getClass().isActor()) castChance = getSpellSuccessChance(spell, caster, nullptr, false, false); // Uncapped casting chance if (castChance > 0) x *= 50 / castChance; float roll = Misc::Rng::rollClosedProbability() * 100; if (magicEffect->mData.mFlags & ESM::MagicEffect::NoMagnitude) roll -= resistance; if (x <= roll) x = 0; else { if (magicEffect->mData.mFlags & ESM::MagicEffect::NoMagnitude) x = 100; else x = roll / std::min(x, 100.f); } x = std::min(x + resistance, 100.f); return x; } float getEffectResistanceAttribute (short effectId, const MagicEffects* actorEffects) { short resistanceEffect = ESM::MagicEffect::getResistanceEffect(effectId); short weaknessEffect = ESM::MagicEffect::getWeaknessEffect(effectId); float resistance = 0; if (resistanceEffect != -1) resistance += actorEffects->get(resistanceEffect).getMagnitude(); if (weaknessEffect != -1) resistance -= actorEffects->get(weaknessEffect).getMagnitude(); if (effectId == ESM::MagicEffect::FireDamage) resistance += actorEffects->get(ESM::MagicEffect::FireShield).getMagnitude(); if (effectId == ESM::MagicEffect::ShockDamage) resistance += actorEffects->get(ESM::MagicEffect::LightningShield).getMagnitude(); if (effectId == ESM::MagicEffect::FrostDamage) resistance += actorEffects->get(ESM::MagicEffect::FrostShield).getMagnitude(); return resistance; } } ================================================ FILE: apps/openmw/mwmechanics/spellresistance.hpp ================================================ #ifndef MWMECHANICS_SPELLRESISTANCE_H #define MWMECHANICS_SPELLRESISTANCE_H namespace ESM { struct Spell; } namespace MWWorld { class Ptr; } namespace MWMechanics { class MagicEffects; /// Get an effect multiplier for applying an effect cast by the given actor in the given spell (optional). /// @return effect multiplier from 0 to 2. (100% net resistance to 100% net weakness) /// @param effects Override the actor's current magicEffects. Useful if there are effects currently /// being applied (but not applied yet) that should also be considered. float getEffectMultiplier(short effectId, const MWWorld::Ptr& actor, const MWWorld::Ptr& caster, const ESM::Spell* spell = nullptr, const MagicEffects* effects = nullptr); /// Get the effective resistance against an effect casted by the given actor in the given spell (optional). /// @return >=100 for fully resisted. can also return negative value for damage amplification. /// @param effects Override the actor's current magicEffects. Useful if there are effects currently /// being applied (but not applied yet) that should also be considered. float getEffectResistance (short effectId, const MWWorld::Ptr& actor, const MWWorld::Ptr& caster, const ESM::Spell* spell = nullptr, const MagicEffects* effects = nullptr); /// Get the resistance attribute against an effect for a given actor. This will add together /// ResistX and Weakness to X effects relevant against the given effect. float getEffectResistanceAttribute (short effectId, const MagicEffects* actorEffects); } #endif ================================================ FILE: apps/openmw/mwmechanics/spells.cpp ================================================ #include "spells.hpp" #include #include #include #include #include /* Start of tes3mp addition Include additional headers for multiplayer purposes */ #include "../mwmp/Main.hpp" #include "../mwmp/LocalPlayer.hpp" /* End of tes3mp addition */ #include "../mwbase/environment.hpp" #include "../mwbase/world.hpp" #include "../mwworld/class.hpp" #include "../mwworld/esmstore.hpp" #include "actorutil.hpp" #include "creaturestats.hpp" #include "magiceffects.hpp" #include "stat.hpp" namespace MWMechanics { Spells::Spells() : mSpellsChanged(false) { } Spells::Spells(const Spells& spells) : mSpellList(spells.mSpellList), mSpells(spells.mSpells), mSelectedSpell(spells.mSelectedSpell), mUsedPowers(spells.mUsedPowers), mSpellsChanged(spells.mSpellsChanged), mEffects(spells.mEffects), mSourcedEffects(spells.mSourcedEffects) { if(mSpellList) mSpellList->addListener(this); } Spells::Spells(Spells&& spells) : mSpellList(std::move(spells.mSpellList)), mSpells(std::move(spells.mSpells)), mSelectedSpell(std::move(spells.mSelectedSpell)), mUsedPowers(std::move(spells.mUsedPowers)), mSpellsChanged(std::move(spells.mSpellsChanged)), mEffects(std::move(spells.mEffects)), mSourcedEffects(std::move(spells.mSourcedEffects)) { if (mSpellList) mSpellList->updateListener(&spells, this); } std::map::const_iterator Spells::begin() const { return mSpells.begin(); } std::map::const_iterator Spells::end() const { return mSpells.end(); } void Spells::rebuildEffects() const { mEffects = MagicEffects(); mSourcedEffects.clear(); for (const auto& iter : mSpells) { const ESM::Spell *spell = iter.first; if (spell->mData.mType==ESM::Spell::ST_Ability || spell->mData.mType==ESM::Spell::ST_Blight || spell->mData.mType==ESM::Spell::ST_Disease || spell->mData.mType==ESM::Spell::ST_Curse) { int i=0; for (const auto& effect : spell->mEffects.mList) { if (iter.second.mPurgedEffects.find(i) != iter.second.mPurgedEffects.end()) { ++i; continue; // effect was purged } float random = 1.f; if (iter.second.mEffectRands.find(i) != iter.second.mEffectRands.end()) random = iter.second.mEffectRands.at(i); float magnitude = effect.mMagnMin + (effect.mMagnMax - effect.mMagnMin) * random; mEffects.add (effect, magnitude); mSourcedEffects[spell].add(MWMechanics::EffectKey(effect), magnitude); ++i; } } } } bool Spells::hasSpell(const std::string &spell) const { return hasSpell(SpellList::getSpell(spell)); } bool Spells::hasSpell(const ESM::Spell *spell) const { return mSpells.find(spell) != mSpells.end(); } void Spells::add (const ESM::Spell* spell) { mSpellList->add(spell); } void Spells::add (const std::string& spellId) { add(SpellList::getSpell(spellId)); } void Spells::addSpell(const ESM::Spell* spell) { if (mSpells.find (spell)==mSpells.end()) { std::map random; // Determine the random magnitudes (unless this is a castable spell, in which case // they will be determined when the spell is cast) if (spell->mData.mType != ESM::Spell::ST_Power && spell->mData.mType != ESM::Spell::ST_Spell) { for (unsigned int i=0; imEffects.mList.size();++i) { if (spell->mEffects.mList[i].mMagnMin != spell->mEffects.mList[i].mMagnMax) { int delta = spell->mEffects.mList[i].mMagnMax - spell->mEffects.mList[i].mMagnMin; random[i] = Misc::Rng::rollDice(delta + 1) / static_cast(delta); } } } SpellParams params; params.mEffectRands = random; mSpells.emplace(spell, params); mSpellsChanged = true; } } void Spells::remove (const std::string& spellId) { const auto spell = SpellList::getSpell(spellId); removeSpell(spell); mSpellList->remove(spell); if (spellId==mSelectedSpell) mSelectedSpell.clear(); } void Spells::removeSpell(const ESM::Spell* spell) { const auto it = mSpells.find(spell); if(it != mSpells.end()) { mSpells.erase(it); mSpellsChanged = true; } } MagicEffects Spells::getMagicEffects() const { if (mSpellsChanged) { rebuildEffects(); mSpellsChanged = false; } return mEffects; } void Spells::removeAllSpells() { mSpells.clear(); mSpellsChanged = true; } void Spells::clear(bool modifyBase) { removeAllSpells(); if(modifyBase) mSpellList->clear(); } void Spells::setSelectedSpell (const std::string& spellId) { mSelectedSpell = spellId; } const std::string Spells::getSelectedSpell() const { return mSelectedSpell; } bool Spells::isSpellActive(const std::string &id) const { if (id.empty()) return false; const ESM::Spell* spell = MWBase::Environment::get().getWorld()->getStore().get().search(id); if (spell && hasSpell(spell)) { auto type = spell->mData.mType; return (type==ESM::Spell::ST_Ability || type==ESM::Spell::ST_Blight || type==ESM::Spell::ST_Disease || type==ESM::Spell::ST_Curse); } return false; } bool Spells::hasDisease(const ESM::Spell::SpellType type) const { for (const auto& iter : mSpells) { const ESM::Spell *spell = iter.first; if (spell->mData.mType == type) return true; } return false; } bool Spells::hasCommonDisease() const { return hasDisease(ESM::Spell::ST_Disease); } bool Spells::hasBlightDisease() const { return hasDisease(ESM::Spell::ST_Blight); } void Spells::purge(const SpellFilter& filter) { std::vector purged; for (auto iter = mSpells.begin(); iter!=mSpells.end();) { const ESM::Spell *spell = iter->first; if (filter(spell)) { /* Start of tes3mp addition Send an ID_PLAYER_SPELLBOOK packet every time a spell is purged here */ mwmp::Main::get().getLocalPlayer()->sendSpellChange(spell->mId, mwmp::SpellbookChanges::REMOVE); /* End of tes3mp addition */ mSpells.erase(iter++); purged.push_back(spell->mId); mSpellsChanged = true; } else ++iter; } if(!purged.empty()) mSpellList->removeAll(purged); } void Spells::purgeCommonDisease() { purge([](auto spell) { return spell->mData.mType == ESM::Spell::ST_Disease; }); } void Spells::purgeBlightDisease() { purge([](auto spell) { return spell->mData.mType == ESM::Spell::ST_Blight && !hasCorprusEffect(spell); }); } void Spells::purgeCorprusDisease() { purge(&hasCorprusEffect); } void Spells::purgeCurses() { purge([](auto spell) { return spell->mData.mType == ESM::Spell::ST_Curse; }); } void Spells::removeEffects(const std::string &id) { if (isSpellActive(id)) { for (auto& spell : mSpells) { if (spell.first == SpellList::getSpell(id)) { for (long unsigned int i = 0; i != spell.first->mEffects.mList.size(); i++) { spell.second.mPurgedEffects.insert(i); } } } mSpellsChanged = true; } } void Spells::visitEffectSources(EffectSourceVisitor &visitor) const { if (mSpellsChanged) { rebuildEffects(); mSpellsChanged = false; } for (const auto& it : mSourcedEffects) { const ESM::Spell * spell = it.first; for (const auto& effectIt : it.second) { // FIXME: since Spells merges effects with the same ID, there is no sense to use multiple effects with same ID here visitor.visit(effectIt.first, -1, spell->mName, spell->mId, -1, effectIt.second.getMagnitude()); } } } bool Spells::hasCorprusEffect(const ESM::Spell *spell) { for (const auto& effectIt : spell->mEffects.mList) { if (effectIt.mEffectID == ESM::MagicEffect::Corprus) { return true; } } return false; } void Spells::purgeEffect(int effectId) { for (auto& spellIt : mSpells) { int i = 0; for (auto& effectIt : spellIt.first->mEffects.mList) { if (effectIt.mEffectID == effectId) { spellIt.second.mPurgedEffects.insert(i); mSpellsChanged = true; } ++i; } } } void Spells::purgeEffect(int effectId, const std::string & sourceId) { // Effect source may be not a spell const ESM::Spell * spell = MWBase::Environment::get().getWorld()->getStore().get().search(sourceId); if (spell == nullptr) return; auto spellIt = mSpells.find(spell); if (spellIt == mSpells.end()) return; int index = 0; for (auto& effectIt : spellIt->first->mEffects.mList) { if (effectIt.mEffectID == effectId) { spellIt->second.mPurgedEffects.insert(index); mSpellsChanged = true; } ++index; } } bool Spells::canUsePower(const ESM::Spell* spell) const { const auto it = mUsedPowers.find(spell); return it == mUsedPowers.end() || it->second + 24 <= MWBase::Environment::get().getWorld()->getTimeStamp(); } void Spells::usePower(const ESM::Spell* spell) { mUsedPowers[spell] = MWBase::Environment::get().getWorld()->getTimeStamp(); /* Start of tes3mp addition Send an ID_PLAYER_COOLDOWN packet every time a cooldown is recorded here */ mwmp::Main::get().getLocalPlayer()->sendCooldownChange(spell->mId, MWBase::Environment::get().getWorld()->getTimeStamp().getDay(), MWBase::Environment::get().getWorld()->getTimeStamp().getHour()); /* End of tes3mp addition */ } /* Start of tes3mp addition Make it possible to set timestamps for power cooldowns, necessary for ID_PLAYER_COOLDOWNS packets */ void Spells::setPowerUseTimestamp(const ESM::Spell* spell, int startDay, float startHour) { ESM::TimeStamp timestamp; timestamp.mDay = startDay; timestamp.mHour = startHour; mUsedPowers[spell] = MWWorld::TimeStamp(timestamp); } /* End of tes3mp addition */ void Spells::readState(const ESM::SpellState &state, CreatureStats* creatureStats) { const auto& baseSpells = mSpellList->getSpells(); for (ESM::SpellState::TContainer::const_iterator it = state.mSpells.begin(); it != state.mSpells.end(); ++it) { // Discard spells that are no longer available due to changed content files const ESM::Spell* spell = MWBase::Environment::get().getWorld()->getStore().get().search(it->first); if (spell) { mSpells[spell].mEffectRands = it->second.mEffectRands; mSpells[spell].mPurgedEffects = it->second.mPurgedEffects; if (it->first == state.mSelectedSpell) mSelectedSpell = it->first; } } // Add spells from the base record for(const std::string& id : baseSpells) { const ESM::Spell* spell = MWBase::Environment::get().getWorld()->getStore().get().search(id); if(spell) addSpell(spell); } for (std::map::const_iterator it = state.mUsedPowers.begin(); it != state.mUsedPowers.end(); ++it) { const ESM::Spell* spell = MWBase::Environment::get().getWorld()->getStore().get().search(it->first); if (!spell) continue; mUsedPowers[spell] = MWWorld::TimeStamp(it->second); } for (std::map::const_iterator it = state.mCorprusSpells.begin(); it != state.mCorprusSpells.end(); ++it) { const ESM::Spell * spell = MWBase::Environment::get().getWorld()->getStore().get().search(it->first); if (!spell) continue; CorprusStats stats; int worsening = state.mCorprusSpells.at(it->first).mWorsenings; for (int i=0; imEffects.mList) { if (effect.mEffectID == ESM::MagicEffect::DrainAttribute) stats.mWorsenings[effect.mAttribute] = worsening; } stats.mNextWorsening = MWWorld::TimeStamp(state.mCorprusSpells.at(it->first).mNextWorsening); creatureStats->addCorprusSpell(it->first, stats); } mSpellsChanged = true; // Permanent effects are used only to keep the custom magnitude of corprus spells effects (after cure too), and only in old saves. Convert data to the new approach. for (std::map >::const_iterator it = state.mPermanentSpellEffects.begin(); it != state.mPermanentSpellEffects.end(); ++it) { const ESM::Spell * spell = MWBase::Environment::get().getWorld()->getStore().get().search(it->first); if (!spell) continue; // Import data only for player, other actors should not suffer from corprus worsening. MWWorld::Ptr player = getPlayer(); if (creatureStats->getActorId() != player.getClass().getCreatureStats(player).getActorId()) return; // Note: if target actor has the Restore attirbute effects, stats will be restored. for (std::vector::const_iterator effectIt = it->second.begin(); effectIt != it->second.end(); ++effectIt) { // Applied corprus effects are already in loaded stats modifiers if (effectIt->mId == ESM::MagicEffect::FortifyAttribute) { AttributeValue attr = creatureStats->getAttribute(effectIt->mArg); attr.setModifier(attr.getModifier() - effectIt->mMagnitude); attr.damage(-effectIt->mMagnitude); creatureStats->setAttribute(effectIt->mArg, attr); } else if (effectIt->mId == ESM::MagicEffect::DrainAttribute) { AttributeValue attr = creatureStats->getAttribute(effectIt->mArg); attr.setModifier(attr.getModifier() + effectIt->mMagnitude); attr.damage(effectIt->mMagnitude); creatureStats->setAttribute(effectIt->mArg, attr); } } } } void Spells::writeState(ESM::SpellState &state) const { const auto& baseSpells = mSpellList->getSpells(); for (const auto& it : mSpells) { // Don't save spells and powers stored in the base record if((it.first->mData.mType != ESM::Spell::ST_Spell && it.first->mData.mType != ESM::Spell::ST_Power) || std::find(baseSpells.begin(), baseSpells.end(), it.first->mId) == baseSpells.end()) { ESM::SpellState::SpellParams params; params.mEffectRands = it.second.mEffectRands; params.mPurgedEffects = it.second.mPurgedEffects; state.mSpells.emplace(it.first->mId, params); } } state.mSelectedSpell = mSelectedSpell; for (const auto& it : mUsedPowers) state.mUsedPowers[it.first->mId] = it.second.toEsm(); } bool Spells::setSpells(const std::string& actorId) { bool result; std::tie(mSpellList, result) = MWBase::Environment::get().getWorld()->getStore().getSpellList(actorId); mSpellList->addListener(this); addAllToInstance(mSpellList->getSpells()); return result; } void Spells::addAllToInstance(const std::vector& spells) { for(const std::string& id : spells) { const ESM::Spell* spell = MWBase::Environment::get().getWorld()->getStore().get().search(id); if(spell) addSpell(spell); else Log(Debug::Warning) << "Warning: ignoring nonexistent spell '" << id << "'"; } } Spells::~Spells() { if(mSpellList) mSpellList->removeListener(this); } } ================================================ FILE: apps/openmw/mwmechanics/spells.hpp ================================================ #ifndef GAME_MWMECHANICS_SPELLS_H #define GAME_MWMECHANICS_SPELLS_H #include #include #include #include #include "../mwworld/timestamp.hpp" #include "magiceffects.hpp" #include "spelllist.hpp" namespace ESM { struct SpellState; } namespace MWMechanics { class CreatureStats; class MagicEffects; /// \brief Spell list /// /// This class manages known spells as well as abilities, powers and permanent negative effects like /// diseases. It also keeps track of used powers (which can only be used every 24h). class Spells { std::shared_ptr mSpellList; std::map mSpells; // Note: this is the spell that's about to be cast, *not* the spell selected in the GUI (which may be different) std::string mSelectedSpell; std::map mUsedPowers; mutable bool mSpellsChanged; mutable MagicEffects mEffects; mutable std::map mSourcedEffects; void rebuildEffects() const; bool hasDisease(const ESM::Spell::SpellType type) const; using SpellFilter = bool (*)(const ESM::Spell*); void purge(const SpellFilter& filter); void addSpell(const ESM::Spell* spell); void removeSpell(const ESM::Spell* spell); void removeAllSpells(); friend class SpellList; public: using TIterator = std::map::const_iterator; Spells(); Spells(const Spells&); Spells(Spells&& spells); ~Spells(); static bool hasCorprusEffect(const ESM::Spell *spell); void purgeEffect(int effectId); void purgeEffect(int effectId, const std::string & sourceId); bool canUsePower (const ESM::Spell* spell) const; void usePower (const ESM::Spell* spell); /* Start of tes3mp addition Make it possible to set timestamps for power cooldowns, necessary for ID_PLAYER_COOLDOWNS packets */ void setPowerUseTimestamp(const ESM::Spell* spell, int startDay, float startHour); /* End of tes3mp addition */ void purgeCommonDisease(); void purgeBlightDisease(); void purgeCorprusDisease(); void purgeCurses(); TIterator begin() const; TIterator end() const; bool hasSpell(const std::string& spell) const; bool hasSpell(const ESM::Spell* spell) const; void add (const std::string& spell); ///< Adding a spell that is already listed in *this is a no-op. void add (const ESM::Spell* spell); ///< Adding a spell that is already listed in *this is a no-op. void remove (const std::string& spell); ///< If the spell to be removed is the selected spell, the selected spell will be changed to /// no spell (empty string). MagicEffects getMagicEffects() const; ///< Return sum of magic effects resulting from abilities, blights, deseases and curses. void clear(bool modifyBase = false); ///< Remove all spells of al types. void setSelectedSpell (const std::string& spellId); ///< This function does not verify, if the spell is available. const std::string getSelectedSpell() const; ///< May return an empty string. bool isSpellActive(const std::string& id) const; ///< Are we under the effects of the given spell ID? bool hasCommonDisease() const; bool hasBlightDisease() const; void removeEffects(const std::string& id); void visitEffectSources (MWMechanics::EffectSourceVisitor& visitor) const; void readState (const ESM::SpellState& state, CreatureStats* creatureStats); void writeState (ESM::SpellState& state) const; bool setSpells(const std::string& id); void addAllToInstance(const std::vector& spells); }; } #endif ================================================ FILE: apps/openmw/mwmechanics/spellutil.cpp ================================================ #include "spellutil.hpp" #include #include "../mwbase/environment.hpp" #include "../mwbase/windowmanager.hpp" #include "../mwbase/world.hpp" #include "../mwworld/class.hpp" #include "../mwworld/esmstore.hpp" #include "actorutil.hpp" #include "creaturestats.hpp" namespace MWMechanics { ESM::Skill::SkillEnum spellSchoolToSkill(int school) { static const std::array schoolSkillArray { ESM::Skill::Alteration, ESM::Skill::Conjuration, ESM::Skill::Destruction, ESM::Skill::Illusion, ESM::Skill::Mysticism, ESM::Skill::Restoration }; return schoolSkillArray.at(school); } float calcEffectCost(const ESM::ENAMstruct& effect, const ESM::MagicEffect* magicEffect) { const MWWorld::ESMStore& store = MWBase::Environment::get().getWorld()->getStore(); if (!magicEffect) magicEffect = store.get().find(effect.mEffectID); bool hasMagnitude = !(magicEffect->mData.mFlags & ESM::MagicEffect::NoMagnitude); bool hasDuration = !(magicEffect->mData.mFlags & ESM::MagicEffect::NoDuration); bool appliedOnce = magicEffect->mData.mFlags & ESM::MagicEffect::AppliedOnce; int minMagn = hasMagnitude ? effect.mMagnMin : 1; int maxMagn = hasMagnitude ? effect.mMagnMax : 1; int duration = hasDuration ? effect.mDuration : 1; if (!appliedOnce) duration = std::max(1, duration); static const float fEffectCostMult = store.get().find("fEffectCostMult")->mValue.getFloat(); float x = 0.5 * (std::max(1, minMagn) + std::max(1, maxMagn)); x *= 0.1 * magicEffect->mData.mBaseCost; x *= 1 + duration; x += 0.05 * std::max(1, effect.mArea) * magicEffect->mData.mBaseCost; return x * fEffectCostMult; } int getEffectiveEnchantmentCastCost(float castCost, const MWWorld::Ptr &actor) { /* * Each point of enchant skill above/under 10 subtracts/adds * one percent of enchantment cost while minimum is 1. */ int eSkill = actor.getClass().getSkill(actor, ESM::Skill::Enchant); const float result = castCost - (castCost / 100) * (eSkill - 10); return static_cast((result < 1) ? 1 : result); } float calcSpellBaseSuccessChance (const ESM::Spell* spell, const MWWorld::Ptr& actor, int* effectiveSchool) { // Morrowind for some reason uses a formula slightly different from magicka cost calculation float y = std::numeric_limits::max(); float lowestSkill = 0; for (const ESM::ENAMstruct& effect : spell->mEffects.mList) { float x = static_cast(effect.mDuration); const auto magicEffect = MWBase::Environment::get().getWorld()->getStore().get().find(effect.mEffectID); if (!(magicEffect->mData.mFlags & ESM::MagicEffect::AppliedOnce)) x = std::max(1.f, x); x *= 0.1f * magicEffect->mData.mBaseCost; x *= 0.5f * (effect.mMagnMin + effect.mMagnMax); x += effect.mArea * 0.05f * magicEffect->mData.mBaseCost; if (effect.mRange == ESM::RT_Target) x *= 1.5f; static const float fEffectCostMult = MWBase::Environment::get().getWorld()->getStore().get().find( "fEffectCostMult")->mValue.getFloat(); x *= fEffectCostMult; float s = 2.0f * actor.getClass().getSkill(actor, spellSchoolToSkill(magicEffect->mData.mSchool)); if (s - x < y) { y = s - x; if (effectiveSchool) *effectiveSchool = magicEffect->mData.mSchool; lowestSkill = s; } } CreatureStats& stats = actor.getClass().getCreatureStats(actor); float actorWillpower = stats.getAttribute(ESM::Attribute::Willpower).getModified(); float actorLuck = stats.getAttribute(ESM::Attribute::Luck).getModified(); float castChance = (lowestSkill - spell->mData.mCost + 0.2f * actorWillpower + 0.1f * actorLuck); return castChance; } float getSpellSuccessChance (const ESM::Spell* spell, const MWWorld::Ptr& actor, int* effectiveSchool, bool cap, bool checkMagicka) { // NB: Base chance is calculated here because the effective school pointer must be filled float baseChance = calcSpellBaseSuccessChance(spell, actor, effectiveSchool); bool godmode = actor == getPlayer() && MWBase::Environment::get().getWorld()->getGodModeState(); CreatureStats& stats = actor.getClass().getCreatureStats(actor); if (stats.getMagicEffects().get(ESM::MagicEffect::Silence).getMagnitude() && !godmode) return 0; if (spell->mData.mType == ESM::Spell::ST_Power) return stats.getSpells().canUsePower(spell) ? 100 : 0; if (godmode) return 100; if (spell->mData.mType != ESM::Spell::ST_Spell) return 100; if (checkMagicka && spell->mData.mCost > 0 && stats.getMagicka().getCurrent() < spell->mData.mCost) return 0; if (spell->mData.mFlags & ESM::Spell::F_Always) return 100; float castBonus = -stats.getMagicEffects().get(ESM::MagicEffect::Sound).getMagnitude(); float castChance = baseChance + castBonus; castChance *= stats.getFatigueTerm(); return std::max(0.f, cap ? std::min(100.f, castChance) : castChance); } float getSpellSuccessChance (const std::string& spellId, const MWWorld::Ptr& actor, int* effectiveSchool, bool cap, bool checkMagicka) { if (const auto spell = MWBase::Environment::get().getWorld()->getStore().get().search(spellId)) return getSpellSuccessChance(spell, actor, effectiveSchool, cap, checkMagicka); return 0.f; } int getSpellSchool(const std::string& spellId, const MWWorld::Ptr& actor) { int school = 0; getSpellSuccessChance(spellId, actor, &school); return school; } int getSpellSchool(const ESM::Spell* spell, const MWWorld::Ptr& actor) { int school = 0; getSpellSuccessChance(spell, actor, &school); return school; } bool spellIncreasesSkill(const ESM::Spell *spell) { return spell->mData.mType == ESM::Spell::ST_Spell && !(spell->mData.mFlags & ESM::Spell::F_Always); } bool spellIncreasesSkill(const std::string &spellId) { const auto spell = MWBase::Environment::get().getWorld()->getStore().get().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.getTypeName() == typeid(ESM::Creature).name() && target.get()->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; } } ================================================ FILE: apps/openmw/mwmechanics/spellutil.hpp ================================================ #ifndef MWMECHANICS_SPELLUTIL_H #define MWMECHANICS_SPELLUTIL_H #include namespace ESM { struct ENAMstruct; struct MagicEffect; struct Spell; } namespace MWWorld { class Ptr; } namespace MWMechanics { ESM::Skill::SkillEnum spellSchoolToSkill(int school); float calcEffectCost(const ESM::ENAMstruct& effect, const ESM::MagicEffect* magicEffect = nullptr); int getEffectiveEnchantmentCastCost (float castCost, const MWWorld::Ptr& actor); /** * @param spell spell to cast * @param actor calculate spell success chance for this actor (depends on actor's skills) * @param effectiveSchool the spell's effective school (relevant for skill progress) will be written here * @param cap cap the result to 100%? * @param checkMagicka check magicka? * @note actor can be an NPC or a creature * @return success chance from 0 to 100 (in percent), if cap=false then chance above 100 may be returned. */ float calcSpellBaseSuccessChance (const ESM::Spell* spell, const MWWorld::Ptr& actor, int* effectiveSchool); float getSpellSuccessChance (const ESM::Spell* spell, const MWWorld::Ptr& actor, int* effectiveSchool = nullptr, bool cap=true, bool checkMagicka=true); float getSpellSuccessChance (const std::string& spellId, const MWWorld::Ptr& actor, int* effectiveSchool = nullptr, bool cap=true, bool checkMagicka=true); int getSpellSchool(const std::string& spellId, const MWWorld::Ptr& actor); int getSpellSchool(const ESM::Spell* spell, const MWWorld::Ptr& actor); /// 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 ================================================ FILE: apps/openmw/mwmechanics/stat.cpp ================================================ #include "stat.hpp" #include namespace MWMechanics { template Stat::Stat() : mBase (0), mModified (0), mCurrentModified (0) {} template Stat::Stat(T base) : mBase (base), mModified (base), mCurrentModified (base) {} template Stat::Stat(T base, T modified) : mBase (base), mModified (modified), mCurrentModified (modified) {} template const T& Stat::getBase() const { return mBase; } template T Stat::getModified(bool capped) const { if(!capped) return mModified; return std::max(static_cast(0), mModified); } template T Stat::getCurrentModified() const { return mCurrentModified; } template T Stat::getModifier() const { return mModified-mBase; } template T Stat::getCurrentModifier() const { return mCurrentModified - mModified; } template void Stat::set (const T& value) { T diff = value - mBase; mBase = mModified = value; mCurrentModified += diff; } template void Stat::setBase (const T& value) { T diff = value - mBase; mBase = value; mModified += diff; mCurrentModified += diff; } template void Stat::setModified (T value, const T& min, const T& max) { T diff = value - mModified; if (mBase+diffmax) { value = max + (mModified - mBase); diff = value - mModified; } mModified = value; mBase += diff; mCurrentModified += diff; } template void Stat::setCurrentModified(T value) { mCurrentModified = value; } template void Stat::setModifier (const T& modifier) { mModified = mBase + modifier; } template void Stat::setCurrentModifier(const T& modifier) { mCurrentModified = mModified + modifier; } template void Stat::writeState (ESM::StatState& state) const { state.mBase = mBase; state.mMod = mCurrentModified; } template void Stat::readState (const ESM::StatState& state) { mBase = state.mBase; mModified = state.mBase; mCurrentModified = state.mMod; } template DynamicStat::DynamicStat() : mStatic (0), mCurrent (0) {} template DynamicStat::DynamicStat(T base) : mStatic (base), mCurrent (base) {} template DynamicStat::DynamicStat(T base, T modified, T current) : mStatic(base, modified), mCurrent (current) {} template DynamicStat::DynamicStat(const Stat &stat, T current) : mStatic(stat), mCurrent (current) {} template const T& DynamicStat::getBase() const { return mStatic.getBase(); } template T DynamicStat::getModified() const { return mStatic.getModified(); } template T DynamicStat::getCurrentModified() const { return mStatic.getCurrentModified(); } template const T& DynamicStat::getCurrent() const { return mCurrent; } template void DynamicStat::set (const T& value) { mStatic.set (value); mCurrent = value; } template void DynamicStat::setBase (const T& value) { mStatic.setBase (value); if (mCurrent>getModified()) mCurrent = getModified(); } template void DynamicStat::setModified (T value, const T& min, const T& max) { mStatic.setModified (value, min, max); if (mCurrent>getModified()) mCurrent = getModified(); } template void DynamicStat::setCurrentModified(T value) { mStatic.setCurrentModified(value); } template void DynamicStat::setCurrent (const T& value, bool allowDecreaseBelowZero, bool allowIncreaseAboveModified) { if (value > mCurrent) { // increase if (value <= getModified() || allowIncreaseAboveModified) mCurrent = value; else if (mCurrent > getModified()) return; else mCurrent = getModified(); } else if (value > 0 || allowDecreaseBelowZero) { // allowed decrease mCurrent = value; } else if (mCurrent > 0) { // capped decrease mCurrent = 0; } } template void DynamicStat::setModifier (const T& modifier, bool allowCurrentDecreaseBelowZero) { T diff = modifier - mStatic.getModifier(); mStatic.setModifier (modifier); setCurrent (getCurrent()+diff, allowCurrentDecreaseBelowZero); } template void DynamicStat::setCurrentModifier(const T& modifier, bool allowCurrentDecreaseBelowZero) { T diff = modifier - mStatic.getCurrentModifier(); mStatic.setCurrentModifier(modifier); // The (modifier > 0) check here allows increase over modified only if the modifier is positive (a fortify effect is active). setCurrent (getCurrent() + diff, allowCurrentDecreaseBelowZero, (modifier > 0)); } template void DynamicStat::writeState (ESM::StatState& state) const { mStatic.writeState (state); state.mCurrent = mCurrent; } template void DynamicStat::readState (const ESM::StatState& state) { mStatic.readState (state); mCurrent = state.mCurrent; } AttributeValue::AttributeValue() : mBase(0.f), mModifier(0.f), mDamage(0.f) { } float AttributeValue::getModified() const { return std::max(0.f, mBase - mDamage + mModifier); } float AttributeValue::getBase() const { return mBase; } float AttributeValue::getModifier() const { return mModifier; } void AttributeValue::setBase(float base) { mBase = base; } void AttributeValue::setModifier(float mod) { mModifier = mod; } void AttributeValue::damage(float damage) { float threshold = mBase + mModifier; if (mDamage + damage > threshold) mDamage = threshold; else mDamage += damage; } void AttributeValue::restore(float amount) { if (mDamage <= 0) return; mDamage -= std::min(mDamage, amount); } float AttributeValue::getDamage() const { return mDamage; } void AttributeValue::writeState (ESM::StatState& state) const { state.mBase = mBase; state.mMod = mModifier; state.mDamage = mDamage; } void AttributeValue::readState (const ESM::StatState& state) { mBase = state.mBase; mModifier = state.mMod; mDamage = state.mDamage; } SkillValue::SkillValue() : mProgress(0) { } float SkillValue::getProgress() const { return mProgress; } void SkillValue::setProgress(float progress) { mProgress = progress; } void SkillValue::writeState (ESM::StatState& state) const { AttributeValue::writeState (state); state.mProgress = mProgress; } void SkillValue::readState (const ESM::StatState& state) { AttributeValue::readState (state); mProgress = state.mProgress; } } template class MWMechanics::Stat; template class MWMechanics::Stat; template class MWMechanics::DynamicStat; template class MWMechanics::DynamicStat; ================================================ FILE: apps/openmw/mwmechanics/stat.hpp ================================================ #ifndef GAME_MWMECHANICS_STAT_H #define GAME_MWMECHANICS_STAT_H #include #include namespace ESM { template struct StatState; } namespace MWMechanics { template class Stat { T mBase; T mModified; T mCurrentModified; public: typedef T Type; Stat(); Stat(T base); Stat(T base, T modified); const T& getBase() const; T getModified(bool capped = true) const; T getCurrentModified() const; T getModifier() const; T getCurrentModifier() const; /// Set base and modified to \a value. void set (const T& value); /// Set base and adjust modified accordingly. void setBase (const T& value); /// Set modified value and adjust base accordingly. void setModified (T value, const T& min, const T& max = std::numeric_limits::max()); /// Set "current modified," used for drain and fortify. Unlike the regular modifier /// this just adds and subtracts from the current value without changing the maximum. void setCurrentModified(T value); void setModifier (const T& modifier); void setCurrentModifier (const T& modifier); void writeState (ESM::StatState& state) const; void readState (const ESM::StatState& state); }; template inline bool operator== (const Stat& left, const Stat& right) { return left.getBase()==right.getBase() && left.getModified()==right.getModified(); } template inline bool operator!= (const Stat& left, const Stat& right) { return !(left==right); } template class DynamicStat { Stat mStatic; T mCurrent; public: typedef T Type; DynamicStat(); DynamicStat(T base); DynamicStat(T base, T modified, T current); DynamicStat(const Stat &stat, T current); const T& getBase() const; T getModified() const; T getCurrentModified() const; const T& getCurrent() const; /// Set base, modified and current to \a value. void set (const T& value); /// Set base and adjust modified accordingly. void setBase (const T& value); /// Set modified value and adjust base accordingly. void setModified (T value, const T& min, const T& max = std::numeric_limits::max()); /// Set "current modified," used for drain and fortify. Unlike the regular modifier /// this just adds and subtracts from the current value without changing the maximum. void setCurrentModified(T value); void setCurrent (const T& value, bool allowDecreaseBelowZero = false, bool allowIncreaseAboveModified = false); void setModifier (const T& modifier, bool allowCurrentToDecreaseBelowZero=false); void setCurrentModifier (const T& modifier, bool allowCurrentToDecreaseBelowZero = false); void writeState (ESM::StatState& state) const; void readState (const ESM::StatState& state); }; template inline bool operator== (const DynamicStat& left, const DynamicStat& right) { return left.getBase()==right.getBase() && left.getModified()==right.getModified() && left.getCurrent()==right.getCurrent(); } template inline bool operator!= (const DynamicStat& left, const DynamicStat& right) { return !(left==right); } class AttributeValue { float mBase; float mModifier; float mDamage; // needs to be float to allow continuous damage public: AttributeValue(); float getModified() const; float getBase() const; float getModifier() const; void setBase(float base); 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. void damage(float damage); void restore(float amount); float getDamage() const; void writeState (ESM::StatState& state) const; void readState (const ESM::StatState& state); }; class SkillValue : public AttributeValue { float mProgress; public: SkillValue(); float getProgress() const; void setProgress(float progress); void writeState (ESM::StatState& state) const; void readState (const ESM::StatState& state); }; inline bool operator== (const AttributeValue& left, const AttributeValue& right) { return left.getBase() == right.getBase() && left.getModifier() == right.getModifier() && left.getDamage() == right.getDamage(); } inline bool operator!= (const AttributeValue& left, const AttributeValue& right) { return !(left == right); } inline bool operator== (const SkillValue& left, const SkillValue& right) { return left.getBase() == right.getBase() && left.getModifier() == right.getModifier() && left.getDamage() == right.getDamage() && left.getProgress() == right.getProgress(); } inline bool operator!= (const SkillValue& left, const SkillValue& right) { return !(left == right); } } #endif ================================================ FILE: apps/openmw/mwmechanics/steering.cpp ================================================ #include "steering.hpp" #include #include #include "../mwworld/class.hpp" #include "../mwworld/ptr.hpp" #include "../mwbase/environment.hpp" #include "movement.hpp" namespace MWMechanics { bool smoothTurn(const MWWorld::Ptr& actor, float targetAngleRadians, int axis, float epsilonRadians) { MWMechanics::Movement& movement = actor.getClass().getMovementSettings(actor); float diff = Misc::normalizeAngle(targetAngleRadians - actor.getRefData().getPosition().rot[axis]); float absDiff = std::abs(diff); // The turning animation actually moves you slightly, so the angle will be wrong again. // Use epsilon to prevent jerkiness. if (absDiff < epsilonRadians) return true; float limit = getAngularVelocity(actor.getClass().getMaxSpeed(actor)) * MWBase::Environment::get().getFrameDuration(); static const bool smoothMovement = Settings::Manager::getBool("smooth movement", "Game"); if (smoothMovement) limit *= std::min(absDiff / osg::PI + 0.1, 0.5); if (absDiff > limit) diff = osg::sign(diff) * limit; movement.mRotation[axis] = diff; return false; } bool zTurn(const MWWorld::Ptr& actor, float targetAngleRadians, float epsilonRadians) { return smoothTurn(actor, targetAngleRadians, 2, epsilonRadians); } } ================================================ FILE: apps/openmw/mwmechanics/steering.hpp ================================================ #ifndef OPENMW_MECHANICS_STEERING_H #define OPENMW_MECHANICS_STEERING_H #include #include namespace MWWorld { class Ptr; } namespace MWMechanics { // Max rotating speed, radian/sec inline float getAngularVelocity(const float actorSpeed) { const float baseAngluarVelocity = 10; const float baseSpeed = 200; return baseAngluarVelocity * std::max(actorSpeed / baseSpeed, 1.0f); } /// configure rotation settings for an actor to reach this target angle (eventually) /// @return have we reached the target angle? bool zTurn(const MWWorld::Ptr& actor, float targetAngleRadians, float epsilonRadians = osg::DegreesToRadians(0.5)); bool smoothTurn(const MWWorld::Ptr& actor, float targetAngleRadians, int axis, float epsilonRadians = osg::DegreesToRadians(0.5)); } #endif ================================================ FILE: apps/openmw/mwmechanics/summoning.cpp ================================================ #include "summoning.hpp" #include /* Start of tes3mp addition Include additional headers for multiplayer purposes */ #include #include "../mwmechanics/creaturestats.hpp" #include "../mwmp/Main.hpp" #include "../mwmp/Networking.hpp" #include "../mwmp/CellController.hpp" #include "../mwmp/ObjectList.hpp" /* End of tes3mp addition */ #include "../mwbase/environment.hpp" #include "../mwbase/world.hpp" #include "../mwbase/mechanicsmanager.hpp" #include "../mwworld/esmstore.hpp" #include "../mwworld/class.hpp" #include "../mwworld/manualref.hpp" #include "../mwworld/inventorystore.hpp" #include "../mwrender/animation.hpp" #include "creaturestats.hpp" #include "aifollow.hpp" namespace MWMechanics { bool isSummoningEffect(int effectId) { return ((effectId >= ESM::MagicEffect::SummonScamp && effectId <= ESM::MagicEffect::SummonStormAtronach) || (effectId == ESM::MagicEffect::SummonCenturionSphere) || (effectId >= ESM::MagicEffect::SummonFabricant && effectId <= ESM::MagicEffect::SummonCreature05)); } std::string getSummonedCreature(int effectId) { static const std::map summonMap { {ESM::MagicEffect::SummonAncestralGhost, "sMagicAncestralGhostID"}, {ESM::MagicEffect::SummonBonelord, "sMagicBonelordID"}, {ESM::MagicEffect::SummonBonewalker, "sMagicLeastBonewalkerID"}, {ESM::MagicEffect::SummonCenturionSphere, "sMagicCenturionSphereID"}, {ESM::MagicEffect::SummonClannfear, "sMagicClannfearID"}, {ESM::MagicEffect::SummonDaedroth, "sMagicDaedrothID"}, {ESM::MagicEffect::SummonDremora, "sMagicDremoraID"}, {ESM::MagicEffect::SummonFabricant, "sMagicFabricantID"}, {ESM::MagicEffect::SummonFlameAtronach, "sMagicFlameAtronachID"}, {ESM::MagicEffect::SummonFrostAtronach, "sMagicFrostAtronachID"}, {ESM::MagicEffect::SummonGoldenSaint, "sMagicGoldenSaintID"}, {ESM::MagicEffect::SummonGreaterBonewalker, "sMagicGreaterBonewalkerID"}, {ESM::MagicEffect::SummonHunger, "sMagicHungerID"}, {ESM::MagicEffect::SummonScamp, "sMagicScampID"}, {ESM::MagicEffect::SummonSkeletalMinion, "sMagicSkeletalMinionID"}, {ESM::MagicEffect::SummonStormAtronach, "sMagicStormAtronachID"}, {ESM::MagicEffect::SummonWingedTwilight, "sMagicWingedTwilightID"}, {ESM::MagicEffect::SummonWolf, "sMagicCreature01ID"}, {ESM::MagicEffect::SummonBear, "sMagicCreature02ID"}, {ESM::MagicEffect::SummonBonewolf, "sMagicCreature03ID"}, {ESM::MagicEffect::SummonCreature04, "sMagicCreature04ID"}, {ESM::MagicEffect::SummonCreature05, "sMagicCreature05ID"} }; auto it = summonMap.find(effectId); if (it != summonMap.end()) return MWBase::Environment::get().getWorld()->getStore().get().find(it->second)->mValue.getString(); return std::string(); } UpdateSummonedCreatures::UpdateSummonedCreatures(const MWWorld::Ptr &actor) : mActor(actor) { } void UpdateSummonedCreatures::visit(EffectKey key, int effectIndex, const std::string &sourceName, const std::string &sourceId, int casterActorId, float magnitude, float remainingTime, float totalTime) { if (isSummoningEffect(key.mId) && magnitude > 0) { mActiveEffects.insert(ESM::SummonKey(key.mId, sourceId, effectIndex)); } } void UpdateSummonedCreatures::process(bool cleanup) { MWMechanics::CreatureStats& creatureStats = mActor.getClass().getCreatureStats(mActor); std::map& creatureMap = creatureStats.getSummonedCreatureMap(); for (std::set::iterator it = mActiveEffects.begin(); it != mActiveEffects.end(); ++it) { bool found = creatureMap.find(*it) != creatureMap.end(); if (!found) { std::string creatureID = getSummonedCreature(it->mEffectId); if (!creatureID.empty()) { int creatureActorId = -1; /* Start of tes3mp change (major) Send an ID_OBJECT_SPAWN packet every time a creature is summoned in a cell that we hold authority over, then delete the creature and wait for the server to send it back with a unique mpNum of its own Comment out most of the code here except for the actual placement of the Ptr and the creatureActorId insertion into the creatureMap */ try { MWWorld::ManualRef ref(MWBase::Environment::get().getWorld()->getStore(), creatureID, 1); /* MWMechanics::CreatureStats& summonedCreatureStats = ref.getPtr().getClass().getCreatureStats(ref.getPtr()); // Make the summoned creature follow its master and help in fights AiFollow package(mActor); summonedCreatureStats.getAiSequence().stack(package, ref.getPtr()); creatureActorId = summonedCreatureStats.getActorId(); */ MWWorld::Ptr placed = MWBase::Environment::get().getWorld()->safePlaceObject(ref.getPtr(), mActor, mActor.getCell(), 0, 120.f); /* MWRender::Animation* anim = MWBase::Environment::get().getWorld()->getAnimation(placed); if (anim) { const ESM::Static* fx = MWBase::Environment::get().getWorld()->getStore().get() .search("VFX_Summon_Start"); if (fx) anim->addEffect("meshes\\" + fx->mModel, -1, false); } */ if (mwmp::Main::get().getCellController()->hasLocalAuthority(*placed.getCell()->getCell())) { mwmp::ObjectList *objectList = mwmp::Main::get().getNetworking()->getObjectList(); objectList->reset(); objectList->packetOrigin = mwmp::CLIENT_GAMEPLAY; MWMechanics::CreatureStats *actorCreatureStats = &mActor.getClass().getCreatureStats(mActor); int effectId = it->mEffectId; std::string sourceId = it->mSourceId; float duration = actorCreatureStats->getActiveSpells().getEffectDuration(effectId, sourceId); objectList->addObjectSpawn(placed, mActor, sourceId, effectId, duration); objectList->sendObjectSpawn(); } MWBase::Environment::get().getWorld()->deleteObject(placed); } catch (std::exception& e) { Log(Debug::Error) << "Failed to spawn summoned creature: " << e.what(); // still insert into creatureMap so we don't try to spawn again every frame, that would spam the warning log } creatureMap.emplace(*it, creatureActorId); /* End of tes3mp change (major) */ } } } // Update summon effects for (std::map::iterator it = creatureMap.begin(); it != creatureMap.end(); ) { bool found = mActiveEffects.find(it->first) != mActiveEffects.end(); if (!found) { // Effect has ended MWBase::Environment::get().getMechanicsManager()->cleanupSummonedCreature(mActor, it->second); creatureMap.erase(it++); continue; } ++it; } std::vector graveyard = creatureStats.getSummonedCreatureGraveyard(); creatureStats.getSummonedCreatureGraveyard().clear(); for (const int creature : graveyard) MWBase::Environment::get().getMechanicsManager()->cleanupSummonedCreature(mActor, creature); if (!cleanup) return; for (std::map::iterator it = creatureMap.begin(); it != creatureMap.end(); ) { if(it->second == -1) { // Keep the spell effect active if we failed to spawn anything it++; continue; } MWWorld::Ptr ptr = MWBase::Environment::get().getWorld()->searchPtrViaActorId(it->second); if (!ptr.isEmpty() && ptr.getClass().getCreatureStats(ptr).isDead() && ptr.getClass().getCreatureStats(ptr).isDeathAnimationFinished()) { // Purge the magic effect so a new creature can be summoned if desired purgeSummonEffect(mActor, *it); creatureMap.erase(it++); } else ++it; } } void purgeSummonEffect(const MWWorld::Ptr& summoner, const std::pair& summon) { auto& creatureStats = summoner.getClass().getCreatureStats(summoner); creatureStats.getActiveSpells().purgeEffect(summon.first.mEffectId, summon.first.mSourceId, summon.first.mEffectIndex); creatureStats.getSpells().purgeEffect(summon.first.mEffectId, summon.first.mSourceId); if (summoner.getClass().hasInventoryStore(summoner)) summoner.getClass().getInventoryStore(summoner).purgeEffect(summon.first.mEffectId, summon.first.mSourceId, false, summon.first.mEffectIndex); MWBase::Environment::get().getMechanicsManager()->cleanupSummonedCreature(summoner, summon.second); } } ================================================ FILE: apps/openmw/mwmechanics/summoning.hpp ================================================ #ifndef OPENMW_MECHANICS_SUMMONING_H #define OPENMW_MECHANICS_SUMMONING_H #include #include "../mwworld/ptr.hpp" #include #include "magiceffects.hpp" namespace MWMechanics { class CreatureStats; bool isSummoningEffect(int effectId); std::string getSummonedCreature(int effectId); void purgeSummonEffect(const MWWorld::Ptr& summoner, const std::pair& summon); struct UpdateSummonedCreatures : public EffectSourceVisitor { UpdateSummonedCreatures(const MWWorld::Ptr& actor); virtual ~UpdateSummonedCreatures() = default; void visit (MWMechanics::EffectKey key, int effectIndex, const std::string& sourceName, const std::string& sourceId, int casterActorId, float magnitude, float remainingTime = -1, float totalTime = -1) override; /// To call after all effect sources have been visited void process(bool cleanup); private: MWWorld::Ptr mActor; std::set mActiveEffects; }; } #endif ================================================ FILE: apps/openmw/mwmechanics/tickableeffects.cpp ================================================ #include "tickableeffects.hpp" #include /* Start of tes3mp addition Include additional headers for multiplayer purposes */ #include "../mwmp/Main.hpp" #include "../mwmp/PlayerList.hpp" #include "../mwmp/CellController.hpp" /* End of tes3mp addition */ #include "../mwbase/environment.hpp" #include "../mwbase/windowmanager.hpp" #include "../mwbase/world.hpp" #include "../mwworld/cellstore.hpp" #include "../mwworld/class.hpp" #include "../mwworld/containerstore.hpp" #include "../mwworld/esmstore.hpp" #include "../mwworld/inventorystore.hpp" #include "actorutil.hpp" #include "npcstats.hpp" namespace MWMechanics { void adjustDynamicStat(CreatureStats& creatureStats, int index, float magnitude, bool allowDecreaseBelowZero = false) { DynamicStat stat = creatureStats.getDynamic(index); stat.setCurrent(stat.getCurrent() + magnitude, allowDecreaseBelowZero); creatureStats.setDynamic(index, stat); } bool disintegrateSlot (const MWWorld::Ptr& ptr, int slot, float disintegrate) { if (!ptr.getClass().hasInventoryStore(ptr)) return false; MWWorld::InventoryStore& inv = ptr.getClass().getInventoryStore(ptr); MWWorld::ContainerStoreIterator item = inv.getSlot(slot); if (item != inv.end() && (item.getType() == MWWorld::ContainerStore::Type_Armor || item.getType() == MWWorld::ContainerStore::Type_Weapon)) { if (!item->getClass().hasItemHealth(*item)) return false; int charge = item->getClass().getItemHealth(*item); if (charge == 0) return false; // Store remainder of disintegrate amount (automatically subtracted if > 1) item->getCellRef().applyChargeRemainderToBeSubtracted(disintegrate - std::floor(disintegrate)); charge = item->getClass().getItemHealth(*item); charge -= std::min(static_cast(disintegrate), charge); item->getCellRef().setCharge(charge); if (charge == 0) { // Will unequip the broken item and try to find a replacement if (ptr != getPlayer()) inv.autoEquip(ptr); else inv.unequipItem(*item, ptr); } return true; } return false; } bool effectTick(CreatureStats& creatureStats, const MWWorld::Ptr& actor, const EffectKey &effectKey, float magnitude) { if (magnitude == 0.f) return false; bool receivedMagicDamage = false; bool godmode = actor == getPlayer() && MWBase::Environment::get().getWorld()->getGodModeState(); switch (effectKey.mId) { case ESM::MagicEffect::DamageAttribute: { if (godmode) break; AttributeValue attr = creatureStats.getAttribute(effectKey.mArg); attr.damage(magnitude); creatureStats.setAttribute(effectKey.mArg, attr); break; } case ESM::MagicEffect::RestoreAttribute: { AttributeValue attr = creatureStats.getAttribute(effectKey.mArg); attr.restore(magnitude); creatureStats.setAttribute(effectKey.mArg, attr); break; } case ESM::MagicEffect::RestoreHealth: case ESM::MagicEffect::RestoreMagicka: case ESM::MagicEffect::RestoreFatigue: adjustDynamicStat(creatureStats, effectKey.mId-ESM::MagicEffect::RestoreHealth, magnitude); break; case ESM::MagicEffect::DamageHealth: if (godmode) break; receivedMagicDamage = true; adjustDynamicStat(creatureStats, effectKey.mId-ESM::MagicEffect::DamageHealth, -magnitude); break; case ESM::MagicEffect::DamageMagicka: case ESM::MagicEffect::DamageFatigue: { if (godmode) break; int index = effectKey.mId-ESM::MagicEffect::DamageHealth; static const bool uncappedDamageFatigue = Settings::Manager::getBool("uncapped damage fatigue", "Game"); adjustDynamicStat(creatureStats, index, -magnitude, index == 2 && uncappedDamageFatigue); break; } case ESM::MagicEffect::AbsorbHealth: if (!godmode || magnitude <= 0) { if (magnitude > 0.f) receivedMagicDamage = true; adjustDynamicStat(creatureStats, effectKey.mId-ESM::MagicEffect::AbsorbHealth, -magnitude); } break; case ESM::MagicEffect::AbsorbMagicka: case ESM::MagicEffect::AbsorbFatigue: if (!godmode || magnitude <= 0) adjustDynamicStat(creatureStats, effectKey.mId-ESM::MagicEffect::AbsorbHealth, -magnitude); break; case ESM::MagicEffect::DisintegrateArmor: { if (godmode) break; static const std::array priorities { MWWorld::InventoryStore::Slot_CarriedLeft, MWWorld::InventoryStore::Slot_Cuirass, MWWorld::InventoryStore::Slot_LeftPauldron, MWWorld::InventoryStore::Slot_RightPauldron, MWWorld::InventoryStore::Slot_LeftGauntlet, MWWorld::InventoryStore::Slot_RightGauntlet, MWWorld::InventoryStore::Slot_Helmet, MWWorld::InventoryStore::Slot_Greaves, MWWorld::InventoryStore::Slot_Boots }; for (const int priority : priorities) { if (disintegrateSlot(actor, priority, magnitude)) break; } break; } case ESM::MagicEffect::DisintegrateWeapon: if (!godmode) disintegrateSlot(actor, MWWorld::InventoryStore::Slot_CarriedRight, magnitude); break; case ESM::MagicEffect::SunDamage: { // isInCell shouldn't be needed, but updateActor called during game start if (!actor.isInCell() || !actor.getCell()->isExterior() || godmode) break; float time = MWBase::Environment::get().getWorld()->getTimeStamp().getHour(); float timeDiff = std::min(7.f, std::max(0.f, std::abs(time - 13))); float damageScale = 1.f - timeDiff / 7.f; // When cloudy, the sun damage effect is halved static float fMagicSunBlockedMult = MWBase::Environment::get().getWorld()->getStore().get().find( "fMagicSunBlockedMult")->mValue.getFloat(); int weather = MWBase::Environment::get().getWorld()->getCurrentWeather(); if (weather > 1) damageScale *= fMagicSunBlockedMult; adjustDynamicStat(creatureStats, 0, -magnitude * damageScale); if (magnitude * damageScale > 0.f) receivedMagicDamage = true; break; } case ESM::MagicEffect::FireDamage: case ESM::MagicEffect::ShockDamage: case ESM::MagicEffect::FrostDamage: case ESM::MagicEffect::Poison: { if (godmode) break; adjustDynamicStat(creatureStats, 0, -magnitude); receivedMagicDamage = true; break; } case ESM::MagicEffect::DamageSkill: case ESM::MagicEffect::RestoreSkill: { if (!actor.getClass().isNpc()) break; if (godmode && effectKey.mId == ESM::MagicEffect::DamageSkill) break; NpcStats &npcStats = actor.getClass().getNpcStats(actor); SkillValue& skill = npcStats.getSkill(effectKey.mArg); if (effectKey.mId == ESM::MagicEffect::RestoreSkill) skill.restore(magnitude); else skill.damage(magnitude); break; } default: return false; } if (receivedMagicDamage && actor == getPlayer()) MWBase::Environment::get().getWindowManager()->activateHitOverlay(false); return true; } } ================================================ FILE: apps/openmw/mwmechanics/tickableeffects.hpp ================================================ #ifndef MWMECHANICS_TICKABLEEFFECTS_H #define MWMECHANICS_TICKABLEEFFECTS_H namespace MWWorld { class Ptr; } namespace MWMechanics { class CreatureStats; struct EffectKey; /// Apply a magic effect that is applied in tick intervals until its remaining time ends or it is removed /// Note: this function works in loop, so magic effects should not be removed here to avoid iterator invalidation. /// @return Was the effect a tickable effect with a magnitude? bool effectTick(CreatureStats& creatureStats, const MWWorld::Ptr& actor, const EffectKey& effectKey, float magnitude); } #endif ================================================ FILE: apps/openmw/mwmechanics/trading.cpp ================================================ #include "trading.hpp" #include #include "../mwbase/environment.hpp" #include "../mwbase/mechanicsmanager.hpp" #include "../mwbase/world.hpp" #include "../mwworld/class.hpp" #include "../mwworld/esmstore.hpp" #include "creaturestats.hpp" namespace MWMechanics { Trading::Trading() {} bool Trading::haggle(const MWWorld::Ptr& player, const MWWorld::Ptr& merchant, int playerOffer, int merchantOffer) { // accept if merchant offer is better than player offer if ( playerOffer <= merchantOffer ) { return true; } // reject if npc is a creature if ( merchant.getTypeName() != typeid(ESM::NPC).name() ) { return false; } const MWWorld::Store &gmst = MWBase::Environment::get().getWorld()->getStore().get(); // Is the player buying? bool buying = (merchantOffer < 0); int a = std::abs(merchantOffer); int b = std::abs(playerOffer); int d = (buying) ? int(100 * (a - b) / a) : int(100 * (b - a) / b); int clampedDisposition = MWBase::Environment::get().getMechanicsManager()->getDerivedDisposition(merchant); const MWMechanics::CreatureStats &merchantStats = merchant.getClass().getCreatureStats(merchant); const MWMechanics::CreatureStats &playerStats = player.getClass().getCreatureStats(player); float a1 = static_cast(player.getClass().getSkill(player, ESM::Skill::Mercantile)); float b1 = 0.1f * playerStats.getAttribute(ESM::Attribute::Luck).getModified(); float c1 = 0.2f * playerStats.getAttribute(ESM::Attribute::Personality).getModified(); float d1 = static_cast(merchant.getClass().getSkill(merchant, ESM::Skill::Mercantile)); float e1 = 0.1f * merchantStats.getAttribute(ESM::Attribute::Luck).getModified(); float f1 = 0.2f * merchantStats.getAttribute(ESM::Attribute::Personality).getModified(); float dispositionTerm = gmst.find("fDispositionMod")->mValue.getFloat() * (clampedDisposition - 50); float pcTerm = (dispositionTerm + a1 + b1 + c1) * playerStats.getFatigueTerm(); float npcTerm = (d1 + e1 + f1) * merchantStats.getFatigueTerm(); float x = gmst.find("fBargainOfferMulti")->mValue.getFloat() * d + gmst.find("fBargainOfferBase")->mValue.getFloat() + int(pcTerm - npcTerm); int roll = Misc::Rng::rollDice(100) + 1; // reject if roll fails // (or if player tries to buy things and get money) if ( roll > x || (merchantOffer < 0 && 0 < playerOffer) ) { return false; } // apply skill gain on successful barter float skillGain = 0.f; int finalPrice = std::abs(playerOffer); int initialMerchantOffer = std::abs(merchantOffer); if ( !buying && (finalPrice > initialMerchantOffer) ) { skillGain = floor(100.f * (finalPrice - initialMerchantOffer) / finalPrice); } else if ( buying && (finalPrice < initialMerchantOffer) ) { skillGain = floor(100.f * (initialMerchantOffer - finalPrice) / initialMerchantOffer); } player.getClass().skillUsageSucceeded(player, ESM::Skill::Mercantile, 0, skillGain); return true; } } ================================================ FILE: apps/openmw/mwmechanics/trading.hpp ================================================ #ifndef OPENMW_MECHANICS_TRADING_H #define OPENMW_MECHANICS_TRADING_H namespace MWWorld { class Ptr; } namespace MWMechanics { class Trading { public: Trading(); bool haggle(const MWWorld::Ptr& player, const MWWorld::Ptr& merchant, int playerOffer, int merchantOffer); }; } #endif ================================================ FILE: apps/openmw/mwmechanics/typedaipackage.hpp ================================================ #ifndef GAME_MWMECHANICS_TYPEDAIPACKAGE_H #define GAME_MWMECHANICS_TYPEDAIPACKAGE_H #include "aipackage.hpp" namespace MWMechanics { template struct TypedAiPackage : public AiPackage { TypedAiPackage() : AiPackage(T::getTypeId(), T::makeDefaultOptions()) {} TypedAiPackage(const Options& options) : AiPackage(T::getTypeId(), options) {} template TypedAiPackage(Derived*) : AiPackage(Derived::getTypeId(), Derived::makeDefaultOptions()) {} std::unique_ptr clone() const override { return std::make_unique(*static_cast(this)); } }; } #endif ================================================ FILE: apps/openmw/mwmechanics/weaponpriority.cpp ================================================ #include "weaponpriority.hpp" #include #include "../mwbase/environment.hpp" #include "../mwbase/world.hpp" #include "../mwworld/class.hpp" #include "../mwworld/esmstore.hpp" #include "../mwworld/inventorystore.hpp" #include "combat.hpp" #include "aicombataction.hpp" #include "spellpriority.hpp" #include "spellutil.hpp" #include "weapontype.hpp" namespace MWMechanics { float rateWeapon (const MWWorld::Ptr &item, const MWWorld::Ptr& actor, const MWWorld::Ptr& enemy, int type, float arrowRating, float boltRating) { if (enemy.isEmpty() || item.getTypeName() != typeid(ESM::Weapon).name()) return 0.f; if (item.getClass().hasItemHealth(item) && item.getClass().getItemHealth(item) == 0) return 0.f; const ESM::Weapon* weapon = item.get()->mBase; if (type != -1 && weapon->mData.mType != type) return 0.f; const MWBase::World* world = MWBase::Environment::get().getWorld(); const MWWorld::Store& gmst = world->getStore().get(); ESM::WeaponType::Class weapclass = MWMechanics::getWeaponType(weapon->mData.mType)->mWeaponClass; if (type == -1 && weapclass == ESM::WeaponType::Ammo) return 0.f; float rating=0.f; static const float fAIMeleeWeaponMult = gmst.find("fAIMeleeWeaponMult")->mValue.getFloat(); float ratingMult = fAIMeleeWeaponMult; if (weapclass != ESM::WeaponType::Melee) { // Underwater ranged combat is impossible if (world->isUnderwater(MWWorld::ConstPtr(actor), 0.75f) || world->isUnderwater(MWWorld::ConstPtr(enemy), 0.75f)) return 0.f; // Use a higher rating multiplier if the actor is out of enemy's reach, use the normal mult otherwise if (getDistanceMinusHalfExtents(actor, enemy) >= getMaxAttackDistance(enemy)) { static const float fAIRangeMeleeWeaponMult = gmst.find("fAIRangeMeleeWeaponMult")->mValue.getFloat(); ratingMult = fAIRangeMeleeWeaponMult; } } const float chop = (weapon->mData.mChop[0] + weapon->mData.mChop[1]) / 2.f; // We need to account for the fact that thrown weapons have 2x real damage applied to the target // as they're both the weapon and the ammo of the hit if (weapclass == ESM::WeaponType::Thrown) { rating = chop * 2; } else if (weapclass != ESM::WeaponType::Melee) { rating = chop; } else { const float slash = (weapon->mData.mSlash[0] + weapon->mData.mSlash[1]) / 2.f; const float thrust = (weapon->mData.mThrust[0] + weapon->mData.mThrust[1]) / 2.f; rating = (slash * slash + thrust * thrust + chop * chop) / (slash + thrust + chop); } adjustWeaponDamage(rating, item, actor); if (weapclass != ESM::WeaponType::Ranged) { resistNormalWeapon(enemy, actor, item, rating); applyWerewolfDamageMult(enemy, item, rating); } else { int ammotype = MWMechanics::getWeaponType(weapon->mData.mType)->mAmmoType; if (ammotype == ESM::Weapon::Arrow) { if (arrowRating <= 0.f) rating = 0.f; else rating += arrowRating; } else if (ammotype == ESM::Weapon::Bolt) { if (boltRating <= 0.f) rating = 0.f; else rating += boltRating; } } if (!weapon->mEnchant.empty()) { const ESM::Enchantment* enchantment = world->getStore().get().find(weapon->mEnchant); if (enchantment->mData.mType == ESM::Enchantment::WhenStrikes) { int castCost = getEffectiveEnchantmentCastCost(static_cast(enchantment->mData.mCost), actor); float charge = item.getCellRef().getEnchantmentCharge(); if (charge == -1 || charge >= castCost || weapclass == ESM::WeaponType::Thrown || weapclass == ESM::WeaponType::Ammo) rating += rateEffects(enchantment->mEffects, actor, enemy); } } int value = 50.f; if (actor.getClass().isNpc()) { int skill = item.getClass().getEquipmentSkill(item); if (skill != -1) value = actor.getClass().getSkill(actor, skill); } else { MWWorld::LiveCellRef *ref = actor.get(); value = ref->mBase->mData.mCombat; } // 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)); if (weapclass != ESM::WeaponType::Ammo) rating *= weapon->mData.mSpeed; return rating * ratingMult; } float rateAmmo(const MWWorld::Ptr &actor, const MWWorld::Ptr &enemy, MWWorld::Ptr &bestAmmo, int ammoType) { float bestAmmoRating = 0.f; if (!actor.getClass().hasInventoryStore(actor)) return bestAmmoRating; MWWorld::InventoryStore& store = actor.getClass().getInventoryStore(actor); for (MWWorld::ContainerStoreIterator it = store.begin(); it != store.end(); ++it) { float rating = rateWeapon(*it, actor, enemy, ammoType); if (rating > bestAmmoRating) { bestAmmoRating = rating; bestAmmo = *it; } } return bestAmmoRating; } float rateAmmo(const MWWorld::Ptr &actor, const MWWorld::Ptr &enemy, int ammoType) { MWWorld::Ptr emptyPtr; return rateAmmo(actor, enemy, emptyPtr, ammoType); } float vanillaRateWeaponAndAmmo(const MWWorld::Ptr& weapon, const MWWorld::Ptr& ammo, const MWWorld::Ptr& actor, const MWWorld::Ptr& enemy) { const MWWorld::Store& gmst = MWBase::Environment::get().getWorld()->getStore().get(); static const float fAIMeleeWeaponMult = gmst.find("fAIMeleeWeaponMult")->mValue.getFloat(); static const float fAIMeleeArmorMult = gmst.find("fAIMeleeArmorMult")->mValue.getFloat(); static const float fAIRangeMeleeWeaponMult = gmst.find("fAIRangeMeleeWeaponMult")->mValue.getFloat(); if (weapon.isEmpty()) return 0.f; float skillMult = actor.getClass().getSkill(actor, weapon.getClass().getEquipmentSkill(weapon)) * 0.01f; float chopMult = fAIMeleeWeaponMult; float bonusDamage = 0.f; const ESM::Weapon* esmWeap = weapon.get()->mBase; int type = esmWeap->mData.mType; if (getWeaponType(type)->mWeaponClass != ESM::WeaponType::Melee) { if (!ammo.isEmpty() && !MWBase::Environment::get().getWorld()->isSwimming(enemy)) { bonusDamage = ammo.get()->mBase->mData.mChop[1]; chopMult = fAIRangeMeleeWeaponMult; } else chopMult = 0.f; } float chopRating = (esmWeap->mData.mChop[1] + bonusDamage) * skillMult * chopMult; float slashRating = esmWeap->mData.mSlash[1] * skillMult * fAIMeleeWeaponMult; float thrustRating = esmWeap->mData.mThrust[1] * skillMult * fAIMeleeWeaponMult; return actor.getClass().getArmorRating(actor) * fAIMeleeArmorMult + std::max(std::max(chopRating, slashRating), thrustRating); } } ================================================ FILE: apps/openmw/mwmechanics/weaponpriority.hpp ================================================ #ifndef OPENMW_WEAPON_PRIORITY_H #define OPENMW_WEAPON_PRIORITY_H #include "../mwworld/ptr.hpp" namespace MWMechanics { float rateWeapon (const MWWorld::Ptr& item, const MWWorld::Ptr& actor, const MWWorld::Ptr& enemy, int type=-1, float arrowRating=0.f, float boltRating=0.f); float rateAmmo(const MWWorld::Ptr &actor, const MWWorld::Ptr &enemy, MWWorld::Ptr &bestAmmo, int ammoType); float rateAmmo(const MWWorld::Ptr &actor, const MWWorld::Ptr &enemy, int ammoType); float vanillaRateWeaponAndAmmo(const MWWorld::Ptr& weapon, const MWWorld::Ptr& ammo, const MWWorld::Ptr& actor, const MWWorld::Ptr& enemy); } #endif ================================================ FILE: apps/openmw/mwmechanics/weapontype.cpp ================================================ #include "weapontype.hpp" #include "../mwworld/class.hpp" namespace MWMechanics { MWWorld::ContainerStoreIterator getActiveWeapon(MWWorld::Ptr actor, int *weaptype) { MWWorld::InventoryStore &inv = actor.getClass().getInventoryStore(actor); CreatureStats &stats = actor.getClass().getCreatureStats(actor); if(stats.getDrawState() == MWMechanics::DrawState_Spell) { *weaptype = ESM::Weapon::Spell; return inv.end(); } if(stats.getDrawState() == MWMechanics::DrawState_Weapon) { MWWorld::ContainerStoreIterator weapon = inv.getSlot(MWWorld::InventoryStore::Slot_CarriedRight); if(weapon == inv.end()) *weaptype = ESM::Weapon::HandToHand; else { const std::string &type = weapon->getTypeName(); if(type == typeid(ESM::Weapon).name()) { const MWWorld::LiveCellRef *ref = weapon->get(); *weaptype = ref->mBase->mData.mType; } else if (type == typeid(ESM::Lockpick).name() || type == typeid(ESM::Probe).name()) *weaptype = ESM::Weapon::PickProbe; } return weapon; } return inv.getSlot(MWWorld::InventoryStore::Slot_CarriedRight); } const ESM::WeaponType* getWeaponType(const int weaponType) { std::map::const_iterator found = sWeaponTypeList.find(weaponType); if (found == sWeaponTypeList.end()) { // Use one-handed short blades as fallback return &sWeaponTypeList[0]; } return &found->second; } } ================================================ FILE: apps/openmw/mwmechanics/weapontype.hpp ================================================ #ifndef GAME_MWMECHANICS_WEAPONTYPE_H #define GAME_MWMECHANICS_WEAPONTYPE_H #include "../mwworld/inventorystore.hpp" namespace MWMechanics { static std::map sWeaponTypeList = { { ESM::Weapon::None, { /* short group */ "", /* long group */ "", /* sound ID */ "", /* attach bone */ "", /* sheath bone */ "", /* usage skill */ ESM::Skill::HandToHand, /* weapon class*/ ESM::WeaponType::Melee, /* ammo type */ ESM::Weapon::None, /* flags */ 0 } }, { ESM::Weapon::PickProbe, { /* short group */ "1h", /* long group */ "pickprobe", /* sound ID */ "", /* attach bone */ "", /* sheath bone */ "", /* usage skill */ ESM::Skill::Security, /* weapon class*/ ESM::WeaponType::Melee, /* ammo type */ ESM::Weapon::None, /* flags */ 0 } }, { ESM::Weapon::Spell, { /* short group */ "spell", /* long group */ "spellcast", /* sound ID */ "", /* attach bone */ "", /* sheath bone */ "", /* usage skill */ ESM::Skill::HandToHand, /* weapon class*/ ESM::WeaponType::Melee, /* ammo type */ ESM::Weapon::None, /* flags */ ESM::WeaponType::TwoHanded } }, { ESM::Weapon::HandToHand, { /* short group */ "hh", /* long group */ "handtohand", /* sound ID */ "", /* attach bone */ "", /* sheath bone */ "", /* usage skill */ ESM::Skill::HandToHand, /* weapon class*/ ESM::WeaponType::Melee, /* ammo type */ ESM::Weapon::None, /* flags */ ESM::WeaponType::TwoHanded } }, { ESM::Weapon::ShortBladeOneHand, { /* short group */ "1s", /* long group */ "shortbladeonehand", /* sound ID */ "Item Weapon Shortblade", /* attach bone */ "Weapon Bone", /* sheath bone */ "Bip01 ShortBladeOneHand", /* usage skill */ ESM::Skill::ShortBlade, /* weapon class*/ ESM::WeaponType::Melee, /* ammo type */ ESM::Weapon::None, /* flags */ ESM::WeaponType::HasHealth } }, { ESM::Weapon::LongBladeOneHand, { /* short group */ "1h", /* long group */ "weapononehand", /* sound ID */ "Item Weapon Longblade", /* attach bone */ "Weapon Bone", /* sheath bone */ "Bip01 LongBladeOneHand", /* usage skill */ ESM::Skill::LongBlade, /* weapon class*/ ESM::WeaponType::Melee, /* ammo type */ ESM::Weapon::None, /* flags */ ESM::WeaponType::HasHealth } }, { ESM::Weapon::BluntOneHand, { /* short group */ "1b", /* long group */ "bluntonehand", /* sound ID */ "Item Weapon Blunt", /* attach bone */ "Weapon Bone", /* sheath bone */ "Bip01 BluntOneHand", /* usage skill */ ESM::Skill::BluntWeapon, /* weapon class*/ ESM::WeaponType::Melee, /* ammo type */ ESM::Weapon::None, /* flags */ ESM::WeaponType::HasHealth } }, { ESM::Weapon::AxeOneHand, { /* short group */ "1b", /* long group */ "bluntonehand", /* sound ID */ "Item Weapon Blunt", /* attach bone */ "Weapon Bone", /* sheath bone */ "Bip01 LongBladeOneHand", /* usage skill */ ESM::Skill::Axe, /* weapon class*/ ESM::WeaponType::Melee, /* ammo type */ ESM::Weapon::None, /* flags */ ESM::WeaponType::HasHealth } }, { ESM::Weapon::LongBladeTwoHand, { /* short group */ "2c", /* long group */ "weapontwohand", /* sound ID */ "Item Weapon Longblade", /* attach bone */ "Weapon Bone", /* sheath bone */ "Bip01 LongBladeTwoClose", /* usage skill */ ESM::Skill::LongBlade, /* weapon class*/ ESM::WeaponType::Melee, /* ammo type */ ESM::Weapon::None, /* flags */ ESM::WeaponType::HasHealth|ESM::WeaponType::TwoHanded } }, { ESM::Weapon::AxeTwoHand, { /* short group */ "2b", /* long group */ "blunttwohand", /* sound ID */ "Item Weapon Blunt", /* attach bone */ "Weapon Bone", /* sheath bone */ "Bip01 AxeTwoClose", /* usage skill */ ESM::Skill::Axe, /* weapon class*/ ESM::WeaponType::Melee, /* ammo type */ ESM::Weapon::None, /* flags */ ESM::WeaponType::HasHealth|ESM::WeaponType::TwoHanded } }, { ESM::Weapon::BluntTwoClose, { /* short group */ "2b", /* long group */ "blunttwohand", /* sound ID */ "Item Weapon Blunt", /* attach bone */ "Weapon Bone", /* sheath bone */ "Bip01 BluntTwoClose", /* usage skill */ ESM::Skill::BluntWeapon, /* weapon class*/ ESM::WeaponType::Melee, /* ammo type */ ESM::Weapon::None, /* flags */ ESM::WeaponType::HasHealth|ESM::WeaponType::TwoHanded } }, { ESM::Weapon::BluntTwoWide, { /* short group */ "2w", /* long group */ "weapontwowide", /* sound ID */ "Item Weapon Blunt", /* attach bone */ "Weapon Bone", /* sheath bone */ "Bip01 BluntTwoWide", /* usage skill */ ESM::Skill::BluntWeapon, /* weapon class*/ ESM::WeaponType::Melee, /* ammo type */ ESM::Weapon::None, /* flags */ ESM::WeaponType::HasHealth|ESM::WeaponType::TwoHanded } }, { ESM::Weapon::SpearTwoWide, { /* short group */ "2w", /* long group */ "weapontwowide", /* sound ID */ "Item Weapon Spear", /* attach bone */ "Weapon Bone", /* sheath bone */ "Bip01 SpearTwoWide", /* usage skill */ ESM::Skill::Spear, /* weapon class*/ ESM::WeaponType::Melee, /* ammo type */ ESM::Weapon::None, /* flags */ ESM::WeaponType::HasHealth|ESM::WeaponType::TwoHanded } }, { ESM::Weapon::MarksmanBow, { /* short group */ "bow", /* long group */ "bowandarrow", /* sound ID */ "Item Weapon Bow", /* attach bone */ "Weapon Bone Left", /* sheath bone */ "Bip01 MarksmanBow", /* usage skill */ ESM::Skill::Marksman, /* weapon class*/ ESM::WeaponType::Ranged, /* ammo type */ ESM::Weapon::Arrow, /* flags */ ESM::WeaponType::HasHealth|ESM::WeaponType::TwoHanded } }, { ESM::Weapon::MarksmanCrossbow, { /* short group */ "crossbow", /* long group */ "crossbow", /* sound ID */ "Item Weapon Crossbow", /* attach bone */ "Weapon Bone", /* sheath bone */ "Bip01 MarksmanCrossbow", /* usage skill */ ESM::Skill::Marksman, /* weapon class*/ ESM::WeaponType::Ranged, /* ammo type */ ESM::Weapon::Bolt, /* flags */ ESM::WeaponType::HasHealth|ESM::WeaponType::TwoHanded } }, { ESM::Weapon::MarksmanThrown, { /* short group */ "1t", /* long group */ "throwweapon", /* sound ID */ "Item Weapon Blunt", /* attach bone */ "Weapon Bone", /* sheath bone */ "Bip01 MarksmanThrown", /* usage skill */ ESM::Skill::Marksman, /* weapon class*/ ESM::WeaponType::Thrown, /* ammo type */ ESM::Weapon::None, /* flags */ 0 } }, { ESM::Weapon::Arrow, { /* short group */ "", /* long group */ "", /* sound ID */ "Item Ammo", /* attach bone */ "Bip01 Arrow", /* sheath bone */ "", /* usage skill */ ESM::Skill::Marksman, /* weapon class*/ ESM::WeaponType::Ammo, /* ammo type */ ESM::Weapon::None, /* flags */ 0 } }, { ESM::Weapon::Bolt, { /* short group */ "", /* long group */ "", /* sound ID */ "Item Ammo", /* attach bone */ "ArrowBone", /* sheath bone */ "", /* usage skill */ ESM::Skill::Marksman, /* weapon class*/ ESM::WeaponType::Ammo, /* ammo type */ ESM::Weapon::None, /* flags */ 0 } } }; MWWorld::ContainerStoreIterator getActiveWeapon(MWWorld::Ptr actor, int *weaptype); const ESM::WeaponType* getWeaponType(const int weaponType); } #endif ================================================ FILE: apps/openmw/mwmp/ActorList.cpp ================================================ #include "ActorList.hpp" #include "Main.hpp" #include "Networking.hpp" #include "LocalPlayer.hpp" #include "MechanicsHelper.hpp" #include "../mwworld/class.hpp" #include using namespace mwmp; ActorList::ActorList() { } ActorList::~ActorList() { } Networking *ActorList::getNetworking() { return mwmp::Main::get().getNetworking(); } void ActorList::reset() { cell.blank(); baseActors.clear(); positionActors.clear(); animFlagsActors.clear(); animPlayActors.clear(); speechActors.clear(); statsDynamicActors.clear(); deathActors.clear(); equipmentActors.clear(); aiActors.clear(); attackActors.clear(); castActors.clear(); cellChangeActors.clear(); guid = mwmp::Main::get().getNetworking()->getLocalPlayer()->guid; } void ActorList::addActor(BaseActor baseActor) { baseActors.push_back(baseActor); } void ActorList::addPositionActor(BaseActor baseActor) { positionActors.push_back(baseActor); } void ActorList::addAnimFlagsActor(BaseActor baseActor) { animFlagsActors.push_back(baseActor); } void ActorList::addAnimPlayActor(BaseActor baseActor) { animPlayActors.push_back(baseActor); } void ActorList::addSpeechActor(BaseActor baseActor) { speechActors.push_back(baseActor); } void ActorList::addStatsDynamicActor(BaseActor baseActor) { statsDynamicActors.push_back(baseActor); } void ActorList::addDeathActor(BaseActor baseActor) { deathActors.push_back(baseActor); } void ActorList::addEquipmentActor(BaseActor baseActor) { equipmentActors.push_back(baseActor); } void ActorList::addAiActor(BaseActor baseActor) { aiActors.push_back(baseActor); } void ActorList::addAiActor(const MWWorld::Ptr& actorPtr, const MWWorld::Ptr& targetPtr, unsigned int aiAction) { mwmp::BaseActor baseActor; baseActor.refNum = actorPtr.getCellRef().getRefNum().mIndex; baseActor.mpNum = actorPtr.getCellRef().getMpNum(); baseActor.aiAction = aiAction; baseActor.aiTarget = MechanicsHelper::getTarget(targetPtr); LOG_MESSAGE_SIMPLE(TimedLog::LOG_INFO, "Preparing to send ID_ACTOR_AI about %s %i-%i\n- action: %i", actorPtr.getCellRef().getRefId().c_str(), baseActor.refNum, baseActor.mpNum, aiAction); if (baseActor.aiTarget.isPlayer) { LOG_MESSAGE_SIMPLE(TimedLog::LOG_INFO, "- Has player target %s", targetPtr.getClass().getName(targetPtr).c_str()); } else { LOG_MESSAGE_SIMPLE(TimedLog::LOG_INFO, "- Has actor target %s %i-%i", targetPtr.getCellRef().getRefId().c_str(), baseActor.aiTarget.refNum, baseActor.aiTarget.mpNum); } addAiActor(baseActor); } void ActorList::addAttackActor(BaseActor baseActor) { attackActors.push_back(baseActor); } void ActorList::addAttackActor(const MWWorld::Ptr& actorPtr, const mwmp::Attack &attack) { mwmp::BaseActor baseActor; baseActor.refNum = actorPtr.getCellRef().getRefNum().mIndex; baseActor.mpNum = actorPtr.getCellRef().getMpNum(); baseActor.attack = attack; attackActors.push_back(baseActor); } void ActorList::addCastActor(BaseActor baseActor) { castActors.push_back(baseActor); } void ActorList::addCellChangeActor(BaseActor baseActor) { cellChangeActors.push_back(baseActor); } void ActorList::sendPositionActors() { if (positionActors.size() > 0) { baseActors = positionActors; Main::get().getNetworking()->getActorPacket(ID_ACTOR_POSITION)->setActorList(this); Main::get().getNetworking()->getActorPacket(ID_ACTOR_POSITION)->Send(); } } void ActorList::sendAnimFlagsActors() { if (animFlagsActors.size() > 0) { baseActors = animFlagsActors; Main::get().getNetworking()->getActorPacket(ID_ACTOR_ANIM_FLAGS)->setActorList(this); Main::get().getNetworking()->getActorPacket(ID_ACTOR_ANIM_FLAGS)->Send(); } } void ActorList::sendAnimPlayActors() { if (animPlayActors.size() > 0) { baseActors = animPlayActors; Main::get().getNetworking()->getActorPacket(ID_ACTOR_ANIM_PLAY)->setActorList(this); Main::get().getNetworking()->getActorPacket(ID_ACTOR_ANIM_PLAY)->Send(); } } void ActorList::sendSpeechActors() { if (speechActors.size() > 0) { baseActors = speechActors; Main::get().getNetworking()->getActorPacket(ID_ACTOR_SPEECH)->setActorList(this); Main::get().getNetworking()->getActorPacket(ID_ACTOR_SPEECH)->Send(); } } void ActorList::sendStatsDynamicActors() { if (statsDynamicActors.size() > 0) { baseActors = statsDynamicActors; Main::get().getNetworking()->getActorPacket(ID_ACTOR_STATS_DYNAMIC)->setActorList(this); Main::get().getNetworking()->getActorPacket(ID_ACTOR_STATS_DYNAMIC)->Send(); } } void ActorList::sendDeathActors() { if (deathActors.size() > 0) { baseActors = deathActors; Main::get().getNetworking()->getActorPacket(ID_ACTOR_DEATH)->setActorList(this); Main::get().getNetworking()->getActorPacket(ID_ACTOR_DEATH)->Send(); } } void ActorList::sendEquipmentActors() { if (equipmentActors.size() > 0) { baseActors = equipmentActors; Main::get().getNetworking()->getActorPacket(ID_ACTOR_EQUIPMENT)->setActorList(this); Main::get().getNetworking()->getActorPacket(ID_ACTOR_EQUIPMENT)->Send(); } } void ActorList::sendAiActors() { if (aiActors.size() > 0) { baseActors = aiActors; Main::get().getNetworking()->getActorPacket(ID_ACTOR_AI)->setActorList(this); Main::get().getNetworking()->getActorPacket(ID_ACTOR_AI)->Send(); } } void ActorList::sendAttackActors() { if (attackActors.size() > 0) { baseActors = attackActors; Main::get().getNetworking()->getActorPacket(ID_ACTOR_ATTACK)->setActorList(this); Main::get().getNetworking()->getActorPacket(ID_ACTOR_ATTACK)->Send(); } } void ActorList::sendCastActors() { if (castActors.size() > 0) { baseActors = castActors; Main::get().getNetworking()->getActorPacket(ID_ACTOR_CAST)->setActorList(this); Main::get().getNetworking()->getActorPacket(ID_ACTOR_CAST)->Send(); } } void ActorList::sendCellChangeActors() { if (cellChangeActors.size() > 0) { baseActors = cellChangeActors; Main::get().getNetworking()->getActorPacket(ID_ACTOR_CELL_CHANGE)->setActorList(this); Main::get().getNetworking()->getActorPacket(ID_ACTOR_CELL_CHANGE)->Send(); } } void ActorList::sendActorsInCell(MWWorld::CellStore* cellStore) { reset(); cell = *cellStore->getCell(); action = BaseActorList::SET; for (auto &ref : cellStore->getNpcs()->mList) { MWWorld::Ptr ptr(&ref, 0); // If this Ptr is lacking a unique index, ignore it if (ptr.getCellRef().getRefNum().mIndex == 0 && ptr.getCellRef().getMpNum() == 0) continue; BaseActor actor; actor.refId = ptr.getCellRef().getRefId(); actor.refNum = ptr.getCellRef().getRefNum().mIndex; actor.mpNum = ptr.getCellRef().getMpNum(); addActor(actor); } for (auto &ref : cellStore->getCreatures()->mList) { MWWorld::Ptr ptr(&ref, 0); // If this Ptr is lacking a unique index, ignore it if (ptr.getCellRef().getRefNum().mIndex == 0 && ptr.getCellRef().getMpNum() == 0) continue; BaseActor actor; actor.refId = ptr.getCellRef().getRefId(); actor.refNum = ptr.getCellRef().getRefNum().mIndex; actor.mpNum = ptr.getCellRef().getMpNum(); addActor(actor); } mwmp::Main::get().getNetworking()->getActorPacket(ID_ACTOR_LIST)->setActorList(this); mwmp::Main::get().getNetworking()->getActorPacket(ID_ACTOR_LIST)->Send(); } ================================================ FILE: apps/openmw/mwmp/ActorList.hpp ================================================ #ifndef OPENMW_ACTORLIST_HPP #define OPENMW_ACTORLIST_HPP #include #include "../mwworld/cellstore.hpp" #include #include "LocalActor.hpp" namespace mwmp { class Networking; class ActorList : public BaseActorList { public: ActorList(); virtual ~ActorList(); void reset(); void addActor(BaseActor baseActor); void addPositionActor(BaseActor baseActor); void addAnimFlagsActor(BaseActor baseActor); void addAnimPlayActor(BaseActor baseActor); void addSpeechActor(BaseActor baseActor); void addStatsDynamicActor(BaseActor baseActor); void addDeathActor(BaseActor baseActor); void addEquipmentActor(BaseActor baseActor); void addAiActor(BaseActor baseActor); void addAiActor(const MWWorld::Ptr& actorPtr, const MWWorld::Ptr& targetPtr, unsigned int aiAction); void addAttackActor(BaseActor baseActor); void addAttackActor(const MWWorld::Ptr& actorPtr, const mwmp::Attack &attack); void addCastActor(BaseActor baseActor); void addCellChangeActor(BaseActor baseActor); void sendPositionActors(); void sendAnimFlagsActors(); void sendAnimPlayActors(); void sendSpeechActors(); void sendStatsDynamicActors(); void sendDeathActors(); void sendEquipmentActors(); void sendAiActors(); void sendAttackActors(); void sendCastActors(); void sendCellChangeActors(); void sendActorsInCell(MWWorld::CellStore* cellStore); private: Networking *getNetworking(); std::vector positionActors; std::vector animFlagsActors; std::vector animPlayActors; std::vector speechActors; std::vector statsDynamicActors; std::vector deathActors; std::vector equipmentActors; std::vector aiActors; std::vector attackActors; std::vector castActors; std::vector cellChangeActors; }; } #endif //OPENMW_ACTORLIST_HPP ================================================ FILE: apps/openmw/mwmp/Cell.cpp ================================================ #include #include #include "../mwbase/environment.hpp" #include "../mwworld/class.hpp" #include "../mwworld/livecellref.hpp" #include "../mwworld/worldimp.hpp" #include "Cell.hpp" #include "Main.hpp" #include "Networking.hpp" #include "LocalPlayer.hpp" #include "CellController.hpp" #include "MechanicsHelper.hpp" using namespace mwmp; mwmp::Cell::Cell(MWWorld::CellStore* cellStore) { store = cellStore; shouldInitializeActors = false; updateTimer = 0; } Cell::~Cell() { } void Cell::updateLocal(bool forceUpdate) { if (localActors.empty()) return; const float timeoutSec = 0.025; if (!forceUpdate && (updateTimer += MWBase::Environment::get().getFrameDuration()) < timeoutSec) return; else updateTimer = 0; CellController *cellController = Main::get().getCellController(); ActorList *actorList = mwmp::Main::get().getNetworking()->getActorList(); actorList->reset(); actorList->cell = *store->getCell(); for (auto it = localActors.begin(); it != localActors.end();) { LocalActor *actor = it->second; MWWorld::CellStore *newStore = actor->getPtr().getCell(); if (newStore != store) { actor->updateCell(); std::string mapIndex = it->first; // If the cell this actor has moved to is under our authority, move them to it if (cellController->hasLocalAuthority(actor->cell)) { LOG_APPEND(TimedLog::LOG_VERBOSE, "- Moving LocalActor %s to our authority in %s", mapIndex.c_str(), actor->cell.getShortDescription().c_str()); Cell *newCell = cellController->getCell(actor->cell); newCell->localActors[mapIndex] = actor; cellController->setLocalActorRecord(mapIndex, newCell->getShortDescription()); } else { LOG_APPEND(TimedLog::LOG_VERBOSE, "- Deleting LocalActor %s which is no longer under our authority", mapIndex.c_str(), getShortDescription().c_str()); cellController->removeLocalActorRecord(mapIndex); delete actor; } localActors.erase(it++); } else { if (actor->getPtr().getRefData().isEnabled()) { if (actor->getPtr().getRefData().isDeleted()) { std::string mapIndex = it->first; LOG_APPEND(TimedLog::LOG_VERBOSE, "- Deleting LocalActor %s whose reference has been deleted", mapIndex.c_str(), getShortDescription().c_str()); cellController->removeLocalActorRecord(mapIndex); delete actor; localActors.erase(it++); } else { // Forcibly update this local actor if its data has never been sent before; // otherwise, use the current forceUpdate value actor->update(actor->hasSentData ? forceUpdate : true); } } ++it; } } actorList->sendPositionActors(); actorList->sendAnimFlagsActors(); actorList->sendAnimPlayActors(); actorList->sendSpeechActors(); actorList->sendDeathActors(); actorList->sendStatsDynamicActors(); actorList->sendEquipmentActors(); actorList->sendAttackActors(); actorList->sendCastActors(); actorList->sendCellChangeActors(); } void Cell::updateDedicated(float dt) { if (dedicatedActors.empty()) return; for (auto &actor : dedicatedActors) actor.second->update(dt); // Are we the authority over this cell? If so, uninitialize DedicatedActors // after the above update if (hasLocalAuthority()) uninitializeDedicatedActors(); } void Cell::readPositions(ActorList& actorList) { initializeDedicatedActors(actorList); if (dedicatedActors.empty()) return; for (const auto &baseActor : actorList.baseActors) { std::string mapIndex = Main::get().getCellController()->generateMapIndex(baseActor); if (dedicatedActors.count(mapIndex) > 0) { DedicatedActor *actor = dedicatedActors[mapIndex]; actor->position = baseActor.position; actor->direction = baseActor.direction; if (!actor->hasPositionData) { actor->hasPositionData = true; // If this is our first packet about this actor's position, force an update // now instead of waiting for its frame // // That way, if this actor is about to become a LocalActor, initial data about it // received from the server still gets set actor->setPosition(); } } } } void Cell::readAnimFlags(ActorList& actorList) { for (const auto &baseActor : actorList.baseActors) { std::string mapIndex = Main::get().getCellController()->generateMapIndex(baseActor); if (dedicatedActors.count(mapIndex) > 0) { DedicatedActor *actor = dedicatedActors[mapIndex]; actor->movementFlags = baseActor.movementFlags; actor->drawState = baseActor.drawState; actor->isFlying = baseActor.isFlying; } } } void Cell::readAnimPlay(ActorList& actorList) { for (const auto &baseActor : actorList.baseActors) { std::string mapIndex = Main::get().getCellController()->generateMapIndex(baseActor); if (dedicatedActors.count(mapIndex) > 0) { DedicatedActor *actor = dedicatedActors[mapIndex]; actor->animation.groupname = baseActor.animation.groupname; actor->animation.mode = baseActor.animation.mode; actor->animation.count = baseActor.animation.count; actor->animation.persist = baseActor.animation.persist; actor->playAnimation(); } } } void Cell::readStatsDynamic(ActorList& actorList) { initializeDedicatedActors(actorList); if (dedicatedActors.empty()) return; for (const auto &baseActor : actorList.baseActors) { std::string mapIndex = Main::get().getCellController()->generateMapIndex(baseActor); if (dedicatedActors.count(mapIndex) > 0) { DedicatedActor *actor = dedicatedActors[mapIndex]; actor->creatureStats = baseActor.creatureStats; if (!actor->hasStatsDynamicData) { actor->hasStatsDynamicData = true; // If this is our first packet about this actor's dynamic stats, force an update // now instead of waiting for its frame // // That way, if this actor is about to become a LocalActor, initial data about it // received from the server still gets set actor->setStatsDynamic(); } } } } void Cell::readDeath(ActorList& actorList) { initializeDedicatedActors(actorList); if (dedicatedActors.empty()) return; for (const auto &baseActor : actorList.baseActors) { std::string mapIndex = Main::get().getCellController()->generateMapIndex(baseActor); if (dedicatedActors.count(mapIndex) > 0) { DedicatedActor *actor = dedicatedActors[mapIndex]; actor->creatureStats.mDead = true; actor->creatureStats.mDynamic[0].mCurrent = 0; Main::get().getCellController()->setQueuedDeathState(actor->getPtr(), baseActor.deathState); LOG_MESSAGE_SIMPLE(TimedLog::LOG_INFO, "Received ID_ACTOR_DEATH about %s %i-%i in cell %s\n- deathState: %d\n-isInstantDeath: %s", actor->refId.c_str(), actor->refNum, actor->mpNum, getShortDescription().c_str(), baseActor.deathState, baseActor.isInstantDeath ? "true" : "false"); if (baseActor.isInstantDeath) { actor->getPtr().getClass().getCreatureStats(actor->getPtr()).setDeathAnimationFinished(true); MWBase::Environment::get().getWorld()->enableActorCollision(actor->getPtr(), false); } } } } void Cell::readEquipment(ActorList& actorList) { initializeDedicatedActors(actorList); if (dedicatedActors.empty()) return; for (const auto &baseActor : actorList.baseActors) { std::string mapIndex = Main::get().getCellController()->generateMapIndex(baseActor); if (dedicatedActors.count(mapIndex) > 0) { DedicatedActor *actor = dedicatedActors[mapIndex]; for (int slot = 0; slot < 19; ++slot) actor->equipmentItems[slot] = baseActor.equipmentItems[slot]; actor->setEquipment(); } } if (hasLocalAuthority()) uninitializeDedicatedActors(actorList); } void Cell::readSpeech(ActorList& actorList) { initializeDedicatedActors(actorList); if (dedicatedActors.empty()) return; for (const auto &baseActor : actorList.baseActors) { std::string mapIndex = Main::get().getCellController()->generateMapIndex(baseActor); if (dedicatedActors.count(mapIndex) > 0) { DedicatedActor *actor = dedicatedActors[mapIndex]; actor->sound = baseActor.sound; actor->playSound(); } } if (hasLocalAuthority()) uninitializeDedicatedActors(actorList); } void Cell::readSpellsActive(ActorList& actorList) { initializeDedicatedActors(actorList); if (dedicatedActors.empty()) return; for (const auto& baseActor : actorList.baseActors) { std::string mapIndex = Main::get().getCellController()->generateMapIndex(baseActor); if (dedicatedActors.count(mapIndex) > 0) { DedicatedActor* actor = dedicatedActors[mapIndex]; actor->spellsActiveChanges = baseActor.spellsActiveChanges; int spellsActiveAction = baseActor.spellsActiveChanges.action; if (spellsActiveAction == SpellsActiveChanges::ADD) actor->addSpellsActive(); else if (spellsActiveAction == SpellsActiveChanges::REMOVE) actor->removeSpellsActive(); else actor->setSpellsActive(); } } if (hasLocalAuthority()) uninitializeDedicatedActors(actorList); } void Cell::readAi(ActorList& actorList) { initializeDedicatedActors(actorList); if (dedicatedActors.empty()) return; for (const auto &baseActor : actorList.baseActors) { std::string mapIndex = Main::get().getCellController()->generateMapIndex(baseActor); if (dedicatedActors.count(mapIndex) > 0) { DedicatedActor *actor = dedicatedActors[mapIndex]; actor->aiAction = baseActor.aiAction; actor->aiDistance = baseActor.aiDistance; actor->aiDuration = baseActor.aiDuration; actor->aiShouldRepeat = baseActor.aiShouldRepeat; actor->aiCoordinates = baseActor.aiCoordinates; actor->hasAiTarget = baseActor.hasAiTarget; actor->aiTarget = baseActor.aiTarget; actor->setAi(); } } if (hasLocalAuthority()) uninitializeDedicatedActors(actorList); } void Cell::readAttack(ActorList& actorList) { for (const auto &baseActor : actorList.baseActors) { std::string mapIndex = Main::get().getCellController()->generateMapIndex(baseActor); if (dedicatedActors.count(mapIndex) > 0) { LOG_MESSAGE_SIMPLE(TimedLog::LOG_INFO, "Reading ActorAttack about %s", mapIndex.c_str()); DedicatedActor *actor = dedicatedActors[mapIndex]; actor->attack = baseActor.attack; MechanicsHelper::processAttack(actor->attack, actor->getPtr()); } } } void Cell::readCast(ActorList& actorList) { for (const auto &baseActor : actorList.baseActors) { std::string mapIndex = Main::get().getCellController()->generateMapIndex(baseActor); if (dedicatedActors.count(mapIndex) > 0) { LOG_MESSAGE_SIMPLE(TimedLog::LOG_INFO, "Reading ActorCast about %s", mapIndex.c_str()); DedicatedActor *actor = dedicatedActors[mapIndex]; actor->cast = baseActor.cast; // Set the correct drawState here if we've somehow we've missed a previous // AnimFlags packet if (actor->drawState != MWMechanics::DrawState_::DrawState_Spell) { actor->drawState = MWMechanics::DrawState_::DrawState_Spell; actor->setAnimFlags(); } MechanicsHelper::processCast(actor->cast, actor->getPtr()); } } } void Cell::readCellChange(ActorList& actorList) { initializeDedicatedActors(actorList); if (dedicatedActors.empty()) return; CellController *cellController = Main::get().getCellController(); for (const auto &baseActor : actorList.baseActors) { std::string mapIndex = Main::get().getCellController()->generateMapIndex(baseActor); // Is a packet mistakenly moving the actor to the cell it's already in? If so, ignore it if (Misc::StringUtils::ciEqual(getShortDescription(), baseActor.cell.getShortDescription())) { LOG_MESSAGE_SIMPLE(TimedLog::LOG_WARN, "Server says DedicatedActor %s moved to %s, but it was already there", mapIndex.c_str(), getShortDescription().c_str()); continue; } if (dedicatedActors.count(mapIndex) > 0) { DedicatedActor *dedicatedActor = dedicatedActors[mapIndex]; dedicatedActor->cell = baseActor.cell; dedicatedActor->position = baseActor.position; dedicatedActor->direction = baseActor.direction; LOG_MESSAGE_SIMPLE(TimedLog::LOG_VERBOSE, "Server says DedicatedActor %s moved to %s", mapIndex.c_str(), dedicatedActor->cell.getShortDescription().c_str()); MWWorld::CellStore *newStore = cellController->getCellStore(dedicatedActor->cell); dedicatedActor->setCell(newStore); // If the cell this actor has moved to is active and not under our authority, move them to it if (cellController->isActiveWorldCell(dedicatedActor->cell) && !cellController->hasLocalAuthority(dedicatedActor->cell)) { LOG_APPEND(TimedLog::LOG_VERBOSE, "- Moving DedicatedActor %s to our active cell %s", mapIndex.c_str(), dedicatedActor->cell.getShortDescription().c_str()); cellController->initializeCell(dedicatedActor->cell); Cell *newCell = cellController->getCell(dedicatedActor->cell); newCell->dedicatedActors[mapIndex] = dedicatedActor; cellController->setDedicatedActorRecord(mapIndex, newCell->getShortDescription()); } else { if (cellController->hasLocalAuthority(dedicatedActor->cell)) { LOG_APPEND(TimedLog::LOG_VERBOSE, "- Creating new LocalActor based on %s in %s", mapIndex.c_str(), dedicatedActor->cell.getShortDescription().c_str()); Cell *newCell = cellController->getCell(dedicatedActor->cell); LocalActor *localActor = new LocalActor(); localActor->cell = dedicatedActor->cell; localActor->setPtr(dedicatedActor->getPtr()); localActor->position = dedicatedActor->position; localActor->direction = dedicatedActor->direction; localActor->movementFlags = dedicatedActor->movementFlags; localActor->drawState = dedicatedActor->drawState; localActor->isFlying = dedicatedActor->isFlying; localActor->creatureStats = dedicatedActor->creatureStats; newCell->localActors[mapIndex] = localActor; cellController->setLocalActorRecord(mapIndex, newCell->getShortDescription()); } LOG_APPEND(TimedLog::LOG_VERBOSE, "- Deleting DedicatedActor %s which is no longer needed", mapIndex.c_str(), getShortDescription().c_str()); cellController->removeDedicatedActorRecord(mapIndex); delete dedicatedActor; } dedicatedActors.erase(mapIndex); } } } void Cell::initializeLocalActor(const MWWorld::Ptr& ptr) { std::string mapIndex = Main::get().getCellController()->generateMapIndex(ptr); LOG_APPEND(TimedLog::LOG_VERBOSE, "- Initializing LocalActor %s in %s", mapIndex.c_str(), getShortDescription().c_str()); LocalActor *actor = new LocalActor(); actor->cell = *store->getCell(); actor->setPtr(ptr); localActors[mapIndex] = actor; Main::get().getCellController()->setLocalActorRecord(mapIndex, getShortDescription()); LOG_APPEND(TimedLog::LOG_VERBOSE, "- Successfully initialized LocalActor %s in %s", mapIndex.c_str(), getShortDescription().c_str()); } void Cell::initializeLocalActors() { LOG_MESSAGE_SIMPLE(TimedLog::LOG_VERBOSE, "Initializing LocalActors in %s", getShortDescription().c_str()); for (const auto &mergedRef : store->getMergedRefs()) { if (mergedRef->mClass->isActor()) { MWWorld::Ptr ptr(mergedRef, store); // If this Ptr is lacking a unique index, ignore it if (ptr.getCellRef().getRefNum().mIndex == 0 && ptr.getCellRef().getMpNum() == 0) continue; // If this Ptr is disabled or deleted, ignore it if (!ptr.getRefData().isEnabled() || ptr.getRefData().isDeleted()) continue; std::string mapIndex = Main::get().getCellController()->generateMapIndex(ptr); // Only initialize this actor if it isn't already initialized if (localActors.count(mapIndex) == 0) initializeLocalActor(ptr); } } LOG_APPEND(TimedLog::LOG_VERBOSE, "- Successfully initialized LocalActors in %s", getShortDescription().c_str()); } void Cell::initializeDedicatedActor(const MWWorld::Ptr& ptr) { std::string mapIndex = Main::get().getCellController()->generateMapIndex(ptr); LOG_APPEND(TimedLog::LOG_VERBOSE, "- Initializing DedicatedActor %s in %s", mapIndex.c_str(), getShortDescription().c_str()); DedicatedActor *actor = new DedicatedActor(); actor->cell = *store->getCell(); actor->setPtr(ptr); dedicatedActors[mapIndex] = actor; Main::get().getCellController()->setDedicatedActorRecord(mapIndex, getShortDescription()); LOG_APPEND(TimedLog::LOG_VERBOSE, "- Successfully initialized DedicatedActor %s in %s", mapIndex.c_str(), getShortDescription().c_str()); } void Cell::initializeDedicatedActors(ActorList& actorList) { for (const auto &baseActor : actorList.baseActors) { std::string mapIndex = Main::get().getCellController()->generateMapIndex(baseActor); // If this key doesn't exist, create it if (dedicatedActors.count(mapIndex) == 0) { MWWorld::Ptr ptrFound = store->searchExact(baseActor.refNum, baseActor.mpNum, baseActor.refId, true); if (!ptrFound) continue; initializeDedicatedActor(ptrFound); } } } void Cell::uninitializeLocalActors() { for (const auto &actor : localActors) { Main::get().getCellController()->removeLocalActorRecord(actor.first); delete actor.second; } localActors.clear(); } void Cell::uninitializeDedicatedActors(ActorList& actorList) { for (const auto &baseActor : actorList.baseActors) { std::string mapIndex = Main::get().getCellController()->generateMapIndex(baseActor); Main::get().getCellController()->removeDedicatedActorRecord(mapIndex); delete dedicatedActors.at(mapIndex); dedicatedActors.erase(mapIndex); } } void Cell::uninitializeDedicatedActors() { for (const auto &actor : dedicatedActors) { Main::get().getCellController()->removeDedicatedActorRecord(actor.first); delete actor.second; } dedicatedActors.clear(); } LocalActor *Cell::getLocalActor(std::string actorIndex) { return localActors.at(actorIndex); } DedicatedActor *Cell::getDedicatedActor(std::string actorIndex) { return dedicatedActors.at(actorIndex); } bool Cell::hasLocalAuthority() { return authorityGuid == Main::get().getLocalPlayer()->guid; } void Cell::setAuthority(const RakNet::RakNetGUID& guid) { authorityGuid = guid; } MWWorld::CellStore *Cell::getCellStore() { return store; } std::string Cell::getShortDescription() { return store->getCell()->getShortDescription(); } ================================================ FILE: apps/openmw/mwmp/Cell.hpp ================================================ #ifndef OPENMW_MPCELL_HPP #define OPENMW_MPCELL_HPP #include "ActorList.hpp" #include "LocalActor.hpp" #include "DedicatedActor.hpp" #include "../mwworld/cellstore.hpp" namespace mwmp { class Cell { public: Cell(MWWorld::CellStore* cellStore); virtual ~Cell(); void updateLocal(bool forceUpdate); void updateDedicated(float dt); void readPositions(ActorList& actorList); void readAnimFlags(ActorList& actorList); void readAnimPlay(ActorList& actorList); void readStatsDynamic(ActorList& actorList); void readDeath(ActorList& actorList); void readEquipment(ActorList& actorList); void readSpeech(ActorList& actorList); void readSpellsActive(ActorList& actorList); void readAi(ActorList& actorList); void readAttack(ActorList& actorList); void readCast(ActorList& actorList); void readCellChange(ActorList& actorList); void initializeLocalActor(const MWWorld::Ptr& ptr); void initializeLocalActors(); void initializeDedicatedActor(const MWWorld::Ptr& ptr); void initializeDedicatedActors(ActorList& actorList); void uninitializeLocalActors(); void uninitializeDedicatedActors(ActorList& actorList); void uninitializeDedicatedActors(); virtual LocalActor *getLocalActor(std::string actorIndex); virtual DedicatedActor *getDedicatedActor(std::string actorIndex); bool hasLocalAuthority(); void setAuthority(const RakNet::RakNetGUID& guid); MWWorld::CellStore* getCellStore(); std::string getShortDescription(); bool shouldInitializeActors; private: MWWorld::CellStore* store; RakNet::RakNetGUID authorityGuid; std::map localActors; std::map dedicatedActors; float updateTimer; }; } #endif //OPENMW_MPCELL_HPP ================================================ FILE: apps/openmw/mwmp/CellController.cpp ================================================ #include #include #include #include #include "../mwbase/environment.hpp" #include "../mwworld/containerstore.hpp" #include "../mwworld/class.hpp" #include "../mwworld/worldimp.hpp" #include "CellController.hpp" #include "Main.hpp" #include "LocalActor.hpp" #include "LocalPlayer.hpp" using namespace mwmp; std::map CellController::cellsInitialized; std::map CellController::localActorsToCells; std::map CellController::dedicatedActorsToCells; std::map CellController::queuedDeathStates; mwmp::CellController::CellController() { } CellController::~CellController() { } void CellController::updateLocal(bool forceUpdate) { MWBase::World* world = MWBase::Environment::get().getWorld(); // Loop through Cells, deleting inactive ones and updating LocalActors in active ones for (auto it = cellsInitialized.begin(); it != cellsInitialized.end();) { mwmp::Cell *mpCell = it->second; if (mpCell->getCellStore() == nullptr || mpCell->getCellStore()->getCell() == nullptr || !world->isCellActive(*mpCell->getCellStore()->getCell())) { mpCell->uninitializeLocalActors(); mpCell->uninitializeDedicatedActors(); delete it->second; cellsInitialized.erase(it++); } else { mpCell->updateLocal(forceUpdate); ++it; } } // If there are cellsInitialized remaining, loop through them and initialize new LocalActors for eligible ones // // // Note: This cannot be combined with the above loop because initializing LocalActors in a Cell before they are // deleted from their previous one can make their records stay deleted if (cellsInitialized.size() > 0) { for (auto& cell : cellsInitialized) { mwmp::Cell* mpCell = cell.second; if (mpCell->shouldInitializeActors == true) { mpCell->shouldInitializeActors = false; mpCell->initializeLocalActors(); } } } // Otherwise, disable the DetourNavigator for advanced pathfinding for the time being else { world->getNavigator()->setUpdatesEnabled(false); } } void CellController::updateDedicated(float dt) { for (const auto &cell : cellsInitialized) cell.second->updateDedicated(dt); } void CellController::initializeCell(const ESM::Cell& cell) { std::string mapIndex = cell.getShortDescription(); // If this key doesn't exist, create it if (cellsInitialized.count(mapIndex) == 0) { LOG_MESSAGE_SIMPLE(TimedLog::LOG_INFO, "Initializing mwmp::Cell %s", cell.getShortDescription().c_str()); MWWorld::CellStore *cellStore = getCellStore(cell); if (!cellStore) return; mwmp::Cell *mpCell = new mwmp::Cell(cellStore); cellsInitialized[mapIndex] = mpCell; LOG_APPEND(TimedLog::LOG_VERBOSE, "- Successfully initialized mwmp::Cell %s", cell.getShortDescription().c_str()); } } void CellController::uninitializeCell(const ESM::Cell& cell) { std::string mapIndex = cell.getShortDescription(); // If this key exists, erase the key-value pair from the map if (cellsInitialized.count(mapIndex) > 0) { mwmp::Cell* mpCell = cellsInitialized.at(mapIndex); mpCell->uninitializeLocalActors(); mpCell->uninitializeDedicatedActors(); delete cellsInitialized.at(mapIndex); cellsInitialized.erase(mapIndex); } } void CellController::uninitializeCells() { if (cellsInitialized.size() > 0) { for (auto it = cellsInitialized.cbegin(); it != cellsInitialized.cend(); it++) { mwmp::Cell* mpCell = it->second; mpCell->uninitializeLocalActors(); mpCell->uninitializeDedicatedActors(); delete it->second; } cellsInitialized.clear(); } } void CellController::readPositions(ActorList& actorList) { std::string mapIndex = actorList.cell.getShortDescription(); initializeCell(actorList.cell); // If this now exists, send it the data if (cellsInitialized.count(mapIndex) > 0) cellsInitialized[mapIndex]->readPositions(actorList); } void CellController::readAnimFlags(ActorList& actorList) { std::string mapIndex = actorList.cell.getShortDescription(); initializeCell(actorList.cell); // If this now exists, send it the data if (cellsInitialized.count(mapIndex) > 0) cellsInitialized[mapIndex]->readAnimFlags(actorList); } void CellController::readAnimPlay(ActorList& actorList) { std::string mapIndex = actorList.cell.getShortDescription(); initializeCell(actorList.cell); // If this now exists, send it the data if (cellsInitialized.count(mapIndex) > 0) cellsInitialized[mapIndex]->readAnimPlay(actorList); } void CellController::readStatsDynamic(ActorList& actorList) { std::string mapIndex = actorList.cell.getShortDescription(); initializeCell(actorList.cell); // If this now exists, send it the data if (cellsInitialized.count(mapIndex) > 0) cellsInitialized[mapIndex]->readStatsDynamic(actorList); } void CellController::readDeath(ActorList& actorList) { std::string mapIndex = actorList.cell.getShortDescription(); initializeCell(actorList.cell); // If this now exists, send it the data if (cellsInitialized.count(mapIndex) > 0) cellsInitialized[mapIndex]->readDeath(actorList); } void CellController::readEquipment(ActorList& actorList) { std::string mapIndex = actorList.cell.getShortDescription(); initializeCell(actorList.cell); // If this now exists, send it the data if (cellsInitialized.count(mapIndex) > 0) cellsInitialized[mapIndex]->readEquipment(actorList); } void CellController::readSpeech(ActorList& actorList) { std::string mapIndex = actorList.cell.getShortDescription(); initializeCell(actorList.cell); // If this now exists, send it the data if (cellsInitialized.count(mapIndex) > 0) cellsInitialized[mapIndex]->readSpeech(actorList); } void CellController::readSpellsActive(ActorList& actorList) { std::string mapIndex = actorList.cell.getShortDescription(); initializeCell(actorList.cell); // If this now exists, send it the data if (cellsInitialized.count(mapIndex) > 0) cellsInitialized[mapIndex]->readSpellsActive(actorList); } void CellController::readAi(ActorList& actorList) { std::string mapIndex = actorList.cell.getShortDescription(); initializeCell(actorList.cell); // If this now exists, send it the data if (cellsInitialized.count(mapIndex) > 0) cellsInitialized[mapIndex]->readAi(actorList); } void CellController::readAttack(ActorList& actorList) { std::string mapIndex = actorList.cell.getShortDescription(); initializeCell(actorList.cell); // If this now exists, send it the data if (cellsInitialized.count(mapIndex) > 0) cellsInitialized[mapIndex]->readAttack(actorList); } void CellController::readCast(ActorList& actorList) { std::string mapIndex = actorList.cell.getShortDescription(); initializeCell(actorList.cell); // If this now exists, send it the data if (cellsInitialized.count(mapIndex) > 0) cellsInitialized[mapIndex]->readCast(actorList); } void CellController::readCellChange(ActorList& actorList) { std::string mapIndex = actorList.cell.getShortDescription(); initializeCell(actorList.cell); // If this now exists, send it the data if (cellsInitialized.count(mapIndex) > 0) cellsInitialized[mapIndex]->readCellChange(actorList); } bool CellController::hasQueuedDeathState(MWWorld::Ptr ptr) { std::string actorIndex = generateMapIndex(ptr); return queuedDeathStates.count(actorIndex) > 0; } unsigned int CellController::getQueuedDeathState(MWWorld::Ptr ptr) { std::string actorIndex = generateMapIndex(ptr); return queuedDeathStates[actorIndex]; } void CellController::clearQueuedDeathState(MWWorld::Ptr ptr) { std::string actorIndex = generateMapIndex(ptr); queuedDeathStates.erase(actorIndex); } void CellController::setQueuedDeathState(MWWorld::Ptr ptr, unsigned int deathState) { std::string actorIndex = generateMapIndex(ptr); queuedDeathStates[actorIndex] = deathState; } void CellController::setLocalActorRecord(std::string actorIndex, std::string cellIndex) { localActorsToCells[actorIndex] = cellIndex; } void CellController::removeLocalActorRecord(std::string actorIndex) { localActorsToCells.erase(actorIndex); } bool CellController::isLocalActor(MWWorld::Ptr ptr) { if (ptr.mRef == nullptr) return false; std::string actorIndex = generateMapIndex(ptr); return localActorsToCells.count(actorIndex) > 0; } bool CellController::isLocalActor(int refNum, int mpNum) { std::string actorIndex = generateMapIndex(refNum, mpNum); return localActorsToCells.count(actorIndex) > 0; } LocalActor *CellController::getLocalActor(MWWorld::Ptr ptr) { std::string actorIndex = generateMapIndex(ptr); std::string cellIndex = localActorsToCells.at(actorIndex); return cellsInitialized.at(cellIndex)->getLocalActor(actorIndex); } LocalActor *CellController::getLocalActor(int refNum, int mpNum) { std::string actorIndex = generateMapIndex(refNum, mpNum); std::string cellIndex = localActorsToCells.at(actorIndex); return cellsInitialized.at(cellIndex)->getLocalActor(actorIndex); } void CellController::setDedicatedActorRecord(std::string actorIndex, std::string cellIndex) { dedicatedActorsToCells[actorIndex] = cellIndex; } void CellController::removeDedicatedActorRecord(std::string actorIndex) { dedicatedActorsToCells.erase(actorIndex); } bool CellController::isDedicatedActor(MWWorld::Ptr ptr) { if (ptr.mRef == nullptr) return false; std::string actorIndex = generateMapIndex(ptr); return dedicatedActorsToCells.count(actorIndex) > 0; } bool CellController::isDedicatedActor(int refNum, int mpNum) { std::string actorIndex = generateMapIndex(refNum, mpNum); return dedicatedActorsToCells.count(actorIndex) > 0; } DedicatedActor *CellController::getDedicatedActor(MWWorld::Ptr ptr) { std::string actorIndex = generateMapIndex(ptr); std::string cellIndex = dedicatedActorsToCells.at(actorIndex); return cellsInitialized.at(cellIndex)->getDedicatedActor(actorIndex); } DedicatedActor *CellController::getDedicatedActor(int refNum, int mpNum) { std::string actorIndex = generateMapIndex(refNum, mpNum); std::string cellIndex = dedicatedActorsToCells.at(actorIndex); return cellsInitialized.at(cellIndex)->getDedicatedActor(actorIndex); } std::string CellController::generateMapIndex(int refNum, int mpNum) { std::string mapIndex = ""; mapIndex = Utils::toString(refNum) + "-" + Utils::toString(mpNum); return mapIndex; } std::string CellController::generateMapIndex(MWWorld::Ptr ptr) { return generateMapIndex(ptr.getCellRef().getRefNum().mIndex, ptr.getCellRef().getMpNum()); } std::string CellController::generateMapIndex(BaseActor baseActor) { return generateMapIndex(baseActor.refNum, baseActor.mpNum); } bool CellController::hasLocalAuthority(const ESM::Cell& cell) { if (isInitializedCell(cell) && isActiveWorldCell(cell)) return getCell(cell)->hasLocalAuthority(); return false; } bool CellController::isInitializedCell(const std::string& cellDescription) { return (cellsInitialized.count(cellDescription) > 0); } bool CellController::isInitializedCell(const ESM::Cell& cell) { return isInitializedCell(cell.getShortDescription()); } bool CellController::isActiveWorldCell(const ESM::Cell& cell) { return MWBase::Environment::get().getWorld()->isCellActive(cell); } Cell *CellController::getCell(const ESM::Cell& cell) { return cellsInitialized.at(cell.getShortDescription()); } MWWorld::CellStore *CellController::getCellStore(const ESM::Cell& cell) { MWWorld::CellStore *cellStore; if (cell.isExterior()) cellStore = MWBase::Environment::get().getWorld()->getExterior(cell.mData.mX, cell.mData.mY); else { try { cellStore = MWBase::Environment::get().getWorld()->getInterior(cell.mName); } catch (std::exception&) { cellStore = nullptr; } } return cellStore; } bool CellController::isSameCell(const ESM::Cell& cell, const ESM::Cell& otherCell) { if (&cell == nullptr || &otherCell == nullptr) return false; bool isCellExterior = false; bool isOtherCellExterior = false; try { isCellExterior = cell.isExterior(); isOtherCellExterior = otherCell.isExterior(); } catch (std::exception& e) { LOG_MESSAGE_SIMPLE(TimedLog::LOG_ERROR, "Failed cell comparison"); return false; } if (isCellExterior && isOtherCellExterior) { if (cell.mData.mX == otherCell.mData.mX && cell.mData.mY == otherCell.mData.mY) return true; } else if (Misc::StringUtils::ciEqual(cell.mName, otherCell.mName)) return true; return false; } int CellController::getCellSize() const { return 8192; } ================================================ FILE: apps/openmw/mwmp/CellController.hpp ================================================ #ifndef OPENMW_CELLCONTROLLER_HPP #define OPENMW_CELLCONTROLLER_HPP #include "Cell.hpp" #include "ActorList.hpp" #include "LocalActor.hpp" #include "DedicatedActor.hpp" #include "../mwworld/cellstore.hpp" namespace mwmp { class CellController { public: CellController(); virtual ~CellController(); void updateLocal(bool forceUpdate); void updateDedicated(float dt); void initializeCell(const ESM::Cell& cell); void uninitializeCell(const ESM::Cell& cell); void uninitializeCells(); void readPositions(mwmp::ActorList& actorList); void readAnimFlags(mwmp::ActorList& actorList); void readAnimPlay(mwmp::ActorList& actorList); void readStatsDynamic(mwmp::ActorList& actorList); void readDeath(mwmp::ActorList& actorList); void readEquipment(mwmp::ActorList& actorList); void readSpeech(mwmp::ActorList& actorList); void readSpellsActive(mwmp::ActorList& actorList); void readAi(mwmp::ActorList& actorList); void readAttack(mwmp::ActorList& actorList); void readCast(mwmp::ActorList& actorList); void readCellChange(mwmp::ActorList& actorList); bool hasQueuedDeathState(MWWorld::Ptr ptr); unsigned int getQueuedDeathState(MWWorld::Ptr ptr); void clearQueuedDeathState(MWWorld::Ptr ptr); void setQueuedDeathState(MWWorld::Ptr ptr, unsigned int deathState); void setLocalActorRecord(std::string actorIndex, std::string cellIndex); void removeLocalActorRecord(std::string actorIndex); bool isLocalActor(MWWorld::Ptr ptr); bool isLocalActor(int refNum, int mpNum); virtual LocalActor *getLocalActor(MWWorld::Ptr ptr); virtual LocalActor *getLocalActor(int refNum, int mpNum); void setDedicatedActorRecord(std::string actorIndex, std::string cellIndex); void removeDedicatedActorRecord(std::string actorIndex); bool isDedicatedActor(MWWorld::Ptr ptr); bool isDedicatedActor(int refNum, int mpNum); virtual DedicatedActor *getDedicatedActor(MWWorld::Ptr ptr); virtual DedicatedActor *getDedicatedActor(int refNum, int mpNum); std::string generateMapIndex(int refNumindex, int mpNum); std::string generateMapIndex(MWWorld::Ptr ptr); std::string generateMapIndex(mwmp::BaseActor baseActor); bool hasLocalAuthority(const ESM::Cell& cell); bool isInitializedCell(const std::string& cellDescription); bool isInitializedCell(const ESM::Cell& cell); bool isActiveWorldCell(const ESM::Cell& cell); virtual Cell *getCell(const ESM::Cell& cell); virtual MWWorld::CellStore *getCellStore(const ESM::Cell& cell); bool isSameCell(const ESM::Cell& cell, const ESM::Cell& otherCell); int getCellSize() const; private: static std::map cellsInitialized; static std::map localActorsToCells; static std::map dedicatedActorsToCells; static std::map queuedDeathStates; }; } #endif //OPENMW_CELLCONTROLLER_HPP ================================================ FILE: apps/openmw/mwmp/DedicatedActor.cpp ================================================ #include #include "../mwbase/environment.hpp" #include "../mwbase/soundmanager.hpp" #include "../mwbase/windowmanager.hpp" #include "../mwdialogue/dialoguemanagerimp.hpp" #include "../mwmechanics/aiactivate.hpp" #include "../mwmechanics/aicombat.hpp" #include "../mwmechanics/aiescort.hpp" #include "../mwmechanics/aifollow.hpp" #include "../mwmechanics/aitravel.hpp" #include "../mwmechanics/aiwander.hpp" #include "../mwmechanics/creaturestats.hpp" #include "../mwmechanics/mechanicsmanagerimp.hpp" #include "../mwmechanics/movement.hpp" #include "../mwrender/animation.hpp" #include "../mwworld/action.hpp" #include "../mwworld/cellstore.hpp" #include "../mwworld/class.hpp" #include "../mwworld/inventorystore.hpp" #include "../mwworld/worldimp.hpp" #include "DedicatedActor.hpp" #include "Main.hpp" #include "CellController.hpp" #include "MechanicsHelper.hpp" using namespace mwmp; DedicatedActor::DedicatedActor() { drawState = MWMechanics::DrawState_::DrawState_Nothing; movementFlags = 0; animation.groupname = ""; sound = ""; hasPositionData = false; hasStatsDynamicData = false; hasReceivedInitialEquipment = false; hasChangedCell = true; attack.pressed = false; cast.pressed = false; } DedicatedActor::~DedicatedActor() { } void DedicatedActor::update(float dt) { // Only move and set anim flags if the framerate isn't too low if (dt < 0.1) { move(dt); setAnimFlags(); } setStatsDynamic(); } void DedicatedActor::setCell(MWWorld::CellStore *cellStore) { MWBase::World *world = MWBase::Environment::get().getWorld(); ptr = world->moveObject(ptr, cellStore, position.pos[0], position.pos[1], position.pos[2]); setMovementSettings(); hasChangedCell = true; } void DedicatedActor::move(float dt) { ESM::Position refPos = ptr.getRefData().getPosition(); MWBase::World *world = MWBase::Environment::get().getWorld(); const int maxInterpolationDistance = 40; // Apply interpolation only if the position hasn't changed too much from last time bool shouldInterpolate = abs(position.pos[0] - refPos.pos[0]) < maxInterpolationDistance && abs(position.pos[1] - refPos.pos[1]) < maxInterpolationDistance && abs(position.pos[2] - refPos.pos[2]) < maxInterpolationDistance; // Don't apply linear interpolation if the DedicatedActor has just gone through a cell change, because // the interpolated position will be invalid, causing a slight hopping glitch if (shouldInterpolate && !hasChangedCell) { static const int timeMultiplier = 15; osg::Vec3f lerp = MechanicsHelper::getLinearInterpolation(refPos.asVec3(), position.asVec3(), dt * timeMultiplier); refPos.pos[0] = lerp.x(); refPos.pos[1] = lerp.y(); refPos.pos[2] = lerp.z(); world->moveObject(ptr, refPos.pos[0], refPos.pos[1], refPos.pos[2]); } else { setPosition(); hasChangedCell = false; } setMovementSettings(); world->rotateObject(ptr, position.rot[0], position.rot[1], position.rot[2]); } void DedicatedActor::setMovementSettings() { MWMechanics::Movement *move = &ptr.getClass().getMovementSettings(ptr); move->mPosition[0] = direction.pos[0]; move->mPosition[1] = direction.pos[1]; move->mPosition[2] = direction.pos[2]; // Make sure the values are valid, or we'll get an infinite error loop if (!isnan(direction.rot[0]) && !isnan(direction.rot[1]) && !isnan(direction.rot[2])) { move->mRotation[0] = direction.rot[0]; move->mRotation[1] = direction.rot[1]; move->mRotation[2] = direction.rot[2]; } } void DedicatedActor::setPosition() { MWBase::World *world = MWBase::Environment::get().getWorld(); world->moveObject(ptr, position.pos[0], position.pos[1], position.pos[2]); } void DedicatedActor::setAnimFlags() { using namespace MWMechanics; MWMechanics::CreatureStats *ptrCreatureStats = &ptr.getClass().getCreatureStats(ptr); ptrCreatureStats->setDrawState(static_cast(drawState)); ptrCreatureStats->setMovementFlag(CreatureStats::Flag_Run, (movementFlags & CreatureStats::Flag_Run) != 0); ptrCreatureStats->setMovementFlag(CreatureStats::Flag_Sneak, (movementFlags & CreatureStats::Flag_Sneak) != 0); ptrCreatureStats->setMovementFlag(CreatureStats::Flag_ForceJump, (movementFlags & CreatureStats::Flag_ForceJump) != 0); ptrCreatureStats->setMovementFlag(CreatureStats::Flag_ForceMoveJump, (movementFlags & CreatureStats::Flag_ForceMoveJump) != 0); } void DedicatedActor::setStatsDynamic() { // Only set dynamic stats if we have received at least one packet about them if (!hasStatsDynamicData) return; MWMechanics::CreatureStats *ptrCreatureStats = &ptr.getClass().getCreatureStats(ptr); MWMechanics::DynamicStat value; // Resurrect this Actor if it's not supposed to be dead according to its authority if (creatureStats.mDynamic[0].mCurrent > 0) MWBase::Environment::get().getMechanicsManager()->resurrect(ptr); for (int i = 0; i < 3; ++i) { value.readState(creatureStats.mDynamic[i]); ptrCreatureStats->setDynamic(i, value); } } void DedicatedActor::setEquipment() { if (!ptr.getClass().hasInventoryStore(ptr)) return; MWWorld::InventoryStore& invStore = ptr.getClass().getInventoryStore(ptr); for (int slot = 0; slot < MWWorld::InventoryStore::Slots; ++slot) { int count = equipmentItems[slot].count; // If we've somehow received a corrupted item with a count lower than 0, ignore it if (count < 0) continue; MWWorld::ContainerStoreIterator it = invStore.getSlot(slot); const std::string &packetRefId = equipmentItems[slot].refId; int packetCharge = equipmentItems[slot].charge; std::string storeRefId = ""; bool equal = false; if (it != invStore.end()) { storeRefId = it->getCellRef().getRefId(); if (!Misc::StringUtils::ciEqual(storeRefId, packetRefId)) // if other item equiped invStore.unequipSlot(slot, ptr); else equal = true; } if (packetRefId.empty() || equal) continue; if (!hasItem(packetRefId, packetCharge)) { ptr.getClass().getContainerStore(ptr).add(packetRefId, count, ptr); } // Equip items silently if this is the first time equipment is being set for this character equipItem(packetRefId, packetCharge, !hasReceivedInitialEquipment); } hasReceivedInitialEquipment = true; } void DedicatedActor::setAi() { MWMechanics::CreatureStats *ptrCreatureStats = &ptr.getClass().getCreatureStats(ptr); ptrCreatureStats->setAiSetting(MWMechanics::CreatureStats::AI_Fight, 0); LOG_APPEND(TimedLog::LOG_VERBOSE, "- actor cellRef: %s %i-%i", ptr.getCellRef().getRefId().c_str(), ptr.getCellRef().getRefNum().mIndex, ptr.getCellRef().getMpNum()); if (aiAction == mwmp::BaseActorList::CANCEL) { LOG_APPEND(TimedLog::LOG_VERBOSE, "-- Cancelling AI sequence"); ptrCreatureStats->getAiSequence().clear(); } else if (aiAction == mwmp::BaseActorList::TRAVEL) { LOG_APPEND(TimedLog::LOG_VERBOSE, "-- Travelling to %f, %f, %f", aiCoordinates.pos[0], aiCoordinates.pos[1], aiCoordinates.pos[2]); MWMechanics::AiTravel package(aiCoordinates.pos[0], aiCoordinates.pos[1], aiCoordinates.pos[2]); ptrCreatureStats->getAiSequence().stack(package, ptr, true); } else if (aiAction == mwmp::BaseActorList::WANDER) { LOG_APPEND(TimedLog::LOG_VERBOSE, "-- Wandering for distance %i and duration %i, repetition is %s", aiDistance, aiDuration, aiShouldRepeat ? "true" : "false"); std::vector idleList; MWMechanics::AiWander package(aiDistance, aiDuration, -1, idleList, aiShouldRepeat); ptrCreatureStats->getAiSequence().stack(package, ptr, true); } else if (hasAiTarget) { MWWorld::Ptr targetPtr; if (aiTarget.isPlayer) { targetPtr = MechanicsHelper::getPlayerPtr(aiTarget); LOG_APPEND(TimedLog::LOG_VERBOSE, "-- Has player target %s", targetPtr.getClass().getName(targetPtr).c_str()); } else { if (mwmp::Main::get().getCellController()->isLocalActor(aiTarget.refNum, aiTarget.mpNum)) targetPtr = mwmp::Main::get().getCellController()->getLocalActor(aiTarget.refNum, aiTarget.mpNum)->getPtr(); else if (mwmp::Main::get().getCellController()->isDedicatedActor(aiTarget.refNum, aiTarget.mpNum)) targetPtr = mwmp::Main::get().getCellController()->getDedicatedActor(aiTarget.refNum, aiTarget.mpNum)->getPtr(); else if (aiAction == mwmp::BaseActorList::ACTIVATE) targetPtr = MWBase::Environment::get().getWorld()->searchPtrViaUniqueIndex(aiTarget.refNum, aiTarget.mpNum); if (targetPtr) { LOG_APPEND(TimedLog::LOG_VERBOSE, "-- Has actor target %s %i-%i", targetPtr.getCellRef().getRefId().c_str(), aiTarget.refNum, aiTarget.mpNum); } else { LOG_APPEND(TimedLog::LOG_VERBOSE, "-- Has invalid actor target %i-%i", aiTarget.refNum, aiTarget.mpNum); } } if (targetPtr) { if (aiAction == mwmp::BaseActorList::ACTIVATE) { LOG_APPEND(TimedLog::LOG_VERBOSE, "-- Activating target"); MWMechanics::AiActivate package(targetPtr); ptrCreatureStats->getAiSequence().stack(package, ptr, true); } if (aiAction == mwmp::BaseActorList::COMBAT) { LOG_APPEND(TimedLog::LOG_VERBOSE, "-- Starting combat with target"); MWMechanics::AiCombat package(targetPtr); ptrCreatureStats->getAiSequence().stack(package, ptr, true); } else if (aiAction == mwmp::BaseActorList::ESCORT) { LOG_APPEND(TimedLog::LOG_VERBOSE, "-- Being escorted by target, for duration %i, to coordinates %f, %f, %f", aiDuration, aiCoordinates.pos[0], aiCoordinates.pos[1], aiCoordinates.pos[2]); MWMechanics::AiEscort package(targetPtr.getCellRef().getRefId(), aiDuration, aiCoordinates.pos[0], aiCoordinates.pos[1], aiCoordinates.pos[2]); ptrCreatureStats->getAiSequence().stack(package, ptr, true); } else if (aiAction == mwmp::BaseActorList::FOLLOW) { LOG_APPEND(TimedLog::LOG_VERBOSE, "-- Following target"); MWMechanics::AiFollow package(targetPtr); package.allowAnyDistance(true); ptrCreatureStats->getAiSequence().stack(package, ptr, true); } } } } void DedicatedActor::playAnimation() { if (!animation.groupname.empty()) { MWBase::Environment::get().getMechanicsManager()->playAnimationGroup(ptr, animation.groupname, animation.mode, animation.count, animation.persist); animation.groupname.clear(); } } void DedicatedActor::playSound() { if (!sound.empty()) { MWBase::Environment::get().getSoundManager()->say(ptr, sound); MWBase::WindowManager *winMgr = MWBase::Environment::get().getWindowManager(); if (winMgr->getSubtitlesEnabled()) winMgr->messageBox(MWBase::Environment::get().getDialogueManager()->getVoiceCaption(sound), MWGui::ShowInDialogueMode_Never); sound.clear(); } } bool DedicatedActor::hasItem(std::string itemId, int charge) { for (const auto &itemPtr : ptr.getClass().getInventoryStore(ptr)) { if (::Misc::StringUtils::ciEqual(itemPtr.getCellRef().getRefId(), itemId) && itemPtr.getCellRef().getCharge() == charge) return true; } return false; } void DedicatedActor::equipItem(std::string itemId, int charge, bool noSound) { for (const auto &itemPtr : ptr.getClass().getInventoryStore(ptr)) { if (::Misc::StringUtils::ciEqual(itemPtr.getCellRef().getRefId(), itemId) && itemPtr.getCellRef().getCharge() == charge) { std::shared_ptr action = itemPtr.getClass().use(itemPtr); action->execute(ptr, noSound); break; } } } void DedicatedActor::addSpellsActive() { MWMechanics::ActiveSpells& activeSpells = getPtr().getClass().getCreatureStats(getPtr()).getActiveSpells(); for (const auto& activeSpell : spellsActiveChanges.activeSpells) { MWWorld::TimeStamp timestamp = MWWorld::TimeStamp(activeSpell.timestampHour, activeSpell.timestampDay); int casterActorId = MechanicsHelper::getActorId(activeSpell.caster); MechanicsHelper::createSpellGfx(getPtr(), activeSpell.params.mEffects); // Don't do a check for a spell's existence, because active effects from potions need to be applied here too activeSpells.addSpell(activeSpell.id, activeSpell.isStackingSpell, activeSpell.params.mEffects, activeSpell.params.mDisplayName, casterActorId, timestamp, false); } } void DedicatedActor::removeSpellsActive() { MWMechanics::ActiveSpells& activeSpells = getPtr().getClass().getCreatureStats(getPtr()).getActiveSpells(); for (const auto& activeSpell : spellsActiveChanges.activeSpells) { // Remove stacking spells based on their timestamps if (activeSpell.isStackingSpell) { MWWorld::TimeStamp timestamp = MWWorld::TimeStamp(activeSpell.timestampHour, activeSpell.timestampDay); activeSpells.removeSpellByTimestamp(activeSpell.id, timestamp); } else { activeSpells.removeEffects(activeSpell.id); } } } void DedicatedActor::setSpellsActive() { MWMechanics::ActiveSpells& activeSpells = getPtr().getClass().getCreatureStats(getPtr()).getActiveSpells(); activeSpells.clear(); // Proceed by adding spells active addSpellsActive(); } MWWorld::Ptr DedicatedActor::getPtr() { return ptr; } void DedicatedActor::setPtr(const MWWorld::Ptr& newPtr) { ptr = newPtr; refId = ptr.getCellRef().getRefId(); refNum = ptr.getCellRef().getRefNum().mIndex; mpNum = ptr.getCellRef().getMpNum(); position = ptr.getRefData().getPosition(); drawState = ptr.getClass().getCreatureStats(ptr).getDrawState(); } void DedicatedActor::reloadPtr() { MWBase::World* world = MWBase::Environment::get().getWorld(); world->disable(ptr); world->enable(ptr); } ================================================ FILE: apps/openmw/mwmp/DedicatedActor.hpp ================================================ #ifndef OPENMW_DEDICATEDACTOR_HPP #define OPENMW_DEDICATEDACTOR_HPP #include #include "../mwmechanics/aisequence.hpp" #include "../mwworld/manualref.hpp" namespace mwmp { class DedicatedActor : public BaseActor { public: DedicatedActor(); virtual ~DedicatedActor(); void update(float dt); void move(float dt); void setCell(MWWorld::CellStore *cellStore); void setMovementSettings(); void setPosition(); void setAnimFlags(); void setStatsDynamic(); void setEquipment(); void setAi(); void playAnimation(); void playSound(); bool hasItem(std::string itemId, int charge); void equipItem(std::string itemId, int charge, bool noSound = false); void addSpellsActive(); void removeSpellsActive(); void setSpellsActive(); MWWorld::Ptr getPtr(); void setPtr(const MWWorld::Ptr& newPtr); void reloadPtr(); private: MWWorld::Ptr ptr; bool hasReceivedInitialEquipment; bool hasChangedCell; }; } #endif //OPENMW_DEDICATEDACTOR_HPP ================================================ FILE: apps/openmw/mwmp/DedicatedPlayer.cpp ================================================ #include #include #include #include "../mwbase/environment.hpp" #include "../mwbase/soundmanager.hpp" #include "../mwclass/npc.hpp" #include "../mwdialogue/dialoguemanagerimp.hpp" #include "../mwgui/windowmanagerimp.hpp" #include "../mwinput/inputmanagerimp.hpp" #include "../mwmechanics/actor.hpp" #include "../mwmechanics/aitravel.hpp" #include "../mwmechanics/creaturestats.hpp" #include "../mwmechanics/movement.hpp" #include "../mwmechanics/npcstats.hpp" #include "../mwmechanics/mechanicsmanagerimp.hpp" #include "../mwmechanics/spellcasting.hpp" #include "../mwstate/statemanagerimp.hpp" #include "../mwworld/action.hpp" #include "../mwworld/cellstore.hpp" #include "../mwworld/customdata.hpp" #include "../mwworld/inventorystore.hpp" #include "../mwworld/player.hpp" #include "../mwworld/worldimp.hpp" #include "DedicatedPlayer.hpp" #include "Main.hpp" #include "GUIController.hpp" #include "CellController.hpp" #include "MechanicsHelper.hpp" #include "RecordHelper.hpp" using namespace mwmp; DedicatedPlayer::DedicatedPlayer(RakNet::RakNetGUID guid) : BasePlayer(guid) { reference = 0; attack.pressed = false; cast.pressed = false; creatureStats.mDead = false; // Give this new character a temporary high fatigue so it doesn't spawn on // the ground creatureStats.mDynamic[2].mBase = 1000; attack.instant = false; MWBase::World* world = MWBase::Environment::get().getWorld(); cell = *world->getInterior(RecordHelper::getPlaceholderInteriorCellName())->getCell(); position.pos[0] = position.pos[1] = position.pos[2] = 0; npc = *world->getPlayerPtr().get()->mBase; npc.mId = ""; previousRace = npc.mRace; hasReceivedInitialEquipment = false; hasFinishedInitialTeleportation = false; isJumping = false; wasJumping = false; } DedicatedPlayer::~DedicatedPlayer() { } void DedicatedPlayer::update(float dt) { // Only move and set anim flags if the framerate isn't too low if (dt < 0.1) { move(dt); setAnimFlags(); } MWMechanics::CreatureStats *ptrCreatureStats = &ptr.getClass().getCreatureStats(ptr); MWMechanics::DynamicStat value; if (creatureStats.mDead) { value.readState(creatureStats.mDynamic[0]); ptrCreatureStats->setHealth(value); return; } for (int i = 0; i < 3; ++i) { value.readState(creatureStats.mDynamic[i]); ptrCreatureStats->setDynamic(i, value); } if (ptrCreatureStats->isDead()) MWBase::Environment::get().getMechanicsManager()->resurrect(ptr); ptrCreatureStats->setAttacked(false); ptrCreatureStats->getAiSequence().stopCombat(); ptrCreatureStats->setAlarmed(false); ptrCreatureStats->setAiSetting(MWMechanics::CreatureStats::AI_Alarm, 0); ptrCreatureStats->setAiSetting(MWMechanics::CreatureStats::AI_Fight, 0); ptrCreatureStats->setAiSetting(MWMechanics::CreatureStats::AI_Flee, 0); ptrCreatureStats->setAiSetting(MWMechanics::CreatureStats::AI_Hello, 0); } void DedicatedPlayer::move(float dt) { if (!reference) return; ESM::Position refPos = ptr.getRefData().getPosition(); MWBase::World *world = MWBase::Environment::get().getWorld(); const int maxInterpolationDistance = 80; // Apply interpolation only if the position hasn't changed too much from last time bool shouldInterpolate = abs(position.pos[0] - refPos.pos[0]) < maxInterpolationDistance && abs(position.pos[1] - refPos.pos[1]) < maxInterpolationDistance && abs(position.pos[2] - refPos.pos[2]) < maxInterpolationDistance; if (shouldInterpolate) { static const int timeMultiplier = 15; osg::Vec3f lerp = MechanicsHelper::getLinearInterpolation(refPos.asVec3(), position.asVec3(), dt * timeMultiplier); world->moveObject(ptr, lerp.x(), lerp.y(), lerp.z()); } else world->moveObject(ptr, position.pos[0], position.pos[1], position.pos[2]); world->rotateObject(ptr, position.rot[0], 0, position.rot[2]); MWMechanics::Movement *move = &ptr.getClass().getMovementSettings(ptr); move->mPosition[0] = direction.pos[0]; move->mPosition[1] = direction.pos[1]; move->mPosition[2] = direction.pos[2]; // Make sure the values are valid, or we'll get an infinite error loop if (!isnan(direction.rot[0]) && !isnan(direction.rot[1]) && !isnan(direction.rot[2])) { move->mRotation[0] = direction.rot[0]; move->mRotation[1] = direction.rot[1]; move->mRotation[2] = direction.rot[2]; } } void DedicatedPlayer::setBaseInfo() { // Use the previous race if the new one doesn't exist if (!RecordHelper::doesRecordIdExist(npc.mRace)) npc.mRace = previousRace; if (!reference) { npc.mId = RecordHelper::createRecord(npc)->mId; createReference(npc.mId); } else { RecordHelper::overrideRecord(npc); reloadPtr(); } // Only set equipment if the player isn't disguised as a creature if (ptr.getTypeName() == typeid(ESM::NPC).name()) setEquipment(); previousRace = npc.mRace; } void DedicatedPlayer::setStatsDynamic() { MWMechanics::CreatureStats* ptrCreatureStats = &getPtr().getClass().getCreatureStats(getPtr()); MWMechanics::DynamicStat value; for (int i = 0; i < 3; ++i) { value.readState(creatureStats.mDynamic[i]); ptrCreatureStats->setDynamic(i, value); } } void DedicatedPlayer::setAnimFlags() { using namespace MWMechanics; MWBase::World *world = MWBase::Environment::get().getWorld(); // Until we figure out a better workaround for disabling player gravity, // simply cast Levitate over and over on a player that's supposed to be flying if (!isFlying && !hasTcl && !isLevitationPurged) { ptr.getClass().getCreatureStats(ptr).getActiveSpells().purgeEffect(ESM::MagicEffect::Levitate); isLevitationPurged = true; } else if ((isFlying || hasTcl) && !world->isFlying(ptr)) { MWMechanics::CastSpell levitationCast(ptr, ptr); levitationCast.mHitPosition = ptr.getRefData().getPosition().asVec3(); levitationCast.mAlwaysSucceed = true; levitationCast.cast("Levitate"); isLevitationPurged = false; } if (isJumping && !wasJumping) { world->setOnGround(ptr, false); wasJumping = true; } else if (wasJumping && !isJumping) { world->setOnGround(ptr, true); wasJumping = false; } MWMechanics::CreatureStats *ptrCreatureStats = &ptr.getClass().getCreatureStats(ptr); ptrCreatureStats->setDrawState(static_cast(drawState)); ptrCreatureStats->setMovementFlag(CreatureStats::Flag_Run, (movementFlags & CreatureStats::Flag_Run) != 0); ptrCreatureStats->setMovementFlag(CreatureStats::Flag_Sneak, (movementFlags & CreatureStats::Flag_Sneak) != 0); ptrCreatureStats->setMovementFlag(CreatureStats::Flag_ForceJump, (movementFlags & CreatureStats::Flag_ForceJump) != 0); ptrCreatureStats->setMovementFlag(CreatureStats::Flag_ForceMoveJump, (movementFlags & CreatureStats::Flag_ForceMoveJump) != 0); } void DedicatedPlayer::setAttributes() { MWMechanics::CreatureStats *ptrCreatureStats = &ptr.getClass().getCreatureStats(ptr); MWMechanics::AttributeValue attributeValue; for (int i = 0; i < 8; ++i) { attributeValue.readState(creatureStats.mAttributes[i]); ptrCreatureStats->setAttribute(i, attributeValue); } } void DedicatedPlayer::setSkills() { // Go no further if the player is disguised as a creature if (ptr.getTypeName() != typeid(ESM::NPC).name()) return; MWMechanics::NpcStats *ptrNpcStats = &ptr.getClass().getNpcStats(ptr); MWMechanics::SkillValue skillValue; for (int i = 0; i < 27; ++i) { skillValue.readState(npcStats.mSkills[i]); ptrNpcStats->setSkill(i, skillValue); } } void DedicatedPlayer::setEquipment() { // Go no further if the player is disguised as a creature if (!ptr.getClass().hasInventoryStore(ptr)) return; bool equippedSomething = false; MWWorld::InventoryStore& invStore = ptr.getClass().getInventoryStore(ptr); for (int slot = 0; slot < MWWorld::InventoryStore::Slots; ++slot) { MWWorld::ContainerStoreIterator it = invStore.getSlot(slot); const std::string &packetRefId = equipmentItems[slot].refId; std::string ptrItemId = ""; bool equal = false; if (it != invStore.end()) { ptrItemId = it->getCellRef().getRefId(); if (!Misc::StringUtils::ciEqual(ptrItemId, packetRefId)) // if other item is now equipped { MWWorld::ContainerStore &store = ptr.getClass().getContainerStore(ptr); // Remove the items that are no longer equipped, except for throwing weapons and ranged weapon ammo that // have just run out but still need to be kept briefly so they can be used in attacks about to be released bool shouldRemove = true; if (attack.type == mwmp::Attack::RANGED && packetRefId.empty() && !attack.pressed) { if (slot == MWWorld::InventoryStore::Slot_CarriedRight && Misc::StringUtils::ciEqual(ptrItemId, attack.rangedWeaponId)) shouldRemove = false; else if (slot == MWWorld::InventoryStore::Slot_Ammunition && Misc::StringUtils::ciEqual(ptrItemId, attack.rangedAmmoId)) shouldRemove = false; } if (shouldRemove) { store.remove(ptrItemId, store.count(ptrItemId), ptr); } } else equal = true; } if (packetRefId.empty() || equal) continue; const int count = equipmentItems[slot].count; ptr.getClass().getContainerStore(ptr).add(packetRefId, count, ptr); // Equip items silently if this is the first time equipment is being set for this character equipItem(packetRefId, !hasReceivedInitialEquipment); equippedSomething = true; } // Only track the initial equipment as received if at least one item has been equipped if (equippedSomething) hasReceivedInitialEquipment = true; } void DedicatedPlayer::setShapeshift() { MWBase::World* world = MWBase::Environment::get().getWorld(); bool isNpc = false; if (reference) isNpc = ptr.getTypeName() == typeid(ESM::NPC).name(); if (creatureRefId != previousCreatureRefId || displayCreatureName != previousDisplayCreatureName) { if (!creatureRefId.empty() && RecordHelper::doesRecordIdExist(creatureRefId)) { deleteReference(); const ESM::Creature* tmpCreature = world->getStore().get().search(creatureRefId); creature = *tmpCreature; creature.mScript = ""; if (!displayCreatureName) creature.mName = npc.mName; LOG_APPEND(TimedLog::LOG_INFO, "- %s is disguised as %s", npc.mName.c_str(), creatureRefId.c_str()); // Is this our first time creating a creature record id for this player? If so, keep it around // and reuse it if (creatureRecordId.empty()) { creature.mId = creatureRecordId = RecordHelper::createRecord(creature)->mId; LOG_APPEND(TimedLog::LOG_INFO, "- Creating new creature record %s", creatureRecordId.c_str()); } else { creature.mId = creatureRecordId; RecordHelper::overrideRecord(creature); } LOG_APPEND(TimedLog::LOG_INFO, "- Creating reference for %s", creature.mId.c_str()); createReference(creature.mId); } // This player was already a creature, but the new creature refId was empty or // invalid, so we'll turn this player into their NPC self again as a result else if (!isNpc) { if (reference) { deleteReference(); } RecordHelper::overrideRecord(npc); createReference(npc.mId); reloadPtr(); } previousCreatureRefId = creatureRefId; previousDisplayCreatureName = displayCreatureName; } if (ptr.getTypeName() == typeid(ESM::NPC).name()) { MWBase::Environment::get().getMechanicsManager()->setWerewolf(ptr, isWerewolf); if (!isWerewolf) setEquipment(); } MWBase::Environment::get().getWorld()->scaleObject(ptr, scale); } void DedicatedPlayer::setCell() { // Prevent cell update when reference doesn't exist if (!reference) return; MWBase::World *world = MWBase::Environment::get().getWorld(); LOG_MESSAGE_SIMPLE(TimedLog::LOG_INFO, "Server says DedicatedPlayer %s moved to %s", npc.mName.c_str(), cell.getShortDescription().c_str()); MWWorld::CellStore *cellStore = Main::get().getCellController()->getCellStore(cell); if (!cellStore) { LOG_APPEND(TimedLog::LOG_INFO, "%s", "- Cell doesn't exist on this client"); world->disable(getPtr()); return; } else world->enable(getPtr()); // Make sure the Ptr's dynamic stats and anim flags are up-to-date, so it doesn't show up // knocked down or in a jump loop when it shouldn't setStatsDynamic(); setAnimFlags(); // Allow this player's reference to move across a cell now that a manual cell // update has been called setPtr(world->moveObject(ptr, cellStore, position.pos[0], position.pos[1], position.pos[2])); // Remove the marker entirely if this player has moved to an interior that is inactive for us if (!cell.isExterior() && !Main::get().getCellController()->isActiveWorldCell(cell)) removeMarker(); // Otherwise, update their marker so the player shows up in the right cell on the world map else { enableMarker(); } // If this player is now in a cell that we are the local authority over, we should send them all // NPC data in that cell if (Main::get().getCellController()->hasLocalAuthority(cell)) Main::get().getCellController()->getCell(cell)->updateLocal(true); // If this player is a new player or is now in a region that we are the weather authority over, // or is a new player, we should send our latest weather data to the server if (world->getWeatherCreationState()) { if (!hasFinishedInitialTeleportation || Misc::StringUtils::ciEqual(getPtr().getCell()->getCell()->mRegion, world->getPlayerPtr().getCell()->getCell()->mRegion)) { world->sendWeather(); } } hasFinishedInitialTeleportation = true; } void DedicatedPlayer::playAnimation() { MWBase::Environment::get().getMechanicsManager()->playAnimationGroup(getPtr(), animation.groupname, animation.mode, animation.count, animation.persist); } void DedicatedPlayer::playSpeech() { MWBase::Environment::get().getSoundManager()->say(getPtr(), sound); MWBase::WindowManager *winMgr = MWBase::Environment::get().getWindowManager(); if (winMgr->getSubtitlesEnabled()) winMgr->messageBox(MWBase::Environment::get().getDialogueManager()->getVoiceCaption(sound), MWGui::ShowInDialogueMode_Never); } void DedicatedPlayer::equipItem(std::string itemId, bool noSound) { for (const auto& itemPtr : ptr.getClass().getInventoryStore(ptr)) { if (::Misc::StringUtils::ciEqual(itemPtr.getCellRef().getRefId(), itemId)) { std::shared_ptr action = itemPtr.getClass().use(itemPtr); action->execute(ptr, noSound); break; } } } void DedicatedPlayer::die() { MWMechanics::DynamicStat health; creatureStats.mDead = true; health.readState(creatureStats.mDynamic[0]); health.setCurrent(0); health.writeState(creatureStats.mDynamic[0]); ptr.getClass().getCreatureStats(ptr).setHealth(health); } void DedicatedPlayer::resurrect() { creatureStats.mDead = false; if (creatureStats.mDynamic[0].mMod < 1) creatureStats.mDynamic[0].mMod = 1; creatureStats.mDynamic[0].mCurrent = creatureStats.mDynamic[0].mMod; MWBase::Environment::get().getMechanicsManager()->resurrect(getPtr()); MWMechanics::DynamicStat health; health.readState(creatureStats.mDynamic[0]); getPtr().getClass().getCreatureStats(getPtr()).setHealth(health); } void DedicatedPlayer::addSpellsActive() { MWMechanics::ActiveSpells& activeSpells = getPtr().getClass().getCreatureStats(getPtr()).getActiveSpells(); for (const auto& activeSpell : spellsActiveChanges.activeSpells) { MWWorld::TimeStamp timestamp = MWWorld::TimeStamp(activeSpell.timestampHour, activeSpell.timestampDay); int casterActorId = MechanicsHelper::getActorId(activeSpell.caster); MechanicsHelper::createSpellGfx(getPtr(), activeSpell.params.mEffects); // Don't do a check for a spell's existence, because active effects from potions need to be applied here too activeSpells.addSpell(activeSpell.id, activeSpell.isStackingSpell, activeSpell.params.mEffects, activeSpell.params.mDisplayName, casterActorId, timestamp, false); } } void DedicatedPlayer::removeSpellsActive() { MWMechanics::ActiveSpells& activeSpells = getPtr().getClass().getCreatureStats(getPtr()).getActiveSpells(); for (const auto& activeSpell : spellsActiveChanges.activeSpells) { // Remove stacking spells based on their timestamps if (activeSpell.isStackingSpell) { MWWorld::TimeStamp timestamp = MWWorld::TimeStamp(activeSpell.timestampHour, activeSpell.timestampDay); activeSpells.removeSpellByTimestamp(activeSpell.id, timestamp); } else { activeSpells.removeEffects(activeSpell.id); } } } void DedicatedPlayer::setSpellsActive() { MWMechanics::ActiveSpells& activeSpells = getPtr().getClass().getCreatureStats(getPtr()).getActiveSpells(); activeSpells.clear(); // Proceed by adding spells active addSpellsActive(); } void DedicatedPlayer::updateMarker() { if (!markerEnabled) { return; } GUIController* gui = Main::get().getGUIController(); if (gui->mPlayerMarkers.contains(marker)) { gui->mPlayerMarkers.deleteMarker(marker); marker = gui->createMarker(guid); gui->mPlayerMarkers.addMarker(marker); } else { gui->mPlayerMarkers.addMarker(marker, true); } } void DedicatedPlayer::enableMarker() { markerEnabled = true; updateMarker(); } void DedicatedPlayer::removeMarker() { if (!markerEnabled) return; markerEnabled = false; GUIController* gui = Main::get().getGUIController(); if (gui->mPlayerMarkers.contains(marker)) { Main::get().getGUIController()->mPlayerMarkers.deleteMarker(marker); } } void DedicatedPlayer::createReference(const std::string& recId) { MWBase::World *world = MWBase::Environment::get().getWorld(); reference = new MWWorld::ManualRef(world->getStore(), recId, 1); LOG_APPEND(TimedLog::LOG_INFO, "- Creating new reference pointer for %s", npc.mName.c_str()); ptr = world->placeObject(reference->getPtr(), Main::get().getCellController()->getCellStore(cell), position); ESM::CustomMarker mEditingMarker = Main::get().getGUIController()->createMarker(guid); marker = mEditingMarker; enableMarker(); } void DedicatedPlayer::deleteReference() { MWBase::World *world = MWBase::Environment::get().getWorld(); LOG_APPEND(TimedLog::LOG_INFO, "- Deleting reference"); world->deleteObject(ptr); delete reference; reference = nullptr; } MWWorld::Ptr DedicatedPlayer::getPtr() { return ptr; } MWWorld::ManualRef *DedicatedPlayer::getRef() { return reference; } void DedicatedPlayer::setPtr(const MWWorld::Ptr& newPtr) { ptr = newPtr; } void DedicatedPlayer::reloadPtr() { MWBase::World *world = MWBase::Environment::get().getWorld(); world->disable(ptr); world->enable(ptr); } ================================================ FILE: apps/openmw/mwmp/DedicatedPlayer.hpp ================================================ #ifndef OPENMW_DEDICATEDPLAYER_HPP #define OPENMW_DEDICATEDPLAYER_HPP #include #include #include #include #include "../mwclass/npc.hpp" #include "../mwmechanics/aisequence.hpp" #include "../mwworld/manualref.hpp" #include #include namespace MWMechanics { class Actor; } namespace mwmp { class DedicatedPlayer : public BasePlayer { friend class PlayerList; public: void update(float dt); void move(float dt); void setBaseInfo(); void setStatsDynamic(); void setAnimFlags(); void setAttributes(); void setSkills(); void setEquipment(); void setShapeshift(); void setCell(); void playAnimation(); void playSpeech(); void equipItem(std::string itemId, bool noSound = false); void die(); void resurrect(); void addSpellsActive(); void removeSpellsActive(); void setSpellsActive(); void updateMarker(); void removeMarker(); void enableMarker(); void createReference(const std::string& recId); void deleteReference(); MWWorld::Ptr getPtr(); MWWorld::ManualRef* getRef(); void setPtr(const MWWorld::Ptr& newPtr); void reloadPtr(); private: DedicatedPlayer(RakNet::RakNetGUID guid); virtual ~DedicatedPlayer(); MWWorld::ManualRef* reference; MWWorld::Ptr ptr; ESM::CustomMarker marker; bool markerEnabled; std::string previousRace; std::string previousCreatureRefId; bool previousDisplayCreatureName; std::string creatureRecordId; bool hasReceivedInitialEquipment; bool hasFinishedInitialTeleportation; bool isLevitationPurged; bool wasJumping; }; } #endif //OPENMW_DEDICATEDPLAYER_HPP ================================================ FILE: apps/openmw/mwmp/GUI/GUIChat.cpp ================================================ #include "GUIChat.hpp" #include #include "apps/openmw/mwbase/environment.hpp" #include "apps/openmw/mwgui/windowmanagerimp.hpp" #include "apps/openmw/mwinput/inputmanagerimp.hpp" #include #include #include "../Networking.hpp" #include "../Main.hpp" #include "../LocalPlayer.hpp" #include "../GUIController.hpp" namespace mwmp { GUIChat::GUIChat(int x, int y, int w, int h) : WindowBase("tes3mp_chat.layout") { setCoord(x, y, w, h); getWidget(mCommandLine, "edit_Command"); getWidget(mHistory, "list_History"); // Set up the command line box mCommandLine->eventEditSelectAccept += newDelegate(this, &GUIChat::acceptCommand); mCommandLine->eventKeyButtonPressed += newDelegate(this, &GUIChat::keyPress); setTitle("Chat"); mHistory->setOverflowToTheLeft(true); mHistory->setEditWordWrap(true); mHistory->setTextShadow(true); mHistory->setTextShadowColour(MyGUI::Colour::Black); mHistory->setNeedKeyFocus(false); windowState = CHAT_DISABLED; mCommandLine->setVisible(false); delay = 3; // 3 sec. } void GUIChat::onOpen() { // Give keyboard focus to the combo box whenever the console is // turned on setEditState(false); if (windowState == CHAT_DISABLED) windowState = CHAT_ENABLED; } void GUIChat::onClose() { setEditState(false); } bool GUIChat::exit() { //WindowBase::exit(); return true; } bool GUIChat::getEditState() { return editState; } void GUIChat::acceptCommand(MyGUI::EditBox *_sender) { const std::string &cm = mCommandLine->getOnlyText(); // If they enter nothing, then it should be canceled. // Otherwise, there's no way of closing without having text. if (cm.empty()) { mCommandLine->setCaption(""); setEditState(false); return; } LOG_MESSAGE_SIMPLE(TimedLog::LOG_INFO, "Player: %s", cm.c_str()); // Add the command to the history, and set the current pointer to // the end of the list if (mCommandHistory.empty() || mCommandHistory.back() != cm) mCommandHistory.push_back(cm); mCurrent = mCommandHistory.end(); mEditString.clear(); // Reset the command line before the command execution. // It prevents the re-triggering of the acceptCommand() event for the same command // during the actual command execution mCommandLine->setCaption(""); setEditState(false); send(cm); } void GUIChat::onResChange(int width, int height) { setCoord(10,10, width-10, height/2); } void GUIChat::setFont(const std::string &fntName) { mHistory->setFontName(fntName); mCommandLine->setFontName(fntName); } void GUIChat::print(const std::string &msg, const std::string &color) { if (windowState == CHAT_HIDDENMODE && !isVisible()) { setVisible(true); } if(msg.size() == 0) { clean(); LOG_MESSAGE_SIMPLE(TimedLog::LOG_INFO, "Chat cleaned"); } else { mHistory->addText(color + msg); LOG_MESSAGE_SIMPLE(TimedLog::LOG_INFO, "%s", msg.c_str()); } } void GUIChat::printOK(const std::string &msg) { print(msg + "\n", "#FF00FF"); } void GUIChat::printError(const std::string &msg) { print(msg + "\n", "#FF2222"); } void GUIChat::send(const std::string &str) { LocalPlayer *localPlayer = Main::get().getLocalPlayer(); Networking *networking = Main::get().getNetworking(); localPlayer->chatMessage = str; networking->getPlayerPacket(ID_CHAT_MESSAGE)->setPlayer(localPlayer); networking->getPlayerPacket(ID_CHAT_MESSAGE)->Send(); } void GUIChat::clean() { mHistory->setCaption(""); } void GUIChat::pressedChatMode() { windowState++; if (windowState == 3) windowState = 0; std::string chatMode = windowState == CHAT_DISABLED ? "Chat hidden" : windowState == CHAT_ENABLED ? "Chat visible" : "Chat appearing when needed"; LOG_MESSAGE_SIMPLE(TimedLog::LOG_VERBOSE, "Switch chat mode to %s", chatMode.c_str()); MWBase::Environment::get().getWindowManager()->messageBox(chatMode); switch (windowState) { case CHAT_DISABLED: setVisible(false); setEditState(false); break; case CHAT_ENABLED: setVisible(true); break; default: //CHAT_HIDDENMODE setVisible(true); curTime = 0; } } void GUIChat::setEditState(bool state) { editState = state; mCommandLine->setVisible(editState); MWBase::Environment::get().getWindowManager()->setKeyFocusWidget(editState ? mCommandLine : nullptr); } void GUIChat::pressedSay() { if (windowState == CHAT_DISABLED) return; if (!mCommandLine->getVisible()) LOG_MESSAGE_SIMPLE(TimedLog::LOG_VERBOSE, "Opening chat."); if (windowState == CHAT_HIDDENMODE) { setVisible(true); curTime = 0; } setEditState(true); } void GUIChat::keyPress(MyGUI::Widget *_sender, MyGUI::KeyCode key, MyGUI::Char _char) { if (mCommandHistory.empty()) return; // Traverse history with up and down arrows if (key == MyGUI::KeyCode::ArrowUp) { // If the user was editing a string, store it for later if (mCurrent == mCommandHistory.end()) mEditString = mCommandLine->getOnlyText(); if (mCurrent != mCommandHistory.begin()) { --mCurrent; mCommandLine->setCaption(*mCurrent); } } else if (key == MyGUI::KeyCode::ArrowDown) { if (mCurrent != mCommandHistory.end()) { ++mCurrent; if (mCurrent != mCommandHistory.end()) mCommandLine->setCaption(*mCurrent); else // Restore the edit string mCommandLine->setCaption(mEditString); } } } void GUIChat::update(float dt) { if (windowState == CHAT_HIDDENMODE && !editState && isVisible()) { curTime += dt; if (curTime >= delay) { setEditState(false); setVisible(false); } } } void GUIChat::setDelay(float newDelay) { this->delay = newDelay; } } ================================================ FILE: apps/openmw/mwmp/GUI/GUIChat.hpp ================================================ #ifndef OPENMW_GUICHAT_HPP #define OPENMW_GUICHAT_HPP #include #include #include #include "apps/openmw/mwgui/windowbase.hpp" namespace mwmp { class GUIController; class GUIChat : public MWGui::WindowBase { friend class GUIController; public: enum { CHAT_DISABLED = 0, CHAT_ENABLED, CHAT_HIDDENMODE } CHAT_WIN_STATE; MyGUI::EditBox* mCommandLine; MyGUI::EditBox* mHistory; typedef std::list StringList; // History of previous entered commands StringList mCommandHistory; StringList::iterator mCurrent; std::string mEditString; GUIChat(int x, int y, int w, int h); void pressedChatMode(); //switch chat mode void pressedSay(); // switch chat focus (if chat mode != CHAT_DISABLED) void setDelay(float newDelay); void update(float dt); virtual void onOpen(); virtual void onClose(); virtual bool exit(); bool getEditState(); void setFont(const std::string &fntName); void onResChange(int width, int height); // Print a message to the console, in specified color. void print(const std::string &msg, const std::string& color = "#FFFFFF"); // Clean chat void clean(); // These are pre-colored versions that you should use. /// Output from successful console command void printOK(const std::string &msg); /// Error message void printError(const std::string &msg); void send(const std::string &str); protected: private: void keyPress(MyGUI::Widget* _sender, MyGUI::KeyCode key, MyGUI::Char _char); void acceptCommand(MyGUI::EditBox* _sender); void setEditState(bool state); int windowState; bool editState; float delay; float curTime; }; } #endif //OPENMW_GUICHAT_HPP ================================================ FILE: apps/openmw/mwmp/GUI/GUIDialogList.cpp ================================================ #include #include "apps/openmw/mwbase/environment.hpp" #include "apps/openmw/mwgui/windowmanagerimp.hpp" #include #include #include #include "GUIDialogList.hpp" #include "../Main.hpp" #include "../Networking.hpp" #include "../LocalPlayer.hpp" using namespace mwmp; GUIDialogList::GUIDialogList(const std::string &message, const std::vector &list) : WindowModal("tes3mp_dialog_list.layout") { center(); // center window getWidget(mListBox, "ListBox"); getWidget(mMessage, "Message"); getWidget(mButton, "OkButton"); mButton->eventMouseButtonClick += MyGUI::newDelegate(this, &GUIDialogList::mousePressed); mMessage->setCaptionWithReplacing(message); for (size_t i = 0; i < list.size(); i++) mListBox->addItem(list[i]); } void GUIDialogList::mousePressed(MyGUI::Widget * /*widget*/) { setVisible(false); MWBase::Environment::get().getWindowManager()->popGuiMode(); size_t id = mListBox->getIndexSelected(); Main::get().getLocalPlayer()->guiMessageBox.data = MyGUI::utility::toString(id); Main::get().getNetworking()->getPlayerPacket(ID_GUI_MESSAGEBOX)->setPlayer(Main::get().getLocalPlayer()); Main::get().getNetworking()->getPlayerPacket(ID_GUI_MESSAGEBOX)->Send(); LOG_MESSAGE_SIMPLE(TimedLog::LOG_VERBOSE, "Selected id: %d", id); if (id == MyGUI::ITEM_NONE) return; std::string itemName = mListBox->getItemNameAt(mListBox->getIndexSelected()).asUTF8(); LOG_APPEND(TimedLog::LOG_VERBOSE, "name of item: '%s'", itemName.c_str()); } void GUIDialogList::onFrame(float frameDuration) { } ================================================ FILE: apps/openmw/mwmp/GUI/GUIDialogList.hpp ================================================ #ifndef OPENMW_GUIDIALOGLIST_HPP #define OPENMW_GUIDIALOGLIST_HPP #include "apps/openmw/mwgui/windowbase.hpp" namespace mwmp { class GUIDialogList : public MWGui::WindowModal { public: GUIDialogList(const std::string &message, const std::vector &list); void mousePressed(MyGUI::Widget *_widget); void onFrame(float frameDuration); private: bool mMarkedToDelete; MyGUI::EditBox *mMessage; MyGUI::ListBox *mListBox; MyGUI::Button *mButton; }; } #endif //OPENMW_GUIDIALOGLIST_HPP ================================================ FILE: apps/openmw/mwmp/GUI/GUILogin.cpp ================================================ #include "GUILogin.hpp" #include #include GUILogin::GUILogin() : WindowModal("tes3mp_login.layout") { center(); // center window setVisible(false); getWidget(mLogin, "EditLogin"); getWidget(mServer, "EditServer"); getWidget(mPort, "EditPort"); getWidget(mConnect, "ButtonConnect"); } ================================================ FILE: apps/openmw/mwmp/GUI/GUILogin.hpp ================================================ #ifndef OPENMW_GUILOGIN_HPP #define OPENMW_GUILOGIN_HPP #include "apps/openmw/mwgui/windowbase.hpp" class GUILogin : public MWGui::WindowModal { public: GUILogin(); MyGUI::EditBox* mLogin; MyGUI::EditBox* mServer; MyGUI::EditBox* mPort; protected: MyGUI::Button* mConnect; }; #endif //OPENMW_GUILOGIN_HPP ================================================ FILE: apps/openmw/mwmp/GUI/PlayerMarkerCollection.cpp ================================================ #include #include "PlayerMarkerCollection.hpp" using namespace mwmp; void PlayerMarkerCollection::addMarker(const ESM::CustomMarker &marker, bool triggerEvent) { mMarkers.insert(std::make_pair(marker.mCell, marker)); if (triggerEvent) eventMarkersChanged(); } void PlayerMarkerCollection::deleteMarker(const ESM::CustomMarker &marker) { std::pair range = mMarkers.equal_range(marker.mCell); for (ContainerType::iterator it = range.first; it != range.second; ++it) { if (it->second == marker) { mMarkers.erase(it); eventMarkersChanged(); return; } } throw std::runtime_error("can't find marker to delete"); } void PlayerMarkerCollection::updateMarker(const ESM::CustomMarker &marker, const std::string &newNote) { std::pair range = mMarkers.equal_range(marker.mCell); for (ContainerType::iterator it = range.first; it != range.second; ++it) { if (it->second == marker) { it->second.mNote = newNote; eventMarkersChanged(); return; } } throw std::runtime_error("can't find marker to update"); } void PlayerMarkerCollection::clear() { mMarkers.clear(); eventMarkersChanged(); } PlayerMarkerCollection::ContainerType::const_iterator PlayerMarkerCollection::begin() const { return mMarkers.begin(); } PlayerMarkerCollection::ContainerType::const_iterator PlayerMarkerCollection::end() const { return mMarkers.end(); } PlayerMarkerCollection::RangeType PlayerMarkerCollection::getMarkers(const ESM::CellId &cellId) const { return mMarkers.equal_range(cellId); } size_t PlayerMarkerCollection::size() const { return mMarkers.size(); } bool PlayerMarkerCollection::contains(const ESM::CustomMarker &marker) { std::pair range = mMarkers.equal_range(marker.mCell); for (ContainerType::iterator it = range.first; it != range.second; ++it) { if (it->second == marker) return true; } return false; } ================================================ FILE: apps/openmw/mwmp/GUI/PlayerMarkerCollection.hpp ================================================ // Copied from MWGui::CustomMarkerCollection #ifndef OPENMW_PLAYERMARKERCOLLECTION_HPP #define OPENMW_PLAYERMARKERCOLLECTION_HPP #include #include #include #include #include namespace mwmp { class PlayerMarkerCollection { public: void addMarker(const ESM::CustomMarker &marker, bool triggerEvent = true); void deleteMarker(const ESM::CustomMarker &marker); void updateMarker(const ESM::CustomMarker &marker, const std::string &newNote); void clear(); size_t size() const; typedef std::multimap ContainerType; typedef std::pair RangeType; ContainerType::const_iterator begin() const; ContainerType::const_iterator end() const; RangeType getMarkers(const ESM::CellId &cellId) const; typedef MyGUI::delegates::CMultiDelegate0 EventHandle_Void; EventHandle_Void eventMarkersChanged; bool contains(const ESM::CustomMarker &marker); private: ContainerType mMarkers; }; } #endif //OPENMW_PLAYERMARKERCOLLECTION_HPP ================================================ FILE: apps/openmw/mwmp/GUI/TextInputDialog.cpp ================================================ #include "TextInputDialog.hpp" #include "apps/openmw/mwbase/windowmanager.hpp" #include "apps/openmw/mwbase/environment.hpp" #include #include #include namespace mwmp { TextInputDialog::TextInputDialog() : MWGui::WindowModal("tes3mp_text_input.layout") { // Centre dialog center(); getWidget(mTextEdit, "TextEdit"); mTextEdit->eventEditSelectAccept += newDelegate(this, &TextInputDialog::onTextAccepted); MyGUI::Button *okButton; getWidget(okButton, "OKButton"); okButton->eventMouseButtonClick += MyGUI::newDelegate(this, &TextInputDialog::onOkClicked); // Make sure the edit box has focus MWBase::Environment::get().getWindowManager()->setKeyFocusWidget(mTextEdit); } void TextInputDialog::setNextButtonShow(bool shown) { MyGUI::Button *okButton; getWidget(okButton, "OKButton"); if (shown) okButton->setCaption(MWBase::Environment::get().getWindowManager()->getGameSettingString("sNext", "")); else okButton->setCaption(MWBase::Environment::get().getWindowManager()->getGameSettingString("sOK", "")); } void TextInputDialog::setEditPassword(bool value) { mTextEdit->setEditPassword(value); } void TextInputDialog::setTextLabel(const std::string &label) { setText("LabelT", label); } void TextInputDialog::setTextNote(const std::string ¬e) { setText("TextNote", note); } void TextInputDialog::onOpen() { WindowModal::onOpen(); // Make sure the edit box has focus MWBase::Environment::get().getWindowManager()->setKeyFocusWidget(mTextEdit); } bool TextInputDialog::exit() { return false; } // widget controls void TextInputDialog::onOkClicked(MyGUI::Widget *_sender) { if (mTextEdit->getCaption() == "") { //MWBase::Environment::get().getWindowManager()->messageBox ("#{sNotifyMessage37}"); MWBase::Environment::get().getWindowManager()->setKeyFocusWidget(mTextEdit); } else eventDone(this); } void TextInputDialog::onTextAccepted(MyGUI::Edit *_sender) { onOkClicked(_sender); } std::string TextInputDialog::getTextInput() const { return mTextEdit->getCaption(); } void TextInputDialog::setTextInput(const std::string &text) { mTextEdit->setCaption(text); } } ================================================ FILE: apps/openmw/mwmp/GUI/TextInputDialog.hpp ================================================ #ifndef OPENMW_TEXTINPUTDIALOG_HPP #define OPENMW_TEXTINPUTDIALOG_HPP #include "apps/openmw/mwgui/windowbase.hpp" namespace MWGui { class WindowManager; } namespace mwmp { class TextInputDialog : public MWGui::WindowModal { public: TextInputDialog(); std::string getTextInput() const; void setTextInput(const std::string &text); void setNextButtonShow(bool shown); void setTextLabel(const std::string &label); void setTextNote(const std::string ¬e); void setEditPassword(bool value); virtual void onOpen(); virtual bool exit(); /** Event : Dialog finished, OK button clicked.\n signature : void method()\n */ EventHandle_WindowBase eventDone; protected: void onOkClicked(MyGUI::Widget *_sender); void onTextAccepted(MyGUI::Edit *_sender); private: MyGUI::EditBox *mTextEdit; }; } #endif //OPENMW_TEXTINPUTDIALOG_HPP ================================================ FILE: apps/openmw/mwmp/GUIController.cpp ================================================ #include #include #include #include #include #include #include #include #include #include #include #include #include #include "../mwbase/environment.hpp" #include "../mwbase/windowmanager.hpp" #include "../mwbase/inputmanager.hpp" #include "../mwgui/mapwindow.hpp" #include "../mwworld/worldimp.hpp" #include "../mwworld/player.hpp" #include "../mwworld/cellstore.hpp" #include "GUIController.hpp" #include "Main.hpp" #include "Networking.hpp" #include "GUI/PlayerMarkerCollection.hpp" #include "GUI/GUIDialogList.hpp" #include "GUI/GUIChat.hpp" #include "LocalPlayer.hpp" #include "DedicatedPlayer.hpp" #include "PlayerList.hpp" mwmp::GUIController::GUIController(): mInputBox(0), mListBox(0) { mChat = nullptr; keySay = SDL_SCANCODE_Y; keyChatMode = SDL_SCANCODE_F2; } mwmp::GUIController::~GUIController() { } void mwmp::GUIController::cleanUp() { mPlayerMarkers.clear(); if (mChat != nullptr) delete mChat; mChat = nullptr; } void mwmp::GUIController::refreshGuiMode(MWGui::GuiMode guiMode) { if (MWBase::Environment::get().getWindowManager()->containsMode(guiMode)) { MWBase::Environment::get().getWindowManager()->removeGuiMode(guiMode); MWBase::Environment::get().getWindowManager()->pushGuiMode(guiMode); } } void mwmp::GUIController::setupChat() { assert(mChat == nullptr); float chatDelay = Settings::Manager::getFloat("delay", "Chat"); int chatY = Settings::Manager::getInt("y", "Chat"); int chatX = Settings::Manager::getInt("x", "Chat"); int chatW = Settings::Manager::getInt("w", "Chat"); int chatH = Settings::Manager::getInt("h", "Chat"); keySay = SDL_GetScancodeFromName(Settings::Manager::getString("keySay", "Chat").c_str()); keyChatMode = SDL_GetScancodeFromName(Settings::Manager::getString("keyChatMode", "Chat").c_str()); mChat = new GUIChat(chatX, chatY, chatW, chatH); mChat->setDelay(chatDelay); } void mwmp::GUIController::printChatMessage(std::string &msg) { if (mChat != nullptr) mChat->print(msg); } void mwmp::GUIController::setChatVisible(bool chatVisible) { mChat->setVisible(chatVisible); } void mwmp::GUIController::showDialogList(const mwmp::BasePlayer::GUIMessageBox &guiMessageBox) { MWBase::WindowManager *windowManager = MWBase::Environment::get().getWindowManager(); if (mListBox != NULL) { windowManager->removeDialog(mListBox); windowManager->removeCurrentModal(mListBox); mListBox = NULL; } std::vector list; std::string buf; for (const auto &data : guiMessageBox.data) { if (data == '\n') { list.push_back(buf); buf.erase(); continue; } buf += data; } list.push_back(buf); mListBox = new GUIDialogList(guiMessageBox.label, list); windowManager->pushGuiMode((MWGui::GuiMode)GM_TES3MP_ListBox); } void mwmp::GUIController::showMessageBox(const BasePlayer::GUIMessageBox &guiMessageBox) { MWBase::WindowManager *windowManager = MWBase::Environment::get().getWindowManager(); windowManager->messageBox(guiMessageBox.label); } std::vector splitString(const std::string &str, char delim = ';') { std::istringstream ss(str); std::vector result; std::string token; while (std::getline(ss, token, delim)) result.push_back(token); return result; } void mwmp::GUIController::showCustomMessageBox(const BasePlayer::GUIMessageBox &guiMessageBox) { MWBase::WindowManager* windowManager = MWBase::Environment::get().getWindowManager(); std::vector buttons = splitString(guiMessageBox.buttons); windowManager->interactiveMessageBox(guiMessageBox.label, buttons, false, true); } void mwmp::GUIController::showInputBox(const BasePlayer::GUIMessageBox &guiMessageBox) { MWBase::WindowManager *windowManager = MWBase::Environment::get().getWindowManager(); windowManager->removeDialog(mInputBox); windowManager->pushGuiMode((MWGui::GuiMode)GM_TES3MP_InputBox); mInputBox = 0; mInputBox = new TextInputDialog(); mInputBox->setEditPassword(guiMessageBox.type == BasePlayer::GUIMessageBox::PasswordDialog); mInputBox->setTextLabel(guiMessageBox.label); mInputBox->setTextNote(guiMessageBox.note); mInputBox->eventDone += MyGUI::newDelegate(this, &GUIController::onInputBoxDone); mInputBox->setVisible(true); } void mwmp::GUIController::onInputBoxDone(MWGui::WindowBase *parWindow) { LocalPlayer *localPlayer = Main::get().getLocalPlayer(); std::string textInput = mInputBox->getTextInput(); // Send input for password dialogs after it's been hashed and rehashed, for some slight // extra security that doesn't require the client to keep storing a salt if (localPlayer->guiMessageBox.type == BasePlayer::GUIMessageBox::PasswordDialog) { textInput = picosha2::hash256_hex_string(textInput); textInput = picosha2::hash256_hex_string(textInput + picosha2::hash256_hex_string(picosha2::hash256_hex_string((textInput)))); } localPlayer->guiMessageBox.data = textInput; PlayerPacket *playerPacket = Main::get().getNetworking()->getPlayerPacket(ID_GUI_MESSAGEBOX); playerPacket->setPlayer(Main::get().getLocalPlayer()); playerPacket->Send(); MWBase::WindowManager *windowManager = MWBase::Environment::get().getWindowManager(); windowManager->removeDialog(mInputBox); mInputBox = 0; windowManager->popGuiMode(); } bool mwmp::GUIController::pressedKey(int key) { MWBase::WindowManager *windowManager = MWBase::Environment::get().getWindowManager(); if (mChat == nullptr || windowManager->isConsoleMode() || windowManager->getMode() != MWGui::GM_None) return false; if (key == keyChatMode) { mChat->pressedChatMode(); return true; } else if (key == keySay) { mChat->pressedSay(); return true; } return false; } void mwmp::GUIController::changeChatMode() { mChat->pressedChatMode(); } bool mwmp::GUIController::getChatEditState() { return mChat->editState; } void mwmp::GUIController::update(float dt) { if (mChat != nullptr) mChat->update(dt); } void mwmp::GUIController::processCustomMessageBoxInput(int pressedButton) { LOG_MESSAGE_SIMPLE(TimedLog::LOG_VERBOSE, "Pressed: %d", pressedButton); LocalPlayer* localPlayer = Main::get().getLocalPlayer(); localPlayer->guiMessageBox.data = MyGUI::utility::toString(pressedButton); PlayerPacket* playerPacket = Main::get().getNetworking()->getPlayerPacket(ID_GUI_MESSAGEBOX); playerPacket->setPlayer(Main::get().getLocalPlayer()); playerPacket->Send(); } void mwmp::GUIController::WM_UpdateVisible(MWGui::GuiMode mode) { switch((int)mode) { case GM_TES3MP_InputBox: { if (mInputBox != 0) mInputBox->setVisible(true); break; } case GM_TES3MP_ListBox: { if (mListBox != 0) mListBox->setVisible(true); break; } default: break; } } class MarkerWidget: public MyGUI::Widget { MYGUI_RTTI_DERIVED(MarkerWidget) public: void setNormalColour(const MyGUI::Colour& colour) { mNormalColour = colour; setColour(colour); } void setHoverColour(const MyGUI::Colour& colour) { mHoverColour = colour; } private: MyGUI::Colour mNormalColour; MyGUI::Colour mHoverColour; void onMouseLostFocus(MyGUI::Widget* _new) { setColour(mNormalColour); } void onMouseSetFocus(MyGUI::Widget* _old) { setColour(mHoverColour); } }; ESM::CustomMarker mwmp::GUIController::createMarker(const RakNet::RakNetGUID &guid) { DedicatedPlayer *player = PlayerList::getPlayer(guid); ESM::CustomMarker mEditingMarker; if (!player) { LOG_MESSAGE_SIMPLE(TimedLog::LOG_ERROR, "Unknown player guid: %s", guid.ToString()); return mEditingMarker; } mEditingMarker.mNote = player->npc.mName; const ESM::Cell *playerCell = &player->cell; mEditingMarker.mCell = player->cell.mCellId; mEditingMarker.mWorldX = player->position.pos[0]; mEditingMarker.mWorldY = player->position.pos[1]; mEditingMarker.mCell.mPaged = playerCell->isExterior(); if (!playerCell->isExterior()) mEditingMarker.mCell.mWorldspace = playerCell->mName; else { mEditingMarker.mCell.mWorldspace = ESM::CellId::sDefaultWorldspace; // Don't remove these, or the markers will stop showing up in exteriors mEditingMarker.mCell.mIndex.mX = playerCell->getGridX(); mEditingMarker.mCell.mIndex.mY = playerCell->getGridY(); } return mEditingMarker; } void mwmp::GUIController::updatePlayersMarkers(MWGui::LocalMapBase *localMapBase) { std::vector::iterator markerWidgetIterator = localMapBase->mPlayerMarkerWidgets.begin(); for (; markerWidgetIterator != localMapBase->mPlayerMarkerWidgets.end(); ++markerWidgetIterator) MyGUI::Gui::getInstance().destroyWidget(*markerWidgetIterator); localMapBase->mPlayerMarkerWidgets.clear(); for (int dX = -localMapBase->mCellDistance; dX <= localMapBase->mCellDistance; ++dX) { for (int dY =-localMapBase->mCellDistance; dY <= localMapBase->mCellDistance; ++dY) { ESM::CellId cellId; cellId.mPaged = !localMapBase->mInterior; cellId.mWorldspace = (localMapBase->mInterior ? localMapBase->mPrefix : ESM::CellId::sDefaultWorldspace); cellId.mIndex.mX = localMapBase->mCurX+dX; cellId.mIndex.mY = localMapBase->mCurY+dY; PlayerMarkerCollection::RangeType markers = mPlayerMarkers.getMarkers(cellId); for (PlayerMarkerCollection::ContainerType::const_iterator markerIterator = markers.first; markerIterator != markers.second; ++markerIterator) { const ESM::CustomMarker &marker = markerIterator->second; MWGui::LocalMapBase::MarkerUserData markerPos (localMapBase->mLocalMapRender); MyGUI::IntPoint widgetPos = localMapBase->getMarkerPosition(marker.mWorldX, marker.mWorldY, markerPos); MyGUI::IntCoord widgetCoord(widgetPos.left - 8, widgetPos.top - 8, 16, 16); MarkerWidget* markerWidget = localMapBase->mLocalMap->createWidget("CustomMarkerButton", widgetCoord, MyGUI::Align::Default); markerWidget->setDepth(0); // Local_MarkerAboveFogLayer markerWidget->setUserString("ToolTipType", "Layout"); markerWidget->setUserString("ToolTipLayout", "TextToolTipOneLine"); markerWidget->setUserString("Caption_TextOneLine", MyGUI::TextIterator::toTagsString(marker.mNote)); markerWidget->setNormalColour(MyGUI::Colour(0.6f, 0.6f, 0.6f)); markerWidget->setHoverColour(MyGUI::Colour(1.0f, 1.0f, 1.0f)); markerWidget->setUserData(marker); markerWidget->setNeedMouseFocus(true); //localMapBase->customMarkerCreated(markerWidget); localMapBase->mPlayerMarkerWidgets.push_back(markerWidget); } } } localMapBase->redraw(); } void mwmp::GUIController::setGlobalMapMarkerTooltip(MWGui::MapWindow *mapWindow, MyGUI::Widget *markerWidget, int x, int y) { ESM::CellId cellId; cellId.mIndex.mX = x; cellId.mIndex.mY = y; cellId.mWorldspace = ESM::CellId::sDefaultWorldspace; cellId.mPaged = true; PlayerMarkerCollection::RangeType markers = mPlayerMarkers.getMarkers(cellId); std::vector destNotes; for (PlayerMarkerCollection::ContainerType::const_iterator it = markers.first; it != markers.second; ++it) destNotes.push_back(it->second.mNote); if (!destNotes.empty()) { MWGui::LocalMapBase::MarkerUserData data (nullptr); data.notes = destNotes; data.caption = markerWidget->getUserString("Caption_TextOneLine"); markerWidget->setUserData(data); markerWidget->setUserString("ToolTipType", "MapMarker"); } else markerWidget->setUserString("ToolTipType", "Layout"); } void mwmp::GUIController::updateGlobalMapMarkerTooltips(MWGui::MapWindow *mapWindow) { for (const auto &widget : mapWindow->mGlobalMapMarkers) { const int x = widget.first.first; const int y = widget.first.second; setGlobalMapMarkerTooltip(mapWindow, widget.second, x, y); } } ================================================ FILE: apps/openmw/mwmp/GUIController.hpp ================================================ #ifndef OPENMW_GUICONTROLLER_HPP #define OPENMW_GUICONTROLLER_HPP #include #include "apps/openmw/mwgui/mode.hpp" #include #include "GUI/PlayerMarkerCollection.hpp" #include "GUI/TextInputDialog.hpp" namespace MWGui { class LocalMapBase; class MapWindow; } namespace mwmp { class GUIDialogList; class GUIChat; class GUIController { public: enum GM { GM_VR_MetaMenu = MWGui::GM_QuickKeysMenu + 1, // Put this dummy GuiMode here because it's used in VR GM_TES3MP_InputBox, GM_TES3MP_ListBox }; GUIController(); ~GUIController(); void cleanUp(); void refreshGuiMode(MWGui::GuiMode guiMode); void setupChat(); void printChatMessage(std::string &msg); void setChatVisible(bool chatVisible); void showMessageBox(const BasePlayer::GUIMessageBox &guiMessageBox); void showCustomMessageBox(const BasePlayer::GUIMessageBox &guiMessageBox); void showInputBox(const BasePlayer::GUIMessageBox &guiMessageBox); void showDialogList(const BasePlayer::GUIMessageBox &guiMessageBox); /// Returns 0 if there was no events bool pressedKey(int key); void changeChatMode(); bool getChatEditState(); void update(float dt); void processCustomMessageBoxInput(int pressedButton); void WM_UpdateVisible(MWGui::GuiMode mode); void updatePlayersMarkers(MWGui::LocalMapBase *localMapBase); void updateGlobalMapMarkerTooltips(MWGui::MapWindow *pWindow); ESM::CustomMarker createMarker(const RakNet::RakNetGUID &guid); PlayerMarkerCollection mPlayerMarkers; private: void setGlobalMapMarkerTooltip(MWGui::MapWindow *mapWindow ,MyGUI::Widget* markerWidget, int x, int y); private: GUIChat *mChat; int keySay; int keyChatMode; long id; TextInputDialog *mInputBox; GUIDialogList *mListBox; void onInputBoxDone(MWGui::WindowBase* parWindow); //MyGUI::Widget *oldFocusWidget, *currentFocusWidget; }; } #endif //OPENMW_GUICONTROLLER_HPP ================================================ FILE: apps/openmw/mwmp/LocalActor.cpp ================================================ #include #include "../mwbase/environment.hpp" #include "../mwmechanics/mechanicsmanagerimp.hpp" #include "../mwmechanics/movement.hpp" #include "../mwrender/animation.hpp" #include "../mwworld/class.hpp" #include "../mwworld/inventorystore.hpp" #include "../mwworld/worldimp.hpp" #include "LocalActor.hpp" #include "Main.hpp" #include "Networking.hpp" #include "ActorList.hpp" #include "MechanicsHelper.hpp" using namespace mwmp; LocalActor::LocalActor() { hasSentData = false; posWasChanged = false; equipmentChanged = false; wasRunning = false; wasSneaking = false; wasForceJumping = false; wasForceMoveJumping = false; wasFlying = false; attack.type = Attack::MELEE; attack.shouldSend = false; attack.instant = false; attack.pressed = false; cast.type = Cast::REGULAR; cast.shouldSend = false; cast.instant = false; cast.pressed = false; killer.isPlayer = false; killer.refId = ""; killer.name = ""; creatureStats.mDead = false; creatureStats.mDeathAnimationFinished = false; } LocalActor::~LocalActor() { } void LocalActor::update(bool forceUpdate) { updateStatsDynamic(forceUpdate); updateEquipment(forceUpdate, false); if (forceUpdate || !creatureStats.mDeathAnimationFinished) { updatePosition(forceUpdate); updateAnimFlags(forceUpdate); updateAnimPlay(); updateSpeech(); updateAttackOrCast(); } hasSentData = true; } void LocalActor::updateCell() { LOG_MESSAGE_SIMPLE(TimedLog::LOG_VERBOSE, "Sending ID_ACTOR_CELL_CHANGE about %s %i-%i in cell %s to server", refId.c_str(), refNum, mpNum, cell.getShortDescription().c_str()); LOG_APPEND(TimedLog::LOG_VERBOSE, "- Moved to cell %s", ptr.getCell()->getCell()->getShortDescription().c_str()); cell = *ptr.getCell()->getCell(); position = ptr.getRefData().getPosition(); isFollowerCellChange = false; mwmp::Main::get().getNetworking()->getActorList()->addCellChangeActor(*this); } void LocalActor::updatePosition(bool forceUpdate) { bool posIsChanging = false; if (creatureStats.mDead) { ESM::Position ptrPosition = ptr.getRefData().getPosition(); posIsChanging = position.pos[0] != ptrPosition.pos[0] || position.pos[1] != ptrPosition.pos[1] || position.pos[2] != ptrPosition.pos[2]; } else { posIsChanging = direction.pos[0] != 0 || direction.pos[1] != 0 || direction.pos[2] != 0 || direction.rot[0] != 0 || direction.rot[1] != 0 || direction.rot[2] != 0 || !MWBase::Environment::get().getWorld()->isOnGround(ptr); } if (forceUpdate || posIsChanging || posWasChanged) { posWasChanged = posIsChanging; position = ptr.getRefData().getPosition(); mwmp::Main::get().getNetworking()->getActorList()->addPositionActor(*this); } } void LocalActor::updateAnimFlags(bool forceUpdate) { MWBase::World *world = MWBase::Environment::get().getWorld(); MWMechanics::CreatureStats ptrCreatureStats = ptr.getClass().getCreatureStats(ptr); using namespace MWMechanics; bool isRunning = ptrCreatureStats.getMovementFlag(CreatureStats::Flag_Run); bool isSneaking = ptrCreatureStats.getMovementFlag(CreatureStats::Flag_Sneak); bool isForceJumping = ptrCreatureStats.getMovementFlag(CreatureStats::Flag_ForceJump); bool isForceMoveJumping = ptrCreatureStats.getMovementFlag(CreatureStats::Flag_ForceMoveJump); isFlying = world->isFlying(ptr); MWMechanics::DrawState_ currentDrawState = ptr.getClass().getCreatureStats(ptr).getDrawState(); if (wasRunning != isRunning || wasSneaking != isSneaking || wasForceJumping != isForceJumping || wasForceMoveJumping != isForceMoveJumping || lastDrawState != currentDrawState || wasFlying != isFlying || forceUpdate) { wasRunning = isRunning; wasSneaking = isSneaking; wasForceJumping = isForceJumping; wasForceMoveJumping = isForceMoveJumping; lastDrawState = currentDrawState; wasFlying = isFlying; movementFlags = 0; #define __SETFLAG(flag, value) (value) ? (movementFlags | flag) : (movementFlags & ~flag) movementFlags = __SETFLAG(CreatureStats::Flag_Sneak, isSneaking); movementFlags = __SETFLAG(CreatureStats::Flag_Run, isRunning); movementFlags = __SETFLAG(CreatureStats::Flag_ForceJump, isForceJumping); movementFlags = __SETFLAG(CreatureStats::Flag_ForceMoveJump, isForceMoveJumping); #undef __SETFLAG drawState = currentDrawState; mwmp::Main::get().getNetworking()->getActorList()->addAnimFlagsActor(*this); } } void LocalActor::updateAnimPlay() { if (!animation.groupname.empty()) { mwmp::Main::get().getNetworking()->getActorList()->addAnimPlayActor(*this); animation.groupname.clear(); } } void LocalActor::updateSpeech() { if (!sound.empty()) { mwmp::Main::get().getNetworking()->getActorList()->addSpeechActor(*this); sound.clear(); } } void LocalActor::updateStatsDynamic(bool forceUpdate) { MWMechanics::CreatureStats *ptrCreatureStats = &ptr.getClass().getCreatureStats(ptr); MWMechanics::DynamicStat health(ptrCreatureStats->getHealth()); MWMechanics::DynamicStat magicka(ptrCreatureStats->getMagicka()); MWMechanics::DynamicStat fatigue(ptrCreatureStats->getFatigue()); // Update stats when they become 0 or they have changed enough // // Also check for an oldHealth of 0 changing to something else for resurrected NPCs auto needUpdate = [](MWMechanics::DynamicStat &oldVal, MWMechanics::DynamicStat &newVal, int limit) { return oldVal != newVal && (newVal.getCurrent() == 0 || oldVal.getCurrent() == 0 || abs(oldVal.getCurrent() - newVal.getCurrent()) >= limit); }; if (forceUpdate || needUpdate(oldHealth, health, 3) || needUpdate(oldMagicka, magicka, 7) || needUpdate(oldFatigue, fatigue, 7)) { oldHealth = health; oldMagicka = magicka; oldFatigue = fatigue; health.writeState(creatureStats.mDynamic[0]); magicka.writeState(creatureStats.mDynamic[1]); fatigue.writeState(creatureStats.mDynamic[2]); creatureStats.mDead = ptrCreatureStats->isDead(); creatureStats.mDeathAnimationFinished = ptrCreatureStats->isDeathAnimationFinished(); mwmp::Main::get().getNetworking()->getActorList()->addStatsDynamicActor(*this); } } void LocalActor::updateEquipment(bool forceUpdate, bool sendImmediately) { if (!ptr.getClass().hasInventoryStore(ptr)) return; MWWorld::InventoryStore &invStore = ptr.getClass().getInventoryStore(ptr); // If we've never sent any data, autoEquip the actor just in case its inventory // slots have been cleared by a previous Container packet if (!hasSentData) invStore.autoEquip(ptr); if (forceUpdate) equipmentChanged = true; for (int slot = 0; slot < MWWorld::InventoryStore::Slots; slot++) { auto &item = equipmentItems[slot]; MWWorld::ContainerStoreIterator it = invStore.getSlot(slot); if (it != invStore.end()) { auto &cellRef = it->getCellRef(); if (!::Misc::StringUtils::ciEqual(cellRef.getRefId(), item.refId)) { equipmentChanged = true; item.refId = cellRef.getRefId(); item.charge = cellRef.getCharge(); item.enchantmentCharge = it->getCellRef().getEnchantmentCharge(); item.count = it->getRefData().getCount(); } } else if (!item.refId.empty()) { equipmentChanged = true; item.refId = ""; item.count = 0; item.charge = -1; item.enchantmentCharge = -1; } } if (equipmentChanged) { if (sendImmediately) sendEquipment(); else mwmp::Main::get().getNetworking()->getActorList()->addEquipmentActor(*this); equipmentChanged = false; } } void LocalActor::updateAttackOrCast() { if (attack.shouldSend) { mwmp::Main::get().getNetworking()->getActorList()->addAttackActor(*this); attack.shouldSend = false; } else if (cast.shouldSend) { mwmp::Main::get().getNetworking()->getActorList()->addCastActor(*this); cast.shouldSend = false; cast.hasProjectile = false; } } void LocalActor::sendEquipment() { ActorList actorList; actorList.cell = cell; actorList.addActor(*this); Main::get().getNetworking()->getActorPacket(ID_ACTOR_EQUIPMENT)->setActorList(&actorList); Main::get().getNetworking()->getActorPacket(ID_ACTOR_EQUIPMENT)->Send(); } void LocalActor::sendSpellsActiveAddition(const std::string id, bool isStackingSpell, const MWMechanics::ActiveSpells::ActiveSpellParams& params) { // Skip any bugged spells that somehow have clientside-only dynamic IDs if (id.find("$dynamic") != std::string::npos) return; spellsActiveChanges.activeSpells.clear(); const MWWorld::Ptr& caster = MWBase::Environment::get().getWorld()->searchPtrViaActorId(params.mCasterActorId); mwmp::ActiveSpell spell; spell.id = id; spell.isStackingSpell = isStackingSpell; spell.caster = MechanicsHelper::getTarget(caster); spell.timestampDay = params.mTimeStamp.getDay(); spell.timestampHour = params.mTimeStamp.getHour(); spell.params.mEffects = params.mEffects; spell.params.mDisplayName = params.mDisplayName; spellsActiveChanges.activeSpells.push_back(spell); spellsActiveChanges.action = mwmp::SpellsActiveChanges::ADD; ActorList actorList; actorList.cell = cell; actorList.addActor(*this); Main::get().getNetworking()->getActorPacket(ID_ACTOR_SPELLS_ACTIVE)->setActorList(&actorList); Main::get().getNetworking()->getActorPacket(ID_ACTOR_SPELLS_ACTIVE)->Send(); } void LocalActor::sendSpellsActiveRemoval(const std::string id, bool isStackingSpell, MWWorld::TimeStamp timestamp) { // Skip any bugged spells that somehow have clientside-only dynamic IDs if (id.find("$dynamic") != std::string::npos) return; spellsActiveChanges.activeSpells.clear(); mwmp::ActiveSpell spell; spell.id = id; spell.isStackingSpell = isStackingSpell; spell.timestampDay = timestamp.getDay(); spell.timestampHour = timestamp.getHour(); spellsActiveChanges.activeSpells.push_back(spell); spellsActiveChanges.action = mwmp::SpellsActiveChanges::REMOVE; ActorList actorList; actorList.cell = cell; actorList.addActor(*this); Main::get().getNetworking()->getActorPacket(ID_ACTOR_SPELLS_ACTIVE)->setActorList(&actorList); Main::get().getNetworking()->getActorPacket(ID_ACTOR_SPELLS_ACTIVE)->Send(); } void LocalActor::sendDeath(char newDeathState) { deathState = newDeathState; if (MechanicsHelper::isEmptyTarget(killer)) killer = MechanicsHelper::getTarget(ptr); LOG_MESSAGE_SIMPLE(TimedLog::LOG_INFO, "Sending ID_ACTOR_DEATH about %s %i-%i in cell %s to server\n- deathState: %d", refId.c_str(), refNum, mpNum, cell.getShortDescription().c_str(), deathState); ActorList actorList; actorList.cell = cell; actorList.addActor(*this); Main::get().getNetworking()->getActorPacket(ID_ACTOR_DEATH)->setActorList(&actorList); Main::get().getNetworking()->getActorPacket(ID_ACTOR_DEATH)->Send(); MechanicsHelper::clearTarget(killer); } MWWorld::Ptr LocalActor::getPtr() { return ptr; } void LocalActor::setPtr(const MWWorld::Ptr& newPtr) { ptr = newPtr; refId = ptr.getCellRef().getRefId(); refNum = ptr.getCellRef().getRefNum().mIndex; mpNum = ptr.getCellRef().getMpNum(); lastDrawState = ptr.getClass().getCreatureStats(ptr).getDrawState(); oldHealth = ptr.getClass().getCreatureStats(ptr).getHealth(); oldMagicka = ptr.getClass().getCreatureStats(ptr).getMagicka(); oldFatigue = ptr.getClass().getCreatureStats(ptr).getFatigue(); } ================================================ FILE: apps/openmw/mwmp/LocalActor.hpp ================================================ #ifndef OPENMW_LOCALACTOR_HPP #define OPENMW_LOCALACTOR_HPP #include #include "../mwmechanics/creaturestats.hpp" #include "../mwmechanics/activespells.hpp" #include "../mwworld/manualref.hpp" #include "../mwworld/timestamp.hpp" namespace mwmp { class LocalActor : public BaseActor { public: LocalActor(); virtual ~LocalActor(); void update(bool forceUpdate); void updateCell(); void updatePosition(bool forceUpdate); void updateAnimFlags(bool forceUpdate); void updateAnimPlay(); void updateSpeech(); void updateStatsDynamic(bool forceUpdate); void updateEquipment(bool forceUpdate, bool sendImmediately = false); void updateAttackOrCast(); void sendEquipment(); void sendSpellsActiveAddition(const std::string id, bool isStackingSpell, const MWMechanics::ActiveSpells::ActiveSpellParams& params); void sendSpellsActiveRemoval(const std::string id, bool isStackingSpell, MWWorld::TimeStamp timestamp); void sendDeath(char newDeathState); MWWorld::Ptr getPtr(); void setPtr(const MWWorld::Ptr& newPtr); bool hasSentData; private: MWWorld::Ptr ptr; bool posWasChanged; bool equipmentChanged; bool wasRunning; bool wasSneaking; bool wasForceJumping; bool wasForceMoveJumping; bool wasJumping; bool wasFlying; MWMechanics::DrawState_ lastDrawState; MWMechanics::DynamicStat oldHealth; MWMechanics::DynamicStat oldMagicka; MWMechanics::DynamicStat oldFatigue; }; } #endif //OPENMW_LOCALACTOR_HPP ================================================ FILE: apps/openmw/mwmp/LocalPlayer.cpp ================================================ #include #include #include #include "../mwbase/environment.hpp" #include "../mwbase/journal.hpp" #include "../mwbase/soundmanager.hpp" #include "../mwclass/creature.hpp" #include "../mwclass/npc.hpp" #include "../mwdialogue/dialoguemanagerimp.hpp" #include "../mwgui/inventorywindow.hpp" #include "../mwgui/windowmanagerimp.hpp" #include "../mwinput/inputmanagerimp.hpp" #include "../mwmechanics/activespells.hpp" #include "../mwmechanics/aitravel.hpp" #include "../mwmechanics/creaturestats.hpp" #include "../mwmechanics/mechanicsmanagerimp.hpp" #include "../mwmechanics/spellcasting.hpp" #include "../mwmechanics/spellutil.hpp" #include "../mwscript/scriptmanagerimp.hpp" #include "../mwstate/statemanagerimp.hpp" #include "../mwworld/cellstore.hpp" #include "../mwworld/customdata.hpp" #include "../mwworld/inventorystore.hpp" #include "../mwworld/manualref.hpp" #include "../mwworld/player.hpp" #include "../mwworld/worldimp.hpp" #include "LocalPlayer.hpp" #include "Main.hpp" #include "Networking.hpp" #include "PlayerList.hpp" #include "CellController.hpp" #include "GUIController.hpp" #include "MechanicsHelper.hpp" using namespace mwmp; std::map storedItemRemovals; LocalPlayer::LocalPlayer() { deathTime = time(0); receivedCharacter = false; charGenState.currentStage = 0; charGenState.endStage = 1; charGenState.isFinished = false; ignorePosPacket = false; ignoreJailTeleportation = false; ignoreJailSkillIncreases = false; attack.shouldSend = false; attack.instant = false; attack.pressed = false; cast.shouldSend = false; cast.instant = false; cast.pressed = false; killer.isPlayer = false; killer.refId = ""; killer.name = ""; isChangingRegion = false; jailProgressText = ""; jailEndText = ""; isUsingBed = false; avoidSendingInventoryPackets = false; isReceivingQuickKeys = false; isPlayingAnimation = false; diedSinceArrestAttempt = false; } LocalPlayer::~LocalPlayer() { } Networking *LocalPlayer::getNetworking() { return mwmp::Main::get().getNetworking(); } MWWorld::Ptr LocalPlayer::getPlayerPtr() { return MWBase::Environment::get().getWorld()->getPlayerPtr(); } void LocalPlayer::update() { static float updateTimer = 0; const float timeoutSec = 0.015; if ((updateTimer += MWBase::Environment::get().getFrameDuration()) >= timeoutSec) { updateTimer = 0; updateCell(); updatePosition(); updateAnimFlags(); updateAttackOrCast(); updateEquipment(); updateStatsDynamic(); updateAttributes(); updateSkills(); updateLevel(); updateBounty(); updateReputation(); } } bool LocalPlayer::processCharGen() { MWBase::WindowManager *windowManager = MWBase::Environment::get().getWindowManager(); // If we haven't finished CharGen and we're in a menu, it must be // one of the CharGen menus, so go no further until it's closed if (windowManager->isGuiMode() && !charGenState.isFinished) { return false; } // If the current stage of CharGen is not the last one, // move to the next one else if (charGenState.currentStage < charGenState.endStage) { switch (charGenState.currentStage) { case 0: windowManager->pushGuiMode(MWGui::GM_Name); break; case 1: windowManager->pushGuiMode(MWGui::GM_Race); break; case 2: windowManager->pushGuiMode(MWGui::GM_Class); break; case 3: windowManager->pushGuiMode(MWGui::GM_Birth); break; default: windowManager->pushGuiMode(MWGui::GM_Review); break; } getNetworking()->getPlayerPacket(ID_PLAYER_CHARGEN)->setPlayer(this); getNetworking()->getPlayerPacket(ID_PLAYER_CHARGEN)->Send(); return false; } // If we've reached the last stage of CharGen, send the // corresponding packets and mark CharGen as finished else if (!charGenState.isFinished) { MWBase::World *world = MWBase::Environment::get().getWorld(); MWWorld::Ptr ptrPlayer = world->getPlayerPtr(); npc = *ptrPlayer.get()->mBase; birthsign = world->getPlayer().getBirthSign(); LOG_MESSAGE_SIMPLE(TimedLog::LOG_INFO, "Sending ID_PLAYER_BASEINFO to server with my CharGen info"); getNetworking()->getPlayerPacket(ID_PLAYER_BASEINFO)->setPlayer(this); getNetworking()->getPlayerPacket(ID_PLAYER_BASEINFO)->Send(); // Send stats packets if this is the 2nd round of CharGen that // only happens for new characters if (charGenState.endStage != 1) { updateStatsDynamic(true); updateAttributes(true); updateSkills(true); updateLevel(true); sendClass(); sendSpellbook(); getNetworking()->getPlayerPacket(ID_PLAYER_CHARGEN)->setPlayer(this); getNetworking()->getPlayerPacket(ID_PLAYER_CHARGEN)->Send(); } // Mark character generation as finished until overridden by a new ID_PLAYER_CHARGEN packet charGenState.isFinished = true; } return true; } bool LocalPlayer::isLoggedIn() { if (charGenState.isFinished && (charGenState.endStage > 1 || receivedCharacter)) return true; return false; } void LocalPlayer::updateStatsDynamic(bool forceUpdate) { if (statsDynamicIndexChanges.size() > 0) statsDynamicIndexChanges.clear(); MWWorld::Ptr ptrPlayer = getPlayerPtr(); MWMechanics::CreatureStats *ptrCreatureStats = &ptrPlayer.getClass().getCreatureStats(ptrPlayer); MWMechanics::DynamicStat health(ptrCreatureStats->getHealth()); MWMechanics::DynamicStat magicka(ptrCreatureStats->getMagicka()); MWMechanics::DynamicStat fatigue(ptrCreatureStats->getFatigue()); static MWMechanics::DynamicStat oldHealth(ptrCreatureStats->getHealth()); static MWMechanics::DynamicStat oldMagicka(ptrCreatureStats->getMagicka()); static MWMechanics::DynamicStat oldFatigue(ptrCreatureStats->getFatigue()); // Update stats when they become 0 or they have changed enough auto needUpdate = [](MWMechanics::DynamicStat &oldVal, MWMechanics::DynamicStat &newVal, int limit) { return oldVal != newVal && (newVal.getCurrent() == 0 || oldVal.getCurrent() == 0 || abs(oldVal.getCurrent() - newVal.getCurrent()) >= limit); }; if (forceUpdate || needUpdate(oldHealth, health, 2)) statsDynamicIndexChanges.push_back(0); if (forceUpdate || needUpdate(oldMagicka, magicka, 4)) statsDynamicIndexChanges.push_back(1); if (forceUpdate || needUpdate(oldFatigue, fatigue, 4)) statsDynamicIndexChanges.push_back(2); if (forceUpdate || statsDynamicIndexChanges.size() > 0) { oldHealth = health; oldMagicka = magicka; oldFatigue = fatigue; health.writeState(creatureStats.mDynamic[0]); magicka.writeState(creatureStats.mDynamic[1]); fatigue.writeState(creatureStats.mDynamic[2]); creatureStats.mDead = ptrCreatureStats->isDead(); exchangeFullInfo = false; getNetworking()->getPlayerPacket(ID_PLAYER_STATS_DYNAMIC)->setPlayer(this); getNetworking()->getPlayerPacket(ID_PLAYER_STATS_DYNAMIC)->Send(); } } void LocalPlayer::updateAttributes(bool forceUpdate) { // Only send attributes if we are not a werewolf, or they will be // overwritten by the werewolf ones if (isWerewolf) return; if (attributeIndexChanges.size() > 0) attributeIndexChanges.clear(); MWWorld::Ptr ptrPlayer = getPlayerPtr(); const MWMechanics::NpcStats &ptrNpcStats = ptrPlayer.getClass().getNpcStats(ptrPlayer); for (int i = 0; i < 8; ++i) { if (ptrNpcStats.getAttribute(i).getBase() != creatureStats.mAttributes[i].mBase || ptrNpcStats.getAttribute(i).getModifier() != creatureStats.mAttributes[i].mMod || ptrNpcStats.getAttribute(i).getDamage() != creatureStats.mAttributes[i].mDamage || ptrNpcStats.getSkillIncrease(i) != npcStats.mSkillIncrease[i] || forceUpdate) { attributeIndexChanges.push_back(i); ptrNpcStats.getAttribute(i).writeState(creatureStats.mAttributes[i]); npcStats.mSkillIncrease[i] = ptrNpcStats.getSkillIncrease(i); } } if (attributeIndexChanges.size() > 0) { exchangeFullInfo = false; getNetworking()->getPlayerPacket(ID_PLAYER_ATTRIBUTE)->setPlayer(this); getNetworking()->getPlayerPacket(ID_PLAYER_ATTRIBUTE)->Send(); } } void LocalPlayer::updateSkills(bool forceUpdate) { // Only send skills if we are not a werewolf, or they will be // overwritten by the werewolf ones if (isWerewolf) return; if (skillIndexChanges.size() > 0) skillIndexChanges.clear(); MWWorld::Ptr ptrPlayer = getPlayerPtr(); const MWMechanics::NpcStats &ptrNpcStats = ptrPlayer.getClass().getNpcStats(ptrPlayer); for (int i = 0; i < 27; ++i) { // Update a skill if its base value has changed at all or its progress has changed enough if (ptrNpcStats.getSkill(i).getBase() != npcStats.mSkills[i].mBase || ptrNpcStats.getSkill(i).getModifier() != npcStats.mSkills[i].mMod || ptrNpcStats.getSkill(i).getDamage() != npcStats.mSkills[i].mDamage || abs(ptrNpcStats.getSkill(i).getProgress() - npcStats.mSkills[i].mProgress) > 0.75 || forceUpdate) { skillIndexChanges.push_back(i); ptrNpcStats.getSkill(i).writeState(npcStats.mSkills[i]); } } if (skillIndexChanges.size() > 0) { exchangeFullInfo = false; getNetworking()->getPlayerPacket(ID_PLAYER_SKILL)->setPlayer(this); getNetworking()->getPlayerPacket(ID_PLAYER_SKILL)->Send(); } } void LocalPlayer::updateLevel(bool forceUpdate) { MWWorld::Ptr ptrPlayer = getPlayerPtr(); const MWMechanics::NpcStats &ptrNpcStats = ptrPlayer.getClass().getNpcStats(ptrPlayer); if (ptrNpcStats.getLevel() != creatureStats.mLevel || ptrNpcStats.getLevelProgress() != npcStats.mLevelProgress || forceUpdate) { creatureStats.mLevel = ptrNpcStats.getLevel(); npcStats.mLevelProgress = ptrNpcStats.getLevelProgress(); getNetworking()->getPlayerPacket(ID_PLAYER_LEVEL)->setPlayer(this); getNetworking()->getPlayerPacket(ID_PLAYER_LEVEL)->Send(); } } void LocalPlayer::updateBounty(bool forceUpdate) { MWWorld::Ptr ptrPlayer = getPlayerPtr(); const MWMechanics::NpcStats &ptrNpcStats = ptrPlayer.getClass().getNpcStats(ptrPlayer); if (ptrNpcStats.getBounty() != npcStats.mBounty || forceUpdate) { npcStats.mBounty = ptrNpcStats.getBounty(); getNetworking()->getPlayerPacket(ID_PLAYER_BOUNTY)->setPlayer(this); getNetworking()->getPlayerPacket(ID_PLAYER_BOUNTY)->Send(); } } void LocalPlayer::updateReputation(bool forceUpdate) { MWWorld::Ptr ptrPlayer = getPlayerPtr(); const MWMechanics::NpcStats &ptrNpcStats = ptrPlayer.getClass().getNpcStats(ptrPlayer); if (ptrNpcStats.getReputation() != npcStats.mReputation || forceUpdate) { npcStats.mReputation = ptrNpcStats.getReputation(); getNetworking()->getPlayerPacket(ID_PLAYER_REPUTATION)->setPlayer(this); getNetworking()->getPlayerPacket(ID_PLAYER_REPUTATION)->Send(); } } void LocalPlayer::updatePosition(bool forceUpdate) { MWBase::World *world = MWBase::Environment::get().getWorld(); MWWorld::Ptr ptrPlayer = world->getPlayerPtr(); static bool posWasChanged = false; static bool isJumping = false; static bool sentJumpEnd = true; static float oldRot[2] = {0}; position = ptrPlayer.getRefData().getPosition(); bool posIsChanging = (direction.pos[0] != 0 || direction.pos[1] != 0 || direction.rot[0] != 0 || direction.rot[1] != 0 || direction.rot[2] != 0); // Animations can change a player's position without actually creating directional movement, // so update positions accordingly if (!posIsChanging && isPlayingAnimation) { if (MWBase::Environment::get().getMechanicsManager()->checkAnimationPlaying(ptrPlayer, animation.groupname)) posIsChanging = true; else isPlayingAnimation = false; } if (forceUpdate || posIsChanging || posWasChanged) { oldRot[0] = position.rot[0]; oldRot[1] = position.rot[2]; posWasChanged = posIsChanging; if (!isJumping && !world->isOnGround(ptrPlayer) && !world->isFlying(ptrPlayer)) isJumping = true; getNetworking()->getPlayerPacket(ID_PLAYER_POSITION)->setPlayer(this); getNetworking()->getPlayerPacket(ID_PLAYER_POSITION)->Send(); } else if (isJumping && world->isOnGround(ptrPlayer)) { isJumping = false; sentJumpEnd = false; } // Packet with jump end position has to be sent one tick after above check else if (!sentJumpEnd) { sentJumpEnd = true; position = ptrPlayer.getRefData().getPosition(); getNetworking()->getPlayerPacket(ID_PLAYER_POSITION)->setPlayer(this); getNetworking()->getPlayerPacket(ID_PLAYER_POSITION)->Send(); } } void LocalPlayer::updateCell(bool forceUpdate) { const ESM::Cell *ptrCell = MWBase::Environment::get().getWorld()->getPlayerPtr().getCell()->getCell(); // If the LocalPlayer's Ptr cell is different from the LocalPlayer's packet cell, proceed if (forceUpdate || !Main::get().getCellController()->isSameCell(*ptrCell, cell)) { LOG_MESSAGE_SIMPLE(TimedLog::LOG_INFO, "Sending ID_PLAYER_CELL_CHANGE about LocalPlayer to server"); LOG_APPEND(TimedLog::LOG_INFO, "- Moved from %s to %s", cell.getShortDescription().c_str(), ptrCell->getShortDescription().c_str()); if (!Misc::StringUtils::ciEqual(cell.mRegion, ptrCell->mRegion)) { LOG_APPEND(TimedLog::LOG_INFO, "- Changed region from %s to %s", cell.mRegion.empty() ? "none" : cell.mRegion.c_str(), ptrCell->mRegion.empty() ? "none" : ptrCell->mRegion.c_str()); isChangingRegion = true; } cell = *ptrCell; previousCellPosition = position; // Make sure the position is updated before a cell packet is sent, or else // cell change events in server scripts will have the wrong player position updatePosition(true); getNetworking()->getPlayerPacket(ID_PLAYER_CELL_CHANGE)->setPlayer(this); getNetworking()->getPlayerPacket(ID_PLAYER_CELL_CHANGE)->Send(); isChangingRegion = false; // If this is an interior cell, are there any other players in it? If so, // enable their markers if (!ptrCell->isExterior()) { mwmp::PlayerList::enableMarkers(*ptrCell); } } } void LocalPlayer::updateEquipment(bool forceUpdate) { if (equipmentIndexChanges.size() > 0) equipmentIndexChanges.clear(); MWWorld::Ptr ptrPlayer = getPlayerPtr(); MWWorld::InventoryStore &invStore = ptrPlayer.getClass().getInventoryStore(ptrPlayer); for (int slot = 0; slot < MWWorld::InventoryStore::Slots; slot++) { auto &item = equipmentItems[slot]; MWWorld::ContainerStoreIterator it = invStore.getSlot(slot); if (it != invStore.end()) { MWWorld::CellRef &cellRef = it->getCellRef(); if (Misc::StringUtils::ciEqual(cellRef.getRefId(), item.refId) == false || cellRef.getCharge() != item.charge || Utils::compareFloats(cellRef.getEnchantmentCharge(), item.enchantmentCharge, 1.0f) == false || it->getRefData().getCount() != item.count || forceUpdate) { equipmentIndexChanges.push_back(slot); item.refId = it->getCellRef().getRefId(); item.count = it->getRefData().getCount(); item.charge = it->getCellRef().getCharge(); item.enchantmentCharge = it->getCellRef().getEnchantmentCharge(); } } else if (!item.refId.empty()) { equipmentIndexChanges.push_back(slot); item.refId = ""; item.count = 0; item.charge = -1; item.enchantmentCharge = -1; } } if (equipmentIndexChanges.size() > 0) { exchangeFullInfo = false; getNetworking()->getPlayerPacket(ID_PLAYER_EQUIPMENT)->setPlayer(this); getNetworking()->getPlayerPacket(ID_PLAYER_EQUIPMENT)->Send(); } } void LocalPlayer::updateInventory(bool forceUpdate) { static bool invChanged = false; if (forceUpdate) invChanged = true; MWWorld::Ptr ptrPlayer = getPlayerPtr(); MWWorld::InventoryStore &ptrInventory = ptrPlayer.getClass().getInventoryStore(ptrPlayer); mwmp::Item item; auto setItem = [](Item &item, const MWWorld::Ptr &iter) { item.refId = iter.getCellRef().getRefId(); if (item.refId.find("$dynamic") != std::string::npos) return true; item.count = iter.getRefData().getCount(); item.charge = iter.getCellRef().getCharge(); item.enchantmentCharge = iter.getCellRef().getEnchantmentCharge(); item.soul = iter.getCellRef().getSoul(); return false; }; if (!invChanged) { for (const auto &itemOld : inventoryChanges.items) { auto result = ptrInventory.begin(); for (; result != ptrInventory.end(); ++result) { if(setItem(item, *result)) continue; if (item == itemOld) break; } if (result == ptrInventory.end()) { invChanged = true; break; } } } if (!invChanged) { for (const auto &iter : ptrInventory) { if(setItem(item, iter)) continue; auto items = inventoryChanges.items; if (find(items.begin(), items.end(), item) == items.end()) { invChanged = true; break; } } } if (!invChanged) return; invChanged = false; sendInventory(); } void LocalPlayer::updateAttackOrCast() { if (attack.shouldSend) { getNetworking()->getPlayerPacket(ID_PLAYER_ATTACK)->setPlayer(this); getNetworking()->getPlayerPacket(ID_PLAYER_ATTACK)->Send(); attack.shouldSend = false; } else if (cast.shouldSend) { getNetworking()->getPlayerPacket(ID_PLAYER_CAST)->setPlayer(this); getNetworking()->getPlayerPacket(ID_PLAYER_CAST)->Send(); cast.shouldSend = false; cast.hasProjectile = false; } } void LocalPlayer::updateAnimFlags(bool forceUpdate) { MWBase::World *world = MWBase::Environment::get().getWorld(); MWWorld::Ptr ptrPlayer = world->getPlayerPtr(); MWMechanics::NpcStats ptrNpcStats = ptrPlayer.getClass().getNpcStats(ptrPlayer); using namespace MWMechanics; static bool wasRunning = ptrNpcStats.getMovementFlag(CreatureStats::Flag_Run); static bool wasSneaking = ptrNpcStats.getMovementFlag(CreatureStats::Flag_Sneak); static bool wasForceJumping = ptrNpcStats.getMovementFlag(CreatureStats::Flag_ForceJump); static bool wasForceMoveJumping = ptrNpcStats.getMovementFlag(CreatureStats::Flag_ForceMoveJump); bool isRunning = ptrNpcStats.getMovementFlag(CreatureStats::Flag_Run); bool isSneaking = ptrNpcStats.getMovementFlag(CreatureStats::Flag_Sneak); bool isForceJumping = ptrNpcStats.getMovementFlag(CreatureStats::Flag_ForceJump); bool isForceMoveJumping = ptrNpcStats.getMovementFlag(CreatureStats::Flag_ForceMoveJump); isFlying = world->isFlying(ptrPlayer); isJumping = !world->isOnGround(ptrPlayer) && !isFlying; // We need to send a new packet at the end of jumping, flying and TCL-ing too, // so keep track of what we were doing last frame static bool wasJumping = false; static bool wasFlying = false; static bool hadTcl = false; drawState = ptrPlayer.getClass().getNpcStats(ptrPlayer).getDrawState(); static char lastDrawState = ptrPlayer.getClass().getNpcStats(ptrPlayer).getDrawState(); if (wasRunning != isRunning || wasSneaking != isSneaking || wasForceJumping != isForceJumping || wasForceMoveJumping != isForceMoveJumping || lastDrawState != drawState || wasJumping || isJumping || wasFlying != isFlying || hadTcl != hasTcl || forceUpdate) { wasSneaking = isSneaking; wasRunning = isRunning; wasForceJumping = isForceJumping; wasForceMoveJumping = isForceMoveJumping; lastDrawState = drawState; wasJumping = isJumping; wasFlying = isFlying; hadTcl = hasTcl; movementFlags = 0; #define __SETFLAG(flag, value) (value) ? (movementFlags | flag) : (movementFlags & ~flag) movementFlags = __SETFLAG(CreatureStats::Flag_Sneak, isSneaking); movementFlags = __SETFLAG(CreatureStats::Flag_Run, isRunning); movementFlags = __SETFLAG(CreatureStats::Flag_ForceJump, isForceJumping); movementFlags = __SETFLAG(CreatureStats::Flag_ForceJump, isJumping); movementFlags = __SETFLAG(CreatureStats::Flag_ForceMoveJump, isForceMoveJumping); #undef __SETFLAG if (isJumping) updatePosition(true); // fix position after jump; getNetworking()->getPlayerPacket(ID_PLAYER_ANIM_FLAGS)->setPlayer(this); getNetworking()->getPlayerPacket(ID_PLAYER_ANIM_FLAGS)->Send(); } } void LocalPlayer::addItems() { MWWorld::Ptr ptrPlayer = getPlayerPtr(); const MWWorld::ESMStore &esmStore = MWBase::Environment::get().getWorld()->getStore(); MWWorld::ContainerStore &ptrStore = ptrPlayer.getClass().getContainerStore(ptrPlayer); for (const auto &item : inventoryChanges.items) { // Skip bound items if (MWBase::Environment::get().getMechanicsManager()->isBoundItem(item.refId)) continue; try { MWWorld::ManualRef itemRef(esmStore, item.refId, item.count); MWWorld::Ptr itemPtr = itemRef.getPtr(); if (item.charge != -1) itemPtr.getCellRef().setCharge(item.charge); if (item.enchantmentCharge != -1) itemPtr.getCellRef().setEnchantmentCharge(item.enchantmentCharge); if (!item.soul.empty()) itemPtr.getCellRef().setSoul(item.soul); LOG_APPEND(TimedLog::LOG_INFO, "- Adding inventory item %s with count %i", item.refId.c_str(), item.count); ptrStore.add(itemPtr, item.count, ptrPlayer); } catch (std::exception&) { LOG_APPEND(TimedLog::LOG_INFO, "- Ignored addition of invalid inventory item %s", item.refId.c_str()); } } updateInventoryWindow(); } void LocalPlayer::addSpells() { MWWorld::Ptr ptrPlayer = getPlayerPtr(); MWMechanics::Spells &ptrSpells = ptrPlayer.getClass().getCreatureStats(ptrPlayer).getSpells(); for (const auto &spell : spellbookChanges.spells) // Only add spells that are ensured to exist if (MWBase::Environment::get().getWorld()->getStore().get().search(spell.mId)) ptrSpells.add(spell.mId); else LOG_APPEND(TimedLog::LOG_INFO, "- Ignored addition of invalid spell %s", spell.mId.c_str()); } void LocalPlayer::addSpellsActive() { MWWorld::Ptr ptrPlayer = getPlayerPtr(); MWMechanics::ActiveSpells& activeSpells = ptrPlayer.getClass().getCreatureStats(ptrPlayer).getActiveSpells(); for (const auto& activeSpell : spellsActiveChanges.activeSpells) { MWWorld::TimeStamp timestamp = MWWorld::TimeStamp(activeSpell.timestampHour, activeSpell.timestampDay); int casterActorId = MechanicsHelper::getActorId(activeSpell.caster); // Don't do a check for a spell's existence, because active effects from potions need to be applied here too activeSpells.addSpell(activeSpell.id, activeSpell.isStackingSpell, activeSpell.params.mEffects, activeSpell.params.mDisplayName, casterActorId, timestamp, false); } } void LocalPlayer::addJournalItems() { for (const auto &journalItem : journalChanges) { MWWorld::Ptr ptrFound; if (journalItem.type == JournalItem::ENTRY) { LOG_APPEND(TimedLog::LOG_VERBOSE, "- type: ENTRY, quest: %s, index: %i, actorRefId: %s", journalItem.quest.c_str(), journalItem.index, journalItem.actorRefId.c_str()); ptrFound = MWBase::Environment::get().getWorld()->searchPtr(journalItem.actorRefId, false); if (!ptrFound) ptrFound = getPlayerPtr(); } else { LOG_APPEND(TimedLog::LOG_VERBOSE, "- type: INDEX, quest: %s, index: %i", journalItem.quest.c_str(), journalItem.index); } try { if (journalItem.type == JournalItem::ENTRY) { if (journalItem.hasTimestamp) { MWBase::Environment::get().getJournal()->addEntry(journalItem.quest, journalItem.index, ptrFound, journalItem.timestamp.daysPassed, journalItem.timestamp.month, journalItem.timestamp.day); } else { MWBase::Environment::get().getJournal()->addEntry(journalItem.quest, journalItem.index, ptrFound); } } else MWBase::Environment::get().getJournal()->setJournalIndex(journalItem.quest, journalItem.index); } catch (std::exception&) { LOG_APPEND(TimedLog::LOG_INFO, "- Ignored addition of invalid journal quest %s", journalItem.quest.c_str()); } } } void LocalPlayer::addTopics() { auto &env = MWBase::Environment::get(); for (const auto &topic : topicChanges) { std::string topicId = topic.topicId; // If we're using a translated version of Morrowind, translate this topic from English into our language if (env.getWindowManager()->getTranslationDataStorage().hasTranslation()) topicId = env.getWindowManager()->getTranslationDataStorage().getLocalizedTopicId(topicId); env.getDialogueManager()->addTopic(topicId); if (env.getWindowManager()->containsMode(MWGui::GM_Dialogue)) env.getDialogueManager()->updateActorKnownTopics(); } } void LocalPlayer::removeItems() { MWWorld::Ptr ptrPlayer = getPlayerPtr(); MWWorld::ContainerStore &ptrStore = ptrPlayer.getClass().getContainerStore(ptrPlayer); for (const auto &item : inventoryChanges.items) { ptrStore.remove(item.refId, item.count, ptrPlayer); LOG_APPEND(TimedLog::LOG_INFO, "- Removing inventory item %s with count %i", item.refId.c_str(), item.count); } } void LocalPlayer::removeSpells() { MWWorld::Ptr ptrPlayer = getPlayerPtr(); MWMechanics::Spells &ptrSpells = ptrPlayer.getClass().getCreatureStats(ptrPlayer).getSpells(); MWBase::WindowManager *wm = MWBase::Environment::get().getWindowManager(); for (const auto &spell : spellbookChanges.spells) { ptrSpells.remove(spell.mId); if (spell.mId == wm->getSelectedSpell()) wm->unsetSelectedSpell(); } } void LocalPlayer::removeSpellsActive() { MWWorld::Ptr ptrPlayer = getPlayerPtr(); MWMechanics::ActiveSpells& activeSpells = ptrPlayer.getClass().getCreatureStats(ptrPlayer).getActiveSpells(); for (const auto& activeSpell : spellsActiveChanges.activeSpells) { LOG_APPEND(TimedLog::LOG_INFO, "- removing %sstacking active spell %s", activeSpell.isStackingSpell ? "" : "non-", activeSpell.id.c_str()); // Remove stacking spells based on their timestamps if (activeSpell.isStackingSpell) { MWWorld::TimeStamp timestamp = MWWorld::TimeStamp(activeSpell.timestampHour, activeSpell.timestampDay); bool foundSpell = activeSpells.removeSpellByTimestamp(activeSpell.id, timestamp); if (!foundSpell) { LOG_APPEND(TimedLog::LOG_INFO, "-- spell with this ID and timestamp could not be found!"); } } else { activeSpells.removeEffects(activeSpell.id); } } } void LocalPlayer::die() { creatureStats.mDead = true; MWWorld::Ptr playerPtr = MWBase::Environment::get().getWorld()->getPlayerPtr(); MWMechanics::DynamicStat health = playerPtr.getClass().getCreatureStats(playerPtr).getHealth(); health.setCurrent(0); playerPtr.getClass().getCreatureStats(playerPtr).setHealth(health); Main::get().getNetworking()->getPlayerPacket(ID_PLAYER_DEATH)->setPlayer(this); Main::get().getNetworking()->getPlayerPacket(ID_PLAYER_DEATH)->Send(); } void LocalPlayer::resurrect() { creatureStats.mDead = false; MWWorld::Ptr ptrPlayer = getPlayerPtr(); if (resurrectType == mwmp::RESURRECT_TYPE::IMPERIAL_SHRINE) MWBase::Environment::get().getWorld()->teleportToClosestMarker(ptrPlayer, "divinemarker"); else if (resurrectType == mwmp::RESURRECT_TYPE::TRIBUNAL_TEMPLE) MWBase::Environment::get().getWorld()->teleportToClosestMarker(ptrPlayer, "templemarker"); MWBase::Environment::get().getMechanicsManager()->resurrect(ptrPlayer); // The player could have died from a hand-to-hand attack, so reset their fatigue // as well if (creatureStats.mDynamic[2].mMod < 1) creatureStats.mDynamic[2].mMod = 1; creatureStats.mDynamic[2].mCurrent = creatureStats.mDynamic[2].mMod; MWMechanics::DynamicStat fatigue; fatigue.readState(creatureStats.mDynamic[2]); ptrPlayer.getClass().getCreatureStats(ptrPlayer).setFatigue(fatigue); // If this player had a weapon or spell readied when dying, they will still have it // readied but be unable to use it unless we clear it here ptrPlayer.getClass().getNpcStats(ptrPlayer).setDrawState(MWMechanics::DrawState_Nothing); // Record that the player has died since the last attempt was made to arrest them, // used to make guards lenient enough to attempt an arrest again diedSinceArrestAttempt = true; deathTime = time(0); LOG_APPEND(TimedLog::LOG_INFO, "- diedSinceArrestAttempt is now true"); // Record that we are no longer a known werewolf, to avoid being attacked infinitely MWBase::Environment::get().getWorld()->setGlobalInt("pcknownwerewolf", 0); // Ensure we unequip any items with constant effects that can put us into an infinite // death loop static const int damageEffects[5] = { ESM::MagicEffect::DrainHealth, ESM::MagicEffect::FireDamage, ESM::MagicEffect::FrostDamage, ESM::MagicEffect::ShockDamage, ESM::MagicEffect::SunDamage }; for (const auto &damageEffect : damageEffects) MechanicsHelper::unequipItemsByEffect(ptrPlayer, ESM::Enchantment::ConstantEffect, damageEffect); Main::get().getNetworking()->getPlayerPacket(ID_PLAYER_RESURRECT)->setPlayer(this); Main::get().getNetworking()->getPlayerPacket(ID_PLAYER_RESURRECT)->Send(); updateStatsDynamic(true); } void LocalPlayer::closeInventoryWindows() { if (MWBase::Environment::get().getWindowManager()->containsMode(MWGui::GM_Container) || MWBase::Environment::get().getWindowManager()->containsMode(MWGui::GM_Inventory)) MWBase::Environment::get().getWindowManager()->popGuiMode(); MWBase::Environment::get().getWindowManager()->finishDragDrop(); } void LocalPlayer::updateInventoryWindow() { MWBase::Environment::get().getWindowManager()->getInventoryWindow()->updateItemView(); } void LocalPlayer::setCharacter() { receivedCharacter = true; MWBase::World *world = MWBase::Environment::get().getWorld(); // Ignore invalid races if (world->getStore().get().search(npc.mRace) != 0) { MWBase::Environment::get().getWorld()->getPlayer().setBirthSign(birthsign); if (resetStats) { MWBase::Environment::get().getMechanicsManager()->setPlayerRace(npc.mRace, npc.isMale(), npc.mHead, npc.mHair); setEquipment(); } else { ESM::NPC player = *world->getPlayerPtr().get()->mBase; player.mRace = npc.mRace; player.mHead = npc.mHead; player.mHair = npc.mHair; player.mModel = npc.mModel; player.setIsMale(npc.isMale()); world->createRecord(player); MWBase::Environment::get().getMechanicsManager()->playerLoaded(); // This is needed to update the player's model instantly if they're in 3rd person world->reattachPlayerCamera(); } MWBase::Environment::get().getWindowManager()->getInventoryWindow()->rebuildAvatar(); } else { LOG_APPEND(TimedLog::LOG_INFO, "- Character update was ignored due to invalid race %s", npc.mRace.c_str()); } } void LocalPlayer::setDynamicStats() { MWBase::World *world = MWBase::Environment::get().getWorld(); MWWorld::Ptr ptrPlayer = world->getPlayerPtr(); MWMechanics::CreatureStats *ptrCreatureStats = &ptrPlayer.getClass().getCreatureStats(ptrPlayer); MWMechanics::DynamicStat dynamicStat; for (int i = 0; i < 3; ++i) { dynamicStat = ptrCreatureStats->getDynamic(i); dynamicStat.setBase(creatureStats.mDynamic[i].mBase); dynamicStat.setCurrent(creatureStats.mDynamic[i].mCurrent); ptrCreatureStats->setDynamic(i, dynamicStat); } } void LocalPlayer::setAttributes() { MWWorld::Ptr ptrPlayer = getPlayerPtr(); MWMechanics::NpcStats *ptrNpcStats = &ptrPlayer.getClass().getNpcStats(ptrPlayer); MWMechanics::AttributeValue attributeValue; for (int attributeIndex = 0; attributeIndex < 8; ++attributeIndex) { // If the server wants to clear our attribute's non-zero modifier, we need to remove // the spell effect causing it, to avoid an infinite loop where the effect keeps resetting // the modifier if (creatureStats.mAttributes[attributeIndex].mMod == 0 && ptrNpcStats->getAttribute(attributeIndex).getModifier() > 0) { ptrNpcStats->getActiveSpells().purgeEffectByArg(ESM::MagicEffect::FortifyAttribute, attributeIndex); MWBase::Environment::get().getMechanicsManager()->updateMagicEffects(ptrPlayer); // Is the modifier for this attribute still higher than 0? If so, unequip items that // fortify the attribute if (ptrNpcStats->getAttribute(attributeIndex).getModifier() > 0) { MechanicsHelper::unequipItemsByEffect(ptrPlayer, ESM::Enchantment::ConstantEffect, ESM::MagicEffect::FortifyAttribute, attributeIndex, -1); mwmp::Main::get().getGUIController()->refreshGuiMode(MWGui::GM_Inventory); } } attributeValue.readState(creatureStats.mAttributes[attributeIndex]); ptrNpcStats->setAttribute(attributeIndex, attributeValue); ptrNpcStats->setSkillIncrease(attributeIndex, npcStats.mSkillIncrease[attributeIndex]); } } void LocalPlayer::setSkills() { MWWorld::Ptr ptrPlayer = getPlayerPtr(); MWMechanics::NpcStats *ptrNpcStats = &ptrPlayer.getClass().getNpcStats(ptrPlayer); MWMechanics::SkillValue skillValue; for (int skillIndex = 0; skillIndex < 27; ++skillIndex) { // If the server wants to clear our skill's non-zero modifier, we need to remove // the spell effect causing it, to avoid an infinite loop where the effect keeps resetting // the modifier if (npcStats.mSkills[skillIndex].mMod == 0 && ptrNpcStats->getSkill(skillIndex).getModifier() > 0) { ptrNpcStats->getActiveSpells().purgeEffectByArg(ESM::MagicEffect::FortifySkill, skillIndex); MWBase::Environment::get().getMechanicsManager()->updateMagicEffects(ptrPlayer); // Is the modifier for this skill still higher than 0? If so, unequip items that // fortify the skill if (ptrNpcStats->getSkill(skillIndex).getModifier() > 0) { MechanicsHelper::unequipItemsByEffect(ptrPlayer, ESM::Enchantment::ConstantEffect, ESM::MagicEffect::FortifySkill, -1, skillIndex); mwmp::Main::get().getGUIController()->refreshGuiMode(MWGui::GM_Inventory); } } skillValue.readState(npcStats.mSkills[skillIndex]); ptrNpcStats->setSkill(skillIndex, skillValue); } } void LocalPlayer::setLevel() { MWBase::World *world = MWBase::Environment::get().getWorld(); MWWorld::Ptr ptrPlayer = world->getPlayerPtr(); MWMechanics::NpcStats *ptrNpcStats = &ptrPlayer.getClass().getNpcStats(ptrPlayer); ptrNpcStats->setLevel(creatureStats.mLevel); ptrNpcStats->setLevelProgress(npcStats.mLevelProgress); } void LocalPlayer::setBounty() { MWBase::World *world = MWBase::Environment::get().getWorld(); MWWorld::Ptr ptrPlayer = world->getPlayerPtr(); MWMechanics::NpcStats *ptrNpcStats = &ptrPlayer.getClass().getNpcStats(ptrPlayer); ptrNpcStats->setBounty(npcStats.mBounty); } void LocalPlayer::setReputation() { MWBase::World *world = MWBase::Environment::get().getWorld(); MWWorld::Ptr ptrPlayer = world->getPlayerPtr(); MWMechanics::NpcStats *ptrNpcStats = &ptrPlayer.getClass().getNpcStats(ptrPlayer); ptrNpcStats->setReputation(npcStats.mReputation); } void LocalPlayer::setPosition() { MWBase::World *world = MWBase::Environment::get().getWorld(); MWWorld::Ptr ptrPlayer = world->getPlayerPtr(); // If we're ignoring this position packet because of an invalid cell change, // don't make the next one get ignored as well if (ignorePosPacket) ignorePosPacket = false; else { world->getPlayer().setTeleported(true); world->moveObject(ptrPlayer, position.pos[0], position.pos[1], position.pos[2]); world->rotateObject(ptrPlayer, position.rot[0], position.rot[1], position.rot[2]); world->setInertialForce(ptrPlayer, osg::Vec3f(0.f, 0.f, 0.f)); } updatePosition(true); // Make sure we update our draw state, or we'll end up with the wrong one updateAnimFlags(true); } void LocalPlayer::setMomentum() { MWBase::World *world = MWBase::Environment::get().getWorld(); MWWorld::Ptr ptrPlayer = world->getPlayerPtr(); world->setInertialForce(ptrPlayer, momentum.asVec3()); } void LocalPlayer::setCell() { MWBase::World *world = MWBase::Environment::get().getWorld(); MWWorld::Ptr ptrPlayer = world->getPlayerPtr(); ESM::Position pos; // To avoid crashes, close container windows this player may be in closeInventoryWindows(); world->getPlayer().setTeleported(true); int x = cell.mData.mX; int y = cell.mData.mY; if (cell.isExterior()) { world->indexToPosition(x, y, pos.pos[0], pos.pos[1], true); pos.pos[2] = 0; pos.rot[0] = pos.rot[1] = pos.rot[2] = 0; world->changeToExteriorCell(pos, true); world->fixPosition(); } else if (world->findExteriorPosition(cell.mName, pos)) { world->changeToExteriorCell(pos, true); world->fixPosition(); } else { try { world->findInteriorPosition(cell.mName, pos); world->changeToInteriorCell(cell.mName, pos, true); } // If we've been sent to an invalid interior, ignore the incoming // packet about our position in that cell catch (std::exception&) { LOG_APPEND(TimedLog::LOG_INFO, "%s", "- Cell doesn't exist on this client"); ignorePosPacket = true; } } updateCell(true); } void LocalPlayer::setClass() { LOG_MESSAGE_SIMPLE(TimedLog::LOG_INFO, "Received ID_PLAYER_CLASS from server"); if (charClass.mId.empty()) // custom class { charClass.mData.mIsPlayable = 0x1; MWBase::Environment::get().getMechanicsManager()->setPlayerClass(charClass); } else { const ESM::Class *existingCharClass = MWBase::Environment::get().getWorld()->getStore().get().search(charClass.mId); if (existingCharClass) { MWBase::Environment::get().getMechanicsManager()->setPlayerClass(charClass.mId); } else LOG_APPEND(TimedLog::LOG_INFO, "- Ignored invalid default class %s", charClass.mId.c_str()); } } void LocalPlayer::setEquipment() { MWWorld::Ptr ptrPlayer = getPlayerPtr(); MWWorld::InventoryStore &ptrInventory = ptrPlayer.getClass().getInventoryStore(ptrPlayer); for (int slot = 0; slot < MWWorld::InventoryStore::Slots; slot++) { mwmp::Item ¤tItem = equipmentItems[slot]; if (!currentItem.refId.empty()) { auto it = find_if(ptrInventory.begin(), ptrInventory.end(), [¤tItem](const MWWorld::Ptr &itemPtr) { return Misc::StringUtils::ciEqual(itemPtr.getCellRef().getRefId(), currentItem.refId); }); // If the item is not in our inventory, add it as long as it's not a bound item if (it == ptrInventory.end()) { if (!MWBase::Environment::get().getMechanicsManager()->isBoundItem(currentItem.refId)) { try { auto addIter = ptrInventory.ContainerStore::add(currentItem.refId.c_str(), currentItem.count, ptrPlayer); ptrInventory.equip(slot, addIter, ptrPlayer); } catch (std::exception&) { LOG_APPEND(TimedLog::LOG_INFO, "- Ignored addition of invalid equipment item %s", currentItem.refId.c_str()); } } } else { // Don't try to equip an item that is already equipped if (ptrInventory.getSlot(slot) != it) ptrInventory.equip(slot, it, ptrPlayer); } } else ptrInventory.unequipSlot(slot, ptrPlayer); } MWBase::Environment::get().getWindowManager()->getInventoryWindow()->updatePlayer(); } void LocalPlayer::setInventory() { MWWorld::Ptr ptrPlayer = getPlayerPtr(); MWWorld::ContainerStore &ptrStore = ptrPlayer.getClass().getContainerStore(ptrPlayer); // Ensure no item is being drag and dropped MWBase::Environment::get().getWindowManager()->finishDragDrop(); // Clear items in inventory ptrStore.clear(); // Proceed by adding items addItems(); // Don't automatically setEquipment() here, or the player could end // up getting a new set of their starting clothes, or other items // supposed to no longer exist // // Instead, expect server scripts to do that manually } void LocalPlayer::setSpellbook() { MWWorld::Ptr ptrPlayer = getPlayerPtr(); MWMechanics::Spells &ptrSpells = ptrPlayer.getClass().getCreatureStats(ptrPlayer).getSpells(); // Clear spells in spellbook, while ignoring abilities, powers, etc. while (true) { MWMechanics::Spells::TIterator iter = ptrSpells.begin(); for (; iter != ptrSpells.end(); iter++) { const ESM::Spell *spell = iter->first; if (spell->mData.mType == ESM::Spell::ST_Spell) { ptrSpells.remove(spell->mId); break; } } if (iter == ptrSpells.end()) break; } // Proceed by adding spells addSpells(); } void LocalPlayer::setSpellsActive() { MWWorld::Ptr ptrPlayer = getPlayerPtr(); MWMechanics::ActiveSpells& activeSpells = ptrPlayer.getClass().getCreatureStats(ptrPlayer).getActiveSpells(); activeSpells.clear(); // Proceed by adding spells active addSpellsActive(); } void LocalPlayer::setCooldowns() { MWBase::World* world = MWBase::Environment::get().getWorld(); MWWorld::Ptr ptrPlayer = getPlayerPtr(); MWMechanics::Spells& ptrSpells = ptrPlayer.getClass().getCreatureStats(ptrPlayer).getSpells(); for (const auto& cooldown : cooldownChanges) { if (world->getStore().get().search(cooldown.id)) { const ESM::Spell* spell = world->getStore().get().search(cooldown.id); ptrSpells.setPowerUseTimestamp(spell, cooldown.startTimestampDay, cooldown.startTimestampHour); } } } void LocalPlayer::setQuickKeys() { MWWorld::Ptr ptrPlayer = getPlayerPtr(); LOG_MESSAGE_SIMPLE(TimedLog::LOG_INFO, "Received ID_PLAYER_QUICKKEYS from server"); for (const auto &quickKey : quickKeyChanges) { LOG_APPEND(TimedLog::LOG_INFO, "- slot: %i, type: %i, itemId: %s", quickKey.slot, quickKey.type, quickKey.itemId.c_str()); if (quickKey.type == QuickKey::ITEM || quickKey.type == QuickKey::ITEM_MAGIC) { MWWorld::InventoryStore &ptrInventory = ptrPlayer.getClass().getInventoryStore(ptrPlayer); auto it = find_if(ptrInventory.begin(), ptrInventory.end(), [&quickKey](const MWWorld::Ptr &inventoryItem) { return Misc::StringUtils::ciEqual(inventoryItem.getCellRef().getRefId(), quickKey.itemId); }); if (it != ptrInventory.end()) MWBase::Environment::get().getWindowManager()->setQuickKey(quickKey.slot, quickKey.type, (*it)); } else if (quickKey.type == QuickKey::MAGIC) { MWMechanics::Spells &ptrSpells = ptrPlayer.getClass().getCreatureStats(ptrPlayer).getSpells(); bool hasSpell = false; MWMechanics::Spells::TIterator iter = ptrSpells.begin(); for (; iter != ptrSpells.end(); iter++) { const ESM::Spell *spell = iter->first; if (Misc::StringUtils::ciEqual(spell->mId, quickKey.itemId)) { hasSpell = true; break; } } if (hasSpell) MWBase::Environment::get().getWindowManager()->setQuickKey(quickKey.slot, quickKey.type, 0, quickKey.itemId); } else MWBase::Environment::get().getWindowManager()->setQuickKey(quickKey.slot, quickKey.type, 0); } } void LocalPlayer::setFactions() { MWWorld::Ptr ptrPlayer = getPlayerPtr(); MWMechanics::NpcStats &ptrNpcStats = ptrPlayer.getClass().getNpcStats(ptrPlayer); LOG_MESSAGE_SIMPLE(TimedLog::LOG_INFO, "Received ID_PLAYER_FACTION from server - action: %i", factionChanges.action); for (const auto &faction : factionChanges.factions) { LOG_APPEND(TimedLog::LOG_VERBOSE, " - processing faction: %s", faction.factionId.c_str()); const ESM::Faction *esmFaction = MWBase::Environment::get().getWorld()->getStore().get().search(faction.factionId); if (!esmFaction) { LOG_APPEND(TimedLog::LOG_INFO, "- Ignored invalid faction %s", faction.factionId.c_str()); continue; } if (factionChanges.action == mwmp::FactionChanges::RANK) { if (!ptrNpcStats.isInFaction(faction.factionId)) { // If the player isn't in this faction, make them join it ptrNpcStats.joinFaction(faction.factionId); LOG_APPEND(TimedLog::LOG_VERBOSE, "\t>JOINED FACTION: %s on rank change to: %d.", faction.factionId.c_str(), faction.rank); } // While the faction rank is different in the packet than in the NpcStats, // adjust the NpcStats accordingly while (faction.rank != ptrNpcStats.getFactionRanks().at(faction.factionId)) { if (faction.rank > ptrNpcStats.getFactionRanks().at(faction.factionId)) ptrNpcStats.raiseRank(faction.factionId); else ptrNpcStats.lowerRank(faction.factionId); } } else if (factionChanges.action == mwmp::FactionChanges::EXPULSION) { // If the expelled state is different in the packet than in the NpcStats, // adjust the NpcStats accordingly if (faction.isExpelled != ptrNpcStats.getExpelled(faction.factionId)) { if (faction.isExpelled) ptrNpcStats.expell(faction.factionId); else ptrNpcStats.clearExpelled(faction.factionId); } } else if (factionChanges.action == mwmp::FactionChanges::REPUTATION) ptrNpcStats.setFactionReputation(faction.factionId, faction.reputation); } } void LocalPlayer::setBooks() { MWWorld::Ptr ptrPlayer = getPlayerPtr(); MWMechanics::NpcStats &ptrNpcStats = ptrPlayer.getClass().getNpcStats(ptrPlayer); for (const auto &book : bookChanges) ptrNpcStats.flagAsUsed(book.bookId); } void LocalPlayer::setShapeshift() { MWWorld::Ptr ptrPlayer = getPlayerPtr(); MWBase::Environment::get().getWorld()->scaleObject(ptrPlayer, scale); MWBase::Environment::get().getMechanicsManager()->setWerewolf(ptrPlayer, isWerewolf); } void LocalPlayer::setMarkLocation() { MWWorld::CellStore *ptrCellStore = Main::get().getCellController()->getCellStore(markCell); if (ptrCellStore) MWBase::Environment::get().getWorld()->getPlayer().markPosition(ptrCellStore, markPosition); } void LocalPlayer::setSelectedSpell() { MWWorld::Ptr ptrPlayer = getPlayerPtr(); MWMechanics::CreatureStats& stats = ptrPlayer.getClass().getCreatureStats(ptrPlayer); MWMechanics::Spells& spells = stats.getSpells(); if (!spells.hasSpell(selectedSpellId)) return; MWBase::Environment::get().getWindowManager()->setSelectedSpell(selectedSpellId, int(MWMechanics::getSpellSuccessChance(selectedSpellId, ptrPlayer))); } void LocalPlayer::sendDeath(char newDeathState) { if (MechanicsHelper::isEmptyTarget(killer)) killer = MechanicsHelper::getTarget(getPlayerPtr()); deathState = newDeathState; LOG_MESSAGE_SIMPLE(TimedLog::LOG_INFO, "Sending ID_PLAYER_DEATH about myself to server\n- deathState: %d", deathState); getNetworking()->getPlayerPacket(ID_PLAYER_DEATH)->setPlayer(this); getNetworking()->getPlayerPacket(ID_PLAYER_DEATH)->Send(); MechanicsHelper::clearTarget(killer); } void LocalPlayer::sendClass() { MWBase::World *world = MWBase::Environment::get().getWorld(); const ESM::NPC *npcBase = world->getPlayerPtr().get()->mBase; const ESM::Class *esmClass = world->getStore().get().find(npcBase->mClass); if (npcBase->mClass.find("$dynamic") != std::string::npos) // custom class { charClass.mId = ""; charClass.mName = esmClass->mName; charClass.mDescription = esmClass->mDescription; charClass.mData = esmClass->mData; } else charClass.mId = esmClass->mId; getNetworking()->getPlayerPacket(ID_PLAYER_CHARCLASS)->setPlayer(this); getNetworking()->getPlayerPacket(ID_PLAYER_CHARCLASS)->Send(); } void LocalPlayer::sendInventory() { LOG_MESSAGE_SIMPLE(TimedLog::LOG_INFO, "Sending entire inventory to server"); MWWorld::Ptr ptrPlayer = getPlayerPtr(); MWWorld::InventoryStore &ptrInventory = ptrPlayer.getClass().getInventoryStore(ptrPlayer); mwmp::Item item; inventoryChanges.items.clear(); for (const auto &iter : ptrInventory) { item.refId = iter.getCellRef().getRefId(); // Skip any items that somehow have clientside-only dynamic IDs if (item.refId.find("$dynamic") != std::string::npos) continue; // Skip bound items if (MWBase::Environment::get().getMechanicsManager()->isBoundItem(item.refId)) continue; item.count = iter.getRefData().getCount(); item.charge = iter.getCellRef().getCharge(); item.enchantmentCharge = iter.getCellRef().getEnchantmentCharge(); item.soul = iter.getCellRef().getSoul(); inventoryChanges.items.push_back(item); } inventoryChanges.action = InventoryChanges::SET; getNetworking()->getPlayerPacket(ID_PLAYER_INVENTORY)->setPlayer(this); getNetworking()->getPlayerPacket(ID_PLAYER_INVENTORY)->Send(); } void LocalPlayer::sendItemChange(const mwmp::Item& item, unsigned int action) { LOG_MESSAGE_SIMPLE(TimedLog::LOG_INFO, "Sending item change for %s with action %i, count %i", item.refId.c_str(), action, item.count); inventoryChanges.items.clear(); inventoryChanges.items.push_back(item); inventoryChanges.action = action; getNetworking()->getPlayerPacket(ID_PLAYER_INVENTORY)->setPlayer(this); getNetworking()->getPlayerPacket(ID_PLAYER_INVENTORY)->Send(); } void LocalPlayer::sendItemChange(const MWWorld::Ptr& itemPtr, int count, unsigned int action) { mwmp::Item item = MechanicsHelper::getItem(itemPtr, count); sendItemChange(item, action); } void LocalPlayer::sendItemChange(const std::string& refId, int count, unsigned int action) { LOG_MESSAGE_SIMPLE(TimedLog::LOG_INFO, "Sending item change for %s with action %i, count %i", refId.c_str(), action, count); inventoryChanges.items.clear(); mwmp::Item item; item.refId = refId; item.count = count; item.charge = -1; item.enchantmentCharge = -1; item.soul = ""; inventoryChanges.items.push_back(item); inventoryChanges.action = action; getNetworking()->getPlayerPacket(ID_PLAYER_INVENTORY)->setPlayer(this); getNetworking()->getPlayerPacket(ID_PLAYER_INVENTORY)->Send(); } void LocalPlayer::sendStoredItemRemovals() { inventoryChanges.items.clear(); LOG_MESSAGE_SIMPLE(TimedLog::LOG_INFO, "Sending stored item removals for LocalPlayer:"); for (auto storedItemRemoval : storedItemRemovals) { mwmp::Item item; item.refId = storedItemRemoval.first; item.count = storedItemRemoval.second; item.charge = -1; item.enchantmentCharge = -1; item.soul = ""; inventoryChanges.items.push_back(item); LOG_APPEND(TimedLog::LOG_INFO, "- %s with count %i", item.refId.c_str(), item.count); } inventoryChanges.action = mwmp::InventoryChanges::ACTION_TYPE::REMOVE; getNetworking()->getPlayerPacket(ID_PLAYER_INVENTORY)->setPlayer(this); getNetworking()->getPlayerPacket(ID_PLAYER_INVENTORY)->Send(); storedItemRemovals.clear(); } void LocalPlayer::sendSpellbook() { MWWorld::Ptr ptrPlayer = getPlayerPtr(); MWMechanics::Spells &ptrSpells = ptrPlayer.getClass().getCreatureStats(ptrPlayer).getSpells(); spellbookChanges.spells.clear(); // Send spells in spellbook, while ignoring abilities, powers, etc. for (const auto &spell : ptrSpells) { if (spell.first->mData.mType == ESM::Spell::ST_Spell) spellbookChanges.spells.push_back(*spell.first); } spellbookChanges.action = SpellbookChanges::SET; getNetworking()->getPlayerPacket(ID_PLAYER_SPELLBOOK)->setPlayer(this); getNetworking()->getPlayerPacket(ID_PLAYER_SPELLBOOK)->Send(); } void LocalPlayer::sendSpellChange(std::string id, unsigned int action) { // Skip any bugged spells that somehow have clientside-only dynamic IDs if (id.find("$dynamic") != std::string::npos) return; spellbookChanges.spells.clear(); ESM::Spell spell; spell.mId = id; spellbookChanges.spells.push_back(spell); spellbookChanges.action = action; getNetworking()->getPlayerPacket(ID_PLAYER_SPELLBOOK)->setPlayer(this); getNetworking()->getPlayerPacket(ID_PLAYER_SPELLBOOK)->Send(); } void LocalPlayer::sendSpellsActive() { MWWorld::Ptr ptrPlayer = getPlayerPtr(); MWMechanics::ActiveSpells& activeSpells = ptrPlayer.getClass().getCreatureStats(ptrPlayer).getActiveSpells(); spellsActiveChanges.activeSpells.clear(); // Send spells in spellbook, while ignoring abilities, powers, etc. for (const auto& ptrSpell : activeSpells) { mwmp::ActiveSpell packetSpell; packetSpell.id = ptrSpell.first; packetSpell.params.mDisplayName = ptrSpell.second.mDisplayName; packetSpell.params.mEffects = ptrSpell.second.mEffects; spellsActiveChanges.activeSpells.push_back(packetSpell); } spellsActiveChanges.action = mwmp::SpellsActiveChanges::SET; getNetworking()->getPlayerPacket(ID_PLAYER_SPELLS_ACTIVE)->setPlayer(this); getNetworking()->getPlayerPacket(ID_PLAYER_SPELLS_ACTIVE)->Send(); } void LocalPlayer::sendSpellsActiveAddition(const std::string id, bool isStackingSpell, const MWMechanics::ActiveSpells::ActiveSpellParams& params) { // Skip any bugged spells that somehow have clientside-only dynamic IDs if (id.find("$dynamic") != std::string::npos) return; spellsActiveChanges.activeSpells.clear(); MWWorld::Ptr caster = MWBase::Environment::get().getWorld()->searchPtrViaActorId(params.mCasterActorId); mwmp::ActiveSpell spell; spell.id = id; spell.isStackingSpell = isStackingSpell; spell.caster = MechanicsHelper::getTarget(caster); spell.timestampDay = params.mTimeStamp.getDay(); spell.timestampHour = params.mTimeStamp.getHour(); spell.params.mEffects = params.mEffects; spell.params.mDisplayName = params.mDisplayName; spellsActiveChanges.activeSpells.push_back(spell); LOG_MESSAGE_SIMPLE(TimedLog::LOG_INFO, "Sending active spell addition with stacking %s, timestamp %i %f", spell.isStackingSpell ? "true" : "false", spell.timestampDay, spell.timestampHour); spellsActiveChanges.action = mwmp::SpellsActiveChanges::ADD; getNetworking()->getPlayerPacket(ID_PLAYER_SPELLS_ACTIVE)->setPlayer(this); getNetworking()->getPlayerPacket(ID_PLAYER_SPELLS_ACTIVE)->Send(); } void LocalPlayer::sendSpellsActiveRemoval(const std::string id, bool isStackingSpell, MWWorld::TimeStamp timestamp) { // Skip any bugged spells that somehow have clientside-only dynamic IDs if (id.find("$dynamic") != std::string::npos) return; spellsActiveChanges.activeSpells.clear(); mwmp::ActiveSpell spell; spell.id = id; spell.isStackingSpell = isStackingSpell; spell.timestampDay = timestamp.getDay(); spell.timestampHour = timestamp.getHour(); spellsActiveChanges.activeSpells.push_back(spell); LOG_MESSAGE_SIMPLE(TimedLog::LOG_INFO, "Sending active spell removal with stacking %s, timestamp %i %f", spell.isStackingSpell ? "true" : "false", spell.timestampDay, spell.timestampHour); spellsActiveChanges.action = mwmp::SpellsActiveChanges::REMOVE; getNetworking()->getPlayerPacket(ID_PLAYER_SPELLS_ACTIVE)->setPlayer(this); getNetworking()->getPlayerPacket(ID_PLAYER_SPELLS_ACTIVE)->Send(); } void LocalPlayer::sendCooldownChange(std::string id, int startTimestampDay, float startTimestampHour) { // Skip any bugged spells that somehow have clientside-only dynamic IDs if (id.find("$dynamic") != std::string::npos) return; cooldownChanges.clear(); SpellCooldown spellCooldown; spellCooldown.id = id; spellCooldown.startTimestampDay = startTimestampDay; spellCooldown.startTimestampHour = startTimestampHour; cooldownChanges.push_back(spellCooldown); ; getNetworking()->getPlayerPacket(ID_PLAYER_COOLDOWNS)->setPlayer(this); getNetworking()->getPlayerPacket(ID_PLAYER_COOLDOWNS)->Send(); } void LocalPlayer::sendQuickKey(unsigned short slot, int type, const std::string& itemId) { quickKeyChanges.clear(); mwmp::QuickKey quickKey; quickKey.slot = slot; quickKey.type = type; quickKey.itemId = itemId; LOG_MESSAGE_SIMPLE(TimedLog::LOG_INFO, "Sending ID_PLAYER_QUICKKEYS", itemId.c_str()); LOG_APPEND(TimedLog::LOG_INFO, "- slot: %i, type: %i, itemId: %s", quickKey.slot, quickKey.type, quickKey.itemId.c_str()); quickKeyChanges.push_back(quickKey); getNetworking()->getPlayerPacket(ID_PLAYER_QUICKKEYS)->setPlayer(this); getNetworking()->getPlayerPacket(ID_PLAYER_QUICKKEYS)->Send(); } void LocalPlayer::sendJournalEntry(const std::string& quest, int index, const MWWorld::Ptr& actor) { journalChanges.clear(); mwmp::JournalItem journalItem; journalItem.type = JournalItem::ENTRY; journalItem.quest = quest; journalItem.index = index; journalItem.actorRefId = actor.getCellRef().getRefId(); journalItem.hasTimestamp = false; journalChanges.push_back(journalItem); getNetworking()->getPlayerPacket(ID_PLAYER_JOURNAL)->setPlayer(this); getNetworking()->getPlayerPacket(ID_PLAYER_JOURNAL)->Send(); } void LocalPlayer::sendJournalIndex(const std::string& quest, int index) { journalChanges.clear(); mwmp::JournalItem journalItem; journalItem.type = JournalItem::INDEX; journalItem.quest = quest; journalItem.index = index; journalChanges.push_back(journalItem); getNetworking()->getPlayerPacket(ID_PLAYER_JOURNAL)->setPlayer(this); getNetworking()->getPlayerPacket(ID_PLAYER_JOURNAL)->Send(); } void LocalPlayer::sendFactionRank(const std::string& factionId, int rank) { factionChanges.factions.clear(); factionChanges.action = FactionChanges::RANK; mwmp::Faction faction; faction.factionId = factionId; faction.rank = rank; factionChanges.factions.push_back(faction); getNetworking()->getPlayerPacket(ID_PLAYER_FACTION)->setPlayer(this); getNetworking()->getPlayerPacket(ID_PLAYER_FACTION)->Send(); } void LocalPlayer::sendFactionExpulsionState(const std::string& factionId, bool isExpelled) { factionChanges.factions.clear(); factionChanges.action = FactionChanges::EXPULSION; mwmp::Faction faction; faction.factionId = factionId; faction.isExpelled = isExpelled; factionChanges.factions.push_back(faction); getNetworking()->getPlayerPacket(ID_PLAYER_FACTION)->setPlayer(this); getNetworking()->getPlayerPacket(ID_PLAYER_FACTION)->Send(); } void LocalPlayer::sendFactionReputation(const std::string& factionId, int reputation) { factionChanges.factions.clear(); factionChanges.action = FactionChanges::REPUTATION; mwmp::Faction faction; faction.factionId = factionId; faction.reputation = reputation; factionChanges.factions.push_back(faction); getNetworking()->getPlayerPacket(ID_PLAYER_FACTION)->setPlayer(this); getNetworking()->getPlayerPacket(ID_PLAYER_FACTION)->Send(); } void LocalPlayer::sendTopic(const std::string& topicId) { topicChanges.clear(); mwmp::Topic topic; // For translated versions of the game, make sure we translate the topic back into English first if (MWBase::Environment::get().getWindowManager()->getTranslationDataStorage().hasTranslation()) topic.topicId = MWBase::Environment::get().getWindowManager()->getTranslationDataStorage().topicID(topicId); else topic.topicId = topicId; LOG_MESSAGE_SIMPLE(TimedLog::LOG_INFO, "Sending ID_PLAYER_TOPIC with topic %s", topic.topicId.c_str()); topicChanges.push_back(topic); getNetworking()->getPlayerPacket(ID_PLAYER_TOPIC)->setPlayer(this); getNetworking()->getPlayerPacket(ID_PLAYER_TOPIC)->Send(); } void LocalPlayer::sendBook(const std::string& bookId) { bookChanges.clear(); mwmp::Book book; book.bookId = bookId; LOG_MESSAGE_SIMPLE(TimedLog::LOG_INFO, "Sending ID_PLAYER_BOOK with book %s", book.bookId.c_str()); bookChanges.push_back(book); getNetworking()->getPlayerPacket(ID_PLAYER_BOOK)->setPlayer(this); getNetworking()->getPlayerPacket(ID_PLAYER_BOOK)->Send(); } void LocalPlayer::sendWerewolfState(bool werewolfState) { isWerewolf = werewolfState; LOG_MESSAGE_SIMPLE(TimedLog::LOG_INFO, "Sending ID_PLAYER_SHAPESHIFT with isWerewolf of %s", isWerewolf ? "true" : "false"); getNetworking()->getPlayerPacket(ID_PLAYER_SHAPESHIFT)->setPlayer(this); getNetworking()->getPlayerPacket(ID_PLAYER_SHAPESHIFT)->Send(); } void LocalPlayer::sendMarkLocation(const ESM::Cell& newMarkCell, const ESM::Position& newMarkPosition) { miscellaneousChangeType = mwmp::MISCELLANEOUS_CHANGE_TYPE::MARK_LOCATION; markCell = newMarkCell; markPosition = newMarkPosition; getNetworking()->getPlayerPacket(ID_PLAYER_MISCELLANEOUS)->setPlayer(this); getNetworking()->getPlayerPacket(ID_PLAYER_MISCELLANEOUS)->Send(); } void LocalPlayer::sendSelectedSpell(const std::string& newSelectedSpellId) { miscellaneousChangeType = mwmp::MISCELLANEOUS_CHANGE_TYPE::SELECTED_SPELL; selectedSpellId = newSelectedSpellId; getNetworking()->getPlayerPacket(ID_PLAYER_MISCELLANEOUS)->setPlayer(this); getNetworking()->getPlayerPacket(ID_PLAYER_MISCELLANEOUS)->Send(); } void LocalPlayer::sendItemUse(const MWWorld::Ptr& itemPtr, bool itemMagicState, char currentDrawState) { usedItem.refId = itemPtr.getCellRef().getRefId(); usedItem.count = itemPtr.getRefData().getCount(); usedItem.charge = itemPtr.getCellRef().getCharge(); usedItem.enchantmentCharge = itemPtr.getCellRef().getEnchantmentCharge(); usedItem.soul = itemPtr.getCellRef().getSoul(); usingItemMagic = itemMagicState; itemUseDrawState = currentDrawState; getNetworking()->getPlayerPacket(ID_PLAYER_ITEM_USE)->setPlayer(this); getNetworking()->getPlayerPacket(ID_PLAYER_ITEM_USE)->Send(); } void LocalPlayer::sendCellStates() { LOG_MESSAGE_SIMPLE(TimedLog::LOG_INFO, "Sending ID_PLAYER_CELL_STATE to server"); getNetworking()->getPlayerPacket(ID_PLAYER_CELL_STATE)->setPlayer(this); getNetworking()->getPlayerPacket(ID_PLAYER_CELL_STATE)->Send(); } void LocalPlayer::clearCellStates() { cellStateChanges.clear(); } void LocalPlayer::clearCurrentContainer() { currentContainer.refId = ""; currentContainer.refNum = 0; currentContainer.mpNum = 0; } void LocalPlayer::storeCellState(const ESM::Cell& storedCell, int stateType) { std::vector::iterator iter; for (iter = cellStateChanges.begin(); iter != cellStateChanges.end(); ) { // If there's already a cell state recorded for this particular cell, // remove it if (storedCell.getShortDescription() == (*iter).cell.getShortDescription()) iter = cellStateChanges.erase(iter); else ++iter; } CellState cellState; cellState.cell = storedCell; cellState.type = stateType; cellStateChanges.push_back(cellState); } void LocalPlayer::storeCurrentContainer(const MWWorld::Ptr &container) { currentContainer.refId = container.getCellRef().getRefId(); currentContainer.refNum = container.getCellRef().getRefNum().mIndex; currentContainer.mpNum = container.getCellRef().getMpNum(); } void LocalPlayer::storeItemRemoval(const std::string& refId, int count) { storedItemRemovals[refId] = storedItemRemovals[refId] + count; } void LocalPlayer::storeLastEnchantmentQuantity(unsigned int quantity) { lastEnchantmentQuantity = quantity; } void LocalPlayer::playAnimation() { MWBase::Environment::get().getMechanicsManager()->playAnimationGroup(getPlayerPtr(), animation.groupname, animation.mode, animation.count, animation.persist); isPlayingAnimation = true; } void LocalPlayer::playSpeech() { MWBase::Environment::get().getSoundManager()->say(getPlayerPtr(), sound); MWBase::WindowManager *winMgr = MWBase::Environment::get().getWindowManager(); if (winMgr->getSubtitlesEnabled()) winMgr->messageBox(MWBase::Environment::get().getDialogueManager()->getVoiceCaption(sound), MWGui::ShowInDialogueMode_Never); } ================================================ FILE: apps/openmw/mwmp/LocalPlayer.hpp ================================================ #ifndef OPENMW_LOCALPLAYER_HPP #define OPENMW_LOCALPLAYER_HPP #include #include "../mwmechanics/activespells.hpp" #include "../mwworld/ptr.hpp" #include "../mwworld/timestamp.hpp" #include namespace mwmp { class Networking; class LocalPlayer : public BasePlayer { public: LocalPlayer(); virtual ~LocalPlayer(); time_t deathTime; bool receivedCharacter; bool isUsingBed; bool avoidSendingInventoryPackets; bool isReceivingQuickKeys; bool isPlayingAnimation; bool diedSinceArrestAttempt; unsigned int lastEnchantmentQuantity; void update(); bool processCharGen(); bool isLoggedIn(); void updateStatsDynamic(bool forceUpdate = false); void updateAttributes(bool forceUpdate = false); void updateSkills(bool forceUpdate = false); void updateLevel(bool forceUpdate = false); void updateBounty(bool forceUpdate = false); void updateReputation(bool forceUpdate = false); void updatePosition(bool forceUpdate = false); void updateCell(bool forceUpdate = false); void updateEquipment(bool forceUpdate = false); void updateInventory(bool forceUpdate = false); void updateAttackOrCast(); void updateAnimFlags(bool forceUpdate = false); void addItems(); void addSpells(); void addSpellsActive(); void addJournalItems(); void addTopics(); void removeItems(); void removeSpells(); void removeSpellsActive(); void die(); void resurrect(); void closeInventoryWindows(); void updateInventoryWindow(); void setCharacter(); void setDynamicStats(); void setAttributes(); void setSkills(); void setLevel(); void setBounty(); void setReputation(); void setPosition(); void setMomentum(); void setCell(); void setClass(); void setEquipment(); void setInventory(); void setSpellbook(); void setSpellsActive(); void setCooldowns(); void setQuickKeys(); void setFactions(); void setBooks(); void setShapeshift(); void setMarkLocation(); void setSelectedSpell(); void sendDeath(char newDeathState); void sendClass(); void sendInventory(); void sendItemChange(const mwmp::Item& item, unsigned int action); void sendItemChange(const MWWorld::Ptr& itemPtr, int count, unsigned int action); void sendItemChange(const std::string& refId, int count, unsigned int action); void sendStoredItemRemovals(); void sendSpellbook(); void sendSpellChange(std::string id, unsigned int action); void sendSpellsActive(); void sendSpellsActiveAddition(const std::string id, bool isStackingSpell, const MWMechanics::ActiveSpells::ActiveSpellParams& params); void sendSpellsActiveRemoval(const std::string id, bool isStackingSpell, MWWorld::TimeStamp timestamp); void sendCooldownChange(std::string id, int startTimestampDay, float startTimestampHour); void sendQuickKey(unsigned short slot, int type, const std::string& itemId = ""); void sendJournalEntry(const std::string& quest, int index, const MWWorld::Ptr& actor); void sendJournalIndex(const std::string& quest, int index); void sendFactionRank(const std::string& factionId, int rank); void sendFactionExpulsionState(const std::string& factionId, bool isExpelled); void sendFactionReputation(const std::string& factionId, int reputation); void sendTopic(const std::string& topic); void sendBook(const std::string& bookId); void sendWerewolfState(bool isWerewolf); void sendMarkLocation(const ESM::Cell& newMarkCell, const ESM::Position& newMarkPosition); void sendSelectedSpell(const std::string& newSelectedSpellId); void sendItemUse(const MWWorld::Ptr& itemPtr, bool usingItemMagic = false, char currentDrawState = 0); void sendCellStates(); void clearCellStates(); void clearCurrentContainer(); void storeCellState(const ESM::Cell& cell, int stateType); void storeCurrentContainer(const MWWorld::Ptr& container); void storeItemRemoval(const std::string& refId, int count); void storeLastEnchantmentQuantity(unsigned int quantity); void playAnimation(); void playSpeech(); MWWorld::Ptr getPlayerPtr(); private: Networking *getNetworking(); }; } #endif //OPENMW_LOCALPLAYER_HPP ================================================ FILE: apps/openmw/mwmp/LocalSystem.cpp ================================================ #include "LocalSystem.hpp" #include "Main.hpp" #include "Networking.hpp" using namespace mwmp; LocalSystem::LocalSystem() { } LocalSystem::~LocalSystem() { } Networking *LocalSystem::getNetworking() { return mwmp::Main::get().getNetworking(); } ================================================ FILE: apps/openmw/mwmp/LocalSystem.hpp ================================================ #ifndef OPENMW_LOCALSYSTEM_HPP #define OPENMW_LOCALSYSTEM_HPP #include #include namespace mwmp { class Networking; class LocalSystem : public BaseSystem { public: LocalSystem(); virtual ~LocalSystem(); private: Networking *getNetworking(); }; } #endif //OPENMW_LOCALSYSTEM_HPP ================================================ FILE: apps/openmw/mwmp/Main.cpp ================================================ #include #include #include #include #include #include #include #include "../mwbase/environment.hpp" #include "../mwclass/creature.hpp" #include "../mwclass/npc.hpp" #include "../mwdialogue/dialoguemanagerimp.hpp" #include "../mwgui/windowmanagerimp.hpp" #include "../mwinput/inputmanagerimp.hpp" #include "../mwmechanics/aitravel.hpp" #include "../mwmechanics/creaturestats.hpp" #include "../mwmechanics/mechanicsmanagerimp.hpp" #include "../mwmechanics/spellcasting.hpp" #include "../mwscript/scriptmanagerimp.hpp" #include "../mwstate/statemanagerimp.hpp" #include "../mwworld/cellstore.hpp" #include "../mwworld/customdata.hpp" #include "../mwworld/inventorystore.hpp" #include "../mwworld/manualref.hpp" #include "../mwworld/player.hpp" #include "../mwworld/ptr.hpp" #include "../mwworld/worldimp.hpp" #include "Main.hpp" #include "Networking.hpp" #include "LocalSystem.hpp" #include "LocalPlayer.hpp" #include "DedicatedPlayer.hpp" #include "PlayerList.hpp" #include "GUIController.hpp" #include "CellController.hpp" #include "MechanicsHelper.hpp" #include "RecordHelper.hpp" using namespace mwmp; Main *Main::pMain = 0; std::string Main::address = ""; std::string Main::serverPassword = TES3MP_DEFAULT_PASSW; std::string Main::resourceDir = ""; std::string Main::getResDir() { return resourceDir; } std::string loadSettings(Settings::Manager& settings) { Files::ConfigurationManager mCfgMgr; // Create the settings manager and load default settings file const std::string localdefault = (mCfgMgr.getLocalPath() / "tes3mp-client-default.cfg").string(); const std::string globaldefault = (mCfgMgr.getGlobalPath() / "tes3mp-client-default.cfg").string(); // prefer local if (boost::filesystem::exists(localdefault)) settings.loadDefault(localdefault, false); else if (boost::filesystem::exists(globaldefault)) settings.loadDefault(globaldefault, false); else throw std::runtime_error ("No default settings file found! Make sure the file \"tes3mp-client-default.cfg\" was properly installed."); // load user settings if they exist const std::string settingspath = (mCfgMgr.getUserConfigPath() / "tes3mp-client.cfg").string(); if (boost::filesystem::exists(settingspath)) settings.loadUser(settingspath); return settingspath; } Main::Main() { LOG_MESSAGE_SIMPLE(TimedLog::LOG_INFO, "tes3mp started"); mNetworking = new Networking(); mLocalSystem = new LocalSystem(); mLocalPlayer = new LocalPlayer(); mGUIController = new GUIController(); mCellController = new CellController(); server = "mp.tes3mp.com"; port = 25565; } Main::~Main() { LOG_MESSAGE_SIMPLE(TimedLog::LOG_INFO, "tes3mp stopped"); delete mNetworking; delete mLocalSystem; delete mLocalPlayer; delete mCellController; delete mGUIController; PlayerList::cleanUp(); } void Main::optionsDesc(boost::program_options::options_description *desc) { namespace bpo = boost::program_options; desc->add_options() ("connect", bpo::value()->default_value(""), "connect to server (e.g. --connect=127.0.0.1:25565)") ("password", bpo::value()->default_value(TES3MP_DEFAULT_PASSW), "сonnect to a secured server. (e.g. --password=AnyPassword"); } void Main::configure(const boost::program_options::variables_map &variables) { Main::address = variables["connect"].as(); Main::serverPassword = variables["password"].as(); resourceDir = variables["resources"].as().mPath.string(); } bool Main::init(std::vector &content, Files::Collections &collections) { assert(!pMain); pMain = new Main(); Settings::Manager manager; loadSettings(manager); int logLevel = manager.getInt("logLevel", "General"); TimedLog::SetLevel(logLevel); if (address.empty()) { pMain->server = manager.getString("destinationAddress", "General"); pMain->port = (unsigned short) manager.getInt("port", "General"); serverPassword = manager.getString("password", "General"); if (serverPassword.empty()) serverPassword = TES3MP_DEFAULT_PASSW; } else { size_t delimPos = address.find(':'); pMain->server = address.substr(0, delimPos); pMain->port = atoi(address.substr(delimPos + 1).c_str()); } get().mLocalSystem->serverPassword = serverPassword; pMain->mNetworking->connect(pMain->server, pMain->port, content, collections); return pMain->mNetworking->isConnected(); } void Main::postInit() { pMain->mGUIController->setupChat(); const MWBase::Environment &environment = MWBase::Environment::get(); environment.getStateManager()->newGame(true); MWBase::Environment::get().getMechanicsManager()->toggleAI(); RecordHelper::createPlaceholderInteriorCell(); } bool Main::isInitialized() { return pMain != nullptr; } void Main::destroy() { assert(pMain); delete pMain; pMain = 0; } void Main::frame(float dt) { get().getNetworking()->update(); PlayerList::update(dt); get().getCellController()->updateDedicated(dt); get().updateWorld(dt); get().getGUIController()->update(dt); } void Main::updateWorld(float dt) const { if (!mLocalPlayer->processCharGen()) return; static bool init = true; if (init) { init = false; LOG_MESSAGE_SIMPLE(TimedLog::LOG_INFO, "Sending ID_PLAYER_BASEINFO to server"); mNetworking->getPlayerPacket(ID_PLAYER_BASEINFO)->setPlayer(getLocalPlayer()); mNetworking->getPlayerPacket(ID_LOADED)->setPlayer(getLocalPlayer()); mNetworking->getPlayerPacket(ID_PLAYER_BASEINFO)->Send(); mNetworking->getPlayerPacket(ID_LOADED)->Send(); mLocalPlayer->updateStatsDynamic(true); get().getGUIController()->setChatVisible(true); } else { mLocalPlayer->update(); mCellController->updateLocal(false); } } const Main &Main::get() { return *pMain; } Networking *Main::getNetworking() const { return mNetworking; } LocalSystem *Main::getLocalSystem() const { return mLocalSystem; } LocalPlayer *Main::getLocalPlayer() const { return mLocalPlayer; } GUIController *Main::getGUIController() const { return mGUIController; } CellController *Main::getCellController() const { return mCellController; } bool Main::isValidPacketScript(std::string scriptId) { mwmp::BaseWorldstate *worldstate = get().getNetworking()->getWorldstate(); if (Utils::vectorContains(worldstate->synchronizedClientScriptIds, scriptId)) return true; return false; } bool Main::isValidPacketGlobal(std::string globalId) { mwmp::BaseWorldstate *worldstate = get().getNetworking()->getWorldstate(); if (Utils::vectorContains(worldstate->synchronizedClientGlobalIds, globalId)) return true; return false; } ================================================ FILE: apps/openmw/mwmp/Main.hpp ================================================ #ifndef OPENMW_MWMP_MAIN #define OPENMW_MWMP_MAIN #include "../mwworld/ptr.hpp" #include #include namespace mwmp { class GUIController; class CellController; class LocalSystem; class LocalPlayer; class Networking; class Main { public: Main(); ~Main(); static void optionsDesc(boost::program_options::options_description *desc); static void configure(const boost::program_options::variables_map &variables); static bool init(std::vector &content, Files::Collections &collections); static void postInit(); static bool isInitialized(); static void destroy(); static const Main &get(); static void frame(float dt); static bool isValidPacketScript(std::string scriptId); static bool isValidPacketGlobal(std::string globalId); static std::string getResDir(); Networking *getNetworking() const; LocalSystem *getLocalSystem() const; LocalPlayer *getLocalPlayer() const; GUIController *getGUIController() const; CellController *getCellController() const; void updateWorld(float dt) const; private: static std::string resourceDir; static std::string address; static std::string serverPassword; Main (const Main&); ///< not implemented Main& operator= (const Main&); ///< not implemented static Main *pMain; Networking *mNetworking; LocalSystem *mLocalSystem; LocalPlayer *mLocalPlayer; GUIController *mGUIController; CellController *mCellController; std::string server; unsigned short port; }; } #endif //OPENMW_MWMP_MAIN ================================================ FILE: apps/openmw/mwmp/MechanicsHelper.cpp ================================================ #include #include #include #include "../mwbase/environment.hpp" #include "../mwbase/mechanicsmanager.hpp" #include "../mwbase/world.hpp" #include "../mwmechanics/creaturestats.hpp" #include "../mwmechanics/combat.hpp" #include "../mwmechanics/levelledlist.hpp" #include "../mwmechanics/spellcasting.hpp" #include "../mwmechanics/spellutil.hpp" #include "../mwrender/animation.hpp" #include "../mwworld/class.hpp" #include "../mwworld/inventorystore.hpp" #include "MechanicsHelper.hpp" #include "Main.hpp" #include "Networking.hpp" #include "LocalPlayer.hpp" #include "DedicatedPlayer.hpp" #include "PlayerList.hpp" #include "ObjectList.hpp" #include "CellController.hpp" using namespace mwmp; osg::Vec3f MechanicsHelper::getLinearInterpolation(osg::Vec3f start, osg::Vec3f end, float percent) { osg::Vec3f position(percent, percent, percent); return (start + osg::componentMultiply(position, (end - start))); } ESM::Position MechanicsHelper::getPositionFromVector(osg::Vec3f vector) { ESM::Position position; position.pos[0] = vector.x(); position.pos[1] = vector.y(); position.pos[2] = vector.z(); return position; } // Inspired by similar code in mwclass\creaturelevlist.cpp // // TODO: Add handling of scaling based on leveled list's assigned scale void MechanicsHelper::spawnLeveledCreatures(MWWorld::CellStore* cellStore) { MWWorld::CellRefList *creatureLevList = cellStore->getCreatureLists(); mwmp::ObjectList *objectList = mwmp::Main::get().getNetworking()->getObjectList(); objectList->reset(); objectList->packetOrigin = mwmp::CLIENT_GAMEPLAY; int spawnCount = 0; for (auto &lref : creatureLevList->mList) { MWWorld::Ptr ptr(&lref, cellStore); std::string id = MWMechanics::getLevelledItem(ptr.get()->mBase, true); if (!id.empty()) { const MWWorld::ESMStore& store = MWBase::Environment::get().getWorld()->getStore(); MWWorld::ManualRef manualRef(store, id); manualRef.getPtr().getCellRef().setPosition(ptr.getCellRef().getPosition()); MWWorld::Ptr placed = MWBase::Environment::get().getWorld()->placeObject(manualRef.getPtr(), ptr.getCell(), ptr.getCellRef().getPosition()); objectList->addObjectSpawn(placed); MWBase::Environment::get().getWorld()->deleteObject(placed); spawnCount++; } } if (spawnCount > 0) objectList->sendObjectSpawn(); } bool MechanicsHelper::isUsingRangedWeapon(const MWWorld::Ptr& ptr) { if (ptr.getClass().hasInventoryStore(ptr)) { MWWorld::InventoryStore &inventoryStore = ptr.getClass().getInventoryStore(ptr); MWWorld::ContainerStoreIterator weaponSlot = inventoryStore.getSlot( MWWorld::InventoryStore::Slot_CarriedRight); if (weaponSlot != inventoryStore.end() && weaponSlot->getTypeName() == typeid(ESM::Weapon).name()) { const ESM::Weapon* weaponRecord = weaponSlot->get()->mBase; if (weaponRecord->mData.mType >= ESM::Weapon::MarksmanBow) return true; } } return false; } Attack *MechanicsHelper::getLocalAttack(const MWWorld::Ptr& ptr) { if (ptr == MWMechanics::getPlayer()) return &mwmp::Main::get().getLocalPlayer()->attack; else if (mwmp::Main::get().getCellController()->isLocalActor(ptr)) return &mwmp::Main::get().getCellController()->getLocalActor(ptr)->attack; return nullptr; } Attack *MechanicsHelper::getDedicatedAttack(const MWWorld::Ptr& ptr) { if (mwmp::PlayerList::isDedicatedPlayer(ptr)) return &mwmp::PlayerList::getPlayer(ptr)->attack; else if (mwmp::Main::get().getCellController()->isDedicatedActor(ptr)) return &mwmp::Main::get().getCellController()->getDedicatedActor(ptr)->attack; return nullptr; } Cast *MechanicsHelper::getLocalCast(const MWWorld::Ptr& ptr) { if (ptr == MWMechanics::getPlayer()) return &mwmp::Main::get().getLocalPlayer()->cast; else if (mwmp::Main::get().getCellController()->isLocalActor(ptr)) return &mwmp::Main::get().getCellController()->getLocalActor(ptr)->cast; return nullptr; } Cast *MechanicsHelper::getDedicatedCast(const MWWorld::Ptr& ptr) { if (mwmp::PlayerList::isDedicatedPlayer(ptr)) return &mwmp::PlayerList::getPlayer(ptr)->cast; else if (mwmp::Main::get().getCellController()->isDedicatedActor(ptr)) return &mwmp::Main::get().getCellController()->getDedicatedActor(ptr)->cast; return nullptr; } MWWorld::Ptr MechanicsHelper::getPlayerPtr(const Target& target) { if (target.guid == mwmp::Main::get().getLocalPlayer()->guid) { return MWMechanics::getPlayer(); } else { mwmp::DedicatedPlayer* dedicatedPlayer = mwmp::PlayerList::getPlayer(target.guid); if (dedicatedPlayer != nullptr) { return dedicatedPlayer->getPtr(); } } return nullptr; } unsigned int MechanicsHelper::getActorId(const mwmp::Target& target) { int actorId = -1; MWWorld::Ptr targetPtr; if (target.isPlayer) { targetPtr = getPlayerPtr(target); } else { auto controller = mwmp::Main::get().getCellController(); if (controller->isLocalActor(target.refNum, target.mpNum)) { targetPtr = controller->getLocalActor(target.refNum, target.mpNum)->getPtr(); } else if (controller->isDedicatedActor(target.refNum, target.mpNum)) { targetPtr = controller->getDedicatedActor(target.refNum, target.mpNum)->getPtr(); } } if (targetPtr) { actorId = targetPtr.getClass().getCreatureStats(targetPtr).getActorId(); } return actorId; } mwmp::Item MechanicsHelper::getItem(const MWWorld::Ptr& itemPtr, int count) { mwmp::Item item; if (itemPtr.getClass().isGold(itemPtr)) item.refId = MWWorld::ContainerStore::sGoldId; else item.refId = itemPtr.getCellRef().getRefId(); item.count = count; item.charge = itemPtr.getCellRef().getCharge(); item.enchantmentCharge = itemPtr.getCellRef().getEnchantmentCharge(); item.soul = itemPtr.getCellRef().getSoul(); return item; } mwmp::Target MechanicsHelper::getTarget(const MWWorld::Ptr& ptr) { mwmp::Target target; clearTarget(target); if (ptr != nullptr) { if (ptr == MWMechanics::getPlayer()) { target.isPlayer = true; target.guid = mwmp::Main::get().getLocalPlayer()->guid; } else if (mwmp::PlayerList::isDedicatedPlayer(ptr)) { target.isPlayer = true; target.guid = mwmp::PlayerList::getPlayer(ptr)->guid; } else { MWWorld::CellRef *ptrRef = &ptr.getCellRef(); if (ptrRef) { target.isPlayer = false; target.refId = ptrRef->getRefId(); target.refNum = ptrRef->getRefNum().mIndex; target.mpNum = ptrRef->getMpNum(); target.name = ptr.getClass().getName(ptr); } } } return target; } void MechanicsHelper::clearTarget(mwmp::Target& target) { target.isPlayer = false; target.refId.clear(); target.refNum = -1; target.mpNum = -1; target.name.clear(); } bool MechanicsHelper::isEmptyTarget(const mwmp::Target& target) { if (target.isPlayer == false && target.refId.empty()) return true; return false; } void MechanicsHelper::assignAttackTarget(Attack* attack, const MWWorld::Ptr& target) { if (target == MWMechanics::getPlayer()) { attack->target.isPlayer = true; attack->target.guid = mwmp::Main::get().getLocalPlayer()->guid; } else if (mwmp::PlayerList::isDedicatedPlayer(target)) { attack->target.isPlayer = true; attack->target.guid = mwmp::PlayerList::getPlayer(target)->guid; } else { MWWorld::CellRef *targetRef = &target.getCellRef(); attack->target.isPlayer = false; attack->target.refId = targetRef->getRefId(); attack->target.refNum = targetRef->getRefNum().mIndex; attack->target.mpNum = targetRef->getMpNum(); } } void MechanicsHelper::resetAttack(Attack* attack) { attack->isHit = false; attack->success = false; attack->knockdown = false; attack->block = false; attack->applyWeaponEnchantment = false; attack->applyAmmoEnchantment = false; attack->hitPosition.pos[0] = attack->hitPosition.pos[1] = attack->hitPosition.pos[2] = 0; attack->target.guid = RakNet::RakNetGUID(); attack->target.refId.clear(); attack->target.refNum = 0; attack->target.mpNum = 0; } void MechanicsHelper::resetCast(Cast* cast) { cast->isHit = false; cast->success = false; cast->target.guid = RakNet::RakNetGUID(); cast->target.refId.clear(); cast->target.refNum = 0; cast->target.mpNum = 0; } bool MechanicsHelper::getSpellSuccess(std::string spellId, const MWWorld::Ptr& caster) { return Misc::Rng::roll0to99() < MWMechanics::getSpellSuccessChance(spellId, caster, nullptr, true, false); } bool MechanicsHelper::isTeamMember(const MWWorld::Ptr& playerChecked, const MWWorld::Ptr& playerWithTeam) { bool isTeamMember = false; bool playerCheckedIsLocal = playerChecked == MWMechanics::getPlayer(); bool playerCheckedIsDedicated = !playerCheckedIsLocal ? mwmp::PlayerList::isDedicatedPlayer(playerChecked) : false; bool playerWithTeamIsLocal = !playerCheckedIsLocal ? playerWithTeam == MWMechanics::getPlayer() : false; bool playerWithTeamIsDedicated = !playerWithTeamIsLocal ? mwmp::PlayerList::isDedicatedPlayer(playerWithTeam) : false; if (playerCheckedIsLocal || playerCheckedIsDedicated) { if (playerWithTeamIsLocal || playerWithTeamIsDedicated) { RakNet::RakNetGUID playerCheckedGuid; if (playerCheckedIsLocal) playerCheckedGuid = mwmp::Main::get().getLocalPlayer()->guid; else playerCheckedGuid = PlayerList::getPlayer(playerChecked)->guid; if (playerWithTeamIsLocal) isTeamMember = Utils::vectorContains(mwmp::Main::get().getLocalPlayer()->alliedPlayers, playerCheckedGuid); else isTeamMember = Utils::vectorContains(PlayerList::getPlayer(playerWithTeam)->alliedPlayers, playerCheckedGuid); } } return isTeamMember; } void MechanicsHelper::processAttack(Attack attack, const MWWorld::Ptr& attacker) { LOG_MESSAGE_SIMPLE(TimedLog::LOG_VERBOSE, "Processing attack from %s of type %i", attacker.getClass().getName(attacker).c_str(), attack.type); LOG_APPEND(TimedLog::LOG_VERBOSE, "- pressed: %s", attack.pressed ? "true" : "false"); if (!attack.pressed) { LOG_APPEND(TimedLog::LOG_VERBOSE, "- success: %s", attack.success ? "true" : "false"); if (attack.success) LOG_APPEND(TimedLog::LOG_VERBOSE, "- damage: %f", attack.damage); } else { if (attack.type == attack.MELEE) LOG_APPEND(TimedLog::LOG_VERBOSE, "- animation: %s", attack.attackAnimation.c_str()); } MWBase::Environment::get().getMechanicsManager()->setAttackingOrSpell(attacker, attack.pressed); MWWorld::Ptr victim; if (attack.target.isPlayer) { if (attack.target.guid == mwmp::Main::get().getLocalPlayer()->guid) victim = MWMechanics::getPlayer(); else if (PlayerList::getPlayer(attack.target.guid) != nullptr) victim = PlayerList::getPlayer(attack.target.guid)->getPtr(); } else { auto controller = mwmp::Main::get().getCellController(); if (controller->isLocalActor(attack.target.refNum, attack.target.mpNum)) victim = controller->getLocalActor(attack.target.refNum, attack.target.mpNum)->getPtr(); else if (controller->isDedicatedActor(attack.target.refNum, attack.target.mpNum)) victim = controller->getDedicatedActor(attack.target.refNum, attack.target.mpNum)->getPtr(); } if (attack.isHit) { bool isRanged = attack.type == attack.RANGED; MWWorld::Ptr weaponPtr; MWWorld::Ptr ammoPtr; bool usedTempRangedWeapon = false; bool usedTempRangedAmmo = false; // Get the attacker's current weapon // // Note: if using hand-to-hand, the weapon is equal to inv.end() if (attacker.getClass().hasInventoryStore(attacker)) { MWWorld::InventoryStore &inventoryStore = attacker.getClass().getInventoryStore(attacker); MWWorld::ContainerStoreIterator weaponSlot = inventoryStore.getSlot( MWWorld::InventoryStore::Slot_CarriedRight); weaponPtr = weaponSlot != inventoryStore.end() ? *weaponSlot : MWWorld::Ptr(); // Is the currently selected weapon different from the one recorded for this ranged attack? // If so, try to find the correct one in the attacker's inventory and use it here. If it // no longer exists, add it back temporarily. if (isRanged) { if (!weaponPtr || !Misc::StringUtils::ciEqual(weaponPtr.getCellRef().getRefId(), attack.rangedWeaponId)) { weaponPtr = inventoryStore.search(attack.rangedWeaponId); if (!weaponPtr) { weaponPtr = *attacker.getClass().getContainerStore(attacker).add(attack.rangedWeaponId, 1, attacker); usedTempRangedWeapon = true; } } if (!attack.rangedAmmoId.empty()) { MWWorld::ContainerStoreIterator ammoSlot = inventoryStore.getSlot( MWWorld::InventoryStore::Slot_Ammunition); ammoPtr = ammoSlot != inventoryStore.end() ? *ammoSlot : MWWorld::Ptr(); if (!ammoPtr || !Misc::StringUtils::ciEqual(ammoPtr.getCellRef().getRefId(), attack.rangedAmmoId)) { ammoPtr = inventoryStore.search(attack.rangedAmmoId); if (!ammoPtr) { ammoPtr = *attacker.getClass().getContainerStore(attacker).add(attack.rangedAmmoId, 1, attacker); usedTempRangedAmmo = true; } } } } if (!weaponPtr.isEmpty() && weaponPtr.getTypeName() != typeid(ESM::Weapon).name()) weaponPtr = MWWorld::Ptr(); } if (!weaponPtr.isEmpty()) { LOG_APPEND(TimedLog::LOG_VERBOSE, "- weapon: %s\n- isRanged: %s\n- applyWeaponEnchantment: %s\n- applyAmmoEnchantment: %s", weaponPtr.getCellRef().getRefId().c_str(), isRanged ? "true" : "false", attack.applyWeaponEnchantment ? "true" : "false", attack.applyAmmoEnchantment ? "true" : "false"); if (attack.applyWeaponEnchantment) { MWMechanics::CastSpell cast(attacker, victim, isRanged); cast.mHitPosition = attack.hitPosition.asVec3(); cast.cast(weaponPtr, false); } if (isRanged && !ammoPtr.isEmpty() && attack.applyAmmoEnchantment) { MWMechanics::CastSpell cast(attacker, victim, isRanged); cast.mHitPosition = attack.hitPosition.asVec3(); cast.cast(ammoPtr, false); } } if (victim.mRef != nullptr) { bool isHealthDamage = true; if (weaponPtr.isEmpty()) { if (attacker.getClass().isBipedal(attacker)) { MWMechanics::CreatureStats &victimStats = victim.getClass().getCreatureStats(victim); isHealthDamage = victimStats.isParalyzed() || victimStats.getKnockedDown(); } } if (!isRanged) MWMechanics::blockMeleeAttack(attacker, victim, weaponPtr, attack.damage, 1); victim.getClass().onHit(victim, attack.damage, isHealthDamage, weaponPtr, attacker, attack.hitPosition.asVec3(), attack.success); } // Remove temporary items that may have been added above for ranged attacks if (isRanged && attacker.getClass().hasInventoryStore(attacker)) { MWWorld::InventoryStore &inventoryStore = attacker.getClass().getInventoryStore(attacker); if (usedTempRangedWeapon) inventoryStore.remove(weaponPtr, 1, attacker); if (usedTempRangedAmmo) inventoryStore.remove(ammoPtr, 1, attacker); } } } void MechanicsHelper::processCast(Cast cast, const MWWorld::Ptr& caster) { LOG_MESSAGE_SIMPLE(TimedLog::LOG_VERBOSE, "Processing cast from %s of type %i", caster.getClass().getName(caster).c_str(), cast.type); LOG_APPEND(TimedLog::LOG_VERBOSE, "- pressed: %s", cast.pressed ? "true" : "false"); if (!cast.pressed) { LOG_APPEND(TimedLog::LOG_VERBOSE, "- success: %s", cast.success ? "true" : "false"); } MWBase::Environment::get().getMechanicsManager()->setAttackingOrSpell(caster, cast.pressed); MWMechanics::CreatureStats &casterStats = caster.getClass().getCreatureStats(caster); MWWorld::Ptr victim; if (cast.target.isPlayer) { if (cast.target.guid == mwmp::Main::get().getLocalPlayer()->guid) victim = MWMechanics::getPlayer(); else if (PlayerList::getPlayer(cast.target.guid) != nullptr) victim = PlayerList::getPlayer(cast.target.guid)->getPtr(); } else { auto controller = mwmp::Main::get().getCellController(); if (controller->isLocalActor(cast.target.refNum, cast.target.mpNum)) victim = controller->getLocalActor(cast.target.refNum, cast.target.mpNum)->getPtr(); else if (controller->isDedicatedActor(cast.target.refNum, cast.target.mpNum)) victim = controller->getDedicatedActor(cast.target.refNum, cast.target.mpNum)->getPtr(); } if (cast.type == cast.REGULAR) { casterStats.getSpells().setSelectedSpell(cast.spellId); if (cast.success) MWBase::Environment::get().getWorld()->castSpell(caster); LOG_APPEND(TimedLog::LOG_VERBOSE, "- spellId: %s", cast.spellId.c_str()); } else if (cast.type == cast.ITEM) { casterStats.getSpells().setSelectedSpell(""); MWWorld::InventoryStore& inventoryStore = caster.getClass().getInventoryStore(caster); MWWorld::ContainerStoreIterator it = inventoryStore.begin(); for (; it != inventoryStore.end(); ++it) { if (Misc::StringUtils::ciEqual(it->getCellRef().getRefId(), cast.itemId)) break; } // Add the item if it's missing if (it == inventoryStore.end()) it = caster.getClass().getContainerStore(caster).add(cast.itemId, 1, caster); inventoryStore.setSelectedEnchantItem(it); LOG_APPEND(TimedLog::LOG_VERBOSE, "- itemId: %s", cast.itemId.c_str()); MWBase::Environment::get().getWorld()->castSpell(caster); inventoryStore.setSelectedEnchantItem(inventoryStore.end()); } } void MechanicsHelper::createSpellGfx(const MWWorld::Ptr& targetPtr, const std::vector& mEffects) { for (auto&& effect : mEffects) { const ESM::MagicEffect* magicEffect = MWBase::Environment::get().getWorld()->getStore().get().find(effect.mEffectId); const ESM::Static* castStatic; if (!magicEffect->mHit.empty()) castStatic = MWBase::Environment::get().getWorld()->getStore().get().find(magicEffect->mHit); else castStatic = MWBase::Environment::get().getWorld()->getStore().get().find("VFX_DefaultHit"); bool loop = (magicEffect->mData.mFlags & ESM::MagicEffect::ContinuousVfx) != 0; // Note: in case of non actor, a free effect should be fine as well MWRender::Animation* anim = MWBase::Environment::get().getWorld()->getAnimation(targetPtr); if (anim && !castStatic->mModel.empty()) { anim->addEffect("meshes\\" + castStatic->mModel, magicEffect->mIndex, loop, "", magicEffect->mParticle); } } } bool MechanicsHelper::isStackingSpell(const std::string& id) { return !MWBase::Environment::get().getWorld()->getStore().get().search(id); } bool MechanicsHelper::doesEffectListContainEffect(const ESM::EffectList& effectList, short effectId, short attributeId, short skillId) { for (const auto &effect : effectList.mList) { if (effect.mEffectID == effectId) { if (attributeId == -1 || effect.mAttribute == attributeId) { if (skillId == -1 || effect.mSkill == skillId) { return true; } } } } return false; } void MechanicsHelper::unequipItemsByEffect(const MWWorld::Ptr& ptr, short enchantmentType, short effectId, short attributeId, short skillId) { MWBase::World *world = MWBase::Environment::get().getWorld(); MWWorld::InventoryStore &ptrInventory = ptr.getClass().getInventoryStore(ptr); for (int slot = 0; slot < MWWorld::InventoryStore::Slots; slot++) { if (ptrInventory.getSlot(slot) != ptrInventory.end()) { MWWorld::ConstContainerStoreIterator itemIterator = ptrInventory.getSlot(slot); std::string enchantmentName = itemIterator->getClass().getEnchantment(*itemIterator); if (!enchantmentName.empty()) { const ESM::Enchantment* enchantment = world->getStore().get().find(enchantmentName); if (enchantment->mData.mType == enchantmentType && doesEffectListContainEffect(enchantment->mEffects, effectId, attributeId, skillId)) ptrInventory.unequipSlot(slot, ptr); } } } } MWWorld::Ptr MechanicsHelper::getItemPtrFromStore(const mwmp::Item& item, MWWorld::ContainerStore& store) { MWWorld::Ptr closestPtr; for (MWWorld::ContainerStoreIterator storeIterator = store.begin(); storeIterator != store.end(); ++storeIterator) { // Enchantment charges are often in the process of refilling themselves, so don't check for them here if (Misc::StringUtils::ciEqual(item.refId, storeIterator->getCellRef().getRefId()) && item.count == storeIterator->getRefData().getCount() && item.charge == storeIterator->getCellRef().getCharge() && Misc::StringUtils::ciEqual(item.soul, storeIterator->getCellRef().getSoul())) { // If we have no closestPtr, set it to the Ptr corresponding to this storeIterator; otherwise, make // sure the storeIterator's enchantmentCharge is closer to our goal than that of the previous closestPtr if (!closestPtr || abs(storeIterator->getCellRef().getEnchantmentCharge() - item.enchantmentCharge) < abs(closestPtr.getCellRef().getEnchantmentCharge() - item.enchantmentCharge)) { closestPtr = *storeIterator; } } } return closestPtr; } ================================================ FILE: apps/openmw/mwmp/MechanicsHelper.hpp ================================================ #ifndef OPENMW_MECHANICSHELPER_HPP #define OPENMW_MECHANICSHELPER_HPP #include #include "../mwworld/containerstore.hpp" #include namespace MechanicsHelper { osg::Vec3f getLinearInterpolation(osg::Vec3f start, osg::Vec3f end, float percent); ESM::Position getPositionFromVector(osg::Vec3f vector); void spawnLeveledCreatures(MWWorld::CellStore* cellStore); bool isUsingRangedWeapon(const MWWorld::Ptr& ptr); mwmp::Attack *getLocalAttack(const MWWorld::Ptr& ptr); mwmp::Attack *getDedicatedAttack(const MWWorld::Ptr& ptr); mwmp::Cast *getLocalCast(const MWWorld::Ptr& ptr); mwmp::Cast *getDedicatedCast(const MWWorld::Ptr& ptr); MWWorld::Ptr getPlayerPtr(const mwmp::Target& target); unsigned int getActorId(const mwmp::Target& target); mwmp::Item getItem(const MWWorld::Ptr& itemPtr, int count); mwmp::Target getTarget(const MWWorld::Ptr& ptr); void clearTarget(mwmp::Target& target); bool isEmptyTarget(const mwmp::Target& target); void assignAttackTarget(mwmp::Attack* attack, const MWWorld::Ptr& target); void resetAttack(mwmp::Attack* attack); void resetCast(mwmp::Cast* cast); // See whether playerChecked belongs to playerWithTeam's team // Note: This is not supposed to also check if playerWithTeam is on playerChecked's // team, because it should technically be possible to be allied to someone // who isn't mutually allied to you bool isTeamMember(const MWWorld::Ptr& playerChecked, const MWWorld::Ptr& playerWithTeam); bool getSpellSuccess(std::string spellId, const MWWorld::Ptr& caster); void processAttack(mwmp::Attack attack, const MWWorld::Ptr& attacker); void processCast(mwmp::Cast cast, const MWWorld::Ptr& caster); void createSpellGfx(const MWWorld::Ptr& targetPtr, const std::vector& mEffects); bool isStackingSpell(const std::string& id); bool doesEffectListContainEffect(const ESM::EffectList& effectList, short effectId, short attributeId = -1, short skillId = -1); void unequipItemsByEffect(const MWWorld::Ptr& ptr, short enchantmentType, short effectId, short attributeId = -1, short skillId = -1); MWWorld::Ptr getItemPtrFromStore(const mwmp::Item& item, MWWorld::ContainerStore& store); } #endif //OPENMW_MECHANICSHELPER_HPP ================================================ FILE: apps/openmw/mwmp/Networking.cpp ================================================ #include #include #include #include #include #include #include #include #include #include "../mwbase/environment.hpp" #include "../mwbase/world.hpp" #include "../mwclass/npc.hpp" #include "../mwmechanics/combat.hpp" #include "../mwmechanics/npcstats.hpp" #include "../mwstate/statemanagerimp.hpp" #include "../mwworld/cellstore.hpp" #include "../mwworld/esmstore.hpp" #include "../mwworld/inventorystore.hpp" #include #include #include #include #include "Networking.hpp" #include "Main.hpp" #include "processors/ProcessorInitializer.hpp" #include "processors/SystemProcessor.hpp" #include "processors/PlayerProcessor.hpp" #include "processors/ObjectProcessor.hpp" #include "processors/ActorProcessor.hpp" #include "processors/WorldstateProcessor.hpp" #include "GUIController.hpp" #include "CellController.hpp" using namespace mwmp; std::string listDiscrepancies(PacketPreInit::PluginContainer checksums, PacketPreInit::PluginContainer checksumsResponse) { std::ostringstream sstr; sstr << "Your plugins or their load order don't match the server's. A full comparison is included in your debug window and latest log file. In short, the following discrepancies have been found:\n\n"; int discrepancyCount = 0; for (size_t fileIndex = 0; fileIndex < checksums.size() || fileIndex < checksumsResponse.size(); fileIndex++) { if (fileIndex >= checksumsResponse.size()) { discrepancyCount++; if (discrepancyCount > 1) sstr << "\n"; std::string clientFilename = checksums.at(fileIndex).first; sstr << fileIndex << ": "; sstr << clientFilename << " is past the number of plugins used by the server"; } else if (fileIndex >= checksums.size()) { discrepancyCount++; if (discrepancyCount > 1) sstr << "\n"; std::string serverFilename = checksumsResponse.at(fileIndex).first; sstr << fileIndex << ": "; sstr << serverFilename << " is completely missing from the client but required by the server"; } else { std::string clientFilename = checksums.at(fileIndex).first; std::string serverFilename = checksumsResponse.at(fileIndex).first; std::string clientChecksum = Utils::intToHexStr(checksums.at(fileIndex).second.at(0)); bool filenameMatches = false; bool checksumMatches = false; std::string eligibleChecksums = ""; if (Misc::StringUtils::ciEqual(clientFilename, serverFilename)) filenameMatches = true; if (checksumsResponse.at(fileIndex).second.size() > 0) { for (size_t checksumIndex = 0; checksumIndex < checksumsResponse.at(fileIndex).second.size(); checksumIndex++) { std::string serverChecksum = Utils::intToHexStr(checksumsResponse.at(fileIndex).second.at(checksumIndex)); if (checksumIndex != 0) eligibleChecksums = eligibleChecksums + " or "; eligibleChecksums = eligibleChecksums + serverChecksum; if (Misc::StringUtils::ciEqual(clientChecksum, serverChecksum)) { checksumMatches = true; break; } } } else checksumMatches = true; if (!filenameMatches || !checksumMatches) { discrepancyCount++; if (discrepancyCount > 1) sstr << "\n"; sstr << fileIndex << ": "; if (!filenameMatches) sstr << clientFilename << " doesn't match " << serverFilename; if (!filenameMatches && !checksumMatches) sstr << ", "; if (!checksumMatches) sstr << "checksum " << clientChecksum << " doesn't match " << eligibleChecksums; } } } return sstr.str(); } std::string listComparison(PacketPreInit::PluginContainer checksums, PacketPreInit::PluginContainer checksumsResponse, bool full = false) { std::ostringstream sstr; size_t pluginNameLen1 = 0; size_t pluginNameLen2 = 0; for (const auto &checksum : checksums) if (pluginNameLen1 < checksum.first.size()) pluginNameLen1 = checksum.first.size(); for (const auto &checksum : checksums) if (pluginNameLen2 < checksum.first.size()) pluginNameLen2 = checksum.first.size(); Utils::printWithWidth(sstr, "Your current plugins are:", pluginNameLen1 + 16); sstr << "To join this server, use:\n"; Utils::printWithWidth(sstr, "name", pluginNameLen1 + 2); Utils::printWithWidth(sstr, "hash", 14); Utils::printWithWidth(sstr, "name", pluginNameLen2 + 2); sstr << "hash\n"; for (size_t i = 0; i < checksums.size() || i < checksumsResponse.size(); i++) { std::string plugin; unsigned val; if (i < checksums.size()) { plugin = checksums.at(i).first; val = checksums.at(i).second[0]; Utils::printWithWidth(sstr, plugin, pluginNameLen1 + 2); Utils::printWithWidth(sstr, Utils::intToHexStr(val), 14); } else Utils::printWithWidth(sstr, "", pluginNameLen1 + 16); if (i < checksumsResponse.size()) { Utils::printWithWidth(sstr, checksumsResponse[i].first, pluginNameLen2 + 2); if (checksumsResponse[i].second.size() > 0) { if (full) for (size_t j = 0; j < checksumsResponse[i].second.size(); j++) Utils::printWithWidth(sstr, Utils::intToHexStr(checksumsResponse[i].second[j]), 14); else sstr << Utils::intToHexStr(checksumsResponse[i].second[0]); } else sstr << "any"; } sstr << "\n"; } return sstr.str(); } Networking::Networking(): peer(RakNet::RakPeerInterface::GetInstance()), systemPacketController(peer), playerPacketController(peer), actorPacketController(peer), objectPacketController(peer), worldstatePacketController(peer) { RakNet::SocketDescriptor sd; sd.port=0; auto b = peer->Startup(1, &sd, 1); RakAssert(b==RakNet::CRABNET_STARTED); systemPacketController.SetStream(0, &bsOut); playerPacketController.SetStream(0, &bsOut); actorPacketController.SetStream(0, &bsOut); objectPacketController.SetStream(0, &bsOut); worldstatePacketController.SetStream(0, &bsOut); connected = 0; ProcessorInitializer(); } Networking::~Networking() { peer->Shutdown(100); peer->CloseConnection(peer->GetSystemAddressFromIndex(0), true, 0); RakNet::RakPeerInterface::DestroyInstance(peer); } void Networking::update() { RakNet::Packet *packet; std::string errmsg = ""; for (packet=peer->Receive(); packet; peer->DeallocatePacket(packet), packet=peer->Receive()) { switch (packet->data[0]) { case ID_REMOTE_DISCONNECTION_NOTIFICATION: LOG_MESSAGE_SIMPLE(TimedLog::LOG_INFO, "Another client has disconnected."); break; case ID_REMOTE_CONNECTION_LOST: LOG_MESSAGE_SIMPLE(TimedLog::LOG_INFO, "Another client has lost connection."); break; case ID_REMOTE_NEW_INCOMING_CONNECTION: LOG_MESSAGE_SIMPLE(TimedLog::LOG_INFO, "Another client has connected."); break; case ID_CONNECTION_REQUEST_ACCEPTED: LOG_MESSAGE_SIMPLE(TimedLog::LOG_WARN, "Our connection request has been accepted."); break; case ID_NEW_INCOMING_CONNECTION: LOG_MESSAGE_SIMPLE(TimedLog::LOG_INFO, "A connection is incoming."); break; case ID_NO_FREE_INCOMING_CONNECTIONS: errmsg = "The server is full."; break; case ID_DISCONNECTION_NOTIFICATION: errmsg = "We have been disconnected."; break; case ID_CONNECTION_LOST: errmsg = "Connection lost."; break; default: receiveMessage(packet); //LOG_MESSAGE_SIMPLE(TimedLog::LOG_INFO, "Message with identifier %i has arrived.", packet->data[0]); break; } } if (!errmsg.empty()) { LOG_MESSAGE_SIMPLE(TimedLog::LOG_ERROR, errmsg.c_str()); SDL_ShowSimpleMessageBox(SDL_MESSAGEBOX_ERROR, "tes3mp", errmsg.c_str(), 0); MWBase::Environment::get().getStateManager()->requestQuit(); } } void Networking::connect(const std::string &ip, unsigned short port, std::vector &content, Files::Collections &collections) { RakNet::SystemAddress master; master.SetBinaryAddress(ip.c_str()); master.SetPortHostOrder(port); std::string errmsg = ""; std::stringstream sstr; sstr << TES3MP_VERSION; sstr << TES3MP_PROTO_VERSION; std::string commitHashString = Version::getOpenmwVersion(Main::getResDir()).mCommitHash; // Remove carriage returns added to version file on Windows commitHashString.erase(std::remove(commitHashString.begin(), commitHashString.end(), '\r'), commitHashString.end()); sstr << commitHashString; if (peer->Connect(master.ToString(false), master.GetPort(), sstr.str().c_str(), (int) sstr.str().size(), 0, 0, 3, 500, 0) != RakNet::CONNECTION_ATTEMPT_STARTED) errmsg = "Connection attempt failed.\n"; bool queue = true; while (queue) { for (RakNet::Packet *packet = peer->Receive(); packet; peer->DeallocatePacket(packet), packet = peer->Receive()) { switch (packet->data[0]) { case ID_CONNECTION_ATTEMPT_FAILED: { errmsg = "Connection failed.\n" "Either the IP address is wrong or a firewall on either system is blocking\n" "UDP packets on the port you have chosen."; queue = false; break; } case ID_INVALID_PASSWORD: { errmsg = "Version mismatch!\nYour client is on version " TES3MP_VERSION "\n" "Please make sure the server is on the same version."; queue = false; break; } case ID_INCOMPATIBLE_PROTOCOL_VERSION: { errmsg = "Network protocol mismatch!\nMake sure your client is really on the same version\n" "as the server you are trying to connect to."; queue = false; break; } case ID_CONNECTION_REQUEST_ACCEPTED: { serverAddr = packet->systemAddress; BaseClientPacketProcessor::SetServerAddr(packet->systemAddress); connected = true; queue = false; LOG_MESSAGE_SIMPLE(TimedLog::LOG_WARN, "Received ID_CONNECTION_REQUESTED_ACCEPTED from %s", serverAddr.ToString()); break; } case ID_DISCONNECTION_NOTIFICATION: throw std::runtime_error("ID_DISCONNECTION_NOTIFICATION.\n"); case ID_CONNECTION_BANNED: throw std::runtime_error("You have been banned from this server.\n"); case ID_CONNECTION_LOST: throw std::runtime_error("ID_CONNECTION_LOST.\n"); default: LOG_MESSAGE_SIMPLE(TimedLog::LOG_INFO, "Connection message with identifier %i has arrived in initialization.", packet->data[0]); } } } if (!errmsg.empty()) { LOG_MESSAGE_SIMPLE(TimedLog::LOG_ERROR, errmsg.c_str()); SDL_ShowSimpleMessageBox(SDL_MESSAGEBOX_ERROR, "tes3mp", errmsg.c_str(), 0); } else preInit(content, collections); getLocalPlayer()->guid = getLocalSystem()->guid = peer->GetMyGUID(); } void Networking::preInit(std::vector &content, Files::Collections &collections) { PacketPreInit::PluginContainer checksums; std::vector::const_iterator it(content.begin()); for (int idx = 0; it != content.end(); ++it, ++idx) { boost::filesystem::path filename(*it); const Files::MultiDirCollection& col = collections.getCollection(filename.extension().string()); if (col.doesExist(*it)) { PacketPreInit::HashList hashList; unsigned crc32 = Utils::crc32Checksum(col.getPath(*it).string()); hashList.push_back(crc32); checksums.push_back(make_pair(*it, hashList)); LOG_APPEND(TimedLog::LOG_WARN, "idx: %d\tchecksum: %X\tfile: %s\n", idx, crc32, col.getPath(*it).string().c_str()); } else throw std::runtime_error("Plugin doesn't exist."); } PacketPreInit packetPreInit(peer); RakNet::BitStream bs; RakNet::RakNetGUID guid; packetPreInit.setChecksums(&checksums); packetPreInit.setGUID(guid); packetPreInit.SetSendStream(&bs); packetPreInit.Send(serverAddr); PacketPreInit::PluginContainer checksumsResponse; bool done = false; while (!done) { RakNet::Packet *packet = peer->Receive(); if (!packet) { RakSleep(500); continue; } RakNet::BitStream bsIn(&packet->data[0], packet->length, false); unsigned char packetId; bsIn.Read(packetId); switch(packetId) { case ID_DISCONNECTION_NOTIFICATION: case ID_CONNECTION_LOST: done = true; break; case ID_GAME_PREINIT: bsIn.IgnoreBytes((unsigned) RakNet::RakNetGUID::size()); packetPreInit.setChecksums(&checksumsResponse); packetPreInit.Packet(&bsIn, false); done = true; break; } peer->DeallocatePacket(packet); } if (!checksumsResponse.empty()) // something wrong { std::string errmsg = listDiscrepancies(checksums, checksumsResponse); LOG_MESSAGE_SIMPLE(TimedLog::LOG_ERROR, listDiscrepancies(checksums, checksumsResponse).c_str()); LOG_MESSAGE_SIMPLE(TimedLog::LOG_ERROR, listComparison(checksums, checksumsResponse, true).c_str()); SDL_ShowSimpleMessageBox(SDL_MESSAGEBOX_ERROR, "tes3mp", errmsg.c_str(), 0); connected = false; } } void Networking::receiveMessage(RakNet::Packet *packet) { if (packet->length < 2) return; if (systemPacketController.ContainsPacket(packet->data[0])) { if (!SystemProcessor::Process(*packet)) LOG_MESSAGE_SIMPLE(TimedLog::LOG_WARN, "Unhandled SystemPacket with identifier %i has arrived", packet->data[0]); } else if (playerPacketController.ContainsPacket(packet->data[0])) { if (!PlayerProcessor::Process(*packet)) LOG_MESSAGE_SIMPLE(TimedLog::LOG_WARN, "Unhandled PlayerPacket with identifier %i has arrived", packet->data[0]); } else if (actorPacketController.ContainsPacket(packet->data[0])) { if (!ActorProcessor::Process(*packet, actorList)) LOG_MESSAGE_SIMPLE(TimedLog::LOG_WARN, "Unhandled ActorPacket with identifier %i has arrived", packet->data[0]); } else if (objectPacketController.ContainsPacket(packet->data[0])) { if (!ObjectProcessor::Process(*packet, objectList)) LOG_MESSAGE_SIMPLE(TimedLog::LOG_WARN, "Unhandled ObjectPacket with identifier %i has arrived", packet->data[0]); } else if (worldstatePacketController.ContainsPacket(packet->data[0])) { if (!WorldstateProcessor::Process(*packet, worldstate)) LOG_MESSAGE_SIMPLE(TimedLog::LOG_WARN, "Unhandled WorldstatePacket with identifier %i has arrived", packet->data[0]); } } SystemPacket *Networking::getSystemPacket(RakNet::MessageID id) { return systemPacketController.GetPacket(id); } PlayerPacket *Networking::getPlayerPacket(RakNet::MessageID id) { return playerPacketController.GetPacket(id); } ActorPacket *Networking::getActorPacket(RakNet::MessageID id) { return actorPacketController.GetPacket(id); } ObjectPacket *Networking::getObjectPacket(RakNet::MessageID id) { return objectPacketController.GetPacket(id); } WorldstatePacket *Networking::getWorldstatePacket(RakNet::MessageID id) { return worldstatePacketController.GetPacket(id); } LocalSystem *Networking::getLocalSystem() { return mwmp::Main::get().getLocalSystem(); } LocalPlayer *Networking::getLocalPlayer() { return mwmp::Main::get().getLocalPlayer(); } ActorList *Networking::getActorList() { return &actorList; } ObjectList *Networking::getObjectList() { return &objectList; } Worldstate *Networking::getWorldstate() { return &worldstate; } bool Networking::isConnected() { return connected; } ================================================ FILE: apps/openmw/mwmp/Networking.hpp ================================================ #ifndef OPENMW_NETWORKING_HPP #define OPENMW_NETWORKING_HPP #include #include #include #include #include #include #include #include #include #include #include "LocalSystem.hpp" #include "ActorList.hpp" #include "ObjectList.hpp" #include "Worldstate.hpp" namespace mwmp { class LocalPlayer; class Networking { public: Networking(); ~Networking(); void connect(const std::string& ip, unsigned short port, std::vector &content, Files::Collections &collections); void update(); SystemPacket *getSystemPacket(RakNet::MessageID id); PlayerPacket *getPlayerPacket(RakNet::MessageID id); ActorPacket *getActorPacket(RakNet::MessageID id); ObjectPacket *getObjectPacket(RakNet::MessageID id); WorldstatePacket *getWorldstatePacket(RakNet::MessageID id); RakNet::SystemAddress serverAddress() { return serverAddr; } bool isConnected(); LocalSystem *getLocalSystem(); LocalPlayer *getLocalPlayer(); ActorList *getActorList(); ObjectList *getObjectList(); Worldstate *getWorldstate(); private: bool connected; RakNet::RakPeerInterface *peer; RakNet::SystemAddress serverAddr; RakNet::BitStream bsOut; SystemPacketController systemPacketController; PlayerPacketController playerPacketController; ActorPacketController actorPacketController; ObjectPacketController objectPacketController; WorldstatePacketController worldstatePacketController; ActorList actorList; ObjectList objectList; Worldstate worldstate; void receiveMessage(RakNet::Packet *packet); void preInit(std::vector &content, Files::Collections &collections); }; } #endif //OPENMW_NETWORKING_HPP ================================================ FILE: apps/openmw/mwmp/ObjectList.cpp ================================================ #include "ObjectList.hpp" #include "Main.hpp" #include "Networking.hpp" #include "MechanicsHelper.hpp" #include "LocalPlayer.hpp" #include "DedicatedPlayer.hpp" #include "PlayerList.hpp" #include "CellController.hpp" #include "RecordHelper.hpp" #include #include #include "../mwbase/world.hpp" #include "../mwbase/environment.hpp" #include "../mwbase/mechanicsmanager.hpp" #include "../mwbase/scriptmanager.hpp" #include "../mwbase/soundmanager.hpp" #include "../mwbase/windowmanager.hpp" #include "../mwgui/container.hpp" #include "../mwgui/dialogue.hpp" #include "../mwgui/inventorywindow.hpp" #include "../mwgui/windowmanagerimp.hpp" #include "../mwmechanics/aifollow.hpp" #include "../mwmechanics/creaturestats.hpp" #include "../mwmechanics/spellcasting.hpp" #include "../mwmechanics/summoning.hpp" #include "../mwrender/animation.hpp" #include "../mwscript/interpretercontext.hpp" #include "../mwworld/class.hpp" #include "../mwworld/containerstore.hpp" #include "../mwworld/esmstore.hpp" #include "../mwworld/inventorystore.hpp" #include "../mwworld/manualref.hpp" #include "../mwworld/timestamp.hpp" using namespace mwmp; ObjectList::ObjectList() { } ObjectList::~ObjectList() { } Networking *ObjectList::getNetworking() { return mwmp::Main::get().getNetworking(); } void ObjectList::reset() { cell.blank(); baseObjects.clear(); guid = mwmp::Main::get().getNetworking()->getLocalPlayer()->guid; action = -1; containerSubAction = 0; } void ObjectList::addBaseObject(BaseObject baseObject) { baseObjects.push_back(baseObject); } mwmp::BaseObject ObjectList::getBaseObjectFromPtr(const MWWorld::Ptr& ptr) { mwmp::BaseObject baseObject; if (ptr == MWBase::Environment::get().getWorld()->getPlayerPtr()) { baseObject.isPlayer = true; baseObject.guid = mwmp::Main::get().getLocalPlayer()->guid; } else if (mwmp::PlayerList::isDedicatedPlayer(ptr)) { baseObject.isPlayer = true; baseObject.guid = mwmp::PlayerList::getPlayer(ptr)->guid; } else { baseObject.isPlayer = false; baseObject.refId = ptr.getCellRef().getRefId(); baseObject.refNum = ptr.getCellRef().getRefNum().mIndex; baseObject.mpNum = ptr.getCellRef().getMpNum(); } return baseObject; } void ObjectList::addContainerItem(mwmp::BaseObject& baseObject, const MWWorld::Ptr& itemPtr, int itemCount, int actionCount) { mwmp::ContainerItem containerItem; containerItem.refId = itemPtr.getCellRef().getRefId(); containerItem.count = itemCount; containerItem.charge = itemPtr.getCellRef().getCharge(); containerItem.enchantmentCharge = itemPtr.getCellRef().getEnchantmentCharge(); containerItem.soul = itemPtr.getCellRef().getSoul(); containerItem.actionCount = actionCount; LOG_APPEND(TimedLog::LOG_VERBOSE, "--- Adding container item %s to packet with count %i and actionCount %i", containerItem.refId.c_str(), itemCount, actionCount); baseObject.containerItems.push_back(containerItem); } void ObjectList::addContainerItem(mwmp::BaseObject& baseObject, const MWGui::ItemStack& itemStack, int itemCount, int actionCount) { mwmp::ContainerItem containerItem; containerItem.refId = itemStack.mBase.getCellRef().getRefId(); containerItem.count = itemCount; containerItem.charge = itemStack.mBase.getCellRef().getCharge(); containerItem.enchantmentCharge = itemStack.mBase.getCellRef().getEnchantmentCharge(); containerItem.soul = itemStack.mBase.getCellRef().getSoul(); containerItem.actionCount = actionCount; LOG_APPEND(TimedLog::LOG_VERBOSE, "--- Adding container item %s to packet with count %i and actionCount %i", containerItem.refId.c_str(), itemCount, actionCount); baseObject.containerItems.push_back(containerItem); } void ObjectList::addContainerItem(mwmp::BaseObject& baseObject, const std::string itemId, int itemCount, int actionCount) { mwmp::ContainerItem containerItem; containerItem.refId = itemId; containerItem.count = itemCount; containerItem.charge = -1; containerItem.enchantmentCharge = -1; containerItem.soul = ""; containerItem.actionCount = actionCount; LOG_APPEND(TimedLog::LOG_VERBOSE, "--- Adding container item %s to packet with count %i and actionCount %i", containerItem.refId.c_str(), itemCount, actionCount); baseObject.containerItems.push_back(containerItem); } void ObjectList::addEntireContainer(const MWWorld::Ptr& ptr) { LOG_APPEND(TimedLog::LOG_VERBOSE, "-- Adding entire container %s %i-%i", ptr.getCellRef().getRefId().c_str(), ptr.getCellRef().getRefNum().mIndex, ptr.getCellRef().getMpNum()); MWWorld::ContainerStore& containerStore = ptr.getClass().getContainerStore(ptr); mwmp::BaseObject baseObject = getBaseObjectFromPtr(ptr); // If the container store has not been populated with items yet, handle that now if (!containerStore.isResolved()) containerStore.resolve(); for (const auto itemPtr : containerStore) { addContainerItem(baseObject, itemPtr, itemPtr.getRefData().getCount(), itemPtr.getRefData().getCount()); } addBaseObject(baseObject); } void ObjectList::editContainers(MWWorld::CellStore* cellStore) { bool isLocalEvent = guid == Main::get().getLocalPlayer()->guid; LOG_APPEND(TimedLog::LOG_VERBOSE, "- isLocalEvent? %s", isLocalEvent ? "true" : "false"); BaseObject baseObject; for (unsigned int i = 0; i < baseObjectCount; i++) { baseObject = baseObjects.at(i); LOG_APPEND(TimedLog::LOG_VERBOSE, "- container %s %i-%i", baseObject.refId.c_str(), baseObject.refNum, baseObject.mpNum); MWWorld::Ptr ptrFound = cellStore->searchExact(baseObject.refNum, baseObject.mpNum, baseObject.refId); if (ptrFound) { LOG_APPEND(TimedLog::LOG_VERBOSE, "-- Found %s %i-%i", ptrFound.getCellRef().getRefId().c_str(), ptrFound.getCellRef().getRefNum(), ptrFound.getCellRef().getMpNum()); bool isCurrentContainer = false; bool hasActorEquipment = ptrFound.getClass().isActor() && ptrFound.getClass().hasInventoryStore(ptrFound); // If we are in a container, and it happens to be this container, keep track of that if (MWBase::Environment::get().getWindowManager()->containsMode(MWGui::GM_Container)) { CurrentContainer *currentContainer = &mwmp::Main::get().getLocalPlayer()->currentContainer; if (currentContainer->refNum == ptrFound.getCellRef().getRefNum().mIndex && currentContainer->mpNum == ptrFound.getCellRef().getMpNum()) { isCurrentContainer = true; } } MWWorld::ContainerStore& containerStore = ptrFound.getClass().getContainerStore(ptrFound); // If we are setting the entire contents, clear the current ones if (action == BaseObjectList::SET) { containerStore.setResolved(true); containerStore.clear(); } bool isLocalDrag = isLocalEvent && containerSubAction == BaseObjectList::DRAG; bool isLocalTakeAll = isLocalEvent && containerSubAction == BaseObjectList::TAKE_ALL; std::string takeAllSound = ""; MWWorld::Ptr ownerPtr = ptrFound.getClass().isActor() ? ptrFound : MWBase::Environment::get().getWorld()->getPlayerPtr(); for (const auto &containerItem : baseObject.containerItems) { //LOG_APPEND(TimedLog::LOG_VERBOSE, "-- containerItem %s, count: %i, actionCount: %i", // containerItem.refId.c_str(), containerItem.count, containerItem.actionCount); if (containerItem.refId.find("$dynamic") != std::string::npos) continue; if (action == BaseObjectList::SET || action == BaseObjectList::ADD) { // Create a ManualRef to be able to set item charge MWWorld::ManualRef ref(MWBase::Environment::get().getWorld()->getStore(), containerItem.refId, 1); MWWorld::Ptr newPtr = ref.getPtr(); if (containerItem.count > 1) newPtr.getRefData().setCount(containerItem.count); if (containerItem.charge > -1) newPtr.getCellRef().setCharge(containerItem.charge); if (containerItem.enchantmentCharge > -1) newPtr.getCellRef().setEnchantmentCharge(containerItem.enchantmentCharge); if (!containerItem.soul.empty()) newPtr.getCellRef().setSoul(containerItem.soul); containerStore.add(newPtr, containerItem.count, ownerPtr); } else if (action == BaseObjectList::REMOVE && containerItem.actionCount > 0) { // We have to find the right item ourselves because ContainerStore has no method // accounting for charge for (const auto itemPtr : containerStore) { if (Misc::StringUtils::ciEqual(itemPtr.getCellRef().getRefId(), containerItem.refId)) { if (itemPtr.getCellRef().getCharge() == containerItem.charge && itemPtr.getCellRef().getEnchantmentCharge() == containerItem.enchantmentCharge && Misc::StringUtils::ciEqual(itemPtr.getCellRef().getSoul(), containerItem.soul)) { // Store the sound of the first item in a TAKE_ALL if (isLocalTakeAll && takeAllSound.empty()) takeAllSound = itemPtr.getClass().getUpSoundId(itemPtr); // Is this an actor's container? If so, unequip this item if it was equipped if (hasActorEquipment) { MWWorld::InventoryStore& invStore = ptrFound.getClass().getInventoryStore(ptrFound); if (invStore.isEquipped(itemPtr)) invStore.unequipItemQuantity(itemPtr, ptrFound, containerItem.count); } bool isDragResolved = false; if (isLocalDrag && isCurrentContainer) { MWGui::ContainerWindow* containerWindow = MWBase::Environment::get().getWindowManager()->getContainerWindow(); if (!containerWindow->isOnDragAndDrop()) { isDragResolved = containerWindow->dragItemByPtr(itemPtr, containerItem.actionCount); } } if (!isLocalDrag || !isDragResolved) { containerStore.remove(itemPtr, containerItem.actionCount, ownerPtr); if (isLocalDrag || isLocalTakeAll) { MWWorld::Ptr ptrPlayer = MWBase::Environment::get().getWorld()->getPlayerPtr(); MWWorld::ContainerStore &playerStore = ptrPlayer.getClass().getContainerStore(ptrPlayer); *playerStore.add(itemPtr, containerItem.actionCount, ownerPtr, false); } } } } } } } // Was this a SET or ADD action on an actor's container, and are we the authority // over the actor? If so, autoequip the actor if ((action == BaseObjectList::ADD || action == BaseObjectList::SET) && hasActorEquipment && mwmp::Main::get().getCellController()->isLocalActor(ptrFound)) { MWWorld::InventoryStore& invStore = ptrFound.getClass().getInventoryStore(ptrFound); invStore.autoEquip(ptrFound); mwmp::Main::get().getCellController()->getLocalActor(ptrFound)->updateEquipment(true, true); } // If this container can be harvested, disable and then enable it again to refresh its animation if (ptrFound.getClass().canBeHarvested(ptrFound)) { MWBase::Environment::get().getWorld()->disable(ptrFound); MWBase::Environment::get().getWorld()->enable(ptrFound); } // If this container was open for us, update its view if (isCurrentContainer) { if (isLocalTakeAll) { MWBase::Environment::get().getWindowManager()->removeGuiMode(MWGui::GM_Container); MWBase::Environment::get().getWindowManager()->playSound(takeAllSound); } else { MWGui::ContainerWindow* containerWindow = MWBase::Environment::get().getWindowManager()->getContainerWindow(); containerWindow->setPtr(ptrFound); } } } } } void ObjectList::activateObjects(MWWorld::CellStore* cellStore) { for (const auto &baseObject : baseObjects) { MWWorld::Ptr ptrFound; if (baseObject.isPlayer) { if (baseObject.guid == Main::get().getLocalPlayer()->guid) { ptrFound = Main::get().getLocalPlayer()->getPlayerPtr(); LOG_APPEND(TimedLog::LOG_VERBOSE, "-- Activated object is local player"); } else { DedicatedPlayer *player = PlayerList::getPlayer(baseObject.guid); if (player != 0) { ptrFound = player->getPtr(); LOG_APPEND(TimedLog::LOG_VERBOSE, "-- Activated object is player %s", player->npc.mName.c_str()); } else { LOG_APPEND(TimedLog::LOG_VERBOSE, "-- Could not find player to activate!"); } } } else { LOG_APPEND(TimedLog::LOG_VERBOSE, "-- Activated object is %s %i-%i", baseObject.refId.c_str(), baseObject.refNum, baseObject.mpNum); ptrFound = cellStore->searchExact(baseObject.refNum, baseObject.mpNum, baseObject.refId); } if (ptrFound) { MWWorld::Ptr activatingActorPtr; if (baseObject.activatingActor.isPlayer) { activatingActorPtr = MechanicsHelper::getPlayerPtr(baseObject.activatingActor); LOG_APPEND(TimedLog::LOG_VERBOSE, "-- Object has been activated by player %s", activatingActorPtr.getClass().getName(activatingActorPtr).c_str()); } else { activatingActorPtr = cellStore->searchExact(baseObject.activatingActor.refNum, baseObject.activatingActor.mpNum, baseObject.activatingActor.refId); LOG_APPEND(TimedLog::LOG_VERBOSE, "-- Object has been activated by actor %s %i-%i", activatingActorPtr.getCellRef().getRefId().c_str(), activatingActorPtr.getCellRef().getRefNum().mIndex, activatingActorPtr.getCellRef().getMpNum()); } if (activatingActorPtr) { // Is an item that can be picked up being activated by the local player with their inventory open? if (activatingActorPtr == MWBase::Environment::get().getWorld()->getPlayerPtr() && (MWBase::Environment::get().getWindowManager()->getMode() == MWGui::GM_Container || MWBase::Environment::get().getWindowManager()->getMode() == MWGui::GM_Inventory)) { MWBase::Environment::get().getWindowManager()->getInventoryWindow()->pickUpObject(ptrFound); } else { MWBase::Environment::get().getWorld()->activate(ptrFound, activatingActorPtr); } } } } } void ObjectList::placeObjects(MWWorld::CellStore* cellStore) { MWBase::World *world = MWBase::Environment::get().getWorld(); for (const auto &baseObject : baseObjects) { LOG_APPEND(TimedLog::LOG_VERBOSE, "- cellRef: %s %i-%i, count: %i, charge: %i, enchantmentCharge: %.2f, soul: %s", baseObject.refId.c_str(), baseObject.refNum, baseObject.mpNum, baseObject.count, baseObject.charge, baseObject.enchantmentCharge, baseObject.soul.c_str()); // Ignore generic dynamic refIds because they could be anything on other clients if (baseObject.refId.find("$dynamic") != std::string::npos) continue; MWWorld::Ptr ptrFound = cellStore->searchExact(0, baseObject.mpNum); // Only create this object if it doesn't already exist if (!ptrFound) { try { MWWorld::ManualRef ref(world->getStore(), baseObject.refId, 1); MWWorld::Ptr newPtr = ref.getPtr(); if (baseObject.count > 1) newPtr.getRefData().setCount(baseObject.count); if (baseObject.charge > -1) newPtr.getCellRef().setCharge(baseObject.charge); if (baseObject.enchantmentCharge > -1) newPtr.getCellRef().setEnchantmentCharge(baseObject.enchantmentCharge); if (!baseObject.soul.empty()) newPtr.getCellRef().setSoul(baseObject.soul); newPtr.getCellRef().setGoldValue(baseObject.goldValue); newPtr = world->placeObject(newPtr, cellStore, baseObject.position); // Because gold automatically gets replaced with a new object, make sure we set the mpNum at the end newPtr.getCellRef().setMpNum(baseObject.mpNum); if (baseObject.droppedByPlayer) { MWBase::Environment::get().getSoundManager()->playSound3D(newPtr, newPtr.getClass().getDownSoundId(newPtr), 1.f, 1.f); if (guid == Main::get().getLocalPlayer()->guid) world->PCDropped(newPtr); } } catch (std::exception&) { LOG_MESSAGE_SIMPLE(TimedLog::LOG_ERROR, "Ignored placement of invalid object %s", baseObject.refId.c_str()); } } else LOG_APPEND(TimedLog::LOG_VERBOSE, "-- Object already existed!"); } } void ObjectList::spawnObjects(MWWorld::CellStore* cellStore) { for (const auto &baseObject : baseObjects) { LOG_APPEND(TimedLog::LOG_VERBOSE, "- cellRef: %s %i-%i", baseObject.refId.c_str(), baseObject.refNum, baseObject.mpNum); // Ignore generic dynamic refIds because they could be anything on other clients if (baseObject.refId.find("$dynamic") != std::string::npos) continue; else if (!RecordHelper::doesRecordIdExist(baseObject.refId) && !RecordHelper::doesRecordIdExist(baseObject.refId)) { LOG_MESSAGE_SIMPLE(TimedLog::LOG_ERROR, "Ignored spawning of invalid object %s", baseObject.refId.c_str()); continue; } MWWorld::Ptr ptrFound = cellStore->searchExact(0, baseObject.mpNum); // Only create this object if it doesn't already exist if (!ptrFound) { MWWorld::ManualRef ref(MWBase::Environment::get().getWorld()->getStore(), baseObject.refId, 1); MWWorld::Ptr newPtr = ref.getPtr(); newPtr.getCellRef().setMpNum(baseObject.mpNum); newPtr = MWBase::Environment::get().getWorld()->placeObject(newPtr, cellStore, baseObject.position); MWMechanics::CreatureStats& creatureStats = newPtr.getClass().getCreatureStats(newPtr); if (baseObject.isSummon) { MWWorld::Ptr masterPtr; if (baseObject.master.isPlayer) masterPtr = MechanicsHelper::getPlayerPtr(baseObject.master); else masterPtr = cellStore->searchExact(baseObject.master.refNum, baseObject.master.mpNum, baseObject.master.refId); if (masterPtr) { LOG_APPEND(TimedLog::LOG_VERBOSE, "-- Actor has master: %s", masterPtr.getCellRef().getRefId().c_str()); MWMechanics::AiFollow package(masterPtr); creatureStats.getAiSequence().stack(package, newPtr); MWRender::Animation* anim = MWBase::Environment::get().getWorld()->getAnimation(newPtr); if (anim) { const ESM::Static* fx = MWBase::Environment::get().getWorld()->getStore().get() .search("VFX_Summon_Start"); if (fx) anim->addEffect("meshes\\" + fx->mModel, -1, false); } int creatureActorId = newPtr.getClass().getCreatureStats(newPtr).getActorId(); MWMechanics::CreatureStats& masterCreatureStats = masterPtr.getClass().getCreatureStats(masterPtr); std::vector activeEffects; ESM::ActiveEffect activeEffect; activeEffect.mEffectId = baseObject.summonEffectId; activeEffect.mDuration = baseObject.summonDuration; activeEffect.mMagnitude = 1; activeEffects.push_back(activeEffect); LOG_APPEND(TimedLog::LOG_INFO, "-- adding active spell to master with id %s, effect %i, duration %f", baseObject.summonSpellId.c_str(), baseObject.summonEffectId, baseObject.summonDuration); auto activeSpells = masterCreatureStats.getActiveSpells(); if (!activeSpells.isSpellActive(baseObject.summonSpellId)) activeSpells.addSpell(baseObject.summonSpellId, false, activeEffects, "", masterCreatureStats.getActorId()); LOG_APPEND(TimedLog::LOG_INFO, "-- setting summoned creatureActorId for %i-%i to %i", newPtr.getCellRef().getRefNum(), newPtr.getCellRef().getMpNum(), creatureActorId); // Check if this creature is present in the summoner's summoned creature map std::map& creatureMap = masterCreatureStats.getSummonedCreatureMap(); bool foundSummonedCreature = false; for (std::map::iterator it = creatureMap.begin(); it != creatureMap.end(); ) { if (it->first.mEffectId == baseObject.summonEffectId && it->first.mSourceId == baseObject.summonSpellId) { foundSummonedCreature = true; break; } ++it; } // If it is, update its creatureActorId if (foundSummonedCreature) { masterCreatureStats.setSummonedCreatureActorId(baseObject.refId, creatureActorId); } // If not, add it to the summoned creature map else { ESM::SummonKey summonKey(baseObject.summonEffectId, baseObject.summonSpellId, -1); creatureMap.emplace(summonKey, creatureActorId); } creatureStats.setFriendlyHits(0); } } } else LOG_APPEND(TimedLog::LOG_VERBOSE, "-- Actor already existed!"); } } void ObjectList::deleteObjects(MWWorld::CellStore* cellStore) { for (const auto &baseObject : baseObjects) { LOG_APPEND(TimedLog::LOG_VERBOSE, "- cellRef: %s %i-%i", baseObject.refId.c_str(), baseObject.refNum, baseObject.mpNum); MWWorld::Ptr ptrFound = cellStore->searchExact(baseObject.refNum, baseObject.mpNum, baseObject.refId); if (ptrFound) { LOG_APPEND(TimedLog::LOG_VERBOSE, "-- Found %s %i-%i", ptrFound.getCellRef().getRefId().c_str(), ptrFound.getCellRef().getRefNum(), ptrFound.getCellRef().getMpNum()); // If we are in a container, and it happens to be this object, exit it if (MWBase::Environment::get().getWindowManager()->containsMode(MWGui::GM_Container)) { CurrentContainer *currentContainer = &mwmp::Main::get().getLocalPlayer()->currentContainer; if (currentContainer->refNum == ptrFound.getCellRef().getRefNum().mIndex && currentContainer->mpNum == ptrFound.getCellRef().getMpNum()) { MWBase::Environment::get().getWindowManager()->removeGuiMode(MWGui::GM_Container); MWBase::Environment::get().getWindowManager()->setDragDrop(false); } } // Is this a dying actor being deleted before its death animation has finished? If so, // increase the death count for the actor if applicable and run the actor's script, // which is the same as what happens in OpenMW's ContainerWindow::onDisposeCorpseButtonClicked() // if an actor's corpse is disposed of before its death animation is finished if (ptrFound.getClass().isActor()) { MWMechanics::CreatureStats& creatureStats = ptrFound.getClass().getCreatureStats(ptrFound); if (creatureStats.isDead() && !creatureStats.isDeathAnimationFinished()) { creatureStats.setDeathAnimationFinished(true); MWBase::Environment::get().getMechanicsManager()->notifyDied(ptrFound); const std::string script = ptrFound.getClass().getScript(ptrFound); if (!script.empty() && MWBase::Environment::get().getWorld()->getScriptsEnabled()) { MWScript::InterpreterContext interpreterContext(&ptrFound.getRefData().getLocals(), ptrFound); MWBase::Environment::get().getScriptManager()->run(script, interpreterContext); } } } MWBase::Environment::get().getWorld()->deleteObject(ptrFound); } } } void ObjectList::lockObjects(MWWorld::CellStore* cellStore) { for (const auto &baseObject : baseObjects) { LOG_APPEND(TimedLog::LOG_VERBOSE, "- cellRef: %s %i-%i", baseObject.refId.c_str(), baseObject.refNum, baseObject.mpNum); MWWorld::Ptr ptrFound = cellStore->searchExact(baseObject.refNum, baseObject.mpNum, baseObject.refId); if (ptrFound) { LOG_APPEND(TimedLog::LOG_VERBOSE, "-- Found %s %i-%i", ptrFound.getCellRef().getRefId().c_str(), ptrFound.getCellRef().getRefNum(), ptrFound.getCellRef().getMpNum()); if (baseObject.lockLevel > 0) ptrFound.getCellRef().lock(baseObject.lockLevel); else ptrFound.getCellRef().unlock(); } } } void ObjectList::triggerTrapObjects(MWWorld::CellStore* cellStore) { for (const auto &baseObject : baseObjects) { LOG_APPEND(TimedLog::LOG_VERBOSE, "- cellRef: %s %i-%i", baseObject.refId.c_str(), baseObject.refNum, baseObject.mpNum); MWWorld::Ptr ptrFound = cellStore->searchExact(baseObject.refNum, baseObject.mpNum, baseObject.refId); if (ptrFound) { LOG_APPEND(TimedLog::LOG_VERBOSE, "-- Found %s %i-%i", ptrFound.getCellRef().getRefId().c_str(), ptrFound.getCellRef().getRefNum(), ptrFound.getCellRef().getMpNum()); if (!baseObject.isDisarmed) { MWMechanics::CastSpell cast(ptrFound, ptrFound); cast.mHitPosition = baseObject.position.asVec3(); cast.cast(ptrFound.getCellRef().getTrap()); } ptrFound.getCellRef().setTrap(""); MWBase::Environment::get().getSoundManager()->playSound3D(ptrFound, "Disarm Trap", 1.0f, 1.0f); } } } void ObjectList::scaleObjects(MWWorld::CellStore* cellStore) { for (const auto &baseObject : baseObjects) { LOG_APPEND(TimedLog::LOG_VERBOSE, "- cellRef: %s %i-%i, scale: %f", baseObject.refId.c_str(), baseObject.refNum, baseObject.mpNum, baseObject.scale); MWWorld::Ptr ptrFound = cellStore->searchExact(baseObject.refNum, baseObject.mpNum, baseObject.refId); if (ptrFound) { LOG_APPEND(TimedLog::LOG_VERBOSE, "-- Found %s %i-%i", ptrFound.getCellRef().getRefId().c_str(), ptrFound.getCellRef().getRefNum(), ptrFound.getCellRef().getMpNum()); MWBase::Environment::get().getWorld()->scaleObject(ptrFound, baseObject.scale); } } } void ObjectList::setObjectStates(MWWorld::CellStore* cellStore) { for (const auto &baseObject : baseObjects) { LOG_APPEND(TimedLog::LOG_VERBOSE, "- cellRef: %s %i-%i, state: %s", baseObject.refId.c_str(), baseObject.refNum, baseObject.mpNum, baseObject.objectState ? "true" : "false"); MWWorld::Ptr ptrFound = cellStore->searchExact(baseObject.refNum, baseObject.mpNum, baseObject.refId); if (ptrFound) { LOG_APPEND(TimedLog::LOG_VERBOSE, "-- Found %s %i-%i", ptrFound.getCellRef().getRefId().c_str(), ptrFound.getCellRef().getRefNum(), ptrFound.getCellRef().getMpNum()); if (baseObject.objectState) { MWBase::Environment::get().getWorld()->enable(ptrFound); // Is this an actor in a cell where we're the authority? If so, initialize it as // a LocalActor if (ptrFound.getClass().isActor() && mwmp::Main::get().getCellController()->hasLocalAuthority(*cellStore->getCell())) { mwmp::Main::get().getCellController()->getCell(*cellStore->getCell())->initializeLocalActor(ptrFound); } } else MWBase::Environment::get().getWorld()->disable(ptrFound); } } } void ObjectList::moveObjects(MWWorld::CellStore* cellStore) { for (const auto &baseObject : baseObjects) { LOG_APPEND(TimedLog::LOG_VERBOSE, "- cellRef: %s %i-%i", baseObject.refId.c_str(), baseObject.refNum, baseObject.mpNum); MWWorld::Ptr ptrFound = cellStore->searchExact(baseObject.refNum, baseObject.mpNum, baseObject.refId); if (ptrFound) { LOG_APPEND(TimedLog::LOG_VERBOSE, "-- Found %s %i-%i", ptrFound.getCellRef().getRefId().c_str(), ptrFound.getCellRef().getRefNum(), ptrFound.getCellRef().getMpNum()); MWBase::Environment::get().getWorld()->moveObject(ptrFound, baseObject.position.pos[0], baseObject.position.pos[1], baseObject.position.pos[2]); } } } void ObjectList::restockObjects(MWWorld::CellStore* cellStore) { for (const auto &baseObject : baseObjects) { LOG_APPEND(TimedLog::LOG_VERBOSE, "- cellRef: %s %i-%i", baseObject.refId.c_str(), baseObject.refNum, baseObject.mpNum); MWWorld::Ptr ptrFound = cellStore->searchExact(baseObject.refNum, baseObject.mpNum, baseObject.refId); if (ptrFound) { LOG_APPEND(TimedLog::LOG_VERBOSE, "-- Found %s %i-%i", ptrFound.getCellRef().getRefId().c_str(), ptrFound.getCellRef().getRefNum(), ptrFound.getCellRef().getMpNum()); //ptrFound.getClass().restock(ptrFound); reset(); packetOrigin = mwmp::PACKET_ORIGIN::CLIENT_GAMEPLAY; cell = *ptrFound.getCell()->getCell(); action = mwmp::BaseObjectList::SET; containerSubAction = mwmp::BaseObjectList::RESTOCK_RESULT; addEntireContainer(ptrFound); sendContainer(); } } } void ObjectList::rotateObjects(MWWorld::CellStore* cellStore) { for (const auto &baseObject : baseObjects) { LOG_APPEND(TimedLog::LOG_VERBOSE, "- cellRef: %s %i-%i", baseObject.refId.c_str(), baseObject.refNum, baseObject.mpNum); MWWorld::Ptr ptrFound = cellStore->searchExact(baseObject.refNum, baseObject.mpNum, baseObject.refId); if (ptrFound) { LOG_APPEND(TimedLog::LOG_VERBOSE, "-- Found %s %i-%i", ptrFound.getCellRef().getRefId().c_str(), ptrFound.getCellRef().getRefNum(), ptrFound.getCellRef().getMpNum()); MWBase::Environment::get().getWorld()->rotateObject(ptrFound, baseObject.position.rot[0], baseObject.position.rot[1], baseObject.position.rot[2], MWBase::RotationFlag_none); } } } void ObjectList::animateObjects(MWWorld::CellStore* cellStore) { for (const auto &baseObject : baseObjects) { LOG_APPEND(TimedLog::LOG_VERBOSE, "- cellRef: %s %i-%i", baseObject.refId.c_str(), baseObject.refNum, baseObject.mpNum); MWWorld::Ptr ptrFound = cellStore->searchExact(baseObject.refNum, baseObject.mpNum, baseObject.refId); if (ptrFound) { LOG_APPEND(TimedLog::LOG_VERBOSE, "-- Found %s %i-%i", ptrFound.getCellRef().getRefId().c_str(), ptrFound.getCellRef().getRefNum(), ptrFound.getCellRef().getMpNum()); MWBase::MechanicsManager * mechanicsManager = MWBase::Environment::get().getMechanicsManager(); mechanicsManager->playAnimationGroup(ptrFound, baseObject.animGroup, baseObject.animMode, std::numeric_limits::max(), true); } } } void ObjectList::playObjectSounds(MWWorld::CellStore* cellStore) { for (const auto &baseObject : baseObjects) { MWWorld::Ptr ptrFound; std::string objectDescription; if (baseObject.isPlayer) { if (baseObject.guid == Main::get().getLocalPlayer()->guid) { objectDescription = "LocalPlayer " + Main::get().getLocalPlayer()->npc.mName; ptrFound = Main::get().getLocalPlayer()->getPlayerPtr(); } else { DedicatedPlayer *player = PlayerList::getPlayer(baseObject.guid); if (player != 0) { objectDescription = "DedicatedPlayer " + player->npc.mName; ptrFound = player->getPtr(); } } } else { objectDescription = baseObject.refId + " " + std::to_string(baseObject.refNum) + "-" + std::to_string(baseObject.mpNum); ptrFound = cellStore->searchExact(baseObject.refNum, baseObject.mpNum, baseObject.refId); } if (ptrFound) { LOG_APPEND(TimedLog::LOG_VERBOSE, "- Playing sound %s on %s", baseObject.soundId.c_str(), objectDescription.c_str()); bool playAtPosition = false; if (ptrFound.isInCell()) { ESM::CellId localCell = Main::get().getLocalPlayer()->cell.getCellId(); ESM::CellId soundCell = ptrFound.getCell()->getCell()->getCellId(); playAtPosition = localCell == soundCell; } if (playAtPosition) { MWBase::Environment::get().getSoundManager()->playSound3D(ptrFound.getRefData().getPosition().asVec3(), baseObject.soundId, baseObject.volume, baseObject.pitch, MWSound::Type::Sfx, MWSound::PlayMode::Normal, 0); } else { MWBase::Environment::get().getSoundManager()->playSound3D(ptrFound, baseObject.soundId, baseObject.volume, baseObject.pitch, MWSound::Type::Sfx, MWSound::PlayMode::Normal, 0); } } } } void ObjectList::setGoldPoolsForObjects(MWWorld::CellStore* cellStore) { for (const auto& baseObject : baseObjects) { LOG_APPEND(TimedLog::LOG_VERBOSE, "- cellRef: %s %i-%i", baseObject.refId.c_str(), baseObject.refNum, baseObject.mpNum); MWWorld::Ptr ptrFound = cellStore->searchExact(baseObject.refNum, baseObject.mpNum, baseObject.refId); if (ptrFound) { LOG_APPEND(TimedLog::LOG_VERBOSE, "-- Found %s %i-%i", ptrFound.getCellRef().getRefId().c_str(), ptrFound.getCellRef().getRefNum(), ptrFound.getCellRef().getMpNum()); if (ptrFound.getClass().isActor()) { LOG_APPEND(TimedLog::LOG_VERBOSE, "-- Setting gold pool to %u", baseObject.goldPool); ptrFound.getClass().getCreatureStats(ptrFound).setGoldPool(baseObject.goldPool); LOG_APPEND(TimedLog::LOG_VERBOSE, "-- Setting last gold restock time to %f hours and %i days passed", baseObject.lastGoldRestockHour, baseObject.lastGoldRestockDay); ptrFound.getClass().getCreatureStats(ptrFound).setLastRestockTime(MWWorld::TimeStamp(baseObject.lastGoldRestockHour, baseObject.lastGoldRestockDay)); } else { LOG_MESSAGE_SIMPLE(TimedLog::LOG_WARN, "Failed to set gold pool on %s %i-%i because it is not an actor!", ptrFound.getCellRef().getRefId().c_str(), ptrFound.getCellRef().getRefNum(), ptrFound.getCellRef().getMpNum()); } } } } void ObjectList::activateDoors(MWWorld::CellStore* cellStore) { for (const auto &baseObject : baseObjects) { LOG_APPEND(TimedLog::LOG_VERBOSE, "- cellRef: %s %i-%i", baseObject.refId.c_str(), baseObject.refNum, baseObject.mpNum); MWWorld::Ptr ptrFound = cellStore->searchExact(baseObject.refNum, baseObject.mpNum, baseObject.refId); if (ptrFound) { LOG_APPEND(TimedLog::LOG_VERBOSE, "-- Found %s %i-%i", ptrFound.getCellRef().getRefId().c_str(), ptrFound.getCellRef().getRefNum(), ptrFound.getCellRef().getMpNum()); MWWorld::DoorState doorState = static_cast(baseObject.doorState); ptrFound.getClass().setDoorState(ptrFound, doorState); MWBase::Environment::get().getWorld()->saveDoorState(ptrFound, doorState); } } } void ObjectList::setDoorDestinations(MWWorld::CellStore* cellStore) { for (const auto &baseObject : baseObjects) { LOG_APPEND(TimedLog::LOG_VERBOSE, "- cellRef: %s %i-%i", baseObject.refId.c_str(), baseObject.refNum, baseObject.mpNum); MWWorld::Ptr ptrFound = cellStore->searchExact(baseObject.refNum, baseObject.mpNum, baseObject.refId); if (ptrFound) { LOG_APPEND(TimedLog::LOG_VERBOSE, "-- Found %s %i-%i", ptrFound.getCellRef().getRefId().c_str(), ptrFound.getCellRef().getRefNum(), ptrFound.getCellRef().getMpNum()); ptrFound.getCellRef().setTeleport(baseObject.teleportState); if (baseObject.teleportState) { ptrFound.getCellRef().setDoorDest(baseObject.destinationPosition); if (baseObject.destinationCell.isExterior()) ptrFound.getCellRef().setDestCell(""); else ptrFound.getCellRef().setDestCell(baseObject.destinationCell.getShortDescription()); } } } } void ObjectList::runConsoleCommands(MWWorld::CellStore* cellStore) { MWBase::WindowManager *windowManager = MWBase::Environment::get().getWindowManager(); LOG_APPEND(TimedLog::LOG_VERBOSE, "- Console command: %s", consoleCommand.c_str()); if (baseObjects.empty()) { windowManager->clearConsolePtr(); LOG_APPEND(TimedLog::LOG_VERBOSE, "-- Running with no object reference"); windowManager->executeCommandInConsole(consoleCommand); } else { for (const auto &baseObject : baseObjects) { windowManager->clearConsolePtr(); if (baseObject.isPlayer) { if (baseObject.guid == Main::get().getLocalPlayer()->guid) { LOG_APPEND(TimedLog::LOG_VERBOSE, "-- Running on local player"); windowManager->setConsolePtr(Main::get().getLocalPlayer()->getPlayerPtr()); windowManager->executeCommandInConsole(consoleCommand); } else { DedicatedPlayer *player = PlayerList::getPlayer(baseObject.guid); if (player != 0) { LOG_APPEND(TimedLog::LOG_VERBOSE, "-- Running on player %s", player->npc.mName.c_str()); windowManager->setConsolePtr(player->getPtr()); windowManager->executeCommandInConsole(consoleCommand); } } } // Only require a valid cellStore if running on cell objects else if (cellStore) { LOG_APPEND(TimedLog::LOG_VERBOSE, "-- Running on object %s %i-%i", baseObject.refId.c_str(), baseObject.refNum, baseObject.mpNum); MWWorld::Ptr ptrFound = cellStore->searchExact(baseObject.refNum, baseObject.mpNum, baseObject.refId); if (ptrFound) { LOG_APPEND(TimedLog::LOG_VERBOSE, "-- Found %s %i-%i", ptrFound.getCellRef().getRefId().c_str(), ptrFound.getCellRef().getRefNum(), ptrFound.getCellRef().getMpNum()); windowManager->setConsolePtr(ptrFound); windowManager->executeCommandInConsole(consoleCommand); } } } windowManager->clearConsolePtr(); } } void ObjectList::makeDialogueChoices(MWWorld::CellStore* cellStore) { for (const auto& baseObject : baseObjects) { LOG_APPEND(TimedLog::LOG_VERBOSE, "- cellRef: %s %i-%i", baseObject.refId.c_str(), baseObject.refNum, baseObject.mpNum); MWWorld::Ptr ptrFound = cellStore->searchExact(baseObject.refNum, baseObject.mpNum, baseObject.refId); if (ptrFound) { LOG_APPEND(TimedLog::LOG_VERBOSE, "-- Found %s %i-%i", ptrFound.getCellRef().getRefId().c_str(), ptrFound.getCellRef().getRefNum(), ptrFound.getCellRef().getMpNum()); if (ptrFound.getClass().isActor()) { // Ensure the dialogue window has the correct Ptr set for it if (MWBase::Environment::get().getWindowManager()->containsMode(MWGui::GM_Dialogue)) { if (MWBase::Environment::get().getWindowManager()->getDialogueWindow()->getPtr() != ptrFound) { MWBase::Environment::get().getWindowManager()->getDialogueWindow()->setPtr(ptrFound); } } else { MWBase::Environment::get().getWindowManager()->pushGuiMode(MWGui::GM_Dialogue, ptrFound); } LOG_APPEND(TimedLog::LOG_VERBOSE, "-- Making dialogue choice of type %i", baseObject.dialogueChoiceType); if (baseObject.dialogueChoiceType == DialogueChoiceType::TOPIC) { LOG_APPEND(TimedLog::LOG_VERBOSE, "-- topic was %s", baseObject.topicId.c_str()); } std::string topic = baseObject.topicId; if (MWBase::Environment::get().getWindowManager()->getTranslationDataStorage().hasTranslation()) { char delimiter = '|'; // If we're using a translated version of Morrowind, we may have received a string that had the original // topic delimited from its possible English translation by a | character, in which case we need to use // the original topic here if (topic.find(delimiter) != std::string::npos) { topic = topic.substr(0, topic.find(delimiter)); } // Alternatively, we may have received a topic that needs to be translated into the current language's // version of it else { std::string translatedTopic = MWBase::Environment::get().getWindowManager()->getTranslationDataStorage().getLocalizedTopicId(topic); if (!translatedTopic.empty()) { topic = translatedTopic; } } } MWBase::Environment::get().getWindowManager()->getDialogueWindow()->activateDialogueChoice(baseObject.dialogueChoiceType, topic); } else { LOG_MESSAGE_SIMPLE(TimedLog::LOG_WARN, "Failed to make dialogue choice for %s %i-%i because it is not an actor!", ptrFound.getCellRef().getRefId().c_str(), ptrFound.getCellRef().getRefNum(), ptrFound.getCellRef().getMpNum()); } } } } void ObjectList::setClientLocals(MWWorld::CellStore* cellStore) { for (const auto &baseObject : baseObjects) { LOG_APPEND(TimedLog::LOG_VERBOSE, "- cellRef: %s %i-%i", baseObject.refId.c_str(), baseObject.refNum, baseObject.mpNum); MWWorld::Ptr ptrFound = cellStore->searchExact(baseObject.refNum, baseObject.mpNum, baseObject.refId); if (ptrFound) { LOG_APPEND(TimedLog::LOG_VERBOSE, "-- Found %s %i-%i", ptrFound.getCellRef().getRefId().c_str(), ptrFound.getCellRef().getRefNum(), ptrFound.getCellRef().getMpNum()); for (const auto& clientLocal : baseObject.clientLocals) { std::string valueAsString; std::string variableTypeAsString; if (clientLocal.variableType == mwmp::VARIABLE_TYPE::SHORT || clientLocal.variableType == mwmp::VARIABLE_TYPE::LONG) { variableTypeAsString = clientLocal.variableType == mwmp::VARIABLE_TYPE::SHORT ? "short" : "long"; valueAsString = std::to_string(clientLocal.intValue); } else if (clientLocal.variableType == mwmp::VARIABLE_TYPE::FLOAT) { variableTypeAsString = "float"; valueAsString = std::to_string(clientLocal.floatValue); } if (clientLocal.variableType == mwmp::VARIABLE_TYPE::SHORT) ptrFound.getRefData().getLocals().mShorts.at(clientLocal.internalIndex) = clientLocal.intValue; else if (clientLocal.variableType == mwmp::VARIABLE_TYPE::LONG) ptrFound.getRefData().getLocals().mLongs.at(clientLocal.internalIndex) = clientLocal.intValue; else if (clientLocal.variableType == mwmp::VARIABLE_TYPE::FLOAT) ptrFound.getRefData().getLocals().mFloats.at(clientLocal.internalIndex) = clientLocal.floatValue; } } } } void ObjectList::setMemberShorts() { /* for (const auto &baseObject : baseObjects) { LOG_APPEND(TimedLog::LOG_VERBOSE, "- cellRef: %s, index: %i, shortVal: %i", baseObject.refId.c_str(), baseObject.index, baseObject.shortVal); // Mimic the way a Ptr is fetched in InterpreterContext for similar situations MWWorld::Ptr ptrFound = MWBase::Environment::get().getWorld()->searchPtr(baseObject.refId, false); if (ptrFound) { LOG_APPEND(TimedLog::LOG_VERBOSE, "-- Found %s %i-%i", ptrFound.getCellRef().getRefId().c_str(), ptrFound.getCellRef().getRefNum(), ptrFound.getCellRef().getMpNum()); std::string scriptId = ptrFound.getClass().getScript(ptrFound); ptrFound.getRefData().setLocals( *MWBase::Environment::get().getWorld()->getStore().get().find(scriptId)); ptrFound.getRefData().getLocals().mShorts.at(baseObject.index) = baseObject.shortVal;; } } */ } void ObjectList::playMusic() { for (const auto &baseObject : baseObjects) { LOG_APPEND(TimedLog::LOG_VERBOSE, "- filename: %s", baseObject.musicFilename.c_str()); MWBase::Environment::get().getSoundManager()->streamMusic(baseObject.musicFilename); } } void ObjectList::playVideo() { for (const auto &baseObject : baseObjects) { LOG_APPEND(TimedLog::LOG_VERBOSE, "- filename: %s, allowSkipping: %s", baseObject.videoFilename.c_str(), baseObject.allowSkipping ? "true" : "false"); MWBase::Environment::get().getWindowManager()->playVideo(baseObject.videoFilename, baseObject.allowSkipping); } } void ObjectList::addAllContainers(MWWorld::CellStore* cellStore) { for (auto &ref : cellStore->getContainers()->mList) { MWWorld::Ptr ptr(&ref, 0); addEntireContainer(ptr); } for (auto &ref : cellStore->getNpcs()->mList) { MWWorld::Ptr ptr(&ref, 0); addEntireContainer(ptr); } for (auto &ref : cellStore->getCreatures()->mList) { MWWorld::Ptr ptr(&ref, 0); addEntireContainer(ptr); } } void ObjectList::addRequestedContainers(MWWorld::CellStore* cellStore, const std::vector& requestObjects) { for (const auto &baseObject : requestObjects) { LOG_APPEND(TimedLog::LOG_VERBOSE, "- cellRef: %s %i-%i", baseObject.refId.c_str(), baseObject.refNum, baseObject.mpNum); MWWorld::Ptr ptrFound = cellStore->searchExact(baseObject.refNum, baseObject.mpNum, baseObject.refId); if (ptrFound) { if (ptrFound.getClass().hasContainerStore(ptrFound)) addEntireContainer(ptrFound); else LOG_APPEND(TimedLog::LOG_VERBOSE, "-- Object lacks container store", ptrFound.getCellRef().getRefId().c_str()); } } } void ObjectList::addObjectGeneric(const MWWorld::Ptr& ptr) { cell = *ptr.getCell()->getCell(); mwmp::BaseObject baseObject = getBaseObjectFromPtr(ptr); addBaseObject(baseObject); } void ObjectList::addObjectActivate(const MWWorld::Ptr& ptr, const MWWorld::Ptr& activatingActor) { cell = *ptr.getCell()->getCell(); mwmp::BaseObject baseObject = getBaseObjectFromPtr(ptr); baseObject.activatingActor = MechanicsHelper::getTarget(activatingActor); addBaseObject(baseObject); } void ObjectList::addObjectHit(const MWWorld::Ptr& ptr, const MWWorld::Ptr& hittingActor) { cell = *ptr.getCell()->getCell(); mwmp::BaseObject baseObject = getBaseObjectFromPtr(ptr); baseObject.hittingActor = MechanicsHelper::getTarget(hittingActor); baseObject.hitAttack.success = false; addBaseObject(baseObject); } void ObjectList::addObjectHit(const MWWorld::Ptr& ptr, const MWWorld::Ptr& hittingActor, const Attack hitAttack) { cell = *ptr.getCell()->getCell(); mwmp::BaseObject baseObject = getBaseObjectFromPtr(ptr); baseObject.hittingActor = MechanicsHelper::getTarget(hittingActor); baseObject.hitAttack = hitAttack; addBaseObject(baseObject); } void ObjectList::addObjectPlace(const MWWorld::Ptr& ptr, bool droppedByPlayer) { if (ptr.getCellRef().getRefId().find("$dynamic") != std::string::npos) { MWBase::Environment::get().getWindowManager()->messageBox("You cannot place unsynchronized custom items in multiplayer."); return; } cell = *ptr.getCell()->getCell(); mwmp::BaseObject baseObject = getBaseObjectFromPtr(ptr); baseObject.charge = ptr.getCellRef().getCharge(); baseObject.enchantmentCharge = ptr.getCellRef().getEnchantmentCharge(); baseObject.soul = ptr.getCellRef().getSoul(); baseObject.droppedByPlayer = droppedByPlayer; baseObject.hasContainer = ptr.getClass().hasContainerStore(ptr); // Make sure we send the RefData position instead of the CellRef one, because that's what // we actually see on this client baseObject.position = ptr.getRefData().getPosition(); // We have to get the count from the dropped object because it gets changed // automatically for stacks of gold baseObject.count = ptr.getRefData().getCount(); // Get the real count of gold in a stack baseObject.goldValue = ptr.getCellRef().getGoldValue(); addBaseObject(baseObject); } void ObjectList::addObjectSpawn(const MWWorld::Ptr& ptr) { if (ptr.getCellRef().getRefId().find("$dynamic") != std::string::npos) { MWBase::Environment::get().getWindowManager()->messageBox("You're trying to spawn a custom object lacking a server-given refId, " "and those cannot be synchronized in multiplayer."); return; } cell = *ptr.getCell()->getCell(); mwmp::BaseObject baseObject = getBaseObjectFromPtr(ptr); baseObject.isSummon = false; baseObject.summonDuration = -1; // Make sure we send the RefData position instead of the CellRef one, because that's what // we actually see on this client baseObject.position = ptr.getRefData().getPosition(); addBaseObject(baseObject); } void ObjectList::addObjectSpawn(const MWWorld::Ptr& ptr, const MWWorld::Ptr& master, std::string spellId, int effectId, float duration) { cell = *ptr.getCell()->getCell(); mwmp::BaseObject baseObject = getBaseObjectFromPtr(ptr); baseObject.isSummon = true; baseObject.summonSpellId = spellId; baseObject.summonEffectId = effectId; baseObject.summonDuration = duration; baseObject.master = MechanicsHelper::getTarget(master); // Make sure we send the RefData position instead of the CellRef one, because that's what // we actually see on this client baseObject.position = ptr.getRefData().getPosition(); addBaseObject(baseObject); } void ObjectList::addObjectLock(const MWWorld::Ptr& ptr, int lockLevel) { cell = *ptr.getCell()->getCell(); mwmp::BaseObject baseObject = getBaseObjectFromPtr(ptr); baseObject.lockLevel = lockLevel; addBaseObject(baseObject); } void ObjectList::addObjectDialogueChoice(const MWWorld::Ptr& ptr, std::string dialogueChoice) { cell = *ptr.getCell()->getCell(); mwmp::BaseObject baseObject = getBaseObjectFromPtr(ptr); const MWWorld::Store& gmst = MWBase::Environment::get().getWorld()->getStore().get(); // Because the actual text for any of the special dialogue choices can vary according to the game language used, // set the type of dialogue choice by doing a lot of checks if (dialogueChoice == gmst.find("sPersuasion")->mValue.getString()) baseObject.dialogueChoiceType = static_cast(DialogueChoiceType::PERSUASION); else if (dialogueChoice == gmst.find("sCompanionShare")->mValue.getString()) baseObject.dialogueChoiceType = DialogueChoiceType::COMPANION_SHARE; else if (dialogueChoice == gmst.find("sBarter")->mValue.getString()) baseObject.dialogueChoiceType = DialogueChoiceType::BARTER; else if (dialogueChoice == gmst.find("sSpells")->mValue.getString()) baseObject.dialogueChoiceType = DialogueChoiceType::SPELLS; else if (dialogueChoice == gmst.find("sTravel")->mValue.getString()) baseObject.dialogueChoiceType = DialogueChoiceType::TRAVEL; else if (dialogueChoice == gmst.find("sSpellMakingMenuTitle")->mValue.getString()) baseObject.dialogueChoiceType = DialogueChoiceType::SPELLMAKING; else if (dialogueChoice == gmst.find("sEnchanting")->mValue.getString()) baseObject.dialogueChoiceType = DialogueChoiceType::ENCHANTING; else if (dialogueChoice == gmst.find("sServiceTrainingTitle")->mValue.getString()) baseObject.dialogueChoiceType = DialogueChoiceType::TRAINING; else if (dialogueChoice == gmst.find("sRepair")->mValue.getString()) baseObject.dialogueChoiceType = DialogueChoiceType::REPAIR; else { baseObject.dialogueChoiceType = DialogueChoiceType::TOPIC; // For translated versions of the game, make sure we translate the topic back into English first if (MWBase::Environment::get().getWindowManager()->getTranslationDataStorage().hasTranslation()) baseObject.topicId = dialogueChoice + "|" + MWBase::Environment::get().getWindowManager()->getTranslationDataStorage().topicID(dialogueChoice); else baseObject.topicId = dialogueChoice; } addBaseObject(baseObject); } void ObjectList::addObjectMiscellaneous(const MWWorld::Ptr& ptr, unsigned int goldPool, float lastGoldRestockHour, int lastGoldRestockDay) { cell = *ptr.getCell()->getCell(); mwmp::BaseObject baseObject = getBaseObjectFromPtr(ptr); baseObject.goldPool = goldPool; baseObject.lastGoldRestockHour = lastGoldRestockHour; baseObject.lastGoldRestockDay = lastGoldRestockDay; addBaseObject(baseObject); } void ObjectList::addObjectTrap(const MWWorld::Ptr& ptr, const ESM::Position& pos, bool isDisarmed) { cell = *ptr.getCell()->getCell(); mwmp::BaseObject baseObject = getBaseObjectFromPtr(ptr); baseObject.isDisarmed = isDisarmed; baseObject.position = pos; addBaseObject(baseObject); } void ObjectList::addObjectScale(const MWWorld::Ptr& ptr, float scale) { cell = *ptr.getCell()->getCell(); mwmp::BaseObject baseObject = getBaseObjectFromPtr(ptr); baseObject.scale = scale; addBaseObject(baseObject); } void ObjectList::addObjectSound(const MWWorld::Ptr& ptr, std::string soundId, float volume, float pitch) { cell = *ptr.getCell()->getCell(); mwmp::BaseObject baseObject = getBaseObjectFromPtr(ptr); baseObject.soundId = soundId; baseObject.volume = volume; baseObject.pitch = pitch; addBaseObject(baseObject); } void ObjectList::addObjectState(const MWWorld::Ptr& ptr, bool objectState) { cell = *ptr.getCell()->getCell(); mwmp::BaseObject baseObject = getBaseObjectFromPtr(ptr); baseObject.objectState = objectState; addBaseObject(baseObject); } void ObjectList::addObjectAnimPlay(const MWWorld::Ptr& ptr, std::string group, int mode) { cell = *ptr.getCell()->getCell(); mwmp::BaseObject baseObject = getBaseObjectFromPtr(ptr); baseObject.animGroup = group; baseObject.animMode = mode; addBaseObject(baseObject); } void ObjectList::addDoorState(const MWWorld::Ptr& ptr, MWWorld::DoorState state) { cell = *ptr.getCell()->getCell(); mwmp::BaseObject baseObject = getBaseObjectFromPtr(ptr); baseObject.doorState = static_cast(state); addBaseObject(baseObject); } void ObjectList::addMusicPlay(std::string filename) { mwmp::BaseObject baseObject; baseObject.musicFilename = filename; addBaseObject(baseObject); } void ObjectList::addVideoPlay(std::string filename, bool allowSkipping) { mwmp::BaseObject baseObject; baseObject.videoFilename = filename; baseObject.allowSkipping = allowSkipping; addBaseObject(baseObject); } void ObjectList::addClientScriptLocal(const MWWorld::Ptr& ptr, int internalIndex, int value, mwmp::VARIABLE_TYPE variableType) { cell = *ptr.getCell()->getCell(); mwmp::BaseObject baseObject = getBaseObjectFromPtr(ptr); ClientVariable clientLocal; clientLocal.internalIndex = internalIndex; clientLocal.variableType = variableType; clientLocal.intValue = value; baseObject.clientLocals.push_back(clientLocal); addBaseObject(baseObject); } void ObjectList::addClientScriptLocal(const MWWorld::Ptr& ptr, int internalIndex, float value) { cell = *ptr.getCell()->getCell(); mwmp::BaseObject baseObject = getBaseObjectFromPtr(ptr); ClientVariable clientLocal; clientLocal.internalIndex = internalIndex; clientLocal.variableType = mwmp::VARIABLE_TYPE::FLOAT; clientLocal.floatValue = value; baseObject.clientLocals.push_back(clientLocal); addBaseObject(baseObject); } void ObjectList::addScriptMemberShort(std::string refId, int index, int shortVal) { /* mwmp::BaseObject baseObject; baseObject.refId = refId; baseObject.index = index; baseObject.shortVal = shortVal; addBaseObject(baseObject); */ } void ObjectList::sendObjectActivate() { mwmp::Main::get().getNetworking()->getObjectPacket(ID_OBJECT_ACTIVATE)->setObjectList(this); mwmp::Main::get().getNetworking()->getObjectPacket(ID_OBJECT_ACTIVATE)->Send(); } void ObjectList::sendObjectHit() { mwmp::Main::get().getNetworking()->getObjectPacket(ID_OBJECT_HIT)->setObjectList(this); mwmp::Main::get().getNetworking()->getObjectPacket(ID_OBJECT_HIT)->Send(); } void ObjectList::sendObjectPlace() { if (baseObjects.size() == 0) return; LOG_MESSAGE_SIMPLE(TimedLog::LOG_VERBOSE, "Sending ID_OBJECT_PLACE about %s", cell.getShortDescription().c_str()); for (const auto &baseObject : baseObjects) LOG_APPEND(TimedLog::LOG_VERBOSE, "- cellRef: %s, count: %i", baseObject.refId.c_str(), baseObject.count); mwmp::Main::get().getNetworking()->getObjectPacket(ID_OBJECT_PLACE)->setObjectList(this); mwmp::Main::get().getNetworking()->getObjectPacket(ID_OBJECT_PLACE)->Send(); } void ObjectList::sendObjectSpawn() { if (baseObjects.size() == 0) return; LOG_MESSAGE_SIMPLE(TimedLog::LOG_VERBOSE, "Sending ID_OBJECT_SPAWN about %s", cell.getShortDescription().c_str()); for (const auto &baseObject : baseObjects) LOG_APPEND(TimedLog::LOG_VERBOSE, "- cellRef: %s-%i", baseObject.refId.c_str(), baseObject.refNum); mwmp::Main::get().getNetworking()->getObjectPacket(ID_OBJECT_SPAWN)->setObjectList(this); mwmp::Main::get().getNetworking()->getObjectPacket(ID_OBJECT_SPAWN)->Send(); } void ObjectList::sendObjectDelete() { mwmp::Main::get().getNetworking()->getObjectPacket(ID_OBJECT_DELETE)->setObjectList(this); mwmp::Main::get().getNetworking()->getObjectPacket(ID_OBJECT_DELETE)->Send(); } void ObjectList::sendObjectLock() { mwmp::Main::get().getNetworking()->getObjectPacket(ID_OBJECT_LOCK)->setObjectList(this); mwmp::Main::get().getNetworking()->getObjectPacket(ID_OBJECT_LOCK)->Send(); } void ObjectList::sendObjectDialogueChoice() { mwmp::Main::get().getNetworking()->getObjectPacket(ID_OBJECT_DIALOGUE_CHOICE)->setObjectList(this); mwmp::Main::get().getNetworking()->getObjectPacket(ID_OBJECT_DIALOGUE_CHOICE)->Send(); } void ObjectList::sendObjectMiscellaneous() { mwmp::Main::get().getNetworking()->getObjectPacket(ID_OBJECT_MISCELLANEOUS)->setObjectList(this); mwmp::Main::get().getNetworking()->getObjectPacket(ID_OBJECT_MISCELLANEOUS)->Send(); } void ObjectList::sendObjectRestock() { mwmp::Main::get().getNetworking()->getObjectPacket(ID_OBJECT_RESTOCK)->setObjectList(this); mwmp::Main::get().getNetworking()->getObjectPacket(ID_OBJECT_RESTOCK)->Send(); } void ObjectList::sendObjectSound() { mwmp::Main::get().getNetworking()->getObjectPacket(ID_OBJECT_SOUND)->setObjectList(this); mwmp::Main::get().getNetworking()->getObjectPacket(ID_OBJECT_SOUND)->Send(); } void ObjectList::sendObjectTrap() { mwmp::Main::get().getNetworking()->getObjectPacket(ID_OBJECT_TRAP)->setObjectList(this); mwmp::Main::get().getNetworking()->getObjectPacket(ID_OBJECT_TRAP)->Send(); } void ObjectList::sendObjectScale() { mwmp::Main::get().getNetworking()->getObjectPacket(ID_OBJECT_SCALE)->setObjectList(this); mwmp::Main::get().getNetworking()->getObjectPacket(ID_OBJECT_SCALE)->Send(); } void ObjectList::sendObjectState() { mwmp::Main::get().getNetworking()->getObjectPacket(ID_OBJECT_STATE)->setObjectList(this); mwmp::Main::get().getNetworking()->getObjectPacket(ID_OBJECT_STATE)->Send(); } void ObjectList::sendObjectAnimPlay() { mwmp::Main::get().getNetworking()->getObjectPacket(ID_OBJECT_ANIM_PLAY)->setObjectList(this); mwmp::Main::get().getNetworking()->getObjectPacket(ID_OBJECT_ANIM_PLAY)->Send(); } void ObjectList::sendDoorState() { LOG_MESSAGE_SIMPLE(TimedLog::LOG_VERBOSE, "Sending ID_DOOR_STATE about %s", cell.getShortDescription().c_str()); for (const auto &baseObject : baseObjects) LOG_APPEND(TimedLog::LOG_VERBOSE, "- cellRef: %s-%i, state: %s", baseObject.refId.c_str(), baseObject.refNum, baseObject.doorState ? "true" : "false"); mwmp::Main::get().getNetworking()->getObjectPacket(ID_DOOR_STATE)->setObjectList(this); mwmp::Main::get().getNetworking()->getObjectPacket(ID_DOOR_STATE)->Send(); } void ObjectList::sendMusicPlay() { mwmp::Main::get().getNetworking()->getObjectPacket(ID_MUSIC_PLAY)->setObjectList(this); mwmp::Main::get().getNetworking()->getObjectPacket(ID_MUSIC_PLAY)->Send(); } void ObjectList::sendVideoPlay() { mwmp::Main::get().getNetworking()->getObjectPacket(ID_VIDEO_PLAY)->setObjectList(this); mwmp::Main::get().getNetworking()->getObjectPacket(ID_VIDEO_PLAY)->Send(); } void ObjectList::sendClientScriptLocal() { LOG_MESSAGE_SIMPLE(TimedLog::LOG_VERBOSE, "Sending ID_CLIENT_SCRIPT_LOCAL about %s", cell.getShortDescription().c_str()); for (const auto &baseObject : baseObjects) { LOG_APPEND(TimedLog::LOG_VERBOSE, "- cellRef: %s %i-%i", baseObject.refId.c_str(), baseObject.refNum, baseObject.mpNum); for (const auto& clientLocal : baseObject.clientLocals) { std::string valueAsString; std::string variableTypeAsString; if (clientLocal.variableType == mwmp::VARIABLE_TYPE::SHORT || clientLocal.variableType == mwmp::VARIABLE_TYPE::LONG) { variableTypeAsString = clientLocal.variableType == mwmp::VARIABLE_TYPE::SHORT ? "short" : "long"; valueAsString = std::to_string(clientLocal.intValue); } else if (clientLocal.variableType == mwmp::VARIABLE_TYPE::FLOAT) { variableTypeAsString = "float"; valueAsString = std::to_string(clientLocal.floatValue); } LOG_APPEND(TimedLog::LOG_VERBOSE, "- type %s, value: %s", variableTypeAsString.c_str(), valueAsString.c_str()); } } mwmp::Main::get().getNetworking()->getObjectPacket(ID_CLIENT_SCRIPT_LOCAL)->setObjectList(this); mwmp::Main::get().getNetworking()->getObjectPacket(ID_CLIENT_SCRIPT_LOCAL)->Send(); } void ObjectList::sendScriptMemberShort() { /* LOG_MESSAGE_SIMPLE(TimedLog::LOG_VERBOSE, "Sending ID_SCRIPT_MEMBER_SHORT"); for (const auto &baseObject : baseObjects) LOG_APPEND(TimedLog::LOG_VERBOSE, "- cellRef: %s, index: %i, shortVal: %i", baseObject.refId.c_str(), baseObject.index, baseObject.shortVal); mwmp::Main::get().getNetworking()->getObjectPacket(ID_SCRIPT_MEMBER_SHORT)->setObjectList(this); mwmp::Main::get().getNetworking()->getObjectPacket(ID_SCRIPT_MEMBER_SHORT)->Send(); */ } void ObjectList::sendContainer() { std::string debugMessage = "Sending ID_CONTAINER with action "; if (action == mwmp::BaseObjectList::SET) debugMessage += "SET"; else if (action == mwmp::BaseObjectList::ADD) debugMessage += "ADD"; else if (action == mwmp::BaseObjectList::REMOVE) debugMessage += "REMOVE"; debugMessage += " and subaction "; if (containerSubAction == mwmp::BaseObjectList::NONE) debugMessage += "NONE"; else if (containerSubAction == mwmp::BaseObjectList::DRAG) debugMessage += "DRAG"; else if (containerSubAction == mwmp::BaseObjectList::DROP) debugMessage += "DROP"; else if (containerSubAction == mwmp::BaseObjectList::TAKE_ALL) debugMessage += "TAKE_ALL"; else if (containerSubAction == mwmp::BaseObjectList::REPLY_TO_REQUEST) debugMessage += "REPLY_TO_REQUEST"; debugMessage += "\n- cell " + cell.getShortDescription(); for (const auto &baseObject : baseObjects) { debugMessage += "\n- container " + baseObject.refId + " " + std::to_string(baseObject.refNum) + "-" + std::to_string(baseObject.mpNum); for (const auto &containerItem : baseObject.containerItems) { debugMessage += "\n-- item " + containerItem.refId + ", count " + std::to_string(containerItem.count) + ", actionCount " + std::to_string(containerItem.actionCount); } } LOG_MESSAGE_SIMPLE(TimedLog::LOG_VERBOSE, "%s", debugMessage.c_str()); mwmp::Main::get().getNetworking()->getObjectPacket(ID_CONTAINER)->setObjectList(this); mwmp::Main::get().getNetworking()->getObjectPacket(ID_CONTAINER)->Send(); } void ObjectList::sendConsoleCommand() { mwmp::Main::get().getNetworking()->getObjectPacket(ID_CONSOLE_COMMAND)->setObjectList(this); mwmp::Main::get().getNetworking()->getObjectPacket(ID_CONSOLE_COMMAND)->Send(); } ================================================ FILE: apps/openmw/mwmp/ObjectList.hpp ================================================ #ifndef OPENMW_OBJECTLIST_HPP #define OPENMW_OBJECTLIST_HPP #include #include "../mwgui/itemmodel.hpp" #include "../mwworld/worldimp.hpp" #include namespace mwmp { class Networking; class ObjectList : public BaseObjectList { public: ObjectList(); virtual ~ObjectList(); void reset(); void addBaseObject(BaseObject baseObject); mwmp::BaseObject getBaseObjectFromPtr(const MWWorld::Ptr& ptr); void addContainerItem(mwmp::BaseObject& baseObject, const MWWorld::Ptr& itemPtr, int itemCount, int actionCount); void addContainerItem(mwmp::BaseObject& baseObject, const MWGui::ItemStack& itemStack, int itemCount, int actionCount); void addContainerItem(mwmp::BaseObject& baseObject, const std::string itemId, int itemCount, int actionCount); void addEntireContainer(const MWWorld::Ptr& ptr); void editContainers(MWWorld::CellStore* cellStore); void activateObjects(MWWorld::CellStore* cellStore); void placeObjects(MWWorld::CellStore* cellStore); void spawnObjects(MWWorld::CellStore* cellStore); void deleteObjects(MWWorld::CellStore* cellStore); void lockObjects(MWWorld::CellStore* cellStore); void triggerTrapObjects(MWWorld::CellStore* cellStore); void scaleObjects(MWWorld::CellStore* cellStore); void setObjectStates(MWWorld::CellStore* cellStore); void moveObjects(MWWorld::CellStore* cellStore); void restockObjects(MWWorld::CellStore* cellStore); void rotateObjects(MWWorld::CellStore* cellStore); void animateObjects(MWWorld::CellStore* cellStore); void playObjectSounds(MWWorld::CellStore* cellStore); void setGoldPoolsForObjects(MWWorld::CellStore* cellStore); void activateDoors(MWWorld::CellStore* cellStore); void setDoorDestinations(MWWorld::CellStore* cellStore); void runConsoleCommands(MWWorld::CellStore* cellStore); void makeDialogueChoices(MWWorld::CellStore* cellStore); void setClientLocals(MWWorld::CellStore* cellStore); void setMemberShorts(); void playMusic(); void playVideo(); void addAllContainers(MWWorld::CellStore* cellStore); void addRequestedContainers(MWWorld::CellStore* cellStore, const std::vector& requestObjects); void addObjectGeneric(const MWWorld::Ptr& ptr); void addObjectActivate(const MWWorld::Ptr& ptr, const MWWorld::Ptr& activatingActor); void addObjectHit(const MWWorld::Ptr& ptr, const MWWorld::Ptr& hittingActor); void addObjectHit(const MWWorld::Ptr& ptr, const MWWorld::Ptr& hittingActor, const Attack hitAttack); void addObjectPlace(const MWWorld::Ptr& ptr, bool droppedByPlayer = false); void addObjectSpawn(const MWWorld::Ptr& ptr); void addObjectSpawn(const MWWorld::Ptr& ptr, const MWWorld::Ptr& master, std::string spellId, int effectId, float duration); void addObjectLock(const MWWorld::Ptr& ptr, int lockLevel); void addObjectDialogueChoice(const MWWorld::Ptr& ptr, std::string dialogueChoice); void addObjectMiscellaneous(const MWWorld::Ptr& ptr, unsigned int goldPool, float lastGoldRestockHour, int lastGoldRestockDay); void addObjectTrap(const MWWorld::Ptr& ptr, const ESM::Position& pos, bool isDisarmed); void addObjectScale(const MWWorld::Ptr& ptr, float scale); void addObjectSound(const MWWorld::Ptr& ptr, std::string soundId, float volume, float pitch); void addObjectState(const MWWorld::Ptr& ptr, bool objectState); void addObjectAnimPlay(const MWWorld::Ptr& ptr, std::string group, int mode); void addDoorState(const MWWorld::Ptr& ptr, MWWorld::DoorState state); void addMusicPlay(std::string filename); void addVideoPlay(std::string filename, bool allowSkipping); void addClientScriptLocal(const MWWorld::Ptr& ptr, int internalIndex, int value, mwmp::VARIABLE_TYPE variableType); void addClientScriptLocal(const MWWorld::Ptr& ptr, int internalIndex, float value); void addScriptMemberShort(std::string refId, int index, int shortVal); void sendObjectActivate(); void sendObjectHit(); void sendObjectPlace(); void sendObjectSpawn(); void sendObjectDelete(); void sendObjectLock(); void sendObjectDialogueChoice(); void sendObjectMiscellaneous(); void sendObjectRestock(); void sendObjectTrap(); void sendObjectScale(); void sendObjectSound(); void sendObjectState(); void sendObjectAnimPlay(); void sendDoorState(); void sendMusicPlay(); void sendVideoPlay(); void sendClientScriptLocal(); void sendScriptMemberShort(); void sendContainer(); void sendConsoleCommand(); private: Networking *getNetworking(); }; } #endif //OPENMW_OBJECTLIST_HPP ================================================ FILE: apps/openmw/mwmp/PlayerList.cpp ================================================ #include #include #include "../mwbase/environment.hpp" #include "../mwclass/npc.hpp" #include "../mwmechanics/creaturestats.hpp" #include "../mwworld/cellstore.hpp" #include "../mwworld/player.hpp" #include "../mwworld/worldimp.hpp" #include "PlayerList.hpp" #include "Main.hpp" #include "DedicatedPlayer.hpp" #include "CellController.hpp" #include "GUIController.hpp" using namespace mwmp; std::map PlayerList::playerList; void PlayerList::update(float dt) { for (auto &playerEntry : playerList) { DedicatedPlayer *player = playerEntry.second; if (player == nullptr) continue; player->update(dt); } } DedicatedPlayer *PlayerList::newPlayer(RakNet::RakNetGUID guid) { LOG_APPEND(TimedLog::LOG_INFO, "- Creating new DedicatedPlayer with guid %s", guid.ToString()); playerList[guid] = new DedicatedPlayer(guid); LOG_APPEND(TimedLog::LOG_INFO, "- There are now %i DedicatedPlayers", playerList.size()); return playerList[guid]; } void PlayerList::deletePlayer(RakNet::RakNetGUID guid) { if (playerList[guid]->reference) playerList[guid]->deleteReference(); delete playerList[guid]; playerList.erase(guid); } void PlayerList::cleanUp() { for (auto &playerEntry : playerList) delete playerEntry.second; } DedicatedPlayer *PlayerList::getPlayer(RakNet::RakNetGUID guid) { return playerList[guid]; } DedicatedPlayer *PlayerList::getPlayer(const MWWorld::Ptr &ptr) { for (auto &playerEntry : playerList) { if (playerEntry.second == nullptr || playerEntry.second->getPtr().mRef == nullptr) continue; std::string refId = ptr.getCellRef().getRefId(); if (playerEntry.second->getPtr().getCellRef().getRefId() == refId) return playerEntry.second; } return nullptr; } DedicatedPlayer* PlayerList::getPlayer(int actorId) { for (auto& playerEntry : playerList) { if (playerEntry.second == nullptr || playerEntry.second->getPtr().mRef == nullptr) continue; MWWorld::Ptr playerPtr = playerEntry.second->getPtr(); int playerActorId = playerPtr.getClass().getCreatureStats(playerPtr).getActorId(); if (actorId == playerActorId) return playerEntry.second; } return nullptr; } std::vector PlayerList::getPlayersInCell(const ESM::Cell& cell) { std::vector playersInCell; for (auto& playerEntry : playerList) { if (playerEntry.first != RakNet::UNASSIGNED_CRABNET_GUID) { if (Main::get().getCellController()->isSameCell(cell, playerEntry.second->cell)) { playersInCell.push_back(playerEntry.first); } } } return playersInCell; } bool PlayerList::isDedicatedPlayer(const MWWorld::Ptr &ptr) { if (ptr.mRef == nullptr) return false; // Players always have 0 as their refNum and mpNum if (ptr.getCellRef().getRefNum().mIndex != 0 || ptr.getCellRef().getMpNum() != 0) return false; return (getPlayer(ptr) != nullptr); } void PlayerList::enableMarkers(const ESM::Cell& cell) { for (auto &playerEntry : playerList) { if (playerEntry.second == nullptr || playerEntry.second->getPtr().mRef == nullptr) continue; if (Main::get().getCellController()->isSameCell(cell, playerEntry.second->cell)) { playerEntry.second->enableMarker(); } } } /* Go through all DedicatedPlayers checking if their mHitAttemptActorId matches this one and set it to -1 if it does This resets the combat target for a DedicatedPlayer's followers in Actors::update() */ void PlayerList::clearHitAttemptActorId(int actorId) { for (auto &playerEntry : playerList) { if (playerEntry.second == nullptr || playerEntry.second->getPtr().mRef == nullptr) continue; MWMechanics::CreatureStats &playerCreatureStats = playerEntry.second->getPtr().getClass().getCreatureStats(playerEntry.second->getPtr()); if (playerCreatureStats.getHitAttemptActorId() == actorId) playerCreatureStats.setHitAttemptActorId(-1); } } ================================================ FILE: apps/openmw/mwmp/PlayerList.hpp ================================================ #ifndef OPENMW_PLAYERLIST_HPP #define OPENMW_PLAYERLIST_HPP #include #include #include #include "../mwmechanics/aisequence.hpp" #include "../mwworld/manualref.hpp" #include "DedicatedPlayer.hpp" #include #include namespace MWMechanics { class Actor; } namespace mwmp { class PlayerList { public: static void update(float dt); static DedicatedPlayer *newPlayer(RakNet::RakNetGUID guid); static void deletePlayer(RakNet::RakNetGUID guid); static void cleanUp(); static DedicatedPlayer *getPlayer(RakNet::RakNetGUID guid); static DedicatedPlayer *getPlayer(const MWWorld::Ptr &ptr); static DedicatedPlayer* getPlayer(int actorId); static std::vector getPlayersInCell(const ESM::Cell& cell); static bool isDedicatedPlayer(const MWWorld::Ptr &ptr); static void enableMarkers(const ESM::Cell& cell); static void clearHitAttemptActorId(int actorId); private: static std::map playerList; }; } #endif //OPENMW_PLAYERLIST_HPP ================================================ FILE: apps/openmw/mwmp/RecordHelper.cpp ================================================ #include #include "../mwworld/cellstore.hpp" #include "../mwworld/worldimp.hpp" #include "RecordHelper.hpp" #include "Main.hpp" #include "CellController.hpp" #include "Cell.hpp" void RecordHelper::overrideRecord(const mwmp::ActivatorRecord& record) { const ESM::Activator &recordData = record.data; if (recordData.mId.empty()) { LOG_APPEND(TimedLog::LOG_INFO, "-- Ignoring record override with no id provided"); return; } bool isExistingId = doesRecordIdExist(recordData.mId); MWBase::World *world = MWBase::Environment::get().getWorld(); if (record.baseId.empty()) { world->getModifiableStore().overrideRecord(recordData); } else if (doesRecordIdExist(record.baseId)) { const ESM::Activator *baseData = world->getStore().get().search(record.baseId); ESM::Activator finalData = *baseData; finalData.mId = recordData.mId; if (record.baseOverrides.hasName) finalData.mName = recordData.mName; if (record.baseOverrides.hasModel) finalData.mModel = recordData.mModel; if (record.baseOverrides.hasScript) finalData.mScript = recordData.mScript; world->getModifiableStore().overrideRecord(finalData); } else { LOG_APPEND(TimedLog::LOG_INFO, "-- Ignoring record override with invalid baseId %s", record.baseId.c_str()); return; } if (isExistingId) world->updatePtrsWithRefId(recordData.mId); } void RecordHelper::overrideRecord(const mwmp::ApparatusRecord& record) { const ESM::Apparatus &recordData = record.data; if (recordData.mId.empty()) { LOG_APPEND(TimedLog::LOG_INFO, "-- Ignoring record override with no id provided"); return; } bool isExistingId = doesRecordIdExist(recordData.mId); MWBase::World *world = MWBase::Environment::get().getWorld(); if (record.baseId.empty()) { world->getModifiableStore().overrideRecord(recordData); } else if (doesRecordIdExist(record.baseId)) { const ESM::Apparatus *baseData = world->getStore().get().search(record.baseId); ESM::Apparatus finalData = *baseData; finalData.mId = recordData.mId; if (record.baseOverrides.hasName) finalData.mName = recordData.mName; if (record.baseOverrides.hasModel) finalData.mModel = recordData.mModel; if (record.baseOverrides.hasIcon) finalData.mIcon = recordData.mIcon; if (record.baseOverrides.hasSubtype) finalData.mData.mType = recordData.mData.mType; if (record.baseOverrides.hasWeight) finalData.mData.mWeight = recordData.mData.mWeight; if (record.baseOverrides.hasValue) finalData.mData.mValue = recordData.mData.mValue; if (record.baseOverrides.hasQuality) finalData.mData.mQuality = recordData.mData.mQuality; if (record.baseOverrides.hasScript) finalData.mScript = recordData.mScript; world->getModifiableStore().overrideRecord(finalData); } else { LOG_APPEND(TimedLog::LOG_INFO, "-- Ignoring record override with invalid baseId %s", record.baseId.c_str()); return; } if (isExistingId) world->updatePtrsWithRefId(recordData.mId); } void RecordHelper::overrideRecord(const mwmp::ArmorRecord& record) { const ESM::Armor &recordData = record.data; if (recordData.mId.empty()) { LOG_APPEND(TimedLog::LOG_INFO, "-- Ignoring record override with no id provided"); return; } bool isExistingId = doesRecordIdExist(recordData.mId); MWBase::World *world = MWBase::Environment::get().getWorld(); if (record.baseId.empty()) { if (!recordData.mEnchant.empty() && !doesRecordIdExist(recordData.mEnchant)) { LOG_APPEND(TimedLog::LOG_INFO, "-- Ignoring new armor record with invalid enchantmentId %s", recordData.mEnchant.c_str()); return; } else world->getModifiableStore().overrideRecord(recordData); } else if (doesRecordIdExist(record.baseId)) { const ESM::Armor *baseData = world->getStore().get().search(record.baseId); ESM::Armor finalData = *baseData; finalData.mId = recordData.mId; finalData.mParts.mParts.at(0); if (record.baseOverrides.hasName) finalData.mName = recordData.mName; if (record.baseOverrides.hasModel) finalData.mModel = recordData.mModel; if (record.baseOverrides.hasIcon) finalData.mIcon = recordData.mIcon; if (record.baseOverrides.hasSubtype) finalData.mData.mType = recordData.mData.mType; if (record.baseOverrides.hasWeight) finalData.mData.mWeight = recordData.mData.mWeight; if (record.baseOverrides.hasValue) finalData.mData.mValue = recordData.mData.mValue; if (record.baseOverrides.hasHealth) finalData.mData.mHealth = recordData.mData.mHealth; if (record.baseOverrides.hasArmorRating) finalData.mData.mArmor = recordData.mData.mArmor; if (record.baseOverrides.hasEnchantmentId) { if (recordData.mEnchant.empty() || doesRecordIdExist(recordData.mEnchant)) finalData.mEnchant = recordData.mEnchant; else LOG_APPEND(TimedLog::LOG_INFO, "-- Ignoring invalid enchantmentId %s", recordData.mEnchant.c_str()); } if (record.baseOverrides.hasEnchantmentCharge) finalData.mData.mEnchant = recordData.mData.mEnchant; if (record.baseOverrides.hasScript) finalData.mScript = recordData.mScript; if (record.baseOverrides.hasBodyParts) finalData.mParts.mParts = recordData.mParts.mParts; world->getModifiableStore().overrideRecord(finalData); } else { LOG_APPEND(TimedLog::LOG_INFO, "-- Ignoring record override with invalid baseId %s", record.baseId.c_str()); return; } if (isExistingId) world->updatePtrsWithRefId(recordData.mId); } void RecordHelper::overrideRecord(const mwmp::BodyPartRecord& record) { const ESM::BodyPart &recordData = record.data; if (recordData.mId.empty()) { LOG_APPEND(TimedLog::LOG_INFO, "-- Ignoring record override with no id provided"); return; } MWBase::World *world = MWBase::Environment::get().getWorld(); if (record.baseId.empty()) { world->getModifiableStore().overrideRecord(recordData); } else if (doesRecordIdExist(record.baseId)) { const ESM::BodyPart *baseData = world->getStore().get().search(record.baseId); ESM::BodyPart finalData = *baseData; finalData.mId = recordData.mId; if (record.baseOverrides.hasModel) finalData.mModel = recordData.mModel; if (record.baseOverrides.hasRace) finalData.mRace = recordData.mRace; if (record.baseOverrides.hasSubtype) finalData.mData.mType = recordData.mData.mType; if (record.baseOverrides.hasBodyPartType) finalData.mData.mPart = recordData.mData.mPart; if (record.baseOverrides.hasVampireState) finalData.mData.mVampire = recordData.mData.mVampire; if (record.baseOverrides.hasFlags) finalData.mData.mFlags = recordData.mData.mFlags; world->getModifiableStore().overrideRecord(finalData); } else { LOG_APPEND(TimedLog::LOG_INFO, "-- Ignoring record override with invalid baseId %s", record.baseId.c_str()); return; } } void RecordHelper::overrideRecord(const mwmp::BookRecord& record) { const ESM::Book &recordData = record.data; if (recordData.mId.empty()) { LOG_APPEND(TimedLog::LOG_INFO, "-- Ignoring record override with no id provided"); return; } bool isExistingId = doesRecordIdExist(recordData.mId); MWBase::World *world = MWBase::Environment::get().getWorld(); if (record.baseId.empty()) { if (!recordData.mEnchant.empty() && !doesRecordIdExist(recordData.mEnchant)) { LOG_APPEND(TimedLog::LOG_INFO, "-- Ignoring new book record with invalid enchantmentId %s", recordData.mEnchant.c_str()); return; } else world->getModifiableStore().overrideRecord(recordData); } else if (doesRecordIdExist(record.baseId)) { const ESM::Book *baseData = world->getStore().get().search(record.baseId); ESM::Book finalData = *baseData; finalData.mId = recordData.mId; if (record.baseOverrides.hasName) finalData.mName = recordData.mName; if (record.baseOverrides.hasModel) finalData.mModel = recordData.mModel; if (record.baseOverrides.hasIcon) finalData.mIcon = recordData.mIcon; if (record.baseOverrides.hasText) finalData.mText = recordData.mText; if (record.baseOverrides.hasWeight) finalData.mData.mWeight = recordData.mData.mWeight; if (record.baseOverrides.hasValue) finalData.mData.mValue = recordData.mData.mValue; if (record.baseOverrides.hasScrollState) finalData.mData.mIsScroll = recordData.mData.mIsScroll; if (record.baseOverrides.hasSkillId) finalData.mData.mSkillId = recordData.mData.mSkillId; if (record.baseOverrides.hasEnchantmentId) { if (recordData.mEnchant.empty() || doesRecordIdExist(recordData.mEnchant)) finalData.mEnchant = recordData.mEnchant; else LOG_APPEND(TimedLog::LOG_INFO, "-- Ignoring invalid enchantmentId %s", recordData.mEnchant.c_str()); } if (record.baseOverrides.hasEnchantmentCharge) finalData.mData.mEnchant = recordData.mData.mEnchant; if (record.baseOverrides.hasScript) finalData.mScript = recordData.mScript; world->getModifiableStore().overrideRecord(finalData); } else { LOG_APPEND(TimedLog::LOG_INFO, "-- Ignoring record override with invalid baseId %s", record.baseId.c_str()); return; } if (isExistingId) world->updatePtrsWithRefId(recordData.mId); } void RecordHelper::overrideRecord(const mwmp::CellRecord& record) { MWBase::World *world = MWBase::Environment::get().getWorld(); ESM::Cell recordData = record.data; if (recordData.mName.empty()) { LOG_APPEND(TimedLog::LOG_INFO, "-- Ignoring record override with no id provided"); return; } MWWorld::Ptr playerPtr = world->getPlayerPtr(); ESM::Cell playerCell = *playerPtr.getCell()->getCell(); ESM::Position playerPos = playerPtr.getRefData().getPosition(); bool isActiveCell = world->isCellActive(recordData); if (isActiveCell) { mwmp::Main::get().getCellController()->uninitializeCell(recordData); // Change to temporary holding interior cell world->changeToInteriorCell(RecordHelper::getPlaceholderInteriorCellName(), playerPos, true, true); } if (record.baseId.empty()) { recordData.mData.mFlags |= ESM::Cell::Flags::Interior; recordData.mCellId.mWorldspace = Misc::StringUtils::lowerCase(recordData.mName); world->unloadCell(recordData); world->clearCellStore(recordData); world->getModifiableStore().overrideRecord(recordData); } else if (doesRecordIdExist(record.baseId)) { const ESM::Cell *baseData = world->getStore().get().search(record.baseId); ESM::Cell finalData = *baseData; finalData.mName = recordData.mName; finalData.mCellId.mWorldspace = Misc::StringUtils::lowerCase(recordData.mName); world->unloadCell(finalData); world->clearCellStore(finalData); world->getModifiableStore().overrideRecord(finalData); // Create a Pathgrid record for this new Cell based on the base Cell's Pathgrid // Note: This has to be done after the new Cell has been created so the Pathgrid override // can correctly determine whether the Cell is an interior or an exterior const ESM::Pathgrid* basePathgrid = world->getStore().get().search(record.baseId); if (basePathgrid) { ESM::Pathgrid finalPathgrid = *basePathgrid; finalPathgrid.mCell = recordData.mName; world->getModifiableStore().overrideRecord(finalPathgrid); } } else { LOG_APPEND(TimedLog::LOG_INFO, "-- Ignoring record override with invalid baseId %s", record.baseId.c_str()); return; } // Move the player back to the cell they were in if (isActiveCell) { if (playerCell.isExterior()) world->changeToExteriorCell(playerPos, true, true); else world->changeToInteriorCell(playerCell.mName, playerPos, true, true); } } void RecordHelper::overrideRecord(const mwmp::ClothingRecord& record) { const ESM::Clothing &recordData = record.data; if (recordData.mId.empty()) { LOG_APPEND(TimedLog::LOG_INFO, "-- Ignoring record override with no id provided"); return; } bool isExistingId = doesRecordIdExist(recordData.mId); MWBase::World *world = MWBase::Environment::get().getWorld(); if (record.baseId.empty()) { if (!recordData.mEnchant.empty() && !doesRecordIdExist(recordData.mEnchant)) { LOG_APPEND(TimedLog::LOG_INFO, "-- Ignoring new clothing record with invalid enchantmentId %s", recordData.mEnchant.c_str()); return; } else world->getModifiableStore().overrideRecord(recordData); } else if (doesRecordIdExist(record.baseId)) { const ESM::Clothing *baseData = world->getStore().get().search(record.baseId); ESM::Clothing finalData = *baseData; finalData.mId = recordData.mId; if (record.baseOverrides.hasName) finalData.mName = recordData.mName; if (record.baseOverrides.hasModel) finalData.mModel = recordData.mModel; if (record.baseOverrides.hasIcon) finalData.mIcon = recordData.mIcon; if (record.baseOverrides.hasSubtype) finalData.mData.mType = recordData.mData.mType; if (record.baseOverrides.hasWeight) finalData.mData.mWeight = recordData.mData.mWeight; if (record.baseOverrides.hasValue) finalData.mData.mValue = recordData.mData.mValue; if (record.baseOverrides.hasEnchantmentId) { if (recordData.mEnchant.empty() || doesRecordIdExist(recordData.mEnchant)) finalData.mEnchant = recordData.mEnchant; else LOG_APPEND(TimedLog::LOG_INFO, "-- Ignoring invalid enchantmentId %s", recordData.mEnchant.c_str()); } if (record.baseOverrides.hasEnchantmentCharge) finalData.mData.mEnchant = recordData.mData.mEnchant; if (record.baseOverrides.hasScript) finalData.mScript = recordData.mScript; if (record.baseOverrides.hasBodyParts) finalData.mParts.mParts = recordData.mParts.mParts; world->getModifiableStore().overrideRecord(finalData); } else { LOG_APPEND(TimedLog::LOG_INFO, "-- Ignoring record override with invalid baseId %s", record.baseId.c_str()); return; } if (isExistingId) world->updatePtrsWithRefId(recordData.mId); } void RecordHelper::overrideRecord(const mwmp::ContainerRecord& record) { const ESM::Container &recordData = record.data; if (recordData.mId.empty()) { LOG_APPEND(TimedLog::LOG_INFO, "-- Ignoring record override with no id provided"); return; } bool isExistingId = doesRecordIdExist(recordData.mId); MWBase::World *world = MWBase::Environment::get().getWorld(); if (record.baseId.empty()) { world->getModifiableStore().overrideRecord(recordData); } else if (doesRecordIdExist(record.baseId)) { const ESM::Container *baseData = world->getStore().get().search(record.baseId); ESM::Container finalData = *baseData; finalData.mId = recordData.mId; if (record.baseOverrides.hasName) finalData.mName = recordData.mName; if (record.baseOverrides.hasModel) finalData.mModel = recordData.mModel; if (record.baseOverrides.hasWeight) finalData.mWeight = recordData.mWeight; if (record.baseOverrides.hasFlags) finalData.mFlags = recordData.mFlags; if (record.baseOverrides.hasScript) finalData.mScript = recordData.mScript; if (record.baseOverrides.hasInventory) finalData.mInventory.mList = recordData.mInventory.mList; world->getModifiableStore().overrideRecord(finalData); } else { LOG_APPEND(TimedLog::LOG_INFO, "-- Ignoring record override with invalid baseId %s", record.baseId.c_str()); return; } if (isExistingId) world->updatePtrsWithRefId(recordData.mId); } void RecordHelper::overrideRecord(const mwmp::CreatureRecord& record) { const ESM::Creature &recordData = record.data; if (recordData.mId.empty()) { LOG_APPEND(TimedLog::LOG_INFO, "-- Ignoring record override with no id provided"); return; } bool isExistingId = doesRecordIdExist(recordData.mId); MWBase::World *world = MWBase::Environment::get().getWorld(); if (record.baseId.empty()) { world->getModifiableStore().overrideRecord(recordData); } else if (doesRecordIdExist(record.baseId)) { const ESM::Creature *baseData = world->getStore().get().search(record.baseId); ESM::Creature finalData = *baseData; finalData.mId = recordData.mId; if (record.baseOverrides.hasName) finalData.mName = recordData.mName; if (record.baseOverrides.hasModel) finalData.mModel = recordData.mModel; if (record.baseOverrides.hasScale) finalData.mScale = recordData.mScale; if (record.baseOverrides.hasBloodType) finalData.mBloodType = recordData.mBloodType; if (record.baseOverrides.hasSubtype) finalData.mData.mType = recordData.mData.mType; if (record.baseOverrides.hasLevel) finalData.mData.mLevel = recordData.mData.mLevel; if (record.baseOverrides.hasHealth) finalData.mData.mHealth = recordData.mData.mHealth; if (record.baseOverrides.hasMagicka) finalData.mData.mMana = recordData.mData.mMana; if (record.baseOverrides.hasFatigue) finalData.mData.mFatigue = recordData.mData.mFatigue; if (record.baseOverrides.hasSoulValue) finalData.mData.mSoul = recordData.mData.mSoul; if (record.baseOverrides.hasDamageChop) { finalData.mData.mAttack[0] = recordData.mData.mAttack[0]; finalData.mData.mAttack[1] = recordData.mData.mAttack[1]; } if (record.baseOverrides.hasDamageSlash) { finalData.mData.mAttack[2] = recordData.mData.mAttack[2]; finalData.mData.mAttack[3] = recordData.mData.mAttack[3]; } if (record.baseOverrides.hasDamageThrust) { finalData.mData.mAttack[4] = recordData.mData.mAttack[4]; finalData.mData.mAttack[5] = recordData.mData.mAttack[5]; } if (record.baseOverrides.hasAiFight) finalData.mAiData.mFight = recordData.mAiData.mFight; if (record.baseOverrides.hasAiFlee) finalData.mAiData.mFlee = recordData.mAiData.mFlee; if (record.baseOverrides.hasAiAlarm) finalData.mAiData.mAlarm = recordData.mAiData.mAlarm; if (record.baseOverrides.hasAiServices) finalData.mAiData.mServices = recordData.mAiData.mServices; if (record.baseOverrides.hasFlags) finalData.mFlags = recordData.mFlags; if (record.baseOverrides.hasScript) finalData.mScript = recordData.mScript; if (!record.inventoryBaseId.empty() && doesRecordIdExist(record.inventoryBaseId)) finalData.mInventory.mList = world->getStore().get().search(record.inventoryBaseId)->mInventory.mList; else if (record.baseOverrides.hasInventory) finalData.mInventory.mList = recordData.mInventory.mList; world->getModifiableStore().overrideRecord(finalData); } else { LOG_APPEND(TimedLog::LOG_INFO, "-- Ignoring record override with invalid baseId %s", record.baseId.c_str()); return; } if (isExistingId) world->updatePtrsWithRefId(recordData.mId); } void RecordHelper::overrideRecord(const mwmp::DoorRecord& record) { const ESM::Door &recordData = record.data; if (recordData.mId.empty()) { LOG_APPEND(TimedLog::LOG_INFO, "-- Ignoring record override with no id provided"); return; } bool isExistingId = doesRecordIdExist(recordData.mId); MWBase::World *world = MWBase::Environment::get().getWorld(); if (record.baseId.empty()) { world->getModifiableStore().overrideRecord(recordData); } else if (doesRecordIdExist(record.baseId)) { const ESM::Door *baseData = world->getStore().get().search(record.baseId); ESM::Door finalData = *baseData; finalData.mId = recordData.mId; if (record.baseOverrides.hasName) finalData.mName = recordData.mName; if (record.baseOverrides.hasModel) finalData.mModel = recordData.mModel; if (record.baseOverrides.hasOpenSound) finalData.mOpenSound = recordData.mOpenSound; if (record.baseOverrides.hasCloseSound) finalData.mCloseSound = recordData.mCloseSound; if (record.baseOverrides.hasScript) finalData.mScript = recordData.mScript; world->getModifiableStore().overrideRecord(finalData); } else { LOG_APPEND(TimedLog::LOG_INFO, "-- Ignoring record override with invalid baseId %s", record.baseId.c_str()); return; } if (isExistingId) world->updatePtrsWithRefId(recordData.mId); } void RecordHelper::overrideRecord(const mwmp::EnchantmentRecord& record) { const ESM::Enchantment &recordData = record.data; if (recordData.mId.empty()) { LOG_APPEND(TimedLog::LOG_INFO, "-- Ignoring record override with no id provided"); return; } MWBase::World *world = MWBase::Environment::get().getWorld(); if (record.baseId.empty()) { if (recordData.mEffects.mList.empty()) { LOG_APPEND(TimedLog::LOG_INFO, "-- Ignoring new enchantment record with no effects"); return; } else world->getModifiableStore().overrideRecord(recordData); } else if (doesRecordIdExist(record.baseId)) { const ESM::Enchantment *baseData = world->getStore().get().search(record.baseId); ESM::Enchantment finalData = *baseData; finalData.mId = recordData.mId; if (record.baseOverrides.hasSubtype) finalData.mData.mType = recordData.mData.mType; if (record.baseOverrides.hasCost) finalData.mData.mCost = recordData.mData.mCost; if (record.baseOverrides.hasCharge) finalData.mData.mCharge = recordData.mData.mCharge; if (record.baseOverrides.hasFlags) finalData.mData.mFlags = recordData.mData.mFlags; if (record.baseOverrides.hasEffects) finalData.mEffects.mList = recordData.mEffects.mList; world->getModifiableStore().overrideRecord(finalData); } else { LOG_APPEND(TimedLog::LOG_INFO, "-- Ignoring record override with invalid baseId %s", record.baseId.c_str()); return; } } void RecordHelper::overrideRecord(const mwmp::GameSettingRecord& record) { const ESM::GameSetting& recordData = record.data; if (recordData.mId.empty()) { LOG_APPEND(TimedLog::LOG_INFO, "-- Ignoring record override with no id provided"); return; } MWBase::World* world = MWBase::Environment::get().getWorld(); if (record.baseId.empty()) { world->getModifiableStore().overrideRecord(recordData); } else if (doesRecordIdExist(record.baseId)) { const ESM::GameSetting* baseData = world->getStore().get().search(record.baseId); ESM::GameSetting finalData = *baseData; finalData.mId = recordData.mId; world->getModifiableStore().overrideRecord(finalData); } else { LOG_APPEND(TimedLog::LOG_INFO, "-- Ignoring record override with invalid baseId %s", record.baseId.c_str()); return; } } void RecordHelper::overrideRecord(const mwmp::IngredientRecord& record) { const ESM::Ingredient &recordData = record.data; if (recordData.mId.empty()) { LOG_APPEND(TimedLog::LOG_INFO, "-- Ignoring record override with no id provided"); return; } bool isExistingId = doesRecordIdExist(recordData.mId); MWBase::World *world = MWBase::Environment::get().getWorld(); if (record.baseId.empty()) { world->getModifiableStore().overrideRecord(recordData); } else if (doesRecordIdExist(record.baseId)) { const ESM::Ingredient *baseData = world->getStore().get().search(record.baseId); ESM::Ingredient finalData = *baseData; finalData.mId = recordData.mId; if (record.baseOverrides.hasName) finalData.mName = recordData.mName; if (record.baseOverrides.hasModel) finalData.mModel = recordData.mModel; if (record.baseOverrides.hasIcon) finalData.mIcon = recordData.mIcon; if (record.baseOverrides.hasWeight) finalData.mData.mWeight = recordData.mData.mWeight; if (record.baseOverrides.hasValue) finalData.mData.mValue = recordData.mData.mValue; if (record.baseOverrides.hasScript) finalData.mScript = recordData.mScript; if (record.baseOverrides.hasEffects) { const static unsigned int effectCap = sizeof(recordData.mData.mEffectID) / sizeof(recordData.mData.mEffectID[0]); for (int effectIndex = 0; effectIndex < effectCap; effectIndex++) { finalData.mData.mEffectID[effectIndex] = recordData.mData.mEffectID[effectIndex]; finalData.mData.mAttributes[effectIndex] = recordData.mData.mAttributes[effectIndex]; finalData.mData.mSkills[effectIndex] = recordData.mData.mSkills[effectIndex]; } } world->getModifiableStore().overrideRecord(finalData); } else { LOG_APPEND(TimedLog::LOG_INFO, "-- Ignoring record override with invalid baseId %s", record.baseId.c_str()); return; } if (isExistingId) world->updatePtrsWithRefId(recordData.mId); } void RecordHelper::overrideRecord(const mwmp::LightRecord& record) { const ESM::Light &recordData = record.data; if (recordData.mId.empty()) { LOG_APPEND(TimedLog::LOG_INFO, "-- Ignoring record override with no id provided"); return; } bool isExistingId = doesRecordIdExist(recordData.mId); MWBase::World *world = MWBase::Environment::get().getWorld(); if (record.baseId.empty()) { world->getModifiableStore().overrideRecord(recordData); } else if (doesRecordIdExist(record.baseId)) { const ESM::Light *baseData = world->getStore().get().search(record.baseId); ESM::Light finalData = *baseData; finalData.mId = recordData.mId; if (record.baseOverrides.hasName) finalData.mName = recordData.mName; if (record.baseOverrides.hasModel) finalData.mModel = recordData.mModel; if (record.baseOverrides.hasIcon) finalData.mIcon = recordData.mIcon; if (record.baseOverrides.hasSound) finalData.mSound = recordData.mSound; if (record.baseOverrides.hasWeight) finalData.mData.mWeight = recordData.mData.mWeight; if (record.baseOverrides.hasValue) finalData.mData.mValue = recordData.mData.mValue; if (record.baseOverrides.hasTime) finalData.mData.mTime = recordData.mData.mTime; if (record.baseOverrides.hasRadius) finalData.mData.mRadius = recordData.mData.mRadius; if (record.baseOverrides.hasColor) finalData.mData.mColor = recordData.mData.mColor; if (record.baseOverrides.hasFlags) finalData.mData.mFlags = recordData.mData.mFlags; if (record.baseOverrides.hasScript) finalData.mScript = recordData.mScript; world->getModifiableStore().overrideRecord(finalData); } else { LOG_APPEND(TimedLog::LOG_INFO, "-- Ignoring record override with invalid baseId %s", record.baseId.c_str()); return; } if (isExistingId) world->updatePtrsWithRefId(recordData.mId); } void RecordHelper::overrideRecord(const mwmp::LockpickRecord& record) { const ESM::Lockpick &recordData = record.data; if (recordData.mId.empty()) { LOG_APPEND(TimedLog::LOG_INFO, "-- Ignoring record override with no id provided"); return; } bool isExistingId = doesRecordIdExist(recordData.mId); MWBase::World *world = MWBase::Environment::get().getWorld(); if (record.baseId.empty()) { world->getModifiableStore().overrideRecord(recordData); } else if (doesRecordIdExist(record.baseId)) { const ESM::Lockpick *baseData = world->getStore().get().search(record.baseId); ESM::Lockpick finalData = *baseData; finalData.mId = recordData.mId; if (record.baseOverrides.hasName) finalData.mName = recordData.mName; if (record.baseOverrides.hasModel) finalData.mModel = recordData.mModel; if (record.baseOverrides.hasIcon) finalData.mIcon = recordData.mIcon; if (record.baseOverrides.hasWeight) finalData.mData.mWeight = recordData.mData.mWeight; if (record.baseOverrides.hasValue) finalData.mData.mValue = recordData.mData.mValue; if (record.baseOverrides.hasQuality) finalData.mData.mQuality = recordData.mData.mQuality; if (record.baseOverrides.hasUses) finalData.mData.mUses = recordData.mData.mUses; if (record.baseOverrides.hasScript) finalData.mScript = recordData.mScript; world->getModifiableStore().overrideRecord(finalData); } else { LOG_APPEND(TimedLog::LOG_INFO, "-- Ignoring record override with invalid baseId %s", record.baseId.c_str()); return; } if (isExistingId) world->updatePtrsWithRefId(recordData.mId); } void RecordHelper::overrideRecord(const mwmp::MiscellaneousRecord& record) { const ESM::Miscellaneous &recordData = record.data; if (recordData.mId.empty()) { LOG_APPEND(TimedLog::LOG_INFO, "-- Ignoring record override with no id provided"); return; } bool isExistingId = doesRecordIdExist(recordData.mId); MWBase::World *world = MWBase::Environment::get().getWorld(); if (record.baseId.empty()) { world->getModifiableStore().overrideRecord(recordData); } else if (doesRecordIdExist(record.baseId)) { const ESM::Miscellaneous *baseData = world->getStore().get().search(record.baseId); ESM::Miscellaneous finalData = *baseData; finalData.mId = recordData.mId; if (record.baseOverrides.hasName) finalData.mName = recordData.mName; if (record.baseOverrides.hasModel) finalData.mModel = recordData.mModel; if (record.baseOverrides.hasIcon) finalData.mIcon = recordData.mIcon; if (record.baseOverrides.hasWeight) finalData.mData.mWeight = recordData.mData.mWeight; if (record.baseOverrides.hasValue) finalData.mData.mValue = recordData.mData.mValue; if (record.baseOverrides.hasKeyState) finalData.mData.mIsKey = recordData.mData.mIsKey; if (record.baseOverrides.hasScript) finalData.mScript = recordData.mScript; world->getModifiableStore().overrideRecord(finalData); } else { LOG_APPEND(TimedLog::LOG_INFO, "-- Ignoring record override with invalid baseId %s", record.baseId.c_str()); return; } if (isExistingId) world->updatePtrsWithRefId(recordData.mId); } void RecordHelper::overrideRecord(const mwmp::NpcRecord& record) { const ESM::NPC &recordData = record.data; if (recordData.mId.empty()) { LOG_APPEND(TimedLog::LOG_INFO, "-- Ignoring record override with no id provided"); return; } bool isExistingId = doesRecordIdExist(recordData.mId); MWBase::World *world = MWBase::Environment::get().getWorld(); if (record.baseId.empty()) { if (!doesRecordIdExist(recordData.mRace)) { LOG_APPEND(TimedLog::LOG_INFO, "-- Ignoring new NPC record with invalid race provided"); return; } else if (!doesRecordIdExist(recordData.mClass)) { LOG_APPEND(TimedLog::LOG_INFO, "-- Ignoring new NPC record with invalid class provided"); return; } else world->getModifiableStore().overrideRecord(recordData); } else if (doesRecordIdExist(record.baseId)) { const ESM::NPC *baseData = world->getStore().get().search(record.baseId); ESM::NPC finalData = *baseData; finalData.mId = recordData.mId; if (record.baseOverrides.hasName) finalData.mName = recordData.mName; if (record.baseOverrides.hasFlags) finalData.mFlags = recordData.mFlags; // Because the gender is part of mFlags and can easily be set incorrectly there, // we handle it separately here if (record.baseOverrides.hasGender) finalData.setIsMale(recordData.isMale()); else finalData.setIsMale(baseData->isMale()); if (record.baseOverrides.hasRace) finalData.mRace = recordData.mRace; if (record.baseOverrides.hasModel) finalData.mModel = recordData.mModel; if (record.baseOverrides.hasHair) finalData.mHair = recordData.mHair; if (record.baseOverrides.hasHead) finalData.mHead = recordData.mHead; if (!recordData.mClass.empty()) finalData.mClass = recordData.mClass; if (record.baseOverrides.hasFaction) finalData.mFaction = recordData.mFaction; if (record.baseOverrides.hasScript) finalData.mScript = recordData.mScript; if (record.baseOverrides.hasLevel) finalData.mNpdt.mLevel = recordData.mNpdt.mLevel; if (record.baseOverrides.hasHealth) finalData.mNpdt.mHealth = recordData.mNpdt.mHealth; if (record.baseOverrides.hasMagicka) finalData.mNpdt.mMana = recordData.mNpdt.mMana; if (record.baseOverrides.hasFatigue) finalData.mNpdt.mFatigue = recordData.mNpdt.mFatigue; if (record.baseOverrides.hasAiFight) finalData.mAiData.mFight = recordData.mAiData.mFight; if (record.baseOverrides.hasAiFlee) finalData.mAiData.mFlee = recordData.mAiData.mFlee; if (record.baseOverrides.hasAiAlarm) finalData.mAiData.mAlarm = recordData.mAiData.mAlarm; if (record.baseOverrides.hasAiServices) finalData.mAiData.mServices = recordData.mAiData.mServices; if (record.baseOverrides.hasFlags) finalData.mFlags = recordData.mFlags; if (record.baseOverrides.hasAutoCalc) { finalData.mNpdtType = recordData.mNpdtType; if ((recordData.mFlags & ESM::NPC::Autocalc) != 0) finalData.mFlags |= ESM::NPC::Autocalc; else finalData.mFlags &= ~ESM::NPC::Autocalc; } if (!record.inventoryBaseId.empty() && doesRecordIdExist(record.inventoryBaseId)) finalData.mInventory.mList = world->getStore().get().search(record.inventoryBaseId)->mInventory.mList; else if (record.baseOverrides.hasInventory) finalData.mInventory.mList = recordData.mInventory.mList; world->getModifiableStore().overrideRecord(finalData); } else { LOG_APPEND(TimedLog::LOG_INFO, "-- Ignoring record override with invalid baseId %s", record.baseId.c_str()); return; } if (isExistingId) world->updatePtrsWithRefId(recordData.mId); } void RecordHelper::overrideRecord(const mwmp::PotionRecord& record) { const ESM::Potion &recordData = record.data; if (recordData.mId.empty()) { LOG_APPEND(TimedLog::LOG_INFO, "-- Ignoring record override with no id provided"); return; } bool isExistingId = doesRecordIdExist(recordData.mId); MWBase::World *world = MWBase::Environment::get().getWorld(); if (record.baseId.empty()) { world->getModifiableStore().overrideRecord(recordData); } else if (doesRecordIdExist(record.baseId)) { const ESM::Potion *baseData = world->getStore().get().search(record.baseId); ESM::Potion finalData = *baseData; finalData.mId = recordData.mId; if (record.baseOverrides.hasName) finalData.mName = recordData.mName; if (record.baseOverrides.hasModel) finalData.mModel = recordData.mModel; if (record.baseOverrides.hasIcon) finalData.mIcon = recordData.mIcon; if (record.baseOverrides.hasWeight) finalData.mData.mWeight = recordData.mData.mWeight; if (record.baseOverrides.hasValue) finalData.mData.mValue = recordData.mData.mValue; if (record.baseOverrides.hasAutoCalc) finalData.mData.mAutoCalc = recordData.mData.mAutoCalc; if (record.baseOverrides.hasScript) finalData.mScript = recordData.mScript; if (record.baseOverrides.hasEffects) finalData.mEffects.mList = recordData.mEffects.mList; world->getModifiableStore().overrideRecord(finalData); } else { LOG_APPEND(TimedLog::LOG_INFO, "-- Ignoring record override with invalid baseId %s", record.baseId.c_str()); return; } if (isExistingId) world->updatePtrsWithRefId(recordData.mId); } void RecordHelper::overrideRecord(const mwmp::ProbeRecord& record) { const ESM::Probe &recordData = record.data; if (recordData.mId.empty()) { LOG_APPEND(TimedLog::LOG_INFO, "-- Ignoring record override with no id provided"); return; } bool isExistingId = doesRecordIdExist(recordData.mId); MWBase::World *world = MWBase::Environment::get().getWorld(); if (record.baseId.empty()) { world->getModifiableStore().overrideRecord(recordData); } else if (doesRecordIdExist(record.baseId)) { const ESM::Probe *baseData = world->getStore().get().search(record.baseId); ESM::Probe finalData = *baseData; finalData.mId = recordData.mId; if (record.baseOverrides.hasName) finalData.mName = recordData.mName; if (record.baseOverrides.hasModel) finalData.mModel = recordData.mModel; if (record.baseOverrides.hasIcon) finalData.mIcon = recordData.mIcon; if (record.baseOverrides.hasWeight) finalData.mData.mWeight = recordData.mData.mWeight; if (record.baseOverrides.hasValue) finalData.mData.mValue = recordData.mData.mValue; if (record.baseOverrides.hasQuality) finalData.mData.mQuality = recordData.mData.mQuality; if (record.baseOverrides.hasUses) finalData.mData.mUses = recordData.mData.mUses; if (record.baseOverrides.hasScript) finalData.mScript = recordData.mScript; world->getModifiableStore().overrideRecord(finalData); } else { LOG_APPEND(TimedLog::LOG_INFO, "-- Ignoring record override with invalid baseId %s", record.baseId.c_str()); return; } if (isExistingId) world->updatePtrsWithRefId(recordData.mId); } void RecordHelper::overrideRecord(const mwmp::RepairRecord& record) { const ESM::Repair &recordData = record.data; if (recordData.mId.empty()) { LOG_APPEND(TimedLog::LOG_INFO, "-- Ignoring record override with no id provided"); return; } bool isExistingId = doesRecordIdExist(recordData.mId); MWBase::World *world = MWBase::Environment::get().getWorld(); if (record.baseId.empty()) { world->getModifiableStore().overrideRecord(recordData); } else if (doesRecordIdExist(record.baseId)) { const ESM::Repair *baseData = world->getStore().get().search(record.baseId); ESM::Repair finalData = *baseData; finalData.mId = recordData.mId; if (record.baseOverrides.hasName) finalData.mName = recordData.mName; if (record.baseOverrides.hasModel) finalData.mModel = recordData.mModel; if (record.baseOverrides.hasIcon) finalData.mIcon = recordData.mIcon; if (record.baseOverrides.hasWeight) finalData.mData.mWeight = recordData.mData.mWeight; if (record.baseOverrides.hasValue) finalData.mData.mValue = recordData.mData.mValue; if (record.baseOverrides.hasQuality) finalData.mData.mQuality = recordData.mData.mQuality; if (record.baseOverrides.hasUses) finalData.mData.mUses = recordData.mData.mUses; if (record.baseOverrides.hasScript) finalData.mScript = recordData.mScript; world->getModifiableStore().overrideRecord(finalData); } else { LOG_APPEND(TimedLog::LOG_INFO, "-- Ignoring record override with invalid baseId %s", record.baseId.c_str()); return; } if (isExistingId) world->updatePtrsWithRefId(recordData.mId); } void RecordHelper::overrideRecord(const mwmp::ScriptRecord& record) { const ESM::Script &recordData = record.data; if (recordData.mId.empty()) { LOG_APPEND(TimedLog::LOG_INFO, "-- Ignoring record override with no id provided"); return; } MWBase::World *world = MWBase::Environment::get().getWorld(); if (record.baseId.empty()) { world->getModifiableStore().overrideRecord(recordData); } else if (doesRecordIdExist(record.baseId)) { const ESM::Script *baseData = world->getStore().get().search(record.baseId); ESM::Script finalData = *baseData; finalData.mId = recordData.mId; if (record.baseOverrides.hasScriptText) finalData.mScriptText = recordData.mScriptText; world->getModifiableStore().overrideRecord(finalData); } else { LOG_APPEND(TimedLog::LOG_INFO, "-- Ignoring record override with invalid baseId %s", record.baseId.c_str()); return; } } void RecordHelper::overrideRecord(const mwmp::SoundRecord& record) { const ESM::Sound& recordData = record.data; if (recordData.mId.empty()) { LOG_APPEND(TimedLog::LOG_INFO, "-- Ignoring record override with no id provided"); return; } bool isExistingId = doesRecordIdExist(recordData.mId); MWBase::World* world = MWBase::Environment::get().getWorld(); if (record.baseId.empty()) { world->getModifiableStore().overrideRecord(recordData); } else if (doesRecordIdExist(record.baseId)) { const ESM::Sound* baseData = world->getStore().get().search(record.baseId); ESM::Sound finalData = *baseData; finalData.mId = recordData.mId; if (record.baseOverrides.hasSound) finalData.mSound = recordData.mSound; if (record.baseOverrides.hasVolume) finalData.mData.mVolume = recordData.mData.mVolume; if (record.baseOverrides.hasMinRange) finalData.mData.mMinRange = recordData.mData.mMinRange; if (record.baseOverrides.hasMaxRange) finalData.mData.mMaxRange = recordData.mData.mMaxRange; world->getModifiableStore().overrideRecord(finalData); } else { LOG_APPEND(TimedLog::LOG_INFO, "-- Ignoring record override with invalid baseId %s", record.baseId.c_str()); return; } if (isExistingId) world->updatePtrsWithRefId(recordData.mId); } void RecordHelper::overrideRecord(const mwmp::SpellRecord& record) { const ESM::Spell &recordData = record.data; if (recordData.mId.empty()) { LOG_APPEND(TimedLog::LOG_INFO, "-- Ignoring record override with no id provided"); return; } bool isExistingId = doesRecordIdExist(recordData.mId); MWBase::World *world = MWBase::Environment::get().getWorld(); if (record.baseId.empty()) { world->getModifiableStore().overrideRecord(recordData); } else if (doesRecordIdExist(record.baseId)) { const ESM::Spell *baseData = world->getStore().get().search(record.baseId); ESM::Spell finalData = *baseData; finalData.mId = recordData.mId; if (record.baseOverrides.hasName) finalData.mName = recordData.mName; if (record.baseOverrides.hasSubtype) finalData.mData.mType = recordData.mData.mType; if (record.baseOverrides.hasCost) finalData.mData.mCost = recordData.mData.mCost; if (record.baseOverrides.hasFlags) finalData.mData.mFlags = recordData.mData.mFlags; if (record.baseOverrides.hasEffects) finalData.mEffects.mList = recordData.mEffects.mList; world->getModifiableStore().overrideRecord(finalData); } else { LOG_APPEND(TimedLog::LOG_INFO, "-- Ignoring record override with invalid baseId %s", record.baseId.c_str()); return; } } void RecordHelper::overrideRecord(const mwmp::StaticRecord& record) { const ESM::Static &recordData = record.data; if (recordData.mId.empty()) { LOG_APPEND(TimedLog::LOG_INFO, "-- Ignoring record override with no id provided"); return; } bool isExistingId = doesRecordIdExist(recordData.mId); MWBase::World *world = MWBase::Environment::get().getWorld(); if (record.baseId.empty()) { world->getModifiableStore().overrideRecord(recordData); } else if (doesRecordIdExist(record.baseId)) { const ESM::Static *baseData = world->getStore().get().search(record.baseId); ESM::Static finalData = *baseData; finalData.mId = recordData.mId; if (record.baseOverrides.hasModel) finalData.mModel = recordData.mModel; world->getModifiableStore().overrideRecord(finalData); } else { LOG_APPEND(TimedLog::LOG_INFO, "-- Ignoring record override with invalid baseId %s", record.baseId.c_str()); return; } if (isExistingId) world->updatePtrsWithRefId(recordData.mId); } void RecordHelper::overrideRecord(const mwmp::WeaponRecord& record) { const ESM::Weapon &recordData = record.data; if (recordData.mId.empty()) { LOG_APPEND(TimedLog::LOG_INFO, "-- Ignoring record override with no id provided"); return; } bool isExistingId = doesRecordIdExist(recordData.mId); MWBase::World *world = MWBase::Environment::get().getWorld(); if (record.baseId.empty()) { if (!recordData.mEnchant.empty() && !doesRecordIdExist(recordData.mEnchant)) { LOG_APPEND(TimedLog::LOG_INFO, "-- Ignoring new weapon record with invalid enchantmentId %s", recordData.mEnchant.c_str()); return; } else world->getModifiableStore().overrideRecord(recordData); } else if (doesRecordIdExist(record.baseId)) { const ESM::Weapon *baseData = world->getStore().get().search(record.baseId); ESM::Weapon finalData = *baseData; finalData.mId = recordData.mId; if (record.baseOverrides.hasName) finalData.mName = recordData.mName; if (record.baseOverrides.hasModel) finalData.mModel = recordData.mModel; if (record.baseOverrides.hasIcon) finalData.mIcon = recordData.mIcon; if (record.baseOverrides.hasSubtype) finalData.mData.mType = recordData.mData.mType; if (record.baseOverrides.hasWeight) finalData.mData.mWeight = recordData.mData.mWeight; if (record.baseOverrides.hasValue) finalData.mData.mValue = recordData.mData.mValue; if (record.baseOverrides.hasHealth) finalData.mData.mHealth = recordData.mData.mHealth; if (record.baseOverrides.hasSpeed) finalData.mData.mSpeed = recordData.mData.mSpeed; if (record.baseOverrides.hasReach) finalData.mData.mReach = recordData.mData.mReach; if (record.baseOverrides.hasDamageChop) { finalData.mData.mChop[0] = recordData.mData.mChop[0]; finalData.mData.mChop[1] = recordData.mData.mChop[1]; } if (record.baseOverrides.hasDamageSlash) { finalData.mData.mSlash[0] = recordData.mData.mSlash[0]; finalData.mData.mSlash[1] = recordData.mData.mSlash[1]; } if (record.baseOverrides.hasDamageThrust) { finalData.mData.mThrust[0] = recordData.mData.mThrust[0]; finalData.mData.mThrust[1] = recordData.mData.mThrust[1]; } if (record.baseOverrides.hasFlags) finalData.mData.mFlags = recordData.mData.mFlags; if (record.baseOverrides.hasEnchantmentId) { if (recordData.mEnchant.empty() || doesRecordIdExist(recordData.mEnchant)) finalData.mEnchant = recordData.mEnchant; else LOG_APPEND(TimedLog::LOG_INFO, "-- Ignoring invalid enchantmentId %s", recordData.mEnchant.c_str()); } if (record.baseOverrides.hasEnchantmentCharge) finalData.mData.mEnchant = recordData.mData.mEnchant; if (record.baseOverrides.hasScript) finalData.mScript = recordData.mScript; world->getModifiableStore().overrideRecord(finalData); } else { LOG_APPEND(TimedLog::LOG_INFO, "-- Ignoring record override with invalid baseId %s", record.baseId.c_str()); return; } if (isExistingId) world->updatePtrsWithRefId(recordData.mId); } void RecordHelper::createPlaceholderInteriorCell() { MWBase::World* world = MWBase::Environment::get().getWorld(); ESM::Cell placeholderInterior; placeholderInterior.mData.mFlags |= ESM::Cell::Flags::Interior; placeholderInterior.mName = placeholderInteriorCellName; world->getModifiableStore().insert(placeholderInterior); } const std::string RecordHelper::getPlaceholderInteriorCellName() { return placeholderInteriorCellName; } ================================================ FILE: apps/openmw/mwmp/RecordHelper.hpp ================================================ #ifndef OPENMW_RECORDHELPER_HPP #define OPENMW_RECORDHELPER_HPP #include #include "../mwbase/environment.hpp" namespace RecordHelper { void overrideRecord(const mwmp::ActivatorRecord& record); void overrideRecord(const mwmp::ApparatusRecord& record); void overrideRecord(const mwmp::ArmorRecord& record); void overrideRecord(const mwmp::BodyPartRecord& record); void overrideRecord(const mwmp::BookRecord& record); void overrideRecord(const mwmp::CellRecord& record); void overrideRecord(const mwmp::ClothingRecord& record); void overrideRecord(const mwmp::ContainerRecord& record); void overrideRecord(const mwmp::CreatureRecord& record); void overrideRecord(const mwmp::DoorRecord& record); void overrideRecord(const mwmp::EnchantmentRecord& record); void overrideRecord(const mwmp::GameSettingRecord& record); void overrideRecord(const mwmp::IngredientRecord& record); void overrideRecord(const mwmp::LightRecord& record); void overrideRecord(const mwmp::LockpickRecord& record); void overrideRecord(const mwmp::MiscellaneousRecord& record); void overrideRecord(const mwmp::NpcRecord& record); void overrideRecord(const mwmp::PotionRecord& record); void overrideRecord(const mwmp::ProbeRecord& record); void overrideRecord(const mwmp::RepairRecord& record); void overrideRecord(const mwmp::ScriptRecord& record); void overrideRecord(const mwmp::SoundRecord& record); void overrideRecord(const mwmp::SpellRecord& record); void overrideRecord(const mwmp::StaticRecord& record); void overrideRecord(const mwmp::WeaponRecord& record); template void overrideRecord(const RecordType &record) { MWBase::World *world = MWBase::Environment::get().getWorld(); world->getModifiableStore().overrideRecord(record); } template const RecordType *createRecord(const RecordType &record) { MWBase::World *world = MWBase::Environment::get().getWorld(); return world->createRecord(record); } template bool doesRecordIdExist(const std::string& id) { MWBase::World *world = MWBase::Environment::get().getWorld(); return world->getStore().get().search(id); } void createPlaceholderInteriorCell(); const std::string getPlaceholderInteriorCellName(); const std::string placeholderInteriorCellName = "$Transitional Void"; } #endif //OPENMW_RECORDHELPER_HPP ================================================ FILE: apps/openmw/mwmp/ScriptController.cpp ================================================ #include #include "../mwscript/interpretercontext.hpp" #include "ScriptController.hpp" unsigned short ScriptController::getPacketOriginFromContextType(unsigned short contextType) { if (contextType == Interpreter::Context::CONSOLE) return mwmp::CLIENT_CONSOLE; else if (contextType == Interpreter::Context::DIALOGUE) return mwmp::CLIENT_DIALOGUE; else if (contextType == Interpreter::Context::SCRIPT_LOCAL) return mwmp::CLIENT_SCRIPT_LOCAL; else if (contextType == Interpreter::Context::SCRIPT_GLOBAL) return mwmp::CLIENT_SCRIPT_GLOBAL; return mwmp::CLIENT_GAMEPLAY; } ================================================ FILE: apps/openmw/mwmp/ScriptController.hpp ================================================ #ifndef OPENMW_SCRIPTCONTROLLER_HPP #define OPENMW_SCRIPTCONTROLLER_HPP namespace ScriptController { unsigned short getPacketOriginFromContextType(unsigned short contextType); } #endif //OPENMW_SCRIPTCONTROLLER_HPP ================================================ FILE: apps/openmw/mwmp/Worldstate.cpp ================================================ #include #include "../mwbase/environment.hpp" #include "../mwgui/windowmanagerimp.hpp" #include "../mwmechanics/mechanicsmanagerimp.hpp" #include "../mwworld/player.hpp" #include "../mwworld/worldimp.hpp" #include "Worldstate.hpp" #include "Main.hpp" #include "Networking.hpp" #include "PlayerList.hpp" #include "DedicatedPlayer.hpp" #include "RecordHelper.hpp" #include "CellController.hpp" using namespace mwmp; Worldstate::Worldstate() { hasPlayerCollision = true; hasActorCollision = true; hasPlacedObjectCollision = false; useActorCollisionForPlacedObjects = false; } Worldstate::~Worldstate() { } Networking *Worldstate::getNetworking() { return mwmp::Main::get().getNetworking(); } void Worldstate::addRecords() { LOG_MESSAGE_SIMPLE(TimedLog::LOG_INFO, "Received ID_RECORD_DYNAMIC with %i records of type %i", recordsCount, recordsType); if (recordsType == mwmp::RECORD_TYPE::SPELL) { for (auto &&record : spellRecords) { bool hasBaseId = !record.baseId.empty(); LOG_APPEND(TimedLog::LOG_INFO, "- spell record %s, %s\n-- baseId is %s", record.data.mId.c_str(), record.data.mName.c_str(), hasBaseId ? record.baseId.c_str() : "empty"); RecordHelper::overrideRecord(record); } } else if (recordsType == mwmp::RECORD_TYPE::POTION) { for (auto &&record : potionRecords) { bool hasBaseId = !record.baseId.empty(); LOG_APPEND(TimedLog::LOG_INFO, "- potion record %s, %s\n-- baseId is %s", record.data.mId.c_str(), record.data.mName.c_str(), hasBaseId ? record.baseId.c_str() : "empty"); RecordHelper::overrideRecord(record); } } else if (recordsType == mwmp::RECORD_TYPE::ENCHANTMENT) { for (auto &&record : enchantmentRecords) { bool hasBaseId = !record.baseId.empty(); LOG_APPEND(TimedLog::LOG_INFO, "- enchantment record %s, %i\n-- baseId is %s", record.data.mId.c_str(), record.data.mData.mType, hasBaseId ? record.baseId.c_str() : "empty"); RecordHelper::overrideRecord(record); } } else if (recordsType == mwmp::RECORD_TYPE::CREATURE) { for (auto &&record : creatureRecords) { bool hasBaseId = !record.baseId.empty(); LOG_APPEND(TimedLog::LOG_INFO, "- creature record %s, %s\n-- baseId is %s", record.data.mId.c_str(), record.data.mName.c_str(), hasBaseId ? record.baseId.c_str() : "empty"); RecordHelper::overrideRecord(record); } } else if (recordsType == mwmp::RECORD_TYPE::NPC) { for (auto &&record : npcRecords) { bool hasBaseId = !record.baseId.empty(); LOG_APPEND(TimedLog::LOG_INFO, "- NPC record %s, %s\n-- baseId is %s", record.data.mId.c_str(), record.data.mName.c_str(), hasBaseId ? record.baseId.c_str() : "empty"); RecordHelper::overrideRecord(record); } } else if (recordsType == mwmp::RECORD_TYPE::ARMOR) { for (auto &&record : armorRecords) { bool hasBaseId = !record.baseId.empty(); LOG_APPEND(TimedLog::LOG_INFO, "- armor record %s, %s\n-- baseId is %s", record.data.mId.c_str(), record.data.mName.c_str(), hasBaseId ? record.baseId.c_str() : "empty"); RecordHelper::overrideRecord(record); } } else if (recordsType == mwmp::RECORD_TYPE::BOOK) { for (auto &&record : bookRecords) { bool hasBaseId = !record.baseId.empty(); LOG_APPEND(TimedLog::LOG_INFO, "- book record %s, %s\n-- baseId is %s", record.data.mId.c_str(), record.data.mName.c_str(), hasBaseId ? record.baseId.c_str() : "empty"); RecordHelper::overrideRecord(record); } } else if (recordsType == mwmp::RECORD_TYPE::CLOTHING) { for (auto &&record : clothingRecords) { bool hasBaseId = !record.baseId.empty(); LOG_APPEND(TimedLog::LOG_INFO, "- clothing record %s, %s\n-- baseId is %s", record.data.mId.c_str(), record.data.mName.c_str(), hasBaseId ? record.baseId.c_str() : "empty"); RecordHelper::overrideRecord(record); } } else if (recordsType == mwmp::RECORD_TYPE::MISCELLANEOUS) { for (auto &&record : miscellaneousRecords) { bool hasBaseId = !record.baseId.empty(); LOG_APPEND(TimedLog::LOG_INFO, "- miscellaneous record %s, %s\n-- baseId is %s", record.data.mId.c_str(), record.data.mName.c_str(), hasBaseId ? record.baseId.c_str() : "empty"); RecordHelper::overrideRecord(record); } } else if (recordsType == mwmp::RECORD_TYPE::WEAPON) { for (auto &&record : weaponRecords) { bool hasBaseId = !record.baseId.empty(); LOG_APPEND(TimedLog::LOG_INFO, "- weapon record %s, %s\n-- baseId is %s", record.data.mId.c_str(), record.data.mName.c_str(), hasBaseId ? record.baseId.c_str() : "empty"); RecordHelper::overrideRecord(record); } } else if (recordsType == mwmp::RECORD_TYPE::CONTAINER) { for (auto &&record : containerRecords) { bool hasBaseId = !record.baseId.empty(); LOG_APPEND(TimedLog::LOG_INFO, "- container record %s, %s\n-- baseId is %s", record.data.mId.c_str(), record.data.mName.c_str(), hasBaseId ? record.baseId.c_str() : "empty"); RecordHelper::overrideRecord(record); } } else if (recordsType == mwmp::RECORD_TYPE::DOOR) { for (auto &&record : doorRecords) { bool hasBaseId = !record.baseId.empty(); LOG_APPEND(TimedLog::LOG_INFO, "- door record %s, %s\n-- baseId is %s", record.data.mId.c_str(), record.data.mName.c_str(), hasBaseId ? record.baseId.c_str() : "empty"); RecordHelper::overrideRecord(record); } } else if (recordsType == mwmp::RECORD_TYPE::ACTIVATOR) { for (auto &&record : activatorRecords) { bool hasBaseId = !record.baseId.empty(); LOG_APPEND(TimedLog::LOG_INFO, "- activator record %s, %s\n-- baseId is %s", record.data.mId.c_str(), record.data.mName.c_str(), hasBaseId ? record.baseId.c_str() : "empty"); RecordHelper::overrideRecord(record); } } else if (recordsType == mwmp::RECORD_TYPE::STATIC) { for (auto &&record : staticRecords) { bool hasBaseId = !record.baseId.empty(); LOG_APPEND(TimedLog::LOG_INFO, "- static record %s\n-- baseId is %s", record.data.mId.c_str(), hasBaseId ? record.baseId.c_str() : "empty"); RecordHelper::overrideRecord(record); } } else if (recordsType == mwmp::RECORD_TYPE::INGREDIENT) { for (auto &&record : ingredientRecords) { bool hasBaseId = !record.baseId.empty(); LOG_APPEND(TimedLog::LOG_INFO, "- ingredient record %s, %s\n-- baseId is %s", record.data.mId.c_str(), record.data.mName.c_str(), hasBaseId ? record.baseId.c_str() : "empty"); RecordHelper::overrideRecord(record); } } else if (recordsType == mwmp::RECORD_TYPE::APPARATUS) { for (auto &&record : apparatusRecords) { bool hasBaseId = !record.baseId.empty(); LOG_APPEND(TimedLog::LOG_INFO, "- apparatus record %s, %s\n-- baseId is %s", record.data.mId.c_str(), record.data.mName.c_str(), hasBaseId ? record.baseId.c_str() : "empty"); RecordHelper::overrideRecord(record); } } else if (recordsType == mwmp::RECORD_TYPE::LOCKPICK) { for (auto &&record : lockpickRecords) { bool hasBaseId = !record.baseId.empty(); LOG_APPEND(TimedLog::LOG_INFO, "- lockpick record %s, %s\n-- baseId is %s", record.data.mId.c_str(), record.data.mName.c_str(), hasBaseId ? record.baseId.c_str() : "empty"); RecordHelper::overrideRecord(record); } } else if (recordsType == mwmp::RECORD_TYPE::PROBE) { for (auto &&record : probeRecords) { bool hasBaseId = !record.baseId.empty(); LOG_APPEND(TimedLog::LOG_INFO, "- probe record %s, %s\n-- baseId is %s", record.data.mId.c_str(), record.data.mName.c_str(), hasBaseId ? record.baseId.c_str() : "empty"); RecordHelper::overrideRecord(record); } } else if (recordsType == mwmp::RECORD_TYPE::REPAIR) { for (auto &&record : repairRecords) { bool hasBaseId = !record.baseId.empty(); LOG_APPEND(TimedLog::LOG_INFO, "- repair record %s, %s\n-- baseId is %s", record.data.mId.c_str(), record.data.mName.c_str(), hasBaseId ? record.baseId.c_str() : "empty"); RecordHelper::overrideRecord(record); } } else if (recordsType == mwmp::RECORD_TYPE::LIGHT) { for (auto &&record : lightRecords) { bool hasBaseId = !record.baseId.empty(); LOG_APPEND(TimedLog::LOG_INFO, "- light record %s, %s\n-- baseId is %s", record.data.mId.c_str(), record.data.mName.c_str(), hasBaseId ? record.baseId.c_str() : "empty"); RecordHelper::overrideRecord(record); } } else if (recordsType == mwmp::RECORD_TYPE::CELL) { for (auto &&record : cellRecords) { bool hasBaseId = !record.baseId.empty(); LOG_APPEND(TimedLog::LOG_INFO, "- cell record %s\n-- baseId is %s", record.data.mName.c_str(), hasBaseId ? record.baseId.c_str() : "empty"); RecordHelper::overrideRecord(record); } } else if (recordsType == mwmp::RECORD_TYPE::SCRIPT) { for (auto &&record : scriptRecords) { bool hasBaseId = !record.baseId.empty(); LOG_APPEND(TimedLog::LOG_INFO, "- script record %s\n-- baseId is %s", record.data.mId.c_str(), hasBaseId ? record.baseId.c_str() : "empty"); RecordHelper::overrideRecord(record); } } else if (recordsType == mwmp::RECORD_TYPE::BODYPART) { for (auto &&record : bodyPartRecords) { bool hasBaseId = !record.baseId.empty(); LOG_APPEND(TimedLog::LOG_INFO, "- bodypart record %s\n-- baseId is %s", record.data.mId.c_str(), hasBaseId ? record.baseId.c_str() : "empty"); RecordHelper::overrideRecord(record); } } else if (recordsType == mwmp::RECORD_TYPE::SOUND) { for (auto&& record : soundRecords) { bool hasBaseId = !record.baseId.empty(); LOG_APPEND(TimedLog::LOG_INFO, "- sound record %s\n-- baseId is %s", record.data.mId.c_str(), hasBaseId ? record.baseId.c_str() : "empty"); RecordHelper::overrideRecord(record); } } else if (recordsType == mwmp::RECORD_TYPE::GAMESETTING) { for (auto&& record : gameSettingRecords) { bool hasBaseId = !record.baseId.empty(); LOG_APPEND(TimedLog::LOG_INFO, "- gameSetting record %s\n-- baseId is %s", record.data.mId.c_str(), hasBaseId ? record.baseId.c_str() : "empty"); RecordHelper::overrideRecord(record); } } } bool Worldstate::containsExploredMapTile(int cellX, int cellY) { for (const auto &mapTile : exploredMapTiles) { if (mapTile.x == cellX && mapTile.y == cellY) return true; } return false; } void Worldstate::markExploredMapTile(int cellX, int cellY) { mwmp::MapTile exploredTile; exploredTile.x = cellX; exploredTile.y = cellY; exploredMapTiles.push_back(exploredTile); } void Worldstate::setClientGlobals() { LOG_MESSAGE_SIMPLE(TimedLog::LOG_INFO, "Received ID_CLIENT_SCRIPT_GLOBAL with the following global values:"); std::string debugMessage = ""; for (const auto &clientGlobal : clientGlobals) { if (TimedLog::GetLevel() <= TimedLog::LOG_INFO) { if (!debugMessage.empty()) debugMessage += ", "; std::string variableTypeAsString; std::string valueAsString; if (clientGlobal.variableType == mwmp::VARIABLE_TYPE::SHORT || clientGlobal.variableType == mwmp::VARIABLE_TYPE::LONG) { variableTypeAsString = clientGlobal.variableType == mwmp::VARIABLE_TYPE::SHORT ? "short" : "long"; valueAsString = std::to_string(clientGlobal.intValue); } else if (clientGlobal.variableType == mwmp::VARIABLE_TYPE::FLOAT) { variableTypeAsString = "float"; valueAsString = std::to_string(clientGlobal.floatValue); } debugMessage += clientGlobal.id + ": " + variableTypeAsString + " " + valueAsString; } MWBase::World* world = MWBase::Environment::get().getWorld(); // If this global doesn't exist, create it if (!world->hasGlobal(clientGlobal.id)) { ESM::VarType varType; if (clientGlobal.variableType == mwmp::VARIABLE_TYPE::SHORT) varType = ESM::VarType::VT_Short; else if (clientGlobal.variableType == mwmp::VARIABLE_TYPE::LONG) varType = ESM::VarType::VT_Long; if (clientGlobal.variableType == mwmp::VARIABLE_TYPE::FLOAT) varType = ESM::VarType::VT_Float; world->createGlobal(clientGlobal.id, varType); } if (clientGlobal.variableType == mwmp::VARIABLE_TYPE::SHORT || clientGlobal.variableType == mwmp::VARIABLE_TYPE::LONG) world->setGlobalInt(clientGlobal.id, clientGlobal.intValue); else if (clientGlobal.variableType == mwmp::VARIABLE_TYPE::FLOAT) world->setGlobalFloat(clientGlobal.id, clientGlobal.floatValue); } LOG_APPEND(TimedLog::LOG_INFO, "- %s", debugMessage.c_str()); } void Worldstate::setKills() { LOG_MESSAGE_SIMPLE(TimedLog::LOG_INFO, "Received ID_WORLD_KILL_COUNT with the following kill counts:"); std::string debugMessage = ""; for (const auto &killChange : killChanges) { if (TimedLog::GetLevel() <= TimedLog::LOG_INFO) { if (!debugMessage.empty()) debugMessage += ", "; debugMessage += killChange.refId + ": " + std::to_string(killChange.number); } MWBase::Environment::get().getMechanicsManager()->setDeaths(killChange.refId, killChange.number); } LOG_APPEND(TimedLog::LOG_INFO, "- %s", debugMessage.c_str()); } void Worldstate::setMapExplored() { for (const auto &mapTile : mapTiles) { const MWWorld::CellStore *cellStore = MWBase::Environment::get().getWorld()->getExterior(mapTile.x, mapTile.y); if (!cellStore->getCell()->mName.empty()) MWBase::Environment::get().getWindowManager()->addVisitedLocation(cellStore->getCell()->mName, mapTile.x, mapTile.y); MWBase::Environment::get().getWindowManager()->setGlobalMapImage(mapTile.x, mapTile.y, mapTile.imageData); // Keep this tile marked as explored so we don't send any more packets for it markExploredMapTile(mapTile.x, mapTile.y); } } void Worldstate::setWeather() { MWBase::World *world = MWBase::Environment::get().getWorld(); // There's a chance we've been sent the weather for a region right after a teleportation // that hasn't been registered in the WeatherManager yet, meaning the WeatherManager // doesn't have the correct new region set for us, so make sure we update it world->updateWeather(0); LOG_MESSAGE_SIMPLE(TimedLog::LOG_INFO, "Setting weather for region: %s, currentWeather: %i, " "nextWeather: %i, queuedWeather: %i, transitionFactor: %f, forceWeather is %s", weather.region.c_str(), weather.currentWeather, weather.nextWeather, weather.queuedWeather, weather.transitionFactor, forceWeather ? "true" : "false"); world->setRegionWeather(weather.region.c_str(), weather.currentWeather, weather.nextWeather, weather.queuedWeather, weather.transitionFactor, forceWeather); } void Worldstate::resetCells(std::vector* cells) { MWBase::World* world = MWBase::Environment::get().getWorld(); bool haveUnloadedActiveCells = false; ESM::Cell playerCell = *world->getPlayerPtr().getCell()->getCell(); ESM::Position playerPos = world->getPlayerPtr().getRefData().getPosition(); std::vector playersInCell; for (auto cell : *cells) { if (!haveUnloadedActiveCells) { if (world->isCellActive(cell)) { playersInCell = mwmp::PlayerList::getPlayersInCell(cell); // If there are any DedicatedPlayers in this cell, also move them to the temporary holding interior cell if (!playersInCell.empty()) { for (RakNet::RakNetGUID otherGuid : playersInCell) { DedicatedPlayer* dedicatedPlayer = mwmp::PlayerList::getPlayer(otherGuid); dedicatedPlayer->cell = *world->getInterior(RecordHelper::getPlaceholderInteriorCellName())->getCell(); dedicatedPlayer->setCell(); } } // Change to temporary holding interior cell world->changeToInteriorCell(RecordHelper::getPlaceholderInteriorCellName(), playerPos, true, true); mwmp::Main::get().getCellController()->uninitializeCells(); world->unloadActiveCells(); haveUnloadedActiveCells = true; } } world->clearCellStore(cell); for (RakNet::RakNetGUID otherGuid : playersInCell) { DedicatedPlayer* dedicatedPlayer = mwmp::PlayerList::getPlayer(otherGuid); dedicatedPlayer->cell = cell; dedicatedPlayer->setCell(); } } // Move the player from their temporary holding cell to their previous cell if (haveUnloadedActiveCells) { if (playerCell.isExterior()) world->changeToExteriorCell(playerPos, true, true); else world->changeToInteriorCell(playerCell.mName, playerPos, true, true); } } void Worldstate::sendClientGlobal(std::string varName, int value, mwmp::VARIABLE_TYPE variableType) { clientGlobals.clear(); mwmp::ClientVariable clientVariable; clientVariable.id = varName; clientVariable.variableType = variableType; clientVariable.intValue = value; std::string variableTypeAsString; if (variableType == mwmp::VARIABLE_TYPE::SHORT) variableTypeAsString = "short"; else if (variableType == mwmp::VARIABLE_TYPE::LONG) variableTypeAsString = "long"; LOG_MESSAGE_SIMPLE(TimedLog::LOG_INFO, "Sending ID_CLIENT_SCRIPT_GLOBAL with name %s, type %s, value %i", varName.c_str(), variableTypeAsString.c_str(), value); clientGlobals.push_back(clientVariable); getNetworking()->getWorldstatePacket(ID_CLIENT_SCRIPT_GLOBAL)->setWorldstate(this); getNetworking()->getWorldstatePacket(ID_CLIENT_SCRIPT_GLOBAL)->Send(); } void Worldstate::sendClientGlobal(std::string varName, float value) { clientGlobals.clear(); mwmp::ClientVariable clientVariable; clientVariable.id = varName; clientVariable.variableType = mwmp::VARIABLE_TYPE::FLOAT; clientVariable.floatValue = value; LOG_MESSAGE_SIMPLE(TimedLog::LOG_INFO, "Sending ID_CLIENT_SCRIPT_GLOBAL with name %s, type float, value %f", varName.c_str(), value); clientGlobals.push_back(clientVariable); getNetworking()->getWorldstatePacket(ID_CLIENT_SCRIPT_GLOBAL)->setWorldstate(this); getNetworking()->getWorldstatePacket(ID_CLIENT_SCRIPT_GLOBAL)->Send(); } void Worldstate::sendMapExplored(int cellX, int cellY, const std::vector& imageData) { mapTiles.clear(); mwmp::MapTile mapTile; mapTile.x = cellX; mapTile.y = cellY; mapTile.imageData = imageData; LOG_MESSAGE_SIMPLE(TimedLog::LOG_INFO, "Sending ID_PLAYER_MAP with x: %i, y: %i", cellX, cellY); mapTiles.push_back(mapTile); getNetworking()->getWorldstatePacket(ID_WORLD_MAP)->setWorldstate(this); getNetworking()->getWorldstatePacket(ID_WORLD_MAP)->Send(); } void Worldstate::sendWeather(std::string region, int currentWeather, int nextWeather, int queuedWeather, float transitionFactor) { forceWeather = false; weather.region = region; weather.currentWeather = currentWeather; weather.nextWeather = nextWeather; weather.queuedWeather = queuedWeather; weather.transitionFactor = transitionFactor; LOG_MESSAGE_SIMPLE(TimedLog::LOG_INFO, "Sending ID_PLAYER_WEATHER with region: %s, currentWeather: %i, " "nextWeather: %i, queuedWeather, %i, transitionFactor: %f", region.c_str(), currentWeather, nextWeather, queuedWeather, transitionFactor); getNetworking()->getWorldstatePacket(ID_WORLD_WEATHER)->setWorldstate(this); getNetworking()->getWorldstatePacket(ID_WORLD_WEATHER)->Send(); } void Worldstate::sendEnchantmentRecord(const ESM::Enchantment* enchantment) { enchantmentRecords.clear(); LOG_MESSAGE_SIMPLE(TimedLog::LOG_INFO, "Sending ID_RECORD_DYNAMIC with enchantment"); recordsType = mwmp::RECORD_TYPE::ENCHANTMENT; mwmp::EnchantmentRecord record; record.data = *enchantment; enchantmentRecords.push_back(record); getNetworking()->getWorldstatePacket(ID_RECORD_DYNAMIC)->setWorldstate(this); getNetworking()->getWorldstatePacket(ID_RECORD_DYNAMIC)->Send(); } void Worldstate::sendPotionRecord(const ESM::Potion* potion, unsigned int quantity) { potionRecords.clear(); LOG_MESSAGE_SIMPLE(TimedLog::LOG_INFO, "Sending ID_RECORD_DYNAMIC with potion %s", potion->mName.c_str()); recordsType = mwmp::RECORD_TYPE::POTION; mwmp::PotionRecord record; record.data = *potion; record.quantity = quantity; potionRecords.push_back(record); getNetworking()->getWorldstatePacket(ID_RECORD_DYNAMIC)->setWorldstate(this); getNetworking()->getWorldstatePacket(ID_RECORD_DYNAMIC)->Send(); } void Worldstate::sendSpellRecord(const ESM::Spell* spell) { spellRecords.clear(); LOG_MESSAGE_SIMPLE(TimedLog::LOG_INFO, "Sending ID_RECORD_DYNAMIC with spell %s", spell->mName.c_str()); recordsType = mwmp::RECORD_TYPE::SPELL; mwmp::SpellRecord record; record.data = *spell; spellRecords.push_back(record); getNetworking()->getWorldstatePacket(ID_RECORD_DYNAMIC)->setWorldstate(this); getNetworking()->getWorldstatePacket(ID_RECORD_DYNAMIC)->Send(); } void Worldstate::sendArmorRecord(const ESM::Armor* armor, std::string baseId) { armorRecords.clear(); LOG_MESSAGE_SIMPLE(TimedLog::LOG_INFO, "Sending ID_RECORD_DYNAMIC with armor %s", armor->mName.c_str()); recordsType = mwmp::RECORD_TYPE::ARMOR; mwmp::ArmorRecord record; record.data = *armor; record.baseId = baseId; record.baseOverrides.hasName = true; record.baseOverrides.hasEnchantmentId = true; record.baseOverrides.hasEnchantmentCharge = true; armorRecords.push_back(record); getNetworking()->getWorldstatePacket(ID_RECORD_DYNAMIC)->setWorldstate(this); getNetworking()->getWorldstatePacket(ID_RECORD_DYNAMIC)->Send(); } void Worldstate::sendBookRecord(const ESM::Book* book, std::string baseId) { bookRecords.clear(); LOG_MESSAGE_SIMPLE(TimedLog::LOG_INFO, "Sending ID_RECORD_DYNAMIC with book %s", book->mName.c_str()); recordsType = mwmp::RECORD_TYPE::BOOK; mwmp::BookRecord record; record.data = *book; record.baseId = baseId; record.baseOverrides.hasName = true; record.baseOverrides.hasEnchantmentId = true; record.baseOverrides.hasEnchantmentCharge = true; bookRecords.push_back(record); getNetworking()->getWorldstatePacket(ID_RECORD_DYNAMIC)->setWorldstate(this); getNetworking()->getWorldstatePacket(ID_RECORD_DYNAMIC)->Send(); } void Worldstate::sendClothingRecord(const ESM::Clothing* clothing, std::string baseId) { clothingRecords.clear(); LOG_MESSAGE_SIMPLE(TimedLog::LOG_INFO, "Sending ID_RECORD_DYNAMIC with clothing %s", clothing->mName.c_str()); recordsType = mwmp::RECORD_TYPE::CLOTHING; mwmp::ClothingRecord record; record.data = *clothing; record.baseId = baseId; record.baseOverrides.hasName = true; record.baseOverrides.hasEnchantmentId = true; record.baseOverrides.hasEnchantmentCharge = true; clothingRecords.push_back(record); getNetworking()->getWorldstatePacket(ID_RECORD_DYNAMIC)->setWorldstate(this); getNetworking()->getWorldstatePacket(ID_RECORD_DYNAMIC)->Send(); } void Worldstate::sendWeaponRecord(const ESM::Weapon* weapon, std::string baseId, unsigned int quantity) { weaponRecords.clear(); LOG_MESSAGE_SIMPLE(TimedLog::LOG_INFO, "Sending ID_RECORD_DYNAMIC with weapon %s", weapon->mName.c_str()); recordsType = mwmp::RECORD_TYPE::WEAPON; mwmp::WeaponRecord record; record.data = *weapon; record.quantity = quantity; record.baseId = baseId; record.baseOverrides.hasName = true; record.baseOverrides.hasEnchantmentId = true; record.baseOverrides.hasEnchantmentCharge = true; weaponRecords.push_back(record); getNetworking()->getWorldstatePacket(ID_RECORD_DYNAMIC)->setWorldstate(this); getNetworking()->getWorldstatePacket(ID_RECORD_DYNAMIC)->Send(); } ================================================ FILE: apps/openmw/mwmp/Worldstate.hpp ================================================ #ifndef OPENMW_WORLDSTATE_HPP #define OPENMW_WORLDSTATE_HPP #include namespace mwmp { class Networking; class Worldstate : public BaseWorldstate { public: Worldstate(); virtual ~Worldstate(); void addRecords(); bool containsExploredMapTile(int cellX, int cellY); void markExploredMapTile(int cellX, int cellY); void setClientGlobals(); void setKills(); void setMapExplored(); void setWeather(); void resetCells(std::vector* cells); void sendClientGlobal(std::string varName, int value, mwmp::VARIABLE_TYPE variableType); void sendClientGlobal(std::string varName, float value); void sendMapExplored(int cellX, int cellY, const std::vector& imageData); void sendWeather(std::string region, int currentWeather, int nextWeather, int queuedWeather, float transitionFactor); void sendEnchantmentRecord(const ESM::Enchantment* enchantment); void sendPotionRecord(const ESM::Potion* potion, unsigned int quantity); void sendSpellRecord(const ESM::Spell* spell); void sendArmorRecord(const ESM::Armor* armor, std::string baseRefId = ""); void sendBookRecord(const ESM::Book* book, std::string baseRefId = ""); void sendClothingRecord(const ESM::Clothing* clothing, std::string baseRefId = ""); void sendWeaponRecord(const ESM::Weapon* weapon, std::string baseRefId = "", unsigned int quantity = 1); private: std::vector exploredMapTiles; Networking *getNetworking(); }; } #endif //OPENMW_WORLDSTATE_HPP ================================================ FILE: apps/openmw/mwmp/processors/ActorProcessor.cpp ================================================ #include "ActorProcessor.hpp" #include "../Networking.hpp" #include "../Main.hpp" using namespace mwmp; template typename BasePacketProcessor::processors_t BasePacketProcessor::processors; ActorProcessor::~ActorProcessor() { } bool ActorProcessor::Process(RakNet::Packet &packet, ActorList &actorList) { RakNet::BitStream bsIn(&packet.data[1], packet.length, false); bsIn.Read(guid); actorList.guid = guid; ActorPacket *myPacket = Main::get().getNetworking()->getActorPacket(packet.data[0]); myPacket->setActorList(&actorList); myPacket->SetReadStream(&bsIn); for (auto &processor : processors) { if (processor.first == packet.data[0]) { myGuid = Main::get().getLocalPlayer()->guid; request = packet.length == myPacket->headerSize(); actorList.isValid = true; if (!request && !processor.second->avoidReading) { myPacket->Read(); } if (actorList.isValid) processor.second->Do(*myPacket, actorList); else LOG_MESSAGE_SIMPLE(TimedLog::LOG_ERROR, "Received %s that failed integrity check and was ignored!", processor.second->strPacketID.c_str()); return true; } } return false; } ================================================ FILE: apps/openmw/mwmp/processors/ActorProcessor.hpp ================================================ #ifndef OPENMW_ACTORPROCESSOR_HPP #define OPENMW_ACTORPROCESSOR_HPP #include #include #include #include "../ObjectList.hpp" #include "../ActorList.hpp" #include "BaseClientPacketProcessor.hpp" namespace mwmp { class ActorProcessor : public BasePacketProcessor, public BaseClientPacketProcessor { public: virtual void Do(ActorPacket &packet, ActorList &actorList) = 0; static bool Process(RakNet::Packet &packet, ActorList &actorList); virtual ~ActorProcessor(); }; } #endif //OPENMW_ACTORPROCESSOR_HPP ================================================ FILE: apps/openmw/mwmp/processors/BaseClientPacketProcessor.cpp ================================================ #include "BaseClientPacketProcessor.hpp" #include "../Main.hpp" using namespace mwmp; RakNet::RakNetGUID BaseClientPacketProcessor::guid; RakNet::RakNetGUID BaseClientPacketProcessor::myGuid; RakNet::SystemAddress BaseClientPacketProcessor::serverAddr; bool BaseClientPacketProcessor::request; LocalPlayer *BaseClientPacketProcessor::getLocalPlayer() { return Main::get().getLocalPlayer(); } ================================================ FILE: apps/openmw/mwmp/processors/BaseClientPacketProcessor.hpp ================================================ #ifndef OPENMW_BASECLIENTPACKETPROCESSOR_HPP #define OPENMW_BASECLIENTPACKETPROCESSOR_HPP #include #include "../LocalPlayer.hpp" #include "../DedicatedPlayer.hpp" namespace mwmp { class BaseClientPacketProcessor { public: static void SetServerAddr(RakNet::SystemAddress addr) { serverAddr = addr; } protected: inline bool isRequest() { return request; } inline bool isLocal() { return guid == myGuid; } LocalPlayer *getLocalPlayer(); protected: static RakNet::RakNetGUID guid, myGuid; static RakNet::SystemAddress serverAddr; static bool request; }; } #endif //OPENMW_BASECLIENTPACKETPROCESSOR_HPP ================================================ FILE: apps/openmw/mwmp/processors/ObjectProcessor.cpp ================================================ #include "../Main.hpp" #include "../Networking.hpp" #include "ObjectProcessor.hpp" using namespace mwmp; template typename BasePacketProcessor::processors_t BasePacketProcessor::processors; ObjectProcessor::~ObjectProcessor() { } bool ObjectProcessor::Process(RakNet::Packet &packet, ObjectList &objectList) { RakNet::BitStream bsIn(&packet.data[1], packet.length, false); bsIn.Read(guid); objectList.guid = guid; ObjectPacket *myPacket = Main::get().getNetworking()->getObjectPacket(packet.data[0]); myPacket->setObjectList(&objectList); myPacket->SetReadStream(&bsIn); for (auto &processor: processors) { if (processor.first == packet.data[0]) { myGuid = Main::get().getLocalPlayer()->guid; request = packet.length == myPacket->headerSize(); objectList.isValid = true; if (!request && !processor.second->avoidReading) myPacket->Read(); if (objectList.isValid) processor.second->Do(*myPacket, objectList); else LOG_MESSAGE_SIMPLE(TimedLog::LOG_ERROR, "Received %s that failed integrity check and was ignored!", processor.second->strPacketID.c_str()); return true; } } return false; } ================================================ FILE: apps/openmw/mwmp/processors/ObjectProcessor.hpp ================================================ #ifndef OPENMW_OBJECTPROCESSSOR_HPP #define OPENMW_OBJECTPROCESSSOR_HPP #include #include #include #include "../ObjectList.hpp" #include "../LocalPlayer.hpp" #include "../DedicatedPlayer.hpp" #include "BaseClientPacketProcessor.hpp" namespace mwmp { class ObjectProcessor : public BasePacketProcessor, public BaseClientPacketProcessor { public: virtual void Do(ObjectPacket &packet, ObjectList &objectList) = 0; static bool Process(RakNet::Packet &packet, ObjectList &objectList); virtual ~ObjectProcessor(); }; } #endif //OPENMW_OBJECTPROCESSSOR_HPP ================================================ FILE: apps/openmw/mwmp/processors/PlayerProcessor.cpp ================================================ #include "../Networking.hpp" #include "PlayerProcessor.hpp" #include "../Main.hpp" using namespace mwmp; template typename BasePacketProcessor::processors_t BasePacketProcessor::processors; PlayerProcessor::~PlayerProcessor() { } bool PlayerProcessor::Process(RakNet::Packet &packet) { RakNet::BitStream bsIn(&packet.data[1], packet.length, false); bsIn.Read(guid); PlayerPacket *myPacket = Main::get().getNetworking()->getPlayerPacket(packet.data[0]); myPacket->SetReadStream(&bsIn); /*if (myPacket == 0) { // error: packet not found }*/ for (auto &processor : processors) { if (processor.first == packet.data[0]) { myGuid = Main::get().getLocalPlayer()->guid; request = packet.length == myPacket->headerSize(); BasePlayer *player = 0; if (guid != myGuid) player = PlayerList::getPlayer(guid); else player = Main::get().getLocalPlayer(); if (!request && !processor.second->avoidReading && player != 0) { myPacket->setPlayer(player); myPacket->Read(); } processor.second->Do(*myPacket, player); return true; } } return false; } ================================================ FILE: apps/openmw/mwmp/processors/PlayerProcessor.hpp ================================================ #ifndef OPENMW_PLAYERPROCESSOR_HPP #define OPENMW_PLAYERPROCESSOR_HPP #include #include #include #include "../LocalPlayer.hpp" #include "../DedicatedPlayer.hpp" #include "../PlayerList.hpp" #include "BaseClientPacketProcessor.hpp" namespace mwmp { class PlayerProcessor : public BasePacketProcessor, public BaseClientPacketProcessor { public: virtual void Do(PlayerPacket &packet, BasePlayer *player) = 0; static bool Process(RakNet::Packet &packet); virtual ~PlayerProcessor(); }; } #endif //OPENMW_PLAYERPROCESSOR_HPP ================================================ FILE: apps/openmw/mwmp/processors/ProcessorInitializer.cpp ================================================ #include "ProcessorInitializer.hpp" #include "SystemProcessor.hpp" #include "system/ProcessorSystemHandshake.hpp" #include "PlayerProcessor.hpp" #include "player/ProcessorChatMessage.hpp" #include "player/ProcessorGUIMessageBox.hpp" #include "player/ProcessorUserDisconnected.hpp" #include "player/ProcessorGameSettings.hpp" #include "player/ProcessorPlayerAlly.hpp" #include "player/ProcessorPlayerAnimFlags.hpp" #include "player/ProcessorPlayerAnimPlay.hpp" #include "player/ProcessorPlayerAttack.hpp" #include "player/ProcessorPlayerAttribute.hpp" #include "player/ProcessorPlayerBaseInfo.hpp" #include "player/ProcessorPlayerBehavior.hpp" #include "player/ProcessorPlayerBook.hpp" #include "player/ProcessorPlayerBounty.hpp" #include "player/ProcessorPlayerCast.hpp" #include "player/ProcessorPlayerCellChange.hpp" #include "player/ProcessorPlayerCellState.hpp" #include "player/ProcessorPlayerCharClass.hpp" #include "player/ProcessorPlayerCharGen.hpp" #include "player/ProcessorPlayerCooldowns.hpp" #include "player/ProcessorPlayerDeath.hpp" #include "player/ProcessorPlayerDisposition.hpp" #include "player/ProcessorPlayerEquipment.hpp" #include "player/ProcessorPlayerFaction.hpp" #include "player/ProcessorPlayerInput.hpp" #include "player/ProcessorPlayerInventory.hpp" #include "player/ProcessorPlayerItemUse.hpp" #include "player/ProcessorPlayerJail.hpp" #include "player/ProcessorPlayerJournal.hpp" #include "player/ProcessorPlayerLevel.hpp" #include "player/ProcessorPlayerMiscellaneous.hpp" #include "player/ProcessorPlayerMomentum.hpp" #include "player/ProcessorPlayerPosition.hpp" #include "player/ProcessorPlayerQuickKeys.hpp" #include "player/ProcessorPlayerReputation.hpp" #include "player/ProcessorPlayerRest.hpp" #include "player/ProcessorPlayerResurrect.hpp" #include "player/ProcessorPlayerShapeshift.hpp" #include "player/ProcessorPlayerSkill.hpp" #include "player/ProcessorPlayerSpeech.hpp" #include "player/ProcessorPlayerSpellbook.hpp" #include "player/ProcessorPlayerSpellsActive.hpp" #include "player/ProcessorPlayerStatsDynamic.hpp" #include "player/ProcessorPlayerTopic.hpp" #include "ObjectProcessor.hpp" #include "object/ProcessorConsoleCommand.hpp" #include "object/ProcessorContainer.hpp" #include "object/ProcessorDoorDestination.hpp" #include "object/ProcessorDoorState.hpp" #include "object/ProcessorMusicPlay.hpp" #include "object/ProcessorObjectActivate.hpp" #include "object/ProcessorObjectAnimPlay.hpp" #include "object/ProcessorObjectAttach.hpp" #include "object/ProcessorObjectDelete.hpp" #include "object/ProcessorObjectDialogueChoice.hpp" #include "object/ProcessorObjectHit.hpp" #include "object/ProcessorObjectLock.hpp" #include "object/ProcessorObjectMiscellaneous.hpp" #include "object/ProcessorObjectMove.hpp" #include "object/ProcessorObjectPlace.hpp" #include "object/ProcessorObjectRestock.hpp" #include "object/ProcessorObjectRotate.hpp" #include "object/ProcessorObjectScale.hpp" #include "object/ProcessorObjectSound.hpp" #include "object/ProcessorObjectSpawn.hpp" #include "object/ProcessorObjectState.hpp" #include "object/ProcessorObjectTrap.hpp" #include "object/ProcessorClientScriptLocal.hpp" #include "object/ProcessorScriptMemberShort.hpp" #include "object/ProcessorVideoPlay.hpp" #include "ActorProcessor.hpp" #include "actor/ProcessorActorAI.hpp" #include "actor/ProcessorActorAnimFlags.hpp" #include "actor/ProcessorActorAnimPlay.hpp" #include "actor/ProcessorActorAttack.hpp" #include "actor/ProcessorActorAuthority.hpp" #include "actor/ProcessorActorCast.hpp" #include "actor/ProcessorActorCellChange.hpp" #include "actor/ProcessorActorDeath.hpp" #include "actor/ProcessorActorEquipment.hpp" #include "actor/ProcessorActorList.hpp" #include "actor/ProcessorActorPosition.hpp" #include "actor/ProcessorActorSpeech.hpp" #include "actor/ProcessorActorSpellsActive.hpp" #include "actor/ProcessorActorStatsDynamic.hpp" #include "actor/ProcessorActorTest.hpp" #include "WorldstateProcessor.hpp" #include "worldstate/ProcessorCellReset.hpp" #include "worldstate/ProcessorClientScriptGlobal.hpp" #include "worldstate/ProcessorClientScriptSettings.hpp" #include "worldstate/ProcessorRecordDynamic.hpp" #include "worldstate/ProcessorWorldCollisionOverride.hpp" #include "worldstate/ProcessorWorldDestinationOverride.hpp" #include "worldstate/ProcessorWorldKillCount.hpp" #include "worldstate/ProcessorWorldMap.hpp" #include "worldstate/ProcessorWorldRegionAuthority.hpp" #include "worldstate/ProcessorWorldTime.hpp" #include "worldstate/ProcessorWorldWeather.hpp" using namespace mwmp; void ProcessorInitializer() { SystemProcessor::AddProcessor(new ProcessorSystemHandshake()); PlayerProcessor::AddProcessor(new ProcessorChatMessage()); PlayerProcessor::AddProcessor(new ProcessorGUIMessageBox()); PlayerProcessor::AddProcessor(new ProcessorUserDisconnected()); PlayerProcessor::AddProcessor(new ProcessorGameSettings()); PlayerProcessor::AddProcessor(new ProcessorPlayerAlly()); PlayerProcessor::AddProcessor(new ProcessorPlayerAnimFlags()); PlayerProcessor::AddProcessor(new ProcessorPlayerAnimPlay()); PlayerProcessor::AddProcessor(new ProcessorPlayerAttack()); PlayerProcessor::AddProcessor(new ProcessorPlayerAttribute()); PlayerProcessor::AddProcessor(new ProcessorPlayerBaseInfo()); PlayerProcessor::AddProcessor(new ProcessorPlayerBehavior()); PlayerProcessor::AddProcessor(new ProcessorPlayerBook()); PlayerProcessor::AddProcessor(new ProcessorPlayerBounty()); PlayerProcessor::AddProcessor(new ProcessorPlayerCast()); PlayerProcessor::AddProcessor(new ProcessorPlayerCellChange()); PlayerProcessor::AddProcessor(new ProcessorPlayerCellState()); PlayerProcessor::AddProcessor(new ProcessorPlayerCharClass()); PlayerProcessor::AddProcessor(new ProcessorPlayerCharGen()); PlayerProcessor::AddProcessor(new ProcessorPlayerCooldowns()); PlayerProcessor::AddProcessor(new ProcessorPlayerDeath()); PlayerProcessor::AddProcessor(new ProcessorPlayerDisposition()); PlayerProcessor::AddProcessor(new ProcessorPlayerEquipment()); PlayerProcessor::AddProcessor(new ProcessorPlayerFaction()); PlayerProcessor::AddProcessor(new ProcessorPlayerInput()); PlayerProcessor::AddProcessor(new ProcessorPlayerInventory()); PlayerProcessor::AddProcessor(new ProcessorPlayerItemUse()); PlayerProcessor::AddProcessor(new ProcessorPlayerJail()); PlayerProcessor::AddProcessor(new ProcessorPlayerJournal()); PlayerProcessor::AddProcessor(new ProcessorPlayerLevel()); PlayerProcessor::AddProcessor(new ProcessorPlayerMiscellaneous()); PlayerProcessor::AddProcessor(new ProcessorPlayerMomentum()); PlayerProcessor::AddProcessor(new ProcessorPlayerPosition()); PlayerProcessor::AddProcessor(new ProcessorPlayerQuickKeys()); PlayerProcessor::AddProcessor(new ProcessorPlayerReputation()); PlayerProcessor::AddProcessor(new ProcessorPlayerRest()); PlayerProcessor::AddProcessor(new ProcessorPlayerResurrect()); PlayerProcessor::AddProcessor(new ProcessorPlayerShapeshift()); PlayerProcessor::AddProcessor(new ProcessorPlayerSkill()); PlayerProcessor::AddProcessor(new ProcessorPlayerSpeech()); PlayerProcessor::AddProcessor(new ProcessorPlayerSpellbook()); PlayerProcessor::AddProcessor(new ProcessorPlayerSpellsActive()); PlayerProcessor::AddProcessor(new ProcessorPlayerStatsDynamic()); PlayerProcessor::AddProcessor(new ProcessorPlayerTopic()); ObjectProcessor::AddProcessor(new ProcessorConsoleCommand()); ObjectProcessor::AddProcessor(new ProcessorContainer()); ObjectProcessor::AddProcessor(new ProcessorDoorDestination()); ObjectProcessor::AddProcessor(new ProcessorDoorState()); ObjectProcessor::AddProcessor(new ProcessorMusicPlay()); ObjectProcessor::AddProcessor(new ProcessorObjectActivate()); ObjectProcessor::AddProcessor(new ProcessorObjectAnimPlay()); ObjectProcessor::AddProcessor(new ProcessorObjectAttach()); ObjectProcessor::AddProcessor(new ProcessorObjectDelete()); ObjectProcessor::AddProcessor(new ProcessorObjectDialogueChoice()); ObjectProcessor::AddProcessor(new ProcessorObjectHit()); ObjectProcessor::AddProcessor(new ProcessorObjectLock()); ObjectProcessor::AddProcessor(new ProcessorObjectMiscellaneous()); ObjectProcessor::AddProcessor(new ProcessorObjectMove()); ObjectProcessor::AddProcessor(new ProcessorObjectPlace()); ObjectProcessor::AddProcessor(new ProcessorObjectRestock()); ObjectProcessor::AddProcessor(new ProcessorObjectRotate()); ObjectProcessor::AddProcessor(new ProcessorObjectScale()); ObjectProcessor::AddProcessor(new ProcessorObjectSound()); ObjectProcessor::AddProcessor(new ProcessorObjectSpawn()); ObjectProcessor::AddProcessor(new ProcessorObjectState()); ObjectProcessor::AddProcessor(new ProcessorObjectTrap()); ObjectProcessor::AddProcessor(new ProcessorClientScriptLocal()); ObjectProcessor::AddProcessor(new ProcessorScriptMemberShort()); ObjectProcessor::AddProcessor(new ProcessorVideoPlay()); ActorProcessor::AddProcessor(new ProcessorActorAI()); ActorProcessor::AddProcessor(new ProcessorActorAnimFlags()); ActorProcessor::AddProcessor(new ProcessorActorAnimPlay()); ActorProcessor::AddProcessor(new ProcessorActorAttack()); ActorProcessor::AddProcessor(new ProcessorActorAuthority()); ActorProcessor::AddProcessor(new ProcessorActorCast()); ActorProcessor::AddProcessor(new ProcessorActorCellChange()); ActorProcessor::AddProcessor(new ProcessorActorDeath()); ActorProcessor::AddProcessor(new ProcessorActorEquipment()); ActorProcessor::AddProcessor(new ProcessorActorList()); ActorProcessor::AddProcessor(new ProcessorActorPosition()); ActorProcessor::AddProcessor(new ProcessorActorSpeech()); ActorProcessor::AddProcessor(new ProcessorActorSpellsActive()); ActorProcessor::AddProcessor(new ProcessorActorStatsDynamic()); ActorProcessor::AddProcessor(new ProcessorActorTest()); WorldstateProcessor::AddProcessor(new ProcessorCellReset()); WorldstateProcessor::AddProcessor(new ProcessorClientScriptGlobal()); WorldstateProcessor::AddProcessor(new ProcessorClientScriptSettings()); WorldstateProcessor::AddProcessor(new ProcessorRecordDynamic()); WorldstateProcessor::AddProcessor(new ProcessorWorldCollisionOverride()); WorldstateProcessor::AddProcessor(new ProcessorWorldDestinationOverride()); WorldstateProcessor::AddProcessor(new ProcessorWorldKillCount()); WorldstateProcessor::AddProcessor(new ProcessorWorldMap()); WorldstateProcessor::AddProcessor(new ProcessorWorldRegionAuthority()); WorldstateProcessor::AddProcessor(new ProcessorWorldTime()); WorldstateProcessor::AddProcessor(new ProcessorWorldWeather()); } ================================================ FILE: apps/openmw/mwmp/processors/ProcessorInitializer.hpp ================================================ #ifndef OPENMW_INIT_PROCESSORS_HPP #define OPENMW_INIT_PROCESSORS_HPP void ProcessorInitializer(); #endif //OPENMW_INIT_PROCESSORS_HPP ================================================ FILE: apps/openmw/mwmp/processors/SystemProcessor.cpp ================================================ #include "../Networking.hpp" #include "SystemProcessor.hpp" #include "../Main.hpp" using namespace mwmp; template typename BasePacketProcessor::processors_t BasePacketProcessor::processors; SystemProcessor::~SystemProcessor() { } bool SystemProcessor::Process(RakNet::Packet &packet) { RakNet::BitStream bsIn(&packet.data[1], packet.length, false); bsIn.Read(guid); SystemPacket *myPacket = Main::get().getNetworking()->getSystemPacket(packet.data[0]); myPacket->SetReadStream(&bsIn); /*if (myPacket == 0) { // error: packet not found }*/ for (auto &processor : processors) { if (processor.first == packet.data[0]) { myGuid = Main::get().getLocalSystem()->guid; request = packet.length == myPacket->headerSize(); BaseSystem *system = 0; system = Main::get().getLocalSystem(); if (!request && !processor.second->avoidReading && system != 0) { myPacket->setSystem(system); myPacket->Read(); } processor.second->Do(*myPacket, system); return true; } } return false; } ================================================ FILE: apps/openmw/mwmp/processors/SystemProcessor.hpp ================================================ #ifndef OPENMW_SYSTEMPROCESSOR_HPP #define OPENMW_SYSTEMPROCESSOR_HPP #include #include #include #include "../LocalSystem.hpp" #include "BaseClientPacketProcessor.hpp" namespace mwmp { class SystemProcessor : public BasePacketProcessor, public BaseClientPacketProcessor { public: virtual void Do(SystemPacket &packet, BaseSystem *system) = 0; static bool Process(RakNet::Packet &packet); virtual ~SystemProcessor(); }; } #endif //OPENMW_SYSTEMPROCESSOR_HPP ================================================ FILE: apps/openmw/mwmp/processors/WorldstateProcessor.cpp ================================================ #include "../Main.hpp" #include "../Networking.hpp" #include "WorldstateProcessor.hpp" using namespace mwmp; template typename BasePacketProcessor::processors_t BasePacketProcessor::processors; WorldstateProcessor::~WorldstateProcessor() { } bool WorldstateProcessor::Process(RakNet::Packet &packet, Worldstate &worldstate) { RakNet::BitStream bsIn(&packet.data[1], packet.length, false); bsIn.Read(guid); worldstate.guid = guid; WorldstatePacket *myPacket = Main::get().getNetworking()->getWorldstatePacket(packet.data[0]); myPacket->setWorldstate(&worldstate); myPacket->SetReadStream(&bsIn); for (auto &processor : processors) { if (processor.first == packet.data[0]) { myGuid = Main::get().getLocalPlayer()->guid; request = packet.length == myPacket->headerSize(); worldstate.isValid = true; if (!request && !processor.second->avoidReading) myPacket->Read(); if (worldstate.isValid) processor.second->Do(*myPacket, worldstate); else LOG_MESSAGE_SIMPLE(TimedLog::LOG_ERROR, "Received %s that failed integrity check and was ignored!", processor.second->strPacketID.c_str()); return true; } } return false; } ================================================ FILE: apps/openmw/mwmp/processors/WorldstateProcessor.hpp ================================================ #ifndef OPENMW_WORLDSTATEPROCESSOR_HPP #define OPENMW_WORLDSTATEPROCESSOR_HPP #include #include #include #include "BaseClientPacketProcessor.hpp" namespace mwmp { class WorldstateProcessor : public BasePacketProcessor, public BaseClientPacketProcessor { public: virtual void Do(WorldstatePacket &packet, Worldstate &worldstate) = 0; static bool Process(RakNet::Packet &packet, Worldstate &worldstate); virtual ~WorldstateProcessor(); }; } #endif //OPENMW_WORLDSTATEPROCESSOR_HPP ================================================ FILE: apps/openmw/mwmp/processors/actor/ProcessorActorAI.hpp ================================================ #ifndef OPENMW_PROCESSORACTORAI_HPP #define OPENMW_PROCESSORACTORAI_HPP #include "../ActorProcessor.hpp" #include "apps/openmw/mwmp/Main.hpp" #include "apps/openmw/mwmp/CellController.hpp" namespace mwmp { class ProcessorActorAI final: public ActorProcessor { public: ProcessorActorAI() { BPP_INIT(ID_ACTOR_AI); } virtual void Do(ActorPacket &packet, ActorList &actorList) { LOG_MESSAGE_SIMPLE(TimedLog::LOG_INFO, "Received %s about %s", strPacketID.c_str(), actorList.cell.getShortDescription().c_str()); Main::get().getCellController()->readAi(actorList); } }; } #endif //OPENMW_PROCESSORACTORAI_HPP ================================================ FILE: apps/openmw/mwmp/processors/actor/ProcessorActorAnimFlags.hpp ================================================ #ifndef OPENMW_PROCESSORACTORANIMFLAGS_HPP #define OPENMW_PROCESSORACTORANIMFLAGS_HPP #include "../ActorProcessor.hpp" #include "apps/openmw/mwmp/Main.hpp" #include "apps/openmw/mwmp/CellController.hpp" namespace mwmp { class ProcessorActorAnimFlags final: public ActorProcessor { public: ProcessorActorAnimFlags() { BPP_INIT(ID_ACTOR_ANIM_FLAGS); } virtual void Do(ActorPacket &packet, ActorList &actorList) { Main::get().getCellController()->readAnimFlags(actorList); } }; } #endif //OPENMW_PROCESSORACTORANIMFLAGS_HPP ================================================ FILE: apps/openmw/mwmp/processors/actor/ProcessorActorAnimPlay.hpp ================================================ #ifndef OPENMW_PROCESSORACTORANIMPLAY_HPP #define OPENMW_PROCESSORACTORANIMPLAY_HPP #include "../ActorProcessor.hpp" #include "apps/openmw/mwmp/Main.hpp" #include "apps/openmw/mwmp/CellController.hpp" namespace mwmp { class ProcessorActorAnimPlay final: public ActorProcessor { public: ProcessorActorAnimPlay() { BPP_INIT(ID_ACTOR_ANIM_PLAY); } virtual void Do(ActorPacket &packet, ActorList &actorList) { Main::get().getCellController()->readAnimPlay(actorList); } }; } #endif //OPENMW_PROCESSORACTORANIMPLAY_HPP ================================================ FILE: apps/openmw/mwmp/processors/actor/ProcessorActorAttack.hpp ================================================ #ifndef OPENMW_PROCESSORACTORATTACK_HPP #define OPENMW_PROCESSORACTORATTACK_HPP #include "../ActorProcessor.hpp" #include "apps/openmw/mwmp/Main.hpp" #include "apps/openmw/mwmp/CellController.hpp" namespace mwmp { class ProcessorActorAttack final: public ActorProcessor { public: ProcessorActorAttack() { BPP_INIT(ID_ACTOR_ATTACK); } virtual void Do(ActorPacket &packet, ActorList &actorList) { Main::get().getCellController()->readAttack(actorList); } }; } #endif //OPENMW_PROCESSORACTORATTACK_HPP ================================================ FILE: apps/openmw/mwmp/processors/actor/ProcessorActorAuthority.hpp ================================================ #ifndef OPENMW_PROCESSORACTORAUTHORITY_HPP #define OPENMW_PROCESSORACTORAUTHORITY_HPP #include "../ActorProcessor.hpp" #include #include "apps/openmw/mwmp/Main.hpp" #include "apps/openmw/mwmp/CellController.hpp" namespace mwmp { class ProcessorActorAuthority final: public ActorProcessor { public: ProcessorActorAuthority() { BPP_INIT(ID_ACTOR_AUTHORITY) } virtual void Do(ActorPacket &packet, ActorList &actorList) { LOG_MESSAGE_SIMPLE(TimedLog::LOG_INFO, "Received %s about %s", strPacketID.c_str(), actorList.cell.getShortDescription().c_str()); mwmp::CellController *cellController = Main::get().getCellController(); // Never initialize LocalActors in a cell that is no longer loaded, if the server's packet arrived too late if (cellController->isActiveWorldCell(actorList.cell)) { cellController->initializeCell(actorList.cell); mwmp::Cell *cell = cellController->getCell(actorList.cell); cell->setAuthority(guid); if (isLocal()) { LOG_APPEND(TimedLog::LOG_INFO, "- The new authority is me"); cell->uninitializeDedicatedActors(); cell->initializeLocalActors(); cell->updateLocal(true); // Enable updates for DetourNavigator for advanced pathfinding MWBase::World* world = MWBase::Environment::get().getWorld(); world->getNavigator()->setUpdatesEnabled(true); world->getNavigator()->update(world->getPlayerPtr().getRefData().getPosition().asVec3()); } else { BasePlayer *player = PlayerList::getPlayer(guid); if (player != 0) LOG_APPEND(TimedLog::LOG_INFO, "- The new authority is %s", player->npc.mName.c_str()); cell->uninitializeLocalActors(); } } else { LOG_APPEND(TimedLog::LOG_INFO, "- Ignoring it because that cell isn't loaded"); } } }; } #endif //OPENMW_PROCESSORACTORAUTHORITY_HPP ================================================ FILE: apps/openmw/mwmp/processors/actor/ProcessorActorCast.hpp ================================================ #ifndef OPENMW_PROCESSORACTORCAST_HPP #define OPENMW_PROCESSORACTORCAST_HPP #include "../ActorProcessor.hpp" #include "apps/openmw/mwmp/Main.hpp" #include "apps/openmw/mwmp/CellController.hpp" namespace mwmp { class ProcessorActorCast final: public ActorProcessor { public: ProcessorActorCast() { BPP_INIT(ID_ACTOR_CAST); } virtual void Do(ActorPacket &packet, ActorList &actorList) { Main::get().getCellController()->readCast(actorList); } }; } #endif //OPENMW_PROCESSORACTORCAST_HPP ================================================ FILE: apps/openmw/mwmp/processors/actor/ProcessorActorCellChange.hpp ================================================ #ifndef OPENMW_PROCESSORACTORCELLCHANGE_HPP #define OPENMW_PROCESSORACTORCELLCHANGE_HPP #include "../ActorProcessor.hpp" #include "apps/openmw/mwmp/Main.hpp" #include "apps/openmw/mwmp/CellController.hpp" namespace mwmp { class ProcessorActorCellChange final: public ActorProcessor { public: ProcessorActorCellChange() { BPP_INIT(ID_ACTOR_CELL_CHANGE); } virtual void Do(ActorPacket &packet, ActorList &actorList) { Main::get().getCellController()->readCellChange(actorList); } }; } #endif //OPENMW_PROCESSORACTORCELLCHANGE_HPP ================================================ FILE: apps/openmw/mwmp/processors/actor/ProcessorActorDeath.hpp ================================================ #ifndef OPENMW_PROCESSORACTORDEATH_HPP #define OPENMW_PROCESSORACTORDEATH_HPP #include "../ActorProcessor.hpp" #include "apps/openmw/mwmp/Main.hpp" #include "apps/openmw/mwmp/CellController.hpp" namespace mwmp { class ProcessorActorDeath final: public ActorProcessor { public: ProcessorActorDeath() { BPP_INIT(ID_ACTOR_DEATH); } virtual void Do(ActorPacket &packet, ActorList &actorList) { Main::get().getCellController()->readDeath(actorList); } }; } #endif //OPENMW_PROCESSORACTORDEATH_HPP ================================================ FILE: apps/openmw/mwmp/processors/actor/ProcessorActorEquipment.hpp ================================================ #ifndef OPENMW_PROCESSORACTOREQUIPMENT_HPP #define OPENMW_PROCESSORACTOREQUIPMENT_HPP #include "../ActorProcessor.hpp" #include "apps/openmw/mwmp/Main.hpp" #include "apps/openmw/mwmp/CellController.hpp" namespace mwmp { class ProcessorActorEquipment final: public ActorProcessor { public: ProcessorActorEquipment() { BPP_INIT(ID_ACTOR_EQUIPMENT); } virtual void Do(ActorPacket &packet, ActorList &actorList) { Main::get().getCellController()->readEquipment(actorList); } }; } #endif //OPENMW_PROCESSORACTOREQUIPMENT_HPP ================================================ FILE: apps/openmw/mwmp/processors/actor/ProcessorActorList.hpp ================================================ #ifndef OPENMW_PROCESSORACTORLIST_HPP #define OPENMW_PROCESSORACTORLIST_HPP #include "../ActorProcessor.hpp" #include "apps/openmw/mwmp/Main.hpp" #include "apps/openmw/mwmp/CellController.hpp" #include "apps/openmw/mwmp/MechanicsHelper.hpp" namespace mwmp { class ProcessorActorList final: public ActorProcessor { public: ProcessorActorList() { BPP_INIT(ID_ACTOR_LIST) } virtual void Do(ActorPacket &packet, ActorList &actorList) { MWWorld::CellStore *ptrCellStore = Main::get().getCellController()->getCellStore(actorList.cell); if (!ptrCellStore) return; LOG_MESSAGE_SIMPLE(TimedLog::LOG_VERBOSE, "Received %s about %s", strPacketID.c_str(), actorList.cell.getShortDescription().c_str()); LOG_APPEND(TimedLog::LOG_VERBOSE, "- action: %i", actorList.action); // If we've received a request for information, comply with it if (actorList.action == mwmp::BaseActorList::REQUEST) { MechanicsHelper::spawnLeveledCreatures(ptrCellStore); actorList.sendActorsInCell(ptrCellStore); } } }; } #endif //OPENMW_PROCESSORACTORLIST_HPP ================================================ FILE: apps/openmw/mwmp/processors/actor/ProcessorActorPosition.hpp ================================================ #ifndef OPENMW_PROCESSORACTORPOSITION_HPP #define OPENMW_PROCESSORACTORPOSITION_HPP #include "../ActorProcessor.hpp" #include "apps/openmw/mwmp/Main.hpp" #include "apps/openmw/mwmp/CellController.hpp" namespace mwmp { class ProcessorActorPosition final: public ActorProcessor { public: ProcessorActorPosition() { BPP_INIT(ID_ACTOR_POSITION); } virtual void Do(ActorPacket &packet, ActorList &actorList) { Main::get().getCellController()->readPositions(actorList); } }; } #endif //OPENMW_PROCESSORACTORPOSITION_HPP ================================================ FILE: apps/openmw/mwmp/processors/actor/ProcessorActorSpeech.hpp ================================================ #ifndef OPENMW_PROCESSORACTORSPEECH_HPP #define OPENMW_PROCESSORACTORSPEECH_HPP #include "../ActorProcessor.hpp" #include "apps/openmw/mwmp/Main.hpp" #include "apps/openmw/mwmp/CellController.hpp" namespace mwmp { class ProcessorActorSpeech final: public ActorProcessor { public: ProcessorActorSpeech() { BPP_INIT(ID_ACTOR_SPEECH); } virtual void Do(ActorPacket &packet, ActorList &actorList) { Main::get().getCellController()->readSpeech(actorList); } }; } #endif //OPENMW_PROCESSORACTORSPEECH_HPP ================================================ FILE: apps/openmw/mwmp/processors/actor/ProcessorActorSpellsActive.hpp ================================================ #ifndef OPENMW_PROCESSORACTORSPELLSACTIVE_HPP #define OPENMW_PROCESSORACTORSPELLSACTIVE_HPP #include "../ActorProcessor.hpp" #include "apps/openmw/mwmp/Main.hpp" #include "apps/openmw/mwmp/CellController.hpp" namespace mwmp { class ProcessorActorSpellsActive final: public ActorProcessor { public: ProcessorActorSpellsActive() { BPP_INIT(ID_ACTOR_SPELLS_ACTIVE); } virtual void Do(ActorPacket &packet, ActorList &actorList) { Main::get().getCellController()->readSpellsActive(actorList); } }; } #endif //OPENMW_PROCESSORACTORSPELLSACTIVE_HPP ================================================ FILE: apps/openmw/mwmp/processors/actor/ProcessorActorStatsDynamic.hpp ================================================ #ifndef OPENMW_PROCESSORACTORSTATSDYNAMIC_HPP #define OPENMW_PROCESSORACTORSTATSDYNAMIC_HPP #include "../ActorProcessor.hpp" #include "apps/openmw/mwmp/Main.hpp" #include "apps/openmw/mwmp/CellController.hpp" namespace mwmp { class ProcessorActorStatsDynamic final: public ActorProcessor { public: ProcessorActorStatsDynamic() { BPP_INIT(ID_ACTOR_STATS_DYNAMIC); } virtual void Do(ActorPacket &packet, ActorList &actorList) { Main::get().getCellController()->readStatsDynamic(actorList); } }; } #endif //OPENMW_PROCESSORACTORSTATSDYNAMIC_HPP ================================================ FILE: apps/openmw/mwmp/processors/actor/ProcessorActorTest.hpp ================================================ #ifndef OPENMW_PROCESSORACTORTEST_HPP #define OPENMW_PROCESSORACTORTEST_HPP #include "../ActorProcessor.hpp" #include "apps/openmw/mwmp/Main.hpp" #include "apps/openmw/mwmp/CellController.hpp" namespace mwmp { class ProcessorActorTest final: public ActorProcessor { public: ProcessorActorTest() { BPP_INIT(ID_ACTOR_TEST); } virtual void Do(ActorPacket &packet, ActorList &actorList) { } }; } #endif //OPENMW_PROCESSORACTORTEST_HPP ================================================ FILE: apps/openmw/mwmp/processors/object/BaseObjectProcessor.hpp ================================================ #ifndef OPENMW_BASEOBJECTPROCESSOR_HPP #define OPENMW_BASEOBJECTPROCESSOR_HPP #include "../ObjectProcessor.hpp" #include "apps/openmw/mwmp/Main.hpp" #include "apps/openmw/mwmp/CellController.hpp" #include "apps/openmw/mwworld/cellstore.hpp" namespace mwmp { class BaseObjectProcessor : public ObjectProcessor { public: virtual void Do(ObjectPacket &packet, ObjectList &objectList) { LOG_MESSAGE_SIMPLE(TimedLog::LOG_VERBOSE, "Received %s about %s", strPacketID.c_str(), objectList.cell.getShortDescription().c_str()); } protected: MWWorld::CellStore *ptrCellStore; }; } #endif //OPENMW_BASEOBJECTPROCESSOR_HPP ================================================ FILE: apps/openmw/mwmp/processors/object/ProcessorClientScriptLocal.hpp ================================================ #ifndef OPENMW_PROCESSORCLIENTSCRIPTLOCAL_HPP #define OPENMW_PROCESSORCLIENTSCRIPTLOCAL_HPP #include "BaseObjectProcessor.hpp" namespace mwmp { class ProcessorClientScriptLocal final: public BaseObjectProcessor { public: ProcessorClientScriptLocal() { BPP_INIT(ID_CLIENT_SCRIPT_LOCAL) } virtual void Do(ObjectPacket &packet, ObjectList &objectList) { BaseObjectProcessor::Do(packet, objectList); ptrCellStore = Main::get().getCellController()->getCellStore(objectList.cell); if (!ptrCellStore) return; objectList.setClientLocals(ptrCellStore); } }; } #endif //OPENMW_PROCESSORCLIENTSCRIPTLOCAL_HPP ================================================ FILE: apps/openmw/mwmp/processors/object/ProcessorConsoleCommand.hpp ================================================ #ifndef OPENMW_PROCESSORCONSOLECOMMAND_HPP #define OPENMW_PROCESSORCONSOLECOMMAND_HPP #include "BaseObjectProcessor.hpp" namespace mwmp { class ProcessorConsoleCommand final: public BaseObjectProcessor { public: ProcessorConsoleCommand() { BPP_INIT(ID_CONSOLE_COMMAND) } virtual void Do(ObjectPacket &packet, ObjectList &objectList) { BaseObjectProcessor::Do(packet, objectList); ptrCellStore = Main::get().getCellController()->getCellStore(objectList.cell); objectList.runConsoleCommands(ptrCellStore); } }; } #endif //OPENMW_PROCESSORCONSOLECOMMAND_HPP ================================================ FILE: apps/openmw/mwmp/processors/object/ProcessorContainer.hpp ================================================ #ifndef OPENMW_PROCESSORCONTAINER_HPP #define OPENMW_PROCESSORCONTAINER_HPP #include "BaseObjectProcessor.hpp" namespace mwmp { class ProcessorContainer final: public BaseObjectProcessor { public: ProcessorContainer() { BPP_INIT(ID_CONTAINER) } virtual void Do(ObjectPacket &packet, ObjectList &objectList) { BaseObjectProcessor::Do(packet, objectList); ptrCellStore = Main::get().getCellController()->getCellStore(objectList.cell); if (!ptrCellStore) return; std::string debugMessage = "- action "; unsigned char action = objectList.action; unsigned char containerSubAction = objectList.containerSubAction; if (action == mwmp::BaseObjectList::SET) debugMessage += "SET"; else if (action == mwmp::BaseObjectList::ADD) debugMessage += "ADD"; else if (action == mwmp::BaseObjectList::REMOVE) debugMessage += "REMOVE"; else if (action == mwmp::BaseObjectList::REQUEST) debugMessage += "REQUEST"; debugMessage += " and subaction "; if (containerSubAction == mwmp::BaseObjectList::NONE) debugMessage += "NONE"; else if (containerSubAction == mwmp::BaseObjectList::DRAG) debugMessage += "DRAG"; else if (containerSubAction == mwmp::BaseObjectList::DROP) debugMessage += "DROP"; else if (containerSubAction == mwmp::BaseObjectList::TAKE_ALL) debugMessage += "TAKE_ALL"; else if (containerSubAction == mwmp::BaseObjectList::REPLY_TO_REQUEST) debugMessage += "REPLY_TO_REQUEST"; LOG_APPEND(TimedLog::LOG_VERBOSE, "%s", debugMessage.c_str()); // If we've received a request for information, comply with it if (objectList.action == mwmp::BaseObjectList::REQUEST) { if (objectList.baseObjectCount == 0) { LOG_APPEND(TimedLog::LOG_VERBOSE, "- Request had no objects attached, so we are sending all containers in the cell %s", objectList.cell.getShortDescription().c_str()); objectList.reset(); objectList.cell = *ptrCellStore->getCell(); objectList.action = mwmp::BaseObjectList::SET; objectList.containerSubAction = mwmp::BaseObjectList::REPLY_TO_REQUEST; objectList.addAllContainers(ptrCellStore); objectList.sendContainer(); } else { LOG_APPEND(TimedLog::LOG_VERBOSE, "- Request was for %i %s", objectList.baseObjectCount, objectList.baseObjectCount == 1 ? "object" : "objects"); std::vector requestObjects = objectList.baseObjects; objectList.reset(); objectList.cell = *ptrCellStore->getCell(); objectList.action = mwmp::BaseObjectList::SET; objectList.containerSubAction = mwmp::BaseObjectList::REPLY_TO_REQUEST; objectList.addRequestedContainers(ptrCellStore, requestObjects); if (objectList.baseObjects.size() > 0) objectList.sendContainer(); } } // Otherwise, edit containers based on the information received else { LOG_APPEND(TimedLog::LOG_VERBOSE, "- Editing container contents to match those of packet", objectList.baseObjectCount); objectList.editContainers(ptrCellStore); } } }; } #endif //OPENMW_PROCESSORCONTAINER_HPP ================================================ FILE: apps/openmw/mwmp/processors/object/ProcessorDoorDestination.hpp ================================================ #ifndef OPENMW_PROCESSDOORDESTINATION_HPP #define OPENMW_PROCESSDOORDESTINATION_HPP #include "BaseObjectProcessor.hpp" namespace mwmp { class ProcessorDoorDestination final: public BaseObjectProcessor { public: ProcessorDoorDestination() { BPP_INIT(ID_DOOR_DESTINATION) } virtual void Do(ObjectPacket &packet, ObjectList &objectList) { BaseObjectProcessor::Do(packet, objectList); ptrCellStore = Main::get().getCellController()->getCellStore(objectList.cell); if (!ptrCellStore) return; objectList.setDoorDestinations(ptrCellStore); } }; } #endif //OPENMW_PROCESSDOORDESTINATION_HPP ================================================ FILE: apps/openmw/mwmp/processors/object/ProcessorDoorState.hpp ================================================ #ifndef OPENMW_PROCESSDOORSTATE_HPP #define OPENMW_PROCESSDOORSTATE_HPP #include "BaseObjectProcessor.hpp" namespace mwmp { class ProcessorDoorState final: public BaseObjectProcessor { public: ProcessorDoorState() { BPP_INIT(ID_DOOR_STATE) } virtual void Do(ObjectPacket &packet, ObjectList &objectList) { BaseObjectProcessor::Do(packet, objectList); ptrCellStore = Main::get().getCellController()->getCellStore(objectList.cell); if (!ptrCellStore) return; objectList.activateDoors(ptrCellStore); } }; } #endif //OPENMW_PROCESSDOORSTATE_HPP ================================================ FILE: apps/openmw/mwmp/processors/object/ProcessorMusicPlay.hpp ================================================ #ifndef OPENMW_PROCESSORMUSICPLAY_HPP #define OPENMW_PROCESSORMUSICPLAY_HPP #include "../ObjectProcessor.hpp" namespace mwmp { class ProcessorMusicPlay final: public ObjectProcessor { public: ProcessorMusicPlay() { BPP_INIT(ID_MUSIC_PLAY) } virtual void Do(ObjectPacket &packet, ObjectList &objectList) { LOG_MESSAGE_SIMPLE(TimedLog::LOG_VERBOSE, "Received %s", strPacketID.c_str()); objectList.playMusic(); } }; } #endif //OPENMW_PROCESSORSCRIPTGLOBALSHORT_HPP ================================================ FILE: apps/openmw/mwmp/processors/object/ProcessorObjectActivate.hpp ================================================ #ifndef OPENMW_PROCESSOROBJECTACTIVATE_HPP #define OPENMW_PROCESSOROBJECTACTIVATE_HPP #include "BaseObjectProcessor.hpp" namespace mwmp { class ProcessorObjectActivate final: public BaseObjectProcessor { public: ProcessorObjectActivate() { BPP_INIT(ID_OBJECT_ACTIVATE) } virtual void Do(ObjectPacket &packet, ObjectList &objectList) { BaseObjectProcessor::Do(packet, objectList); ptrCellStore = Main::get().getCellController()->getCellStore(objectList.cell); if (!ptrCellStore) return; objectList.activateObjects(ptrCellStore); } }; } #endif //OPENMW_PROCESSOROBJECTACTIVATE_HPP ================================================ FILE: apps/openmw/mwmp/processors/object/ProcessorObjectAnimPlay.hpp ================================================ #ifndef OPENMW_PROCESSOROBJECTANIMPLAY_HPP #define OPENMW_PROCESSOROBJECTANIMPLAY_HPP #include "BaseObjectProcessor.hpp" namespace mwmp { class ProcessorObjectAnimPlay final: public BaseObjectProcessor { public: ProcessorObjectAnimPlay() { BPP_INIT(ID_OBJECT_ANIM_PLAY) } virtual void Do(ObjectPacket &packet, ObjectList &objectList) { BaseObjectProcessor::Do(packet, objectList); ptrCellStore = Main::get().getCellController()->getCellStore(objectList.cell); if (!ptrCellStore) return; objectList.animateObjects(ptrCellStore); } }; } #endif //OPENMW_PROCESSOROBJECTANIMPLAY_HPP ================================================ FILE: apps/openmw/mwmp/processors/object/ProcessorObjectAttach.hpp ================================================ #ifndef OPENMW_PROCESSOROBJECTATTACH_HPP #define OPENMW_PROCESSOROBJECTATTACH_HPP #include "BaseObjectProcessor.hpp" namespace mwmp { class ProcessorObjectAttach final: public BaseObjectProcessor { public: ProcessorObjectAttach() { BPP_INIT(ID_OBJECT_ATTACH) } virtual void Do(ObjectPacket &packet, ObjectList &objectList) { BaseObjectProcessor::Do(packet, objectList); ptrCellStore = Main::get().getCellController()->getCellStore(objectList.cell); if (!ptrCellStore) return; //objectList.attachObjects(ptrCellStore); } }; } #endif //OPENMW_PROCESSOROBJECTATTACH_HPP ================================================ FILE: apps/openmw/mwmp/processors/object/ProcessorObjectDelete.hpp ================================================ #ifndef OPENMW_PROCESSOROBJECTDELETE_HPP #define OPENMW_PROCESSOROBJECTDELETE_HPP #include "BaseObjectProcessor.hpp" namespace mwmp { class ProcessorObjectDelete final: public BaseObjectProcessor { public: ProcessorObjectDelete() { BPP_INIT(ID_OBJECT_DELETE) } virtual void Do(ObjectPacket &packet, ObjectList &objectList) { BaseObjectProcessor::Do(packet, objectList); ptrCellStore = Main::get().getCellController()->getCellStore(objectList.cell); if (!ptrCellStore) return; objectList.deleteObjects(ptrCellStore); } }; } #endif //OPENMW_PROCESSOROBJECTDELETE_HPP ================================================ FILE: apps/openmw/mwmp/processors/object/ProcessorObjectDialogueChoice.hpp ================================================ #ifndef OPENMW_PROCESSOROBJECTDIALOGUECHOICE_HPP #define OPENMW_PROCESSOROBJECTDIALOGUECHOICE_HPP #include "BaseObjectProcessor.hpp" namespace mwmp { class ProcessorObjectDialogueChoice final: public BaseObjectProcessor { public: ProcessorObjectDialogueChoice() { BPP_INIT(ID_OBJECT_DIALOGUE_CHOICE) } virtual void Do(ObjectPacket &packet, ObjectList &objectList) { BaseObjectProcessor::Do(packet, objectList); ptrCellStore = Main::get().getCellController()->getCellStore(objectList.cell); if (!ptrCellStore) return; objectList.makeDialogueChoices(ptrCellStore); } }; } #endif //OPENMW_PROCESSOROBJECTDIALOGUECHOICE_HPP ================================================ FILE: apps/openmw/mwmp/processors/object/ProcessorObjectHit.hpp ================================================ #ifndef OPENMW_PROCESSOROBJECTHIT_HPP #define OPENMW_PROCESSOROBJECTHIT_HPP #include "BaseObjectProcessor.hpp" namespace mwmp { class ProcessorObjectHit final: public BaseObjectProcessor { public: ProcessorObjectHit() { BPP_INIT(ID_OBJECT_HIT) } virtual void Do(ObjectPacket &packet, ObjectList &objectList) { BaseObjectProcessor::Do(packet, objectList); ptrCellStore = Main::get().getCellController()->getCellStore(objectList.cell); if (!ptrCellStore) return; //objectList.hitObjects(ptrCellStore); } }; } #endif //OPENMW_PROCESSOROBJECTHIT_HPP ================================================ FILE: apps/openmw/mwmp/processors/object/ProcessorObjectLock.hpp ================================================ #ifndef OPENMW_PROCESSOROBJECTLOCK_HPP #define OPENMW_PROCESSOROBJECTLOCK_HPP #include "BaseObjectProcessor.hpp" namespace mwmp { class ProcessorObjectLock final: public BaseObjectProcessor { public: ProcessorObjectLock() { BPP_INIT(ID_OBJECT_LOCK) } virtual void Do(ObjectPacket &packet, ObjectList &objectList) { BaseObjectProcessor::Do(packet, objectList); ptrCellStore = Main::get().getCellController()->getCellStore(objectList.cell); if (!ptrCellStore) return; objectList.lockObjects(ptrCellStore); } }; } #endif //OPENMW_PROCESSOROBJECTLOCK_HPP ================================================ FILE: apps/openmw/mwmp/processors/object/ProcessorObjectMiscellaneous.hpp ================================================ #ifndef OPENMW_PROCESSOROBJECTMISCELLANEOUS_HPP #define OPENMW_PROCESSOROBJECTMISCELLANEOUS_HPP #include "BaseObjectProcessor.hpp" namespace mwmp { class ProcessorObjectMiscellaneous final: public BaseObjectProcessor { public: ProcessorObjectMiscellaneous() { BPP_INIT(ID_OBJECT_MISCELLANEOUS) } virtual void Do(ObjectPacket &packet, ObjectList &objectList) { BaseObjectProcessor::Do(packet, objectList); ptrCellStore = Main::get().getCellController()->getCellStore(objectList.cell); if (!ptrCellStore) return; objectList.setGoldPoolsForObjects(ptrCellStore); } }; } #endif //OPENMW_PROCESSOROBJECTMISCELLANEOUS_HPP ================================================ FILE: apps/openmw/mwmp/processors/object/ProcessorObjectMove.hpp ================================================ #ifndef OPENMW_PROCESSOROBJECTMOVE_HPP #define OPENMW_PROCESSOROBJECTMOVE_HPP #include "BaseObjectProcessor.hpp" namespace mwmp { class ProcessorObjectMove final: public BaseObjectProcessor { public: ProcessorObjectMove() { BPP_INIT(ID_OBJECT_MOVE) } virtual void Do(ObjectPacket &packet, ObjectList &objectList) { BaseObjectProcessor::Do(packet, objectList); ptrCellStore = Main::get().getCellController()->getCellStore(objectList.cell); if (!ptrCellStore) return; objectList.moveObjects(ptrCellStore); } }; } #endif //OPENMW_PROCESSOROBJECTMOVE_HPP ================================================ FILE: apps/openmw/mwmp/processors/object/ProcessorObjectPlace.hpp ================================================ #ifndef OPENMW_PROCESSOROBJECTPLACE_HPP #define OPENMW_PROCESSOROBJECTPLACE_HPP #include "BaseObjectProcessor.hpp" namespace mwmp { class ProcessorObjectPlace final: public BaseObjectProcessor { public: ProcessorObjectPlace() { BPP_INIT(ID_OBJECT_PLACE) } virtual void Do(ObjectPacket &packet, ObjectList &objectList) { BaseObjectProcessor::Do(packet, objectList); ptrCellStore = Main::get().getCellController()->getCellStore(objectList.cell); if (!ptrCellStore) return; objectList.placeObjects(ptrCellStore); } }; } #endif //OPENMW_PROCESSOROBJECTPLACE_HPP ================================================ FILE: apps/openmw/mwmp/processors/object/ProcessorObjectRestock.hpp ================================================ #ifndef OPENMW_PROCESSOROBJECTRESTOCK_HPP #define OPENMW_PROCESSOROBJECTRESTOCK_HPP #include "BaseObjectProcessor.hpp" namespace mwmp { class ProcessorObjectRestock final: public BaseObjectProcessor { public: ProcessorObjectRestock() { BPP_INIT(ID_OBJECT_RESTOCK) } virtual void Do(ObjectPacket &packet, ObjectList &objectList) { BaseObjectProcessor::Do(packet, objectList); ptrCellStore = Main::get().getCellController()->getCellStore(objectList.cell); if (!ptrCellStore) return; objectList.restockObjects(ptrCellStore); } }; } #endif //OPENMW_PROCESSOROBJECTRESTOCK_HPP ================================================ FILE: apps/openmw/mwmp/processors/object/ProcessorObjectRotate.hpp ================================================ #ifndef OPENMW_PROCESSOROBJECTROTATE_HPP #define OPENMW_PROCESSOROBJECTROTATE_HPP #include "BaseObjectProcessor.hpp" namespace mwmp { class ProcessorObjectRotate final: public BaseObjectProcessor { public: ProcessorObjectRotate() { BPP_INIT(ID_OBJECT_ROTATE) } virtual void Do(ObjectPacket &packet, ObjectList &objectList) { BaseObjectProcessor::Do(packet, objectList); ptrCellStore = Main::get().getCellController()->getCellStore(objectList.cell); if (!ptrCellStore) return; objectList.rotateObjects(ptrCellStore); } }; } #endif //OPENMW_PROCESSOROBJECTROTATE_HPP ================================================ FILE: apps/openmw/mwmp/processors/object/ProcessorObjectScale.hpp ================================================ #ifndef OPENMW_PROCESSOROBJECTSCALE_HPP #define OPENMW_PROCESSOROBJECTSCALE_HPP #include "BaseObjectProcessor.hpp" namespace mwmp { class ProcessorObjectScale final: public BaseObjectProcessor { public: ProcessorObjectScale() { BPP_INIT(ID_OBJECT_SCALE) } virtual void Do(ObjectPacket &packet, ObjectList &objectList) { BaseObjectProcessor::Do(packet, objectList); ptrCellStore = Main::get().getCellController()->getCellStore(objectList.cell); if (!ptrCellStore) return; objectList.scaleObjects(ptrCellStore); } }; } #endif //OPENMW_PROCESSOROBJECTSCALE_HPP ================================================ FILE: apps/openmw/mwmp/processors/object/ProcessorObjectSound.hpp ================================================ #ifndef OPENMW_PROCESSOROBJECTSOUND_HPP #define OPENMW_PROCESSOROBJECTSOUND_HPP #include "BaseObjectProcessor.hpp" namespace mwmp { class ProcessorObjectSound final: public BaseObjectProcessor { public: ProcessorObjectSound() { BPP_INIT(ID_OBJECT_SOUND) } virtual void Do(ObjectPacket &packet, ObjectList &objectList) { BaseObjectProcessor::Do(packet, objectList); ptrCellStore = Main::get().getCellController()->getCellStore(objectList.cell); if (!ptrCellStore) return; MWBase::World* world = MWBase::Environment::get().getWorld(); // Only play sounds in active cells if (world->isCellActive(*ptrCellStore->getCell())) { objectList.playObjectSounds(ptrCellStore); } } }; } #endif //OPENMW_PROCESSOROBJECTSOUND_HPP ================================================ FILE: apps/openmw/mwmp/processors/object/ProcessorObjectSpawn.hpp ================================================ #ifndef OPENMW_PROCESSOROBJECTSPAWN_HPP #define OPENMW_PROCESSOROBJECTSPAWN_HPP #include "BaseObjectProcessor.hpp" namespace mwmp { class ProcessorObjectSpawn final: public BaseObjectProcessor { public: ProcessorObjectSpawn() { BPP_INIT(ID_OBJECT_SPAWN) } virtual void Do(ObjectPacket &packet, ObjectList &objectList) { BaseObjectProcessor::Do(packet, objectList); ptrCellStore = Main::get().getCellController()->getCellStore(objectList.cell); if (!ptrCellStore) return; objectList.spawnObjects(ptrCellStore); } }; } #endif //OPENMW_PROCESSOROBJECTSPAWN_HPP ================================================ FILE: apps/openmw/mwmp/processors/object/ProcessorObjectState.hpp ================================================ #ifndef OPENMW_PROCESSOROBJECTSTATE_HPP #define OPENMW_PROCESSOROBJECTSTATE_HPP #include "BaseObjectProcessor.hpp" namespace mwmp { class ProcessorObjectState final: public BaseObjectProcessor { public: ProcessorObjectState() { BPP_INIT(ID_OBJECT_STATE) } virtual void Do(ObjectPacket &packet, ObjectList &objectList) { BaseObjectProcessor::Do(packet, objectList); ptrCellStore = Main::get().getCellController()->getCellStore(objectList.cell); if (!ptrCellStore) return; objectList.setObjectStates(ptrCellStore); } }; } #endif //OPENMW_PROCESSOROBJECTSTATE_HPP ================================================ FILE: apps/openmw/mwmp/processors/object/ProcessorObjectTrap.hpp ================================================ #ifndef OPENMW_PROCESSOROBJECTTRAP_HPP #define OPENMW_PROCESSOROBJECTTRAP_HPP #include "BaseObjectProcessor.hpp" namespace mwmp { class ProcessorObjectTrap final: public BaseObjectProcessor { public: ProcessorObjectTrap() { BPP_INIT(ID_OBJECT_TRAP) } virtual void Do(ObjectPacket &packet, ObjectList &objectList) { BaseObjectProcessor::Do(packet, objectList); ptrCellStore = Main::get().getCellController()->getCellStore(objectList.cell); if (!ptrCellStore) return; objectList.triggerTrapObjects(ptrCellStore); } }; } #endif //OPENMW_PROCESSOROBJECTTRAP_HPP ================================================ FILE: apps/openmw/mwmp/processors/object/ProcessorScriptMemberShort.hpp ================================================ #ifndef OPENMW_PROCESSORSCRIPTMEMBERSHORT_HPP #define OPENMW_PROCESSORSCRIPTMEMBERSHORT_HPP #include "../ObjectProcessor.hpp" namespace mwmp { class ProcessorScriptMemberShort final: public ObjectProcessor { public: ProcessorScriptMemberShort() { BPP_INIT(ID_SCRIPT_MEMBER_SHORT) } virtual void Do(ObjectPacket &packet, ObjectList &objectList) { LOG_MESSAGE_SIMPLE(TimedLog::LOG_VERBOSE, "Received %s", strPacketID.c_str()); objectList.setMemberShorts(); } }; } #endif //OPENMW_PROCESSORSCRIPTMEMBERSHORT_HPP ================================================ FILE: apps/openmw/mwmp/processors/object/ProcessorVideoPlay.hpp ================================================ #ifndef OPENMW_PROCESSORVIDEOPLAY_HPP #define OPENMW_PROCESSORVIDEOPLAY_HPP #include "../ObjectProcessor.hpp" namespace mwmp { class ProcessorVideoPlay final: public ObjectProcessor { public: ProcessorVideoPlay() { BPP_INIT(ID_VIDEO_PLAY) } virtual void Do(ObjectPacket &packet, ObjectList &objectList) { LOG_MESSAGE_SIMPLE(TimedLog::LOG_VERBOSE, "Received %s", strPacketID.c_str()); objectList.playVideo(); } }; } #endif //OPENMW_PROCESSORVIDEOPLAY_HPP ================================================ FILE: apps/openmw/mwmp/processors/player/ProcessorChatMessage.hpp ================================================ #ifndef OPENMW_PROCESSORCHATMESSAGE_HPP #define OPENMW_PROCESSORCHATMESSAGE_HPP #include "../PlayerProcessor.hpp" #include "apps/openmw/mwmp/Main.hpp" #include "apps/openmw/mwmp/GUIController.hpp" namespace mwmp { class ProcessorChatMessage final: public PlayerProcessor { public: ProcessorChatMessage() { BPP_INIT(ID_CHAT_MESSAGE) } virtual void Do(PlayerPacket &packet, BasePlayer *player) { if (player != 0) Main::get().getGUIController()->printChatMessage(player->chatMessage); } }; } #endif //OPENMW_PROCESSORCHATMESSAGE_HPP ================================================ FILE: apps/openmw/mwmp/processors/player/ProcessorGUIMessageBox.hpp ================================================ #ifndef OPENMW_PROCESSORGUIMESSAGEBOX_HPP #define OPENMW_PROCESSORGUIMESSAGEBOX_HPP #include "../PlayerProcessor.hpp" namespace mwmp { class ProcessorGUIMessageBox final: public PlayerProcessor { public: ProcessorGUIMessageBox() { BPP_INIT(ID_GUI_MESSAGEBOX) } virtual void Do(PlayerPacket &packet, BasePlayer *player) { if (isLocal()) { LOG_MESSAGE_SIMPLE(TimedLog::LOG_INFO, "ID_GUI_MESSAGEBOX, Type %d, MSG %s", player->guiMessageBox.type, player->guiMessageBox.label.c_str()); switch(player->guiMessageBox.type) { case BasePlayer::GUIMessageBox::MessageBox: Main::get().getGUIController()->showMessageBox(player->guiMessageBox); break; case BasePlayer::GUIMessageBox::CustomMessageBox: Main::get().getGUIController()->showCustomMessageBox(player->guiMessageBox); break; case BasePlayer::GUIMessageBox::InputDialog: case BasePlayer::GUIMessageBox::PasswordDialog: Main::get().getGUIController()->showInputBox(player->guiMessageBox); break; case BasePlayer::GUIMessageBox::ListBox: Main::get().getGUIController()->showDialogList(player->guiMessageBox); break; } } } }; } #endif //OPENMW_PROCESSORGUIMESSAGEBOX_HPP ================================================ FILE: apps/openmw/mwmp/processors/player/ProcessorGameSettings.hpp ================================================ #ifndef OPENMW_PROCESSORGAMESETTINGS_HPP #define OPENMW_PROCESSORGAMESETTINGS_HPP #include "apps/openmw/mwbase/environment.hpp" #include "apps/openmw/mwworld/worldimp.hpp" #include "apps/openmw/mwgui/windowmanagerimp.hpp" #include "../PlayerProcessor.hpp" namespace mwmp { class ProcessorGameSettings final: public PlayerProcessor { const std::string GAME_SETTING_CATEGORY = "Game"; const std::string VR_SETTING_CATEGORY = "VR"; public: ProcessorGameSettings() { BPP_INIT(ID_GAME_SETTINGS) } virtual void Do(PlayerPacket &packet, BasePlayer *player) { static const int initialLogLevel = TimedLog::GetLevel(); if (isLocal()) { LOG_MESSAGE_SIMPLE(TimedLog::LOG_INFO, "Received ID_GAME_SETTINGS"); LOG_APPEND(TimedLog::LOG_INFO, "- player %s rest in beds, %s rest in the wilderness, %s wait", player->bedRestAllowed ? "can" : "cannot", player->wildernessRestAllowed ? "can" : "cannot", player->waitAllowed ? "can" : "cannot"); if (MWBase::Environment::get().getWindowManager()->isGuiMode()) { if (MWBase::Environment::get().getWindowManager()->isConsoleMode() && !player->consoleAllowed) MWBase::Environment::get().getWindowManager()->popGuiMode(); else if (MWBase::Environment::get().getWindowManager()->getMode() == MWGui::GM_Rest && (!player->bedRestAllowed || !player->wildernessRestAllowed || !player->waitAllowed)) MWBase::Environment::get().getWindowManager()->popGuiMode(); } if (player->enforcedLogLevel > -1) { LOG_APPEND(TimedLog::LOG_INFO, "- server is enforcing log level %i", player->enforcedLogLevel); TimedLog::SetLevel(player->enforcedLogLevel); } else if (initialLogLevel != TimedLog::GetLevel()) { LOG_APPEND(TimedLog::LOG_INFO, "- log level has been reset to initial value %i", initialLogLevel); TimedLog::SetLevel(initialLogLevel); } MWBase::Environment::get().getWorld()->setPhysicsFramerate(player->physicsFramerate); for (auto setting : player->gameSettings) { Settings::Manager::setString(setting.first, GAME_SETTING_CATEGORY, setting.second); } // Only read VR settings for players using a VR build #ifdef USE_OPENXR for (auto setting : player->vrSettings) { Settings::Manager::setString(setting.first, VR_SETTING_CATEGORY, setting.second); } #endif } } }; } #endif //OPENMW_PROCESSORGAMESETTINGS_HPP ================================================ FILE: apps/openmw/mwmp/processors/player/ProcessorPlayerAlly.hpp ================================================ #ifndef OPENMW_PROCESSORPLAYERALLY_HPP #define OPENMW_PROCESSORPLAYERALLY_HPP #include "../PlayerProcessor.hpp" #include "apps/openmw/mwmp/Main.hpp" #include "apps/openmw/mwmp/LocalPlayer.hpp" namespace mwmp { class ProcessorPlayerAlly final: public PlayerProcessor { public: ProcessorPlayerAlly() { BPP_INIT(ID_PLAYER_ALLY) } virtual void Do(PlayerPacket &packet, BasePlayer *player) { mwmp::LocalPlayer *localPlayer = mwmp::Main::get().getLocalPlayer(); if (isLocal()) { LOG_MESSAGE_SIMPLE(TimedLog::LOG_INFO, "Received ID_PLAYER_ALLY about LocalPlayer %s from server", localPlayer->npc.mName.c_str()); for (std::vector::iterator iter = localPlayer->alliedPlayers.begin(); iter != localPlayer->alliedPlayers.end(); ) { DedicatedPlayer *dedicatedPlayer = PlayerList::getPlayer(*iter); if (dedicatedPlayer) { LOG_APPEND(TimedLog::LOG_INFO, "- Adding DedicatedPlayer %s to our allied players", dedicatedPlayer->npc.mName.c_str()); } ++iter; } } else if (player != 0) { LOG_MESSAGE_SIMPLE(TimedLog::LOG_INFO, "Received ID_PLAYER_ALLY about DedicatedPlayer %s from server", player->npc.mName.c_str()); for (std::vector::iterator iter = player->alliedPlayers.begin(); iter != player->alliedPlayers.end(); ) { if (*iter == localPlayer->guid) { LOG_APPEND(TimedLog::LOG_INFO, "- Adding LocalPlayer %s to their allied players", localPlayer->npc.mName.c_str()); } else { DedicatedPlayer *otherDedicatedPlayer = PlayerList::getPlayer(*iter); if (otherDedicatedPlayer) { LOG_APPEND(TimedLog::LOG_INFO, "- Adding DedicatedPlayer %s to their allied players", otherDedicatedPlayer->npc.mName.c_str()); } } ++iter; } } } }; } #endif //OPENMW_PROCESSORPLAYERALLY_HPP ================================================ FILE: apps/openmw/mwmp/processors/player/ProcessorPlayerAnimFlags.hpp ================================================ #ifndef OPENMW_PROCESSORPLAYERANIMFLAGS_HPP #define OPENMW_PROCESSORPLAYERANIMFLAGS_HPP #include "../PlayerProcessor.hpp" namespace mwmp { class ProcessorPlayerAnimFlags final: public PlayerProcessor { public: ProcessorPlayerAnimFlags() { BPP_INIT(ID_PLAYER_ANIM_FLAGS) } virtual void Do(PlayerPacket &packet, BasePlayer *player) { if (isLocal()) { if (isRequest()) static_cast(player)->updateAnimFlags(true); } else if (player != 0) static_cast(player)->setAnimFlags(); } }; } #endif //OPENMW_PROCESSORPLAYERANIMFLAGS_HPP ================================================ FILE: apps/openmw/mwmp/processors/player/ProcessorPlayerAnimPlay.hpp ================================================ #ifndef OPENMW_PROCESSORPLAYERANIMPLAY_HPP #define OPENMW_PROCESSORPLAYERANIMPLAY_HPP #include "../PlayerProcessor.hpp" namespace mwmp { class ProcessorPlayerAnimPlay final: public PlayerProcessor { public: ProcessorPlayerAnimPlay() { BPP_INIT(ID_PLAYER_ANIM_PLAY) } virtual void Do(PlayerPacket &packet, BasePlayer *player) { if (isLocal()) { LOG_MESSAGE_SIMPLE(TimedLog::LOG_INFO, "Received ID_PLAYER_ANIM_PLAY about LocalPlayer from server"); static_cast(player)->playAnimation(); } else if (player != 0) static_cast(player)->playAnimation(); } }; } #endif //OPENMW_PROCESSORPLAYERANIMPLAY_HPP ================================================ FILE: apps/openmw/mwmp/processors/player/ProcessorPlayerAttack.hpp ================================================ #ifndef OPENMW_PROCESSORPLAYERATTACK_HPP #define OPENMW_PROCESSORPLAYERATTACK_HPP #include "apps/openmw/mwmp/Main.hpp" #include "../PlayerProcessor.hpp" #include "apps/openmw/mwmp/MechanicsHelper.hpp" namespace mwmp { class ProcessorPlayerAttack final: public PlayerProcessor { public: ProcessorPlayerAttack() { BPP_INIT(ID_PLAYER_ATTACK) } virtual void Do(PlayerPacket &packet, BasePlayer *player) { if (!isLocal() && player != 0) { DedicatedPlayer& dedicatedPlayer = static_cast(*player); MWWorld::Ptr playerPtr = dedicatedPlayer.getPtr(); MWBase::World* world = MWBase::Environment::get().getWorld(); world->moveObject(playerPtr, dedicatedPlayer.position.pos[0], dedicatedPlayer.position.pos[1], dedicatedPlayer.position.pos[2]); world->rotateObject(playerPtr, dedicatedPlayer.position.rot[0], 0, dedicatedPlayer.position.rot[2]); MechanicsHelper::processAttack(player->attack, playerPtr); } } }; } #endif //OPENMW_PROCESSORPLAYERATTACK_HPP ================================================ FILE: apps/openmw/mwmp/processors/player/ProcessorPlayerAttribute.hpp ================================================ #ifndef OPENMW_PROCESSORPLAYERATTRIBUTE_HPP #define OPENMW_PROCESSORPLAYERATTRIBUTE_HPP #include "../PlayerProcessor.hpp" #include "apps/openmw/mwmechanics/npcstats.hpp" #include "apps/openmw/mwclass/npc.hpp" namespace mwmp { class ProcessorPlayerAttribute final: public PlayerProcessor { public: ProcessorPlayerAttribute() { BPP_INIT(ID_PLAYER_ATTRIBUTE) } virtual void Do(PlayerPacket &packet, BasePlayer *player) { if (isLocal()) { if (isRequest()) static_cast(player)->updateAttributes(true); else static_cast(player)->setAttributes(); } else if (player != 0) { static_cast(player)->setAttributes(); } } }; } #endif //OPENMW_PROCESSORPLAYERATTRIBUTE_HPP ================================================ FILE: apps/openmw/mwmp/processors/player/ProcessorPlayerBaseInfo.hpp ================================================ #ifndef OPENMW_PROCESSORPLAYERBASEINFO_HPP #define OPENMW_PROCESSORPLAYERBASEINFO_HPP #include "../PlayerProcessor.hpp" namespace mwmp { class ProcessorPlayerBaseInfo final: public PlayerProcessor { public: ProcessorPlayerBaseInfo() { BPP_INIT(ID_PLAYER_BASEINFO) } virtual void Do(PlayerPacket &packet, BasePlayer *player) { LOG_MESSAGE_SIMPLE(TimedLog::LOG_INFO, "Received ID_PLAYER_BASEINFO from server"); if (isLocal()) { LOG_APPEND(TimedLog::LOG_INFO, "- Packet was about LocalPlayer"); if (isRequest()) { LOG_APPEND(TimedLog::LOG_INFO, "- Requesting info"); packet.Send(serverAddr); } else { LOG_APPEND(TimedLog::LOG_INFO, "- Setting character for LocalPlayer"); static_cast(player)->setCharacter(); } } else { LOG_APPEND(TimedLog::LOG_INFO, "- Packet was about %s", player == 0 ? "new player" : player->npc.mName.c_str()); if (player == 0) { LOG_APPEND(TimedLog::LOG_INFO, "- Exchanging data with new player"); player = PlayerList::newPlayer(guid); packet.setPlayer(player); packet.Read(); } static_cast(player)->setBaseInfo(); } } }; } #endif //OPENMW_PROCESSORPLAYERBASEINFO_HPP ================================================ FILE: apps/openmw/mwmp/processors/player/ProcessorPlayerBehavior.hpp ================================================ #ifndef OPENMW_PROCESSORPLAYERBEHAVIOR_HPP #define OPENMW_PROCESSORPLAYERBEHAVIOR_HPP #include "../PlayerProcessor.hpp" namespace mwmp { class ProcessorPlayerBehavior final: public PlayerProcessor { public: ProcessorPlayerBehavior() { BPP_INIT(ID_PLAYER_BEHAVIOR) } virtual void Do(PlayerPacket &packet, BasePlayer *player) { if (isLocal()) { //static_cast(player)->setBehavior(); } else if (player != 0) { //static_cast(player)->setBehavior(); } } }; } #endif //OPENMW_PROCESSORPLAYERBEHAVIOR_HPP ================================================ FILE: apps/openmw/mwmp/processors/player/ProcessorPlayerBook.hpp ================================================ #ifndef OPENMW_PROCESSORPLAYERBOOK_HPP #define OPENMW_PROCESSORPLAYERBOOK_HPP #include "../PlayerProcessor.hpp" namespace mwmp { class ProcessorPlayerBook final: public PlayerProcessor { public: ProcessorPlayerBook() { BPP_INIT(ID_PLAYER_BOOK) } virtual void Do(PlayerPacket &packet, BasePlayer *player) { if (isLocal()) { static_cast(player)->setBooks(); } } }; } #endif //OPENMW_PROCESSORPLAYERBOOK_HPP ================================================ FILE: apps/openmw/mwmp/processors/player/ProcessorPlayerBounty.hpp ================================================ #ifndef OPENMW_PROCESSORPLAYERBOUNTY_HPP #define OPENMW_PROCESSORPLAYERBOUNTY_HPP #include "../PlayerProcessor.hpp" namespace mwmp { class ProcessorPlayerBounty final: public PlayerProcessor { public: ProcessorPlayerBounty() { BPP_INIT(ID_PLAYER_BOUNTY) } virtual void Do(PlayerPacket &packet, BasePlayer *player) { if (isLocal()) { if (isRequest()) static_cast(player)->updateBounty(true); else static_cast(player)->setBounty(); } else if (player != 0) { MWWorld::Ptr ptrPlayer = static_cast(player)->getPtr(); MWMechanics::NpcStats *ptrNpcStats = &ptrPlayer.getClass().getNpcStats(ptrPlayer); ptrNpcStats->setBounty(player->npcStats.mBounty); } } }; } #endif //OPENMW_PROCESSORPLAYERBOUNTY_HPP ================================================ FILE: apps/openmw/mwmp/processors/player/ProcessorPlayerCast.hpp ================================================ #ifndef OPENMW_PROCESSORPLAYERCAST_HPP #define OPENMW_PROCESSORPLAYERCAST_HPP #include "apps/openmw/mwmp/Main.hpp" #include "../PlayerProcessor.hpp" #include "apps/openmw/mwmp/MechanicsHelper.hpp" namespace mwmp { class ProcessorPlayerCast final: public PlayerProcessor { public: ProcessorPlayerCast() { BPP_INIT(ID_PLAYER_CAST) } virtual void Do(PlayerPacket &packet, BasePlayer *player) { if (!isLocal() && player != 0) { DedicatedPlayer& dedicatedPlayer = static_cast(*player); MWWorld::Ptr playerPtr = dedicatedPlayer.getPtr(); MWBase::World* world = MWBase::Environment::get().getWorld(); world->moveObject(playerPtr, dedicatedPlayer.position.pos[0], dedicatedPlayer.position.pos[1], dedicatedPlayer.position.pos[2]); world->rotateObject(playerPtr, dedicatedPlayer.position.rot[0], 0, dedicatedPlayer.position.rot[2]); MechanicsHelper::processCast(player->cast, playerPtr); } } }; } #endif //OPENMW_PROCESSORPLAYERCAST_HPP ================================================ FILE: apps/openmw/mwmp/processors/player/ProcessorPlayerCellChange.hpp ================================================ #ifndef OPENMW_PROCESSORPLAYERCELLCHANGE_HPP #define OPENMW_PROCESSORPLAYERCELLCHANGE_HPP #include "../PlayerProcessor.hpp" namespace mwmp { class ProcessorPlayerCellChange final: public PlayerProcessor { public: ProcessorPlayerCellChange() { BPP_INIT(ID_PLAYER_CELL_CHANGE) } virtual void Do(PlayerPacket &packet, BasePlayer *player) { if (isLocal()) { LOG_MESSAGE_SIMPLE(TimedLog::LOG_INFO, "Received ID_PLAYER_CELL_CHANGE from server about me"); if (isRequest()) static_cast(player)->updateCell(true); else static_cast(player)->setCell(); } else if (player != 0) static_cast(player)->setCell(); } }; } #endif //OPENMW_PROCESSORPLAYERCELLCHANGE_HPP ================================================ FILE: apps/openmw/mwmp/processors/player/ProcessorPlayerCellState.hpp ================================================ #ifndef OPENMW_PROCESSORPLAYERCELLSTATE_HPP #define OPENMW_PROCESSORPLAYERCELLSTATE_HPP #include "../PlayerProcessor.hpp" namespace mwmp { class ProcessorPlayerCellState final: public PlayerProcessor { public: ProcessorPlayerCellState() { BPP_INIT(ID_PLAYER_CELL_STATE) avoidReading = true; } virtual void Do(PlayerPacket &packet, BasePlayer *player) { if (isLocal() && isRequest()) static_cast(player)->sendCellStates(); } }; } #endif //OPENMW_PROCESSORPLAYERCELLSTATE_HPP ================================================ FILE: apps/openmw/mwmp/processors/player/ProcessorPlayerCharClass.hpp ================================================ #ifndef OPENMW_PROCESSORPLAYERCHARCLASS_HPP #define OPENMW_PROCESSORPLAYERCHARCLASS_HPP #include "../PlayerProcessor.hpp" namespace mwmp { class ProcessorPlayerCharClass final: public PlayerProcessor { public: ProcessorPlayerCharClass() { BPP_INIT(ID_PLAYER_CHARCLASS) } virtual void Do(PlayerPacket &packet, BasePlayer *player) { if (isLocal()) { if (isRequest()) static_cast(player)->sendClass(); else static_cast(player)->setClass(); } } }; } #endif //OPENMW_PROCESSORPLAYERCHARCLASS_HPP ================================================ FILE: apps/openmw/mwmp/processors/player/ProcessorPlayerCharGen.hpp ================================================ #ifndef OPENMW_PROCESSORPLAYERCHARGEN_HPP #define OPENMW_PROCESSORPLAYERCHARGEN_HPP #include "../PlayerProcessor.hpp" namespace mwmp { class ProcessorPlayerCharGen final: public PlayerProcessor { public: ProcessorPlayerCharGen() { BPP_INIT(ID_PLAYER_CHARGEN) } virtual void Do(PlayerPacket &packet, BasePlayer *player) { } }; } #endif //OPENMW_PROCESSORPLAYERCHARGEN_HPP ================================================ FILE: apps/openmw/mwmp/processors/player/ProcessorPlayerCooldowns.hpp ================================================ #ifndef OPENMW_PROCESSORPLAYERCOOLDOWNS_HPP #define OPENMW_PROCESSORPLAYERCOOLDOWNS_HPP #include "../PlayerProcessor.hpp" namespace mwmp { class ProcessorPlayerCooldowns final: public PlayerProcessor { public: ProcessorPlayerCooldowns() { BPP_INIT(ID_PLAYER_COOLDOWNS) } virtual void Do(PlayerPacket &packet, BasePlayer *player) { if (!isLocal()) return; LOG_MESSAGE_SIMPLE(TimedLog::LOG_INFO, "Received ID_PLAYER_COOLDOWNS about LocalPlayer from server"); LocalPlayer &localPlayer = static_cast(*player); localPlayer.setCooldowns(); } }; } #endif //OPENMW_PROCESSORPLAYERCOOLDOWNS_HPP ================================================ FILE: apps/openmw/mwmp/processors/player/ProcessorPlayerDeath.hpp ================================================ #ifndef OPENMW_PROCESSORPLAYERDEATH_HPP #define OPENMW_PROCESSORPLAYERDEATH_HPP #include "../PlayerProcessor.hpp" namespace mwmp { class ProcessorPlayerDeath final: public PlayerProcessor { public: ProcessorPlayerDeath() { BPP_INIT(ID_PLAYER_DEATH) } virtual void Do(PlayerPacket &packet, BasePlayer *player) { LOG_MESSAGE_SIMPLE(TimedLog::LOG_INFO, "Received ID_PLAYER_DEATH from server"); if (isLocal()) { LOG_APPEND(TimedLog::LOG_INFO, "- Packet was about me"); static_cast(player)->die(); } else if (player != 0) { LOG_APPEND(TimedLog::LOG_INFO, "- Packet was about %s", player->npc.mName.c_str()); static_cast(player)->die(); } } }; } #endif //OPENMW_PROCESSORPLAYERDEATH_HPP ================================================ FILE: apps/openmw/mwmp/processors/player/ProcessorPlayerDisposition.hpp ================================================ #ifndef OPENMW_PROCESSORPLAYERDISPOSITION_HPP #define OPENMW_PROCESSORPLAYERDISPOSITION_HPP #include "../PlayerProcessor.hpp" namespace mwmp { class ProcessorPlayerDisposition final: public PlayerProcessor { public: ProcessorPlayerDisposition() { BPP_INIT(ID_PLAYER_DISPOSITION) } virtual void Do(PlayerPacket &packet, BasePlayer *player) { // Placeholder to be filled in later } }; } #endif //OPENMW_PROCESSORPLAYERDISPOSITION_HPP ================================================ FILE: apps/openmw/mwmp/processors/player/ProcessorPlayerEquipment.hpp ================================================ #ifndef OPENMW_PROCESSORPLAYEREQUIPMENT_HPP #define OPENMW_PROCESSORPLAYEREQUIPMENT_HPP #include "../PlayerProcessor.hpp" namespace mwmp { class ProcessorPlayerEquipment final: public PlayerProcessor { public: ProcessorPlayerEquipment() { BPP_INIT(ID_PLAYER_EQUIPMENT) } virtual void Do(PlayerPacket &packet, BasePlayer *player) { if (isLocal()) { LOG_MESSAGE_SIMPLE(TimedLog::LOG_INFO, "Received ID_PLAYER_EQUIPMENT about LocalPlayer from server"); if (isRequest()) static_cast(player)->updateEquipment(true); else static_cast(player)->setEquipment(); } else if (player != 0) static_cast(player)->setEquipment(); } }; } #endif //OPENMW_PROCESSORPLAYEREQUIPMENT_HPP ================================================ FILE: apps/openmw/mwmp/processors/player/ProcessorPlayerFaction.hpp ================================================ #ifndef OPENMW_PROCESSORPLAYERFACTION_HPP #define OPENMW_PROCESSORPLAYERFACTION_HPP #include "../PlayerProcessor.hpp" namespace mwmp { class ProcessorPlayerFaction final: public PlayerProcessor { public: ProcessorPlayerFaction() { BPP_INIT(ID_PLAYER_FACTION) } virtual void Do(PlayerPacket &packet, BasePlayer *player) { if (isRequest()) { // Entire faction membership cannot currently be requested from players } else if (player != 0) { static_cast(player)->setFactions(); } } }; } #endif //OPENMW_PROCESSORPLAYERFACTION_HPP ================================================ FILE: apps/openmw/mwmp/processors/player/ProcessorPlayerInput.hpp ================================================ #ifndef OPENMW_PROCESSORPLAYERINPUT_HPP #define OPENMW_PROCESSORPLAYERINPUT_HPP #include "apps/openmw/mwmp/Main.hpp" #include "../PlayerProcessor.hpp" #include "apps/openmw/mwmp/MechanicsHelper.hpp" namespace mwmp { class ProcessorPlayerInput final: public PlayerProcessor { public: ProcessorPlayerInput() { BPP_INIT(ID_PLAYER_INPUT) } virtual void Do(PlayerPacket &packet, BasePlayer *player) { //if (player != 0) // MechanicsHelper::processInteraction(player->interaction, static_cast(player)->getPtr()); } }; } #endif //OPENMW_PROCESSORPLAYERINPUT_HPP ================================================ FILE: apps/openmw/mwmp/processors/player/ProcessorPlayerInventory.hpp ================================================ #ifndef OPENMW_PROCESSORPLAYERUPDATEINVENTORY_HPP #define OPENMW_PROCESSORPLAYERUPDATEINVENTORY_HPP #include "../PlayerProcessor.hpp" namespace mwmp { class ProcessorPlayerInventory final: public PlayerProcessor { public: ProcessorPlayerInventory() { BPP_INIT(ID_PLAYER_INVENTORY) } virtual void Do(PlayerPacket &packet, BasePlayer *player) { if (!isLocal()) return; LOG_MESSAGE_SIMPLE(TimedLog::LOG_INFO, "Received ID_PLAYER_INVENTORY about LocalPlayer from server"); if (isRequest()) static_cast(player)->updateInventory(true); else { LocalPlayer &localPlayer = static_cast(*player); int inventoryAction = localPlayer.inventoryChanges.action; // Because we send PlayerInventory packets from the same OpenMW methods that we use to set the // items received, we need to set a boolean to prevent resending the items set here localPlayer.avoidSendingInventoryPackets = true; if (inventoryAction == InventoryChanges::ADD) localPlayer.addItems(); else if (inventoryAction == InventoryChanges::REMOVE) localPlayer.removeItems(); else // InventoryChanges::SET localPlayer.setInventory(); localPlayer.avoidSendingInventoryPackets = false; } } }; } #endif //OPENMW_PROCESSORPLAYERUPDATEINVENTORY_HPP ================================================ FILE: apps/openmw/mwmp/processors/player/ProcessorPlayerItemUse.hpp ================================================ #ifndef OPENMW_PROCESSORPLAYERITEMUSE_HPP #define OPENMW_PROCESSORPLAYERITEMUSE_HPP #include "apps/openmw/mwbase/environment.hpp" #include "apps/openmw/mwgui/inventorywindow.hpp" #include "apps/openmw/mwgui/windowmanagerimp.hpp" #include "apps/openmw/mwworld/inventorystore.hpp" #include "apps/openmw/mwmp/MechanicsHelper.hpp" #include "../PlayerProcessor.hpp" namespace mwmp { class ProcessorPlayerItemUse final: public PlayerProcessor { public: ProcessorPlayerItemUse() { BPP_INIT(ID_PLAYER_ITEM_USE) } virtual void Do(PlayerPacket &packet, BasePlayer *player) { if (!isLocal()) return; LOG_MESSAGE_SIMPLE(TimedLog::LOG_INFO, "Received ID_PLAYER_ITEM_USE about LocalPlayer from server"); if (!isRequest()) { LOG_APPEND(TimedLog::LOG_INFO, "- refId: %s, count: %i, charge: %i, enchantmentCharge: %f, soul: %s", player->usedItem.refId.c_str(), player->usedItem.count, player->usedItem.charge, player->usedItem.enchantmentCharge, player->usedItem.soul.c_str()); MWWorld::Ptr playerPtr = MWBase::Environment::get().getWorld()->getPlayerPtr(); MWWorld::InventoryStore &inventoryStore = playerPtr.getClass().getInventoryStore(playerPtr); MWWorld::Ptr itemPtr = MechanicsHelper::getItemPtrFromStore(player->usedItem, inventoryStore); if (itemPtr) { MWBase::Environment::get().getWindowManager()->getInventoryWindow()->useItem(itemPtr); if (player->usingItemMagic) { MWWorld::ContainerStoreIterator storeIterator = inventoryStore.begin(); for (; storeIterator != inventoryStore.end(); ++storeIterator) { if (*storeIterator == itemPtr) break; } inventoryStore.setSelectedEnchantItem(storeIterator); } if (player->itemUseDrawState != MWMechanics::DrawState_Nothing) playerPtr.getClass().getNpcStats(playerPtr).setDrawState(static_cast(player->itemUseDrawState)); } else LOG_MESSAGE_SIMPLE(TimedLog::LOG_ERROR, "Cannot use non-existent item %s", player->usedItem.refId.c_str()); } } }; } #endif //OPENMW_PROCESSORPLAYERITEMUSE_HPP ================================================ FILE: apps/openmw/mwmp/processors/player/ProcessorPlayerJail.hpp ================================================ #ifndef OPENMW_PROCESSORPLAYERJAIL_HPP #define OPENMW_PROCESSORPLAYERJAIL_HPP #include "apps/openmw/mwbase/environment.hpp" #include "apps/openmw/mwgui/windowmanagerimp.hpp" #include "../PlayerProcessor.hpp" #include "apps/openmw/mwmp/Main.hpp" #include "apps/openmw/mwmp/Networking.hpp" namespace mwmp { class ProcessorPlayerJail final: public PlayerProcessor { public: ProcessorPlayerJail() { BPP_INIT(ID_PLAYER_JAIL) } virtual void Do(PlayerPacket &packet, BasePlayer *player) { LOG_MESSAGE_SIMPLE(TimedLog::LOG_INFO, "Received ID_PLAYER_JAIL from server"); if (isLocal()) { // Apply death penalties if (player->jailDays > 0) { MWBase::Environment::get().getWindowManager()->goToJail(player->jailDays); } } } }; } #endif //OPENMW_PROCESSORPLAYERJAIL_HPP ================================================ FILE: apps/openmw/mwmp/processors/player/ProcessorPlayerJournal.hpp ================================================ #ifndef OPENMW_PROCESSORPLAYERJOURNAL_HPP #define OPENMW_PROCESSORPLAYERJOURNAL_HPP #include "../PlayerProcessor.hpp" namespace mwmp { class ProcessorPlayerJournal final: public PlayerProcessor { public: ProcessorPlayerJournal() { BPP_INIT(ID_PLAYER_JOURNAL) } virtual void Do(PlayerPacket &packet, BasePlayer *player) { LOG_MESSAGE_SIMPLE(TimedLog::LOG_INFO, "Received ID_PLAYER_JOURNAL from server"); if (isRequest()) { // Entire journal cannot currently be requested from players } else if (player != 0) { static_cast(player)->addJournalItems(); } } }; } #endif //OPENMW_PROCESSORPLAYERJOURNAL_HPP ================================================ FILE: apps/openmw/mwmp/processors/player/ProcessorPlayerLevel.hpp ================================================ #ifndef OPENMW_PROCESSORPLAYERLEVEL_HPP #define OPENMW_PROCESSORPLAYERLEVEL_HPP #include "../PlayerProcessor.hpp" namespace mwmp { class ProcessorPlayerLevel final: public PlayerProcessor { public: ProcessorPlayerLevel() { BPP_INIT(ID_PLAYER_LEVEL) } virtual void Do(PlayerPacket &packet, BasePlayer *player) { if (isLocal()) { if (isRequest()) static_cast(player)->updateLevel(true); else static_cast(player)->setLevel(); } else if (player != 0) { MWWorld::Ptr ptrPlayer = static_cast(player)->getPtr(); MWMechanics::CreatureStats *ptrCreatureStats = &ptrPlayer.getClass().getCreatureStats(ptrPlayer); ptrCreatureStats->setLevel(player->creatureStats.mLevel); } } }; } #endif //OPENMW_PROCESSORPLAYERLEVEL_HPP ================================================ FILE: apps/openmw/mwmp/processors/player/ProcessorPlayerMiscellaneous.hpp ================================================ #ifndef OPENMW_PROCESSORPLAYERMISCELLANEOUS_HPP #define OPENMW_PROCESSORPLAYERMISCELLANEOUS_HPP #include "../PlayerProcessor.hpp" namespace mwmp { class ProcessorPlayerMiscellaneous final: public PlayerProcessor { public: ProcessorPlayerMiscellaneous() { BPP_INIT(ID_PLAYER_MISCELLANEOUS) } virtual void Do(PlayerPacket &packet, BasePlayer *player) { if (!isLocal()) return; LOG_MESSAGE_SIMPLE(TimedLog::LOG_INFO, "Received ID_PLAYER_MISCELLANEOUS about LocalPlayer from server"); if (!isRequest()) { LocalPlayer &localPlayer = static_cast(*player); if (player->miscellaneousChangeType == mwmp::MISCELLANEOUS_CHANGE_TYPE::MARK_LOCATION) localPlayer.setMarkLocation(); else if (player->miscellaneousChangeType == mwmp::MISCELLANEOUS_CHANGE_TYPE::SELECTED_SPELL) localPlayer.setSelectedSpell(); } } }; } #endif //OPENMW_PROCESSORPLAYERMISCELLANEOUS_HPP ================================================ FILE: apps/openmw/mwmp/processors/player/ProcessorPlayerMomentum.hpp ================================================ #ifndef OPENMW_PROCESSORPLAYERMOMENTUM_HPP #define OPENMW_PROCESSORPLAYERMOMENTUM_HPP #include "../PlayerProcessor.hpp" namespace mwmp { class ProcessorPlayerMomentum final: public PlayerProcessor { public: ProcessorPlayerMomentum() { BPP_INIT(ID_PLAYER_MOMENTUM) } virtual void Do(PlayerPacket &packet, BasePlayer *player) { if (!isLocal()) return; LOG_MESSAGE_SIMPLE(TimedLog::LOG_INFO, "Received ID_PLAYER_MOMENTUM about LocalPlayer from server"); if (!isRequest()) { LocalPlayer &localPlayer = static_cast(*player); localPlayer.setMomentum(); } } }; } #endif //OPENMW_PROCESSORPLAYERMOMENTUM_HPP ================================================ FILE: apps/openmw/mwmp/processors/player/ProcessorPlayerPosition.hpp ================================================ #ifndef OPENMW_PROCESSORPLAYERPOSITION_HPP #define OPENMW_PROCESSORPLAYERPOSITION_HPP #include "../PlayerProcessor.hpp" namespace mwmp { class ProcessorPlayerPosition final: public PlayerProcessor { public: ProcessorPlayerPosition() { BPP_INIT(ID_PLAYER_POSITION) } virtual void Do(PlayerPacket &packet, BasePlayer *player) { if (isLocal()) { if (!isRequest()) { LOG_MESSAGE_SIMPLE(TimedLog::LOG_INFO, "ID_PLAYER_POSITION changed by server"); static_cast(player)->setPosition(); } else static_cast(player)->updatePosition(true); } else if (player != 0) // dedicated player static_cast(player)->updateMarker(); } }; } #endif //OPENMW_PROCESSORPLAYERPOSITION_HPP ================================================ FILE: apps/openmw/mwmp/processors/player/ProcessorPlayerQuickKeys.hpp ================================================ #ifndef OPENMW_PROCESSORPLAYERQUICKKEYS_HPP #define OPENMW_PROCESSORPLAYERQUICKKEYS_HPP #include "../PlayerProcessor.hpp" namespace mwmp { class ProcessorPlayerQuickKeys final: public PlayerProcessor { public: ProcessorPlayerQuickKeys() { BPP_INIT(ID_PLAYER_QUICKKEYS) } virtual void Do(PlayerPacket &packet, BasePlayer *player) { if (!isLocal()) return; LOG_MESSAGE_SIMPLE(TimedLog::LOG_INFO, "Received ID_PLAYER_QUICKKEYS about LocalPlayer from server"); if (!isRequest()) { LocalPlayer &localPlayer = static_cast(*player); // Because we send PlayerQuickKeys packets from the same OpenMW methods that we use to set the // quick keys received, we need to set a boolean to prevent resending the keys set here localPlayer.isReceivingQuickKeys = true; localPlayer.setQuickKeys(); localPlayer.isReceivingQuickKeys = false; } } }; } #endif //OPENMW_PROCESSORPLAYERQUICKKEYS_HPP ================================================ FILE: apps/openmw/mwmp/processors/player/ProcessorPlayerReputation.hpp ================================================ #ifndef OPENMW_PROCESSORPLAYERREPUTATION_HPP #define OPENMW_PROCESSORPLAYERREPUTATION_HPP #include "../PlayerProcessor.hpp" namespace mwmp { class ProcessorPlayerReputation final: public PlayerProcessor { public: ProcessorPlayerReputation() { BPP_INIT(ID_PLAYER_REPUTATION) } virtual void Do(PlayerPacket &packet, BasePlayer *player) { if (isRequest()) { static_cast(player)->updateReputation(true); } else if (player != 0) { static_cast(player)->setReputation(); } } }; } #endif //OPENMW_PROCESSORPLAYERREPUTATION_HPP ================================================ FILE: apps/openmw/mwmp/processors/player/ProcessorPlayerRest.hpp ================================================ #ifndef OPENMW_PROCESSORPLAYERREST_HPP #define OPENMW_PROCESSORPLAYERREST_HPP #include "../PlayerProcessor.hpp" namespace mwmp { class ProcessorPlayerRest final: public PlayerProcessor { public: ProcessorPlayerRest() { BPP_INIT(ID_PLAYER_REST) } virtual void Do(PlayerPacket &packet, BasePlayer *player) { // Placeholder to be filled in later } }; } #endif //OPENMW_PROCESSORPLAYERREST_HPP ================================================ FILE: apps/openmw/mwmp/processors/player/ProcessorPlayerResurrect.hpp ================================================ #ifndef OPENMW_PROCESSORPLAYERRESURRECT_HPP #define OPENMW_PROCESSORPLAYERRESURRECT_HPP #include "../PlayerProcessor.hpp" #include "apps/openmw/mwmechanics/mechanicsmanagerimp.hpp" #include "apps/openmw/mwmp/Main.hpp" #include "apps/openmw/mwmp/Networking.hpp" namespace mwmp { class ProcessorPlayerResurrect final: public PlayerProcessor { public: ProcessorPlayerResurrect() { BPP_INIT(ID_PLAYER_RESURRECT) } virtual void Do(PlayerPacket &packet, BasePlayer *player) { LOG_MESSAGE_SIMPLE(TimedLog::LOG_INFO, "Received ID_PLAYER_RESURRECT from server"); if (isLocal()) { LOG_APPEND(TimedLog::LOG_INFO, "- Packet was about me with resurrectType of %i", player->resurrectType); static_cast(player)->resurrect(); } else if (player != 0) { LOG_APPEND(TimedLog::LOG_INFO, "- Packet was about %s", player->npc.mName.c_str()); static_cast(player)->resurrect(); } } }; } #endif //OPENMW_PROCESSORPLAYERRESURRECT_HPP ================================================ FILE: apps/openmw/mwmp/processors/player/ProcessorPlayerShapeshift.hpp ================================================ #ifndef OPENMW_PROCESSORPLAYERSHAPESHIFT_HPP #define OPENMW_PROCESSORPLAYERSHAPESHIFT_HPP #include "../PlayerProcessor.hpp" namespace mwmp { class ProcessorPlayerShapeshift final: public PlayerProcessor { public: ProcessorPlayerShapeshift() { BPP_INIT(ID_PLAYER_SHAPESHIFT) } virtual void Do(PlayerPacket &packet, BasePlayer *player) { LOG_MESSAGE_SIMPLE(TimedLog::LOG_INFO, "Received ID_PLAYER_SHAPESHIFT from server"); if (isLocal()) { LOG_APPEND(TimedLog::LOG_INFO, "- Packet was about LocalPlayer"); static_cast(player)->setShapeshift(); } else if (player != 0) { static_cast(player)->setShapeshift(); } } }; } #endif //OPENMW_PROCESSORPLAYERSHAPESHIFT_HPP ================================================ FILE: apps/openmw/mwmp/processors/player/ProcessorPlayerSkill.hpp ================================================ #ifndef OPENMW_PROCESSORPLAYERSKILL_HPP #define OPENMW_PROCESSORPLAYERSKILL_HPP #include "../PlayerProcessor.hpp" namespace mwmp { class ProcessorPlayerSkill final: public PlayerProcessor { public: ProcessorPlayerSkill() { BPP_INIT(ID_PLAYER_SKILL) } virtual void Do(PlayerPacket &packet, BasePlayer *player) { if (isLocal()) { if (isRequest()) static_cast(player)->updateSkills(true); else static_cast(player)->setSkills(); } else if (player != 0) { static_cast(player)->setSkills(); } } }; } #endif //OPENMW_PROCESSORPLAYERSKILL_HPP ================================================ FILE: apps/openmw/mwmp/processors/player/ProcessorPlayerSpeech.hpp ================================================ #ifndef OPENMW_PROCESSORPLAYERSPEECH_HPP #define OPENMW_PROCESSORPLAYERSPEECH_HPP #include "../PlayerProcessor.hpp" namespace mwmp { class ProcessorPlayerSpeech final: public PlayerProcessor { public: ProcessorPlayerSpeech() { BPP_INIT(ID_PLAYER_SPEECH) } virtual void Do(PlayerPacket &packet, BasePlayer *player) { if (isLocal()) { LOG_MESSAGE_SIMPLE(TimedLog::LOG_INFO, "Received ID_PLAYER_SPEECH about LocalPlayer from server"); static_cast(player)->playSpeech(); } else if (player != 0) static_cast(player)->playSpeech(); } }; } #endif //OPENMW_PROCESSORPLAYERSPEECH_HPP ================================================ FILE: apps/openmw/mwmp/processors/player/ProcessorPlayerSpellbook.hpp ================================================ #ifndef OPENMW_PROCESSORPLAYERSPELLBOOK_HPP #define OPENMW_PROCESSORPLAYERSPELLBOOK_HPP #include "../PlayerProcessor.hpp" namespace mwmp { class ProcessorPlayerSpellbook final: public PlayerProcessor { public: ProcessorPlayerSpellbook() { BPP_INIT(ID_PLAYER_SPELLBOOK) } virtual void Do(PlayerPacket &packet, BasePlayer *player) { if (!isLocal()) return; LOG_MESSAGE_SIMPLE(TimedLog::LOG_INFO, "Received ID_PLAYER_SPELLBOOK about LocalPlayer from server"); if (isRequest()) static_cast(player)->sendSpellbook(); else { LocalPlayer &localPlayer = static_cast(*player); int spellbookAction = localPlayer.spellbookChanges.action; if (spellbookAction == SpellbookChanges::ADD) localPlayer.addSpells(); else if (spellbookAction == SpellbookChanges::REMOVE) localPlayer.removeSpells(); else // SpellbookChanges::SET localPlayer.setSpellbook(); } } }; } #endif //OPENMW_PROCESSORPLAYERSPELLBOOK_HPP ================================================ FILE: apps/openmw/mwmp/processors/player/ProcessorPlayerSpellsActive.hpp ================================================ #ifndef OPENMW_PROCESSORPLAYERSPELLSACTIVE_HPP #define OPENMW_PROCESSORPLAYERSPELLSACTIVE_HPP #include "../PlayerProcessor.hpp" namespace mwmp { class ProcessorPlayerSpellsActive final: public PlayerProcessor { public: ProcessorPlayerSpellsActive() { BPP_INIT(ID_PLAYER_SPELLS_ACTIVE) } virtual void Do(PlayerPacket &packet, BasePlayer *player) { LOG_MESSAGE_SIMPLE(TimedLog::LOG_INFO, "Received ID_PLAYER_SPELLS_ACTIVE from server"); if (isLocal()) { LOG_APPEND(TimedLog::LOG_INFO, "- Packet was about me"); if (isRequest()) static_cast(player)->sendSpellsActive(); else { LocalPlayer& localPlayer = static_cast(*player); int spellsActiveAction = localPlayer.spellsActiveChanges.action; if (spellsActiveAction == SpellsActiveChanges::ADD) localPlayer.addSpellsActive(); else if (spellsActiveAction == SpellsActiveChanges::REMOVE) localPlayer.removeSpellsActive(); else // SpellsActiveChanges::SET localPlayer.setSpellsActive(); } } else if (player != 0) { LOG_APPEND(TimedLog::LOG_INFO, "- Packet was about %s", player->npc.mName.c_str()); DedicatedPlayer& dedicatedPlayer = static_cast(*player); int spellsActiveAction = dedicatedPlayer.spellsActiveChanges.action; if (spellsActiveAction == SpellsActiveChanges::ADD) dedicatedPlayer.addSpellsActive(); else if (spellsActiveAction == SpellsActiveChanges::REMOVE) dedicatedPlayer.removeSpellsActive(); else // SpellsActiveChanges::SET dedicatedPlayer.setSpellsActive(); } } }; } #endif //OPENMW_PROCESSORPLAYERSPELLSACTIVE_HPP ================================================ FILE: apps/openmw/mwmp/processors/player/ProcessorPlayerStatsDynamic.hpp ================================================ #ifndef OPENMW_PROCESSORPLAYERSTATSDYNAMIC_HPP #define OPENMW_PROCESSORPLAYERSTATSDYNAMIC_HPP #include "../PlayerProcessor.hpp" namespace mwmp { class ProcessorPlayerStatsDynamic final: public PlayerProcessor { public: ProcessorPlayerStatsDynamic() { BPP_INIT(ID_PLAYER_STATS_DYNAMIC) } virtual void Do(PlayerPacket &packet, BasePlayer *player) { if (isLocal()) { if (isRequest()) static_cast(player)->updateStatsDynamic(true); else static_cast(player)->setDynamicStats(); } else if (player != 0) { static_cast(player)->setStatsDynamic(); } } }; } #endif //OPENMW_PROCESSORPLAYERSTATSDYNAMIC_HPP ================================================ FILE: apps/openmw/mwmp/processors/player/ProcessorPlayerTopic.hpp ================================================ #ifndef OPENMW_PROCESSORPLAYERTOPIC_HPP #define OPENMW_PROCESSORPLAYERTOPIC_HPP #include "../PlayerProcessor.hpp" namespace mwmp { class ProcessorPlayerTopic final: public PlayerProcessor { public: ProcessorPlayerTopic() { BPP_INIT(ID_PLAYER_TOPIC) } virtual void Do(PlayerPacket &packet, BasePlayer *player) { if (isRequest()) { // Entire list of topics cannot currently be requested from players } else if (player != 0) { static_cast(player)->addTopics(); } } }; } #endif //OPENMW_PROCESSORPLAYERTOPIC_HPP ================================================ FILE: apps/openmw/mwmp/processors/player/ProcessorUserDisconnected.hpp ================================================ #ifndef OPENMW_PROCESSORUSERDISCONNECTED_HPP #define OPENMW_PROCESSORUSERDISCONNECTED_HPP #include "../PlayerProcessor.hpp" #include #include #include "apps/openmw/mwstate/statemanagerimp.hpp" namespace mwmp { class ProcessorUserDisconnected final: public PlayerProcessor { public: ProcessorUserDisconnected() { BPP_INIT(ID_USER_DISCONNECTED) avoidReading = true; } virtual void Do(PlayerPacket &packet, BasePlayer *player) { if (isLocal()) MWBase::Environment::get().getStateManager()->requestQuit(); else if (player != 0) { mwmp::LocalPlayer *localPlayer = mwmp::Main::get().getLocalPlayer(); for (std::vector::iterator iter = localPlayer->alliedPlayers.begin(); iter != localPlayer->alliedPlayers.end(); ) { if (*iter == guid) { DedicatedPlayer *dedicatedPlayer = PlayerList::getPlayer(guid); LOG_APPEND(TimedLog::LOG_INFO, "- Deleting %s from our allied players", dedicatedPlayer->npc.mName.c_str()); iter = localPlayer->alliedPlayers.erase(iter); } else ++iter; } PlayerList::deletePlayer(guid); } } }; } #endif //OPENMW_PROCESSORUSERDISCONNECTED_HPP ================================================ FILE: apps/openmw/mwmp/processors/system/ProcessorSystemHandshake.hpp ================================================ #ifndef OPENMW_PROCESSORSYSTEMHANDSHAKE_HPP #define OPENMW_PROCESSORSYSTEMHANDSHAKE_HPP #include #include "apps/openmw/mwmp/Main.hpp" #include "../SystemProcessor.hpp" namespace mwmp { class ProcessorSystemHandshake final: public SystemProcessor { public: ProcessorSystemHandshake() { BPP_INIT(ID_SYSTEM_HANDSHAKE) } virtual void Do(SystemPacket &packet, BaseSystem *system) { packet.setSystem(Main::get().getLocalSystem()); packet.Send(serverAddr); } }; } #endif //OPENMW_PROCESSORSYSTEMHANDSHAKE_HPP ================================================ FILE: apps/openmw/mwmp/processors/worldstate/ProcessorCellReset.hpp ================================================ #ifndef OPENMW_PROCESSORCELLRESET_HPP #define OPENMW_PROCESSORCELLRESET_HPP #include "../WorldstateProcessor.hpp" #include namespace mwmp { class ProcessorCellReset final: public WorldstateProcessor { public: ProcessorCellReset() { BPP_INIT(ID_CELL_RESET) } virtual void Do(WorldstatePacket &packet, Worldstate &worldstate) { LOG_MESSAGE_SIMPLE(TimedLog::LOG_INFO, "Received ID_CELL_RESET"); CellController* cellController = Main::get().getCellController(); mwmp::Main::get().getNetworking()->getWorldstate()->resetCells(&worldstate.cellsToReset); } }; } #endif //OPENMW_PROCESSORCELLRESET_HPP ================================================ FILE: apps/openmw/mwmp/processors/worldstate/ProcessorClientScriptGlobal.hpp ================================================ #ifndef OPENMW_PROCESSORCLIENTSCRIPTGLOBAL_HPP #define OPENMW_PROCESSORCLIENTSCRIPTGLOBAL_HPP #include "../WorldstateProcessor.hpp" namespace mwmp { class ProcessorClientScriptGlobal final: public WorldstateProcessor { public: ProcessorClientScriptGlobal() { BPP_INIT(ID_CLIENT_SCRIPT_GLOBAL) } virtual void Do(WorldstatePacket &packet, Worldstate &worldstate) { mwmp::Main::get().getNetworking()->getWorldstate()->setClientGlobals(); } }; } #endif //OPENMW_PROCESSORCLIENTSCRIPTGLOBAL_HPP ================================================ FILE: apps/openmw/mwmp/processors/worldstate/ProcessorClientScriptSettings.hpp ================================================ #ifndef OPENMW_PROCESSORCLIENTSCRIPTSETTINGS_HPP #define OPENMW_PROCESSORCLIENTSCRIPTSETTINGS_HPP #include #include #include "../WorldstateProcessor.hpp" namespace mwmp { class ProcessorClientScriptSettings final: public WorldstateProcessor { public: ProcessorClientScriptSettings() { BPP_INIT(ID_CLIENT_SCRIPT_SETTINGS) } virtual void Do(WorldstatePacket &packet, Worldstate &worldstate) { LOG_MESSAGE_SIMPLE(TimedLog::LOG_INFO, "Received ID_CLIENT_SCRIPT_SETTINGS making us send packets for the following globals:"); std::string debugMessage = ""; for (const auto &globalId : worldstate.synchronizedClientGlobalIds) { if (TimedLog::GetLevel() <= TimedLog::LOG_INFO) { if (!debugMessage.empty()) debugMessage += ", "; debugMessage += globalId; } } LOG_APPEND(TimedLog::LOG_INFO, "- %s", debugMessage.c_str()); } }; } #endif //OPENMW_PROCESSORCLIENTSCRIPTSETTINGS_HPP ================================================ FILE: apps/openmw/mwmp/processors/worldstate/ProcessorRecordDynamic.hpp ================================================ #ifndef OPENMW_PROCESSORRECORDDYNAMIC_HPP #define OPENMW_PROCESSORRECORDDYNAMIC_HPP #include "../WorldstateProcessor.hpp" namespace mwmp { class ProcessorRecordDynamic final: public WorldstateProcessor { public: ProcessorRecordDynamic() { BPP_INIT(ID_RECORD_DYNAMIC) } virtual void Do(WorldstatePacket &packet, Worldstate &worldstate) { worldstate.addRecords(); } }; } #endif //OPENMW_PROCESSORRECORDDYNAMIC_HPP ================================================ FILE: apps/openmw/mwmp/processors/worldstate/ProcessorWorldCollisionOverride.hpp ================================================ #ifndef OPENMW_PROCESSORWORLDCOLLISIONOVERRIDE_HPP #define OPENMW_PROCESSORWORLDCOLLISIONOVERRIDE_HPP #include #include #include "../WorldstateProcessor.hpp" namespace mwmp { class ProcessorWorldCollisionOverride final: public WorldstateProcessor { public: ProcessorWorldCollisionOverride() { BPP_INIT(ID_WORLD_COLLISION_OVERRIDE) } virtual void Do(WorldstatePacket &packet, Worldstate &worldstate) { // Placeholder } }; } #endif //OPENMW_PROCESSORWORLDCOLLISIONOVERRIDE_HPP ================================================ FILE: apps/openmw/mwmp/processors/worldstate/ProcessorWorldDestinationOverride.hpp ================================================ #ifndef OPENMW_PROCESSORWORLDDESTINATIONOVERRIDE_HPP #define OPENMW_PROCESSORWORLDDESTINATIONOVERRIDE_HPP #include #include #include "../WorldstateProcessor.hpp" namespace mwmp { class ProcessorWorldDestinationOverride final: public WorldstateProcessor { public: ProcessorWorldDestinationOverride() { BPP_INIT(ID_WORLD_DESTINATION_OVERRIDE) } virtual void Do(WorldstatePacket &packet, Worldstate &worldstate) { LOG_MESSAGE_SIMPLE(TimedLog::LOG_INFO, "Received ID_WORLD_DESTINATION_OVERRIDE with the following overrides:"); for (auto iterator : worldstate.destinationOverrides) { LOG_APPEND(TimedLog::LOG_INFO, "- %s now leads to %s", iterator.first.c_str(), iterator.second.c_str()); } } }; } #endif //OPENMW_PROCESSORWORLDDESTINATIONOVERRIDE_HPP ================================================ FILE: apps/openmw/mwmp/processors/worldstate/ProcessorWorldKillCount.hpp ================================================ #ifndef OPENMW_PROCESSORWORLDKILLCOUNT_HPP #define OPENMW_PROCESSORWORLDKILLCOUNT_HPP #include "../WorldstateProcessor.hpp" namespace mwmp { class ProcessorWorldKillCount final: public WorldstateProcessor { public: ProcessorWorldKillCount() { BPP_INIT(ID_WORLD_KILL_COUNT) } virtual void Do(WorldstatePacket &packet, Worldstate &worldstate) { mwmp::Main::get().getNetworking()->getWorldstate()->setKills(); } }; } #endif //OPENMW_PROCESSORWORLDKILLCOUNT_HPP ================================================ FILE: apps/openmw/mwmp/processors/worldstate/ProcessorWorldMap.hpp ================================================ #ifndef OPENMW_PROCESSORWORLDMAP_HPP #define OPENMW_PROCESSORWORLDMAP_HPP #include "../WorldstateProcessor.hpp" namespace mwmp { class ProcessorWorldMap final: public WorldstateProcessor { public: ProcessorWorldMap() { BPP_INIT(ID_WORLD_MAP) } virtual void Do(WorldstatePacket &packet, Worldstate &worldstate) { worldstate.setMapExplored(); } }; } #endif //OPENMW_PROCESSORWORLDMAP_HPP ================================================ FILE: apps/openmw/mwmp/processors/worldstate/ProcessorWorldRegionAuthority.hpp ================================================ #ifndef OPENMW_PROCESSORWORLDREGIONAUTHORITY_HPP #define OPENMW_PROCESSORWORLDREGIONAUTHORITY_HPP #include #include "../PlayerProcessor.hpp" namespace mwmp { class ProcessorWorldRegionAuthority final: public WorldstateProcessor { public: ProcessorWorldRegionAuthority() { BPP_INIT(ID_WORLD_REGION_AUTHORITY) } virtual void Do(WorldstatePacket &packet, Worldstate &worldstate) { MWBase::World *world = MWBase::Environment::get().getWorld(); if (!worldstate.authorityRegion.empty() && Misc::StringUtils::ciEqual(worldstate.authorityRegion, world->getPlayerPtr().getCell()->getCell()->mRegion)) { LOG_MESSAGE_SIMPLE(TimedLog::LOG_INFO, "Received %s about %s", strPacketID.c_str(), worldstate.authorityRegion.c_str()); if (isLocal()) { LOG_APPEND(TimedLog::LOG_INFO, "- The new region authority is me"); // There's a chance we've been made the region authority right after a teleportation that hasn't // been registered in the WeatherManager yet, so make sure we update it world->updateWeather(0); world->setWeatherCreationState(true); world->sendWeather(); } else { BasePlayer *player = PlayerList::getPlayer(guid); if (player != 0) LOG_APPEND(TimedLog::LOG_INFO, "- The new region authority is %s", player->npc.mName.c_str()); world->setWeatherCreationState(false); } } } }; } #endif //OPENMW_PROCESSORWORLDREGIONAUTHORITY_HPP ================================================ FILE: apps/openmw/mwmp/processors/worldstate/ProcessorWorldTime.hpp ================================================ #ifndef OPENMW_PROCESSORWORLDTIME_HPP #define OPENMW_PROCESSORWORLDTIME_HPP #include #include #include "../WorldstateProcessor.hpp" namespace mwmp { class ProcessorWorldTime final: public WorldstateProcessor { public: ProcessorWorldTime() { BPP_INIT(ID_WORLD_TIME) } virtual void Do(WorldstatePacket &packet, Worldstate &worldstate) { MWBase::World *world = MWBase::Environment::get().getWorld(); if (worldstate.time.hour != -1) world->setGlobalFloat("gamehour", worldstate.time.hour); if (worldstate.time.day != -1) world->setGlobalInt("day", worldstate.time.day); if (worldstate.time.month != -1) world->setGlobalInt("month", worldstate.time.month); if (worldstate.time.year != -1) world->setGlobalInt("year", worldstate.time.year); if (worldstate.time.timeScale != -1) world->setGlobalFloat("timescale", worldstate.time.timeScale); if (worldstate.time.daysPassed != -1) world->setGlobalInt("dayspassed", worldstate.time.daysPassed); } }; } #endif //OPENMW_PROCESSORWORLDTIME_HPP ================================================ FILE: apps/openmw/mwmp/processors/worldstate/ProcessorWorldWeather.hpp ================================================ #ifndef OPENMW_PROCESSORWORLDWEATHER_HPP #define OPENMW_PROCESSORWORLDWEATHER_HPP #include "../PlayerProcessor.hpp" namespace mwmp { class ProcessorWorldWeather final: public WorldstateProcessor { public: ProcessorWorldWeather() { BPP_INIT(ID_WORLD_WEATHER) } virtual void Do(WorldstatePacket &packet, Worldstate &worldstate) { worldstate.setWeather(); } }; } #endif //OPENMW_PROCESSORWORLDWEATHER_HPP ================================================ FILE: apps/openmw/mwphysics/actor.cpp ================================================ #include "actor.hpp" /* Start of tes3mp addition Include additional headers for multiplayer purposes */ #include #include "../mwmp/Main.hpp" #include "../mwmp/Networking.hpp" #include "../mwmp/PlayerList.hpp" /* End of tes3mp addition */ #include #include #include #include #include #include #include "../mwworld/class.hpp" #include "collisiontype.hpp" #include "mtphysics.hpp" #include namespace MWPhysics { Actor::Actor(const MWWorld::Ptr& ptr, const Resource::BulletShape* shape, PhysicsTaskScheduler* scheduler, bool canWaterWalk) : mStandingOnPtr(nullptr), mCanWaterWalk(canWaterWalk), mWalkingOnWater(false) , mCollisionObject(nullptr), mMeshTranslation(shape->mCollisionBox.center), mHalfExtents(shape->mCollisionBox.extents) , mVelocity(0,0,0), mStuckFrames(0), mLastStuckPosition{0, 0, 0} , mForce(0.f, 0.f, 0.f), mOnGround(true), mOnSlope(false) , mInternalCollisionMode(true) , mExternalCollisionMode(true) , mTaskScheduler(scheduler) { mPtr = ptr; // We can not create actor without collisions - he will fall through the ground. // In this case we should autogenerate collision box based on mesh shape // (NPCs have bodyparts and use a different approach) if (!ptr.getClass().isNpc() && mHalfExtents.length2() == 0.f) { if (shape->mCollisionShape) { btTransform transform; transform.setIdentity(); btVector3 min; btVector3 max; shape->mCollisionShape->getAabb(transform, min, max); mHalfExtents.x() = (max[0] - min[0])/2.f; mHalfExtents.y() = (max[1] - min[1])/2.f; mHalfExtents.z() = (max[2] - min[2])/2.f; mMeshTranslation = osg::Vec3f(0.f, 0.f, mHalfExtents.z()); } if (mHalfExtents.length2() == 0.f) Log(Debug::Error) << "Error: Failed to calculate bounding box for actor \"" << ptr.getCellRef().getRefId() << "\"."; } mShape.reset(new btBoxShape(Misc::Convert::toBullet(mHalfExtents))); mRotationallyInvariant = (mMeshTranslation.x() == 0.0 && mMeshTranslation.y() == 0.0) && std::fabs(mHalfExtents.x() - mHalfExtents.y()) < 2.2; mConvexShape = static_cast(mShape.get()); mCollisionObject = std::make_unique(); mCollisionObject->setCollisionFlags(btCollisionObject::CF_KINEMATIC_OBJECT); mCollisionObject->setActivationState(DISABLE_DEACTIVATION); mCollisionObject->setCollisionShape(mShape.get()); mCollisionObject->setUserPointer(this); updateScale(); if(!mRotationallyInvariant) updateRotation(); updatePosition(); addCollisionMask(getCollisionMask()); /* Start of tes3mp addition Make it possible to disable collision for players or regular actors from a packet */ mwmp::BaseWorldstate *worldstate = mwmp::Main::get().getNetworking()->getWorldstate(); if (mwmp::PlayerList::isDedicatedPlayer(ptr)) { if (!worldstate->hasPlayerCollision) enableCollisionBody(false); } else { if (!worldstate->hasActorCollision) enableCollisionBody(false); } /* End of tes3mp addition */ updateCollisionObjectPosition(); } Actor::~Actor() { mTaskScheduler->removeCollisionObject(mCollisionObject.get()); } void Actor::enableCollisionMode(bool collision) { mInternalCollisionMode.store(collision, std::memory_order_release); } void Actor::enableCollisionBody(bool collision) { if (mExternalCollisionMode != collision) { mExternalCollisionMode = collision; updateCollisionMask(); } } void Actor::addCollisionMask(int collisionMask) { mTaskScheduler->addCollisionObject(mCollisionObject.get(), CollisionType_Actor, collisionMask); } void Actor::updateCollisionMask() { mTaskScheduler->setCollisionFilterMask(mCollisionObject.get(), getCollisionMask()); } int Actor::getCollisionMask() const { int collisionMask = CollisionType_World | CollisionType_HeightMap; if (mExternalCollisionMode) collisionMask |= CollisionType_Actor | CollisionType_Projectile | CollisionType_Door; if (mCanWaterWalk) collisionMask |= CollisionType_Water; return collisionMask; } void Actor::updatePosition() { std::scoped_lock lock(mPositionMutex); const auto worldPosition = mPtr.getRefData().getPosition().asVec3(); mPreviousPosition = worldPosition; mPosition = worldPosition; mSimulationPosition = worldPosition; mPositionOffset = osg::Vec3f(); mStandingOnPtr = nullptr; mSkipCollisions = true; mSkipSimulation = true; } void Actor::setSimulationPosition(const osg::Vec3f& position) { if (!std::exchange(mSkipSimulation, false)) mSimulationPosition = position; } osg::Vec3f Actor::getSimulationPosition() const { return mSimulationPosition; } osg::Vec3f Actor::getScaledMeshTranslation() const { return mRotation * osg::componentMultiply(mMeshTranslation, mScale); } void Actor::updateCollisionObjectPosition() { std::scoped_lock lock(mPositionMutex); mShape->setLocalScaling(Misc::Convert::toBullet(mScale)); osg::Vec3f newPosition = getScaledMeshTranslation() + mPosition; auto& trans = mCollisionObject->getWorldTransform(); trans.setOrigin(Misc::Convert::toBullet(newPosition)); trans.setRotation(Misc::Convert::toBullet(mRotation)); mCollisionObject->setWorldTransform(trans); mWorldPositionChanged = false; } osg::Vec3f Actor::getCollisionObjectPosition() const { std::scoped_lock lock(mPositionMutex); return getScaledMeshTranslation() + mPosition; } bool Actor::setPosition(const osg::Vec3f& position) { std::scoped_lock lock(mPositionMutex); applyOffsetChange(); bool hasChanged = mPosition != position || mWorldPositionChanged; mPreviousPosition = mPosition; mPosition = position; return hasChanged; } void Actor::adjustPosition(const osg::Vec3f& offset, bool ignoreCollisions) { std::scoped_lock lock(mPositionMutex); mPositionOffset += offset; mSkipCollisions = mSkipCollisions || ignoreCollisions; } void Actor::applyOffsetChange() { if (mPositionOffset.length() == 0) return; mPosition += mPositionOffset; mPreviousPosition += mPositionOffset; mSimulationPosition += mPositionOffset; mPositionOffset = osg::Vec3f(); mWorldPositionChanged = true; } osg::Vec3f Actor::getPosition() const { return mPosition; } osg::Vec3f Actor::getPreviousPosition() const { return mPreviousPosition; } void Actor::updateRotation () { std::scoped_lock lock(mPositionMutex); mRotation = mPtr.getRefData().getBaseNode()->getAttitude(); } bool Actor::isRotationallyInvariant() const { return mRotationallyInvariant; } void Actor::updateScale() { std::scoped_lock lock(mPositionMutex); float scale = mPtr.getCellRef().getScale(); osg::Vec3f scaleVec(scale,scale,scale); mPtr.getClass().adjustScale(mPtr, scaleVec, false); mScale = scaleVec; scaleVec = osg::Vec3f(scale,scale,scale); mPtr.getClass().adjustScale(mPtr, scaleVec, true); mRenderingScale = scaleVec; } osg::Vec3f Actor::getHalfExtents() const { std::scoped_lock lock(mPositionMutex); return osg::componentMultiply(mHalfExtents, mScale); } osg::Vec3f Actor::getOriginalHalfExtents() const { return mHalfExtents; } osg::Vec3f Actor::getRenderingHalfExtents() const { std::scoped_lock lock(mPositionMutex); return osg::componentMultiply(mHalfExtents, mRenderingScale); } void Actor::setInertialForce(const osg::Vec3f &force) { mForce = force; } void Actor::setOnGround(bool grounded) { mOnGround.store(grounded, std::memory_order_release); } void Actor::setOnSlope(bool slope) { mOnSlope.store(slope, std::memory_order_release); } bool Actor::isWalkingOnWater() const { return mWalkingOnWater.load(std::memory_order_acquire); } void Actor::setWalkingOnWater(bool walkingOnWater) { mWalkingOnWater.store(walkingOnWater, std::memory_order_release); } void Actor::setCanWaterWalk(bool waterWalk) { if (waterWalk != mCanWaterWalk) { mCanWaterWalk = waterWalk; updateCollisionMask(); } } MWWorld::Ptr Actor::getStandingOnPtr() const { std::scoped_lock lock(mPositionMutex); return mStandingOnPtr; } void Actor::setStandingOnPtr(const MWWorld::Ptr& ptr) { std::scoped_lock lock(mPositionMutex); mStandingOnPtr = ptr; } bool Actor::skipCollisions() { return std::exchange(mSkipCollisions, false); } void Actor::setVelocity(osg::Vec3f velocity) { mVelocity = velocity; } osg::Vec3f Actor::velocity() { return std::exchange(mVelocity, osg::Vec3f()); } } ================================================ FILE: apps/openmw/mwphysics/actor.hpp ================================================ #ifndef OPENMW_MWPHYSICS_ACTOR_H #define OPENMW_MWPHYSICS_ACTOR_H #include #include #include #include "ptrholder.hpp" #include #include #include class btCollisionShape; class btCollisionObject; class btConvexShape; namespace Resource { class BulletShape; } namespace MWPhysics { class PhysicsTaskScheduler; class Actor final : public PtrHolder { public: Actor(const MWWorld::Ptr& ptr, const Resource::BulletShape* shape, PhysicsTaskScheduler* scheduler, bool canWaterWalk); ~Actor() override; /** * Sets the collisionMode for this actor. If disabled, the actor can fly and clip geometry. */ void enableCollisionMode(bool collision); bool getCollisionMode() const { return mInternalCollisionMode.load(std::memory_order_acquire); } btConvexShape* getConvexShape() const { return mConvexShape; } /** * Enables or disables the *external* collision body. If disabled, other actors will not collide with this actor. */ void enableCollisionBody(bool collision); void updateScale(); void updateRotation(); /** * Return true if the collision shape looks the same no matter how its Z rotated. */ bool isRotationallyInvariant() const; /** * Used by the physics simulation to store the simulation result. Used in conjunction with mWorldPosition * to account for e.g. scripted movements */ void setSimulationPosition(const osg::Vec3f& position); osg::Vec3f getSimulationPosition() const; void updateCollisionObjectPosition(); /** * Returns the half extents of the collision body (scaled according to collision scale) */ osg::Vec3f getHalfExtents() const; /** * Returns the half extents of the collision body (not scaled) */ osg::Vec3f getOriginalHalfExtents() const; /** * Returns the position of the collision body * @note The collision shape's origin is in its center, so the position returned can be described as center of the actor collision box in world space. */ osg::Vec3f getCollisionObjectPosition() const; /** * Store the current position into mPreviousPosition, then move to this position. * Returns true if the new position is different. */ bool setPosition(const osg::Vec3f& position); // force set actor position to be as in Ptr::RefData void updatePosition(); // register a position offset that will be applied during simulation. void adjustPosition(const osg::Vec3f& offset, bool ignoreCollisions); // apply position offset. Can't be called during simulation void applyOffsetChange(); osg::Vec3f getPosition() const; osg::Vec3f getPreviousPosition() const; /** * Returns the half extents of the collision body (scaled according to rendering scale) * @note The reason we need this extra method is because of an inconsistency in MW - NPC race scales aren't applied to the collision shape, * most likely to make environment collision testing easier. However in some cases (swimming level) we want the actual scale. */ osg::Vec3f getRenderingHalfExtents() const; /** * Sets the current amount of inertial force (incl. gravity) affecting this physic actor */ void setInertialForce(const osg::Vec3f &force); /** * Gets the current amount of inertial force (incl. gravity) affecting this physic actor */ const osg::Vec3f &getInertialForce() const { return mForce; } void setOnGround(bool grounded); bool getOnGround() const { return mInternalCollisionMode.load(std::memory_order_acquire) && mOnGround.load(std::memory_order_acquire); } void setOnSlope(bool slope); bool getOnSlope() const { return mInternalCollisionMode.load(std::memory_order_acquire) && mOnSlope.load(std::memory_order_acquire); } btCollisionObject* getCollisionObject() const { return mCollisionObject.get(); } /// Sets whether this actor should be able to collide with the water surface void setCanWaterWalk(bool waterWalk); /// Sets whether this actor has been walking on the water surface in the last frame void setWalkingOnWater(bool walkingOnWater); bool isWalkingOnWater() const; MWWorld::Ptr getStandingOnPtr() const; void setStandingOnPtr(const MWWorld::Ptr& ptr); unsigned int getStuckFrames() const { return mStuckFrames; } void setStuckFrames(unsigned int frames) { mStuckFrames = frames; } const osg::Vec3f &getLastStuckPosition() const { return mLastStuckPosition; } void setLastStuckPosition(osg::Vec3f position) { mLastStuckPosition = position; } bool skipCollisions(); void setVelocity(osg::Vec3f velocity); osg::Vec3f velocity(); private: MWWorld::Ptr mStandingOnPtr; /// Removes then re-adds the collision object to the dynamics world void updateCollisionMask(); void addCollisionMask(int collisionMask); int getCollisionMask() const; /// Returns the mesh translation, scaled and rotated as necessary osg::Vec3f getScaledMeshTranslation() const; bool mCanWaterWalk; std::atomic mWalkingOnWater; bool mRotationallyInvariant; std::unique_ptr mShape; btConvexShape* mConvexShape; std::unique_ptr mCollisionObject; osg::Vec3f mMeshTranslation; osg::Vec3f mHalfExtents; osg::Quat mRotation; osg::Vec3f mScale; osg::Vec3f mRenderingScale; osg::Vec3f mSimulationPosition; osg::Vec3f mPosition; osg::Vec3f mPreviousPosition; osg::Vec3f mPositionOffset; osg::Vec3f mVelocity; bool mWorldPositionChanged; bool mSkipCollisions; bool mSkipSimulation; mutable std::mutex mPositionMutex; unsigned int mStuckFrames; osg::Vec3f mLastStuckPosition; osg::Vec3f mForce; std::atomic mOnGround; std::atomic mOnSlope; std::atomic mInternalCollisionMode; bool mExternalCollisionMode; PhysicsTaskScheduler* mTaskScheduler; Actor(const Actor&); Actor& operator=(const Actor&); }; } #endif ================================================ FILE: apps/openmw/mwphysics/actorconvexcallback.cpp ================================================ #include "actorconvexcallback.hpp" #include "collisiontype.hpp" #include "contacttestwrapper.h" #include #include #include "projectile.hpp" namespace MWPhysics { class ActorOverlapTester : public btCollisionWorld::ContactResultCallback { public: bool overlapping = false; btScalar addSingleResult(btManifoldPoint& cp, const btCollisionObjectWrapper* colObj0Wrap, int partId0, int index0, const btCollisionObjectWrapper* colObj1Wrap, int partId1, int index1) override { if(cp.getDistance() <= 0.0f) overlapping = true; return btScalar(1); } }; ActorConvexCallback::ActorConvexCallback(const btCollisionObject *me, const btVector3 &motion, btScalar minCollisionDot, const btCollisionWorld * world) : btCollisionWorld::ClosestConvexResultCallback(btVector3(0.0, 0.0, 0.0), btVector3(0.0, 0.0, 0.0)), mMe(me), mMotion(motion), mMinCollisionDot(minCollisionDot), mWorld(world) { } btScalar ActorConvexCallback::addSingleResult(btCollisionWorld::LocalConvexResult& convexResult, bool normalInWorldSpace) { if (convexResult.m_hitCollisionObject == mMe) return btScalar(1); // override data for actor-actor collisions // vanilla Morrowind seems to make overlapping actors collide as though they are both cylinders with a diameter of the distance between them // For some reason this doesn't work as well as it should when using capsules, but it still helps a lot. if(convexResult.m_hitCollisionObject->getBroadphaseHandle()->m_collisionFilterGroup == CollisionType_Actor) { ActorOverlapTester isOverlapping; // FIXME: This is absolutely terrible and bullet should feel terrible for not making contactPairTest const-correct. ContactTestWrapper::contactPairTest(const_cast(mWorld), const_cast(mMe), const_cast(convexResult.m_hitCollisionObject), isOverlapping); if(isOverlapping.overlapping) { auto originA = Misc::Convert::toOsg(mMe->getWorldTransform().getOrigin()); auto originB = Misc::Convert::toOsg(convexResult.m_hitCollisionObject->getWorldTransform().getOrigin()); osg::Vec3f motion = Misc::Convert::toOsg(mMotion); osg::Vec3f normal = (originA-originB); normal.z() = 0; normal.normalize(); // only collide if horizontally moving towards the hit actor (note: the motion vector appears to be inverted) // FIXME: This kinda screws with standing on actors that walk up slopes for some reason. Makes you fall through them. // It happens in vanilla Morrowind too, but much less often. // I tried hunting down why but couldn't figure it out. Possibly a stair stepping or ground ejection bug. if(normal * motion > 0.0f) { convexResult.m_hitFraction = 0.0f; convexResult.m_hitNormalLocal = Misc::Convert::toBullet(normal); return ClosestConvexResultCallback::addSingleResult(convexResult, true); } else { return btScalar(1); } } } if (convexResult.m_hitCollisionObject->getBroadphaseHandle()->m_collisionFilterGroup == CollisionType_Projectile) { auto* projectileHolder = static_cast(convexResult.m_hitCollisionObject->getUserPointer()); if (!projectileHolder->isActive()) return btScalar(1); auto* targetHolder = static_cast(mMe->getUserPointer()); const MWWorld::Ptr target = targetHolder->getPtr(); if (projectileHolder->isValidTarget(target)) projectileHolder->hit(target, convexResult.m_hitPointLocal, convexResult.m_hitNormalLocal); return btScalar(1); } btVector3 hitNormalWorld; if (normalInWorldSpace) hitNormalWorld = convexResult.m_hitNormalLocal; else { ///need to transform normal into worldspace hitNormalWorld = convexResult.m_hitCollisionObject->getWorldTransform().getBasis()*convexResult.m_hitNormalLocal; } // dot product of the motion vector against the collision contact normal btScalar dotCollision = mMotion.dot(hitNormalWorld); if (dotCollision <= mMinCollisionDot) return btScalar(1); return ClosestConvexResultCallback::addSingleResult(convexResult, normalInWorldSpace); } } ================================================ FILE: apps/openmw/mwphysics/actorconvexcallback.hpp ================================================ #ifndef OPENMW_MWPHYSICS_ACTORCONVEXCALLBACK_H #define OPENMW_MWPHYSICS_ACTORCONVEXCALLBACK_H #include class btCollisionObject; namespace MWPhysics { class ActorConvexCallback : public btCollisionWorld::ClosestConvexResultCallback { public: ActorConvexCallback(const btCollisionObject *me, const btVector3 &motion, btScalar minCollisionDot, const btCollisionWorld * world); btScalar addSingleResult(btCollisionWorld::LocalConvexResult& convexResult,bool normalInWorldSpace) override; protected: const btCollisionObject *mMe; const btVector3 mMotion; const btScalar mMinCollisionDot; const btCollisionWorld * mWorld; }; } #endif ================================================ FILE: apps/openmw/mwphysics/closestnotmerayresultcallback.cpp ================================================ #include "closestnotmerayresultcallback.hpp" #include #include #include #include "../mwworld/class.hpp" #include "ptrholder.hpp" namespace MWPhysics { ClosestNotMeRayResultCallback::ClosestNotMeRayResultCallback(const btCollisionObject* me, std::vector targets, const btVector3& from, const btVector3& to) : btCollisionWorld::ClosestRayResultCallback(from, to) , mMe(me), mTargets(std::move(targets)) { } btScalar ClosestNotMeRayResultCallback::addSingleResult(btCollisionWorld::LocalRayResult& rayResult, bool normalInWorldSpace) { if (rayResult.m_collisionObject == mMe) return 1.f; if (!mTargets.empty()) { if ((std::find(mTargets.begin(), mTargets.end(), rayResult.m_collisionObject) == mTargets.end())) { auto* holder = static_cast(rayResult.m_collisionObject->getUserPointer()); if (holder && !holder->getPtr().isEmpty() && holder->getPtr().getClass().isActor()) return 1.f; } } return btCollisionWorld::ClosestRayResultCallback::addSingleResult(rayResult, normalInWorldSpace); } } ================================================ FILE: apps/openmw/mwphysics/closestnotmerayresultcallback.hpp ================================================ #ifndef OPENMW_MWPHYSICS_CLOSESTNOTMERAYRESULTCALLBACK_H #define OPENMW_MWPHYSICS_CLOSESTNOTMERAYRESULTCALLBACK_H #include #include class btCollisionObject; namespace MWPhysics { class Projectile; class ClosestNotMeRayResultCallback : public btCollisionWorld::ClosestRayResultCallback { public: ClosestNotMeRayResultCallback(const btCollisionObject* me, std::vector targets, const btVector3& from, const btVector3& to); btScalar addSingleResult(btCollisionWorld::LocalRayResult& rayResult, bool normalInWorldSpace) override; private: const btCollisionObject* mMe; const std::vector mTargets; }; } #endif ================================================ FILE: apps/openmw/mwphysics/collisiontype.hpp ================================================ #ifndef OPENMW_MWPHYSICS_COLLISIONTYPE_H #define OPENMW_MWPHYSICS_COLLISIONTYPE_H namespace MWPhysics { enum CollisionType { CollisionType_World = 1<<0, CollisionType_Door = 1<<1, CollisionType_Actor = 1<<2, CollisionType_HeightMap = 1<<3, CollisionType_Projectile = 1<<4, CollisionType_Water = 1<<5 }; } #endif ================================================ FILE: apps/openmw/mwphysics/constants.hpp ================================================ #ifndef OPENMW_MWPHYSICS_CONSTANTS_H #define OPENMW_MWPHYSICS_CONSTANTS_H namespace MWPhysics { static constexpr float sStepSizeUp = 34.0f; static constexpr float sStepSizeDown = 62.0f; static constexpr float sMinStep = 10.0f; // hack to skip over tiny unwalkable slopes static constexpr float sMinStep2 = 20.0f; // hack to skip over shorter but longer/wider/further unwalkable slopes // whether to do the above stairstepping logic hacks to work around bad morrowind assets - disabling causes problems but improves performance static constexpr bool sDoExtraStairHacks = true; static constexpr float sGroundOffset = 1.0f; static constexpr float sMaxSlope = 49.0f; // Arbitrary number. To prevent infinite loops. They shouldn't happen but it's good to be prepared. static constexpr int sMaxIterations = 8; // Allows for more precise movement solving without getting stuck or snagging too easily. static constexpr float sCollisionMargin = 0.1f; // Allow for a small amount of penetration to prevent numerical precision issues from causing the "unstuck"ing code to run unnecessarily // Currently set to 0 because having the "unstuck"ing code run whenever possible prevents some glitchy snagging issues static constexpr float sAllowedPenetration = 0.0f; } #endif ================================================ FILE: apps/openmw/mwphysics/contacttestresultcallback.cpp ================================================ #include "contacttestresultcallback.hpp" #include #include "components/misc/convert.hpp" #include "ptrholder.hpp" namespace MWPhysics { ContactTestResultCallback::ContactTestResultCallback(const btCollisionObject* testedAgainst) : mTestedAgainst(testedAgainst) { } btScalar ContactTestResultCallback::addSingleResult(btManifoldPoint& cp, const btCollisionObjectWrapper* col0Wrap,int partId0,int index0, const btCollisionObjectWrapper* col1Wrap,int partId1,int index1) { const btCollisionObject* collisionObject = col0Wrap->m_collisionObject; if (collisionObject == mTestedAgainst) collisionObject = col1Wrap->m_collisionObject; PtrHolder* holder = static_cast(collisionObject->getUserPointer()); if (holder) mResult.emplace_back(ContactPoint{holder->getPtr(), Misc::Convert::toOsg(cp.m_positionWorldOnB), Misc::Convert::toOsg(cp.m_normalWorldOnB)}); return 0.f; } } ================================================ FILE: apps/openmw/mwphysics/contacttestresultcallback.hpp ================================================ #ifndef OPENMW_MWPHYSICS_CONTACTTESTRESULTCALLBACK_H #define OPENMW_MWPHYSICS_CONTACTTESTRESULTCALLBACK_H #include #include #include "physicssystem.hpp" class btCollisionObject; struct btCollisionObjectWrapper; namespace MWPhysics { class ContactTestResultCallback : public btCollisionWorld::ContactResultCallback { const btCollisionObject* mTestedAgainst; public: ContactTestResultCallback(const btCollisionObject* testedAgainst); btScalar addSingleResult(btManifoldPoint& cp, const btCollisionObjectWrapper* col0Wrap,int partId0,int index0, const btCollisionObjectWrapper* col1Wrap,int partId1,int index1) override; std::vector mResult; }; } #endif ================================================ FILE: apps/openmw/mwphysics/contacttestwrapper.cpp ================================================ #include #include "contacttestwrapper.h" namespace MWPhysics { // Concurrent calls to contactPairTest (and by extension contactTest) are forbidden. static std::mutex contactMutex; void ContactTestWrapper::contactTest(btCollisionWorld* collisionWorld, btCollisionObject* colObj, btCollisionWorld::ContactResultCallback& resultCallback) { std::unique_lock lock(contactMutex); collisionWorld->contactTest(colObj, resultCallback); } void ContactTestWrapper::contactPairTest(btCollisionWorld* collisionWorld, btCollisionObject* colObjA, btCollisionObject* colObjB, btCollisionWorld::ContactResultCallback& resultCallback) { std::unique_lock lock(contactMutex); collisionWorld->contactPairTest(colObjA, colObjB, resultCallback); } } ================================================ FILE: apps/openmw/mwphysics/contacttestwrapper.h ================================================ #ifndef OPENMW_MWPHYSICS_CONTACTTESTWRAPPER_H #define OPENMW_MWPHYSICS_CONTACTTESTWRAPPER_H #include namespace MWPhysics { struct ContactTestWrapper { static void contactTest(btCollisionWorld* collisionWorld, btCollisionObject* colObj, btCollisionWorld::ContactResultCallback& resultCallback); static void contactPairTest(btCollisionWorld* collisionWorld, btCollisionObject* colObjA, btCollisionObject* colObjB, btCollisionWorld::ContactResultCallback& resultCallback); }; } #endif ================================================ FILE: apps/openmw/mwphysics/deepestnotmecontacttestresultcallback.cpp ================================================ #include "deepestnotmecontacttestresultcallback.hpp" #include #include #include "../mwworld/class.hpp" #include "ptrholder.hpp" namespace MWPhysics { DeepestNotMeContactTestResultCallback::DeepestNotMeContactTestResultCallback(const btCollisionObject* me, const std::vector& targets, const btVector3 &origin) : mMe(me), mTargets(targets), mOrigin(origin), mLeastDistSqr(std::numeric_limits::max()) { } btScalar DeepestNotMeContactTestResultCallback::addSingleResult(btManifoldPoint& cp, const btCollisionObjectWrapper* col0Wrap,int partId0,int index0, const btCollisionObjectWrapper* col1Wrap,int partId1,int index1) { const btCollisionObject* collisionObject = col1Wrap->m_collisionObject; if (collisionObject != mMe) { if (!mTargets.empty()) { if ((std::find(mTargets.begin(), mTargets.end(), collisionObject) == mTargets.end())) { PtrHolder* holder = static_cast(collisionObject->getUserPointer()); if (holder && !holder->getPtr().isEmpty() && holder->getPtr().getClass().isActor()) return 0.f; } } btScalar distsqr = mOrigin.distance2(cp.getPositionWorldOnA()); if(!mObject || distsqr < mLeastDistSqr) { mObject = collisionObject; mLeastDistSqr = distsqr; mContactPoint = cp.getPositionWorldOnA(); mContactNormal = cp.m_normalWorldOnB; } } return 0.f; } } ================================================ FILE: apps/openmw/mwphysics/deepestnotmecontacttestresultcallback.hpp ================================================ #ifndef OPENMW_MWPHYSICS_DEEPESTNOTMECONTACTTESTRESULTCALLBACK_H #define OPENMW_MWPHYSICS_DEEPESTNOTMECONTACTTESTRESULTCALLBACK_H #include #include class btCollisionObject; namespace MWPhysics { class DeepestNotMeContactTestResultCallback : public btCollisionWorld::ContactResultCallback { const btCollisionObject* mMe; const std::vector mTargets; // Store the real origin, since the shape's origin is its center btVector3 mOrigin; public: const btCollisionObject *mObject{nullptr}; btVector3 mContactPoint{0,0,0}; btVector3 mContactNormal{0,0,0}; btScalar mLeastDistSqr; DeepestNotMeContactTestResultCallback(const btCollisionObject* me, const std::vector& targets, const btVector3 &origin); btScalar addSingleResult(btManifoldPoint& cp, const btCollisionObjectWrapper* col0Wrap,int partId0,int index0, const btCollisionObjectWrapper* col1Wrap,int partId1,int index1) override; }; } #endif ================================================ FILE: apps/openmw/mwphysics/hasspherecollisioncallback.hpp ================================================ #ifndef OPENMW_MWPHYSICS_HASSPHERECOLLISIONCALLBACK_H #define OPENMW_MWPHYSICS_HASSPHERECOLLISIONCALLBACK_H #include #include #include #include #include namespace MWPhysics { // https://developer.mozilla.org/en-US/docs/Games/Techniques/3D_collision_detection bool testAabbAgainstSphere(const btVector3& aabbMin, const btVector3& aabbMax, 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())) ); return nearest.distance(position) < radius; } class HasSphereCollisionCallback final : public btBroadphaseAabbCallback { public: HasSphereCollisionCallback(const btVector3& position, const btScalar radius, btCollisionObject* object, const int mask, const int group) : mPosition(position), mRadius(radius), mCollisionObject(object), mCollisionFilterMask(mask), mCollisionFilterGroup(group) { } bool process(const btBroadphaseProxy* proxy) override { if (mResult) return false; const auto collisionObject = static_cast(proxy->m_clientObject); if (collisionObject == mCollisionObject) return true; if (needsCollision(*proxy)) mResult = testAabbAgainstSphere(proxy->m_aabbMin, proxy->m_aabbMax, mPosition, mRadius); return !mResult; } bool getResult() const { return mResult; } private: btVector3 mPosition; btScalar mRadius; btCollisionObject* mCollisionObject; int mCollisionFilterMask; int mCollisionFilterGroup; bool mResult = false; bool needsCollision(const btBroadphaseProxy& proxy) const { bool collides = (proxy.m_collisionFilterGroup & mCollisionFilterMask) != 0; collides = collides && (mCollisionFilterGroup & proxy.m_collisionFilterMask); return collides; } }; } #endif ================================================ FILE: apps/openmw/mwphysics/heightfield.cpp ================================================ #include "heightfield.hpp" #include "mtphysics.hpp" #include #include #include #include #include #if BT_BULLET_VERSION < 310 // Older Bullet versions only support `btScalar` heightfields. // Our heightfield data is `float`. // // These functions handle conversion from `float` to `double` when // `btScalar` is `double` (`BT_USE_DOUBLE_PRECISION`). namespace { template auto makeHeights(const T* heights, float sqrtVerts) -> std::enable_if_t::value, std::vector> { return {}; } template auto makeHeights(const T* heights, float sqrtVerts) -> std::enable_if_t::value, std::vector> { return std::vector(heights, heights + static_cast(sqrtVerts * sqrtVerts)); } template auto getHeights(const T* floatHeights, const std::vector&) -> std::enable_if_t::value, const btScalar*> { return floatHeights; } template auto getHeights(const T*, const std::vector& btScalarHeights) -> std::enable_if_t::value, const btScalar*> { return btScalarHeights.data(); } } #endif namespace MWPhysics { HeightField::HeightField() {} HeightField::HeightField(const HeightField&, const osg::CopyOp&) {} HeightField::HeightField(const float* heights, int x, int y, float triSize, float sqrtVerts, float minH, float maxH, const osg::Object* holdObject, PhysicsTaskScheduler* scheduler) : mHoldObject(holdObject) #if BT_BULLET_VERSION < 310 , mHeights(makeHeights(heights, sqrtVerts)) #endif , mTaskScheduler(scheduler) { #if BT_BULLET_VERSION < 310 mShape = std::make_unique( sqrtVerts, sqrtVerts, getHeights(heights, mHeights), 1, minH, maxH, 2, PHY_FLOAT, false ); #else mShape = std::make_unique( sqrtVerts, sqrtVerts, heights, minH, maxH, 2, false); #endif mShape->setUseDiamondSubdivision(true); mShape->setLocalScaling(btVector3(triSize, triSize, 1)); #if BT_BULLET_VERSION >= 289 // Accelerates some collision tests. // // Note: The accelerator data structure in Bullet is only used // in some operations. This could be improved, see: // https://github.com/bulletphysics/bullet3/issues/3276 mShape->buildAccelerator(); #endif btTransform transform(btQuaternion::getIdentity(), btVector3((x+0.5f) * triSize * (sqrtVerts-1), (y+0.5f) * triSize * (sqrtVerts-1), (maxH+minH)*0.5f)); mCollisionObject = std::make_unique(); mCollisionObject->setCollisionShape(mShape.get()); mCollisionObject->setWorldTransform(transform); mTaskScheduler->addCollisionObject(mCollisionObject.get(), CollisionType_HeightMap, CollisionType_Actor|CollisionType_Projectile); } HeightField::~HeightField() { mTaskScheduler->removeCollisionObject(mCollisionObject.get()); } btCollisionObject* HeightField::getCollisionObject() { return mCollisionObject.get(); } const btCollisionObject* HeightField::getCollisionObject() const { return mCollisionObject.get(); } const btHeightfieldTerrainShape* HeightField::getShape() const { return mShape.get(); } } ================================================ FILE: apps/openmw/mwphysics/heightfield.hpp ================================================ #ifndef OPENMW_MWPHYSICS_HEIGHTFIELD_H #define OPENMW_MWPHYSICS_HEIGHTFIELD_H #include #include #include #include #include class btCollisionObject; class btHeightfieldTerrainShape; namespace osg { class Object; } namespace MWPhysics { class PhysicsTaskScheduler; class HeightField : public osg::Object { public: HeightField(const float* heights, int x, int y, float triSize, float sqrtVerts, float minH, float maxH, const osg::Object* holdObject, PhysicsTaskScheduler* scheduler); ~HeightField(); META_Object(MWPhysics, HeightField) btCollisionObject* getCollisionObject(); const btCollisionObject* getCollisionObject() const; const btHeightfieldTerrainShape* getShape() const; private: std::unique_ptr mShape; std::unique_ptr mCollisionObject; osg::ref_ptr mHoldObject; #if BT_BULLET_VERSION < 310 std::vector mHeights; #endif PhysicsTaskScheduler* mTaskScheduler; HeightField(); HeightField(const HeightField&, const osg::CopyOp&); void operator=(const HeightField&); HeightField(const HeightField&); }; } #endif ================================================ FILE: apps/openmw/mwphysics/movementsolver.cpp ================================================ #include "movementsolver.hpp" #include #include #include #include #include #include "../mwbase/world.hpp" #include "../mwbase/environment.hpp" #include "../mwworld/class.hpp" #include "../mwworld/esmstore.hpp" #include "../mwworld/refdata.hpp" #include "actor.hpp" #include "collisiontype.hpp" #include "constants.hpp" #include "contacttestwrapper.h" #include "physicssystem.hpp" #include "stepper.hpp" #include "trace.h" #include namespace MWPhysics { static bool isActor(const btCollisionObject *obj) { assert(obj); return obj->getBroadphaseHandle()->m_collisionFilterGroup == CollisionType_Actor; } class ContactCollectionCallback : public btCollisionWorld::ContactResultCallback { public: ContactCollectionCallback(const btCollisionObject * me, osg::Vec3f velocity) : mMe(me) { m_collisionFilterGroup = me->getBroadphaseHandle()->m_collisionFilterGroup; m_collisionFilterMask = me->getBroadphaseHandle()->m_collisionFilterMask & ~CollisionType_Projectile; mVelocity = Misc::Convert::toBullet(velocity); } btScalar addSingleResult(btManifoldPoint & contact, const btCollisionObjectWrapper * colObj0Wrap, int partId0, int index0, const btCollisionObjectWrapper * colObj1Wrap, int partId1, int index1) override { if (isActor(colObj0Wrap->getCollisionObject()) && isActor(colObj1Wrap->getCollisionObject())) return 0.0; // ignore overlap if we're moving in the same direction as it would push us out (don't change this to >=, that would break detection when not moving) if (contact.m_normalWorldOnB.dot(mVelocity) > 0.0) return 0.0; auto delta = contact.m_normalWorldOnB * -contact.m_distance1; mContactSum += delta; mMaxX = std::max(std::abs(delta.x()), mMaxX); mMaxY = std::max(std::abs(delta.y()), mMaxY); mMaxZ = std::max(std::abs(delta.z()), mMaxZ); if (contact.m_distance1 < mDistance) { mDistance = contact.m_distance1; mNormal = contact.m_normalWorldOnB; mDelta = delta; return mDistance; } else { return 0.0; } } btScalar mMaxX = 0.0; btScalar mMaxY = 0.0; btScalar mMaxZ = 0.0; btVector3 mContactSum{0.0, 0.0, 0.0}; btVector3 mNormal{0.0, 0.0, 0.0}; // points towards "me" btVector3 mDelta{0.0, 0.0, 0.0}; // points towards "me" btScalar mDistance = 0.0; // negative or zero protected: btVector3 mVelocity; const btCollisionObject * mMe; }; osg::Vec3f MovementSolver::traceDown(const MWWorld::Ptr &ptr, const osg::Vec3f& position, Actor* actor, btCollisionWorld* collisionWorld, float maxHeight) { osg::Vec3f offset = actor->getCollisionObjectPosition() - ptr.getRefData().getPosition().asVec3(); ActorTracer tracer; tracer.findGround(actor, position + offset, position + offset - osg::Vec3f(0,0,maxHeight), collisionWorld); if (tracer.mFraction >= 1.0f) { actor->setOnGround(false); return position; } actor->setOnGround(true); // Check if we actually found a valid spawn point (use an infinitely thin ray this time). // Required for some broken door destinations in Morrowind.esm, where the spawn point // intersects with other geometry if the actor's base is taken into account btVector3 from = Misc::Convert::toBullet(position); btVector3 to = from - btVector3(0,0,maxHeight); btCollisionWorld::ClosestRayResultCallback resultCallback1(from, to); resultCallback1.m_collisionFilterGroup = 0xff; resultCallback1.m_collisionFilterMask = CollisionType_World|CollisionType_HeightMap; collisionWorld->rayTest(from, to, resultCallback1); if (resultCallback1.hasHit() && ((Misc::Convert::toOsg(resultCallback1.m_hitPointWorld) - tracer.mEndPos + offset).length2() > 35*35 || !isWalkableSlope(tracer.mPlaneNormal))) { actor->setOnSlope(!isWalkableSlope(resultCallback1.m_hitNormalWorld)); return Misc::Convert::toOsg(resultCallback1.m_hitPointWorld) + osg::Vec3f(0.f, 0.f, sGroundOffset); } actor->setOnSlope(!isWalkableSlope(tracer.mPlaneNormal)); return tracer.mEndPos-offset + osg::Vec3f(0.f, 0.f, sGroundOffset); } void MovementSolver::move(ActorFrameData& actor, float time, const btCollisionWorld* collisionWorld, WorldFrameData& worldData) { auto* physicActor = actor.mActorRaw; const ESM::Position& refpos = actor.mRefpos; // Early-out for totally static creatures // (Not sure if gravity should still apply?) { const auto ptr = physicActor->getPtr(); if (!ptr.getClass().isMobile(ptr)) return; } // Reset per-frame data physicActor->setWalkingOnWater(false); // Anything to collide with? if(!physicActor->getCollisionMode() || actor.mSkipCollisionDetection) { actor.mPosition += (osg::Quat(refpos.rot[0], osg::Vec3f(-1, 0, 0)) * osg::Quat(refpos.rot[2], osg::Vec3f(0, 0, -1)) ) * actor.mMovement * time; return; } const btCollisionObject *colobj = physicActor->getCollisionObject(); // Adjust for collision mesh offset relative to actor's "location" // (doTrace doesn't take local/interior collision shape translation into account, so we have to do it on our own) // for compatibility with vanilla assets, we have to derive this from the vertical half extent instead of from internal hull translation // if not for this hack, the "correct" collision hull position would be physicActor->getScaledMeshTranslation() osg::Vec3f halfExtents = physicActor->getHalfExtents(); actor.mPosition.z() += halfExtents.z(); // vanilla-accurate static const float fSwimHeightScale = MWBase::Environment::get().getWorld()->getStore().get().find("fSwimHeightScale")->mValue.getFloat(); float swimlevel = actor.mWaterlevel + halfExtents.z() - (physicActor->getRenderingHalfExtents().z() * 2 * fSwimHeightScale); ActorTracer tracer; osg::Vec3f inertia = physicActor->getInertialForce(); osg::Vec3f velocity; if (actor.mPosition.z() < swimlevel || actor.mFlying) { velocity = (osg::Quat(refpos.rot[0], osg::Vec3f(-1, 0, 0)) * osg::Quat(refpos.rot[2], osg::Vec3f(0, 0, -1))) * actor.mMovement; } else { velocity = (osg::Quat(refpos.rot[2], osg::Vec3f(0, 0, -1))) * actor.mMovement; if ((velocity.z() > 0.f && physicActor->getOnGround() && !physicActor->getOnSlope()) || (velocity.z() > 0.f && velocity.z() + inertia.z() <= -velocity.z() && physicActor->getOnSlope())) inertia = velocity; else if (!physicActor->getOnGround() || physicActor->getOnSlope()) velocity = velocity + inertia; } // Dead and paralyzed actors underwater will float to the surface, // if the CharacterController tells us to do so if (actor.mMovement.z() > 0 && actor.mFloatToSurface && actor.mPosition.z() < swimlevel) velocity = osg::Vec3f(0,0,1) * 25; if (actor.mWantJump) actor.mDidJump = true; // Now that we have the effective movement vector, apply wind forces to it if (worldData.mIsInStorm) { osg::Vec3f stormDirection = worldData.mStormDirection; float angleDegrees = osg::RadiansToDegrees(std::acos(stormDirection * velocity / (stormDirection.length() * velocity.length()))); static const float fStromWalkMult = MWBase::Environment::get().getWorld()->getStore().get().find("fStromWalkMult")->mValue.getFloat(); velocity *= 1.f-(fStromWalkMult * (angleDegrees/180.f)); } Stepper stepper(collisionWorld, colobj); osg::Vec3f origVelocity = velocity; osg::Vec3f newPosition = actor.mPosition; /* * A loop to find newPosition using tracer, if successful different from the starting position. * nextpos is the local variable used to find potential newPosition, using velocity and remainingTime * The initial velocity was set earlier (see above). */ float remainingTime = time; bool seenGround = physicActor->getOnGround() && !physicActor->getOnSlope() && !actor.mFlying; int numTimesSlid = 0; osg::Vec3f lastSlideNormal(0,0,1); osg::Vec3f lastSlideNormalFallback(0,0,1); bool forceGroundTest = false; for (int iterations = 0; iterations < sMaxIterations && remainingTime > 0.0001f; ++iterations) { osg::Vec3f nextpos = newPosition + velocity * remainingTime; // If not able to fly, don't allow to swim up into the air if(!actor.mFlying && nextpos.z() > swimlevel && newPosition.z() < swimlevel) { const osg::Vec3f down(0,0,-1); velocity = reject(velocity, down); // NOTE: remainingTime is unchanged before the loop continues continue; // velocity updated, calculate nextpos again } if((newPosition - nextpos).length2() > 0.0001) { // trace to where character would go if there were no obstructions tracer.doTrace(colobj, newPosition, nextpos, collisionWorld); // check for obstructions if(tracer.mFraction >= 1.0f) { newPosition = tracer.mEndPos; // ok to move, so set newPosition break; } } else { // The current position and next position are nearly the same, so just exit. // Note: Bullet can trigger an assert in debug modes if the positions // are the same, since that causes it to attempt to normalize a zero // length vector (which can also happen with nearly identical vectors, since // precision can be lost due to any math Bullet does internally). Since we // aren't performing any collision detection, we want to reject the next // position, so that we don't slowly move inside another object. break; } if (isWalkableSlope(tracer.mPlaneNormal) && !actor.mFlying && newPosition.z() >= swimlevel) seenGround = true; // We hit something. Check if we can step up. float hitHeight = tracer.mHitPoint.z() - tracer.mEndPos.z() + halfExtents.z(); osg::Vec3f oldPosition = newPosition; bool usedStepLogic = false; if (hitHeight < sStepSizeUp && !isActor(tracer.mHitObject)) { // Try to step up onto it. // NOTE: this modifies newPosition and velocity on its own if successful usedStepLogic = stepper.step(newPosition, velocity, remainingTime, seenGround, iterations == 0); } if (usedStepLogic) { // don't let pure water creatures move out of water after stepMove const auto ptr = physicActor->getPtr(); if (ptr.getClass().isPureWaterCreature(ptr) && newPosition.z() + halfExtents.z() > actor.mWaterlevel) newPosition = oldPosition; else if(!actor.mFlying && actor.mPosition.z() >= swimlevel) forceGroundTest = true; } else { // Can't step up, so slide against what we ran into remainingTime *= (1.0f-tracer.mFraction); auto planeNormal = tracer.mPlaneNormal; // If we touched the ground this frame, and whatever we ran into is a wall of some sort, // pretend that its collision normal is pointing horizontally // (fixes snagging on slightly downward-facing walls, and crawling up the bases of very steep walls because of the collision margin) if (seenGround && !isWalkableSlope(planeNormal) && planeNormal.z() != 0) { planeNormal.z() = 0; planeNormal.normalize(); } // Move up to what we ran into (with a bit of a collision margin) if ((newPosition-tracer.mEndPos).length2() > sCollisionMargin*sCollisionMargin) { auto direction = velocity; direction.normalize(); newPosition = tracer.mEndPos; newPosition -= direction*sCollisionMargin; } osg::Vec3f newVelocity = (velocity * planeNormal <= 0.0) ? reject(velocity, planeNormal) : velocity; bool usedSeamLogic = false; // check for the current and previous collision planes forming an acute angle; slide along the seam if they do if(numTimesSlid > 0) { auto dotA = lastSlideNormal * planeNormal; auto dotB = lastSlideNormalFallback * planeNormal; if(numTimesSlid <= 1) // ignore fallback normal if this is only the first or second slide dotB = 1.0; if(dotA <= 0.0 || dotB <= 0.0) { osg::Vec3f bestNormal = lastSlideNormal; // use previous-to-previous collision plane if it's acute with current plane but actual previous plane isn't if(dotB < dotA) { bestNormal = lastSlideNormalFallback; lastSlideNormal = lastSlideNormalFallback; } auto constraintVector = bestNormal ^ planeNormal; // cross product if(constraintVector.length2() > 0) // only if it's not zero length { constraintVector.normalize(); newVelocity = project(velocity, constraintVector); // version of surface rejection for acute crevices/seams auto averageNormal = bestNormal + planeNormal; averageNormal.normalize(); tracer.doTrace(colobj, newPosition, newPosition + averageNormal*(sCollisionMargin*2.0), collisionWorld); newPosition = (newPosition + tracer.mEndPos)/2.0; usedSeamLogic = true; } } } // otherwise just keep the normal vector rejection // if this isn't the first iteration, or if the first iteration is also the last iteration, // move away from the collision plane slightly, if possible // this reduces getting stuck in some concave geometry, like the gaps above the railings in some ald'ruhn buildings // this is different from the normal collision margin, because the normal collision margin is along the movement path, // but this is along the collision normal if(!usedSeamLogic && (iterations > 0 || remainingTime < 0.01f)) { tracer.doTrace(colobj, newPosition, newPosition + planeNormal*(sCollisionMargin*2.0), collisionWorld); newPosition = (newPosition + tracer.mEndPos)/2.0; } // Do not allow sliding up steep slopes if there is gravity. if (newPosition.z() >= swimlevel && !actor.mFlying && !isWalkableSlope(planeNormal)) newVelocity.z() = std::min(newVelocity.z(), velocity.z()); if (newVelocity * origVelocity <= 0.0f) break; numTimesSlid += 1; lastSlideNormalFallback = lastSlideNormal; lastSlideNormal = planeNormal; velocity = newVelocity; } } bool isOnGround = false; bool isOnSlope = false; if (forceGroundTest || (inertia.z() <= 0.f && newPosition.z() >= swimlevel)) { osg::Vec3f from = newPosition; auto dropDistance = 2*sGroundOffset + (physicActor->getOnGround() ? sStepSizeDown : 0); osg::Vec3f to = newPosition - osg::Vec3f(0,0,dropDistance); tracer.doTrace(colobj, from, to, collisionWorld); if(tracer.mFraction < 1.0f) { if (!isActor(tracer.mHitObject)) { isOnGround = true; isOnSlope = !isWalkableSlope(tracer.mPlaneNormal); const btCollisionObject* standingOn = tracer.mHitObject; PtrHolder* ptrHolder = static_cast(standingOn->getUserPointer()); if (ptrHolder) actor.mStandingOn = ptrHolder->getPtr(); if (standingOn->getBroadphaseHandle()->m_collisionFilterGroup == CollisionType_Water) physicActor->setWalkingOnWater(true); if (!actor.mFlying && !isOnSlope) { if (tracer.mFraction*dropDistance > sGroundOffset) newPosition.z() = tracer.mEndPos.z() + sGroundOffset; else { newPosition.z() = tracer.mEndPos.z(); tracer.doTrace(colobj, newPosition, newPosition + osg::Vec3f(0, 0, 2*sGroundOffset), collisionWorld); newPosition = (newPosition+tracer.mEndPos)/2.0; } } } else { // Vanilla allows actors to float on top of other actors. Do not push them off. if (!actor.mFlying && isWalkableSlope(tracer.mPlaneNormal) && tracer.mEndPos.z()+sGroundOffset <= newPosition.z()) newPosition.z() = tracer.mEndPos.z() + sGroundOffset; isOnGround = false; } } // forcibly treat stuck actors as if they're on flat ground because buggy collisions when inside of things can/will break ground detection if(physicActor->getStuckFrames() > 0) { isOnGround = true; isOnSlope = false; } } if((isOnGround && !isOnSlope) || newPosition.z() < swimlevel || actor.mFlying) physicActor->setInertialForce(osg::Vec3f(0.f, 0.f, 0.f)); else { inertia.z() -= time * Constants::GravityConst * Constants::UnitsPerMeter; if (inertia.z() < 0) inertia.z() *= actor.mSlowFall; if (actor.mSlowFall < 1.f) { inertia.x() *= actor.mSlowFall; inertia.y() *= actor.mSlowFall; } physicActor->setInertialForce(inertia); } physicActor->setOnGround(isOnGround); physicActor->setOnSlope(isOnSlope); actor.mPosition = newPosition; // remove what was added earlier in compensating for doTrace not taking interior transformation into account actor.mPosition.z() -= halfExtents.z(); // vanilla-accurate } btVector3 addMarginToDelta(btVector3 delta) { if(delta.length2() == 0.0) return delta; return delta + delta.normalized() * sCollisionMargin; } void MovementSolver::unstuck(ActorFrameData& actor, const btCollisionWorld* collisionWorld) { const auto& ptr = actor.mActorRaw->getPtr(); if (!ptr.getClass().isMobile(ptr)) return; auto* physicActor = actor.mActorRaw; if(!physicActor->getCollisionMode() || actor.mSkipCollisionDetection) // noclipping/tcl return; auto* collisionObject = physicActor->getCollisionObject(); auto tempPosition = actor.mPosition; if(physicActor->getStuckFrames() >= 10) { if((physicActor->getLastStuckPosition() - actor.mPosition).length2() < 100) return; else { physicActor->setStuckFrames(0); physicActor->setLastStuckPosition({0, 0, 0}); } } // use vanilla-accurate collision hull position hack (do same hitbox offset hack as movement solver) // if vanilla compatibility didn't matter, the "correct" collision hull position would be physicActor->getScaledMeshTranslation() const auto verticalHalfExtent = osg::Vec3f(0.0, 0.0, physicActor->getHalfExtents().z()); // use a 3d approximation of the movement vector to better judge player intent auto velocity = (osg::Quat(actor.mRefpos.rot[0], osg::Vec3f(-1, 0, 0)) * osg::Quat(actor.mRefpos.rot[2], osg::Vec3f(0, 0, -1))) * actor.mMovement; // try to pop outside of the world before doing anything else if we're inside of it if (!physicActor->getOnGround() || physicActor->getOnSlope()) velocity += physicActor->getInertialForce(); // because of the internal collision box offset hack, and the fact that we're moving the collision box manually, // we need to replicate part of the collision box's transform process from scratch osg::Vec3f refPosition = tempPosition + verticalHalfExtent; osg::Vec3f goodPosition = refPosition; const btTransform oldTransform = collisionObject->getWorldTransform(); btTransform newTransform = oldTransform; auto gatherContacts = [&](btVector3 newOffset) -> ContactCollectionCallback { goodPosition = refPosition + Misc::Convert::toOsg(addMarginToDelta(newOffset)); newTransform.setOrigin(Misc::Convert::toBullet(goodPosition)); collisionObject->setWorldTransform(newTransform); ContactCollectionCallback callback{collisionObject, velocity}; ContactTestWrapper::contactTest(const_cast(collisionWorld), collisionObject, callback); return callback; }; // check whether we're inside the world with our collision box with manually-derived offset auto contactCallback = gatherContacts({0.0, 0.0, 0.0}); if(contactCallback.mDistance < -sAllowedPenetration) { physicActor->setStuckFrames(physicActor->getStuckFrames() + 1); physicActor->setLastStuckPosition(actor.mPosition); // we are; try moving it out of the world auto positionDelta = contactCallback.mContactSum; // limit rejection delta to the largest known individual rejections if(std::abs(positionDelta.x()) > contactCallback.mMaxX) positionDelta *= contactCallback.mMaxX / std::abs(positionDelta.x()); if(std::abs(positionDelta.y()) > contactCallback.mMaxY) positionDelta *= contactCallback.mMaxY / std::abs(positionDelta.y()); if(std::abs(positionDelta.z()) > contactCallback.mMaxZ) positionDelta *= contactCallback.mMaxZ / std::abs(positionDelta.z()); auto contactCallback2 = gatherContacts(positionDelta); // successfully moved further out from contact (does not have to be in open space, just less inside of things) if(contactCallback2.mDistance > contactCallback.mDistance) tempPosition = goodPosition - verticalHalfExtent; // try again but only upwards (fixes some bad coc floors) else { // upwards-only offset auto contactCallback3 = gatherContacts({0.0, 0.0, std::abs(positionDelta.z())}); // success if(contactCallback3.mDistance > contactCallback.mDistance) tempPosition = goodPosition - verticalHalfExtent; else // try again but fixed distance up { auto contactCallback4 = gatherContacts({0.0, 0.0, 10.0}); // success if(contactCallback4.mDistance > contactCallback.mDistance) tempPosition = goodPosition - verticalHalfExtent; } } } else { physicActor->setStuckFrames(0); physicActor->setLastStuckPosition({0, 0, 0}); } collisionObject->setWorldTransform(oldTransform); actor.mPosition = tempPosition; } } ================================================ FILE: apps/openmw/mwphysics/movementsolver.hpp ================================================ #ifndef OPENMW_MWPHYSICS_MOVEMENTSOLVER_H #define OPENMW_MWPHYSICS_MOVEMENTSOLVER_H #include #include "constants.hpp" #include "../mwworld/ptr.hpp" class btCollisionWorld; namespace MWWorld { class Ptr; } namespace MWPhysics { /// Vector projection static inline osg::Vec3f project(const osg::Vec3f& u, const osg::Vec3f &v) { return v * (u * v); } /// Vector rejection static inline osg::Vec3f reject(const osg::Vec3f& direction, const osg::Vec3f &planeNormal) { return direction - project(direction, planeNormal); } template static bool isWalkableSlope(const Vec3 &normal) { static const float sMaxSlopeCos = std::cos(osg::DegreesToRadians(sMaxSlope)); return (normal.z() > sMaxSlopeCos); } class Actor; struct ActorFrameData; struct WorldFrameData; class MovementSolver { public: static osg::Vec3f traceDown(const MWWorld::Ptr &ptr, const osg::Vec3f& position, Actor* actor, btCollisionWorld* collisionWorld, float maxHeight); static void move(ActorFrameData& actor, float time, const btCollisionWorld* collisionWorld, WorldFrameData& worldData); static void unstuck(ActorFrameData& actor, const btCollisionWorld* collisionWorld); }; } #endif ================================================ FILE: apps/openmw/mwphysics/mtphysics.cpp ================================================ #include #include #include #include "components/debug/debuglog.hpp" #include #include "components/misc/convert.hpp" #include "components/settings/settings.hpp" #include "../mwmechanics/actorutil.hpp" #include "../mwmechanics/movement.hpp" #include "../mwrender/bulletdebugdraw.hpp" #include "../mwworld/class.hpp" #include "../mwworld/player.hpp" #include "actor.hpp" #include "contacttestwrapper.h" #include "movementsolver.hpp" #include "mtphysics.hpp" #include "object.hpp" #include "physicssystem.hpp" #include "projectile.hpp" namespace { /// @brief A scoped lock that is either shared or exclusive depending on configuration template class MaybeSharedLock { public: /// @param mutex a shared mutex /// @param canBeSharedLock decide wether the lock will be shared or exclusive MaybeSharedLock(Mutex& mutex, bool canBeSharedLock) : mMutex(mutex), mCanBeSharedLock(canBeSharedLock) { if (mCanBeSharedLock) mMutex.lock_shared(); else mMutex.lock(); } ~MaybeSharedLock() { if (mCanBeSharedLock) mMutex.unlock_shared(); else mMutex.unlock(); } private: Mutex& mMutex; bool mCanBeSharedLock; }; void handleFall(MWPhysics::ActorFrameData& actorData, bool simulationPerformed) { const float heightDiff = actorData.mPosition.z() - actorData.mOldHeight; const bool isStillOnGround = (simulationPerformed && actorData.mWasOnGround && actorData.mActorRaw->getOnGround()); if (isStillOnGround || actorData.mFlying || actorData.mSwimming || actorData.mSlowFall < 1) actorData.mNeedLand = true; else if (heightDiff < 0) actorData.mFallHeight += heightDiff; } void handleJump(const MWWorld::Ptr &ptr) { const bool isPlayer = (ptr == MWMechanics::getPlayer()); // Advance acrobatics and set flag for GetPCJumping if (isPlayer) { ptr.getClass().skillUsageSucceeded(ptr, ESM::Skill::Acrobatics, 0); MWBase::Environment::get().getWorld()->getPlayer().setJumping(true); } // Decrease fatigue if (!isPlayer || !MWBase::Environment::get().getWorld()->getGodModeState()) { const MWWorld::Store &gmst = MWBase::Environment::get().getWorld()->getStore().get(); const float fFatigueJumpBase = gmst.find("fFatigueJumpBase")->mValue.getFloat(); const float fFatigueJumpMult = gmst.find("fFatigueJumpMult")->mValue.getFloat(); const float normalizedEncumbrance = std::min(1.f, ptr.getClass().getNormalizedEncumbrance(ptr)); const float fatigueDecrease = fFatigueJumpBase + normalizedEncumbrance * fFatigueJumpMult; MWMechanics::DynamicStat fatigue = ptr.getClass().getCreatureStats(ptr).getFatigue(); fatigue.setCurrent(fatigue.getCurrent() - fatigueDecrease); ptr.getClass().getCreatureStats(ptr).setFatigue(fatigue); } ptr.getClass().getMovementSettings(ptr).mPosition[2] = 0; } void updateMechanics(MWPhysics::ActorFrameData& actorData) { auto ptr = actorData.mActorRaw->getPtr(); if (actorData.mDidJump) handleJump(ptr); MWMechanics::CreatureStats& stats = ptr.getClass().getCreatureStats(ptr); if (actorData.mNeedLand) stats.land(ptr == MWMechanics::getPlayer() && (actorData.mFlying || actorData.mSwimming)); else if (actorData.mFallHeight < 0) stats.addToFallHeight(-actorData.mFallHeight); } osg::Vec3f interpolateMovements(MWPhysics::ActorFrameData& actorData, float timeAccum, float physicsDt) { const float interpolationFactor = std::clamp(timeAccum / physicsDt, 0.0f, 1.0f); return actorData.mPosition * interpolationFactor + actorData.mActorRaw->getPreviousPosition() * (1.f - interpolationFactor); } namespace Config { /// @return either the number of thread as configured by the user, or 1 if Bullet doesn't support multithreading int computeNumThreads(bool& threadSafeBullet) { int wantedThread = Settings::Manager::getInt("async num threads", "Physics"); auto broad = std::make_unique(); auto maxSupportedThreads = broad->m_rayTestStacks.size(); threadSafeBullet = (maxSupportedThreads > 1); if (!threadSafeBullet && wantedThread > 1) { Log(Debug::Warning) << "Bullet was not compiled with multithreading support, 1 async thread will be used"; return 1; } return std::max(0, wantedThread); } } } namespace MWPhysics { PhysicsTaskScheduler::PhysicsTaskScheduler(float physicsDt, btCollisionWorld *collisionWorld, MWRender::DebugDrawer* debugDrawer) : mDefaultPhysicsDt(physicsDt) , mPhysicsDt(physicsDt) , mTimeAccum(0.f) , mCollisionWorld(collisionWorld) , mDebugDrawer(debugDrawer) , mNumJobs(0) , mRemainingSteps(0) , mLOSCacheExpiry(Settings::Manager::getInt("lineofsight keep inactive cache", "Physics")) , mDeferAabbUpdate(Settings::Manager::getBool("defer aabb update", "Physics")) , mFrameCounter(0) , mAdvanceSimulation(false) , mQuit(false) , mNextJob(0) , mNextLOS(0) , mFrameNumber(0) , mTimer(osg::Timer::instance()) , mPrevStepCount(1) , mBudget(physicsDt) , mAsyncBudget(0.0f) , mBudgetCursor(0) , mAsyncStartTime(0) , mTimeBegin(0) , mTimeEnd(0) , mFrameStart(0) { mNumThreads = Config::computeNumThreads(mThreadSafeBullet); if (mNumThreads >= 1) { for (int i = 0; i < mNumThreads; ++i) mThreads.emplace_back([&] { worker(); } ); } else { mLOSCacheExpiry = -1; mDeferAabbUpdate = false; } mPreStepBarrier = std::make_unique(mNumThreads); mPostStepBarrier = std::make_unique(mNumThreads); mPostSimBarrier = std::make_unique(mNumThreads); } PhysicsTaskScheduler::~PhysicsTaskScheduler() { waitForWorkers(); std::unique_lock lock(mSimulationMutex); mQuit = true; mNumJobs = 0; mRemainingSteps = 0; mHasJob.notify_all(); lock.unlock(); for (auto& thread : mThreads) thread.join(); } std::tuple PhysicsTaskScheduler::calculateStepConfig(float timeAccum) const { int maxAllowedSteps = 2; int numSteps = timeAccum / mDefaultPhysicsDt; // adjust maximum step count based on whether we're likely physics bottlenecked or not // if maxAllowedSteps ends up higher than numSteps, we will not invoke delta time // if it ends up lower than numSteps, but greater than 1, we will run a number of true delta time physics steps that we expect to be within budget // if it ends up lower than numSteps and also 1, we will run a single delta time physics step // if we did not do this, and had a fixed step count limit, // we would have an unnecessarily low render framerate if we were only physics bottlenecked, // and we would be unnecessarily invoking true delta time if we were only render bottlenecked // get physics timing stats float budgetMeasurement = std::max(mBudget.get(), mAsyncBudget.get()); // time spent per step in terms of the intended physics framerate budgetMeasurement /= mDefaultPhysicsDt; // ensure sane minimum value budgetMeasurement = std::max(0.00001f, budgetMeasurement); // we're spending almost or more than realtime per physics frame; limit to a single step if (budgetMeasurement > 0.95) maxAllowedSteps = 1; // physics is fairly cheap; limit based on expense if (budgetMeasurement < 0.5) maxAllowedSteps = std::ceil(1.0/budgetMeasurement); // limit to a reasonable amount maxAllowedSteps = std::min(10, maxAllowedSteps); // fall back to delta time for this frame if fixed timestep physics would fall behind float actualDelta = mDefaultPhysicsDt; if (numSteps > maxAllowedSteps) { numSteps = maxAllowedSteps; // ensure that we do not simulate a frame ahead when doing delta time; this reduces stutter and latency // this causes interpolation to 100% use the most recent physics result when true delta time is happening // and we deliberately simulate up to exactly the timestamp that we want to render actualDelta = timeAccum/float(numSteps+1); // actually: if this results in a per-step delta less than the target physics steptime, clamp it // this might reintroduce some stutter, but only comes into play in obscure cases // (because numSteps is originally based on mDefaultPhysicsDt, this won't cause us to overrun) actualDelta = std::max(actualDelta, mDefaultPhysicsDt); } return std::make_tuple(numSteps, actualDelta); } const std::vector& PhysicsTaskScheduler::moveActors(float & timeAccum, std::vector&& actorsData, 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. std::unique_lock lock(mSimulationMutex); double timeStart = mTimer->tick(); mMovedActors.clear(); // start by finishing previous background computation if (mNumThreads != 0) { for (auto& data : mActorsFrameData) { const auto actorActive = [&data](const auto& newFrameData) -> bool { const auto actor = data.mActor.lock(); return actor && actor->getPtr() == newFrameData.mActorRaw->getPtr(); }; // Only return actors that are still part of the scene if (std::any_of(actorsData.begin(), actorsData.end(), actorActive)) { updateMechanics(data); // these variables are accessed directly from the main thread, update them here to prevent accessing "too new" values if (mAdvanceSimulation) data.mActorRaw->setStandingOnPtr(data.mStandingOn); data.mActorRaw->setSimulationPosition(interpolateMovements(data, mTimeAccum, mPhysicsDt)); mMovedActors.emplace_back(data.mActorRaw->getPtr()); } } if(mAdvanceSimulation) mAsyncBudget.update(mTimer->delta_s(mAsyncStartTime, mTimeEnd), mPrevStepCount, mBudgetCursor); updateStats(frameStart, frameNumber, stats); } auto [numSteps, newDelta] = calculateStepConfig(timeAccum); timeAccum -= numSteps*newDelta; // init for (auto& data : actorsData) data.updatePosition(mCollisionWorld); mPrevStepCount = numSteps; mRemainingSteps = numSteps; mTimeAccum = timeAccum; mPhysicsDt = newDelta; mActorsFrameData = std::move(actorsData); mAdvanceSimulation = (mRemainingSteps != 0); ++mFrameCounter; mNumJobs = mActorsFrameData.size(); mNextLOS.store(0, std::memory_order_relaxed); mNextJob.store(0, std::memory_order_release); if (mAdvanceSimulation) mWorldFrameData = std::make_unique(); if (mAdvanceSimulation) mBudgetCursor += 1; if (mNumThreads == 0) { syncComputation(); if(mAdvanceSimulation) mBudget.update(mTimer->delta_s(timeStart, mTimer->tick()), numSteps, mBudgetCursor); return mMovedActors; } mAsyncStartTime = mTimer->tick(); mHasJob.notify_all(); lock.unlock(); if (mAdvanceSimulation) mBudget.update(mTimer->delta_s(timeStart, mTimer->tick()), 1, mBudgetCursor); return mMovedActors; } const std::vector& PhysicsTaskScheduler::resetSimulation(const ActorMap& actors) { waitForWorkers(); std::unique_lock lock(mSimulationMutex); mBudget.reset(mDefaultPhysicsDt); mAsyncBudget.reset(0.0f); mMovedActors.clear(); mActorsFrameData.clear(); for (const auto& [_, actor] : actors) { actor->updatePosition(); actor->updateCollisionObjectPosition(); mMovedActors.emplace_back(actor->getPtr()); } return mMovedActors; } void PhysicsTaskScheduler::rayTest(const btVector3& rayFromWorld, const btVector3& rayToWorld, btCollisionWorld::RayResultCallback& resultCallback) const { MaybeSharedLock lock(mCollisionWorldMutex, mThreadSafeBullet); mCollisionWorld->rayTest(rayFromWorld, rayToWorld, resultCallback); } void PhysicsTaskScheduler::convexSweepTest(const btConvexShape* castShape, const btTransform& from, const btTransform& to, btCollisionWorld::ConvexResultCallback& resultCallback) const { MaybeSharedLock lock(mCollisionWorldMutex, mThreadSafeBullet); mCollisionWorld->convexSweepTest(castShape, from, to, resultCallback); } void PhysicsTaskScheduler::contactTest(btCollisionObject* colObj, btCollisionWorld::ContactResultCallback& resultCallback) { std::shared_lock lock(mCollisionWorldMutex); ContactTestWrapper::contactTest(mCollisionWorld, colObj, resultCallback); } std::optional PhysicsTaskScheduler::getHitPoint(const btTransform& from, btCollisionObject* target) { MaybeSharedLock lock(mCollisionWorldMutex, mThreadSafeBullet); // target the collision object's world origin, this should be the center of the collision object btTransform rayTo; rayTo.setIdentity(); rayTo.setOrigin(target->getWorldTransform().getOrigin()); btCollisionWorld::ClosestRayResultCallback cb(from.getOrigin(), rayTo.getOrigin()); mCollisionWorld->rayTestSingle(from, rayTo, target, target->getCollisionShape(), target->getWorldTransform(), cb); if (!cb.hasHit()) // didn't hit the target. this could happen if point is already inside the collision box return std::nullopt; return {cb.m_hitPointWorld}; } void PhysicsTaskScheduler::aabbTest(const btVector3& aabbMin, const btVector3& aabbMax, btBroadphaseAabbCallback& callback) { std::shared_lock lock(mCollisionWorldMutex); mCollisionWorld->getBroadphase()->aabbTest(aabbMin, aabbMax, callback); } void PhysicsTaskScheduler::getAabb(const btCollisionObject* obj, btVector3& min, btVector3& max) { std::shared_lock lock(mCollisionWorldMutex); obj->getCollisionShape()->getAabb(obj->getWorldTransform(), min, max); } void PhysicsTaskScheduler::setCollisionFilterMask(btCollisionObject* collisionObject, int collisionFilterMask) { std::unique_lock lock(mCollisionWorldMutex); collisionObject->getBroadphaseHandle()->m_collisionFilterMask = collisionFilterMask; } void PhysicsTaskScheduler::addCollisionObject(btCollisionObject* collisionObject, int collisionFilterGroup, int collisionFilterMask) { std::unique_lock lock(mCollisionWorldMutex); mCollisionWorld->addCollisionObject(collisionObject, collisionFilterGroup, collisionFilterMask); } void PhysicsTaskScheduler::removeCollisionObject(btCollisionObject* collisionObject) { std::unique_lock lock(mCollisionWorldMutex); mCollisionWorld->removeCollisionObject(collisionObject); } void PhysicsTaskScheduler::updateSingleAabb(std::weak_ptr ptr, bool immediate) { if (!mDeferAabbUpdate || immediate) { updatePtrAabb(ptr); } else { std::unique_lock lock(mUpdateAabbMutex); mUpdateAabb.insert(std::move(ptr)); } } bool PhysicsTaskScheduler::getLineOfSight(const std::weak_ptr& actor1, const std::weak_ptr& actor2) { std::unique_lock lock(mLOSCacheMutex); auto actorPtr1 = actor1.lock(); auto actorPtr2 = actor2.lock(); if (!actorPtr1 || !actorPtr2) return false; auto req = LOSRequest(actor1, actor2); auto result = std::find(mLOSCache.begin(), mLOSCache.end(), req); if (result == mLOSCache.end()) { req.mResult = hasLineOfSight(actorPtr1.get(), actorPtr2.get()); if (mLOSCacheExpiry >= 0) mLOSCache.push_back(req); return req.mResult; } result->mAge = 0; return result->mResult; } void PhysicsTaskScheduler::refreshLOSCache() { std::shared_lock lock(mLOSCacheMutex); int job = 0; int numLOS = mLOSCache.size(); while ((job = mNextLOS.fetch_add(1, std::memory_order_relaxed)) < numLOS) { auto& req = mLOSCache[job]; auto actorPtr1 = req.mActors[0].lock(); auto actorPtr2 = req.mActors[1].lock(); if (req.mAge++ > mLOSCacheExpiry || !actorPtr1 || !actorPtr2) req.mStale = true; else req.mResult = hasLineOfSight(actorPtr1.get(), actorPtr2.get()); } } void PhysicsTaskScheduler::updateAabbs() { std::scoped_lock lock(mUpdateAabbMutex); std::for_each(mUpdateAabb.begin(), mUpdateAabb.end(), [this](const std::weak_ptr& ptr) { updatePtrAabb(ptr); }); mUpdateAabb.clear(); } void PhysicsTaskScheduler::updatePtrAabb(const std::weak_ptr& ptr) { if (const auto p = ptr.lock()) { std::scoped_lock lock(mCollisionWorldMutex); if (const auto actor = std::dynamic_pointer_cast(p)) { actor->updateCollisionObjectPosition(); mCollisionWorld->updateSingleAabb(actor->getCollisionObject()); } else if (const auto object = std::dynamic_pointer_cast(p)) { object->commitPositionChange(); mCollisionWorld->updateSingleAabb(object->getCollisionObject()); } else if (const auto projectile = std::dynamic_pointer_cast(p)) { projectile->commitPositionChange(); mCollisionWorld->updateSingleAabb(projectile->getCollisionObject()); } }; } void PhysicsTaskScheduler::worker() { std::size_t lastFrame = 0; std::shared_lock lock(mSimulationMutex); while (!mQuit) { if (mRemainingSteps == 0 && lastFrame == mFrameCounter) mHasJob.wait(lock, [&] { return mQuit || lastFrame != mFrameCounter; }); lastFrame = mFrameCounter; mPreStepBarrier->wait([this] { afterPreStep(); }); int job = 0; while (mRemainingSteps && (job = mNextJob.fetch_add(1, std::memory_order_relaxed)) < mNumJobs) { if(const auto actor = mActorsFrameData[job].mActor.lock()) { MaybeSharedLock lockColWorld(mCollisionWorldMutex, mThreadSafeBullet); MovementSolver::move(mActorsFrameData[job], mPhysicsDt, mCollisionWorld, *mWorldFrameData); } } mPostStepBarrier->wait([this] { afterPostStep(); }); if (!mRemainingSteps) { while ((job = mNextJob.fetch_add(1, std::memory_order_relaxed)) < mNumJobs) { if(const auto actor = mActorsFrameData[job].mActor.lock()) { auto& actorData = mActorsFrameData[job]; handleFall(actorData, mAdvanceSimulation); } } if (mLOSCacheExpiry >= 0) refreshLOSCache(); mPostSimBarrier->wait([this] { afterPostSim(); }); } } } void PhysicsTaskScheduler::updateActorsPositions() { for (auto& actorData : mActorsFrameData) { if(const auto actor = actorData.mActor.lock()) { if (actor->setPosition(actorData.mPosition)) { std::scoped_lock lock(mCollisionWorldMutex); actorData.mPosition = actor->getPosition(); // account for potential position change made by script actor->updateCollisionObjectPosition(); mCollisionWorld->updateSingleAabb(actor->getCollisionObject()); } } } } bool PhysicsTaskScheduler::hasLineOfSight(const Actor* actor1, const Actor* actor2) { btVector3 pos1 = Misc::Convert::toBullet(actor1->getCollisionObjectPosition() + osg::Vec3f(0,0,actor1->getHalfExtents().z() * 0.9)); // eye level btVector3 pos2 = Misc::Convert::toBullet(actor2->getCollisionObjectPosition() + osg::Vec3f(0,0,actor2->getHalfExtents().z() * 0.9)); btCollisionWorld::ClosestRayResultCallback resultCallback(pos1, pos2); resultCallback.m_collisionFilterGroup = 0xFF; resultCallback.m_collisionFilterMask = CollisionType_World|CollisionType_HeightMap|CollisionType_Door; MaybeSharedLock lockColWorld(mCollisionWorldMutex, mThreadSafeBullet); mCollisionWorld->rayTest(pos1, pos2, resultCallback); return !resultCallback.hasHit(); } void PhysicsTaskScheduler::syncComputation() { while (mRemainingSteps--) { for (auto& actorData : mActorsFrameData) { MovementSolver::unstuck(actorData, mCollisionWorld); MovementSolver::move(actorData, mPhysicsDt, mCollisionWorld, *mWorldFrameData); } updateActorsPositions(); } for (auto& actorData : mActorsFrameData) { handleFall(actorData, mAdvanceSimulation); actorData.mActorRaw->setSimulationPosition(interpolateMovements(actorData, mTimeAccum, mPhysicsDt)); updateMechanics(actorData); mMovedActors.emplace_back(actorData.mActorRaw->getPtr()); if (mAdvanceSimulation) actorData.mActorRaw->setStandingOnPtr(actorData.mStandingOn); } } void PhysicsTaskScheduler::updateStats(osg::Timer_t frameStart, unsigned int frameNumber, osg::Stats& stats) { if (!stats.collectStats("engine")) return; if (mFrameNumber == frameNumber - 1) { stats.setAttribute(mFrameNumber, "physicsworker_time_begin", mTimer->delta_s(mFrameStart, mTimeBegin)); stats.setAttribute(mFrameNumber, "physicsworker_time_taken", mTimer->delta_s(mTimeBegin, mTimeEnd)); stats.setAttribute(mFrameNumber, "physicsworker_time_end", mTimer->delta_s(mFrameStart, mTimeEnd)); } mFrameStart = frameStart; mTimeBegin = mTimer->tick(); mFrameNumber = frameNumber; } void PhysicsTaskScheduler::debugDraw() { std::shared_lock lock(mCollisionWorldMutex); mDebugDrawer->step(); } void PhysicsTaskScheduler::afterPreStep() { if (mDeferAabbUpdate) updateAabbs(); if (!mRemainingSteps) return; for (auto& data : mActorsFrameData) if (const auto actor = data.mActor.lock()) { std::unique_lock lock(mCollisionWorldMutex); MovementSolver::unstuck(data, mCollisionWorld); } } void PhysicsTaskScheduler::afterPostStep() { if (mRemainingSteps) { --mRemainingSteps; updateActorsPositions(); } mNextJob.store(0, std::memory_order_release); } void PhysicsTaskScheduler::afterPostSim() { if (mLOSCacheExpiry >= 0) { std::unique_lock lock(mLOSCacheMutex); mLOSCache.erase( std::remove_if(mLOSCache.begin(), mLOSCache.end(), [](const LOSRequest& req) { return req.mStale; }), mLOSCache.end()); } mTimeEnd = mTimer->tick(); std::unique_lock lock(mWorkersDoneMutex); ++mWorkersFrameCounter; mWorkersDone.notify_all(); } // 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); } } ================================================ FILE: apps/openmw/mwphysics/mtphysics.hpp ================================================ #ifndef OPENMW_MWPHYSICS_MTPHYSICS_H #define OPENMW_MWPHYSICS_MTPHYSICS_H #include #include #include #include #include #include #include #include "physicssystem.hpp" #include "ptrholder.hpp" #include "components/misc/budgetmeasurement.hpp" namespace Misc { class Barrier; } namespace MWRender { class DebugDrawer; } namespace MWPhysics { class PhysicsTaskScheduler { public: PhysicsTaskScheduler(float physicsDt, btCollisionWorld* collisionWorld, MWRender::DebugDrawer* debugDrawer); ~PhysicsTaskScheduler(); /// @brief move actors taking into account desired movements and collisions /// @param numSteps how much simulation step to run /// @param timeAccum accumulated time from previous run to interpolate movements /// @param actorsData per actor data needed to compute new positions /// @return new position of each actor const std::vector& moveActors(float & timeAccum, std::vector&& actorsData, osg::Timer_t frameStart, unsigned int frameNumber, osg::Stats& stats); const std::vector& resetSimulation(const ActorMap& actors); // Thread safe wrappers void rayTest(const btVector3& rayFromWorld, const btVector3& rayToWorld, btCollisionWorld::RayResultCallback& resultCallback) const; void convexSweepTest(const btConvexShape* castShape, const btTransform& from, const btTransform& to, btCollisionWorld::ConvexResultCallback& resultCallback) const; void contactTest(btCollisionObject* colObj, btCollisionWorld::ContactResultCallback& resultCallback); std::optional getHitPoint(const btTransform& from, btCollisionObject* target); void aabbTest(const btVector3& aabbMin, const btVector3& aabbMax, btBroadphaseAabbCallback& callback); void getAabb(const btCollisionObject* obj, btVector3& min, btVector3& max); void setCollisionFilterMask(btCollisionObject* collisionObject, int collisionFilterMask); void addCollisionObject(btCollisionObject* collisionObject, int collisionFilterGroup, int collisionFilterMask); void removeCollisionObject(btCollisionObject* collisionObject); void updateSingleAabb(std::weak_ptr ptr, bool immediate=false); bool getLineOfSight(const std::weak_ptr& actor1, const std::weak_ptr& actor2); void debugDraw(); private: void syncComputation(); void worker(); void updateActorsPositions(); bool hasLineOfSight(const Actor* actor1, const Actor* actor2); void refreshLOSCache(); void updateAabbs(); void updatePtrAabb(const std::weak_ptr& ptr); void updateStats(osg::Timer_t frameStart, unsigned int frameNumber, osg::Stats& stats); std::tuple calculateStepConfig(float timeAccum) const; void afterPreStep(); void afterPostStep(); void afterPostSim(); void waitForWorkers(); std::unique_ptr mWorldFrameData; std::vector mActorsFrameData; std::vector mMovedActors; float mDefaultPhysicsDt; /* Start of tes3mp change (major) Turn mPhysicsDt into a public variable so it can be set from elsewhere */ public: float mPhysicsDt; private: /* End of tes3mp change (major) */ float mTimeAccum; btCollisionWorld* mCollisionWorld; MWRender::DebugDrawer* mDebugDrawer; std::vector mLOSCache; std::set, std::owner_less>> mUpdateAabb; // TODO: use std::experimental::flex_barrier or std::barrier once it becomes a thing std::unique_ptr mPreStepBarrier; std::unique_ptr mPostStepBarrier; std::unique_ptr mPostSimBarrier; int mNumThreads; int mNumJobs; int mRemainingSteps; int mLOSCacheExpiry; bool mDeferAabbUpdate; std::size_t mFrameCounter; bool mAdvanceSimulation; bool mThreadSafeBullet; bool mQuit; std::atomic mNextJob; std::atomic mNextLOS; std::vector 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; mutable std::mutex mUpdateAabbMutex; std::condition_variable_any mHasJob; unsigned int mFrameNumber; const osg::Timer* mTimer; int mPrevStepCount; Misc::BudgetMeasurement mBudget; Misc::BudgetMeasurement mAsyncBudget; unsigned int mBudgetCursor; osg::Timer_t mAsyncStartTime; osg::Timer_t mTimeBegin; osg::Timer_t mTimeEnd; osg::Timer_t mFrameStart; }; } #endif ================================================ FILE: apps/openmw/mwphysics/object.cpp ================================================ #include "object.hpp" #include "mtphysics.hpp" #include #include #include #include #include #include #include #include namespace MWPhysics { Object::Object(const MWWorld::Ptr& ptr, osg::ref_ptr shapeInstance, int collisionType, PhysicsTaskScheduler* scheduler) : mShapeInstance(shapeInstance) , mSolid(true) , mTaskScheduler(scheduler) { mPtr = ptr; mCollisionObject = std::make_unique(); mCollisionObject->setCollisionShape(shapeInstance->getCollisionShape()); mCollisionObject->setUserPointer(this); setScale(ptr.getCellRef().getScale()); setRotation(ptr.getRefData().getBaseNode()->getAttitude()); updatePosition(); commitPositionChange(); mTaskScheduler->addCollisionObject(mCollisionObject.get(), collisionType, CollisionType_Actor|CollisionType_HeightMap|CollisionType_Projectile); } Object::~Object() { mTaskScheduler->removeCollisionObject(mCollisionObject.get()); } const Resource::BulletShapeInstance* Object::getShapeInstance() const { return mShapeInstance.get(); } void Object::setScale(float scale) { std::unique_lock lock(mPositionMutex); mScale = { scale,scale,scale }; mScaleUpdatePending = true; } void Object::setRotation(const osg::Quat& quat) { std::unique_lock lock(mPositionMutex); mRotation = quat; mTransformUpdatePending = true; } void Object::updatePosition() { std::unique_lock lock(mPositionMutex); mPosition = mPtr.getRefData().getPosition().asVec3(); mTransformUpdatePending = true; } void Object::commitPositionChange() { std::unique_lock lock(mPositionMutex); if (mScaleUpdatePending) { mShapeInstance->setLocalScaling(mScale); mScaleUpdatePending = false; } if (mTransformUpdatePending) { btTransform trans; trans.setOrigin(Misc::Convert::toBullet(mPosition)); trans.setRotation(Misc::Convert::toBullet(mRotation)); mCollisionObject->setWorldTransform(trans); mTransformUpdatePending = false; } } btCollisionObject* Object::getCollisionObject() { return mCollisionObject.get(); } const btCollisionObject* Object::getCollisionObject() const { return mCollisionObject.get(); } btTransform Object::getTransform() const { std::unique_lock lock(mPositionMutex); btTransform trans; trans.setOrigin(Misc::Convert::toBullet(mPosition)); trans.setRotation(Misc::Convert::toBullet(mRotation)); return trans; } bool Object::isSolid() const { return mSolid; } void Object::setSolid(bool solid) { mSolid = solid; } bool Object::isAnimated() const { return !mShapeInstance->mAnimatedShapes.empty(); } bool Object::animateCollisionShapes() { if (mShapeInstance->mAnimatedShapes.empty()) return false; assert (mShapeInstance->getCollisionShape()->isCompound()); btCompoundShape* compound = static_cast(mShapeInstance->getCollisionShape()); for (const auto& [recIndex, shapeIndex] : mShapeInstance->mAnimatedShapes) { auto nodePathFound = mRecIndexToNodePath.find(recIndex); if (nodePathFound == mRecIndexToNodePath.end()) { NifOsg::FindGroupByRecIndex visitor(recIndex); mPtr.getRefData().getBaseNode()->accept(visitor); if (!visitor.mFound) { Log(Debug::Warning) << "Warning: animateCollisionShapes can't find node " << recIndex << " for " << mPtr.getCellRef().getRefId(); // Remove nonexistent nodes from animated shapes map and early out mShapeInstance->mAnimatedShapes.erase(recIndex); return false; } osg::NodePath nodePath = visitor.mFoundPath; nodePath.erase(nodePath.begin()); nodePathFound = mRecIndexToNodePath.emplace(recIndex, nodePath).first; } osg::NodePath& nodePath = nodePathFound->second; osg::Matrixf matrix = osg::computeLocalToWorld(nodePath); matrix.orthoNormalize(matrix); btTransform transform; transform.setOrigin(Misc::Convert::toBullet(matrix.getTrans()) * compound->getLocalScaling()); for (int i=0; i<3; ++i) for (int j=0; j<3; ++j) transform.getBasis()[i][j] = matrix(j,i); // NB column/row major difference // Note: we can not apply scaling here for now since we treat scaled shapes // as new shapes (btScaledBvhTriangleMeshShape) with 1.0 scale for now if (!(transform == compound->getChildTransform(shapeIndex))) compound->updateChildTransform(shapeIndex, transform); } return true; } } ================================================ FILE: apps/openmw/mwphysics/object.hpp ================================================ #ifndef OPENMW_MWPHYSICS_OBJECT_H #define OPENMW_MWPHYSICS_OBJECT_H #include "ptrholder.hpp" #include #include #include #include #include namespace Resource { class BulletShapeInstance; } class btCollisionObject; class btVector3; namespace MWPhysics { class PhysicsTaskScheduler; class Object final : public PtrHolder { public: Object(const MWWorld::Ptr& ptr, osg::ref_ptr shapeInstance, int collisionType, PhysicsTaskScheduler* scheduler); ~Object() override; const Resource::BulletShapeInstance* getShapeInstance() const; void setScale(float scale); void setRotation(const osg::Quat& quat); void updatePosition(); void commitPositionChange(); btCollisionObject* getCollisionObject(); const btCollisionObject* getCollisionObject() const; btTransform getTransform() const; /// Return solid flag. Not used by the object itself, true by default. bool isSolid() const; void setSolid(bool solid); bool isAnimated() const; /// @brief update object shape /// @return true if shape changed bool animateCollisionShapes(); private: std::unique_ptr mCollisionObject; osg::ref_ptr mShapeInstance; std::map mRecIndexToNodePath; bool mSolid; btVector3 mScale; osg::Vec3f mPosition; osg::Quat mRotation; bool mScaleUpdatePending; bool mTransformUpdatePending; mutable std::mutex mPositionMutex; PhysicsTaskScheduler* mTaskScheduler; }; } #endif ================================================ FILE: apps/openmw/mwphysics/physicssystem.cpp ================================================ #include "physicssystem.hpp" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include // FindRecIndexVisitor #include "../mwbase/world.hpp" #include "../mwbase/environment.hpp" #include "../mwmechanics/creaturestats.hpp" #include "../mwmechanics/actorutil.hpp" #include "../mwmechanics/movement.hpp" #include "../mwworld/esmstore.hpp" #include "../mwworld/cellstore.hpp" #include "../mwrender/bulletdebugdraw.hpp" #include "../mwworld/class.hpp" #include "collisiontype.hpp" #include "actor.hpp" #include "projectile.hpp" #include "trace.h" #include "object.hpp" #include "heightfield.hpp" #include "hasspherecollisioncallback.hpp" #include "deepestnotmecontacttestresultcallback.hpp" #include "closestnotmerayresultcallback.hpp" #include "contacttestresultcallback.hpp" #include "projectileconvexcallback.hpp" #include "movementsolver.hpp" #include "mtphysics.hpp" namespace { bool canMoveToWaterSurface(const MWPhysics::Actor* physicActor, const float waterlevel, btCollisionWorld* world) { if (!physicActor) return false; const float halfZ = physicActor->getHalfExtents().z(); const osg::Vec3f actorPosition = physicActor->getPosition(); const osg::Vec3f startingPosition(actorPosition.x(), actorPosition.y(), actorPosition.z() + halfZ); const osg::Vec3f destinationPosition(actorPosition.x(), actorPosition.y(), waterlevel + halfZ); MWPhysics::ActorTracer tracer; tracer.doTrace(physicActor->getCollisionObject(), startingPosition, destinationPosition, world); return (tracer.mFraction >= 1.0f); } } namespace MWPhysics { PhysicsSystem::PhysicsSystem(Resource::ResourceSystem* resourceSystem, osg::ref_ptr parentNode) : mShapeManager(new Resource::BulletShapeManager(resourceSystem->getVFS(), resourceSystem->getSceneManager(), resourceSystem->getNifFileManager())) , mResourceSystem(resourceSystem) , mDebugDrawEnabled(false) , mTimeAccum(0.0f) , mProjectileId(0) , mWaterHeight(0) , mWaterEnabled(false) , mParentNode(parentNode) , mPhysicsDt(1.f / 60.f) { mResourceSystem->addResourceManager(mShapeManager.get()); mCollisionConfiguration = std::make_unique(); mDispatcher = std::make_unique(mCollisionConfiguration.get()); mBroadphase = std::make_unique(); mCollisionWorld = std::make_unique(mDispatcher.get(), mBroadphase.get(), mCollisionConfiguration.get()); // Don't update AABBs of all objects every frame. Most objects in MW are static, so we don't need this. // Should a "static" object ever be moved, we have to update its AABB manually using DynamicsWorld::updateSingleAabb. mCollisionWorld->setForceUpdateAllAabbs(false); // Check if a user decided to override a physics system FPS const char* env = getenv("OPENMW_PHYSICS_FPS"); if (env) { float physFramerate = std::atof(env); if (physFramerate > 0) { mPhysicsDt = 1.f / physFramerate; Log(Debug::Warning) << "Warning: using custom physics framerate (" << physFramerate << " FPS)."; } } mDebugDrawer = std::make_unique(mParentNode, mCollisionWorld.get(), mDebugDrawEnabled); mTaskScheduler = std::make_unique(mPhysicsDt, mCollisionWorld.get(), mDebugDrawer.get()); } PhysicsSystem::~PhysicsSystem() { mResourceSystem->removeResourceManager(mShapeManager.get()); if (mWaterCollisionObject) mTaskScheduler->removeCollisionObject(mWaterCollisionObject.get()); mHeightFields.clear(); mObjects.clear(); mActors.clear(); mProjectiles.clear(); } void PhysicsSystem::setUnrefQueue(SceneUtil::UnrefQueue *unrefQueue) { mUnrefQueue = unrefQueue; } Resource::BulletShapeManager *PhysicsSystem::getShapeManager() { return mShapeManager.get(); } bool PhysicsSystem::toggleDebugRendering() { mDebugDrawEnabled = !mDebugDrawEnabled; mCollisionWorld->setDebugDrawer(mDebugDrawEnabled ? mDebugDrawer.get() : nullptr); mDebugDrawer->setDebugMode(mDebugDrawEnabled); return mDebugDrawEnabled; } void PhysicsSystem::markAsNonSolid(const MWWorld::ConstPtr &ptr) { ObjectMap::iterator found = mObjects.find(ptr); if (found == mObjects.end()) return; found->second->setSolid(false); } bool PhysicsSystem::isOnSolidGround (const MWWorld::Ptr& actor) const { const Actor* physactor = getActor(actor); if (!physactor || !physactor->getOnGround()) return false; const auto obj = physactor->getStandingOnPtr(); if (obj.isEmpty()) return true; // assume standing on terrain (which is a non-object, so not collision tracked) ObjectMap::const_iterator foundObj = mObjects.find(obj); if (foundObj == mObjects.end()) return false; if (!foundObj->second->isSolid()) return false; return true; } /* Start of tes3mp addition Make it possible to set the physics framerate from elsewhere */ void PhysicsSystem::setPhysicsFramerate(float physFramerate) { if (physFramerate > 0 && physFramerate < 100) { mPhysicsDt = 1.f / physFramerate; mTaskScheduler->mPhysicsDt = mPhysicsDt; std::cerr << "Warning: physics framerate was overridden (a new value is " << physFramerate << ")." << std::endl; } else { std::cerr << "Warning: attempted to override physics framerate with new value of " << physFramerate << ", but it was outside accepted values." << std::endl; } } /* End of tes3mp addition */ std::pair PhysicsSystem::getHitContact(const MWWorld::ConstPtr& actor, const osg::Vec3f &origin, const osg::Quat &orient, float queryDistance, std::vector& targets) { // First of all, try to hit where you aim to int hitmask = CollisionType_World | CollisionType_Door | CollisionType_HeightMap | CollisionType_Actor; RayCastingResult result = castRay(origin, origin + (orient * osg::Vec3f(0.0f, queryDistance, 0.0f)), actor, targets, hitmask, CollisionType_Actor); if (result.mHit) { reportCollision(Misc::Convert::toBullet(result.mHitPos), Misc::Convert::toBullet(result.mHitNormal)); return std::make_pair(result.mHitObject, result.mHitPos); } // Use cone shape as fallback const MWWorld::Store &store = MWBase::Environment::get().getWorld()->getStore().get(); btConeShape shape (osg::DegreesToRadians(store.find("fCombatAngleXY")->mValue.getFloat()/2.0f), queryDistance); shape.setLocalScaling(btVector3(1, 1, osg::DegreesToRadians(store.find("fCombatAngleZ")->mValue.getFloat()/2.0f) / shape.getRadius())); // The shape origin is its center, so we have to move it forward by half the length. The // real origin will be provided to getFilteredContact to find the closest. osg::Vec3f center = origin + (orient * osg::Vec3f(0.0f, queryDistance*0.5f, 0.0f)); btCollisionObject object; object.setCollisionShape(&shape); object.setWorldTransform(btTransform(Misc::Convert::toBullet(orient), Misc::Convert::toBullet(center))); const btCollisionObject* me = nullptr; std::vector targetCollisionObjects; const Actor* physactor = getActor(actor); if (physactor) me = physactor->getCollisionObject(); if (!targets.empty()) { for (MWWorld::Ptr& target : targets) { const Actor* targetActor = getActor(target); if (targetActor) targetCollisionObjects.push_back(targetActor->getCollisionObject()); } } DeepestNotMeContactTestResultCallback resultCallback(me, targetCollisionObjects, Misc::Convert::toBullet(origin)); resultCallback.m_collisionFilterGroup = CollisionType_Actor; resultCallback.m_collisionFilterMask = CollisionType_World | CollisionType_Door | CollisionType_HeightMap | CollisionType_Actor; mTaskScheduler->contactTest(&object, resultCallback); if (resultCallback.mObject) { PtrHolder* holder = static_cast(resultCallback.mObject->getUserPointer()); if (holder) { reportCollision(resultCallback.mContactPoint, resultCallback.mContactNormal); return std::make_pair(holder->getPtr(), Misc::Convert::toOsg(resultCallback.mContactPoint)); } } return std::make_pair(MWWorld::Ptr(), osg::Vec3f()); } float PhysicsSystem::getHitDistance(const osg::Vec3f &point, const MWWorld::ConstPtr &target) const { btCollisionObject* targetCollisionObj = nullptr; const Actor* actor = getActor(target); if (actor) targetCollisionObj = actor->getCollisionObject(); if (!targetCollisionObj) return 0.f; btTransform rayFrom; rayFrom.setIdentity(); rayFrom.setOrigin(Misc::Convert::toBullet(point)); auto hitpoint = mTaskScheduler->getHitPoint(rayFrom, targetCollisionObj); if (hitpoint) return (point - Misc::Convert::toOsg(*hitpoint)).length(); // didn't hit the target. this could happen if point is already inside the collision box return 0.f; } RayCastingResult PhysicsSystem::castRay(const osg::Vec3f &from, const osg::Vec3f &to, const MWWorld::ConstPtr& ignore, std::vector targets, int mask, int group) const { if (from == to) { RayCastingResult result; result.mHit = false; return result; } btVector3 btFrom = Misc::Convert::toBullet(from); btVector3 btTo = Misc::Convert::toBullet(to); const btCollisionObject* me = nullptr; std::vector targetCollisionObjects; if (!ignore.isEmpty()) { const Actor* actor = getActor(ignore); if (actor) me = actor->getCollisionObject(); else { const Object* object = getObject(ignore); if (object) me = object->getCollisionObject(); } } if (!targets.empty()) { for (MWWorld::Ptr& target : targets) { const Actor* actor = getActor(target); if (actor) targetCollisionObjects.push_back(actor->getCollisionObject()); } } ClosestNotMeRayResultCallback resultCallback(me, targetCollisionObjects, btFrom, btTo); resultCallback.m_collisionFilterGroup = group; resultCallback.m_collisionFilterMask = mask; mTaskScheduler->rayTest(btFrom, btTo, resultCallback); RayCastingResult result; result.mHit = resultCallback.hasHit(); if (resultCallback.hasHit()) { result.mHitPos = Misc::Convert::toOsg(resultCallback.m_hitPointWorld); result.mHitNormal = Misc::Convert::toOsg(resultCallback.m_hitNormalWorld); if (PtrHolder* ptrHolder = static_cast(resultCallback.m_collisionObject->getUserPointer())) result.mHitObject = ptrHolder->getPtr(); } return result; } RayCastingResult PhysicsSystem::castSphere(const osg::Vec3f &from, const osg::Vec3f &to, float radius) const { btCollisionWorld::ClosestConvexResultCallback callback(Misc::Convert::toBullet(from), Misc::Convert::toBullet(to)); callback.m_collisionFilterGroup = 0xff; callback.m_collisionFilterMask = CollisionType_World|CollisionType_HeightMap|CollisionType_Door; btSphereShape shape(radius); const btQuaternion btrot = btQuaternion::getIdentity(); btTransform from_ (btrot, Misc::Convert::toBullet(from)); btTransform to_ (btrot, Misc::Convert::toBullet(to)); mTaskScheduler->convexSweepTest(&shape, from_, to_, callback); RayCastingResult result; result.mHit = callback.hasHit(); if (result.mHit) { result.mHitPos = Misc::Convert::toOsg(callback.m_hitPointWorld); result.mHitNormal = Misc::Convert::toOsg(callback.m_hitNormalWorld); } return result; } bool PhysicsSystem::getLineOfSight(const MWWorld::ConstPtr &actor1, const MWWorld::ConstPtr &actor2) const { const auto getWeakPtr = [&](const MWWorld::ConstPtr &ptr) -> std::weak_ptr { const auto found = mActors.find(ptr); if (found != mActors.end()) return { found->second }; return {}; }; return mTaskScheduler->getLineOfSight(getWeakPtr(actor1), getWeakPtr(actor2)); } bool PhysicsSystem::isOnGround(const MWWorld::Ptr &actor) { Actor* physactor = getActor(actor); return physactor && physactor->getOnGround(); } bool PhysicsSystem::canMoveToWaterSurface(const MWWorld::ConstPtr &actor, const float waterlevel) { return ::canMoveToWaterSurface(getActor(actor), waterlevel, mCollisionWorld.get()); } osg::Vec3f PhysicsSystem::getHalfExtents(const MWWorld::ConstPtr &actor) const { const Actor* physactor = getActor(actor); if (physactor) return physactor->getHalfExtents(); else return osg::Vec3f(); } osg::Vec3f PhysicsSystem::getOriginalHalfExtents(const MWWorld::ConstPtr &actor) const { if (const Actor* physactor = getActor(actor)) return physactor->getOriginalHalfExtents(); else return osg::Vec3f(); } osg::Vec3f PhysicsSystem::getRenderingHalfExtents(const MWWorld::ConstPtr &actor) const { const Actor* physactor = getActor(actor); if (physactor) return physactor->getRenderingHalfExtents(); else return osg::Vec3f(); } osg::BoundingBox PhysicsSystem::getBoundingBox(const MWWorld::ConstPtr &object) const { const Object * physobject = getObject(object); if (!physobject) return osg::BoundingBox(); btVector3 min, max; mTaskScheduler->getAabb(physobject->getCollisionObject(), min, max); return osg::BoundingBox(Misc::Convert::toOsg(min), Misc::Convert::toOsg(max)); } osg::Vec3f PhysicsSystem::getCollisionObjectPosition(const MWWorld::ConstPtr &actor) const { const Actor* physactor = getActor(actor); if (physactor) return physactor->getCollisionObjectPosition(); else return osg::Vec3f(); } std::vector PhysicsSystem::getCollisionsPoints(const MWWorld::ConstPtr &ptr, int collisionGroup, int collisionMask) const { btCollisionObject* me = nullptr; auto found = mObjects.find(ptr); if (found != mObjects.end()) me = found->second->getCollisionObject(); else return {}; ContactTestResultCallback resultCallback (me); resultCallback.m_collisionFilterGroup = collisionGroup; resultCallback.m_collisionFilterMask = collisionMask; mTaskScheduler->contactTest(me, resultCallback); return resultCallback.mResult; } std::vector PhysicsSystem::getCollisions(const MWWorld::ConstPtr &ptr, int collisionGroup, int collisionMask) const { std::vector actors; for (auto& [actor, point, normal] : getCollisionsPoints(ptr, collisionGroup, collisionMask)) actors.emplace_back(actor); return actors; } osg::Vec3f PhysicsSystem::traceDown(const MWWorld::Ptr &ptr, const osg::Vec3f& position, float maxHeight) { ActorMap::iterator found = mActors.find(ptr); if (found == mActors.end()) return ptr.getRefData().getPosition().asVec3(); return MovementSolver::traceDown(ptr, position, found->second.get(), mCollisionWorld.get(), maxHeight); } void PhysicsSystem::addHeightField (const float* heights, int x, int y, float triSize, float sqrtVerts, float minH, float maxH, const osg::Object* holdObject) { mHeightFields[std::make_pair(x,y)] = osg::ref_ptr(new HeightField(heights, x, y, triSize, sqrtVerts, minH, maxH, holdObject, mTaskScheduler.get())); } void PhysicsSystem::removeHeightField (int x, int y) { HeightFieldMap::iterator heightfield = mHeightFields.find(std::make_pair(x,y)); if(heightfield != mHeightFields.end()) mHeightFields.erase(heightfield); } const HeightField* PhysicsSystem::getHeightField(int x, int y) const { const auto heightField = mHeightFields.find(std::make_pair(x, y)); if (heightField == mHeightFields.end()) return nullptr; return heightField->second.get(); } void PhysicsSystem::addObject (const MWWorld::Ptr& ptr, const std::string& mesh, int collisionType) { osg::ref_ptr shapeInstance = mShapeManager->getInstance(mesh); if (!shapeInstance || !shapeInstance->getCollisionShape()) return; auto obj = std::make_shared(ptr, shapeInstance, collisionType, mTaskScheduler.get()); mObjects.emplace(ptr, obj); if (obj->isAnimated()) mAnimatedObjects.insert(obj.get()); } void PhysicsSystem::remove(const MWWorld::Ptr &ptr) { ObjectMap::iterator found = mObjects.find(ptr); if (found != mObjects.end()) { if (mUnrefQueue.get()) mUnrefQueue->push(found->second->getShapeInstance()); mAnimatedObjects.erase(found->second.get()); mObjects.erase(found); } ActorMap::iterator foundActor = mActors.find(ptr); if (foundActor != mActors.end()) { mActors.erase(foundActor); } } void PhysicsSystem::removeProjectile(const int projectileId) { ProjectileMap::iterator foundProjectile = mProjectiles.find(projectileId); if (foundProjectile != mProjectiles.end()) mProjectiles.erase(foundProjectile); } void PhysicsSystem::updatePtr(const MWWorld::Ptr &old, const MWWorld::Ptr &updated) { ObjectMap::iterator found = mObjects.find(old); if (found != mObjects.end()) { auto obj = found->second; obj->updatePtr(updated); mObjects.erase(found); mObjects.emplace(updated, std::move(obj)); } ActorMap::iterator foundActor = mActors.find(old); if (foundActor != mActors.end()) { auto actor = foundActor->second; actor->updatePtr(updated); mActors.erase(foundActor); mActors.emplace(updated, std::move(actor)); } for (auto& [_, actor] : mActors) { if (actor->getStandingOnPtr() == old) actor->setStandingOnPtr(updated); } for (auto& [_, projectile] : mProjectiles) { if (projectile->getCaster() == old) projectile->setCaster(updated); } } Actor *PhysicsSystem::getActor(const MWWorld::Ptr &ptr) { ActorMap::iterator found = mActors.find(ptr); if (found != mActors.end()) return found->second.get(); return nullptr; } const Actor *PhysicsSystem::getActor(const MWWorld::ConstPtr &ptr) const { ActorMap::const_iterator found = mActors.find(ptr); if (found != mActors.end()) return found->second.get(); return nullptr; } const Object* PhysicsSystem::getObject(const MWWorld::ConstPtr &ptr) const { ObjectMap::const_iterator found = mObjects.find(ptr); if (found != mObjects.end()) return found->second.get(); return nullptr; } Projectile* PhysicsSystem::getProjectile(int projectileId) const { ProjectileMap::const_iterator found = mProjectiles.find(projectileId); if (found != mProjectiles.end()) return found->second.get(); return nullptr; } void PhysicsSystem::updateScale(const MWWorld::Ptr &ptr) { ObjectMap::iterator found = mObjects.find(ptr); if (found != mObjects.end()) { float scale = ptr.getCellRef().getScale(); found->second->setScale(scale); mTaskScheduler->updateSingleAabb(found->second); return; } ActorMap::iterator foundActor = mActors.find(ptr); if (foundActor != mActors.end()) { foundActor->second->updateScale(); mTaskScheduler->updateSingleAabb(foundActor->second); return; } } void PhysicsSystem::updateProjectile(const int projectileId, const osg::Vec3f &position) const { const auto foundProjectile = mProjectiles.find(projectileId); assert(foundProjectile != mProjectiles.end()); auto* projectile = foundProjectile->second.get(); btVector3 btFrom = Misc::Convert::toBullet(projectile->getPosition()); btVector3 btTo = Misc::Convert::toBullet(position); if (btFrom == btTo) return; const auto casterPtr = projectile->getCaster(); const auto* caster = [this,&casterPtr]() -> const btCollisionObject* { const Actor* actor = getActor(casterPtr); if (actor) return actor->getCollisionObject(); const Object* object = getObject(casterPtr); if (object) return object->getCollisionObject(); return nullptr; }(); ProjectileConvexCallback resultCallback(caster, btFrom, btTo, projectile); resultCallback.m_collisionFilterMask = 0xff; resultCallback.m_collisionFilterGroup = CollisionType_Projectile; const btQuaternion btrot = btQuaternion::getIdentity(); btTransform from_ (btrot, btFrom); btTransform to_ (btrot, btTo); mTaskScheduler->convexSweepTest(projectile->getConvexShape(), from_, to_, resultCallback); const auto newpos = projectile->isActive() ? position : Misc::Convert::toOsg(projectile->getHitPosition()); projectile->setPosition(newpos); mTaskScheduler->updateSingleAabb(foundProjectile->second); } void PhysicsSystem::updateRotation(const MWWorld::Ptr &ptr) { ObjectMap::iterator found = mObjects.find(ptr); if (found != mObjects.end()) { found->second->setRotation(ptr.getRefData().getBaseNode()->getAttitude()); mTaskScheduler->updateSingleAabb(found->second); return; } ActorMap::iterator foundActor = mActors.find(ptr); if (foundActor != mActors.end()) { if (!foundActor->second->isRotationallyInvariant()) { foundActor->second->updateRotation(); mTaskScheduler->updateSingleAabb(foundActor->second); } return; } } void PhysicsSystem::updatePosition(const MWWorld::Ptr &ptr) { ObjectMap::iterator found = mObjects.find(ptr); if (found != mObjects.end()) { found->second->updatePosition(); mTaskScheduler->updateSingleAabb(found->second); return; } ActorMap::iterator foundActor = mActors.find(ptr); if (foundActor != mActors.end()) { foundActor->second->updatePosition(); mTaskScheduler->updateSingleAabb(foundActor->second, true); return; } } void PhysicsSystem::addActor (const MWWorld::Ptr& ptr, const std::string& mesh) { osg::ref_ptr shape = mShapeManager->getShape(mesh); // Try to get shape from basic model as fallback for creatures if (!ptr.getClass().isNpc() && shape && shape->mCollisionBox.extents.length2() == 0) { const std::string fallbackModel = ptr.getClass().getModel(ptr); if (fallbackModel != mesh) { shape = mShapeManager->getShape(fallbackModel); } } if (!shape) return; // check if Actor should spawn above water const MWMechanics::MagicEffects& effects = ptr.getClass().getCreatureStats(ptr).getMagicEffects(); const bool canWaterWalk = effects.get(ESM::MagicEffect::WaterWalking).getMagnitude() > 0; auto actor = std::make_shared(ptr, shape, mTaskScheduler.get(), canWaterWalk); // check if Actor is on the ground or in the air traceDown(ptr, ptr.getRefData().getPosition().asVec3(), 10.f); mActors.emplace(ptr, std::move(actor)); } int PhysicsSystem::addProjectile (const MWWorld::Ptr& caster, const osg::Vec3f& position, const std::string& mesh, bool computeRadius) { osg::ref_ptr shapeInstance = mShapeManager->getInstance(mesh); assert(shapeInstance); float radius = computeRadius ? shapeInstance->mCollisionBox.extents.length() / 2.f : 1.f; mProjectileId++; auto projectile = std::make_shared(caster, position, radius, mTaskScheduler.get(), this); mProjectiles.emplace(mProjectileId, std::move(projectile)); return mProjectileId; } void PhysicsSystem::setCaster(int projectileId, const MWWorld::Ptr& caster) { const auto foundProjectile = mProjectiles.find(projectileId); assert(foundProjectile != mProjectiles.end()); auto* projectile = foundProjectile->second.get(); projectile->setCaster(caster); } bool PhysicsSystem::toggleCollisionMode() { ActorMap::iterator found = mActors.find(MWMechanics::getPlayer()); if (found != mActors.end()) { bool cmode = found->second->getCollisionMode(); cmode = !cmode; found->second->enableCollisionMode(cmode); // NB: Collision body isn't disabled for vanilla TCL compatibility return cmode; } return false; } void PhysicsSystem::queueObjectMovement(const MWWorld::Ptr &ptr, const osg::Vec3f &velocity) { ActorMap::iterator found = mActors.find(ptr); if (found != mActors.end()) found->second->setVelocity(velocity); } void PhysicsSystem::clearQueuedMovement() { for (const auto& [_, actor] : mActors) actor->setVelocity(osg::Vec3f()); } const std::vector& PhysicsSystem::applyQueuedMovement(float dt, bool skipSimulation, osg::Timer_t frameStart, unsigned int frameNumber, osg::Stats& stats) { mTimeAccum += dt; if (skipSimulation) return mTaskScheduler->resetSimulation(mActors); // modifies mTimeAccum return mTaskScheduler->moveActors(mTimeAccum, prepareFrameData(mTimeAccum >= mPhysicsDt), frameStart, frameNumber, stats); } std::vector PhysicsSystem::prepareFrameData(bool willSimulate) { std::vector actorsFrameData; actorsFrameData.reserve(mActors.size()); const MWBase::World *world = MWBase::Environment::get().getWorld(); for (const auto& [ptr, physicActor] : mActors) { float waterlevel = -std::numeric_limits::max(); const MWWorld::CellStore *cell = ptr.getCell(); if(cell->getCell()->hasWater()) waterlevel = cell->getWaterLevel(); const MWMechanics::MagicEffects& effects = ptr.getClass().getCreatureStats(physicActor->getPtr()).getMagicEffects(); bool waterCollision = false; if (cell->getCell()->hasWater() && effects.get(ESM::MagicEffect::WaterWalking).getMagnitude()) { if (physicActor->getCollisionMode() || !world->isUnderwater(ptr.getCell(), osg::Vec3f(ptr.getRefData().getPosition().asVec3()))) waterCollision = true; } 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)); // Ue current value only if we don't advance the simulation. Otherwise we might get a stale value. MWWorld::Ptr standingOn; if (!willSimulate) standingOn = physicActor->getStandingOnPtr(); actorsFrameData.emplace_back(physicActor, standingOn, waterCollision, slowFall, waterlevel); } return actorsFrameData; } void PhysicsSystem::stepSimulation() { for (Object* animatedObject : mAnimatedObjects) if (animatedObject->animateCollisionShapes()) { auto obj = mObjects.find(animatedObject->getPtr()); assert(obj != mObjects.end()); mTaskScheduler->updateSingleAabb(obj->second); } #ifndef BT_NO_PROFILE CProfileManager::Reset(); CProfileManager::Increment_Frame_Counter(); #endif } void PhysicsSystem::updateAnimatedCollisionShape(const MWWorld::Ptr& object) { ObjectMap::iterator found = mObjects.find(object); if (found != mObjects.end()) if (found->second->animateCollisionShapes()) mTaskScheduler->updateSingleAabb(found->second); } void PhysicsSystem::debugDraw() { if (mDebugDrawEnabled) mTaskScheduler->debugDraw(); } bool PhysicsSystem::isActorStandingOn(const MWWorld::Ptr &actor, const MWWorld::ConstPtr &object) const { const auto physActor = mActors.find(actor); if (physActor != mActors.end()) return physActor->second->getStandingOnPtr() == object; return false; } void PhysicsSystem::getActorsStandingOn(const MWWorld::ConstPtr &object, std::vector &out) const { for (const auto& [_, actor] : mActors) { if (actor->getStandingOnPtr() == object) out.emplace_back(actor->getPtr()); } } bool PhysicsSystem::isActorCollidingWith(const MWWorld::Ptr &actor, const MWWorld::ConstPtr &object) const { std::vector collisions = getCollisions(object, CollisionType_World, CollisionType_Actor); return (std::find(collisions.begin(), collisions.end(), actor) != collisions.end()); } void PhysicsSystem::getActorsCollidingWith(const MWWorld::ConstPtr &object, std::vector &out) const { std::vector collisions = getCollisions(object, CollisionType_World, CollisionType_Actor); out.insert(out.end(), collisions.begin(), collisions.end()); } void PhysicsSystem::disableWater() { if (mWaterEnabled) { mWaterEnabled = false; updateWater(); } } void PhysicsSystem::enableWater(float height) { if (!mWaterEnabled || mWaterHeight != height) { mWaterEnabled = true; mWaterHeight = height; updateWater(); } } void PhysicsSystem::setWaterHeight(float height) { if (mWaterHeight != height) { mWaterHeight = height; updateWater(); } } void PhysicsSystem::updateWater() { if (mWaterCollisionObject) { mTaskScheduler->removeCollisionObject(mWaterCollisionObject.get()); } if (!mWaterEnabled) { mWaterCollisionObject.reset(); return; } mWaterCollisionObject.reset(new btCollisionObject()); mWaterCollisionShape.reset(new btStaticPlaneShape(btVector3(0,0,1), mWaterHeight)); mWaterCollisionObject->setCollisionShape(mWaterCollisionShape.get()); mTaskScheduler->addCollisionObject(mWaterCollisionObject.get(), CollisionType_Water, CollisionType_Actor|CollisionType_Projectile); } bool PhysicsSystem::isAreaOccupiedByOtherActor(const osg::Vec3f& position, const float radius, const MWWorld::ConstPtr& ignore) const { btCollisionObject* object = nullptr; const auto it = mActors.find(ignore); if (it != mActors.end()) object = it->second->getCollisionObject(); const auto bulletPosition = Misc::Convert::toBullet(position); const auto aabbMin = bulletPosition - btVector3(radius, radius, radius); const auto aabbMax = bulletPosition + btVector3(radius, radius, radius); const int mask = MWPhysics::CollisionType_Actor; const int group = 0xff; HasSphereCollisionCallback callback(bulletPosition, radius, object, mask, group); mTaskScheduler->aabbTest(aabbMin, aabbMax, callback); return callback.getResult(); } void PhysicsSystem::reportStats(unsigned int frameNumber, osg::Stats& stats) const { stats.setAttribute(frameNumber, "Physics Actors", mActors.size()); stats.setAttribute(frameNumber, "Physics Objects", mObjects.size()); stats.setAttribute(frameNumber, "Physics HeightFields", mHeightFields.size()); } void PhysicsSystem::reportCollision(const btVector3& position, const btVector3& normal) { if (mDebugDrawEnabled) mDebugDrawer->addCollision(position, normal); } ActorFrameData::ActorFrameData(const std::shared_ptr& actor, const MWWorld::Ptr standingOn, bool waterCollision, float slowFall, float waterlevel) : mActor(actor), mActorRaw(actor.get()), mStandingOn(standingOn), mDidJump(false), mNeedLand(false), mWaterCollision(waterCollision), mSkipCollisionDetection(actor->skipCollisions()), mWaterlevel(waterlevel), mSlowFall(slowFall), mOldHeight(0), mFallHeight(0), mMovement(actor->velocity()), mPosition(), mRefpos() { const MWBase::World *world = MWBase::Environment::get().getWorld(); const auto ptr = actor->getPtr(); mFlying = world->isFlying(ptr); mSwimming = world->isSwimming(ptr); mWantJump = ptr.getClass().getMovementSettings(ptr).mPosition[2] != 0; auto& stats = ptr.getClass().getCreatureStats(ptr); const bool godmode = ptr == world->getPlayerConstPtr() && world->getGodModeState(); mFloatToSurface = stats.isDead() || (!godmode && stats.getMagicEffects().get(ESM::MagicEffect::Paralyze).getModifier() > 0); mWasOnGround = actor->getOnGround(); } void ActorFrameData::updatePosition(btCollisionWorld* world) { mActorRaw->applyOffsetChange(); mPosition = mActorRaw->getPosition(); if (mWaterCollision && mPosition.z() < mWaterlevel && canMoveToWaterSurface(mActorRaw, mWaterlevel, world)) { mPosition.z() = mWaterlevel; MWBase::Environment::get().getWorld()->moveObject(mActorRaw->getPtr(), mPosition.x(), mPosition.y(), mPosition.z(), false); } mOldHeight = mPosition.z(); mRefpos = mActorRaw->getPtr().getRefData().getPosition(); } WorldFrameData::WorldFrameData() : mIsInStorm(MWBase::Environment::get().getWorld()->isInStorm()) , mStormDirection(MWBase::Environment::get().getWorld()->getStormDirection()) {} LOSRequest::LOSRequest(const std::weak_ptr& a1, const std::weak_ptr& a2) : mResult(false), mStale(false), mAge(0) { // we use raw actor pointer pair to uniquely identify request // sort the pointer value in ascending order to not duplicate equivalent requests, eg. getLOS(A, B) and getLOS(B, A) auto* raw1 = a1.lock().get(); auto* raw2 = a2.lock().get(); assert(raw1 != raw2); if (raw1 < raw2) { mActors = {a1, a2}; mRawActors = {raw1, raw2}; } else { mActors = {a2, a1}; mRawActors = {raw2, raw1}; } } bool operator==(const LOSRequest& lhs, const LOSRequest& rhs) noexcept { return lhs.mRawActors == rhs.mRawActors; } } ================================================ FILE: apps/openmw/mwphysics/physicssystem.hpp ================================================ #ifndef OPENMW_MWPHYSICS_PHYSICSSYSTEM_H #define OPENMW_MWPHYSICS_PHYSICSSYSTEM_H #include #include #include #include #include #include #include #include #include #include "../mwworld/ptr.hpp" #include "collisiontype.hpp" #include "raycasting.hpp" namespace osg { class Group; class Object; class Stats; } namespace MWRender { class DebugDrawer; } namespace Resource { class BulletShapeManager; class ResourceSystem; } namespace SceneUtil { class UnrefQueue; } class btCollisionWorld; class btBroadphaseInterface; class btDefaultCollisionConfiguration; class btCollisionDispatcher; class btCollisionObject; class btCollisionShape; class btVector3; namespace MWPhysics { class HeightField; class Object; class Actor; class PhysicsTaskScheduler; class Projectile; using ActorMap = std::map>; struct ContactPoint { MWWorld::Ptr mObject; osg::Vec3f mPoint; osg::Vec3f mNormal; }; struct LOSRequest { LOSRequest(const std::weak_ptr& a1, const std::weak_ptr& a2); std::array, 2> mActors; std::array mRawActors; bool mResult; bool mStale; int mAge; }; bool operator==(const LOSRequest& lhs, const LOSRequest& rhs) noexcept; struct ActorFrameData { ActorFrameData(const std::shared_ptr& actor, const MWWorld::Ptr standingOn, bool moveToWaterSurface, float slowFall, float waterlevel); void updatePosition(btCollisionWorld* world); std::weak_ptr mActor; Actor* mActorRaw; MWWorld::Ptr mStandingOn; bool mFlying; bool mSwimming; bool mWasOnGround; bool mWantJump; bool mDidJump; bool mFloatToSurface; bool mNeedLand; bool mWaterCollision; bool mSkipCollisionDetection; float mWaterlevel; float mSlowFall; float mOldHeight; float mFallHeight; osg::Vec3f mMovement; osg::Vec3f mPosition; ESM::Position mRefpos; }; struct WorldFrameData { WorldFrameData(); bool mIsInStorm; osg::Vec3f mStormDirection; }; class PhysicsSystem : public RayCastingInterface { public: PhysicsSystem (Resource::ResourceSystem* resourceSystem, osg::ref_ptr parentNode); virtual ~PhysicsSystem (); void setUnrefQueue(SceneUtil::UnrefQueue* unrefQueue); Resource::BulletShapeManager* getShapeManager(); void enableWater(float height); void setWaterHeight(float height); void disableWater(); void addObject (const MWWorld::Ptr& ptr, const std::string& mesh, int collisionType = CollisionType_World); void addActor (const MWWorld::Ptr& ptr, const std::string& mesh); int addProjectile(const MWWorld::Ptr& caster, const osg::Vec3f& position, const std::string& mesh, bool computeRadius); void setCaster(int projectileId, const MWWorld::Ptr& caster); void updateProjectile(const int projectileId, const osg::Vec3f &position) const; void removeProjectile(const int projectileId); void updatePtr (const MWWorld::Ptr& old, const MWWorld::Ptr& updated); Actor* getActor(const MWWorld::Ptr& ptr); const Actor* getActor(const MWWorld::ConstPtr& ptr) const; const Object* getObject(const MWWorld::ConstPtr& ptr) const; Projectile* getProjectile(int projectileId) const; // Object or Actor void remove (const MWWorld::Ptr& ptr); void updateScale (const MWWorld::Ptr& ptr); void updateRotation (const MWWorld::Ptr& ptr); void updatePosition (const MWWorld::Ptr& ptr); void addHeightField (const float* heights, int x, int y, float triSize, float sqrtVerts, float minH, float maxH, const osg::Object* holdObject); void removeHeightField (int x, int y); const HeightField* getHeightField(int x, int y) const; bool toggleCollisionMode(); void stepSimulation(); void debugDraw(); std::vector getCollisions(const MWWorld::ConstPtr &ptr, int collisionGroup, int collisionMask) const; ///< get handles this object collides with std::vector getCollisionsPoints(const MWWorld::ConstPtr &ptr, int collisionGroup, int collisionMask) const; osg::Vec3f traceDown(const MWWorld::Ptr &ptr, const osg::Vec3f& position, float maxHeight); std::pair getHitContact(const MWWorld::ConstPtr& actor, const osg::Vec3f &origin, const osg::Quat &orientation, float queryDistance, std::vector& targets); /// Get distance from \a point to the collision shape of \a target. Uses a raycast to find where the /// target vector hits the collision shape and then calculates distance from the intersection point. /// This can be used to find out how much nearer we need to move to the target for a "getHitContact" to be successful. /// \note Only Actor targets are supported at the moment. float getHitDistance(const osg::Vec3f& point, const MWWorld::ConstPtr& target) const override; /// @param me Optional, a Ptr to ignore in the list of results. targets are actors to filter for, ignoring all other actors. RayCastingResult castRay(const osg::Vec3f &from, const osg::Vec3f &to, const MWWorld::ConstPtr& ignore = MWWorld::ConstPtr(), std::vector targets = std::vector(), int mask = CollisionType_World|CollisionType_HeightMap|CollisionType_Actor|CollisionType_Door, int group=0xff) const override; RayCastingResult castSphere(const osg::Vec3f& from, const osg::Vec3f& to, float radius) const override; /// Return true if actor1 can see actor2. bool getLineOfSight(const MWWorld::ConstPtr& actor1, const MWWorld::ConstPtr& actor2) const override; bool isOnGround (const MWWorld::Ptr& actor); bool canMoveToWaterSurface (const MWWorld::ConstPtr &actor, const float waterlevel); /// Get physical half extents (scaled) of the given actor. osg::Vec3f getHalfExtents(const MWWorld::ConstPtr& actor) const; /// Get physical half extents (not scaled) of the given actor. osg::Vec3f getOriginalHalfExtents(const MWWorld::ConstPtr& actor) const; /// @see MWPhysics::Actor::getRenderingHalfExtents osg::Vec3f getRenderingHalfExtents(const MWWorld::ConstPtr& actor) const; /// Get the position of the collision shape for the actor. Use together with getHalfExtents() to get the collision bounds in world space. /// @note The collision shape's origin is in its center, so the position returned can be described as center of the actor collision box in world space. osg::Vec3f getCollisionObjectPosition(const MWWorld::ConstPtr& actor) const; /// Get bounding box in world space of the given object. osg::BoundingBox getBoundingBox(const MWWorld::ConstPtr &object) const; /// Queues velocity movement for a Ptr. If a Ptr is already queued, its velocity will /// be overwritten. Valid until the next call to applyQueuedMovement. void queueObjectMovement(const MWWorld::Ptr &ptr, const osg::Vec3f &velocity); /// Apply all queued movements, then clear the list. const std::vector& applyQueuedMovement(float dt, bool skipSimulation, osg::Timer_t frameStart, unsigned int frameNumber, osg::Stats& stats); /// Clear the queued movements list without applying. void clearQueuedMovement(); /// Return true if \a actor has been standing on \a object in this frame /// This will trigger whenever the object is directly below the actor. /// It doesn't matter if the actor is stationary or moving. bool isActorStandingOn(const MWWorld::Ptr& actor, const MWWorld::ConstPtr& object) const; /// Get the handle of all actors standing on \a object in this frame. void getActorsStandingOn(const MWWorld::ConstPtr& object, std::vector& out) const; /// Return true if \a actor has collided with \a object in this frame. /// This will detect running into objects, but will not detect climbing stairs, stepping up a small object, etc. bool isActorCollidingWith(const MWWorld::Ptr& actor, const MWWorld::ConstPtr& object) const; /// Get the handle of all actors colliding with \a object in this frame. void getActorsCollidingWith(const MWWorld::ConstPtr& object, std::vector& out) const; bool toggleDebugRendering(); /// Mark the given object as a 'non-solid' object. A non-solid object means that /// \a isOnSolidGround will return false for actors standing on that object. void markAsNonSolid (const MWWorld::ConstPtr& ptr); bool isOnSolidGround (const MWWorld::Ptr& actor) const; /* Start of tes3mp addition Make it possible to set the physics framerate from elsewhere */ void setPhysicsFramerate(float physFramerate); /* End of tes3mp addition */ void updateAnimatedCollisionShape(const MWWorld::Ptr& object); template void forEachAnimatedObject(Function&& function) const { std::for_each(mAnimatedObjects.begin(), mAnimatedObjects.end(), function); } bool isAreaOccupiedByOtherActor(const osg::Vec3f& position, const float radius, const MWWorld::ConstPtr& ignore) const; void reportStats(unsigned int frameNumber, osg::Stats& stats) const; void reportCollision(const btVector3& position, const btVector3& normal); private: void updateWater(); std::vector prepareFrameData(bool willSimulate); osg::ref_ptr mUnrefQueue; std::unique_ptr mBroadphase; std::unique_ptr mCollisionConfiguration; std::unique_ptr mDispatcher; std::unique_ptr mCollisionWorld; std::unique_ptr mTaskScheduler; std::unique_ptr mShapeManager; Resource::ResourceSystem* mResourceSystem; using ObjectMap = std::map>; ObjectMap mObjects; std::set mAnimatedObjects; // stores pointers to elements in mObjects ActorMap mActors; using ProjectileMap = std::map>; ProjectileMap mProjectiles; using HeightFieldMap = std::map, osg::ref_ptr>; HeightFieldMap mHeightFields; bool mDebugDrawEnabled; float mTimeAccum; unsigned int mProjectileId; float mWaterHeight; bool mWaterEnabled; std::unique_ptr mWaterCollisionObject; std::unique_ptr mWaterCollisionShape; std::unique_ptr mDebugDrawer; osg::ref_ptr mParentNode; float mPhysicsDt; PhysicsSystem (const PhysicsSystem&); PhysicsSystem& operator= (const PhysicsSystem&); }; } #endif ================================================ FILE: apps/openmw/mwphysics/projectile.cpp ================================================ #include #include #include #include #include "../mwworld/class.hpp" #include "collisiontype.hpp" #include "mtphysics.hpp" #include "projectile.hpp" namespace MWPhysics { Projectile::Projectile(const MWWorld::Ptr& caster, const osg::Vec3f& position, float radius, PhysicsTaskScheduler* scheduler, PhysicsSystem* physicssystem) : mHitWater(false) , mActive(true) , mCaster(caster) , mPhysics(physicssystem) , mTaskScheduler(scheduler) { mShape = std::make_unique(radius); mConvexShape = static_cast(mShape.get()); mCollisionObject = std::make_unique(); mCollisionObject->setCollisionFlags(btCollisionObject::CF_KINEMATIC_OBJECT); mCollisionObject->setActivationState(DISABLE_DEACTIVATION); mCollisionObject->setCollisionShape(mShape.get()); mCollisionObject->setUserPointer(this); setPosition(position); const int collisionMask = CollisionType_World | CollisionType_HeightMap | CollisionType_Actor | CollisionType_Door | CollisionType_Water | CollisionType_Projectile; mTaskScheduler->addCollisionObject(mCollisionObject.get(), CollisionType_Projectile, collisionMask); commitPositionChange(); } Projectile::~Projectile() { if (!mActive) mPhysics->reportCollision(mHitPosition, mHitNormal); mTaskScheduler->removeCollisionObject(mCollisionObject.get()); } void Projectile::commitPositionChange() { std::scoped_lock lock(mMutex); if (mTransformUpdatePending) { auto& trans = mCollisionObject->getWorldTransform(); trans.setOrigin(Misc::Convert::toBullet(mPosition)); mCollisionObject->setWorldTransform(trans); mTransformUpdatePending = false; } } void Projectile::setPosition(const osg::Vec3f &position) { std::scoped_lock lock(mMutex); mPosition = position; mTransformUpdatePending = true; } osg::Vec3f Projectile::getPosition() const { std::scoped_lock lock(mMutex); return mPosition; } void Projectile::hit(MWWorld::Ptr target, btVector3 pos, btVector3 normal) { if (!mActive.load(std::memory_order_acquire)) return; std::scoped_lock lock(mMutex); mHitTarget = target; mHitPosition = pos; mHitNormal = normal; mActive.store(false, std::memory_order_release); } MWWorld::Ptr Projectile::getCaster() const { std::scoped_lock lock(mMutex); return mCaster; } void Projectile::setCaster(MWWorld::Ptr caster) { std::scoped_lock lock(mMutex); mCaster = caster; } void Projectile::setValidTargets(const std::vector& targets) { std::scoped_lock lock(mMutex); mValidTargets = targets; } bool Projectile::isValidTarget(const MWWorld::Ptr& target) const { std::scoped_lock lock(mMutex); if (mCaster == target) return false; if (target.isEmpty() || mValidTargets.empty()) return true; bool validTarget = false; for (const auto& targetActor : mValidTargets) { if (targetActor == target) { validTarget = true; break; } } return validTarget; } } ================================================ FILE: apps/openmw/mwphysics/projectile.hpp ================================================ #ifndef OPENMW_MWPHYSICS_PROJECTILE_H #define OPENMW_MWPHYSICS_PROJECTILE_H #include #include #include #include #include "ptrholder.hpp" class btCollisionObject; class btCollisionShape; class btConvexShape; namespace osg { class Vec3f; } namespace Resource { class BulletShape; } namespace MWPhysics { class PhysicsTaskScheduler; class PhysicsSystem; class Projectile final : public PtrHolder { public: Projectile(const MWWorld::Ptr& caster, const osg::Vec3f& position, float radius, PhysicsTaskScheduler* scheduler, PhysicsSystem* physicssystem); ~Projectile() override; btConvexShape* getConvexShape() const { return mConvexShape; } void commitPositionChange(); void setPosition(const osg::Vec3f& position); osg::Vec3f getPosition() const; btCollisionObject* getCollisionObject() const { return mCollisionObject.get(); } bool isActive() const { return mActive.load(std::memory_order_acquire); } MWWorld::Ptr getTarget() const { assert(!mActive); return mHitTarget; } MWWorld::Ptr getCaster() const; void setCaster(MWWorld::Ptr caster); void setHitWater() { mHitWater = true; } bool getHitWater() const { return mHitWater; } void hit(MWWorld::Ptr target, btVector3 pos, btVector3 normal); void setValidTargets(const std::vector& targets); bool isValidTarget(const MWWorld::Ptr& target) const; btVector3 getHitPosition() const { return mHitPosition; } private: std::unique_ptr mShape; btConvexShape* mConvexShape; std::unique_ptr mCollisionObject; bool mTransformUpdatePending; bool mHitWater; std::atomic mActive; MWWorld::Ptr mCaster; MWWorld::Ptr mHitTarget; osg::Vec3f mPosition; btVector3 mHitPosition; btVector3 mHitNormal; std::vector mValidTargets; mutable std::mutex mMutex; PhysicsSystem *mPhysics; PhysicsTaskScheduler *mTaskScheduler; Projectile(const Projectile&); Projectile& operator=(const Projectile&); }; } #endif ================================================ FILE: apps/openmw/mwphysics/projectileconvexcallback.cpp ================================================ #include "../mwworld/class.hpp" #include "actor.hpp" #include "collisiontype.hpp" #include "projectile.hpp" #include "projectileconvexcallback.hpp" #include "ptrholder.hpp" namespace MWPhysics { ProjectileConvexCallback::ProjectileConvexCallback(const btCollisionObject* me, const btVector3& from, const btVector3& to, Projectile* proj) : btCollisionWorld::ClosestConvexResultCallback(from, to) , mMe(me), mProjectile(proj) { assert(mProjectile); } btScalar ProjectileConvexCallback::addSingleResult(btCollisionWorld::LocalConvexResult& result, bool normalInWorldSpace) { // don't hit the caster if (result.m_hitCollisionObject == mMe) return 1.f; // don't hit the projectile if (result.m_hitCollisionObject == mProjectile->getCollisionObject()) return 1.f; btCollisionWorld::ClosestConvexResultCallback::addSingleResult(result, normalInWorldSpace); switch (result.m_hitCollisionObject->getBroadphaseHandle()->m_collisionFilterGroup) { case CollisionType_Actor: { auto* target = static_cast(result.m_hitCollisionObject->getUserPointer()); if (!mProjectile->isValidTarget(target->getPtr())) return 1.f; mProjectile->hit(target->getPtr(), result.m_hitPointLocal, result.m_hitNormalLocal); break; } case CollisionType_Projectile: { auto* target = static_cast(result.m_hitCollisionObject->getUserPointer()); if (!mProjectile->isValidTarget(target->getCaster())) return 1.f; target->hit(mProjectile->getPtr(), m_hitPointWorld, m_hitNormalWorld); mProjectile->hit(target->getPtr(), m_hitPointWorld, m_hitNormalWorld); break; } case CollisionType_Water: { mProjectile->setHitWater(); mProjectile->hit(MWWorld::Ptr(), m_hitPointWorld, m_hitNormalWorld); break; } default: { auto* target = static_cast(result.m_hitCollisionObject->getUserPointer()); auto ptr = target ? target->getPtr() : MWWorld::Ptr(); mProjectile->hit(ptr, m_hitPointWorld, m_hitNormalWorld); break; } } return result.m_hitFraction; } } ================================================ FILE: apps/openmw/mwphysics/projectileconvexcallback.hpp ================================================ #ifndef OPENMW_MWPHYSICS_PROJECTILECONVEXCALLBACK_H #define OPENMW_MWPHYSICS_PROJECTILECONVEXCALLBACK_H #include class btCollisionObject; namespace MWPhysics { class Projectile; class ProjectileConvexCallback : public btCollisionWorld::ClosestConvexResultCallback { public: ProjectileConvexCallback(const btCollisionObject* me, const btVector3& from, const btVector3& to, Projectile* proj); btScalar addSingleResult(btCollisionWorld::LocalConvexResult& result, bool normalInWorldSpace) override; private: const btCollisionObject* mMe; Projectile* mProjectile; }; } #endif ================================================ FILE: apps/openmw/mwphysics/ptrholder.hpp ================================================ #ifndef OPENMW_MWPHYSICS_PTRHOLDER_H #define OPENMW_MWPHYSICS_PTRHOLDER_H #include #include "../mwworld/ptr.hpp" namespace MWPhysics { class PtrHolder { public: virtual ~PtrHolder() {} void updatePtr(const MWWorld::Ptr& updated) { std::scoped_lock lock(mMutex); mPtr = updated; } MWWorld::Ptr getPtr() { std::scoped_lock lock(mMutex); return mPtr; } MWWorld::ConstPtr getPtr() const { std::scoped_lock lock(mMutex); return mPtr; } protected: MWWorld::Ptr mPtr; private: mutable std::mutex mMutex; }; } #endif ================================================ FILE: apps/openmw/mwphysics/raycasting.hpp ================================================ #ifndef OPENMW_MWPHYSICS_RAYCASTING_H #define OPENMW_MWPHYSICS_RAYCASTING_H #include #include "../mwworld/ptr.hpp" #include "collisiontype.hpp" namespace MWPhysics { struct RayCastingResult { bool mHit; osg::Vec3f mHitPos; osg::Vec3f mHitNormal; MWWorld::Ptr mHitObject; }; class RayCastingInterface { public: /// Get distance from \a point to the collision shape of \a target. Uses a raycast to find where the /// target vector hits the collision shape and then calculates distance from the intersection point. /// This can be used to find out how much nearer we need to move to the target for a "getHitContact" to be successful. /// \note Only Actor targets are supported at the moment. virtual float getHitDistance(const osg::Vec3f& point, const MWWorld::ConstPtr& target) const = 0; /// @param me Optional, a Ptr to ignore in the list of results. targets are actors to filter for, ignoring all other actors. virtual RayCastingResult castRay(const osg::Vec3f &from, const osg::Vec3f &to, const MWWorld::ConstPtr& ignore = MWWorld::ConstPtr(), std::vector targets = std::vector(), int mask = CollisionType_World|CollisionType_HeightMap|CollisionType_Actor|CollisionType_Door, int group=0xff) const = 0; virtual RayCastingResult castSphere(const osg::Vec3f& from, const osg::Vec3f& to, float radius) const = 0; /// Return true if actor1 can see actor2. virtual bool getLineOfSight(const MWWorld::ConstPtr& actor1, const MWWorld::ConstPtr& actor2) const = 0; }; } #endif ================================================ FILE: apps/openmw/mwphysics/stepper.cpp ================================================ #include "stepper.hpp" #include #include #include "collisiontype.hpp" #include "constants.hpp" #include "movementsolver.hpp" namespace MWPhysics { static bool canStepDown(const ActorTracer &stepper) { if (!stepper.mHitObject) return false; static const float sMaxSlopeCos = std::cos(osg::DegreesToRadians(sMaxSlope)); if (stepper.mPlaneNormal.z() <= sMaxSlopeCos) return false; return stepper.mHitObject->getBroadphaseHandle()->m_collisionFilterGroup != CollisionType_Actor; } Stepper::Stepper(const btCollisionWorld *colWorld, const btCollisionObject *colObj) : mColWorld(colWorld) , mColObj(colObj) { } bool Stepper::step(osg::Vec3f &position, osg::Vec3f &velocity, float &remainingTime, const bool & onGround, bool firstIteration) { if(velocity.x() == 0.0 && velocity.y() == 0.0) return false; // Stairstepping algorithms work by moving up to avoid the step, moving forwards, then moving back down onto the ground. // This algorithm has a couple of minor problems, but they don't cause problems for sane geometry, and just prevent stepping on insane geometry. mUpStepper.doTrace(mColObj, position, position+osg::Vec3f(0.0f,0.0f,sStepSizeUp), mColWorld); float upDistance = 0; if(!mUpStepper.mHitObject) upDistance = sStepSizeUp; else if(mUpStepper.mFraction*sStepSizeUp > sCollisionMargin) upDistance = mUpStepper.mFraction*sStepSizeUp - sCollisionMargin; else { return false; } auto toMove = velocity * remainingTime; osg::Vec3f tracerPos = position + osg::Vec3f(0.0f, 0.0f, upDistance); osg::Vec3f tracerDest; auto normalMove = toMove; auto moveDistance = normalMove.normalize(); // attempt 1: normal movement // attempt 2: fixed distance movement, only happens on the first movement solver iteration/bounce each frame to avoid a glitch // attempt 3: further, less tall fixed distance movement, same as above // If you're making a full conversion you should purge the logic for attempts 2 and 3. Attempts 2 and 3 just try to work around problems with vanilla Morrowind assets. int attempt = 0; float downStepSize = 0; while(attempt < 3) { attempt++; if(attempt == 1) tracerDest = tracerPos + toMove; else if (!sDoExtraStairHacks) // early out if we have extra hacks disabled { return false; } else if(attempt == 2) { moveDistance = sMinStep; tracerDest = tracerPos + normalMove*sMinStep; } else if(attempt == 3) { if(upDistance > sStepSizeUp) { upDistance = sStepSizeUp; tracerPos = position + osg::Vec3f(0.0f, 0.0f, upDistance); } moveDistance = sMinStep2; tracerDest = tracerPos + normalMove*sMinStep2; } mTracer.doTrace(mColObj, tracerPos, tracerDest, mColWorld); if(mTracer.mHitObject) { // map against what we hit, minus the safety margin moveDistance *= mTracer.mFraction; if(moveDistance <= sCollisionMargin) // didn't move enough to accomplish anything { return false; } moveDistance -= sCollisionMargin; tracerDest = tracerPos + normalMove*moveDistance; // safely eject from what we hit by the safety margin auto tempDest = tracerDest + mTracer.mPlaneNormal*sCollisionMargin*2; ActorTracer tempTracer; tempTracer.doTrace(mColObj, tracerDest, tempDest, mColWorld); if(tempTracer.mFraction > 0.5f) // distance to any object is greater than sCollisionMargin (we checked sCollisionMargin*2 distance) { auto effectiveFraction = tempTracer.mFraction*2.0f - 1.0f; tracerDest += mTracer.mPlaneNormal*sCollisionMargin*effectiveFraction; } } if(attempt > 2) // do not allow stepping down below original height for attempt 3 downStepSize = upDistance; else downStepSize = moveDistance + upDistance + sStepSizeDown; mDownStepper.doTrace(mColObj, tracerDest, tracerDest + osg::Vec3f(0.0f, 0.0f, -downStepSize), mColWorld); // can't step down onto air, non-walkable-slopes, or actors // NOTE: using a capsule causes isWalkableSlope (used in canStepDown) to fail on certain geometry that were intended to be valid at the bottoms of stairs // (like the bottoms of the staircases in aldruhn's guild of mages) // The old code worked around this by trying to do mTracer again with a fixed distance of sMinStep (10.0) but it caused all sorts of other problems. // Switched back to cylinders to avoid that and similer problems. if(canStepDown(mDownStepper)) { break; } else { // do not try attempt 3 if we just tried attempt 2 and the horizontal distance was rather large // (forces actor to get snug against the defective ledge for attempt 3 to be tried) if(attempt == 2 && moveDistance > upDistance-(mDownStepper.mFraction*downStepSize)) { return false; } // do next attempt if first iteration of movement solver and not out of attempts if(firstIteration && attempt < 3) { continue; } return false; } } // note: can't downstep onto actors so no need to pick safety margin float downDistance = 0; if(mDownStepper.mFraction*downStepSize > sCollisionMargin) downDistance = mDownStepper.mFraction*downStepSize - sCollisionMargin; if(downDistance-sCollisionMargin-sGroundOffset > upDistance && !onGround) return false; auto newpos = tracerDest + osg::Vec3f(0.0f, 0.0f, -downDistance); if((position-newpos).length2() < sCollisionMargin*sCollisionMargin) return false; if(mTracer.mHitObject) { auto planeNormal = mTracer.mPlaneNormal; if (onGround && !isWalkableSlope(planeNormal) && planeNormal.z() != 0) { planeNormal.z() = 0; planeNormal.normalize(); } velocity = reject(velocity, planeNormal); } velocity = reject(velocity, mDownStepper.mPlaneNormal); position = newpos; remainingTime *= (1.0f-mTracer.mFraction); // remaining time is proportional to remaining distance return true; } } ================================================ FILE: apps/openmw/mwphysics/stepper.hpp ================================================ #ifndef OPENMW_MWPHYSICS_STEPPER_H #define OPENMW_MWPHYSICS_STEPPER_H #include "trace.h" class btCollisionObject; class btCollisionWorld; namespace osg { class Vec3f; } namespace MWPhysics { class Stepper { private: const btCollisionWorld *mColWorld; const btCollisionObject *mColObj; ActorTracer mTracer, mUpStepper, mDownStepper; public: Stepper(const btCollisionWorld *colWorld, const btCollisionObject *colObj); bool step(osg::Vec3f &position, osg::Vec3f &velocity, float &remainingTime, const bool & onGround, bool firstIteration); }; } #endif ================================================ FILE: apps/openmw/mwphysics/trace.cpp ================================================ #include "trace.h" #include #include #include #include "collisiontype.hpp" #include "actor.hpp" #include "actorconvexcallback.hpp" namespace MWPhysics { void ActorTracer::doTrace(const btCollisionObject *actor, const osg::Vec3f& start, const osg::Vec3f& end, const btCollisionWorld* world) { const btVector3 btstart = Misc::Convert::toBullet(start); const btVector3 btend = Misc::Convert::toBullet(end); const btTransform &trans = actor->getWorldTransform(); btTransform from(trans); btTransform to(trans); from.setOrigin(btstart); to.setOrigin(btend); const btVector3 motion = btstart-btend; ActorConvexCallback newTraceCallback(actor, motion, btScalar(0.0), world); // Inherit the actor's collision group and mask newTraceCallback.m_collisionFilterGroup = actor->getBroadphaseHandle()->m_collisionFilterGroup; newTraceCallback.m_collisionFilterMask = actor->getBroadphaseHandle()->m_collisionFilterMask; const btCollisionShape *shape = actor->getCollisionShape(); assert(shape->isConvex()); world->convexSweepTest(static_cast(shape), from, to, newTraceCallback); // Copy the hit data over to our trace results struct: if(newTraceCallback.hasHit()) { mFraction = newTraceCallback.m_closestHitFraction; mPlaneNormal = Misc::Convert::toOsg(newTraceCallback.m_hitNormalWorld); mEndPos = (end-start)*mFraction + start; mHitPoint = Misc::Convert::toOsg(newTraceCallback.m_hitPointWorld); mHitObject = newTraceCallback.m_hitCollisionObject; } else { mEndPos = end; mPlaneNormal = osg::Vec3f(0.0f, 0.0f, 1.0f); mFraction = 1.0f; mHitPoint = end; mHitObject = nullptr; } } void ActorTracer::findGround(const Actor* actor, const osg::Vec3f& start, const osg::Vec3f& end, const btCollisionWorld* world) { const btVector3 btstart = Misc::Convert::toBullet(start); const btVector3 btend = Misc::Convert::toBullet(end); const btTransform &trans = actor->getCollisionObject()->getWorldTransform(); btTransform from(trans.getBasis(), btstart); btTransform to(trans.getBasis(), btend); const btVector3 motion = btstart-btend; ActorConvexCallback newTraceCallback(actor->getCollisionObject(), motion, btScalar(0.0), world); // Inherit the actor's collision group and mask newTraceCallback.m_collisionFilterGroup = actor->getCollisionObject()->getBroadphaseHandle()->m_collisionFilterGroup; newTraceCallback.m_collisionFilterMask = actor->getCollisionObject()->getBroadphaseHandle()->m_collisionFilterMask; newTraceCallback.m_collisionFilterMask &= ~CollisionType_Actor; world->convexSweepTest(actor->getConvexShape(), from, to, newTraceCallback); if(newTraceCallback.hasHit()) { mFraction = newTraceCallback.m_closestHitFraction; mPlaneNormal = Misc::Convert::toOsg(newTraceCallback.m_hitNormalWorld); mEndPos = (end-start)*mFraction + start; } else { mEndPos = end; mPlaneNormal = osg::Vec3f(0.0f, 0.0f, 1.0f); mFraction = 1.0f; } } } ================================================ FILE: apps/openmw/mwphysics/trace.h ================================================ #ifndef OENGINE_BULLET_TRACE_H #define OENGINE_BULLET_TRACE_H #include class btCollisionObject; class btCollisionWorld; namespace MWPhysics { class Actor; struct ActorTracer { osg::Vec3f mEndPos; osg::Vec3f mPlaneNormal; osg::Vec3f mHitPoint; const btCollisionObject* mHitObject; float mFraction; void doTrace(const btCollisionObject *actor, const osg::Vec3f& start, const osg::Vec3f& end, const btCollisionWorld* world); void findGround(const Actor* actor, const osg::Vec3f& start, const osg::Vec3f& end, const btCollisionWorld* world); }; } #endif ================================================ FILE: apps/openmw/mwrender/.gitignore ================================================ old ================================================ FILE: apps/openmw/mwrender/actoranimation.cpp ================================================ #include "actoranimation.hpp" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "../mwbase/environment.hpp" #include "../mwbase/world.hpp" #include "../mwworld/ptr.hpp" #include "../mwworld/class.hpp" #include "../mwworld/cellstore.hpp" #include "../mwworld/esmstore.hpp" #include "../mwmechanics/actorutil.hpp" #include "../mwmechanics/weapontype.hpp" #include "vismask.hpp" namespace MWRender { ActorAnimation::ActorAnimation(const MWWorld::Ptr& ptr, osg::ref_ptr parentNode, Resource::ResourceSystem* resourceSystem) : Animation(ptr, parentNode, resourceSystem) { MWWorld::ContainerStore& store = mPtr.getClass().getContainerStore(mPtr); for (MWWorld::ConstContainerStoreIterator iter = store.cbegin(MWWorld::ContainerStore::Type_Light); iter != store.cend(); ++iter) { const ESM::Light* light = iter->get()->mBase; if (!(light->mData.mFlags & ESM::Light::Carry)) { addHiddenItemLight(*iter, light); } } // Make sure we cleaned object from effects, just in cast if we re-use node removeEffects(); } ActorAnimation::~ActorAnimation() { for (ItemLightMap::iterator iter = mItemLights.begin(); iter != mItemLights.end(); ++iter) { mInsert->removeChild(iter->second); } mScabbard.reset(); mHolsteredShield.reset(); } PartHolderPtr ActorAnimation::attachMesh(const std::string& model, const std::string& bonename, bool enchantedGlow, osg::Vec4f* glowColor) { osg::Group* parent = getBoneByName(bonename); if (!parent) return nullptr; osg::ref_ptr instance = mResourceSystem->getSceneManager()->getInstance(model, parent); const NodeMap& nodeMap = getNodeMap(); NodeMap::const_iterator found = nodeMap.find(Misc::StringUtils::lowerCase(bonename)); if (found == nodeMap.end()) return PartHolderPtr(); if (enchantedGlow) mGlowUpdater = SceneUtil::addEnchantedGlow(instance, mResourceSystem, *glowColor); return PartHolderPtr(new PartHolder(instance)); } std::string ActorAnimation::getShieldMesh(MWWorld::ConstPtr shield) const { std::string mesh = shield.getClass().getModel(shield); const ESM::Armor *armor = shield.get()->mBase; const std::vector& bodyparts = armor->mParts.mParts; if (!bodyparts.empty()) { const MWWorld::ESMStore &store = MWBase::Environment::get().getWorld()->getStore(); const MWWorld::Store &partStore = store.get(); // Try to get shield model from bodyparts first, with ground model as fallback for (const auto& part : bodyparts) { // Assume all creatures use the male mesh. if (part.mPart != ESM::PRT_Shield || part.mMale.empty()) continue; const ESM::BodyPart *bodypart = partStore.search(part.mMale); if (bodypart && bodypart->mData.mType == ESM::BodyPart::MT_Armor && !bodypart->mModel.empty()) { mesh = "meshes\\" + bodypart->mModel; break; } } } if (mesh.empty()) return mesh; std::string holsteredName = mesh; holsteredName = holsteredName.replace(holsteredName.size()-4, 4, "_sh.nif"); if(mResourceSystem->getVFS()->exists(holsteredName)) { osg::ref_ptr shieldTemplate = mResourceSystem->getSceneManager()->getInstance(holsteredName); SceneUtil::FindByNameVisitor findVisitor ("Bip01 Sheath"); shieldTemplate->accept(findVisitor); osg::ref_ptr sheathNode = findVisitor.mFoundNode; if(!sheathNode) return std::string(); } return mesh; } bool ActorAnimation::updateCarriedLeftVisible(const int weaptype) const { static const bool shieldSheathing = Settings::Manager::getBool("shield sheathing", "Game"); if (shieldSheathing) { const MWWorld::Class &cls = mPtr.getClass(); MWMechanics::CreatureStats &stats = cls.getCreatureStats(mPtr); if (cls.hasInventoryStore(mPtr) && weaptype != ESM::Weapon::Spell) { SceneUtil::FindByNameVisitor findVisitor ("Bip01 AttachShield"); mObjectRoot->accept(findVisitor); if (findVisitor.mFoundNode || (mPtr == MWMechanics::getPlayer() && mPtr.isInCell() && MWBase::Environment::get().getWorld()->isFirstPerson())) { const MWWorld::InventoryStore& inv = cls.getInventoryStore(mPtr); const MWWorld::ConstContainerStoreIterator weapon = inv.getSlot(MWWorld::InventoryStore::Slot_CarriedRight); const MWWorld::ConstContainerStoreIterator shield = inv.getSlot(MWWorld::InventoryStore::Slot_CarriedLeft); if (shield != inv.end() && shield->getTypeName() == typeid(ESM::Armor).name() && !getShieldMesh(*shield).empty()) { if(stats.getDrawState() != MWMechanics::DrawState_Weapon) return false; if (weapon != inv.end()) { const std::string &type = weapon->getTypeName(); if(type == typeid(ESM::Weapon).name()) { const MWWorld::LiveCellRef *ref = weapon->get(); ESM::Weapon::Type weaponType = (ESM::Weapon::Type)ref->mBase->mData.mType; return !(MWMechanics::getWeaponType(weaponType)->mFlags & ESM::WeaponType::TwoHanded); } else if (type == typeid(ESM::Lockpick).name() || type == typeid(ESM::Probe).name()) return true; } } } } } return !(MWMechanics::getWeaponType(weaptype)->mFlags & ESM::WeaponType::TwoHanded); } void ActorAnimation::updateHolsteredShield(bool showCarriedLeft) { static const bool shieldSheathing = Settings::Manager::getBool("shield sheathing", "Game"); if (!shieldSheathing) return; if (!mPtr.getClass().hasInventoryStore(mPtr)) return; mHolsteredShield.reset(); if (showCarriedLeft) return; const MWWorld::InventoryStore& inv = mPtr.getClass().getInventoryStore(mPtr); MWWorld::ConstContainerStoreIterator shield = inv.getSlot(MWWorld::InventoryStore::Slot_CarriedLeft); if (shield == inv.end() || shield->getTypeName() != typeid(ESM::Armor).name()) return; // Can not show holdstered shields with two-handed weapons at all const MWWorld::ConstContainerStoreIterator weapon = inv.getSlot(MWWorld::InventoryStore::Slot_CarriedRight); if(weapon == inv.end()) return; const std::string &type = weapon->getTypeName(); if(type == typeid(ESM::Weapon).name()) { const MWWorld::LiveCellRef *ref = weapon->get(); ESM::Weapon::Type weaponType = (ESM::Weapon::Type)ref->mBase->mData.mType; if (MWMechanics::getWeaponType(weaponType)->mFlags & ESM::WeaponType::TwoHanded) return; } std::string mesh = getShieldMesh(*shield); if (mesh.empty()) return; std::string boneName = "Bip01 AttachShield"; osg::Vec4f glowColor = shield->getClass().getEnchantmentColor(*shield); std::string holsteredName = mesh; holsteredName = holsteredName.replace(holsteredName.size()-4, 4, "_sh.nif"); bool isEnchanted = !shield->getClass().getEnchantment(*shield).empty(); // If we have no dedicated sheath model, use basic shield model as fallback. if (!mResourceSystem->getVFS()->exists(holsteredName)) mHolsteredShield = attachMesh(mesh, boneName, isEnchanted, &glowColor); else mHolsteredShield = attachMesh(holsteredName, boneName, isEnchanted, &glowColor); if (!mHolsteredShield) return; SceneUtil::FindByNameVisitor findVisitor ("Bip01 Sheath"); mHolsteredShield->getNode()->accept(findVisitor); osg::Group* shieldNode = findVisitor.mFoundNode; // If mesh author declared an empty sheath node, use transformation from this node, but use the common shield mesh. // This approach allows to tweak shield position without need to store the whole shield mesh in the _sh file. if (shieldNode && !shieldNode->getNumChildren()) { osg::ref_ptr fallbackNode = mResourceSystem->getSceneManager()->getInstance(mesh, shieldNode); if (isEnchanted) SceneUtil::addEnchantedGlow(shieldNode, mResourceSystem, glowColor); } if (mAlpha != 1.f) mResourceSystem->getSceneManager()->recreateShaders(mHolsteredShield->getNode()); } bool ActorAnimation::useShieldAnimations() const { static const bool shieldSheathing = Settings::Manager::getBool("shield sheathing", "Game"); if (!shieldSheathing) return false; const MWWorld::Class &cls = mPtr.getClass(); if (!cls.hasInventoryStore(mPtr)) return false; if (getTextKeyTime("shield: equip attach") < 0 || getTextKeyTime("shield: unequip detach") < 0) return false; const MWWorld::InventoryStore& inv = cls.getInventoryStore(mPtr); const MWWorld::ConstContainerStoreIterator weapon = inv.getSlot(MWWorld::InventoryStore::Slot_CarriedRight); const MWWorld::ConstContainerStoreIterator shield = inv.getSlot(MWWorld::InventoryStore::Slot_CarriedLeft); if (weapon != inv.end() && shield != inv.end() && shield->getTypeName() == typeid(ESM::Armor).name() && !getShieldMesh(*shield).empty()) { const std::string &type = weapon->getTypeName(); if(type == typeid(ESM::Weapon).name()) { const MWWorld::LiveCellRef *ref = weapon->get(); ESM::Weapon::Type weaponType = (ESM::Weapon::Type)ref->mBase->mData.mType; return !(MWMechanics::getWeaponType(weaponType)->mFlags & ESM::WeaponType::TwoHanded); } else if (type == typeid(ESM::Lockpick).name() || type == typeid(ESM::Probe).name()) return true; } return false; } osg::Group* ActorAnimation::getBoneByName(const std::string& boneName) { if (!mObjectRoot) return nullptr; SceneUtil::FindByNameVisitor findVisitor (boneName); mObjectRoot->accept(findVisitor); return findVisitor.mFoundNode; } std::string ActorAnimation::getHolsteredWeaponBoneName(const MWWorld::ConstPtr& weapon) { std::string boneName; if(weapon.isEmpty()) return boneName; const std::string &type = weapon.getClass().getTypeName(); if(type == typeid(ESM::Weapon).name()) { const MWWorld::LiveCellRef *ref = weapon.get(); int weaponType = ref->mBase->mData.mType; return MWMechanics::getWeaponType(weaponType)->mSheathingBone; } return boneName; } void ActorAnimation::resetControllers(osg::Node* node) { if (node == nullptr) return; std::shared_ptr src; src.reset(new NullAnimationTime); SceneUtil::AssignControllerSourcesVisitor removeVisitor(src); node->accept(removeVisitor); } void ActorAnimation::updateHolsteredWeapon(bool showHolsteredWeapons) { static const bool weaponSheathing = Settings::Manager::getBool("weapon sheathing", "Game"); if (!weaponSheathing) return; if (!mPtr.getClass().hasInventoryStore(mPtr)) return; mScabbard.reset(); const MWWorld::InventoryStore& inv = mPtr.getClass().getInventoryStore(mPtr); MWWorld::ConstContainerStoreIterator weapon = inv.getSlot(MWWorld::InventoryStore::Slot_CarriedRight); if (weapon == inv.end() || weapon->getTypeName() != typeid(ESM::Weapon).name()) return; // Since throwing weapons stack themselves, do not show such weapon itself int type = weapon->get()->mBase->mData.mType; if (MWMechanics::getWeaponType(type)->mWeaponClass == ESM::WeaponType::Thrown) showHolsteredWeapons = false; std::string mesh = weapon->getClass().getModel(*weapon); std::string scabbardName = mesh; std::string boneName = getHolsteredWeaponBoneName(*weapon); if (mesh.empty() || boneName.empty()) return; // If the scabbard is not found, use a weapon mesh as fallback. // Note: it is unclear how to handle time for controllers attached to bodyparts, so disable them for now. // We use the similar approach for other bodyparts. scabbardName = scabbardName.replace(scabbardName.size()-4, 4, "_sh.nif"); bool isEnchanted = !weapon->getClass().getEnchantment(*weapon).empty(); if(!mResourceSystem->getVFS()->exists(scabbardName)) { if (showHolsteredWeapons) { osg::Vec4f glowColor = weapon->getClass().getEnchantmentColor(*weapon); mScabbard = attachMesh(mesh, boneName, isEnchanted, &glowColor); if (mScabbard) resetControllers(mScabbard->getNode()); } return; } mScabbard = attachMesh(scabbardName, boneName); if (mScabbard) resetControllers(mScabbard->getNode()); osg::Group* weaponNode = getBoneByName("Bip01 Weapon"); if (!weaponNode) return; // When we draw weapon, hide the Weapon node from sheath model. // Otherwise add the enchanted glow to it. if (!showHolsteredWeapons) { weaponNode->setNodeMask(0); } else { // If mesh author declared empty weapon node, use transformation from this node, but use the common weapon mesh. // This approach allows to tweak weapon position without need to store the whole weapon mesh in the _sh file. if (!weaponNode->getNumChildren()) { osg::ref_ptr fallbackNode = mResourceSystem->getSceneManager()->getInstance(mesh, weaponNode); resetControllers(fallbackNode); } if (isEnchanted) { osg::Vec4f glowColor = weapon->getClass().getEnchantmentColor(*weapon); mGlowUpdater = SceneUtil::addEnchantedGlow(weaponNode, mResourceSystem, glowColor); } } } void ActorAnimation::updateQuiver() { static const bool weaponSheathing = Settings::Manager::getBool("weapon sheathing", "Game"); if (!weaponSheathing) return; if (!mPtr.getClass().hasInventoryStore(mPtr)) return; const MWWorld::InventoryStore& inv = mPtr.getClass().getInventoryStore(mPtr); MWWorld::ConstContainerStoreIterator weapon = inv.getSlot(MWWorld::InventoryStore::Slot_CarriedRight); if(weapon == inv.end() || weapon->getTypeName() != typeid(ESM::Weapon).name()) return; std::string mesh = weapon->getClass().getModel(*weapon); std::string boneName = getHolsteredWeaponBoneName(*weapon); if (mesh.empty() || boneName.empty()) return; osg::Group* ammoNode = getBoneByName("Bip01 Ammo"); if (!ammoNode) return; // Special case for throwing weapons - they do not use ammo, but they stack themselves bool suitableAmmo = false; MWWorld::ConstContainerStoreIterator ammo = weapon; unsigned int ammoCount = 0; int type = weapon->get()->mBase->mData.mType; const auto& weaponType = MWMechanics::getWeaponType(type); if (weaponType->mWeaponClass == ESM::WeaponType::Thrown) { ammoCount = ammo->getRefData().getCount(); osg::Group* throwingWeaponNode = getBoneByName(weaponType->mAttachBone); if (throwingWeaponNode && throwingWeaponNode->getNumChildren()) ammoCount--; suitableAmmo = true; } else { ammo = inv.getSlot(MWWorld::InventoryStore::Slot_Ammunition); if (ammo == inv.end()) return; ammoCount = ammo->getRefData().getCount(); bool arrowAttached = isArrowAttached(); if (arrowAttached) ammoCount--; suitableAmmo = ammo->get()->mBase->mData.mType == weaponType->mAmmoType; } if (!suitableAmmo) return; // We should not show more ammo than equipped and more than quiver mesh has ammoCount = std::min(ammoCount, ammoNode->getNumChildren()); // Remove existing ammo nodes for (unsigned int i=0; igetNumChildren(); ++i) { osg::ref_ptr arrowNode = ammoNode->getChild(i)->asGroup(); if (!arrowNode->getNumChildren()) continue; osg::ref_ptr arrowChildNode = arrowNode->getChild(0); arrowNode->removeChild(arrowChildNode); } // Add new ones osg::Vec4f glowColor = ammo->getClass().getEnchantmentColor(*ammo); std::string model = ammo->getClass().getModel(*ammo); for (unsigned int i=0; i arrowNode = ammoNode->getChild(i)->asGroup(); osg::ref_ptr arrow = mResourceSystem->getSceneManager()->getInstance(model, arrowNode); if (!ammo->getClass().getEnchantment(*ammo).empty()) mGlowUpdater = SceneUtil::addEnchantedGlow(arrow, mResourceSystem, glowColor); } } void ActorAnimation::itemAdded(const MWWorld::ConstPtr& item, int /*count*/) { if (item.getTypeName() == typeid(ESM::Light).name()) { const ESM::Light* light = item.get()->mBase; if (!(light->mData.mFlags & ESM::Light::Carry)) { addHiddenItemLight(item, light); } } if (!mPtr.getClass().hasInventoryStore(mPtr)) return; // If the count of equipped ammo or throwing weapon was changed, we should update quiver const MWWorld::InventoryStore& inv = mPtr.getClass().getInventoryStore(mPtr); MWWorld::ConstContainerStoreIterator weapon = inv.getSlot(MWWorld::InventoryStore::Slot_CarriedRight); if(weapon == inv.end() || weapon->getTypeName() != typeid(ESM::Weapon).name()) return; MWWorld::ConstContainerStoreIterator ammo = inv.end(); int type = weapon->get()->mBase->mData.mType; if (MWMechanics::getWeaponType(type)->mWeaponClass == ESM::WeaponType::Thrown) ammo = weapon; else ammo = inv.getSlot(MWWorld::InventoryStore::Slot_Ammunition); if(ammo != inv.end() && item.getCellRef().getRefId() == ammo->getCellRef().getRefId()) updateQuiver(); } void ActorAnimation::itemRemoved(const MWWorld::ConstPtr& item, int /*count*/) { if (item.getTypeName() == typeid(ESM::Light).name()) { ItemLightMap::iterator iter = mItemLights.find(item); if (iter != mItemLights.end()) { if (!item.getRefData().getCount()) { removeHiddenItemLight(item); } } } if (!mPtr.getClass().hasInventoryStore(mPtr)) return; // If the count of equipped ammo or throwing weapon was changed, we should update quiver const MWWorld::InventoryStore& inv = mPtr.getClass().getInventoryStore(mPtr); MWWorld::ConstContainerStoreIterator weapon = inv.getSlot(MWWorld::InventoryStore::Slot_CarriedRight); if(weapon == inv.end() || weapon->getTypeName() != typeid(ESM::Weapon).name()) return; MWWorld::ConstContainerStoreIterator ammo = inv.end(); int type = weapon->get()->mBase->mData.mType; if (MWMechanics::getWeaponType(type)->mWeaponClass == ESM::WeaponType::Thrown) ammo = weapon; else ammo = inv.getSlot(MWWorld::InventoryStore::Slot_Ammunition); if(ammo != inv.end() && item.getCellRef().getRefId() == ammo->getCellRef().getRefId()) updateQuiver(); } void ActorAnimation::addHiddenItemLight(const MWWorld::ConstPtr& item, const ESM::Light* esmLight) { if (mItemLights.find(item) != mItemLights.end()) return; bool exterior = mPtr.isInCell() && mPtr.getCell()->getCell()->isExterior(); osg::Vec4f ambient(1,1,1,1); osg::ref_ptr lightSource = SceneUtil::createLightSource(esmLight, Mask_Lighting, exterior, ambient); mInsert->addChild(lightSource); if (mLightListCallback && mPtr == MWMechanics::getPlayer()) mLightListCallback->getIgnoredLightSources().insert(lightSource.get()); mItemLights.insert(std::make_pair(item, lightSource)); } void ActorAnimation::removeHiddenItemLight(const MWWorld::ConstPtr& item) { ItemLightMap::iterator iter = mItemLights.find(item); if (iter == mItemLights.end()) return; if (mLightListCallback && mPtr == MWMechanics::getPlayer()) { std::set::iterator ignoredIter = mLightListCallback->getIgnoredLightSources().find(iter->second.get()); if (ignoredIter != mLightListCallback->getIgnoredLightSources().end()) mLightListCallback->getIgnoredLightSources().erase(ignoredIter); } mInsert->removeChild(iter->second); mItemLights.erase(iter); } } ================================================ FILE: apps/openmw/mwrender/actoranimation.hpp ================================================ #ifndef GAME_RENDER_ACTORANIMATION_H #define GAME_RENDER_ACTORANIMATION_H #include #include #include "../mwworld/containerstore.hpp" #include "animation.hpp" namespace osg { class Node; } namespace MWWorld { class ConstPtr; } namespace SceneUtil { class LightSource; class LightListCallback; } namespace MWRender { class ActorAnimation : public Animation, public MWWorld::ContainerStoreListener { public: ActorAnimation(const MWWorld::Ptr &ptr, osg::ref_ptr parentNode, Resource::ResourceSystem* resourceSystem); virtual ~ActorAnimation(); void itemAdded(const MWWorld::ConstPtr& item, int count) override; void itemRemoved(const MWWorld::ConstPtr& item, int count) override; virtual bool isArrowAttached() const { return false; } bool useShieldAnimations() const override; bool updateCarriedLeftVisible(const int weaptype) const override; protected: osg::Group* getBoneByName(const std::string& boneName); virtual void updateHolsteredWeapon(bool showHolsteredWeapons); virtual void updateHolsteredShield(bool showCarriedLeft); virtual void updateQuiver(); virtual std::string getShieldMesh(MWWorld::ConstPtr shield) const; virtual std::string getHolsteredWeaponBoneName(const MWWorld::ConstPtr& weapon); virtual PartHolderPtr attachMesh(const std::string& model, const std::string& bonename, bool enchantedGlow, osg::Vec4f* glowColor); virtual PartHolderPtr attachMesh(const std::string& model, const std::string& bonename) { osg::Vec4f stubColor = osg::Vec4f(0,0,0,0); return attachMesh(model, bonename, false, &stubColor); }; PartHolderPtr mScabbard; PartHolderPtr mHolsteredShield; private: void addHiddenItemLight(const MWWorld::ConstPtr& item, const ESM::Light* esmLight); void removeHiddenItemLight(const MWWorld::ConstPtr& item); void resetControllers(osg::Node* node); typedef std::map > ItemLightMap; ItemLightMap mItemLights; }; } #endif ================================================ FILE: apps/openmw/mwrender/actorspaths.cpp ================================================ #include "actorspaths.hpp" #include "vismask.hpp" #include #include namespace MWRender { ActorsPaths::ActorsPaths(const osg::ref_ptr& root, bool enabled) : mRootNode(root) , mEnabled(enabled) { } ActorsPaths::~ActorsPaths() { if (mEnabled) disable(); } bool ActorsPaths::toggle() { if (mEnabled) disable(); else enable(); return mEnabled; } void ActorsPaths::update(const MWWorld::ConstPtr& actor, const std::deque& path, const osg::Vec3f& halfExtents, const osg::Vec3f& start, const osg::Vec3f& end, const DetourNavigator::Settings& settings) { if (!mEnabled) return; const auto group = mGroups.find(actor); if (group != mGroups.end()) mRootNode->removeChild(group->second); const auto newGroup = SceneUtil::createAgentPathGroup(path, halfExtents, start, end, settings); if (newGroup) { newGroup->setNodeMask(Mask_Debug); mRootNode->addChild(newGroup); mGroups[actor] = newGroup; } } void ActorsPaths::remove(const MWWorld::ConstPtr& actor) { const auto group = mGroups.find(actor); if (group != mGroups.end()) { mRootNode->removeChild(group->second); mGroups.erase(group); } } void ActorsPaths::removeCell(const MWWorld::CellStore* const store) { for (auto it = mGroups.begin(); it != mGroups.end(); ) { if (it->first.getCell() == store) { mRootNode->removeChild(it->second); it = mGroups.erase(it); } else ++it; } } void ActorsPaths::updatePtr(const MWWorld::ConstPtr& old, const MWWorld::ConstPtr& updated) { const auto it = mGroups.find(old); if (it == mGroups.end()) return; auto group = std::move(it->second); mGroups.erase(it); mGroups.insert(std::make_pair(updated, std::move(group))); } void ActorsPaths::enable() { std::for_each(mGroups.begin(), mGroups.end(), [&] (const Groups::value_type& v) { mRootNode->addChild(v.second); }); mEnabled = true; } void ActorsPaths::disable() { std::for_each(mGroups.begin(), mGroups.end(), [&] (const Groups::value_type& v) { mRootNode->removeChild(v.second); }); mEnabled = false; } } ================================================ FILE: apps/openmw/mwrender/actorspaths.hpp ================================================ #ifndef OPENMW_MWRENDER_AGENTSPATHS_H #define OPENMW_MWRENDER_AGENTSPATHS_H #include #include #include #include #include namespace osg { class Group; } namespace MWRender { class ActorsPaths { public: ActorsPaths(const osg::ref_ptr& root, bool enabled); ~ActorsPaths(); bool toggle(); void update(const MWWorld::ConstPtr& actor, const std::deque& path, const osg::Vec3f& halfExtents, const osg::Vec3f& start, const osg::Vec3f& end, const DetourNavigator::Settings& settings); void remove(const MWWorld::ConstPtr& actor); void removeCell(const MWWorld::CellStore* const store); void updatePtr(const MWWorld::ConstPtr& old, const MWWorld::ConstPtr& updated); void enable(); void disable(); private: using Groups = std::map>; osg::ref_ptr mRootNode; Groups mGroups; bool mEnabled; }; } #endif ================================================ FILE: apps/openmw/mwrender/animation.cpp ================================================ #include "animation.hpp" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "../mwbase/environment.hpp" #include "../mwbase/world.hpp" #include "../mwworld/esmstore.hpp" #include "../mwworld/class.hpp" #include "../mwworld/cellstore.hpp" #include "../mwmechanics/character.hpp" // FIXME: for MWMechanics::Priority #include "vismask.hpp" #include "util.hpp" #include "rotatecontroller.hpp" namespace { /// Removes all particle systems and related nodes in a subgraph. class RemoveParticlesVisitor : public osg::NodeVisitor { public: RemoveParticlesVisitor() : osg::NodeVisitor(TRAVERSE_ALL_CHILDREN) { } void apply(osg::Node &node) override { if (dynamic_cast(&node)) mToRemove.emplace_back(&node); traverse(node); } void apply(osg::Drawable& drw) override { if (osgParticle::ParticleSystem* partsys = dynamic_cast(&drw)) mToRemove.emplace_back(partsys); } void remove() { for (osg::Node* node : mToRemove) { // FIXME: a Drawable might have more than one parent if (node->getNumParents()) node->getParent(0)->removeChild(node); } mToRemove.clear(); } private: std::vector > mToRemove; }; class DayNightCallback : public osg::NodeCallback { public: DayNightCallback() : mCurrentState(0) { } void operator()(osg::Node* node, osg::NodeVisitor* nv) override { unsigned int state = MWBase::Environment::get().getWorld()->getNightDayMode(); const unsigned int newState = node->asGroup()->getNumChildren() > state ? state : 0; if (newState != mCurrentState) { mCurrentState = newState; node->asSwitch()->setSingleChildOn(mCurrentState); } traverse(node, nv); } private: unsigned int mCurrentState; }; class AddSwitchCallbacksVisitor : public osg::NodeVisitor { public: AddSwitchCallbacksVisitor() : osg::NodeVisitor(TRAVERSE_ALL_CHILDREN) { } void apply(osg::Switch &switchNode) override { if (switchNode.getName() == Constants::NightDayLabel) switchNode.addUpdateCallback(new DayNightCallback()); traverse(switchNode); } }; class HarvestVisitor : public osg::NodeVisitor { public: HarvestVisitor() : osg::NodeVisitor(TRAVERSE_ALL_CHILDREN) { } void apply(osg::Switch& node) override { if (node.getName() == Constants::HerbalismLabel) { node.setSingleChildOn(1); } traverse(node); } }; float calcAnimVelocity(const SceneUtil::TextKeyMap& keys, SceneUtil::KeyframeController *nonaccumctrl, const osg::Vec3f& accum, const std::string &groupname) { const std::string start = groupname+": start"; const std::string loopstart = groupname+": loop start"; const std::string loopstop = groupname+": loop stop"; const std::string stop = groupname+": stop"; float starttime = std::numeric_limits::max(); float stoptime = 0.0f; // Pick the last Loop Stop key and the last Loop Start key. // This is required because of broken text keys in AshVampire.nif. // It has *two* WalkForward: Loop Stop keys at different times, the first one is used for stopping playback // but the animation velocity calculation uses the second one. // As result the animation velocity calculation is not correct, and this incorrect velocity must be replicated, // because otherwise the Creature's Speed (dagoth uthol) would not be sufficient to move fast enough. auto keyiter = keys.rbegin(); while(keyiter != keys.rend()) { if(keyiter->second == start || keyiter->second == loopstart) { starttime = keyiter->first; break; } ++keyiter; } keyiter = keys.rbegin(); while(keyiter != keys.rend()) { if (keyiter->second == stop) stoptime = keyiter->first; else if (keyiter->second == loopstop) { stoptime = keyiter->first; break; } ++keyiter; } if(stoptime > starttime) { osg::Vec3f startpos = osg::componentMultiply(nonaccumctrl->getTranslation(starttime), accum); osg::Vec3f endpos = osg::componentMultiply(nonaccumctrl->getTranslation(stoptime), accum); return (startpos-endpos).length() / (stoptime - starttime); } return 0.0f; } /// @brief Base class for visitors that remove nodes from a scene graph. /// Subclasses need to fill the mToRemove vector. /// To use, node->accept(removeVisitor); removeVisitor.remove(); class RemoveVisitor : public osg::NodeVisitor { public: RemoveVisitor() : osg::NodeVisitor(TRAVERSE_ALL_CHILDREN) { } void remove() { for (RemoveVec::iterator it = mToRemove.begin(); it != mToRemove.end(); ++it) { if (!it->second->removeChild(it->first)) Log(Debug::Error) << "Error removing " << it->first->getName(); } } protected: // typedef std::vector > RemoveVec; std::vector > mToRemove; }; class GetExtendedBonesVisitor : public osg::NodeVisitor { public: GetExtendedBonesVisitor() : osg::NodeVisitor(TRAVERSE_ALL_CHILDREN) { } void apply(osg::Node& node) override { if (SceneUtil::hasUserDescription(&node, "CustomBone")) { mFoundBones.emplace_back(&node, node.getParent(0)); return; } traverse(node); } std::vector > mFoundBones; }; class RemoveFinishedCallbackVisitor : public RemoveVisitor { public: bool mHasMagicEffects; RemoveFinishedCallbackVisitor() : RemoveVisitor() , mHasMagicEffects(false) { } void apply(osg::Node &node) override { traverse(node); } void apply(osg::Group &group) override { traverse(group); osg::Callback* callback = group.getUpdateCallback(); if (callback) { // We should remove empty transformation nodes and finished callbacks here MWRender::UpdateVfxCallback* vfxCallback = dynamic_cast(callback); if (vfxCallback) { if (vfxCallback->mFinished) mToRemove.emplace_back(group.asNode(), group.getParent(0)); else mHasMagicEffects = true; } } } void apply(osg::MatrixTransform &node) override { traverse(node); } void apply(osg::Geometry&) override { } }; class RemoveCallbackVisitor : public RemoveVisitor { public: bool mHasMagicEffects; RemoveCallbackVisitor() : RemoveVisitor() , mHasMagicEffects(false) , mEffectId(-1) { } RemoveCallbackVisitor(int effectId) : RemoveVisitor() , mHasMagicEffects(false) , mEffectId(effectId) { } void apply(osg::Node &node) override { traverse(node); } void apply(osg::Group &group) override { traverse(group); osg::Callback* callback = group.getUpdateCallback(); if (callback) { MWRender::UpdateVfxCallback* vfxCallback = dynamic_cast(callback); if (vfxCallback) { bool toRemove = mEffectId < 0 || vfxCallback->mParams.mEffectId == mEffectId; if (toRemove) mToRemove.emplace_back(group.asNode(), group.getParent(0)); else mHasMagicEffects = true; } } } void apply(osg::MatrixTransform &node) override { traverse(node); } void apply(osg::Geometry&) override { } private: int mEffectId; }; class FindVfxCallbacksVisitor : public osg::NodeVisitor { public: std::vector mCallbacks; FindVfxCallbacksVisitor() : osg::NodeVisitor(TRAVERSE_ALL_CHILDREN) , mEffectId(-1) { } FindVfxCallbacksVisitor(int effectId) : osg::NodeVisitor(TRAVERSE_ALL_CHILDREN) , mEffectId(effectId) { } void apply(osg::Node &node) override { traverse(node); } void apply(osg::Group &group) override { osg::Callback* callback = group.getUpdateCallback(); if (callback) { MWRender::UpdateVfxCallback* vfxCallback = dynamic_cast(callback); if (vfxCallback) { if (mEffectId < 0 || vfxCallback->mParams.mEffectId == mEffectId) { mCallbacks.push_back(vfxCallback); } } } traverse(group); } void apply(osg::MatrixTransform &node) override { traverse(node); } void apply(osg::Geometry&) override { } private: int mEffectId; }; // Removes all drawables from a graph. class CleanObjectRootVisitor : public RemoveVisitor { public: void apply(osg::Drawable& drw) override { applyDrawable(drw); } void apply(osg::Group& node) override { applyNode(node); } void apply(osg::MatrixTransform& node) override { applyNode(node); } void apply(osg::Node& node) override { applyNode(node); } void applyNode(osg::Node& node) { if (node.getStateSet()) node.setStateSet(nullptr); if (node.getNodeMask() == 0x1 && node.getNumParents() == 1) mToRemove.emplace_back(&node, node.getParent(0)); else traverse(node); } void applyDrawable(osg::Node& node) { osg::NodePath::iterator parent = getNodePath().end()-2; // We know that the parent is a Group because only Groups can have children. osg::Group* parentGroup = static_cast(*parent); // Try to prune nodes that would be empty after the removal if (parent != getNodePath().begin()) { // This could be extended to remove the parent's parent, and so on if they are empty as well. // But for NIF files, there won't be a benefit since only TriShapes can be set to STATIC dataVariance. osg::Group* parentParent = static_cast(*(parent - 1)); if (parentGroup->getNumChildren() == 1 && parentGroup->getDataVariance() == osg::Object::STATIC) { mToRemove.emplace_back(parentGroup, parentParent); return; } } mToRemove.emplace_back(&node, parentGroup); } }; class RemoveTriBipVisitor : public RemoveVisitor { public: void apply(osg::Drawable& drw) override { applyImpl(drw); } void apply(osg::Group& node) override { traverse(node); } void apply(osg::MatrixTransform& node) override { traverse(node); } void applyImpl(osg::Node& node) { const std::string toFind = "tri bip"; if (Misc::StringUtils::ciCompareLen(node.getName(), toFind, toFind.size()) == 0) { osg::Group* parent = static_cast(*(getNodePath().end()-2)); // Not safe to remove in apply(), since the visitor is still iterating the child list mToRemove.emplace_back(&node, parent); } } }; } namespace MWRender { class TransparencyUpdater : public SceneUtil::StateSetUpdater { public: TransparencyUpdater(const float alpha) : mAlpha(alpha) { } void setAlpha(const float alpha) { mAlpha = alpha; } void setLightSource(const osg::ref_ptr& lightSource) { mLightSource = lightSource; } protected: void setDefaults(osg::StateSet* stateset) override { osg::BlendFunc* blendfunc (new osg::BlendFunc); stateset->setAttributeAndModes(blendfunc, osg::StateAttribute::ON|osg::StateAttribute::OVERRIDE); stateset->setRenderingHint(osg::StateSet::TRANSPARENT_BIN); stateset->setRenderBinMode(osg::StateSet::OVERRIDE_RENDERBIN_DETAILS); // FIXME: overriding diffuse/ambient/emissive colors osg::Material* material = new osg::Material; material->setColorMode(osg::Material::OFF); material->setDiffuse(osg::Material::FRONT_AND_BACK, osg::Vec4f(1,1,1,mAlpha)); material->setAmbient(osg::Material::FRONT_AND_BACK, osg::Vec4f(1,1,1,1)); stateset->setAttributeAndModes(material, osg::StateAttribute::ON|osg::StateAttribute::OVERRIDE); stateset->addUniform(new osg::Uniform("colorMode", 0), osg::StateAttribute::ON|osg::StateAttribute::OVERRIDE); } void apply(osg::StateSet* stateset, osg::NodeVisitor* /*nv*/) override { osg::Material* material = static_cast(stateset->getAttribute(osg::StateAttribute::MATERIAL)); material->setAlpha(osg::Material::FRONT_AND_BACK, mAlpha); if (mLightSource) mLightSource->setActorFade(mAlpha); } private: float mAlpha; osg::ref_ptr mLightSource; }; struct Animation::AnimSource { osg::ref_ptr mKeyframes; typedef std::map > ControllerMap; ControllerMap mControllerMap[Animation::sNumBlendMasks]; const SceneUtil::TextKeyMap& getTextKeys() const; }; void UpdateVfxCallback::operator()(osg::Node* node, osg::NodeVisitor* nv) { traverse(node, nv); if (mFinished) return; double newTime = nv->getFrameStamp()->getSimulationTime(); if (mStartingTime == 0) { mStartingTime = newTime; return; } double duration = newTime - mStartingTime; mStartingTime = newTime; mParams.mAnimTime->addTime(duration); if (mParams.mAnimTime->getTime() >= mParams.mMaxControllerLength) { if (mParams.mLoop) { // Start from the beginning again; carry over the remainder // Not sure if this is actually needed, the controller function might already handle loops float remainder = mParams.mAnimTime->getTime() - mParams.mMaxControllerLength; mParams.mAnimTime->resetTime(remainder); } else { // Hide effect immediately node->setNodeMask(0); mFinished = true; } } } class ResetAccumRootCallback : public osg::NodeCallback { public: void operator()(osg::Node* node, osg::NodeVisitor* nv) override { osg::MatrixTransform* transform = static_cast(node); osg::Matrix mat = transform->getMatrix(); osg::Vec3f position = mat.getTrans(); position = osg::componentMultiply(mResetAxes, position); mat.setTrans(position); transform->setMatrix(mat); traverse(node, nv); } void setAccumulate(const osg::Vec3f& accumulate) { // anything that accumulates (1.f) should be reset in the callback to (0.f) mResetAxes.x() = accumulate.x() != 0.f ? 0.f : 1.f; mResetAxes.y() = accumulate.y() != 0.f ? 0.f : 1.f; mResetAxes.z() = accumulate.z() != 0.f ? 0.f : 1.f; } private: osg::Vec3f mResetAxes; }; Animation::Animation(const MWWorld::Ptr &ptr, osg::ref_ptr parentNode, Resource::ResourceSystem* resourceSystem) : mInsert(parentNode) , mSkeleton(nullptr) , mNodeMapCreated(false) , mPtr(ptr) , mResourceSystem(resourceSystem) , mAccumulate(1.f, 1.f, 0.f) , mTextKeyListener(nullptr) , mHeadYawRadians(0.f) , mHeadPitchRadians(0.f) , mUpperBodyYawRadians(0.f) , mLegsYawRadians(0.f) , mBodyPitchRadians(0.f) , mHasMagicEffects(false) , mAlpha(1.f) { for(size_t i = 0;i < sNumBlendMasks;i++) mAnimationTimePtr[i].reset(new AnimationTime); mLightListCallback = new SceneUtil::LightListCallback; } Animation::~Animation() { Animation::setLightEffect(0.f); if (mObjectRoot) mInsert->removeChild(mObjectRoot); } MWWorld::ConstPtr Animation::getPtr() const { return mPtr; } MWWorld::Ptr Animation::getPtr() { return mPtr; } void Animation::setActive(int active) { if (mSkeleton) mSkeleton->setActive(static_cast(active)); } void Animation::updatePtr(const MWWorld::Ptr &ptr) { mPtr = ptr; } void Animation::setAccumulation(const osg::Vec3f& accum) { mAccumulate = accum; if (mResetAccumRootCallback) mResetAccumRootCallback->setAccumulate(mAccumulate); } size_t Animation::detectBlendMask(const osg::Node* node) const { static const char sBlendMaskRoots[sNumBlendMasks][32] = { "", /* Lower body / character root */ "Bip01 Spine1", /* Torso */ "Bip01 L Clavicle", /* Left arm */ "Bip01 R Clavicle", /* Right arm */ }; while(node != mObjectRoot) { const std::string &name = node->getName(); for(size_t i = 1;i < sNumBlendMasks;i++) { if(name == sBlendMaskRoots[i]) return i; } assert(node->getNumParents() > 0); node = node->getParent(0); } return 0; } const SceneUtil::TextKeyMap &Animation::AnimSource::getTextKeys() const { return mKeyframes->mTextKeys; } void Animation::loadAllAnimationsInFolder(const std::string &model, const std::string &baseModel) { const std::map& index = mResourceSystem->getVFS()->getIndex(); std::string animationPath = model; if (animationPath.find("meshes") == 0) { animationPath.replace(0, 6, "animations"); } animationPath.replace(animationPath.size()-3, 3, "/"); mResourceSystem->getVFS()->normalizeFilename(animationPath); std::map::const_iterator found = index.lower_bound(animationPath); while (found != index.end()) { const std::string& name = found->first; if (name.size() >= animationPath.size() && name.substr(0, animationPath.size()) == animationPath) { size_t pos = name.find_last_of('.'); if (pos != std::string::npos && name.compare(pos, name.size()-pos, ".kf") == 0) addSingleAnimSource(name, baseModel); } else break; ++found; } } void Animation::addAnimSource(const std::string &model, const std::string& baseModel) { std::string kfname = model; Misc::StringUtils::lowerCaseInPlace(kfname); if(kfname.size() > 4 && kfname.compare(kfname.size()-4, 4, ".nif") == 0) kfname.replace(kfname.size()-4, 4, ".kf"); addSingleAnimSource(kfname, baseModel); static const bool useAdditionalSources = Settings::Manager::getBool ("use additional anim sources", "Game"); if (useAdditionalSources) loadAllAnimationsInFolder(kfname, baseModel); } void Animation::addSingleAnimSource(const std::string &kfname, const std::string& baseModel) { if(!mResourceSystem->getVFS()->exists(kfname)) return; std::shared_ptr animsrc; animsrc.reset(new AnimSource); animsrc->mKeyframes = mResourceSystem->getKeyframeManager()->get(kfname); if (!animsrc->mKeyframes || animsrc->mKeyframes->mTextKeys.empty() || animsrc->mKeyframes->mKeyframeControllers.empty()) return; const NodeMap& nodeMap = getNodeMap(); for (SceneUtil::KeyframeHolder::KeyframeControllerMap::const_iterator it = animsrc->mKeyframes->mKeyframeControllers.begin(); it != animsrc->mKeyframes->mKeyframeControllers.end(); ++it) { std::string bonename = Misc::StringUtils::lowerCase(it->first); NodeMap::const_iterator found = nodeMap.find(bonename); if (found == nodeMap.end()) { Log(Debug::Warning) << "Warning: addAnimSource: can't find bone '" + bonename << "' in " << baseModel << " (referenced by " << kfname << ")"; continue; } osg::Node* node = found->second; size_t blendMask = detectBlendMask(node); // clone the controller, because each Animation needs its own ControllerSource osg::ref_ptr cloned = osg::clone(it->second.get(), osg::CopyOp::SHALLOW_COPY); cloned->setSource(mAnimationTimePtr[blendMask]); animsrc->mControllerMap[blendMask].insert(std::make_pair(bonename, cloned)); } mAnimSources.push_back(animsrc); SceneUtil::AssignControllerSourcesVisitor assignVisitor(mAnimationTimePtr[0]); mObjectRoot->accept(assignVisitor); 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; } } void Animation::clearAnimSources() { mStates.clear(); for(size_t i = 0;i < sNumBlendMasks;i++) mAnimationTimePtr[i]->setTimePtr(std::shared_ptr()); mAccumCtrl = nullptr; mAnimSources.clear(); mAnimVelocities.clear(); } bool Animation::hasAnimation(const std::string &anim) const { AnimSourceList::const_iterator iter(mAnimSources.begin()); for(;iter != mAnimSources.end();++iter) { const SceneUtil::TextKeyMap &keys = (*iter)->getTextKeys(); if (keys.hasGroupStart(anim)) return true; } return false; } float Animation::getStartTime(const std::string &groupname) const { for(AnimSourceList::const_reverse_iterator iter(mAnimSources.rbegin()); iter != mAnimSources.rend(); ++iter) { const SceneUtil::TextKeyMap &keys = (*iter)->getTextKeys(); const auto found = keys.findGroupStart(groupname); if(found != keys.end()) return found->first; } return -1.f; } float Animation::getTextKeyTime(const std::string &textKey) const { for(AnimSourceList::const_reverse_iterator iter(mAnimSources.rbegin()); iter != mAnimSources.rend(); ++iter) { const SceneUtil::TextKeyMap &keys = (*iter)->getTextKeys(); for(auto iterKey = keys.begin(); iterKey != keys.end(); ++iterKey) { if(iterKey->second.compare(0, textKey.size(), textKey) == 0) return iterKey->first; } } return -1.f; } void Animation::handleTextKey(AnimState &state, const std::string &groupname, SceneUtil::TextKeyMap::ConstIterator key, const SceneUtil::TextKeyMap& map) { const std::string &evt = key->second; size_t off = groupname.size()+2; size_t len = evt.size() - off; if(evt.compare(0, groupname.size(), groupname) == 0 && evt.compare(groupname.size(), 2, ": ") == 0) { if(evt.compare(off, len, "loop start") == 0) state.mLoopStartTime = key->first; else if(evt.compare(off, len, "loop stop") == 0) state.mLoopStopTime = key->first; } if (mTextKeyListener) { try { mTextKeyListener->handleTextKey(groupname, key, map); } catch (std::exception& e) { Log(Debug::Error) << "Error handling text key " << evt << ": " << e.what(); } } } void Animation::play(const std::string &groupname, const AnimPriority& priority, int blendMask, bool autodisable, float speedmult, const std::string &start, const std::string &stop, float startpoint, size_t loops, bool loopfallback) { if(!mObjectRoot || mAnimSources.empty()) return; if(groupname.empty()) { resetActiveGroups(); return; } AnimStateMap::iterator stateiter = mStates.begin(); while(stateiter != mStates.end()) { if(stateiter->second.mPriority == priority) mStates.erase(stateiter++); else ++stateiter; } stateiter = mStates.find(groupname); if(stateiter != mStates.end()) { stateiter->second.mPriority = priority; resetActiveGroups(); return; } /* Look in reverse; last-inserted source has priority. */ AnimState state; AnimSourceList::reverse_iterator iter(mAnimSources.rbegin()); for(;iter != mAnimSources.rend();++iter) { const SceneUtil::TextKeyMap &textkeys = (*iter)->getTextKeys(); if(reset(state, textkeys, groupname, start, stop, startpoint, loopfallback)) { state.mSource = *iter; state.mSpeedMult = speedmult; state.mLoopCount = loops; state.mPlaying = (state.getTime() < state.mStopTime); state.mPriority = priority; state.mBlendMask = blendMask; state.mAutoDisable = autodisable; mStates[groupname] = state; if (state.mPlaying) { auto textkey = textkeys.lowerBound(state.getTime()); while(textkey != textkeys.end() && textkey->first <= state.getTime()) { handleTextKey(state, groupname, textkey, textkeys); ++textkey; } } if(state.getTime() >= state.mLoopStopTime && state.mLoopCount > 0) { state.mLoopCount--; state.setTime(state.mLoopStartTime); state.mPlaying = true; if(state.getTime() >= state.mLoopStopTime) break; auto textkey = textkeys.lowerBound(state.getTime()); while(textkey != textkeys.end() && textkey->first <= state.getTime()) { handleTextKey(state, groupname, textkey, textkeys); ++textkey; } } break; } } resetActiveGroups(); } bool Animation::reset(AnimState &state, const SceneUtil::TextKeyMap &keys, const std::string &groupname, const std::string &start, const std::string &stop, float startpoint, bool loopfallback) { // Look for text keys in reverse. This normally wouldn't matter, but for some reason undeadwolf_2.nif has two // separate walkforward keys, and the last one is supposed to be used. auto groupend = keys.rbegin(); for(;groupend != keys.rend();++groupend) { if(groupend->second.compare(0, groupname.size(), groupname) == 0 && groupend->second.compare(groupname.size(), 2, ": ") == 0) break; } std::string starttag = groupname+": "+start; auto startkey = groupend; while(startkey != keys.rend() && startkey->second != starttag) ++startkey; if(startkey == keys.rend() && start == "loop start") { starttag = groupname+": start"; startkey = groupend; while(startkey != keys.rend() && startkey->second != starttag) ++startkey; } if(startkey == keys.rend()) return false; const std::string stoptag = groupname+": "+stop; auto stopkey = groupend; while(stopkey != keys.rend() // We have to ignore extra garbage at the end. // The Scrib's idle3 animation has "Idle3: Stop." instead of "Idle3: Stop". // Why, just why? :( && (stopkey->second.size() < stoptag.size() || stopkey->second.compare(0,stoptag.size(), stoptag) != 0)) ++stopkey; if(stopkey == keys.rend()) return false; if(startkey->first > stopkey->first) return false; state.mStartTime = startkey->first; if (loopfallback) { state.mLoopStartTime = startkey->first; state.mLoopStopTime = stopkey->first; } else { state.mLoopStartTime = startkey->first; state.mLoopStopTime = std::numeric_limits::max(); } state.mStopTime = stopkey->first; state.setTime(state.mStartTime + ((state.mStopTime - state.mStartTime) * startpoint)); // mLoopStartTime and mLoopStopTime normally get assigned when encountering these keys while playing the animation // (see handleTextKey). But if startpoint is already past these keys, or start time is == stop time, we need to assign them now. const std::string loopstarttag = groupname+": loop start"; const std::string loopstoptag = groupname+": loop stop"; auto key = groupend; for (; key != startkey && key != keys.rend(); ++key) { if (key->first > state.getTime()) continue; if (key->second == loopstarttag) state.mLoopStartTime = key->first; else if (key->second == loopstoptag) state.mLoopStopTime = key->first; } return true; } void Animation::setTextKeyListener(Animation::TextKeyListener *listener) { mTextKeyListener = listener; } const Animation::NodeMap &Animation::getNodeMap() const { if (!mNodeMapCreated && mObjectRoot) { SceneUtil::NodeMapVisitor visitor(mNodeMap); mObjectRoot->accept(visitor); mNodeMapCreated = true; } return mNodeMap; } void Animation::resetActiveGroups() { // remove all previous external controllers from the scene graph for (auto it = mActiveControllers.begin(); it != mActiveControllers.end(); ++it) { osg::Node* node = it->first; node->removeUpdateCallback(it->second); // Should be no longer needed with OSG 3.4 it->second->setNestedCallback(nullptr); } mActiveControllers.clear(); mAccumCtrl = nullptr; for(size_t blendMask = 0;blendMask < sNumBlendMasks;blendMask++) { AnimStateMap::const_iterator active = mStates.end(); AnimStateMap::const_iterator state = mStates.begin(); for(;state != mStates.end();++state) { if(!(state->second.mBlendMask&(1<second.mPriority[(BoneGroup)blendMask] < state->second.mPriority[(BoneGroup)blendMask]) active = state; } mAnimationTimePtr[blendMask]->setTimePtr(active == mStates.end() ? std::shared_ptr() : active->second.mTime); // add external controllers for the AnimSource active in this blend mask if (active != mStates.end()) { std::shared_ptr animsrc = active->second.mSource; for (AnimSource::ControllerMap::iterator it = animsrc->mControllerMap[blendMask].begin(); it != animsrc->mControllerMap[blendMask].end(); ++it) { osg::ref_ptr node = getNodeMap().at(it->first); // this should not throw, we already checked for the node existing in addAnimSource node->addUpdateCallback(it->second); mActiveControllers.emplace_back(node, it->second); if (blendMask == 0 && node == mAccumRoot) { mAccumCtrl = it->second; // make sure reset is last in the chain of callbacks if (!mResetAccumRootCallback) { mResetAccumRootCallback = new ResetAccumRootCallback; mResetAccumRootCallback->setAccumulate(mAccumulate); } mAccumRoot->addUpdateCallback(mResetAccumRootCallback); mActiveControllers.emplace_back(mAccumRoot, mResetAccumRootCallback); } } } } addControllers(); } void Animation::adjustSpeedMult(const std::string &groupname, float speedmult) { AnimStateMap::iterator state(mStates.find(groupname)); if(state != mStates.end()) state->second.mSpeedMult = speedmult; } bool Animation::isPlaying(const std::string &groupname) const { AnimStateMap::const_iterator state(mStates.find(groupname)); if(state != mStates.end()) return state->second.mPlaying; return false; } bool Animation::getInfo(const std::string &groupname, float *complete, float *speedmult) const { AnimStateMap::const_iterator iter = mStates.find(groupname); if(iter == mStates.end()) { if(complete) *complete = 0.0f; if(speedmult) *speedmult = 0.0f; return false; } if(complete) { if(iter->second.mStopTime > iter->second.mStartTime) *complete = (iter->second.getTime() - iter->second.mStartTime) / (iter->second.mStopTime - iter->second.mStartTime); else *complete = (iter->second.mPlaying ? 0.0f : 1.0f); } if(speedmult) *speedmult = iter->second.mSpeedMult; return true; } float Animation::getCurrentTime(const std::string &groupname) const { AnimStateMap::const_iterator iter = mStates.find(groupname); if(iter == mStates.end()) return -1.f; return iter->second.getTime(); } size_t Animation::getCurrentLoopCount(const std::string& groupname) const { AnimStateMap::const_iterator iter = mStates.find(groupname); if(iter == mStates.end()) return 0; return iter->second.mLoopCount; } void Animation::disable(const std::string &groupname) { AnimStateMap::iterator iter = mStates.find(groupname); if(iter != mStates.end()) mStates.erase(iter); resetActiveGroups(); } float Animation::getVelocity(const std::string &groupname) const { if (!mAccumRoot) return 0.0f; std::map::const_iterator found = mAnimVelocities.find(groupname); if (found != mAnimVelocities.end()) return found->second; // Look in reverse; last-inserted source has priority. AnimSourceList::const_reverse_iterator animsrc(mAnimSources.rbegin()); for(;animsrc != mAnimSources.rend();++animsrc) { const SceneUtil::TextKeyMap &keys = (*animsrc)->getTextKeys(); if (keys.hasGroupStart(groupname)) break; } if(animsrc == mAnimSources.rend()) return 0.0f; float velocity = 0.0f; const SceneUtil::TextKeyMap &keys = (*animsrc)->getTextKeys(); const AnimSource::ControllerMap& ctrls = (*animsrc)->mControllerMap[0]; for (AnimSource::ControllerMap::const_iterator it = ctrls.begin(); it != ctrls.end(); ++it) { if (Misc::StringUtils::ciEqual(it->first, mAccumRoot->getName())) { velocity = calcAnimVelocity(keys, it->second, mAccumulate, groupname); break; } } // If there's no velocity, keep looking if(!(velocity > 1.0f)) { AnimSourceList::const_reverse_iterator animiter = mAnimSources.rbegin(); while(*animiter != *animsrc) ++animiter; while(!(velocity > 1.0f) && ++animiter != mAnimSources.rend()) { const SceneUtil::TextKeyMap &keys2 = (*animiter)->getTextKeys(); const AnimSource::ControllerMap& ctrls2 = (*animiter)->mControllerMap[0]; for (AnimSource::ControllerMap::const_iterator it = ctrls2.begin(); it != ctrls2.end(); ++it) { if (Misc::StringUtils::ciEqual(it->first, mAccumRoot->getName())) { velocity = calcAnimVelocity(keys2, it->second, mAccumulate, groupname); break; } } } } mAnimVelocities.insert(std::make_pair(groupname, velocity)); return velocity; } void Animation::updatePosition(float oldtime, float newtime, osg::Vec3f& position) { // Get the difference from the last update, and move the position osg::Vec3f off = osg::componentMultiply(mAccumCtrl->getTranslation(newtime), mAccumulate); position += off - osg::componentMultiply(mAccumCtrl->getTranslation(oldtime), mAccumulate); } osg::Vec3f Animation::runAnimation(float duration) { // If we have scripted animations, play only them bool hasScriptedAnims = false; for (AnimStateMap::iterator stateiter = mStates.begin(); stateiter != mStates.end(); stateiter++) { if (stateiter->second.mPriority.contains(int(MWMechanics::Priority_Persistent)) && stateiter->second.mPlaying) { hasScriptedAnims = true; break; } } osg::Vec3f movement(0.f, 0.f, 0.f); AnimStateMap::iterator stateiter = mStates.begin(); while(stateiter != mStates.end()) { AnimState &state = stateiter->second; if (hasScriptedAnims && !state.mPriority.contains(int(MWMechanics::Priority_Persistent))) { ++stateiter; continue; } const SceneUtil::TextKeyMap &textkeys = state.mSource->getTextKeys(); auto textkey = textkeys.upperBound(state.getTime()); float timepassed = duration * state.mSpeedMult; while(state.mPlaying) { if (!state.shouldLoop()) { float targetTime = state.getTime() + timepassed; if(textkey == textkeys.end() || textkey->first > targetTime) { if(mAccumCtrl && state.mTime == mAnimationTimePtr[0]->getTimePtr()) updatePosition(state.getTime(), targetTime, movement); state.setTime(std::min(targetTime, state.mStopTime)); } else { if(mAccumCtrl && state.mTime == mAnimationTimePtr[0]->getTimePtr()) updatePosition(state.getTime(), textkey->first, movement); state.setTime(textkey->first); } state.mPlaying = (state.getTime() < state.mStopTime); timepassed = targetTime - state.getTime(); while(textkey != textkeys.end() && textkey->first <= state.getTime()) { handleTextKey(state, stateiter->first, textkey, textkeys); ++textkey; } } if(state.shouldLoop()) { state.mLoopCount--; state.setTime(state.mLoopStartTime); state.mPlaying = true; textkey = textkeys.lowerBound(state.getTime()); while(textkey != textkeys.end() && textkey->first <= state.getTime()) { handleTextKey(state, stateiter->first, textkey, textkeys); ++textkey; } if(state.getTime() >= state.mLoopStopTime) break; } if(timepassed <= 0.0f) break; } if(!state.mPlaying && state.mAutoDisable) { mStates.erase(stateiter++); resetActiveGroups(); } else ++stateiter; } updateEffects(); const float epsilon = 0.001f; float yawOffset = 0; if (mRootController) { bool enable = std::abs(mLegsYawRadians) > epsilon || std::abs(mBodyPitchRadians) > epsilon; mRootController->setEnabled(enable); if (enable) { mRootController->setRotate(osg::Quat(mLegsYawRadians, osg::Vec3f(0,0,1)) * osg::Quat(mBodyPitchRadians, osg::Vec3f(1,0,0))); yawOffset = mLegsYawRadians; } } if (mSpineController) { float yaw = mUpperBodyYawRadians - yawOffset; bool enable = std::abs(yaw) > epsilon; mSpineController->setEnabled(enable); if (enable) { mSpineController->setRotate(osg::Quat(yaw, osg::Vec3f(0,0,1))); yawOffset = mUpperBodyYawRadians; } } if (mHeadController) { float yaw = mHeadYawRadians - yawOffset; bool enable = (std::abs(mHeadPitchRadians) > epsilon || std::abs(yaw) > epsilon); mHeadController->setEnabled(enable); if (enable) mHeadController->setRotate(osg::Quat(mHeadPitchRadians, osg::Vec3f(1,0,0)) * osg::Quat(yaw, osg::Vec3f(0,0,1))); } // Scripted animations should not cause movement if (hasScriptedAnims) return osg::Vec3f(0, 0, 0); return movement; } void Animation::setLoopingEnabled(const std::string &groupname, bool enabled) { AnimStateMap::iterator state(mStates.find(groupname)); if(state != mStates.end()) state->second.mLoopingEnabled = enabled; } void loadBonesFromFile(osg::ref_ptr& baseNode, const std::string &model, Resource::ResourceSystem* resourceSystem) { const osg::Node* node = resourceSystem->getSceneManager()->getTemplate(model).get(); osg::ref_ptr sheathSkeleton (const_cast(node)); // const-trickery required because there is no const version of NodeVisitor GetExtendedBonesVisitor getBonesVisitor; sheathSkeleton->accept(getBonesVisitor); for (auto& nodePair : getBonesVisitor.mFoundBones) { SceneUtil::FindByNameVisitor findVisitor (nodePair.second->getName()); baseNode->accept(findVisitor); osg::Group* sheathParent = findVisitor.mFoundNode; if (sheathParent) { osg::Node* copy = static_cast(nodePair.first->clone(osg::CopyOp::DEEP_COPY_NODES)); sheathParent->addChild(copy); } } } void injectCustomBones(osg::ref_ptr& node, const std::string& model, Resource::ResourceSystem* resourceSystem) { if (model.empty()) return; const std::map& index = resourceSystem->getVFS()->getIndex(); std::string animationPath = model; if (animationPath.find("meshes") == 0) { animationPath.replace(0, 6, "animations"); } animationPath.replace(animationPath.size()-4, 4, "/"); resourceSystem->getVFS()->normalizeFilename(animationPath); std::map::const_iterator found = index.lower_bound(animationPath); while (found != index.end()) { const std::string& name = found->first; if (name.size() >= animationPath.size() && name.substr(0, animationPath.size()) == animationPath) { size_t pos = name.find_last_of('.'); if (pos != std::string::npos && name.compare(pos, name.size()-pos, ".nif") == 0) loadBonesFromFile(node, name, resourceSystem); } else break; ++found; } } osg::ref_ptr getModelInstance(Resource::ResourceSystem* resourceSystem, const std::string& model, bool baseonly, bool inject, const std::string& defaultSkeleton) { Resource::SceneManager* sceneMgr = resourceSystem->getSceneManager(); if (baseonly) { typedef std::map > Cache; static Cache cache; Cache::iterator found = cache.find(model); if (found == cache.end()) { osg::ref_ptr created = sceneMgr->getInstance(model); if (inject) { injectCustomBones(created, defaultSkeleton, resourceSystem); injectCustomBones(created, model, resourceSystem); } SceneUtil::CleanObjectRootVisitor removeDrawableVisitor; created->accept(removeDrawableVisitor); removeDrawableVisitor.remove(); cache.insert(std::make_pair(model, created)); return sceneMgr->createInstance(created); } else return sceneMgr->createInstance(found->second); } else { osg::ref_ptr created = sceneMgr->getInstance(model); if (inject) { injectCustomBones(created, defaultSkeleton, resourceSystem); injectCustomBones(created, model, resourceSystem); } return created; } } void Animation::setObjectRoot(const std::string &model, bool forceskeleton, bool baseonly, bool isCreature) { osg::ref_ptr previousStateset; if (mObjectRoot) { if (mLightListCallback) mObjectRoot->removeCullCallback(mLightListCallback); if (mTransparencyUpdater) mObjectRoot->removeCullCallback(mTransparencyUpdater); previousStateset = mObjectRoot->getStateSet(); mObjectRoot->getParent(0)->removeChild(mObjectRoot); } mObjectRoot = nullptr; mSkeleton = nullptr; mNodeMap.clear(); mNodeMapCreated = false; mActiveControllers.clear(); mAccumRoot = nullptr; mAccumCtrl = nullptr; static const bool useAdditionalSources = Settings::Manager::getBool ("use additional anim sources", "Game"); std::string defaultSkeleton; bool inject = false; if (useAdditionalSources && mPtr.getClass().isActor()) { if (isCreature) { MWWorld::LiveCellRef *ref = mPtr.get(); if(ref->mBase->mFlags & ESM::Creature::Bipedal) { defaultSkeleton = Settings::Manager::getString("xbaseanim", "Models"); inject = true; } } else { inject = true; MWWorld::LiveCellRef *ref = mPtr.get(); if (!ref->mBase->mModel.empty()) { // If NPC has a custom animation model attached, we should inject bones from default skeleton for given race and gender as well // Since it is a quite rare case, there should not be a noticable performance loss // Note: consider that player and werewolves have no custom animation files attached for now const MWWorld::ESMStore &store = MWBase::Environment::get().getWorld()->getStore(); const ESM::Race *race = store.get().find(ref->mBase->mRace); bool isBeast = (race->mData.mFlags & ESM::Race::Beast) != 0; bool isFemale = !ref->mBase->isMale(); defaultSkeleton = SceneUtil::getActorSkeleton(false, isFemale, isBeast, false); defaultSkeleton = Misc::ResourceHelpers::correctActorModelPath(defaultSkeleton, mResourceSystem->getVFS()); } } } if (!forceskeleton) { osg::ref_ptr created = getModelInstance(mResourceSystem, model, baseonly, inject, defaultSkeleton); mInsert->addChild(created); mObjectRoot = created->asGroup(); if (!mObjectRoot) { mInsert->removeChild(created); mObjectRoot = new osg::Group; mObjectRoot->addChild(created); mInsert->addChild(mObjectRoot); } osg::ref_ptr skel = dynamic_cast(mObjectRoot.get()); if (skel) mSkeleton = skel.get(); } else { osg::ref_ptr created = getModelInstance(mResourceSystem, model, baseonly, inject, defaultSkeleton); osg::ref_ptr skel = dynamic_cast(created.get()); if (!skel) { skel = new SceneUtil::Skeleton; skel->addChild(created); } mSkeleton = skel.get(); mObjectRoot = skel; mInsert->addChild(mObjectRoot); } if (previousStateset) mObjectRoot->setStateSet(previousStateset); if (isCreature) { SceneUtil::RemoveTriBipVisitor removeTriBipVisitor; mObjectRoot->accept(removeTriBipVisitor); removeTriBipVisitor.remove(); } if (!mLightListCallback) mLightListCallback = new SceneUtil::LightListCallback; mObjectRoot->addCullCallback(mLightListCallback); if (mTransparencyUpdater) mObjectRoot->addCullCallback(mTransparencyUpdater); } osg::Group* Animation::getObjectRoot() { return mObjectRoot.get(); } osg::Group* Animation::getOrCreateObjectRoot() { if (mObjectRoot) return mObjectRoot.get(); mObjectRoot = new osg::Group; mInsert->addChild(mObjectRoot); return mObjectRoot.get(); } void Animation::addSpellCastGlow(const ESM::MagicEffect *effect, float glowDuration) { osg::Vec4f glowColor(1,1,1,1); glowColor.x() = effect->mData.mRed / 255.f; glowColor.y() = effect->mData.mGreen / 255.f; glowColor.z() = effect->mData.mBlue / 255.f; if (!mGlowUpdater || (mGlowUpdater->isDone() || (mGlowUpdater->isPermanentGlowUpdater() == true))) { if (mGlowUpdater && mGlowUpdater->isDone()) mObjectRoot->removeUpdateCallback(mGlowUpdater); if (mGlowUpdater && mGlowUpdater->isPermanentGlowUpdater()) { mGlowUpdater->setColor(glowColor); mGlowUpdater->setDuration(glowDuration); } else mGlowUpdater = SceneUtil::addEnchantedGlow(mObjectRoot, mResourceSystem, glowColor, glowDuration); } } void Animation::addExtraLight(osg::ref_ptr parent, const ESM::Light *esmLight) { bool exterior = mPtr.isInCell() && mPtr.getCell()->getCell()->isExterior(); mExtraLightSource = SceneUtil::addLight(parent, esmLight, Mask_ParticleSystem, Mask_Lighting, exterior); } void Animation::addEffect (const std::string& model, int effectId, bool loop, const std::string& bonename, const std::string& texture) { if (!mObjectRoot.get()) return; // Early out if we already have this effect FindVfxCallbacksVisitor visitor(effectId); mInsert->accept(visitor); for (std::vector::iterator it = visitor.mCallbacks.begin(); it != visitor.mCallbacks.end(); ++it) { UpdateVfxCallback* callback = *it; if (loop && !callback->mFinished && callback->mParams.mLoop && callback->mParams.mBoneName == bonename) return; } EffectParams params; params.mModelName = model; osg::ref_ptr parentNode; if (bonename.empty()) parentNode = mInsert; else { NodeMap::const_iterator found = getNodeMap().find(Misc::StringUtils::lowerCase(bonename)); if (found == getNodeMap().end()) throw std::runtime_error("Can't find bone " + bonename); parentNode = found->second; } osg::ref_ptr trans = new osg::PositionAttitudeTransform; if (!mPtr.getClass().isNpc()) { osg::Vec3f bounds (MWBase::Environment::get().getWorld()->getHalfExtents(mPtr) * 2.f / Constants::UnitsPerFoot); float scale = std::max({ bounds.x()/3.f, bounds.y()/3.f, bounds.z()/6.f }); trans->setScale(osg::Vec3f(scale, scale, scale)); } parentNode->addChild(trans); osg::ref_ptr node = mResourceSystem->getSceneManager()->getInstance(model, trans); node->getOrCreateStateSet()->setMode(GL_LIGHTING, osg::StateAttribute::OFF); SceneUtil::FindMaxControllerLengthVisitor findMaxLengthVisitor; node->accept(findMaxLengthVisitor); // FreezeOnCull doesn't work so well with effect particles, that tend to have moving emitters SceneUtil::DisableFreezeOnCullVisitor disableFreezeOnCullVisitor; node->accept(disableFreezeOnCullVisitor); node->setNodeMask(Mask_Effect); params.mMaxControllerLength = findMaxLengthVisitor.getMaxLength(); params.mLoop = loop; params.mEffectId = effectId; params.mBoneName = bonename; params.mAnimTime = std::shared_ptr(new EffectAnimationTime); trans->addUpdateCallback(new UpdateVfxCallback(params)); SceneUtil::AssignControllerSourcesVisitor assignVisitor(std::shared_ptr(params.mAnimTime)); node->accept(assignVisitor); // Notify that this animation has attached magic effects mHasMagicEffects = true; overrideFirstRootTexture(texture, mResourceSystem, node); } void Animation::removeEffect(int effectId) { RemoveCallbackVisitor visitor(effectId); mInsert->accept(visitor); visitor.remove(); mHasMagicEffects = visitor.mHasMagicEffects; } void Animation::removeEffects() { removeEffect(-1); } void Animation::getLoopingEffects(std::vector &out) const { if (!mHasMagicEffects) return; FindVfxCallbacksVisitor visitor; mInsert->accept(visitor); for (std::vector::iterator it = visitor.mCallbacks.begin(); it != visitor.mCallbacks.end(); ++it) { UpdateVfxCallback* callback = *it; if (callback->mParams.mLoop && !callback->mFinished) out.push_back(callback->mParams.mEffectId); } } void Animation::updateEffects() { // We do not need to visit scene every frame. // We can use a bool flag to check in spellcasting effect found. if (!mHasMagicEffects) return; // TODO: objects without animation still will have // transformation nodes with finished callbacks RemoveFinishedCallbackVisitor visitor; mInsert->accept(visitor); visitor.remove(); mHasMagicEffects = visitor.mHasMagicEffects; } bool Animation::upperBodyReady() const { for (AnimStateMap::const_iterator stateiter = mStates.begin(); stateiter != mStates.end(); ++stateiter) { if (stateiter->second.mPriority.contains(int(MWMechanics::Priority_Hit)) || stateiter->second.mPriority.contains(int(MWMechanics::Priority_Weapon)) || stateiter->second.mPriority.contains(int(MWMechanics::Priority_Knockdown)) || stateiter->second.mPriority.contains(int(MWMechanics::Priority_Death))) return false; } return true; } const osg::Node* Animation::getNode(const std::string &name) const { std::string lowerName = Misc::StringUtils::lowerCase(name); NodeMap::const_iterator found = getNodeMap().find(lowerName); if (found == getNodeMap().end()) return nullptr; else return found->second; } void Animation::setAlpha(float alpha) { if (alpha == mAlpha) return; mAlpha = alpha; // TODO: we use it to fade actors away too, but it would be nice to have a dithering shader instead. if (alpha != 1.f) { if (mTransparencyUpdater == nullptr) { mTransparencyUpdater = new TransparencyUpdater(alpha); mTransparencyUpdater->setLightSource(mExtraLightSource); mObjectRoot->addCullCallback(mTransparencyUpdater); } else mTransparencyUpdater->setAlpha(alpha); } else { mObjectRoot->removeCullCallback(mTransparencyUpdater); mTransparencyUpdater = nullptr; } } void Animation::setLightEffect(float effect) { if (effect == 0) { if (mGlowLight) { mInsert->removeChild(mGlowLight); mGlowLight = nullptr; } } else { // 1 pt of Light magnitude corresponds to 1 foot of radius float radius = effect * std::ceil(Constants::UnitsPerFoot); // Arbitrary multiplier used to make the obvious cut-off less obvious float cutoffMult = 3; if (!mGlowLight || (radius * cutoffMult) != mGlowLight->getRadius()) { if (mGlowLight) { mInsert->removeChild(mGlowLight); mGlowLight = nullptr; } osg::ref_ptr light (new osg::Light); light->setDiffuse(osg::Vec4f(0,0,0,0)); light->setSpecular(osg::Vec4f(0,0,0,0)); light->setAmbient(osg::Vec4f(1.5f,1.5f,1.5f,1.f)); bool isExterior = mPtr.isInCell() && mPtr.getCell()->getCell()->isExterior(); SceneUtil::configureLight(light, radius, isExterior); mGlowLight = new SceneUtil::LightSource; mGlowLight->setNodeMask(Mask_Lighting); mInsert->addChild(mGlowLight); mGlowLight->setLight(light); } mGlowLight->setRadius(radius * cutoffMult); } } void Animation::addControllers() { mHeadController = addRotateController("bip01 head"); mSpineController = addRotateController("bip01 spine1"); mRootController = addRotateController("bip01"); } RotateController* Animation::addRotateController(std::string bone) { auto iter = getNodeMap().find(bone); if (iter == getNodeMap().end()) return nullptr; osg::MatrixTransform* node = iter->second; bool foundKeyframeCtrl = false; osg::Callback* cb = node->getUpdateCallback(); while (cb) { if (dynamic_cast(cb)) { foundKeyframeCtrl = true; break; } cb = cb->getNestedCallback(); } // Without KeyframeController the orientation will not be reseted each frame, so // RotateController shouldn't be used for such nodes. if (!foundKeyframeCtrl) return nullptr; RotateController* controller = new RotateController(mObjectRoot.get()); node->addUpdateCallback(controller); mActiveControllers.emplace_back(node, controller); return controller; } void Animation::setHeadPitch(float pitchRadians) { mHeadPitchRadians = pitchRadians; } void Animation::setHeadYaw(float yawRadians) { mHeadYawRadians = yawRadians; } float Animation::getHeadPitch() const { return mHeadPitchRadians; } float Animation::getHeadYaw() const { return mHeadYawRadians; } // ------------------------------------------------------ float Animation::AnimationTime::getValue(osg::NodeVisitor*) { if (mTimePtr) return *mTimePtr; return 0.f; } float EffectAnimationTime::getValue(osg::NodeVisitor*) { return mTime; } void EffectAnimationTime::addTime(float duration) { mTime += duration; } void EffectAnimationTime::resetTime(float time) { mTime = time; } float EffectAnimationTime::getTime() const { return mTime; } // -------------------------------------------------------------------------------- ObjectAnimation::ObjectAnimation(const MWWorld::Ptr &ptr, const std::string &model, Resource::ResourceSystem* resourceSystem, bool animated, bool allowLight) : Animation(ptr, osg::ref_ptr(ptr.getRefData().getBaseNode()), resourceSystem) { if (!model.empty()) { setObjectRoot(model, false, false, false); if (animated) addAnimSource(model, model); if (!ptr.getClass().getEnchantment(ptr).empty()) mGlowUpdater = SceneUtil::addEnchantedGlow(mObjectRoot, mResourceSystem, ptr.getClass().getEnchantmentColor(ptr)); } if (ptr.getTypeName() == typeid(ESM::Light).name() && allowLight) addExtraLight(getOrCreateObjectRoot(), ptr.get()->mBase); if (!allowLight && mObjectRoot) { RemoveParticlesVisitor visitor; mObjectRoot->accept(visitor); visitor.remove(); } if (SceneUtil::hasUserDescription(mObjectRoot, Constants::NightDayLabel)) { AddSwitchCallbacksVisitor visitor; mObjectRoot->accept(visitor); } if (ptr.getRefData().getCustomData() != nullptr && canBeHarvested()) { const MWWorld::ContainerStore& store = ptr.getClass().getContainerStore(ptr); if (!store.hasVisibleItems()) { HarvestVisitor visitor; mObjectRoot->accept(visitor); } } } bool ObjectAnimation::canBeHarvested() const { if (mPtr.getTypeName() != typeid(ESM::Container).name()) return false; const MWWorld::LiveCellRef* ref = mPtr.get(); if (!(ref->mBase->mFlags & ESM::Container::Organic)) return false; return SceneUtil::hasUserDescription(mObjectRoot, Constants::HerbalismLabel); } Animation::AnimState::~AnimState() { } // ------------------------------ PartHolder::PartHolder(osg::ref_ptr node) : mNode(node) { } PartHolder::~PartHolder() { if (mNode.get() && !mNode->getNumParents()) Log(Debug::Verbose) << "Part \"" << mNode->getName() << "\" has no parents" ; if (mNode.get() && mNode->getNumParents()) { if (mNode->getNumParents() > 1) Log(Debug::Verbose) << "Part \"" << mNode->getName() << "\" has multiple (" << mNode->getNumParents() << ") parents"; mNode->getParent(0)->removeChild(mNode); } } } ================================================ FILE: apps/openmw/mwrender/animation.hpp ================================================ #ifndef GAME_RENDER_ANIMATION_H #define GAME_RENDER_ANIMATION_H #include "../mwworld/ptr.hpp" #include #include #include #include namespace ESM { struct Light; struct MagicEffect; } namespace Resource { class ResourceSystem; } namespace SceneUtil { class KeyframeHolder; class KeyframeController; class LightSource; class LightListCallback; class Skeleton; } namespace MWRender { class ResetAccumRootCallback; class RotateController; class TransparencyUpdater; class EffectAnimationTime : public SceneUtil::ControllerSource { private: float mTime; public: float getValue(osg::NodeVisitor* nv) override; void addTime(float duration); void resetTime(float time); float getTime() const; EffectAnimationTime() : mTime(0) { } }; /// @brief Detaches the node from its parent when the object goes out of scope. class PartHolder { public: PartHolder(osg::ref_ptr node); ~PartHolder(); osg::ref_ptr getNode() { return mNode; } private: osg::ref_ptr mNode; void operator= (const PartHolder&); PartHolder(const PartHolder&); }; typedef std::shared_ptr PartHolderPtr; struct EffectParams { std::string mModelName; // Just here so we don't add the same effect twice std::shared_ptr mAnimTime; float mMaxControllerLength; int mEffectId; bool mLoop; std::string mBoneName; }; class Animation : public osg::Referenced { public: enum BoneGroup { BoneGroup_LowerBody = 0, BoneGroup_Torso, BoneGroup_LeftArm, BoneGroup_RightArm }; enum BlendMask { BlendMask_LowerBody = 1<<0, BlendMask_Torso = 1<<1, BlendMask_LeftArm = 1<<2, BlendMask_RightArm = 1<<3, BlendMask_UpperBody = BlendMask_Torso | BlendMask_LeftArm | BlendMask_RightArm, BlendMask_All = BlendMask_LowerBody | BlendMask_UpperBody }; /* This is the number of *discrete* blend masks. */ static constexpr size_t sNumBlendMasks = 4; /// Holds an animation priority value for each BoneGroup. struct AnimPriority { /// Convenience constructor, initialises all priorities to the same value. AnimPriority(int priority) { for (unsigned int i=0; i mTimePtr; public: void setTimePtr(std::shared_ptr time) { mTimePtr = time; } std::shared_ptr getTimePtr() const { return mTimePtr; } float getValue(osg::NodeVisitor* nv) override; }; class NullAnimationTime : public SceneUtil::ControllerSource { public: float getValue(osg::NodeVisitor *nv) override { return 0.f; } }; struct AnimSource; struct AnimState { std::shared_ptr mSource; float mStartTime; float mLoopStartTime; float mLoopStopTime; float mStopTime; typedef std::shared_ptr TimePtr; TimePtr mTime; float mSpeedMult; bool mPlaying; bool mLoopingEnabled; size_t mLoopCount; AnimPriority mPriority; int mBlendMask; bool mAutoDisable; AnimState() : mStartTime(0.0f), mLoopStartTime(0.0f), mLoopStopTime(0.0f), mStopTime(0.0f), mTime(new float), mSpeedMult(1.0f), mPlaying(false), mLoopingEnabled(true), mLoopCount(0), mPriority(0), mBlendMask(0), mAutoDisable(true) { } ~AnimState(); float getTime() const { return *mTime; } void setTime(float time) { *mTime = time; } bool shouldLoop() const { return getTime() >= mLoopStopTime && mLoopingEnabled && mLoopCount > 0; } }; typedef std::map AnimStateMap; AnimStateMap mStates; typedef std::vector > AnimSourceList; AnimSourceList mAnimSources; osg::ref_ptr mInsert; osg::ref_ptr mObjectRoot; SceneUtil::Skeleton* mSkeleton; // The node expected to accumulate movement during movement animations. osg::ref_ptr mAccumRoot; // The controller animating that node. osg::ref_ptr mAccumCtrl; // Used to reset the position of the accumulation root every frame - the movement should be applied to the physics system osg::ref_ptr mResetAccumRootCallback; // Keep track of controllers that we added to our scene graph. // We may need to rebuild these controllers when the active animation groups / sources change. std::vector, osg::ref_ptr>> mActiveControllers; std::shared_ptr mAnimationTimePtr[sNumBlendMasks]; // Stored in all lowercase for a case-insensitive lookup typedef std::map > NodeMap; mutable NodeMap mNodeMap; mutable bool mNodeMapCreated; MWWorld::Ptr mPtr; Resource::ResourceSystem* mResourceSystem; osg::Vec3f mAccumulate; TextKeyListener* mTextKeyListener; osg::ref_ptr mHeadController; osg::ref_ptr mSpineController; osg::ref_ptr mRootController; float mHeadYawRadians; float mHeadPitchRadians; float mUpperBodyYawRadians; float mLegsYawRadians; float mBodyPitchRadians; RotateController* addRotateController(std::string bone); bool mHasMagicEffects; osg::ref_ptr mGlowLight; osg::ref_ptr mGlowUpdater; osg::ref_ptr mTransparencyUpdater; osg::ref_ptr mExtraLightSource; float mAlpha; mutable std::map mAnimVelocities; osg::ref_ptr mLightListCallback; const NodeMap& getNodeMap() const; /* Sets the appropriate animations on the bone groups based on priority. */ void resetActiveGroups(); size_t detectBlendMask(const osg::Node* node) const; /* Updates the position of the accum root node for the given time, and * returns the wanted movement vector from the previous time. */ void updatePosition(float oldtime, float newtime, osg::Vec3f& position); /* Resets the animation to the time of the specified start marker, without * moving anything, and set the end time to the specified stop marker. If * the marker is not found, or if the markers are the same, it returns * false. */ bool reset(AnimState &state, const SceneUtil::TextKeyMap &keys, const std::string &groupname, const std::string &start, const std::string &stop, float startpoint, bool loopfallback); void handleTextKey(AnimState &state, const std::string &groupname, SceneUtil::TextKeyMap::ConstIterator key, const SceneUtil::TextKeyMap& map); /** Sets the root model of the object. * * Note that you must make sure all animation sources are cleared before resetting the object * root. All nodes previously retrieved with getNode will also become invalidated. * @param forceskeleton Wrap the object root in a Skeleton, even if it contains no skinned parts. Use this if you intend to add skinned parts manually. * @param baseonly If true, then any meshes or particle systems in the model are ignored * (useful for NPCs, where only the skeleton is needed for the root, and the actual NPC parts are then assembled from separate files). */ void setObjectRoot(const std::string &model, bool forceskeleton, bool baseonly, bool isCreature); void loadAllAnimationsInFolder(const std::string &model, const std::string &baseModel); /** Adds the keyframe controllers in the specified model as a new animation source. * @note Later added animation sources have the highest priority when it comes to finding a particular animation. * @param model The file to add the keyframes for. Note that the .nif file extension will be replaced with .kf. * @param baseModel The filename of the mObjectRoot, only used for error messages. */ void addAnimSource(const std::string &model, const std::string& baseModel); void addSingleAnimSource(const std::string &model, const std::string& baseModel); /** Adds an additional light to the given node using the specified ESM record. */ void addExtraLight(osg::ref_ptr parent, const ESM::Light *light); void clearAnimSources(); /** * Provided to allow derived classes adding their own controllers. Note, the controllers must be added to mActiveControllers * so they get cleaned up properly on the next controller rebuild. A controller rebuild may be necessary to ensure correct ordering. */ virtual void addControllers(); public: Animation(const MWWorld::Ptr &ptr, osg::ref_ptr parentNode, Resource::ResourceSystem* resourceSystem); /// Must be thread safe virtual ~Animation(); MWWorld::ConstPtr getPtr() const; MWWorld::Ptr getPtr(); /// Set active flag on the object skeleton, if one exists. /// @see SceneUtil::Skeleton::setActive /// 0 = Inactive, 1 = Active in place, 2 = Active void setActive(int active); osg::Group* getOrCreateObjectRoot(); osg::Group* getObjectRoot(); /** * @brief Add an effect mesh attached to a bone or the insert scene node * @param model * @param effectId An ID for this effect by which you can identify it later. If this is not wanted, set to -1. * @param loop Loop the effect. If false, it is removed automatically after it finishes playing. If true, * you need to remove it manually using removeEffect when the effect should end. * @param bonename Bone to attach to, or empty string to use the scene node instead * @param texture override the texture specified in the model's materials - if empty, do not override * @note Will not add an effect twice. */ void addEffect (const std::string& model, int effectId, bool loop = false, const std::string& bonename = "", const std::string& texture = ""); void removeEffect (int effectId); void removeEffects (); void getLoopingEffects (std::vector& out) const; // Add a spell casting glow to an object. From measuring video taken from the original engine, // the glow seems to be about 1.5 seconds except for telekinesis, which is 1 second. void addSpellCastGlow(const ESM::MagicEffect *effect, float glowDuration = 1.5); virtual void updatePtr(const MWWorld::Ptr &ptr); bool hasAnimation(const std::string &anim) const; // Specifies the axis' to accumulate on. Non-accumulated axis will just // move visually, but not affect the actual movement. Each x/y/z value // should be on the scale of 0 to 1. void setAccumulation(const osg::Vec3f& accum); /** Plays an animation. * \param groupname Name of the animation group to play. * \param priority Priority of the animation. The animation will play on * bone groups that don't have another animation set of a * higher priority. * \param blendMask Bone groups to play the animation on. * \param autodisable Automatically disable the animation when it stops * playing. * \param speedmult Speed multiplier for the animation. * \param start Key marker from which to start. * \param stop Key marker to stop at. * \param startpoint How far in between the two markers to start. 0 starts * at the start marker, 1 starts at the stop marker. * \param loops How many times to loop the animation. This will use the * "loop start" and "loop stop" markers if they exist, * otherwise it may fall back to "start" and "stop", but only if * the \a loopFallback parameter is true. * \param loopFallback Allow looping an animation that has no loop keys, i.e. fall back to use * the "start" and "stop" keys for looping? */ void play(const std::string &groupname, const AnimPriority& priority, int blendMask, bool autodisable, float speedmult, const std::string &start, const std::string &stop, float startpoint, size_t loops, bool loopfallback=false); /** Adjust the speed multiplier of an already playing animation. */ void adjustSpeedMult (const std::string& groupname, float speedmult); /** Returns true if the named animation group is playing. */ bool isPlaying(const std::string &groupname) const; /// Returns true if no important animations are currently playing on the upper body. bool upperBodyReady() const; /** Gets info about the given animation group. * \param groupname Animation group to check. * \param complete Stores completion amount (0 = at start key, 0.5 = half way between start and stop keys), etc. * \param speedmult Stores the animation speed multiplier * \return True if the animation is active, false otherwise. */ bool getInfo(const std::string &groupname, float *complete=nullptr, float *speedmult=nullptr) const; /// Get the absolute position in the animation track of the first text key with the given group. float getStartTime(const std::string &groupname) const; /// Get the absolute position in the animation track of the text key float getTextKeyTime(const std::string &textKey) const; /// Get the current absolute position in the animation track for the animation that is currently playing from the given group. float getCurrentTime(const std::string& groupname) const; size_t getCurrentLoopCount(const std::string& groupname) const; /** Disables the specified animation group; * \param groupname Animation group to disable. */ void disable(const std::string &groupname); /** Retrieves the velocity (in units per second) that the animation will move. */ float getVelocity(const std::string &groupname) const; virtual osg::Vec3f runAnimation(float duration); void setLoopingEnabled(const std::string &groupname, bool enabled); /// This is typically called as part of runAnimation, but may be called manually if needed. void updateEffects(); /// Return a node with the specified name, or nullptr if not existing. /// @note The matching is case-insensitive. const osg::Node* getNode(const std::string& name) const; virtual bool useShieldAnimations() const { return false; } virtual void showWeapons(bool showWeapon) {} virtual bool getCarriedLeftShown() const { return false; } virtual void showCarriedLeft(bool show) {} virtual void setWeaponGroup(const std::string& group, bool relativeDuration) {} virtual void setVampire(bool vampire) {} /// A value < 1 makes the animation translucent, 1.f = fully opaque void setAlpha(float alpha); virtual void setPitchFactor(float factor) {} virtual void attachArrow() {} virtual void detachArrow() {} virtual void releaseArrow(float attackStrength) {} virtual void enableHeadAnimation(bool enable) {} // TODO: move outside of this class /// Makes this object glow, by placing a Light in its center. /// @param effect Controls the radius and intensity of the light. virtual void setLightEffect(float effect); virtual void setHeadPitch(float pitchRadians); virtual void setHeadYaw(float yawRadians); virtual float getHeadPitch() const; virtual float getHeadYaw() const; virtual void setUpperBodyYawRadians(float v) { mUpperBodyYawRadians = v; } virtual void setLegsYawRadians(float v) { mLegsYawRadians = v; } virtual float getUpperBodyYawRadians() const { return mUpperBodyYawRadians; } virtual float getLegsYawRadians() const { return mLegsYawRadians; } virtual void setBodyPitchRadians(float v) { mBodyPitchRadians = v; } virtual float getBodyPitchRadians() const { return mBodyPitchRadians; } virtual void setAccurateAiming(bool enabled) {} virtual bool canBeHarvested() const { return false; } private: Animation(const Animation&); void operator=(Animation&); }; class ObjectAnimation : public Animation { public: ObjectAnimation(const MWWorld::Ptr& ptr, const std::string &model, Resource::ResourceSystem* resourceSystem, bool animated, bool allowLight); bool canBeHarvested() const override; }; class UpdateVfxCallback : public osg::NodeCallback { public: UpdateVfxCallback(EffectParams& params) : mFinished(false) , mParams(params) , mStartingTime(0) { } bool mFinished; EffectParams mParams; void operator()(osg::Node* node, osg::NodeVisitor* nv) override; private: double mStartingTime; }; } #endif ================================================ FILE: apps/openmw/mwrender/bulletdebugdraw.cpp ================================================ #include #include #include #include #include #include #include #include #include #include "bulletdebugdraw.hpp" #include "vismask.hpp" namespace MWRender { DebugDrawer::DebugDrawer(osg::ref_ptr parentNode, btCollisionWorld *world, int debugMode) : mParentNode(parentNode), mWorld(world) { setDebugMode(debugMode); } void DebugDrawer::createGeometry() { if (!mGeometry) { mGeometry = new osg::Geometry; mGeometry->setNodeMask(Mask_Debug); mVertices = new osg::Vec3Array; mColors = new osg::Vec4Array; mDrawArrays = new osg::DrawArrays(osg::PrimitiveSet::LINES); mGeometry->setUseDisplayList(false); mGeometry->setVertexArray(mVertices); mGeometry->setColorArray(mColors); mGeometry->setColorBinding(osg::Geometry::BIND_PER_VERTEX); mGeometry->setDataVariance(osg::Object::DYNAMIC); mGeometry->addPrimitiveSet(mDrawArrays); mParentNode->addChild(mGeometry); auto* stateSet = new osg::StateSet; stateSet->setAttributeAndModes(new osg::PolygonMode(osg::PolygonMode::FRONT_AND_BACK, osg::PolygonMode::LINE), osg::StateAttribute::ON); mShapesRoot = new osg::Group; mShapesRoot->setStateSet(stateSet); mShapesRoot->setDataVariance(osg::Object::DYNAMIC); mShapesRoot->setNodeMask(Mask_Debug); mParentNode->addChild(mShapesRoot); } } void DebugDrawer::destroyGeometry() { if (mGeometry) { mParentNode->removeChild(mGeometry); mParentNode->removeChild(mShapesRoot); mGeometry = nullptr; mVertices = nullptr; mDrawArrays = nullptr; } } DebugDrawer::~DebugDrawer() { destroyGeometry(); } void DebugDrawer::step() { if (mDebugOn) { mVertices->clear(); mColors->clear(); mShapesRoot->removeChildren(0, mShapesRoot->getNumChildren()); mWorld->debugDrawWorld(); showCollisions(); mDrawArrays->setCount(mVertices->size()); mVertices->dirty(); mColors->dirty(); mGeometry->dirtyBound(); } } void DebugDrawer::drawLine(const btVector3 &from, const btVector3 &to, const btVector3 &color) { mVertices->push_back(Misc::Convert::toOsg(from)); mVertices->push_back(Misc::Convert::toOsg(to)); mColors->push_back({1,1,1,1}); mColors->push_back({1,1,1,1}); } void DebugDrawer::addCollision(const btVector3& orig, const btVector3& normal) { mCollisionViews.emplace_back(orig, normal); } void DebugDrawer::showCollisions() { const auto now = std::chrono::steady_clock::now(); for (auto& [from, to , created] : mCollisionViews) { if (now - created < std::chrono::seconds(2)) { mVertices->push_back(Misc::Convert::toOsg(from)); mVertices->push_back(Misc::Convert::toOsg(to)); mColors->push_back({1,0,0,1}); mColors->push_back({1,0,0,1}); } } mCollisionViews.erase(std::remove_if(mCollisionViews.begin(), mCollisionViews.end(), [&now](const CollisionView& view) { return now - view.mCreated >= std::chrono::seconds(2); }), mCollisionViews.end()); } void DebugDrawer::drawSphere(btScalar radius, const btTransform& transform, const btVector3& color) { auto* geom = new osg::ShapeDrawable(new osg::Sphere(Misc::Convert::toOsg(transform.getOrigin()), radius)); geom->setColor(osg::Vec4(1, 1, 1, 1)); mShapesRoot->addChild(geom); } void DebugDrawer::reportErrorWarning(const char *warningString) { Log(Debug::Warning) << warningString; } void DebugDrawer::setDebugMode(int isOn) { mDebugOn = (isOn != 0); if (!mDebugOn) destroyGeometry(); else createGeometry(); } int DebugDrawer::getDebugMode() const { return mDebugOn; } } ================================================ FILE: apps/openmw/mwrender/bulletdebugdraw.hpp ================================================ #ifndef OPENMW_MWRENDER_BULLETDEBUGDRAW_H #define OPENMW_MWRENDER_BULLETDEBUGDRAW_H #include #include #include #include #include #include class btCollisionWorld; namespace osg { class Group; class Geometry; } namespace MWRender { class DebugDrawer : public btIDebugDraw { private: struct CollisionView { btVector3 mOrig; btVector3 mEnd; std::chrono::time_point mCreated; CollisionView(btVector3 orig, btVector3 normal) : mOrig(orig), mEnd(orig + normal * 20), mCreated(std::chrono::steady_clock::now()) {}; }; std::vector mCollisionViews; osg::ref_ptr mShapesRoot; protected: osg::ref_ptr mParentNode; btCollisionWorld *mWorld; osg::ref_ptr mGeometry; osg::ref_ptr mVertices; osg::ref_ptr mColors; osg::ref_ptr mDrawArrays; bool mDebugOn; void createGeometry(); void destroyGeometry(); public: DebugDrawer(osg::ref_ptr parentNode, btCollisionWorld *world, int debugMode = 1); ~DebugDrawer(); void step(); void drawLine(const btVector3& from,const btVector3& to,const btVector3& color) override; void addCollision(const btVector3& orig, const btVector3& normal); void showCollisions(); void drawContactPoint(const btVector3& PointOnB,const btVector3& normalOnB,btScalar distance,int lifeTime,const btVector3& color) override {}; void drawSphere(btScalar radius, const btTransform& transform, const btVector3& color) override; void reportErrorWarning(const char* warningString) override; void draw3dText(const btVector3& location,const char* textString) override {} //0 for off, anything else for on. void setDebugMode(int isOn) override; //0 for off, anything else for on. int getDebugMode() const override; }; } #endif ================================================ FILE: apps/openmw/mwrender/camera.cpp ================================================ #include "camera.hpp" #include #include #include #include #include "../mwbase/environment.hpp" #include "../mwbase/windowmanager.hpp" #include "../mwbase/world.hpp" #include "../mwworld/class.hpp" #include "../mwworld/ptr.hpp" #include "../mwworld/refdata.hpp" #include "../mwmechanics/drawstate.hpp" #include "../mwmechanics/movement.hpp" #include "../mwmechanics/npcstats.hpp" #include "../mwphysics/raycasting.hpp" #include "npcanimation.hpp" namespace { class UpdateRenderCameraCallback : public osg::NodeCallback { public: UpdateRenderCameraCallback(MWRender::Camera* cam) : mCamera(cam) { } void operator()(osg::Node* node, osg::NodeVisitor* nv) override { osg::Camera* cam = static_cast(node); // traverse first to update animations, in case the camera is attached to an animated node traverse(node, nv); mCamera->updateCamera(cam); } private: MWRender::Camera* mCamera; }; } namespace MWRender { Camera::Camera (osg::Camera* camera) : mHeightScale(1.f), mCamera(camera), mAnimation(nullptr), mFirstPersonView(true), mMode(Mode::Normal), 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), 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), 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), mDeferredRotation(osg::Vec3f()), mDeferredRotationDisabled(false) { mCameraDistance = mBaseCameraDistance; mUpdateCallback = new UpdateRenderCameraCallback(this); mCamera->addUpdateCallback(mUpdateCallback); } Camera::~Camera() { mCamera->removeUpdateCallback(mUpdateCallback); } osg::Vec3d Camera::getFocalPoint() const { if (!mTrackingNode) return osg::Vec3d(); osg::NodePathList nodepaths = mTrackingNode->getParentalNodePaths(); 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 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(); 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::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) { pitch += getPitch(); yaw += getYaw(); } setYaw(yaw); setPitch(pitch); } 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; } } 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; 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(); } void Camera::updatePosition() { mFocalPointAdjustment = osg::Vec3d(); if (isFirstPerson()) return; const float cameraObstacleLimit = 5.0f; const float focalObstacleLimit = 10.f; const auto* rayCasting = MWBase::Environment::get().getWorld()->getRayCasting(); // Adjust focal point to prevent clipping. osg::Vec3d focal = getFocalPoint(); osg::Vec3d focalOffset = getFocalPointOffset(); float offsetLen = focalOffset.length(); if (offsetLen > 0) { MWPhysics::RayCastingResult result = rayCasting->castSphere(focal - focalOffset, focal, focalObstacleLimit); if (result.mHit) { double adjustmentCoef = -(result.mHitPos + result.mHitNormal * focalObstacleLimit - focal).length() / offsetLen; mFocalPointAdjustment = 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); if (result.mHit) mCameraDistance = (result.mHitPos + result.mHitNormal * cameraObstacleLimit - focal).length(); } void Camera::updateStandingPreviewMode() { if (!mStandingPreviewAllowed) 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) { mMode = Mode::Normal; calculateDeferredRotation(); } else if (standingStill && mMode == Mode::Normal) mMode = Mode::StandingPreview; } void Camera::setFocalPointTargetOffset(osg::Vec2d v) { mFocalPointTargetOffset = v; mPreviousTransitionSpeed = mFocalPointTransitionSpeed; mPreviousTransitionInfluence = 1.0f; } void Camera::updateFocalPointOffset(float duration) { if (duration <= 0) return; if (mSkipFocalPointTransition) { mSkipFocalPointTransition = false; mPreviousExtraOffset = osg::Vec2d(); mPreviousTransitionInfluence = 0.f; mFocalPointCurrentOffset = mFocalPointTargetOffset; } osg::Vec2d oldOffset = mFocalPointCurrentOffset; if (mPreviousTransitionInfluence > 0) { mFocalPointCurrentOffset -= mPreviousExtraOffset; mPreviousExtraOffset = mPreviousExtraOffset / mPreviousTransitionInfluence + mPreviousTransitionSpeed * duration; mPreviousTransitionInfluence = std::max(0.f, mPreviousTransitionInfluence - duration * mFocalPointTransitionSpeedCoef); mPreviousExtraOffset *= mPreviousTransitionInfluence; mFocalPointCurrentOffset += mPreviousExtraOffset; } osg::Vec2d delta = mFocalPointTargetOffset - mFocalPointCurrentOffset; if (delta.length2() > 0) { float coef = duration * (1.0 + 5.0 / delta.length()) * mFocalPointTransitionSpeedCoef * (1.0f - mPreviousTransitionInfluence); mFocalPointCurrentOffset += delta * std::min(coef, 1.0f); } else { mPreviousExtraOffset = osg::Vec2d(); mPreviousTransitionInfluence = 0.f; } mFocalPointTransitionSpeed = (mFocalPointCurrentOffset - oldOffset) / duration; } 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; } 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(); } void Camera::setSneakOffset(float offset) { mAnimation->setFirstPersonOffset(osg::Vec3f(0,0,-offset)); } void Camera::setYaw(float angle) { mYaw = Misc::normalizeAngle(angle); } void Camera::setPitch(float angle) { const float epsilon = 0.000001f; float limit = static_cast(osg::PI_2) - epsilon; mPitch = osg::clampBetween(angle, -limit, limit); } float Camera::getCameraDistance() const { if (isFirstPerson()) return 0.f; return mCameraDistance; } void Camera::adjustCameraDistance(float delta) { 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; } void Camera::setAnimation(NpcAnimation *anim) { mAnimation = anim; processViewChange(); } void Camera::processViewChange() { if(isFirstPerson()) { mAnimation->setViewMode(NpcAnimation::VM_FirstPerson); mTrackingNode = mAnimation->getNode("Camera"); if (!mTrackingNode) mTrackingNode = mAnimation->getNode("Head"); mHeightScale = 1.f; } else { mAnimation->setViewMode(NpcAnimation::VM_Normal); SceneUtil::PositionAttitudeTransform* transform = mTrackingPtr.getRefData().getBaseNode(); mTrackingNode = transform; if (transform) mHeightScale = transform->getScale().z(); else mHeightScale = 1.f; } rotateCamera(getPitch(), getYaw(), false); } void Camera::applyDeferredPreviewRotationToPlayer(float dt) { if (isVanityOrPreviewModeEnabled() || mTrackingPtr.isEmpty()) return; osg::Vec3f rot = mDeferredRotation; float delta = rot.normalize(); delta = std::min(delta, (delta + 1.f) * 3 * dt); rot *= delta; mDeferredRotation -= rot; if (mDeferredRotationDisabled) { mDeferredRotationDisabled = delta > 0.0001; rotateCameraToTrackingPtr(); return; } auto& movement = mTrackingPtr.getClass().getMovementSettings(mTrackingPtr); movement.mRotation[0] += rot.x(); movement.mRotation[1] += rot.y(); movement.mRotation[2] += rot.z(); if (std::abs(mDeferredRotation.z()) > 0.0001) { float s = std::sin(mDeferredRotation.z()); float c = std::cos(mDeferredRotation.z()); float x = movement.mPosition[0]; float y = movement.mPosition[1]; movement.mPosition[0] = x * c + y * s; movement.mPosition[1] = x * -s + y * c; } } void Camera::rotateCameraToTrackingPtr() { setPitch(-mTrackingPtr.getRefData().getPosition().rot[0] - mDeferredRotation.x()); setYaw(-mTrackingPtr.getRefData().getPosition().rot[2] - mDeferredRotation.z()); } void Camera::instantTransition() { mSkipFocalPointTransition = true; mDeferredRotationDisabled = false; mDeferredRotation = osg::Vec3f(); rotateCameraToTrackingPtr(); } void Camera::calculateDeferredRotation() { MWWorld::Ptr ptr = mTrackingPtr; if (isVanityOrPreviewModeEnabled() || ptr.isEmpty()) return; if (mFirstPersonView) { instantTransition(); return; } mDeferredRotation.x() = Misc::normalizeAngle(-ptr.getRefData().getPosition().rot[0] - mPitch); mDeferredRotation.z() = Misc::normalizeAngle(-ptr.getRefData().getPosition().rot[2] - mYaw); } } ================================================ FILE: apps/openmw/mwrender/camera.hpp ================================================ #ifndef GAME_MWRENDER_CAMERA_H #define GAME_MWRENDER_CAMERA_H #include #include #include #include #include "../mwworld/ptr.hpp" namespace osg { class Camera; class NodeCallback; class Node; } namespace MWRender { class NpcAnimation; /// \brief Camera control class Camera { public: enum class Mode { Normal, Vanity, Preview, StandingPreview }; private: MWWorld::Ptr mTrackingPtr; osg::ref_ptr mTrackingNode; float mHeightScale; osg::ref_ptr 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 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(); /// Attach camera to object void attachTo(const MWWorld::Ptr &ptr) { mTrackingPtr = ptr; } MWWorld::Ptr getTrackingPtr() const { return mTrackingPtr; } void setFocalPointTransitionSpeed(float v) { mFocalPointTransitionSpeedCoef = v; } void setFocalPointTargetOffset(osg::Vec2d v); void instantTransition(); void enableDynamicCameraDistance(bool v) { mDynamicCameraDistanceEnabled = v; } void enableCrosshairInThirdPersonMode(bool v) { mShowCrosshairInThirdPersonMode = v; } /// Update the view matrix of \a cam void updateCamera(osg::Camera* cam); /// Reset to defaults void reset(); /// 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 getYaw() const { return mYaw; } void setYaw(float angle); float getPitch() const { return mPitch; } void setPitch(float 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; } /// \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; 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; bool isVanityOrPreviewModeEnabled() const { return mMode != Mode::Normal; } Mode getMode() const { return mMode; } bool isNearest() const { return mIsNearest; } }; } #endif ================================================ FILE: apps/openmw/mwrender/cell.hpp ================================================ #ifndef GAME_RENDER_CELL_H #define GAME_RENDER_CELL_H #include namespace MWRender { class CellRender { public: virtual ~CellRender() {}; /// Make the cell visible. Load the cell if necessary. virtual void show() = 0; /// Remove the cell from rendering, but don't remove it from /// memory. virtual void hide() = 0; /// Destroy all rendering objects connected with this cell. virtual void destroy() = 0; /// Make the reference with the given handle visible. virtual void enable (const std::string& handle) = 0; /// Make the reference with the given handle invisible. virtual void disable (const std::string& handle) = 0; /// Remove the reference with the given handle permanently from the scene. virtual void deleteObject (const std::string& handle) = 0; }; } #endif ================================================ FILE: apps/openmw/mwrender/characterpreview.cpp ================================================ #include "characterpreview.hpp" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "../mwbase/world.hpp" #include "../mwworld/class.hpp" #include "../mwworld/inventorystore.hpp" #include "../mwmechanics/actorutil.hpp" #include "../mwmechanics/weapontype.hpp" #include "npcanimation.hpp" #include "vismask.hpp" namespace MWRender { class DrawOnceCallback : public osg::NodeCallback { public: DrawOnceCallback () : mRendered(false) , mLastRenderedFrame(0) { } void operator () (osg::Node* node, osg::NodeVisitor* nv) override { if (!mRendered) { mRendered = true; mLastRenderedFrame = nv->getTraversalNumber(); osg::ref_ptr previousFramestamp = const_cast(nv->getFrameStamp()); osg::FrameStamp* fs = new osg::FrameStamp(*previousFramestamp); fs->setSimulationTime(0.0); nv->setFrameStamp(fs); traverse(node, nv); nv->setFrameStamp(previousFramestamp); } else { node->setNodeMask(0); } } void redrawNextFrame() { mRendered = false; } unsigned int getLastRenderedFrame() const { return mLastRenderedFrame; } private: bool mRendered; unsigned int mLastRenderedFrame; }; // Set up alpha blending mode to avoid issues caused by transparent objects writing onto the alpha value of the FBO // This makes the RTT have premultiplied alpha, though, so the source blend factor must be GL_ONE when it's applied class SetUpBlendVisitor : public osg::NodeVisitor { public: SetUpBlendVisitor(): osg::NodeVisitor(TRAVERSE_ALL_CHILDREN), mNoAlphaUniform(new osg::Uniform("noAlpha", false)) { } void apply(osg::Node& node) override { if (osg::ref_ptr stateset = node.getStateSet()) { osg::ref_ptr newStateSet; if (stateset->getAttribute(osg::StateAttribute::BLENDFUNC) || stateset->getBinNumber() == osg::StateSet::TRANSPARENT_BIN) { osg::BlendFunc* blendFunc = static_cast(stateset->getAttribute(osg::StateAttribute::BLENDFUNC)); if (blendFunc) { newStateSet = new osg::StateSet(*stateset, osg::CopyOp::SHALLOW_COPY); node.setStateSet(newStateSet); osg::ref_ptr newBlendFunc = new osg::BlendFunc(*blendFunc); newStateSet->setAttribute(newBlendFunc, osg::StateAttribute::ON); // I *think* (based on some by-hand maths) that the RGB and dest alpha factors are unchanged, and only dest determines source alpha factor // This has the benefit of being idempotent if we assume nothing used glBlendFuncSeparate before we touched it if (blendFunc->getDestination() == osg::BlendFunc::ONE_MINUS_SRC_ALPHA) newBlendFunc->setSourceAlpha(osg::BlendFunc::ONE); else if (blendFunc->getDestination() == osg::BlendFunc::ONE) newBlendFunc->setSourceAlpha(osg::BlendFunc::ZERO); // Other setups barely exist in the wild and aren't worth supporting as they're not equippable gear else Log(Debug::Info) << "Unable to adjust blend mode for character preview. Source factor 0x" << std::hex << blendFunc->getSource() << ", destination factor 0x" << blendFunc->getDestination() << std::dec; } } if (stateset->getMode(GL_BLEND) & osg::StateAttribute::ON) { if (!newStateSet) { newStateSet = new osg::StateSet(*stateset, osg::CopyOp::SHALLOW_COPY); node.setStateSet(newStateSet); } // Disable noBlendAlphaEnv newStateSet->setTextureMode(7, GL_TEXTURE_2D, osg::StateAttribute::OFF); newStateSet->addUniform(mNoAlphaUniform); } } traverse(node); } private: osg::ref_ptr mNoAlphaUniform; }; CharacterPreview::CharacterPreview(osg::Group* parent, Resource::ResourceSystem* resourceSystem, const MWWorld::Ptr& character, int sizeX, int sizeY, const osg::Vec3f& position, const osg::Vec3f& lookAt) : mParent(parent) , mResourceSystem(resourceSystem) , mPosition(position) , mLookAt(lookAt) , mCharacter(character) , mAnimation(nullptr) , mSizeX(sizeX) , mSizeY(sizeY) { mTexture = new osg::Texture2D; mTexture->setTextureSize(sizeX, sizeY); mTexture->setInternalFormat(GL_RGBA); mTexture->setFilter(osg::Texture::MIN_FILTER, osg::Texture::LINEAR); mTexture->setFilter(osg::Texture::MAG_FILTER, osg::Texture::LINEAR); mTexture->setUserValue("premultiplied alpha", true); mCamera = new osg::Camera; // hints that the camera is not relative to the master camera mCamera->setReferenceFrame(osg::Camera::ABSOLUTE_RF); 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(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")); mCamera->setName("CharacterPreview"); mCamera->setComputeNearFarMode(osg::Camera::COMPUTE_NEAR_FAR_USING_BOUNDING_VOLUMES); mCamera->setCullMask(~(Mask_UpdateVisitor)); mCamera->setNodeMask(Mask_RenderToTexture); bool ffp = mResourceSystem->getSceneManager()->getLightingMethod() == SceneUtil::LightingMethod::FFP; osg::ref_ptr lightManager = new SceneUtil::LightManager(ffp); lightManager->setStartLight(1); osg::ref_ptr stateset = lightManager->getOrCreateStateSet(); stateset->setMode(GL_LIGHTING, osg::StateAttribute::ON); stateset->setMode(GL_NORMALIZE, osg::StateAttribute::ON); stateset->setMode(GL_CULL_FACE, osg::StateAttribute::ON); osg::ref_ptr defaultMat (new osg::Material); defaultMat->setColorMode(osg::Material::OFF); defaultMat->setAmbient(osg::Material::FRONT_AND_BACK, osg::Vec4f(1,1,1,1)); 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); SceneUtil::ShadowManager::disableShadowsForStateSet(stateset); // assign large value to effectively turn off fog // shaders don't respect glDisable(GL_FOG) osg::ref_ptr fog (new osg::Fog); fog->setStart(10000000); fog->setEnd(10000000); stateset->setAttributeAndModes(fog, osg::StateAttribute::OFF|osg::StateAttribute::OVERRIDE); // Opaque stuff must have 1 as its fragment alpha as the FBO is translucent, so having blending off isn't enough osg::ref_ptr noBlendAlphaEnv = new osg::TexEnvCombine(); noBlendAlphaEnv->setCombine_Alpha(osg::TexEnvCombine::REPLACE); noBlendAlphaEnv->setSource0_Alpha(osg::TexEnvCombine::CONSTANT); noBlendAlphaEnv->setConstantColor(osg::Vec4(0.0, 0.0, 0.0, 1.0)); noBlendAlphaEnv->setCombine_RGB(osg::TexEnvCombine::REPLACE); noBlendAlphaEnv->setSource0_RGB(osg::TexEnvCombine::PREVIOUS); osg::ref_ptr dummyTexture = new osg::Texture2D(); dummyTexture->setInternalFormat(GL_DEPTH_COMPONENT); dummyTexture->setTextureSize(1, 1); // This might clash with a shadow map, so make sure it doesn't cast shadows dummyTexture->setShadowComparison(true); dummyTexture->setShadowCompareFunc(osg::Texture::ShadowCompareFunc::ALWAYS); stateset->setTextureAttributeAndModes(7, dummyTexture, osg::StateAttribute::ON); stateset->setTextureAttribute(7, noBlendAlphaEnv, osg::StateAttribute::ON); stateset->addUniform(new osg::Uniform("noAlpha", true)); osg::ref_ptr lightmodel = new osg::LightModel; lightmodel->setAmbientIntensity(osg::Vec4(0.0, 0.0, 0.0, 1.0)); stateset->setAttributeAndModes(lightmodel, osg::StateAttribute::ON); osg::ref_ptr light = new osg::Light; float diffuseR = Fallback::Map::getFloat("Inventory_DirectionalDiffuseR"); float diffuseG = Fallback::Map::getFloat("Inventory_DirectionalDiffuseG"); float diffuseB = Fallback::Map::getFloat("Inventory_DirectionalDiffuseB"); float ambientR = Fallback::Map::getFloat("Inventory_DirectionalAmbientR"); float ambientG = Fallback::Map::getFloat("Inventory_DirectionalAmbientG"); float ambientB = Fallback::Map::getFloat("Inventory_DirectionalAmbientB"); float azimuth = osg::DegreesToRadians(Fallback::Map::getFloat("Inventory_DirectionalRotationX")); float altitude = osg::DegreesToRadians(Fallback::Map::getFloat("Inventory_DirectionalRotationY")); float positionX = -std::cos(azimuth) * std::sin(altitude); float positionY = std::sin(azimuth) * std::sin(altitude); float positionZ = std::cos(altitude); light->setPosition(osg::Vec4(positionX,positionY,positionZ, 0.0)); light->setDiffuse(osg::Vec4(diffuseR,diffuseG,diffuseB,1)); osg::Vec4 ambientRGBA = osg::Vec4(ambientR,ambientG,ambientB,1); if (mResourceSystem->getSceneManager()->getForceShaders()) { // When using shaders, we now skip the ambient sun calculation as this is the only place it's used. // Using the scene ambient will give identical results. lightmodel->setAmbientIntensity(ambientRGBA); light->setAmbient(osg::Vec4(0,0,0,1)); } else light->setAmbient(ambientRGBA); light->setSpecular(osg::Vec4(0,0,0,0)); light->setLightNum(0); light->setConstantAttenuation(1.f); light->setLinearAttenuation(0.f); light->setQuadraticAttenuation(0.f); lightManager->setSunlight(light); osg::ref_ptr lightSource = new osg::LightSource; lightSource->setLight(light); lightSource->setStateSetModes(*stateset, osg::StateAttribute::ON); lightManager->addChild(lightSource); mCamera->addChild(lightManager); mNode = new osg::PositionAttitudeTransform; lightManager->addChild(mNode); mDrawOnceCallback = new DrawOnceCallback; mCamera->addUpdateCallback(mDrawOnceCallback); mParent->addChild(mCamera); mCharacter.mCell = nullptr; } CharacterPreview::~CharacterPreview () { mCamera->removeChildren(0, mCamera->getNumChildren()); mParent->removeChild(mCamera); } int CharacterPreview::getTextureWidth() const { return mSizeX; } int CharacterPreview::getTextureHeight() const { return mSizeY; } void CharacterPreview::setBlendMode() { mResourceSystem->getSceneManager()->recreateShaders(mNode, "objects", true); SetUpBlendVisitor visitor; mNode->accept(visitor); } void CharacterPreview::onSetup() { setBlendMode(); } osg::ref_ptr CharacterPreview::getTexture() { return mTexture; } void CharacterPreview::rebuild() { mAnimation = nullptr; mAnimation = new NpcAnimation(mCharacter, mNode, mResourceSystem, true, (renderHeadOnly() ? NpcAnimation::VM_HeadOnly : NpcAnimation::VM_Normal)); onSetup(); redraw(); } void CharacterPreview::redraw() { mCamera->setNodeMask(Mask_RenderToTexture); mDrawOnceCallback->redrawNextFrame(); } // -------------------------------------------------------------------------------------------------- InventoryPreview::InventoryPreview(osg::Group* parent, Resource::ResourceSystem* resourceSystem, const MWWorld::Ptr& character) : CharacterPreview(parent, resourceSystem, character, 512, 1024, osg::Vec3f(0, 700, 71), osg::Vec3f(0,0,71)) { } void InventoryPreview::setViewport(int sizeX, int sizeY) { sizeX = std::max(sizeX, 0); sizeY = std::max(sizeY, 0); // NB Camera::setViewport has threading issues osg::ref_ptr stateset = new osg::StateSet; mViewport = new osg::Viewport(0, mSizeY-sizeY, std::min(mSizeX, sizeX), std::min(mSizeY, sizeY)); stateset->setAttributeAndModes(mViewport); mCamera->setStateSet(stateset); redraw(); } void InventoryPreview::update() { if (!mAnimation.get()) return; mAnimation->showWeapons(true); mAnimation->updateParts(); MWWorld::InventoryStore &inv = mCharacter.getClass().getInventoryStore(mCharacter); MWWorld::ContainerStoreIterator iter = inv.getSlot(MWWorld::InventoryStore::Slot_CarriedRight); std::string groupname = "inventoryhandtohand"; bool showCarriedLeft = true; if(iter != inv.end()) { groupname = "inventoryweapononehand"; if(iter->getTypeName() == typeid(ESM::Weapon).name()) { MWWorld::LiveCellRef *ref = iter->get(); int type = ref->mBase->mData.mType; const ESM::WeaponType* weaponInfo = MWMechanics::getWeaponType(type); showCarriedLeft = !(weaponInfo->mFlags & ESM::WeaponType::TwoHanded); std::string inventoryGroup = weaponInfo->mLongGroup; inventoryGroup = "inventory" + inventoryGroup; // We still should use one-handed animation as fallback if (mAnimation->hasAnimation(inventoryGroup)) groupname = inventoryGroup; else { static const std::string oneHandFallback = "inventory" + MWMechanics::getWeaponType(ESM::Weapon::LongBladeOneHand)->mLongGroup; static const std::string twoHandFallback = "inventory" + MWMechanics::getWeaponType(ESM::Weapon::LongBladeTwoHand)->mLongGroup; // For real two-handed melee weapons use 2h swords animations as fallback, otherwise use the 1h ones if (weaponInfo->mFlags & ESM::WeaponType::TwoHanded && weaponInfo->mWeaponClass == ESM::WeaponType::Melee) groupname = twoHandFallback; else groupname = oneHandFallback; } } } mAnimation->showCarriedLeft(showCarriedLeft); mCurrentAnimGroup = groupname; mAnimation->play(mCurrentAnimGroup, 1, Animation::BlendMask_All, false, 1.0f, "start", "stop", 0.0f, 0); MWWorld::ConstContainerStoreIterator torch = inv.getSlot(MWWorld::InventoryStore::Slot_CarriedLeft); if(torch != inv.end() && torch->getTypeName() == typeid(ESM::Light).name() && showCarriedLeft) { if(!mAnimation->getInfo("torch")) mAnimation->play("torch", 2, Animation::BlendMask_LeftArm, false, 1.0f, "start", "stop", 0.0f, ~0ul, true); } else if(mAnimation->getInfo("torch")) mAnimation->disable("torch"); mAnimation->runAnimation(0.0f); setBlendMode(); redraw(); } int InventoryPreview::getSlotSelected (int posX, int posY) { if (!mViewport) return -1; float projX = (posX / mViewport->width()) * 2 - 1.f; float projY = (posY / mViewport->height()) * 2 - 1.f; // With Intersector::WINDOW, the intersection ratios are slightly inaccurate. Seems to be a // precision issue - compiling with OSG_USE_FLOAT_MATRIX=0, Intersector::WINDOW works ok. // Using Intersector::PROJECTION results in better precision because the start/end points and the model matrices // don't go through as many transformations. osg::ref_ptr intersector (new osgUtil::LineSegmentIntersector(osgUtil::Intersector::PROJECTION, projX, projY)); intersector->setIntersectionLimit(osgUtil::LineSegmentIntersector::LIMIT_NEAREST); osgUtil::IntersectionVisitor visitor(intersector); visitor.setTraversalMode(osg::NodeVisitor::TRAVERSE_ACTIVE_CHILDREN); // Set the traversal number from the last draw, so that the frame switch used for RigGeometry double buffering works correctly visitor.setTraversalNumber(mDrawOnceCallback->getLastRenderedFrame()); osg::Node::NodeMask nodeMask = mCamera->getNodeMask(); mCamera->setNodeMask(~0u); mCamera->accept(visitor); mCamera->setNodeMask(nodeMask); if (intersector->containsIntersections()) { osgUtil::LineSegmentIntersector::Intersection intersection = intersector->getFirstIntersection(); return mAnimation->getSlot(intersection.nodePath); } return -1; } void InventoryPreview::updatePtr(const MWWorld::Ptr &ptr) { mCharacter = MWWorld::Ptr(ptr.getBase(), nullptr); } void InventoryPreview::onSetup() { CharacterPreview::onSetup(); osg::Vec3f scale (1.f, 1.f, 1.f); mCharacter.getClass().adjustScale(mCharacter, scale, true); mNode->setScale(scale); mCamera->setViewMatrixAsLookAt(mPosition * scale.z(), mLookAt * scale.z(), osg::Vec3f(0,0,1)); } // -------------------------------------------------------------------------------------------------- RaceSelectionPreview::RaceSelectionPreview(osg::Group* parent, Resource::ResourceSystem* resourceSystem) : CharacterPreview(parent, resourceSystem, MWMechanics::getPlayer(), 512, 512, osg::Vec3f(0, 125, 8), osg::Vec3f(0,0,8)) , mBase (*mCharacter.get()->mBase) , mRef(&mBase) , mPitchRadians(osg::DegreesToRadians(6.f)) { mCharacter = MWWorld::Ptr(&mRef, nullptr); } RaceSelectionPreview::~RaceSelectionPreview() { } void RaceSelectionPreview::setAngle(float angleRadians) { mNode->setAttitude(osg::Quat(mPitchRadians, osg::Vec3(1,0,0)) * osg::Quat(angleRadians, osg::Vec3(0,0,1))); redraw(); } void RaceSelectionPreview::setPrototype(const ESM::NPC &proto) { mBase = proto; mBase.mId = "player"; rebuild(); } class UpdateCameraCallback : public osg::NodeCallback { public: UpdateCameraCallback(osg::ref_ptr nodeToFollow, const osg::Vec3& posOffset, const osg::Vec3& lookAtOffset) : mNodeToFollow(nodeToFollow) , mPosOffset(posOffset) , mLookAtOffset(lookAtOffset) { } void operator()(osg::Node* node, osg::NodeVisitor* nv) override { osg::Camera* cam = static_cast(node); // Update keyframe controllers in the scene graph first... traverse(node, nv); // Now update camera utilizing the updated head position osg::NodePathList nodepaths = mNodeToFollow->getParentalNodePaths(); if (nodepaths.empty()) return; osg::Matrix worldMat = osg::computeLocalToWorld(nodepaths[0]); osg::Vec3 headOffset = worldMat.getTrans(); cam->setViewMatrixAsLookAt(headOffset + mPosOffset, headOffset + mLookAtOffset, osg::Vec3(0,0,1)); } private: osg::ref_ptr mNodeToFollow; osg::Vec3 mPosOffset; osg::Vec3 mLookAtOffset; }; void RaceSelectionPreview::onSetup () { CharacterPreview::onSetup(); mAnimation->play("idle", 1, Animation::BlendMask_All, false, 1.0f, "start", "stop", 0.0f, 0); mAnimation->runAnimation(0.f); // attach camera to follow the head node if (mUpdateCameraCallback) mCamera->removeUpdateCallback(mUpdateCameraCallback); const osg::Node* head = mAnimation->getNode("Bip01 Head"); if (head) { mUpdateCameraCallback = new UpdateCameraCallback(head, mPosition, mLookAt); mCamera->addUpdateCallback(mUpdateCameraCallback); } else Log(Debug::Error) << "Error: Bip01 Head node not found"; } } ================================================ FILE: apps/openmw/mwrender/characterpreview.hpp ================================================ #ifndef MWRENDER_CHARACTERPREVIEW_H #define MWRENDER_CHARACTERPREVIEW_H #include #include #include #include #include #include "../mwworld/ptr.hpp" namespace osg { class Texture2D; class Camera; class Group; class Viewport; } namespace MWRender { class NpcAnimation; class DrawOnceCallback; class CharacterPreview { public: CharacterPreview(osg::Group* parent, Resource::ResourceSystem* resourceSystem, const MWWorld::Ptr& character, int sizeX, int sizeY, const osg::Vec3f& position, const osg::Vec3f& lookAt); virtual ~CharacterPreview(); int getTextureWidth() const; int getTextureHeight() const; void redraw(); void rebuild(); osg::ref_ptr getTexture(); private: CharacterPreview(const CharacterPreview&); CharacterPreview& operator=(const CharacterPreview&); protected: virtual bool renderHeadOnly() { return false; } void setBlendMode(); virtual void onSetup(); osg::ref_ptr mParent; Resource::ResourceSystem* mResourceSystem; osg::ref_ptr mTexture; osg::ref_ptr mCamera; osg::ref_ptr mDrawOnceCallback; osg::Vec3f mPosition; osg::Vec3f mLookAt; MWWorld::Ptr mCharacter; osg::ref_ptr mAnimation; osg::ref_ptr mNode; std::string mCurrentAnimGroup; int mSizeX; int mSizeY; }; class InventoryPreview : public CharacterPreview { public: InventoryPreview(osg::Group* parent, Resource::ResourceSystem* resourceSystem, const MWWorld::Ptr& character); void updatePtr(const MWWorld::Ptr& ptr); void update(); // Render preview again, e.g. after changed equipment void setViewport(int sizeX, int sizeY); int getSlotSelected(int posX, int posY); protected: osg::ref_ptr mViewport; void onSetup() override; }; class UpdateCameraCallback; class RaceSelectionPreview : public CharacterPreview { ESM::NPC mBase; MWWorld::LiveCellRef mRef; protected: bool renderHeadOnly() override { return true; } void onSetup() override; public: RaceSelectionPreview(osg::Group* parent, Resource::ResourceSystem* resourceSystem); virtual ~RaceSelectionPreview(); void setAngle(float angleRadians); const ESM::NPC &getPrototype() const { return mBase; } void setPrototype(const ESM::NPC &proto); private: osg::ref_ptr mUpdateCameraCallback; float mPitchRadians; }; } #endif ================================================ FILE: apps/openmw/mwrender/creatureanimation.cpp ================================================ #include "creatureanimation.hpp" #include #include #include #include #include #include #include #include #include #include #include #include "../mwbase/environment.hpp" #include "../mwbase/world.hpp" #include "../mwmechanics/weapontype.hpp" #include "../mwworld/class.hpp" #include "../mwworld/esmstore.hpp" namespace MWRender { CreatureAnimation::CreatureAnimation(const MWWorld::Ptr &ptr, const std::string& model, Resource::ResourceSystem* resourceSystem) : ActorAnimation(ptr, osg::ref_ptr(ptr.getRefData().getBaseNode()), resourceSystem) { MWWorld::LiveCellRef *ref = mPtr.get(); if(!model.empty()) { setObjectRoot(model, false, false, true); if((ref->mBase->mFlags&ESM::Creature::Bipedal)) addAnimSource(Settings::Manager::getString("xbaseanim", "Models"), model); addAnimSource(model, model); } } CreatureWeaponAnimation::CreatureWeaponAnimation(const MWWorld::Ptr &ptr, const std::string& model, Resource::ResourceSystem* resourceSystem) : ActorAnimation(ptr, osg::ref_ptr(ptr.getRefData().getBaseNode()), resourceSystem) , mShowWeapons(false) , mShowCarriedLeft(false) { MWWorld::LiveCellRef *ref = mPtr.get(); if(!model.empty()) { setObjectRoot(model, true, false, true); if((ref->mBase->mFlags&ESM::Creature::Bipedal)) { addAnimSource(Settings::Manager::getString("xbaseanim", "Models"), model); } addAnimSource(model, model); mPtr.getClass().getInventoryStore(mPtr).setInvListener(this, mPtr); updateParts(); } mWeaponAnimationTime = std::shared_ptr(new WeaponAnimationTime(this)); } void CreatureWeaponAnimation::showWeapons(bool showWeapon) { if (showWeapon != mShowWeapons) { mShowWeapons = showWeapon; updateParts(); } } void CreatureWeaponAnimation::showCarriedLeft(bool show) { if (show != mShowCarriedLeft) { mShowCarriedLeft = show; updateParts(); } } void CreatureWeaponAnimation::updateParts() { mAmmunition.reset(); mWeapon.reset(); mShield.reset(); updateHolsteredWeapon(!mShowWeapons); updateQuiver(); updateHolsteredShield(mShowCarriedLeft); if (mShowWeapons) updatePart(mWeapon, MWWorld::InventoryStore::Slot_CarriedRight); if (mShowCarriedLeft) updatePart(mShield, MWWorld::InventoryStore::Slot_CarriedLeft); } void CreatureWeaponAnimation::updatePart(PartHolderPtr& scene, int slot) { if (!mObjectRoot) return; const MWWorld::InventoryStore& inv = mPtr.getClass().getInventoryStore(mPtr); MWWorld::ConstContainerStoreIterator it = inv.getSlot(slot); if (it == inv.end()) { scene.reset(); return; } MWWorld::ConstPtr item = *it; std::string bonename; std::string itemModel = item.getClass().getModel(item); if (slot == MWWorld::InventoryStore::Slot_CarriedRight) { if(item.getTypeName() == typeid(ESM::Weapon).name()) { int type = item.get()->mBase->mData.mType; bonename = MWMechanics::getWeaponType(type)->mAttachBone; if (bonename != "Weapon Bone") { const NodeMap& nodeMap = getNodeMap(); NodeMap::const_iterator found = nodeMap.find(Misc::StringUtils::lowerCase(bonename)); if (found == nodeMap.end()) bonename = "Weapon Bone"; } } else bonename = "Weapon Bone"; } else { bonename = "Shield Bone"; if (item.getTypeName() == typeid(ESM::Armor).name()) { // Shield body part model should be used if possible. const MWWorld::ESMStore &store = MWBase::Environment::get().getWorld()->getStore(); for (const auto& part : item.get()->mBase->mParts.mParts) { // Assume all creatures use the male mesh. if (part.mPart != ESM::PRT_Shield || part.mMale.empty()) continue; const ESM::BodyPart *bodypart = store.get().search(part.mMale); if (bodypart && bodypart->mData.mType == ESM::BodyPart::MT_Armor && !bodypart->mModel.empty()) { itemModel = "meshes\\" + bodypart->mModel; break; } } } } try { osg::ref_ptr node = mResourceSystem->getSceneManager()->getInstance(itemModel); const NodeMap& nodeMap = getNodeMap(); NodeMap::const_iterator found = nodeMap.find(Misc::StringUtils::lowerCase(bonename)); if (found == nodeMap.end()) throw std::runtime_error("Can't find attachment node " + bonename); osg::ref_ptr attached = SceneUtil::attach(node, mObjectRoot, bonename, found->second.get()); scene.reset(new PartHolder(attached)); if (!item.getClass().getEnchantment(item).empty()) mGlowUpdater = SceneUtil::addEnchantedGlow(attached, mResourceSystem, item.getClass().getEnchantmentColor(item)); // Crossbows start out with a bolt attached // FIXME: code duplicated from NpcAnimation if (slot == MWWorld::InventoryStore::Slot_CarriedRight && item.getTypeName() == typeid(ESM::Weapon).name() && item.get()->mBase->mData.mType == ESM::Weapon::MarksmanCrossbow) { const ESM::WeaponType* weaponInfo = MWMechanics::getWeaponType(ESM::Weapon::MarksmanCrossbow); MWWorld::ConstContainerStoreIterator ammo = inv.getSlot(MWWorld::InventoryStore::Slot_Ammunition); if (ammo != inv.end() && ammo->get()->mBase->mData.mType == weaponInfo->mAmmoType) attachArrow(); else mAmmunition.reset(); } else mAmmunition.reset(); std::shared_ptr source; if (slot == MWWorld::InventoryStore::Slot_CarriedRight) source = mWeaponAnimationTime; else source.reset(new NullAnimationTime); SceneUtil::AssignControllerSourcesVisitor assignVisitor(source); attached->accept(assignVisitor); } catch (std::exception& e) { Log(Debug::Error) << "Can not add creature part: " << e.what(); } } bool CreatureWeaponAnimation::isArrowAttached() const { return mAmmunition != nullptr; } void CreatureWeaponAnimation::detachArrow() { WeaponAnimation::detachArrow(mPtr); updateQuiver(); } void CreatureWeaponAnimation::attachArrow() { WeaponAnimation::attachArrow(mPtr); const MWWorld::InventoryStore& inv = mPtr.getClass().getInventoryStore(mPtr); MWWorld::ConstContainerStoreIterator ammo = inv.getSlot(MWWorld::InventoryStore::Slot_Ammunition); if (ammo != inv.end() && !ammo->getClass().getEnchantment(*ammo).empty()) { osg::Group* bone = getArrowBone(); if (bone != nullptr && bone->getNumChildren()) SceneUtil::addEnchantedGlow(bone->getChild(0), mResourceSystem, ammo->getClass().getEnchantmentColor(*ammo)); } updateQuiver(); } void CreatureWeaponAnimation::releaseArrow(float attackStrength) { WeaponAnimation::releaseArrow(mPtr, attackStrength); updateQuiver(); } osg::Group *CreatureWeaponAnimation::getArrowBone() { if (!mWeapon) return nullptr; if (!mPtr.getClass().hasInventoryStore(mPtr)) return nullptr; const MWWorld::InventoryStore& inv = mPtr.getClass().getInventoryStore(mPtr); MWWorld::ConstContainerStoreIterator weapon = inv.getSlot(MWWorld::InventoryStore::Slot_CarriedRight); if(weapon == inv.end() || weapon->getTypeName() != typeid(ESM::Weapon).name()) return nullptr; int type = weapon->get()->mBase->mData.mType; int ammoType = MWMechanics::getWeaponType(type)->mAmmoType; // Try to find and attachment bone in actor's skeleton, otherwise fall back to the ArrowBone in weapon's mesh osg::Group* bone = getBoneByName(MWMechanics::getWeaponType(ammoType)->mAttachBone); if (bone == nullptr) { SceneUtil::FindByNameVisitor findVisitor ("ArrowBone"); mWeapon->getNode()->accept(findVisitor); bone = findVisitor.mFoundNode; } return bone; } osg::Node *CreatureWeaponAnimation::getWeaponNode() { return mWeapon ? mWeapon->getNode().get() : nullptr; } Resource::ResourceSystem *CreatureWeaponAnimation::getResourceSystem() { return mResourceSystem; } void CreatureWeaponAnimation::addControllers() { Animation::addControllers(); WeaponAnimation::addControllers(mNodeMap, mActiveControllers, mObjectRoot.get()); } osg::Vec3f CreatureWeaponAnimation::runAnimation(float duration) { osg::Vec3f ret = Animation::runAnimation(duration); WeaponAnimation::configureControllers(mPtr.getRefData().getPosition().rot[0] + getBodyPitchRadians()); return ret; } } ================================================ FILE: apps/openmw/mwrender/creatureanimation.hpp ================================================ #ifndef GAME_RENDER_CREATUREANIMATION_H #define GAME_RENDER_CREATUREANIMATION_H #include "actoranimation.hpp" #include "weaponanimation.hpp" #include "../mwworld/inventorystore.hpp" namespace MWWorld { class Ptr; } namespace MWRender { class CreatureAnimation : public ActorAnimation { public: CreatureAnimation(const MWWorld::Ptr &ptr, const std::string& model, Resource::ResourceSystem* resourceSystem); virtual ~CreatureAnimation() {} }; // For creatures with weapons and shields // Animation is already virtual anyway, so might as well make a separate class. // Most creatures don't need weapons/shields, so this will save some memory. class CreatureWeaponAnimation : public ActorAnimation, public WeaponAnimation, public MWWorld::InventoryStoreListener { public: CreatureWeaponAnimation(const MWWorld::Ptr &ptr, const std::string& model, Resource::ResourceSystem* resourceSystem); virtual ~CreatureWeaponAnimation() {} void equipmentChanged() override { updateParts(); } void showWeapons(bool showWeapon) override; bool getCarriedLeftShown() const override { return mShowCarriedLeft; } void showCarriedLeft(bool show) override; void updateParts(); void updatePart(PartHolderPtr& scene, int slot); void attachArrow() override; void detachArrow() override; void releaseArrow(float attackStrength) override; // WeaponAnimation osg::Group* getArrowBone() override; osg::Node* getWeaponNode() override; Resource::ResourceSystem* getResourceSystem() override; void showWeapon(bool show) override { showWeapons(show); } void setWeaponGroup(const std::string& group, bool relativeDuration) override { mWeaponAnimationTime->setGroup(group, relativeDuration); } void addControllers() override; osg::Vec3f runAnimation(float duration) override; /// A relative factor (0-1) that decides if and how much the skeleton should be pitched /// to indicate the facing orientation of the character. void setPitchFactor(float factor) override { mPitchFactor = factor; } protected: bool isArrowAttached() const override; private: PartHolderPtr mWeapon; PartHolderPtr mShield; bool mShowWeapons; bool mShowCarriedLeft; std::shared_ptr mWeaponAnimationTime; }; } #endif ================================================ FILE: apps/openmw/mwrender/effectmanager.cpp ================================================ #include "effectmanager.hpp" #include #include #include #include #include "animation.hpp" #include "vismask.hpp" #include "util.hpp" namespace MWRender { EffectManager::EffectManager(osg::ref_ptr parent, Resource::ResourceSystem* resourceSystem) : mParentNode(parent) , mResourceSystem(resourceSystem) { } EffectManager::~EffectManager() { clear(); } void EffectManager::addEffect(const std::string &model, const std::string& textureOverride, const osg::Vec3f &worldPosition, float scale, bool isMagicVFX) { osg::ref_ptr node = mResourceSystem->getSceneManager()->getInstance(model); node->setNodeMask(Mask_Effect); Effect effect; effect.mAnimTime.reset(new EffectAnimationTime); SceneUtil::FindMaxControllerLengthVisitor findMaxLengthVisitor; node->accept(findMaxLengthVisitor); effect.mMaxControllerLength = findMaxLengthVisitor.getMaxLength(); osg::ref_ptr trans = new osg::PositionAttitudeTransform; trans->setPosition(worldPosition); trans->setScale(osg::Vec3f(scale, scale, scale)); trans->addChild(node); SceneUtil::AssignControllerSourcesVisitor assignVisitor(effect.mAnimTime); node->accept(assignVisitor); if (isMagicVFX) overrideFirstRootTexture(textureOverride, mResourceSystem, node); else overrideTexture(textureOverride, mResourceSystem, node); mParentNode->addChild(trans); mEffects[trans] = effect; } void EffectManager::update(float dt) { for (EffectMap::iterator it = mEffects.begin(); it != mEffects.end(); ) { it->second.mAnimTime->addTime(dt); if (it->second.mAnimTime->getTime() >= it->second.mMaxControllerLength) { mParentNode->removeChild(it->first); mEffects.erase(it++); } else ++it; } } void EffectManager::clear() { for (EffectMap::iterator it = mEffects.begin(); it != mEffects.end(); ++it) { mParentNode->removeChild(it->first); } mEffects.clear(); } } ================================================ FILE: apps/openmw/mwrender/effectmanager.hpp ================================================ #ifndef OPENMW_MWRENDER_EFFECTMANAGER_H #define OPENMW_MWRENDER_EFFECTMANAGER_H #include #include #include #include namespace osg { class Group; class Vec3f; class PositionAttitudeTransform; } namespace Resource { class ResourceSystem; } namespace MWRender { class EffectAnimationTime; // Note: effects attached to another object should be managed by MWRender::Animation::addEffect. // This class manages "free" effects, i.e. attached to a dedicated scene node in the world. class EffectManager { public: EffectManager(osg::ref_ptr parent, Resource::ResourceSystem* resourceSystem); ~EffectManager(); /// Add an effect. When it's finished playing, it will be removed automatically. void addEffect (const std::string& model, const std::string& textureOverride, const osg::Vec3f& worldPosition, float scale, bool isMagicVFX = true); void update(float dt); /// Remove all effects void clear(); private: struct Effect { float mMaxControllerLength; std::shared_ptr mAnimTime; }; typedef std::map, Effect> EffectMap; EffectMap mEffects; osg::ref_ptr mParentNode; Resource::ResourceSystem* mResourceSystem; EffectManager(const EffectManager&); void operator=(const EffectManager&); }; } #endif ================================================ FILE: apps/openmw/mwrender/fogmanager.cpp ================================================ #include "fogmanager.hpp" #include #include #include #include #include namespace { float DLLandFogStart; float DLLandFogEnd; float DLUnderwaterFogStart; float DLUnderwaterFogEnd; float DLInteriorFogStart; float DLInteriorFogEnd; } namespace MWRender { FogManager::FogManager() : mLandFogStart(0.f) , mLandFogEnd(std::numeric_limits::max()) , mUnderwaterFogStart(0.f) , mUnderwaterFogEnd(std::numeric_limits::max()) , mFogColor(osg::Vec4f()) , mDistantFog(Settings::Manager::getBool("use distant fog", "Fog")) , mUnderwaterColor(Fallback::Map::getColour("Water_UnderwaterColor")) , mUnderwaterWeight(Fallback::Map::getFloat("Water_UnderwaterColorWeight")) , mUnderwaterIndoorFog(Fallback::Map::getFloat("Water_UnderwaterIndoorFog")) { DLLandFogStart = Settings::Manager::getFloat("distant land fog start", "Fog"); DLLandFogEnd = Settings::Manager::getFloat("distant land fog end", "Fog"); DLUnderwaterFogStart = Settings::Manager::getFloat("distant underwater fog start", "Fog"); DLUnderwaterFogEnd = Settings::Manager::getFloat("distant underwater fog end", "Fog"); DLInteriorFogStart = Settings::Manager::getFloat("distant interior fog start", "Fog"); DLInteriorFogEnd = Settings::Manager::getFloat("distant interior fog end", "Fog"); } void FogManager::configure(float viewDistance, const ESM::Cell *cell) { osg::Vec4f color = SceneUtil::colourFromRGB(cell->mAmbi.mFog); if (mDistantFog) { float density = std::max(0.2f, cell->mAmbi.mFogDensity); mLandFogStart = DLInteriorFogEnd * (1.0f - density) + DLInteriorFogStart*density; mLandFogEnd = DLInteriorFogEnd; mUnderwaterFogStart = DLUnderwaterFogStart; mUnderwaterFogEnd = DLUnderwaterFogEnd; mFogColor = color; } else configure(viewDistance, cell->mAmbi.mFogDensity, mUnderwaterIndoorFog, 1.0f, 0.0f, color); } void FogManager::configure(float viewDistance, float fogDepth, float underwaterFog, float dlFactor, float dlOffset, const osg::Vec4f &color) { if (mDistantFog) { mLandFogStart = dlFactor * (DLLandFogStart - dlOffset * DLLandFogEnd); mLandFogEnd = dlFactor * (1.0f - dlOffset) * DLLandFogEnd; mUnderwaterFogStart = DLUnderwaterFogStart; mUnderwaterFogEnd = DLUnderwaterFogEnd; } else { if (fogDepth == 0.0) { mLandFogStart = 0.0f; mLandFogEnd = std::numeric_limits::max(); } else { mLandFogStart = viewDistance * (1 - fogDepth); mLandFogEnd = viewDistance; } mUnderwaterFogStart = std::min(viewDistance, 7168.f) * (1 - underwaterFog); mUnderwaterFogEnd = std::min(viewDistance, 7168.f); } mFogColor = color; } float FogManager::getFogStart(bool isUnderwater) const { return isUnderwater ? mUnderwaterFogStart : mLandFogStart; } float FogManager::getFogEnd(bool isUnderwater) const { return isUnderwater ? mUnderwaterFogEnd : mLandFogEnd; } osg::Vec4f FogManager::getFogColor(bool isUnderwater) const { if (isUnderwater) { return mUnderwaterColor * mUnderwaterWeight + mFogColor * (1.f-mUnderwaterWeight); } return mFogColor; } } ================================================ FILE: apps/openmw/mwrender/fogmanager.hpp ================================================ #ifndef OPENMW_MWRENDER_FOGMANAGER_H #define OPENMW_MWRENDER_FOGMANAGER_H #include namespace ESM { struct Cell; } namespace MWRender { class FogManager { public: FogManager(); void configure(float viewDistance, const ESM::Cell *cell); void configure(float viewDistance, float fogDepth, float underwaterFog, float dlFactor, float dlOffset, const osg::Vec4f &color); osg::Vec4f getFogColor(bool isUnderwater) const; float getFogStart(bool isUnderwater) const; float getFogEnd(bool isUnderwater) const; private: float mLandFogStart; float mLandFogEnd; float mUnderwaterFogStart; float mUnderwaterFogEnd; osg::Vec4f mFogColor; bool mDistantFog; osg::Vec4f mUnderwaterColor; float mUnderwaterWeight; float mUnderwaterIndoorFog; }; } #endif ================================================ FILE: apps/openmw/mwrender/globalmap.cpp ================================================ #include "globalmap.hpp" #include #include #include #include #include #include #include #include #include #include #include #include /* Start of tes3mp addition Include additional headers for multiplayer purposes */ #include #include "../mwmp/Main.hpp" #include "../mwmp/Networking.hpp" #include "../mwmp/Worldstate.hpp" /* End of tes3mp addition */ #include "../mwbase/environment.hpp" #include "../mwbase/world.hpp" #include "../mwworld/esmstore.hpp" #include "vismask.hpp" namespace { // Create a screen-aligned quad with given texture coordinates. // Assumes a top-left origin of the sampled image. osg::ref_ptr createTexturedQuad(float leftTexCoord, float topTexCoord, float rightTexCoord, float bottomTexCoord) { osg::ref_ptr geom = new osg::Geometry; osg::ref_ptr verts = new osg::Vec3Array; verts->push_back(osg::Vec3f(-1, -1, 0)); verts->push_back(osg::Vec3f(-1, 1, 0)); verts->push_back(osg::Vec3f(1, 1, 0)); verts->push_back(osg::Vec3f(1, -1, 0)); geom->setVertexArray(verts); osg::ref_ptr texcoords = new osg::Vec2Array; texcoords->push_back(osg::Vec2f(leftTexCoord, 1.f-bottomTexCoord)); texcoords->push_back(osg::Vec2f(leftTexCoord, 1.f-topTexCoord)); texcoords->push_back(osg::Vec2f(rightTexCoord, 1.f-topTexCoord)); texcoords->push_back(osg::Vec2f(rightTexCoord, 1.f-bottomTexCoord)); osg::ref_ptr colors = new osg::Vec4Array; colors->push_back(osg::Vec4(1.f, 1.f, 1.f, 1.f)); geom->setColorArray(colors, osg::Array::BIND_OVERALL); geom->setTexCoordArray(0, texcoords, osg::Array::BIND_PER_VERTEX); geom->addPrimitiveSet(new osg::DrawArrays(osg::PrimitiveSet::QUADS,0,4)); return geom; } class CameraUpdateGlobalCallback : public osg::NodeCallback { public: CameraUpdateGlobalCallback(MWRender::GlobalMap* parent) : mRendered(false) , mParent(parent) { } void operator()(osg::Node* node, osg::NodeVisitor* nv) override { if (mRendered) { if (mParent->copyResult(static_cast(node), nv->getTraversalNumber())) { node->setNodeMask(0); mParent->markForRemoval(static_cast(node)); } return; } traverse(node, nv); mRendered = true; } private: bool mRendered; MWRender::GlobalMap* mParent; }; } namespace MWRender { /* Start of tes3mp addition Use maps to track which global map coordinates belong to which cell coordinates without having to significantly change other methods */ std::map originToCellX; std::map originToCellY; /* End of tes3mp addition */ class CreateMapWorkItem : public SceneUtil::WorkItem { public: CreateMapWorkItem(int width, int height, int minX, int minY, int maxX, int maxY, int cellSize, const MWWorld::Store& landStore) : mWidth(width), mHeight(height), mMinX(minX), mMinY(minY), mMaxX(maxX), mMaxY(maxY), mCellSize(cellSize), mLandStore(landStore) { } void doWork() override { osg::ref_ptr image = new osg::Image; image->allocateImage(mWidth, mHeight, 1, GL_RGB, GL_UNSIGNED_BYTE); unsigned char* data = image->data(); osg::ref_ptr alphaImage = new osg::Image; alphaImage->allocateImage(mWidth, mHeight, 1, GL_ALPHA, GL_UNSIGNED_BYTE); unsigned char* alphaData = alphaImage->data(); for (int x = mMinX; x <= mMaxX; ++x) { for (int y = mMinY; y <= mMaxY; ++y) { const ESM::Land* land = mLandStore.search (x,y); for (int cellY=0; cellY(float(cellX) / float(mCellSize) * 9); int vertexY = static_cast(float(cellY) / float(mCellSize) * 9); int texelX = (x-mMinX) * mCellSize + cellX; int texelY = (y-mMinY) * mCellSize + cellY; unsigned char r,g,b; float y2 = 0; if (land && (land->mDataTypes & ESM::Land::DATA_WNAM)) y2 = land->mWnam[vertexY * 9 + vertexX] / 128.f; else y2 = SCHAR_MIN / 128.f; if (y2 < 0) { r = static_cast(14 * y2 + 38); g = static_cast(20 * y2 + 56); b = static_cast(18 * y2 + 51); } else if (y2 < 0.3f) { if (y2 < 0.1f) y2 *= 8.f; else { y2 -= 0.1f; y2 += 0.8f; } r = static_cast(66 - 32 * y2); g = static_cast(48 - 23 * y2); b = static_cast(33 - 16 * y2); } else { y2 -= 0.3f; y2 *= 1.428f; r = static_cast(34 - 29 * y2); g = static_cast(25 - 20 * y2); b = static_cast(17 - 12 * y2); } data[texelY * mWidth * 3 + texelX * 3] = r; data[texelY * mWidth * 3 + texelX * 3+1] = g; data[texelY * mWidth * 3 + texelX * 3+2] = b; alphaData[texelY * mWidth+ texelX] = (y2 < 0) ? static_cast(0) : static_cast(255); } } } } mBaseTexture = new osg::Texture2D; mBaseTexture->setWrap(osg::Texture::WRAP_S, osg::Texture::CLAMP_TO_EDGE); mBaseTexture->setWrap(osg::Texture::WRAP_T, osg::Texture::CLAMP_TO_EDGE); mBaseTexture->setFilter(osg::Texture::MIN_FILTER, osg::Texture::LINEAR); mBaseTexture->setFilter(osg::Texture::MAG_FILTER, osg::Texture::LINEAR); mBaseTexture->setImage(image); mBaseTexture->setResizeNonPowerOfTwoHint(false); mAlphaTexture = new osg::Texture2D; mAlphaTexture->setWrap(osg::Texture::WRAP_S, osg::Texture::CLAMP_TO_EDGE); mAlphaTexture->setWrap(osg::Texture::WRAP_T, osg::Texture::CLAMP_TO_EDGE); mAlphaTexture->setFilter(osg::Texture::MIN_FILTER, osg::Texture::LINEAR); mAlphaTexture->setFilter(osg::Texture::MAG_FILTER, osg::Texture::LINEAR); mAlphaTexture->setImage(alphaImage); mAlphaTexture->setResizeNonPowerOfTwoHint(false); mOverlayImage = new osg::Image; mOverlayImage->allocateImage(mWidth, mHeight, 1, GL_RGBA, GL_UNSIGNED_BYTE); assert(mOverlayImage->isDataContiguous()); memset(mOverlayImage->data(), 0, mOverlayImage->getTotalSizeInBytes()); mOverlayTexture = new osg::Texture2D; mOverlayTexture->setWrap(osg::Texture::WRAP_S, osg::Texture::CLAMP_TO_EDGE); mOverlayTexture->setWrap(osg::Texture::WRAP_T, osg::Texture::CLAMP_TO_EDGE); mOverlayTexture->setFilter(osg::Texture::MIN_FILTER, osg::Texture::LINEAR); mOverlayTexture->setFilter(osg::Texture::MAG_FILTER, osg::Texture::LINEAR); mOverlayTexture->setResizeNonPowerOfTwoHint(false); mOverlayTexture->setInternalFormat(GL_RGBA); mOverlayTexture->setTextureSize(mWidth, mHeight); } int mWidth, mHeight; int mMinX, mMinY, mMaxX, mMaxY; int mCellSize; const MWWorld::Store& mLandStore; osg::ref_ptr mBaseTexture; osg::ref_ptr mAlphaTexture; osg::ref_ptr mOverlayImage; osg::ref_ptr mOverlayTexture; }; GlobalMap::GlobalMap(osg::Group* root, SceneUtil::WorkQueue* workQueue) : mRoot(root) , mWorkQueue(workQueue) , mWidth(0) , mHeight(0) , mMinX(0), mMaxX(0) , mMinY(0), mMaxY(0) { /* Start of tes3mp change (major) We need map tiles to have consistent sizes, because the server's map is gradually filled in through tiles sent by players via WorldMap packets As a result, the default value is enforced for the time being */ //mCellSize = Settings::Manager::getInt("global map cell size", "Map"); mCellSize = 18; /* End of tes3mp change (major) */ } GlobalMap::~GlobalMap() { for (auto& camera : mCamerasPendingRemoval) removeCamera(camera); for (auto& camera : mActiveCameras) removeCamera(camera); if (mWorkItem) mWorkItem->waitTillDone(); } void GlobalMap::render () { const MWWorld::ESMStore &esmStore = MWBase::Environment::get().getWorld()->getStore(); // get the size of the world MWWorld::Store::iterator it = esmStore.get().extBegin(); for (; it != esmStore.get().extEnd(); ++it) { if (it->getGridX() < mMinX) mMinX = it->getGridX(); if (it->getGridX() > mMaxX) mMaxX = it->getGridX(); if (it->getGridY() < mMinY) mMinY = it->getGridY(); if (it->getGridY() > mMaxY) mMaxY = it->getGridY(); } mWidth = mCellSize*(mMaxX-mMinX+1); mHeight = mCellSize*(mMaxY-mMinY+1); mWorkItem = new CreateMapWorkItem(mWidth, mHeight, mMinX, mMinY, mMaxX, mMaxY, mCellSize, esmStore.get()); mWorkQueue->addWorkItem(mWorkItem); } void GlobalMap::worldPosToImageSpace(float x, float z, float& imageX, float& imageY) { imageX = float(x / float(Constants::CellSizeInUnits) - mMinX) / (mMaxX - mMinX + 1); imageY = 1.f-float(z / float(Constants::CellSizeInUnits) - mMinY) / (mMaxY - mMinY + 1); } void GlobalMap::cellTopLeftCornerToImageSpace(int x, int y, float& imageX, float& imageY) { imageX = float(x - mMinX) / (mMaxX - mMinX + 1); // NB y + 1, because we want the top left corner, not bottom left where the origin of the cell is imageY = 1.f-float(y - mMinY + 1) / (mMaxY - mMinY + 1); } void GlobalMap::requestOverlayTextureUpdate(int x, int y, int width, int height, osg::ref_ptr texture, bool clear, bool cpuCopy, float srcLeft, float srcTop, float srcRight, float srcBottom) { osg::ref_ptr camera (new osg::Camera); camera->setNodeMask(Mask_RenderToTexture); camera->setReferenceFrame(osg::Camera::ABSOLUTE_RF); camera->setViewMatrix(osg::Matrix::identity()); camera->setProjectionMatrix(osg::Matrix::identity()); camera->setProjectionResizePolicy(osg::Camera::FIXED); camera->setRenderOrder(osg::Camera::PRE_RENDER, 1); // Make sure the global map is rendered after the local map y = mHeight - y - height; // convert top-left origin to bottom-left camera->setViewport(x, y, width, height); if (clear) { camera->setClearMask(GL_COLOR_BUFFER_BIT); camera->setClearColor(osg::Vec4(0,0,0,0)); } else camera->setClearMask(GL_NONE); camera->setUpdateCallback(new CameraUpdateGlobalCallback(this)); camera->setRenderTargetImplementation(osg::Camera::FRAME_BUFFER_OBJECT, osg::Camera::PIXEL_BUFFER_RTT); camera->attach(osg::Camera::COLOR_BUFFER, mOverlayTexture); // no need for a depth buffer camera->setImplicitBufferAttachmentMask(osg::DisplaySettings::IMPLICIT_COLOR_BUFFER_ATTACHMENT); if (cpuCopy) { // Attach an image to copy the render back to the CPU when finished osg::ref_ptr image (new osg::Image); image->setPixelFormat(mOverlayImage->getPixelFormat()); image->setDataType(mOverlayImage->getDataType()); camera->attach(osg::Camera::COLOR_BUFFER, image); ImageDest imageDest; imageDest.mImage = image; imageDest.mX = x; imageDest.mY = y; mPendingImageDest[camera] = imageDest; } // Create a quad rendering the updated texture if (texture) { osg::ref_ptr geom = createTexturedQuad(srcLeft, srcTop, srcRight, srcBottom); osg::ref_ptr depth = new osg::Depth; depth->setWriteMask(0); osg::StateSet* stateset = geom->getOrCreateStateSet(); stateset->setAttribute(depth); stateset->setTextureAttributeAndModes(0, texture, osg::StateAttribute::ON); stateset->setMode(GL_LIGHTING, osg::StateAttribute::OFF); stateset->setMode(GL_DEPTH_TEST, osg::StateAttribute::OFF); if (mAlphaTexture) { osg::ref_ptr texcoords = new osg::Vec2Array; float x1 = x / static_cast(mWidth); float x2 = (x + width) / static_cast(mWidth); float y1 = y / static_cast(mHeight); float y2 = (y + height) / static_cast(mHeight); texcoords->push_back(osg::Vec2f(x1, y1)); texcoords->push_back(osg::Vec2f(x1, y2)); texcoords->push_back(osg::Vec2f(x2, y2)); texcoords->push_back(osg::Vec2f(x2, y1)); geom->setTexCoordArray(1, texcoords, osg::Array::BIND_PER_VERTEX); stateset->setTextureAttributeAndModes(1, mAlphaTexture, osg::StateAttribute::ON); osg::ref_ptr texEnvCombine = new osg::TexEnvCombine; texEnvCombine->setCombine_RGB(osg::TexEnvCombine::REPLACE); texEnvCombine->setSource0_RGB(osg::TexEnvCombine::PREVIOUS); stateset->setTextureAttributeAndModes(1, texEnvCombine); } camera->addChild(geom); } mRoot->addChild(camera); mActiveCameras.push_back(camera); } void GlobalMap::exploreCell(int cellX, int cellY, osg::ref_ptr localMapTexture) { ensureLoaded(); if (!localMapTexture) return; int originX = (cellX - mMinX) * mCellSize; int originY = (cellY - mMinY + 1) * mCellSize; // +1 because we want the top left corner of the cell, not the bottom left if (cellX > mMaxX || cellX < mMinX || cellY > mMaxY || cellY < mMinY) return; /* Start of tes3mp addition Track the cell coordinates corresponding to these map image coordinates */ originToCellX[originX] = cellX; originToCellY[originY - mCellSize] = cellY; /* End of tes3mp addition */ requestOverlayTextureUpdate(originX, mHeight - originY, mCellSize, mCellSize, localMapTexture, false, true); } void GlobalMap::clear() { ensureLoaded(); memset(mOverlayImage->data(), 0, mOverlayImage->getTotalSizeInBytes()); mPendingImageDest.clear(); // just push a Camera to clear the FBO, instead of setImage()/dirty() // easier, since we don't need to worry about synchronizing access :) requestOverlayTextureUpdate(0, 0, mWidth, mHeight, osg::ref_ptr(), true, false); } void GlobalMap::write(ESM::GlobalMap& map) { ensureLoaded(); map.mBounds.mMinX = mMinX; map.mBounds.mMaxX = mMaxX; map.mBounds.mMinY = mMinY; map.mBounds.mMaxY = mMaxY; std::ostringstream ostream; osgDB::ReaderWriter* readerwriter = osgDB::Registry::instance()->getReaderWriterForExtension("png"); if (!readerwriter) { Log(Debug::Error) << "Error: Can't write map overlay: no png readerwriter found"; return; } osgDB::ReaderWriter::WriteResult result = readerwriter->writeImage(*mOverlayImage, ostream); if (!result.success()) { Log(Debug::Warning) << "Error: Can't write map overlay: " << result.message() << " code " << result.status(); return; } std::string data = ostream.str(); map.mImageData = std::vector(data.begin(), data.end()); } struct Box { int mLeft, mTop, mRight, mBottom; Box(int left, int top, int right, int bottom) : mLeft(left), mTop(top), mRight(right), mBottom(bottom) { } bool operator == (const Box& other) { return mLeft == other.mLeft && mTop == other.mTop && mRight == other.mRight && mBottom == other.mBottom; } }; void GlobalMap::read(ESM::GlobalMap& map) { ensureLoaded(); const ESM::GlobalMap::Bounds& bounds = map.mBounds; if (bounds.mMaxX-bounds.mMinX < 0) return; if (bounds.mMaxY-bounds.mMinY < 0) return; if (bounds.mMinX > bounds.mMaxX || bounds.mMinY > bounds.mMaxY) throw std::runtime_error("invalid map bounds"); if (map.mImageData.empty()) return; Files::IMemStream istream(map.mImageData.data(), map.mImageData.size()); osgDB::ReaderWriter* readerwriter = osgDB::Registry::instance()->getReaderWriterForExtension("png"); if (!readerwriter) { Log(Debug::Error) << "Error: Can't read map overlay: no png readerwriter found"; return; } osgDB::ReaderWriter::ReadResult result = readerwriter->readImage(istream); if (!result.success()) { Log(Debug::Error) << "Error: Can't read map overlay: " << result.message() << " code " << result.status(); return; } osg::ref_ptr image = result.getImage(); int imageWidth = image->s(); int imageHeight = image->t(); int xLength = (bounds.mMaxX-bounds.mMinX+1); int yLength = (bounds.mMaxY-bounds.mMinY+1); // Size of one cell in image space int cellImageSizeSrc = imageWidth / xLength; if (int(imageHeight / yLength) != cellImageSizeSrc) throw std::runtime_error("cell size must be quadratic"); // If cell bounds of the currently loaded content and the loaded savegame do not match, // we need to resize source/dest boxes to accommodate // This means nonexisting cells will be dropped silently int cellImageSizeDst = mCellSize; // Completely off-screen? -> no need to blit anything if (bounds.mMaxX < mMinX || bounds.mMaxY < mMinY || bounds.mMinX > mMaxX || bounds.mMinY > mMaxY) return; int leftDiff = (mMinX - bounds.mMinX); int topDiff = (bounds.mMaxY - mMaxY); int rightDiff = (bounds.mMaxX - mMaxX); int bottomDiff = (mMinY - bounds.mMinY); Box srcBox ( std::max(0, leftDiff * cellImageSizeSrc), std::max(0, topDiff * cellImageSizeSrc), std::min(imageWidth, imageWidth - rightDiff * cellImageSizeSrc), std::min(imageHeight, imageHeight - bottomDiff * cellImageSizeSrc)); Box destBox ( std::max(0, -leftDiff * cellImageSizeDst), std::max(0, -topDiff * cellImageSizeDst), std::min(mWidth, mWidth + rightDiff * cellImageSizeDst), std::min(mHeight, mHeight + bottomDiff * cellImageSizeDst)); osg::ref_ptr texture (new osg::Texture2D); texture->setImage(image); texture->setWrap(osg::Texture::WRAP_S, osg::Texture::CLAMP_TO_EDGE); texture->setWrap(osg::Texture::WRAP_T, osg::Texture::CLAMP_TO_EDGE); texture->setFilter(osg::Texture::MIN_FILTER, osg::Texture::LINEAR); texture->setFilter(osg::Texture::MAG_FILTER, osg::Texture::LINEAR); texture->setResizeNonPowerOfTwoHint(false); if (srcBox == destBox && imageWidth == mWidth && imageHeight == mHeight) { mOverlayImage = image; requestOverlayTextureUpdate(0, 0, mWidth, mHeight, texture, true, false); } else { // Dimensions don't match. This could mean a changed map region, or a changed map resolution. // In the latter case, we'll want filtering. // Create a RTT Camera and draw the image onto mOverlayImage in the next frame. requestOverlayTextureUpdate(destBox.mLeft, destBox.mTop, destBox.mRight-destBox.mLeft, destBox.mBottom-destBox.mTop, texture, true, true, srcBox.mLeft/float(imageWidth), srcBox.mTop/float(imageHeight), srcBox.mRight/float(imageWidth), srcBox.mBottom/float(imageHeight)); } } osg::ref_ptr GlobalMap::getBaseTexture() { ensureLoaded(); return mBaseTexture; } osg::ref_ptr GlobalMap::getOverlayTexture() { ensureLoaded(); return mOverlayTexture; } void GlobalMap::ensureLoaded() { if (mWorkItem) { mWorkItem->waitTillDone(); mOverlayImage = mWorkItem->mOverlayImage; mBaseTexture = mWorkItem->mBaseTexture; mAlphaTexture = mWorkItem->mAlphaTexture; mOverlayTexture = mWorkItem->mOverlayTexture; requestOverlayTextureUpdate(0, 0, mWidth, mHeight, osg::ref_ptr(), true, false); mWorkItem = nullptr; } } bool GlobalMap::copyResult(osg::Camera *camera, unsigned int frame) { ImageDestMap::iterator it = mPendingImageDest.find(camera); if (it == mPendingImageDest.end()) return true; else { ImageDest& imageDest = it->second; if (imageDest.mFrameDone == 0) imageDest.mFrameDone = frame+2; // wait an extra frame to ensure the draw thread has completed its frame. if (imageDest.mFrameDone > frame) { ++it; return false; } mOverlayImage->copySubImage(imageDest.mX, imageDest.mY, 0, imageDest.mImage); /* Start of tes3mp addition Send an ID_PLAYER_MAP packet with this map tile to the server, but only if: 1) We have recorded the exterior cell corresponding to this tile's coordinates 2) The tile has not previously been marked as explored in this client's mwmp::Worldstate 3) The tile does not belong to a Wilderness cell */ if (originToCellX.count(imageDest.mX) > 0 && originToCellY.count(imageDest.mY) > 0) { int cellX = originToCellX.at(imageDest.mX); int cellY = originToCellY.at(imageDest.mY); mwmp::Worldstate *worldstate = mwmp::Main::get().getNetworking()->getWorldstate(); if (!worldstate->containsExploredMapTile(cellX, cellY)) { // Keep this tile marked as explored so we don't send any more packets for it worldstate->markExploredMapTile(cellX, cellY); if (MWBase::Environment::get().getWorld()->getExterior(cellX, cellY)->getCell()->mContextList.empty() == false) { LOG_MESSAGE_SIMPLE(TimedLog::LOG_INFO, "New global map tile corresponds to cell %i, %i", originToCellX.at(imageDest.mX), originToCellY.at(imageDest.mY)); osgDB::ReaderWriter* readerwriter = osgDB::Registry::instance()->getReaderWriterForExtension("png"); if (!readerwriter) { std::cerr << "Error: Can't write temporary map image, no '" << "png" << "' readerwriter found" << std::endl; return false; } std::ostringstream ostream; osgDB::ReaderWriter::WriteResult result = readerwriter->writeImage(*imageDest.mImage, ostream); if (!result.success()) { std::cerr << "Error: Can't write temporary map image: " << result.message() << " code " << result.status() << std::endl; } std::string stringData = ostream.str(); std::vector vectorData = std::vector(stringData.begin(), stringData.end()); worldstate->sendMapExplored(cellX, cellY, vectorData); } } } /* End of tes3mp addition */ mPendingImageDest.erase(it); return true; } } void GlobalMap::markForRemoval(osg::Camera *camera) { CameraVector::iterator found = std::find(mActiveCameras.begin(), mActiveCameras.end(), camera); if (found == mActiveCameras.end()) { Log(Debug::Error) << "Error: GlobalMap trying to remove an inactive camera"; return; } mActiveCameras.erase(found); mCamerasPendingRemoval.push_back(camera); } void GlobalMap::cleanupCameras() { for (auto& camera : mCamerasPendingRemoval) removeCamera(camera); mCamerasPendingRemoval.clear(); } void GlobalMap::removeCamera(osg::Camera *cam) { cam->removeChildren(0, cam->getNumChildren()); mRoot->removeChild(cam); } /* Start of tes3mp addition Allow the setting of the image data for a global map tile from elsewhere in the code */ void GlobalMap::setImage(int cellX, int cellY, const std::vector& imageData) { Files::IMemStream istream(&imageData[0], imageData.size()); osgDB::ReaderWriter* reader = osgDB::Registry::instance()->getReaderWriterForExtension("png"); if (!reader) { std::cerr << "Error: Failed to read map tile image data, no png readerwriter found" << std::endl; return; } osgDB::ReaderWriter::ReadResult result = reader->readImage(istream); if (!result.success()) { std::cerr << "Error: Can't read map tile image: " << result.message() << " code " << result.status() << std::endl; return; } osg::ref_ptr image = result.getImage(); int posX = (cellX - mMinX) * mCellSize; int posY = (cellY - mMinY + 1) * mCellSize; if (cellX > mMaxX || cellX < mMinX || cellY > mMaxY || cellY < mMinY) return; osg::ref_ptr texture(new osg::Texture2D); texture->setImage(image); texture->setWrap(osg::Texture::WRAP_S, osg::Texture::CLAMP_TO_EDGE); texture->setWrap(osg::Texture::WRAP_T, osg::Texture::CLAMP_TO_EDGE); texture->setFilter(osg::Texture::MIN_FILTER, osg::Texture::LINEAR); texture->setFilter(osg::Texture::MAG_FILTER, osg::Texture::LINEAR); texture->setResizeNonPowerOfTwoHint(false); requestOverlayTextureUpdate(posX, mHeight - posY, mCellSize, mCellSize, texture, true, false); } /* End of tes3mp addition */ } ================================================ FILE: apps/openmw/mwrender/globalmap.hpp ================================================ #ifndef GAME_RENDER_GLOBALMAP_H #define GAME_RENDER_GLOBALMAP_H #include #include #include #include namespace osg { class Texture2D; class Image; class Group; class Camera; } namespace ESM { struct GlobalMap; } namespace SceneUtil { class WorkQueue; } namespace MWRender { class CreateMapWorkItem; class GlobalMap { public: GlobalMap(osg::Group* root, SceneUtil::WorkQueue* workQueue); ~GlobalMap(); void render(); int getWidth() const { return mWidth; } int getHeight() const { return mHeight; } int getCellSize() const { return mCellSize; } void worldPosToImageSpace(float x, float z, float& imageX, float& imageY); void cellTopLeftCornerToImageSpace(int x, int y, float& imageX, float& imageY); void exploreCell (int cellX, int cellY, osg::ref_ptr localMapTexture); /// Clears the overlay void clear(); /** * Removes cameras that have already been rendered. Should be called every frame to ensure that * we do not render the same map more than once. Note, this cleanup is difficult to implement in an * automated fashion, since we can't alter the scene graph structure from within an update callback. */ void cleanupCameras(); void removeCamera(osg::Camera* cam); bool copyResult(osg::Camera* cam, unsigned int frame); /* Start of tes3mp addition Allow the setting of the image data for a global map tile from elsewhere in the code */ void setImage(int cellX, int cellY, const std::vector& imageData); /* End of tes3mp addition */ /** * Mark a camera for cleanup in the next update. For internal use only. */ void markForRemoval(osg::Camera* camera); void write (ESM::GlobalMap& map); void read (ESM::GlobalMap& map); osg::ref_ptr getBaseTexture(); osg::ref_ptr getOverlayTexture(); void ensureLoaded(); private: /** * Request rendering a 2d quad onto mOverlayTexture. * x, y, width and height are the destination coordinates (top-left coordinate origin) * @param cpuCopy copy the resulting render onto mOverlayImage as well? */ void requestOverlayTextureUpdate(int x, int y, int width, int height, osg::ref_ptr texture, bool clear, bool cpuCopy, float srcLeft = 0.f, float srcTop = 0.f, float srcRight = 1.f, float srcBottom = 1.f); int mCellSize; osg::ref_ptr mRoot; typedef std::vector > CameraVector; CameraVector mActiveCameras; CameraVector mCamerasPendingRemoval; struct ImageDest { ImageDest() : mX(0), mY(0) , mFrameDone(0) { } osg::ref_ptr mImage; int mX, mY; unsigned int mFrameDone; }; typedef std::map, ImageDest> ImageDestMap; ImageDestMap mPendingImageDest; std::vector< std::pair > mExploredCells; osg::ref_ptr mBaseTexture; osg::ref_ptr mAlphaTexture; // GPU copy of overlay // Note, uploads are pushed through a Camera, instead of through mOverlayImage osg::ref_ptr mOverlayTexture; // CPU copy of overlay osg::ref_ptr mOverlayImage; osg::ref_ptr mWorkQueue; osg::ref_ptr mWorkItem; int mWidth; int mHeight; int mMinX, mMaxX, mMinY, mMaxY; }; } #endif ================================================ FILE: apps/openmw/mwrender/groundcover.cpp ================================================ #include "groundcover.hpp" #include #include #include #include #include #include "apps/openmw/mwworld/esmstore.hpp" #include "apps/openmw/mwbase/environment.hpp" #include "apps/openmw/mwbase/world.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().searchStatic(id)->mModel; default: return std::string(); } } void GroundcoverUpdater::setWindSpeed(float windSpeed) { mWindSpeed = windSpeed; } void GroundcoverUpdater::setPlayerPos(osg::Vec3f playerPos) { mPlayerPos = playerPos; } void GroundcoverUpdater::setDefaults(osg::StateSet *stateset) { osg::ref_ptr windUniform = new osg::Uniform("windSpeed", 0.0f); stateset->addUniform(windUniform.get()); osg::ref_ptr playerPosUniform = new osg::Uniform("playerPos", osg::Vec3f(0.f, 0.f, 0.f)); stateset->addUniform(playerPosUniform.get()); } void GroundcoverUpdater::apply(osg::StateSet *stateset, osg::NodeVisitor *nv) { osg::ref_ptr windUniform = stateset->getUniform("windSpeed"); if (windUniform != nullptr) windUniform->set(mWindSpeed); osg::ref_ptr playerPosUniform = stateset->getUniform("playerPos"); if (playerPosUniform != nullptr) playerPosUniform->set(mPlayerPos); } class InstancingVisitor : public osg::NodeVisitor { public: InstancingVisitor(std::vector& instances, osg::Vec3f& chunkPosition) : osg::NodeVisitor(TRAVERSE_ALL_CHILDREN) , mInstances(instances) , mChunkPosition(chunkPosition) { } void apply(osg::Node& node) override { osg::ref_ptr ss = node.getStateSet(); if (ss != nullptr) { ss->removeAttribute(osg::StateAttribute::MATERIAL); removeAlpha(ss); } traverse(node); } void apply(osg::Geometry& geom) override { for (unsigned int i = 0; i < geom.getNumPrimitiveSets(); ++i) { geom.getPrimitiveSet(i)->setNumInstances(mInstances.size()); } osg::ref_ptr transforms = new osg::Vec4Array(mInstances.size()); osg::BoundingBox box; float radius = geom.getBoundingBox().radius(); for (unsigned int i = 0; i < transforms->getNumElements(); i++) { osg::Vec3f pos(mInstances[i].mPos.asVec3()); osg::Vec3f relativePos = pos - mChunkPosition; (*transforms)[i] = osg::Vec4f(relativePos, mInstances[i].mScale); // Use an additional margin due to groundcover animation float instanceRadius = radius * mInstances[i].mScale * 1.1f; osg::BoundingSphere instanceBounds(relativePos, instanceRadius); box.expandBy(instanceBounds); } geom.setInitialBound(box); osg::ref_ptr rotations = new osg::Vec3Array(mInstances.size()); for (unsigned int i = 0; i < rotations->getNumElements(); i++) { (*rotations)[i] = mInstances[i].mPos.asRotationVec3(); } // Display lists do not support instancing in OSG 3.4 geom.setUseDisplayList(false); geom.setVertexAttribArray(6, transforms.get(), osg::Array::BIND_PER_VERTEX); geom.setVertexAttribArray(7, rotations.get(), osg::Array::BIND_PER_VERTEX); osg::ref_ptr ss = geom.getOrCreateStateSet(); ss->setAttribute(new osg::VertexAttribDivisor(6, 1)); ss->setAttribute(new osg::VertexAttribDivisor(7, 1)); ss->removeAttribute(osg::StateAttribute::MATERIAL); removeAlpha(ss); traverse(geom); } private: std::vector mInstances; osg::Vec3f mChunkPosition; void removeAlpha(osg::StateSet* stateset) { // MGE uses default alpha settings for groundcover, so we can not rely on alpha properties stateset->removeAttribute(osg::StateAttribute::ALPHAFUNC); stateset->removeMode(GL_ALPHA_TEST); stateset->removeAttribute(osg::StateAttribute::BLENDFUNC); stateset->removeMode(GL_BLEND); stateset->setRenderBinToInherit(); } }; class DensityCalculator { public: DensityCalculator(float density) : mDensity(density) { } bool isInstanceEnabled() { if (mDensity >= 1.f) return true; mCurrentGroundcover += mDensity; if (mCurrentGroundcover < 1.f) return false; mCurrentGroundcover -= 1.f; return true; } void reset() { mCurrentGroundcover = 0.f; } private: float mCurrentGroundcover = 0.f; float mDensity = 0.f; }; inline bool isInChunkBorders(ESM::CellRef& ref, osg::Vec2f& minBound, osg::Vec2f& maxBound) { osg::Vec2f size = maxBound - minBound; if (size.x() >=1 && size.y() >=1) return true; osg::Vec3f pos = ref.mPos.asVec3(); osg::Vec3f cellPos = pos / ESM::Land::REAL_SIZE; if ((minBound.x() > std::floor(minBound.x()) && cellPos.x() < minBound.x()) || (minBound.y() > std::floor(minBound.y()) && cellPos.y() < minBound.y()) || (maxBound.x() < std::ceil(maxBound.x()) && cellPos.x() >= maxBound.x()) || (minBound.y() < std::ceil(maxBound.y()) && cellPos.y() >= maxBound.y())) return false; return true; } osg::ref_ptr Groundcover::getChunk(float size, const osg::Vec2f& center, unsigned char lod, unsigned int lodFlags, bool activeGrid, const osg::Vec3f& viewPoint, bool compile) { ChunkId id = std::make_tuple(center, size, activeGrid); osg::ref_ptr obj = mCache->getRefFromObjectCache(id); if (obj) return obj->asNode(); else { InstanceMap instances; collectInstances(instances, size, center); osg::ref_ptr node = createChunk(instances, center); mCache->addEntryToObjectCache(id, node.get()); return node; } } Groundcover::Groundcover(Resource::SceneManager* sceneManager, float density) : GenericResourceManager(nullptr) , mSceneManager(sceneManager) , mDensity(density) { } void Groundcover::collectInstances(InstanceMap& instances, float size, const osg::Vec2f& center) { const MWWorld::ESMStore& store = MWBase::Environment::get().getWorld()->getStore(); 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); std::vector esm; osg::Vec2i startCell = osg::Vec2i(std::floor(center.x() - size/2.f), std::floor(center.y() - size/2.f)); for (int cellX = startCell.x(); cellX < startCell.x() + size; ++cellX) { for (int cellY = startCell.y(); cellY < startCell.y() + size; ++cellY) { const ESM::Cell* cell = store.get().searchStatic(cellX, cellY); if (!cell) continue; calculator.reset(); for (size_t i=0; imContextList.size(); ++i) { unsigned int index = cell->mContextList.at(i).index; if (esm.size() <= index) esm.resize(index+1); cell->restore(esm[index], i); ESM::CellRef ref; ref.mRefNum.mContentFile = ESM::RefNum::RefNum_NoContentFile; bool deleted = false; 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; 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; instances[model].emplace_back(std::move(ref), std::move(model)); } } } } } osg::ref_ptr Groundcover::createChunk(InstanceMap& instances, const osg::Vec2f& center) { osg::ref_ptr group = new osg::Group; osg::Vec3f worldCenter = osg::Vec3f(center.x(), center.y(), 0)*ESM::Land::REAL_SIZE; for (auto& pair : instances) { const osg::Node* temp = mSceneManager->getTemplate(pair.first); osg::ref_ptr node = static_cast(temp->clone(osg::CopyOp::DEEP_COPY_ALL&(~osg::CopyOp::DEEP_COPY_TEXTURES))); // Keep link to original mesh to keep it in cache group->getOrCreateUserDataContainer()->addUserObject(new Resource::TemplateRef(temp)); mSceneManager->reinstateRemovedState(node); InstancingVisitor visitor(pair.second, worldCenter); node->accept(visitor); group->addChild(node); } // Force a unified alpha handling instead of data from meshes osg::ref_ptr alpha = new osg::AlphaFunc(osg::AlphaFunc::GEQUAL, 128.f / 255.f); group->getOrCreateStateSet()->setAttributeAndModes(alpha.get(), osg::StateAttribute::ON); group->getBound(); group->setNodeMask(Mask_Groundcover); if (mSceneManager->getLightingMethod() != SceneUtil::LightingMethod::FFP) group->setCullCallback(new SceneUtil::LightListCallback); mSceneManager->recreateShaders(group, "groundcover", false, true); return group; } unsigned int Groundcover::getNodeMask() { return Mask_Groundcover; } void Groundcover::reportStats(unsigned int frameNumber, osg::Stats *stats) const { stats->setAttribute(frameNumber, "Groundcover Chunk", mCache->getCacheSize()); } } ================================================ FILE: apps/openmw/mwrender/groundcover.hpp ================================================ #ifndef OPENMW_MWRENDER_GROUNDCOVER_H #define OPENMW_MWRENDER_GROUNDCOVER_H #include #include #include #include namespace MWRender { class GroundcoverUpdater : public SceneUtil::StateSetUpdater { public: GroundcoverUpdater() : mWindSpeed(0.f) , mPlayerPos(osg::Vec3f()) { } void setWindSpeed(float windSpeed); void setPlayerPos(osg::Vec3f playerPos); protected: void setDefaults(osg::StateSet *stateset) override; void apply(osg::StateSet *stateset, osg::NodeVisitor *nv) override; private: float mWindSpeed; osg::Vec3f mPlayerPos; }; typedef std::tuple ChunkId; // Center, Size, ActiveGrid class Groundcover : public Resource::GenericResourceManager, public Terrain::QuadTreeWorld::ChunkManager { public: Groundcover(Resource::SceneManager* sceneManager, float density); ~Groundcover() = default; osg::ref_ptr getChunk(float size, const osg::Vec2f& center, unsigned char lod, unsigned int lodFlags, bool activeGrid, const osg::Vec3f& viewPoint, bool compile) override; unsigned int getNodeMask() override; void reportStats(unsigned int frameNumber, osg::Stats* stats) const override; struct GroundcoverEntry { ESM::Position mPos; float mScale; std::string mModel; GroundcoverEntry(const ESM::CellRef& ref, const std::string& model) { mPos = ref.mPos; mScale = ref.mScale; mModel = model; } }; private: Resource::SceneManager* mSceneManager; float mDensity; typedef std::map> InstanceMap; osg::ref_ptr createChunk(InstanceMap& instances, const osg::Vec2f& center); void collectInstances(InstanceMap& instances, float size, const osg::Vec2f& center); }; } #endif ================================================ FILE: apps/openmw/mwrender/landmanager.cpp ================================================ #include "landmanager.hpp" #include #include #include "../mwbase/environment.hpp" #include "../mwbase/world.hpp" #include "../mwworld/esmstore.hpp" namespace MWRender { LandManager::LandManager(int loadFlags) : GenericResourceManager >(nullptr) , mLoadFlags(loadFlags) { mCache = new CacheType; } osg::ref_ptr LandManager::getLand(int x, int y) { osg::ref_ptr obj = mCache->getRefFromObjectCache(std::make_pair(x,y)); if (obj) return static_cast(obj.get()); else { const ESM::Land* land = MWBase::Environment::get().getWorld()->getStore().get().search(x,y); if (!land) return nullptr; osg::ref_ptr landObj (new ESMTerrain::LandObject(land, mLoadFlags)); mCache->addEntryToObjectCache(std::make_pair(x,y), landObj.get()); return landObj; } } void LandManager::reportStats(unsigned int frameNumber, osg::Stats *stats) const { stats->setAttribute(frameNumber, "Land", mCache->getCacheSize()); } } ================================================ FILE: apps/openmw/mwrender/landmanager.hpp ================================================ #ifndef OPENMW_MWRENDER_LANDMANAGER_H #define OPENMW_MWRENDER_LANDMANAGER_H #include #include #include namespace ESM { struct Land; } namespace MWRender { class LandManager : public Resource::GenericResourceManager > { public: LandManager(int loadFlags); /// @note Will return nullptr if not found. osg::ref_ptr getLand(int x, int y); void reportStats(unsigned int frameNumber, osg::Stats* stats) const override; private: int mLoadFlags; }; } #endif ================================================ FILE: apps/openmw/mwrender/localmap.cpp ================================================ #include "localmap.hpp" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "../mwbase/environment.hpp" #include "../mwbase/windowmanager.hpp" #include "../mwbase/world.hpp" #include "../mwworld/cellstore.hpp" #include "vismask.hpp" namespace { class CameraLocalUpdateCallback : public osg::NodeCallback { public: CameraLocalUpdateCallback(MWRender::LocalMap* parent) : mRendered(false) , mParent(parent) { } void operator()(osg::Node* node, osg::NodeVisitor*) override { if (mRendered) node->setNodeMask(0); if (!mRendered) { mRendered = true; mParent->markForRemoval(static_cast(node)); } // Note, we intentionally do not traverse children here. The map camera's scene data is the same as the master camera's, // so it has been updated already. //traverse(node, nv); } private: bool mRendered; MWRender::LocalMap* mParent; }; float square(float val) { return val*val; } std::pair divideIntoSegments(const osg::BoundingBox& bounds, float mapSize) { osg::Vec2f min(bounds.xMin(), bounds.yMin()); osg::Vec2f max(bounds.xMax(), bounds.yMax()); osg::Vec2f length = max - min; const int segsX = static_cast(std::ceil(length.x() / mapSize)); const int segsY = static_cast(std::ceil(length.y() / mapSize)); return {segsX, segsY}; } } namespace MWRender { LocalMap::LocalMap(osg::Group* root) : mRoot(root) , mMapResolution(Settings::Manager::getInt("local map resolution", "Map")) , mMapWorldSize(Constants::CellSizeInUnits) , mCellDistance(Constants::CellGridRadius) , mAngle(0.f) , mInterior(false) { // Increase map resolution, if use UI scaling float uiScale = MWBase::Environment::get().getWindowManager()->getScalingFactor(); mMapResolution *= uiScale; SceneUtil::FindByNameVisitor find("Scene Root"); mRoot->accept(find); mSceneRoot = find.mFoundNode; if (!mSceneRoot) throw std::runtime_error("no scene root found"); } LocalMap::~LocalMap() { for (auto& camera : mActiveCameras) removeCamera(camera); for (auto& camera : mCamerasPendingRemoval) removeCamera(camera); } const osg::Vec2f LocalMap::rotatePoint(const osg::Vec2f& point, const osg::Vec2f& center, const float angle) { return osg::Vec2f( std::cos(angle) * (point.x() - center.x()) - std::sin(angle) * (point.y() - center.y()) + center.x(), std::sin(angle) * (point.x() - center.x()) + std::cos(angle) * (point.y() - center.y()) + center.y()); } void LocalMap::clear() { mSegments.clear(); } void LocalMap::saveFogOfWar(MWWorld::CellStore* cell) { if (!mInterior) { const MapSegment& segment = mSegments[std::make_pair(cell->getCell()->getGridX(), cell->getCell()->getGridY())]; if (segment.mFogOfWarImage && segment.mHasFogState) { std::unique_ptr fog (new ESM::FogState()); fog->mFogTextures.emplace_back(); segment.saveFogOfWar(fog->mFogTextures.back()); cell->setFog(fog.release()); } } else { auto segments = divideIntoSegments(mBounds, mMapWorldSize); std::unique_ptr fog (new ESM::FogState()); fog->mBounds.mMinX = mBounds.xMin(); fog->mBounds.mMaxX = mBounds.xMax(); fog->mBounds.mMinY = mBounds.yMin(); fog->mBounds.mMaxY = mBounds.yMax(); fog->mNorthMarkerAngle = mAngle; fog->mFogTextures.reserve(segments.first * segments.second); for (int x = 0; x < segments.first; ++x) { for (int y = 0; y < segments.second; ++y) { const MapSegment& segment = mSegments[std::make_pair(x,y)]; fog->mFogTextures.emplace_back(); // saving even if !segment.mHasFogState so we don't mess up the segmenting // plus, older openmw versions can't deal with empty images segment.saveFogOfWar(fog->mFogTextures.back()); fog->mFogTextures.back().mX = x; fog->mFogTextures.back().mY = y; } } cell->setFog(fog.release()); } } osg::ref_ptr LocalMap::createOrthographicCamera(float x, float y, float width, float height, const osg::Vec3d& upVector, float zmin, float zmax) { osg::ref_ptr camera (new osg::Camera); camera->setProjectionMatrixAsOrtho(-width/2, width/2, -height/2, height/2, 5, (zmax-zmin) + 10); camera->setComputeNearFarMode(osg::Camera::DO_NOT_COMPUTE_NEAR_FAR); camera->setViewMatrixAsLookAt(osg::Vec3d(x, y, zmax + 5), osg::Vec3d(x, y, zmin), upVector); camera->setReferenceFrame(osg::Camera::ABSOLUTE_RF_INHERIT_VIEWPOINT); camera->setRenderTargetImplementation(osg::Camera::FRAME_BUFFER_OBJECT, osg::Camera::PIXEL_BUFFER_RTT); camera->setClearColor(osg::Vec4(0.f, 0.f, 0.f, 1.f)); camera->setClearMask(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); camera->setRenderOrder(osg::Camera::PRE_RENDER); camera->setCullMask(Mask_Scene | Mask_SimpleWater | Mask_Terrain | Mask_Object | Mask_Static); camera->setNodeMask(Mask_RenderToTexture); // Disable small feature culling, it's not going to be reliable for this camera osg::Camera::CullingMode cullingMode = (osg::Camera::DEFAULT_CULLING|osg::Camera::FAR_PLANE_CULLING) & ~(osg::CullStack::SMALL_FEATURE_CULLING); camera->setCullingMode(cullingMode); osg::ref_ptr stateset = new osg::StateSet; stateset->setAttribute(new osg::PolygonMode(osg::PolygonMode::FRONT_AND_BACK, osg::PolygonMode::FILL), osg::StateAttribute::OVERRIDE); // assign large value to effectively turn off fog // shaders don't respect glDisable(GL_FOG) osg::ref_ptr fog (new osg::Fog); fog->setStart(10000000); fog->setEnd(10000000); stateset->setAttributeAndModes(fog, osg::StateAttribute::OFF|osg::StateAttribute::OVERRIDE); osg::ref_ptr lightmodel = new osg::LightModel; lightmodel->setAmbientIntensity(osg::Vec4(0.3f, 0.3f, 0.3f, 1.f)); stateset->setAttributeAndModes(lightmodel, osg::StateAttribute::ON|osg::StateAttribute::OVERRIDE); osg::ref_ptr light = new osg::Light; light->setPosition(osg::Vec4(-0.3f, -0.3f, 0.7f, 0.f)); light->setDiffuse(osg::Vec4(0.7f, 0.7f, 0.7f, 1.f)); light->setAmbient(osg::Vec4(0,0,0,1)); light->setSpecular(osg::Vec4(0,0,0,0)); light->setLightNum(0); light->setConstantAttenuation(1.f); light->setLinearAttenuation(0.f); light->setQuadraticAttenuation(0.f); osg::ref_ptr lightSource = new osg::LightSource; lightSource->setLight(light); lightSource->setStateSetModes(*stateset, osg::StateAttribute::ON|osg::StateAttribute::OVERRIDE); SceneUtil::ShadowManager::disableShadowsForStateSet(stateset); // override sun for local map SceneUtil::configureStateSetSunOverride(static_cast(mSceneRoot.get()), light, stateset); camera->addChild(lightSource); camera->setStateSet(stateset); camera->setViewport(0, 0, mMapResolution, mMapResolution); camera->setUpdateCallback(new CameraLocalUpdateCallback(this)); return camera; } void LocalMap::setupRenderToTexture(osg::ref_ptr camera, int x, int y) { osg::ref_ptr texture (new osg::Texture2D); texture->setTextureSize(mMapResolution, mMapResolution); texture->setInternalFormat(GL_RGB); texture->setFilter(osg::Texture::MIN_FILTER, osg::Texture::LINEAR); texture->setFilter(osg::Texture::MAG_FILTER, osg::Texture::LINEAR); texture->setWrap(osg::Texture::WRAP_S, osg::Texture::CLAMP_TO_EDGE); texture->setWrap(osg::Texture::WRAP_T, osg::Texture::CLAMP_TO_EDGE); SceneUtil::attachAlphaToCoverageFriendlyFramebufferToCamera(camera, osg::Camera::COLOR_BUFFER, texture); camera->addChild(mSceneRoot); mRoot->addChild(camera); mActiveCameras.push_back(camera); MapSegment& segment = mSegments[std::make_pair(x, y)]; segment.mMapTexture = texture; } bool needUpdate(std::set >& renderedGrid, std::set >& currentGrid, int cellX, int cellY) { // if all the cells of the current grid are contained in the rendered grid then we can keep the old render for (int dx=-1;dx<2;dx+=1) { for (int dy=-1;dy<2;dy+=1) { bool haveInRenderedGrid = renderedGrid.find(std::make_pair(cellX+dx,cellY+dy)) != renderedGrid.end(); bool haveInCurrentGrid = currentGrid.find(std::make_pair(cellX+dx,cellY+dy)) != currentGrid.end(); if (haveInCurrentGrid && !haveInRenderedGrid) return true; } } return false; } void LocalMap::requestMap(const MWWorld::CellStore* cell) { if (cell->isExterior()) { int cellX = cell->getCell()->getGridX(); int cellY = cell->getCell()->getGridY(); MapSegment& segment = mSegments[std::make_pair(cellX, cellY)]; if (!needUpdate(segment.mGrid, mCurrentGrid, cellX, cellY)) return; else { segment.mGrid = mCurrentGrid; requestExteriorMap(cell); } } else requestInteriorMap(cell); } void LocalMap::addCell(MWWorld::CellStore *cell) { if (cell->isExterior()) mCurrentGrid.emplace(cell->getCell()->getGridX(), cell->getCell()->getGridY()); } void LocalMap::removeCell(MWWorld::CellStore *cell) { saveFogOfWar(cell); if (cell->isExterior()) { std::pair coords = std::make_pair(cell->getCell()->getGridX(), cell->getCell()->getGridY()); mSegments.erase(coords); mCurrentGrid.erase(coords); } else mSegments.clear(); } osg::ref_ptr LocalMap::getMapTexture(int x, int y) { SegmentMap::iterator found = mSegments.find(std::make_pair(x, y)); if (found == mSegments.end()) return osg::ref_ptr(); else return found->second.mMapTexture; } osg::ref_ptr LocalMap::getFogOfWarTexture(int x, int y) { SegmentMap::iterator found = mSegments.find(std::make_pair(x, y)); if (found == mSegments.end()) return osg::ref_ptr(); else return found->second.mFogOfWarTexture; } void LocalMap::removeCamera(osg::Camera *cam) { cam->removeChildren(0, cam->getNumChildren()); mRoot->removeChild(cam); } void LocalMap::markForRemoval(osg::Camera *cam) { CameraVector::iterator found = std::find(mActiveCameras.begin(), mActiveCameras.end(), cam); if (found == mActiveCameras.end()) { Log(Debug::Error) << "Error: trying to remove an inactive camera"; return; } mActiveCameras.erase(found); mCamerasPendingRemoval.push_back(cam); } void LocalMap::cleanupCameras() { if (mCamerasPendingRemoval.empty()) return; for (auto& camera : mCamerasPendingRemoval) removeCamera(camera); mCamerasPendingRemoval.clear(); } void LocalMap::requestExteriorMap(const MWWorld::CellStore* cell) { mInterior = false; int x = cell->getCell()->getGridX(); int y = cell->getCell()->getGridY(); osg::BoundingSphere bound = mSceneRoot->getBound(); float zmin = bound.center().z() - bound.radius(); float zmax = bound.center().z() + bound.radius(); osg::ref_ptr camera = createOrthographicCamera(x*mMapWorldSize + mMapWorldSize/2.f, y*mMapWorldSize + mMapWorldSize/2.f, mMapWorldSize, mMapWorldSize, osg::Vec3d(0,1,0), zmin, zmax); setupRenderToTexture(camera, cell->getCell()->getGridX(), cell->getCell()->getGridY()); MapSegment& segment = mSegments[std::make_pair(cell->getCell()->getGridX(), cell->getCell()->getGridY())]; if (!segment.mFogOfWarImage) { if (cell->getFog()) segment.loadFogOfWar(cell->getFog()->mFogTextures.back()); else segment.initFogOfWar(); } } void LocalMap::requestInteriorMap(const MWWorld::CellStore* cell) { osg::ComputeBoundsVisitor computeBoundsVisitor; computeBoundsVisitor.setTraversalMask(Mask_Scene | Mask_Terrain | Mask_Object | Mask_Static); mSceneRoot->accept(computeBoundsVisitor); osg::BoundingBox bounds = computeBoundsVisitor.getBoundingBox(); // If we're in an empty cell, bail out // The operations in this function are only valid for finite bounds if (!bounds.valid() || bounds.radius2() == 0.0) return; mInterior = true; mBounds = bounds; // Get the cell's NorthMarker rotation. This is used to rotate the entire map. osg::Vec2f north = MWBase::Environment::get().getWorld()->getNorthVector(cell); mAngle = std::atan2(north.x(), north.y()); // Rotate the cell and merge the rotated corners to the bounding box osg::Vec2f origCenter(bounds.center().x(), bounds.center().y()); osg::Vec3f origCorners[8]; for (int i=0; i<8; ++i) origCorners[i] = mBounds.corner(i); for (int i=0; i<8; ++i) { osg::Vec3f corner = origCorners[i]; osg::Vec2f corner2d (corner.x(), corner.y()); corner2d = rotatePoint(corner2d, origCenter, mAngle); mBounds.expandBy(osg::Vec3f(corner2d.x(), corner2d.y(), 0)); } // Do NOT change padding! This will break older savegames. // If the padding really needs to be changed, then it must be saved in the ESM::FogState and // assume the old (500) value as default for older savegames. const float padding = 500.0f; // Apply a little padding mBounds.set(mBounds._min - osg::Vec3f(padding,padding,0.f), mBounds._max + osg::Vec3f(padding,padding,0.f)); float zMin = mBounds.zMin(); float zMax = mBounds.zMax(); // If there is fog state in the CellStore (e.g. when it came from a savegame) we need to do some checks // to see if this state is still valid. // Both the cell bounds and the NorthMarker rotation could be changed by the content files or exchanged models. // If they changed by too much then parts of the interior might not be covered by the map anymore. // The following code detects this, and discards the CellStore's fog state if it needs to. std::vector> segmentMappings; if (cell->getFog()) { ESM::FogState* fog = cell->getFog(); if (std::abs(mAngle - fog->mNorthMarkerAngle) < osg::DegreesToRadians(5.f)) { // Expand mBounds so the saved textures fit the same grid int xOffset = 0; int yOffset = 0; if(fog->mBounds.mMinX < mBounds.xMin()) { mBounds.xMin() = fog->mBounds.mMinX; } else if(fog->mBounds.mMinX > mBounds.xMin()) { float diff = fog->mBounds.mMinX - mBounds.xMin(); xOffset += diff / mMapWorldSize; xOffset++; mBounds.xMin() = fog->mBounds.mMinX - xOffset * mMapWorldSize; } if(fog->mBounds.mMinY < mBounds.yMin()) { mBounds.yMin() = fog->mBounds.mMinY; } else if(fog->mBounds.mMinY > mBounds.yMin()) { float diff = fog->mBounds.mMinY - mBounds.yMin(); yOffset += diff / mMapWorldSize; yOffset++; mBounds.yMin() = fog->mBounds.mMinY - yOffset * mMapWorldSize; } mBounds.xMax() = std::max(mBounds.xMax(), fog->mBounds.mMaxX); mBounds.yMax() = std::max(mBounds.yMax(), fog->mBounds.mMaxY); if(xOffset != 0 || yOffset != 0) Log(Debug::Warning) << "Warning: expanding fog by " << xOffset << ", " << yOffset; const auto& textures = fog->mFogTextures; segmentMappings.reserve(textures.size()); osg::BoundingBox savedBounds{ fog->mBounds.mMinX, fog->mBounds.mMinY, 0, fog->mBounds.mMaxX, fog->mBounds.mMaxY, 0 }; auto segments = divideIntoSegments(savedBounds, mMapWorldSize); for (int x = 0; x < segments.first; ++x) for (int y = 0; y < segments.second; ++y) segmentMappings.emplace_back(std::make_pair(x + xOffset, y + yOffset)); mAngle = fog->mNorthMarkerAngle; } } osg::Vec2f min(mBounds.xMin(), mBounds.yMin()); osg::Vec2f center(mBounds.center().x(), mBounds.center().y()); osg::Quat cameraOrient (mAngle, osg::Vec3d(0,0,-1)); auto segments = divideIntoSegments(mBounds, mMapWorldSize); for (int x = 0; x < segments.first; ++x) { for (int y = 0; y < segments.second; ++y) { osg::Vec2f start = min + osg::Vec2f(mMapWorldSize*x, mMapWorldSize*y); osg::Vec2f newcenter = start + osg::Vec2f(mMapWorldSize/2.f, mMapWorldSize/2.f); osg::Vec2f a = newcenter - center; osg::Vec3f rotatedCenter = cameraOrient * (osg::Vec3f(a.x(), a.y(), 0)); osg::Vec2f pos = osg::Vec2f(rotatedCenter.x(), rotatedCenter.y()) + center; osg::ref_ptr camera = createOrthographicCamera(pos.x(), pos.y(), mMapWorldSize, mMapWorldSize, osg::Vec3f(north.x(), north.y(), 0.f), zMin, zMax); setupRenderToTexture(camera, x, y); auto coords = std::make_pair(x,y); MapSegment& segment = mSegments[coords]; if (!segment.mFogOfWarImage) { bool loaded = false; for(size_t index{}; index < segmentMappings.size(); index++) { if(segmentMappings[index] == coords) { ESM::FogState* fog = cell->getFog(); segment.loadFogOfWar(fog->mFogTextures[index]); loaded = true; break; } } if(!loaded) segment.initFogOfWar(); } } } } void LocalMap::worldToInteriorMapPosition (osg::Vec2f pos, float& nX, float& nY, int& x, int& y) { pos = rotatePoint(pos, osg::Vec2f(mBounds.center().x(), mBounds.center().y()), mAngle); osg::Vec2f min(mBounds.xMin(), mBounds.yMin()); x = static_cast(std::ceil((pos.x() - min.x()) / mMapWorldSize) - 1); y = static_cast(std::ceil((pos.y() - min.y()) / mMapWorldSize) - 1); nX = (pos.x() - min.x() - mMapWorldSize*x)/mMapWorldSize; nY = 1.0f-(pos.y() - min.y() - mMapWorldSize*y)/mMapWorldSize; } osg::Vec2f LocalMap::interiorMapToWorldPosition (float nX, float nY, int x, int y) { osg::Vec2f min(mBounds.xMin(), mBounds.yMin()); osg::Vec2f pos (mMapWorldSize * (nX + x) + min.x(), mMapWorldSize * (1.0f-nY + y) + min.y()); pos = rotatePoint(pos, osg::Vec2f(mBounds.center().x(), mBounds.center().y()), -mAngle); return pos; } bool LocalMap::isPositionExplored (float nX, float nY, int x, int y) { const MapSegment& segment = mSegments[std::make_pair(x, 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)); int texU = static_cast((sFogOfWarResolution - 1) * nX); int texV = static_cast((sFogOfWarResolution - 1) * nY); uint32_t clr = ((const uint32_t*)segment.mFogOfWarImage->data())[texV * sFogOfWarResolution + texU]; uint8_t alpha = (clr >> 24); return alpha < 200; } osg::Group* LocalMap::getRoot() { return mRoot; } void LocalMap::updatePlayer (const osg::Vec3f& position, const osg::Quat& orientation, float& u, float& v, int& x, int& y, osg::Vec3f& direction) { // retrieve the x,y grid coordinates the player is in osg::Vec2f pos(position.x(), position.y()); if (mInterior) { worldToInteriorMapPosition(pos, u,v, x,y); osg::Quat cameraOrient (mAngle, osg::Vec3(0,0,-1)); direction = orientation * cameraOrient.inverse() * osg::Vec3f(0,1,0); } else { direction = orientation * osg::Vec3f(0,1,0); x = static_cast(std::ceil(pos.x() / mMapWorldSize) - 1); y = static_cast(std::ceil(pos.y() / mMapWorldSize) - 1); // convert from world coordinates to texture UV coordinates u = std::abs((pos.x() - (mMapWorldSize*x))/mMapWorldSize); v = 1.0f-std::abs((pos.y() - (mMapWorldSize*y))/mMapWorldSize); } // explore radius (squared) const float exploreRadius = 0.17f * (sFogOfWarResolution-1); // explore radius from 0 to sFogOfWarResolution-1 const float sqrExploreRadius = square(exploreRadius); const float exploreRadiusUV = exploreRadius / sFogOfWarResolution; // explore radius from 0 to 1 (UV space) // change the affected fog of war textures (in a 3x3 grid around the player) for (int mx = -mCellDistance; mx<=mCellDistance; ++mx) { for (int my = -mCellDistance; my<=mCellDistance; ++my) { // is this texture affected at all? bool affected = false; if (mx == 0 && my == 0) // the player is always in the center of the 3x3 grid affected = true; else { bool affectsX = (mx > 0)? (u + exploreRadiusUV > 1) : (u - exploreRadiusUV < 0); bool affectsY = (my > 0)? (v + exploreRadiusUV > 1) : (v - exploreRadiusUV < 0); affected = (affectsX && (my == 0)) || (affectsY && mx == 0) || (affectsX && affectsY); } if (!affected) continue; int texX = x + mx; int texY = y + my*-1; MapSegment& segment = mSegments[std::make_pair(texX, texY)]; if (!segment.mFogOfWarImage || !segment.mMapTexture) continue; uint32_t* data = (uint32_t*)segment.mFogOfWarImage->data(); bool changed = false; for (int texV = 0; texV> 24); alpha = std::min( alpha, (uint8_t) (std::max(0.f, std::min(1.f, (sqrDist/sqrExploreRadius)))*255) ); uint32_t val = (uint32_t) (alpha << 24); if ( *data != val) { *data = val; changed = true; } ++data; } } if (changed) { segment.mHasFogState = true; segment.mFogOfWarImage->dirty(); } } } } LocalMap::MapSegment::MapSegment() : mHasFogState(false) { } LocalMap::MapSegment::~MapSegment() { } void LocalMap::MapSegment::createFogOfWarTexture() { if (mFogOfWarTexture) return; mFogOfWarTexture = new osg::Texture2D; // TODO: synchronize access? for now, the worst that could happen is the draw thread jumping a frame ahead. //mFogOfWarTexture->setDataVariance(osg::Object::DYNAMIC); mFogOfWarTexture->setFilter(osg::Texture::MIN_FILTER, osg::Texture::LINEAR); mFogOfWarTexture->setFilter(osg::Texture::MAG_FILTER, osg::Texture::LINEAR); mFogOfWarTexture->setWrap(osg::Texture::WRAP_S, osg::Texture::CLAMP_TO_EDGE); mFogOfWarTexture->setWrap(osg::Texture::WRAP_T, osg::Texture::CLAMP_TO_EDGE); mFogOfWarTexture->setUnRefImageDataAfterApply(false); } void LocalMap::MapSegment::initFogOfWar() { mFogOfWarImage = new osg::Image; // Assign a PixelBufferObject for asynchronous transfer of data to the GPU mFogOfWarImage->setPixelBufferObject(new osg::PixelBufferObject); mFogOfWarImage->allocateImage(sFogOfWarResolution, sFogOfWarResolution, 1, GL_RGBA, GL_UNSIGNED_BYTE); assert(mFogOfWarImage->isDataContiguous()); std::vector data; data.resize(sFogOfWarResolution*sFogOfWarResolution, 0xff000000); memcpy(mFogOfWarImage->data(), &data[0], data.size()*4); createFogOfWarTexture(); mFogOfWarTexture->setImage(mFogOfWarImage); } void LocalMap::MapSegment::loadFogOfWar(const ESM::FogTexture &esm) { const std::vector& data = esm.mImageData; if (data.empty()) { initFogOfWar(); return; } osgDB::ReaderWriter* readerwriter = osgDB::Registry::instance()->getReaderWriterForExtension("png"); if (!readerwriter) { Log(Debug::Error) << "Error: Unable to load fog, can't find a png ReaderWriter" ; return; } Files::IMemStream in(&data[0], data.size()); osgDB::ReaderWriter::ReadResult result = readerwriter->readImage(in); if (!result.success()) { Log(Debug::Error) << "Error: Failed to read fog: " << result.message() << " code " << result.status(); return; } mFogOfWarImage = result.getImage(); mFogOfWarImage->flipVertical(); mFogOfWarImage->dirty(); createFogOfWarTexture(); mFogOfWarTexture->setImage(mFogOfWarImage); mHasFogState = true; } void LocalMap::MapSegment::saveFogOfWar(ESM::FogTexture &fog) const { if (!mFogOfWarImage) return; std::ostringstream ostream; osgDB::ReaderWriter* readerwriter = osgDB::Registry::instance()->getReaderWriterForExtension("png"); if (!readerwriter) { Log(Debug::Error) << "Error: Unable to write fog, can't find a png ReaderWriter"; return; } // extra flips are unfortunate, but required for compatibility with older versions mFogOfWarImage->flipVertical(); osgDB::ReaderWriter::WriteResult result = readerwriter->writeImage(*mFogOfWarImage, ostream); if (!result.success()) { Log(Debug::Error) << "Error: Unable to write fog: " << result.message() << " code " << result.status(); return; } mFogOfWarImage->flipVertical(); std::string data = ostream.str(); fog.mImageData = std::vector(data.begin(), data.end()); } } ================================================ FILE: apps/openmw/mwrender/localmap.hpp ================================================ #ifndef GAME_RENDER_LOCALMAP_H #define GAME_RENDER_LOCALMAP_H #include #include #include #include #include #include namespace MWWorld { class CellStore; } namespace ESM { struct FogTexture; } namespace osg { class Texture2D; class Image; class Camera; class Group; class Node; } namespace MWRender { /// /// \brief Local map rendering /// class LocalMap { public: LocalMap(osg::Group* root); ~LocalMap(); /** * Clear all savegame-specific data (i.e. fog of war textures) */ void clear(); /** * Request a map render for the given cell. Render textures will be immediately created and can be retrieved with the getMapTexture function. */ void requestMap (const MWWorld::CellStore* cell); void addCell(MWWorld::CellStore* cell); void removeCell (MWWorld::CellStore* cell); osg::ref_ptr getMapTexture (int x, int y); osg::ref_ptr getFogOfWarTexture (int x, int y); void removeCamera(osg::Camera* cam); /** * Indicates a camera has been queued for rendering and can be cleaned up in the next frame. For internal use only. */ void markForRemoval(osg::Camera* cam); /** * Removes cameras that have already been rendered. Should be called every frame to ensure that * we do not render the same map more than once. Note, this cleanup is difficult to implement in an * automated fashion, since we can't alter the scene graph structure from within an update callback. */ void cleanupCameras(); /** * Set the position & direction of the player, and returns the position in map space through the reference parameters. * @remarks This is used to draw a "fog of war" effect * to hide areas on the map the player has not discovered yet. */ void updatePlayer (const osg::Vec3f& position, const osg::Quat& orientation, float& u, float& v, int& x, int& y, osg::Vec3f& direction); /** * Save the fog of war for this cell to its CellStore. * @remarks This should be called when unloading a cell, and for all active cells prior to saving the game. */ void saveFogOfWar(MWWorld::CellStore* cell); /** * Get the interior map texture index and normalized position on this texture, given a world position */ void worldToInteriorMapPosition (osg::Vec2f pos, float& nX, float& nY, int& x, int& y); osg::Vec2f interiorMapToWorldPosition (float nX, float nY, int x, int y); /** * Check if a given position is explored by the player (i.e. not obscured by fog of war) */ bool isPositionExplored (float nX, float nY, int x, int y); osg::Group* getRoot(); private: osg::ref_ptr mRoot; osg::ref_ptr mSceneRoot; typedef std::vector< osg::ref_ptr > CameraVector; CameraVector mActiveCameras; CameraVector mCamerasPendingRemoval; typedef std::set > Grid; Grid mCurrentGrid; struct MapSegment { MapSegment(); ~MapSegment(); void initFogOfWar(); void loadFogOfWar(const ESM::FogTexture& fog); void saveFogOfWar(ESM::FogTexture& fog) const; void createFogOfWarTexture(); osg::ref_ptr mMapTexture; osg::ref_ptr mFogOfWarTexture; osg::ref_ptr mFogOfWarImage; Grid mGrid; // the grid that was active at the time of rendering this segment bool mHasFogState; }; typedef std::map, MapSegment> SegmentMap; SegmentMap mSegments; int mMapResolution; // the dynamic texture is a bottleneck, so don't set this too high static const int sFogOfWarResolution = 32; // size of a map segment (for exteriors, 1 cell) float mMapWorldSize; int mCellDistance; float mAngle; const osg::Vec2f rotatePoint(const osg::Vec2f& point, const osg::Vec2f& center, const float angle); void requestExteriorMap(const MWWorld::CellStore* cell); void requestInteriorMap(const MWWorld::CellStore* cell); osg::ref_ptr createOrthographicCamera(float left, float top, float width, float height, const osg::Vec3d& upVector, float zmin, float zmax); void setupRenderToTexture(osg::ref_ptr camera, int x, int y); bool mInterior; osg::BoundingBox mBounds; }; } #endif ================================================ FILE: apps/openmw/mwrender/navmesh.cpp ================================================ #include "navmesh.hpp" #include "vismask.hpp" #include #include namespace MWRender { NavMesh::NavMesh(const osg::ref_ptr& root, bool enabled) : mRootNode(root) , mEnabled(enabled) , mGeneration(0) , mRevision(0) { } NavMesh::~NavMesh() { if (mEnabled) disable(); } bool NavMesh::toggle() { if (mEnabled) disable(); else enable(); 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) { if (!mEnabled || (mGroup && mId == id && mGeneration == generation && mRevision == revision)) return; mId = id; mGeneration = generation; mRevision = revision; if (mGroup) mRootNode->removeChild(mGroup); mGroup = SceneUtil::createNavMeshGroup(navMesh, settings); if (mGroup) { mGroup->setNodeMask(Mask_Debug); mRootNode->addChild(mGroup); } } void NavMesh::reset() { if (mGroup) { mRootNode->removeChild(mGroup); mGroup = nullptr; } } void NavMesh::enable() { if (mGroup) mRootNode->addChild(mGroup); mEnabled = true; } void NavMesh::disable() { if (mGroup) mRootNode->removeChild(mGroup); mEnabled = false; } } ================================================ FILE: apps/openmw/mwrender/navmesh.hpp ================================================ #ifndef OPENMW_MWRENDER_NAVMESH_H #define OPENMW_MWRENDER_NAVMESH_H #include #include namespace osg { class Group; class Geometry; } namespace MWRender { class NavMesh { public: NavMesh(const osg::ref_ptr& root, bool enabled); ~NavMesh(); 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 reset(); void enable(); void disable(); bool isEnabled() const { return mEnabled; } private: osg::ref_ptr mRootNode; bool mEnabled; std::size_t mId = std::numeric_limits::max(); std::size_t mGeneration; std::size_t mRevision; osg::ref_ptr mGroup; }; } #endif ================================================ FILE: apps/openmw/mwrender/npcanimation.cpp ================================================ #include "npcanimation.hpp" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "../mwworld/esmstore.hpp" #include "../mwworld/inventorystore.hpp" #include "../mwworld/class.hpp" #include "../mwworld/player.hpp" #include "../mwmechanics/npcstats.hpp" #include "../mwmechanics/actorutil.hpp" #include "../mwmechanics/weapontype.hpp" #include "../mwbase/environment.hpp" #include "../mwbase/world.hpp" #include "../mwbase/mechanicsmanager.hpp" #include "../mwbase/soundmanager.hpp" #include "camera.hpp" #include "rotatecontroller.hpp" #include "renderbin.hpp" #include "vismask.hpp" namespace { std::string getVampireHead(const std::string& race, bool female) { static std::map , const ESM::BodyPart* > sVampireMapping; std::pair thisCombination = std::make_pair(race, int(female)); if (sVampireMapping.find(thisCombination) == sVampireMapping.end()) { const MWWorld::ESMStore &store = MWBase::Environment::get().getWorld()->getStore(); for (const ESM::BodyPart& bodypart : store.get()) { if (!bodypart.mData.mVampire) continue; if (bodypart.mData.mType != ESM::BodyPart::MT_Skin) continue; if (bodypart.mData.mPart != ESM::BodyPart::MP_Head) continue; if (female != (bodypart.mData.mFlags & ESM::BodyPart::BPF_Female)) continue; if (!Misc::StringUtils::ciEqual(bodypart.mRace, race)) continue; sVampireMapping[thisCombination] = &bodypart; } } sVampireMapping.emplace(thisCombination, nullptr); const ESM::BodyPart* bodyPart = sVampireMapping[thisCombination]; if (!bodyPart) return std::string(); return "meshes\\" + bodyPart->mModel; } std::string getShieldBodypartMesh(const std::vector& bodyparts, bool female) { const MWWorld::ESMStore &store = MWBase::Environment::get().getWorld()->getStore(); const MWWorld::Store &partStore = store.get(); for (const auto& part : bodyparts) { if (part.mPart != ESM::PRT_Shield) continue; std::string bodypartName; if (female && !part.mFemale.empty()) bodypartName = part.mFemale; else if (!part.mMale.empty()) bodypartName = part.mMale; if (!bodypartName.empty()) { const ESM::BodyPart *bodypart = partStore.search(bodypartName); if (bodypart == nullptr || bodypart->mData.mType != ESM::BodyPart::MT_Armor) return std::string(); if (!bodypart->mModel.empty()) return "meshes\\" + bodypart->mModel; } } return std::string(); } } namespace MWRender { class HeadAnimationTime : public SceneUtil::ControllerSource { private: MWWorld::Ptr mReference; float mTalkStart; float mTalkStop; float mBlinkStart; float mBlinkStop; float mBlinkTimer; bool mEnabled; float mValue; private: void resetBlinkTimer(); public: HeadAnimationTime(const MWWorld::Ptr& reference); void updatePtr(const MWWorld::Ptr& updated); void update(float dt); void setEnabled(bool enabled); void setTalkStart(float value); void setTalkStop(float value); void setBlinkStart(float value); void setBlinkStop(float value); float getValue(osg::NodeVisitor* nv) override; }; // -------------------------------------------------------------------------------- /// Subclass RotateController to add a Z-offset for sneaking in first person mode. /// @note We use inheritance instead of adding another controller, so that we do not have to compute the worldOrient twice. /// @note Must be set on a MatrixTransform. class NeckController : public RotateController { public: NeckController(osg::Node* relativeTo) : RotateController(relativeTo) { } void setOffset(const osg::Vec3f& offset) { mOffset = offset; } void operator()(osg::Node* node, osg::NodeVisitor* nv) override { osg::MatrixTransform* transform = static_cast(node); osg::Matrix matrix = transform->getMatrix(); osg::Quat worldOrient = getWorldOrientation(node); osg::Quat orient = worldOrient * mRotate * worldOrient.inverse() * matrix.getRotate(); matrix.setRotate(orient); matrix.setTrans(matrix.getTrans() + worldOrient.inverse() * mOffset); transform->setMatrix(matrix); traverse(node,nv); } private: osg::Vec3f mOffset; }; // -------------------------------------------------------------------------------------------------------------- HeadAnimationTime::HeadAnimationTime(const MWWorld::Ptr& reference) : mReference(reference), mTalkStart(0), mTalkStop(0), mBlinkStart(0), mBlinkStop(0), mEnabled(true), mValue(0) { resetBlinkTimer(); } void HeadAnimationTime::updatePtr(const MWWorld::Ptr &updated) { mReference = updated; } void HeadAnimationTime::setEnabled(bool enabled) { mEnabled = enabled; } void HeadAnimationTime::resetBlinkTimer() { mBlinkTimer = -(2.0f + Misc::Rng::rollDice(6)); } void HeadAnimationTime::update(float dt) { if (!mEnabled) return; if (!MWBase::Environment::get().getSoundManager()->sayActive(mReference)) { mBlinkTimer += dt; float duration = mBlinkStop - mBlinkStart; if (mBlinkTimer >= 0 && mBlinkTimer <= duration) { mValue = mBlinkStart + mBlinkTimer; } else mValue = mBlinkStop; if (mBlinkTimer > duration) resetBlinkTimer(); } else { // FIXME: would be nice to hold on to the SoundPtr so we don't have to retrieve it every frame mValue = mTalkStart + (mTalkStop - mTalkStart) * std::min(1.f, MWBase::Environment::get().getSoundManager()->getSaySoundLoudness(mReference)*2); // Rescale a bit (most voices are not very loud) } } float HeadAnimationTime::getValue(osg::NodeVisitor*) { return mValue; } void HeadAnimationTime::setTalkStart(float value) { mTalkStart = value; } void HeadAnimationTime::setTalkStop(float value) { mTalkStop = value; } void HeadAnimationTime::setBlinkStart(float value) { mBlinkStart = value; } void HeadAnimationTime::setBlinkStop(float value) { mBlinkStop = value; } // ---------------------------------------------------- NpcAnimation::NpcType NpcAnimation::getNpcType() const { const MWWorld::Class &cls = mPtr.getClass(); // Dead vampires should typically stay vampires. if (mNpcType == Type_Vampire && cls.getNpcStats(mPtr).isDead() && !cls.getNpcStats(mPtr).isWerewolf()) return mNpcType; return getNpcType(mPtr); } NpcAnimation::NpcType NpcAnimation::getNpcType(const MWWorld::Ptr& ptr) { const MWWorld::Class &cls = ptr.getClass(); NpcAnimation::NpcType curType = Type_Normal; if (cls.getCreatureStats(ptr).getMagicEffects().get(ESM::MagicEffect::Vampirism).getMagnitude() > 0) curType = Type_Vampire; if (cls.getNpcStats(ptr).isWerewolf()) curType = Type_Werewolf; return curType; } static NpcAnimation::PartBoneMap createPartListMap() { NpcAnimation::PartBoneMap result; result.insert(std::make_pair(ESM::PRT_Head, "Head")); result.insert(std::make_pair(ESM::PRT_Hair, "Head")); // note it uses "Head" as attach bone, but "Hair" as filter result.insert(std::make_pair(ESM::PRT_Neck, "Neck")); result.insert(std::make_pair(ESM::PRT_Cuirass, "Chest")); result.insert(std::make_pair(ESM::PRT_Groin, "Groin")); result.insert(std::make_pair(ESM::PRT_Skirt, "Groin")); result.insert(std::make_pair(ESM::PRT_RHand, "Right Hand")); result.insert(std::make_pair(ESM::PRT_LHand, "Left Hand")); result.insert(std::make_pair(ESM::PRT_RWrist, "Right Wrist")); result.insert(std::make_pair(ESM::PRT_LWrist, "Left Wrist")); result.insert(std::make_pair(ESM::PRT_Shield, "Shield Bone")); result.insert(std::make_pair(ESM::PRT_RForearm, "Right Forearm")); result.insert(std::make_pair(ESM::PRT_LForearm, "Left Forearm")); result.insert(std::make_pair(ESM::PRT_RUpperarm, "Right Upper Arm")); result.insert(std::make_pair(ESM::PRT_LUpperarm, "Left Upper Arm")); result.insert(std::make_pair(ESM::PRT_RFoot, "Right Foot")); result.insert(std::make_pair(ESM::PRT_LFoot, "Left Foot")); result.insert(std::make_pair(ESM::PRT_RAnkle, "Right Ankle")); result.insert(std::make_pair(ESM::PRT_LAnkle, "Left Ankle")); result.insert(std::make_pair(ESM::PRT_RKnee, "Right Knee")); result.insert(std::make_pair(ESM::PRT_LKnee, "Left Knee")); result.insert(std::make_pair(ESM::PRT_RLeg, "Right Upper Leg")); result.insert(std::make_pair(ESM::PRT_LLeg, "Left Upper Leg")); result.insert(std::make_pair(ESM::PRT_RPauldron, "Right Clavicle")); result.insert(std::make_pair(ESM::PRT_LPauldron, "Left Clavicle")); result.insert(std::make_pair(ESM::PRT_Weapon, "Weapon Bone")); // Fallback. The real node name depends on the current weapon type. result.insert(std::make_pair(ESM::PRT_Tail, "Tail")); return result; } const NpcAnimation::PartBoneMap NpcAnimation::sPartList = createPartListMap(); NpcAnimation::~NpcAnimation() { mAmmunition.reset(); } NpcAnimation::NpcAnimation(const MWWorld::Ptr& ptr, osg::ref_ptr parentNode, Resource::ResourceSystem* resourceSystem, bool disableSounds, ViewMode viewMode, float firstPersonFieldOfView) : ActorAnimation(ptr, parentNode, resourceSystem), mViewMode(viewMode), mShowWeapons(false), mShowCarriedLeft(true), mNpcType(getNpcType(ptr)), mFirstPersonFieldOfView(firstPersonFieldOfView), mSoundsDisabled(disableSounds), mAccurateAiming(false), mAimingFactor(0.f) { mNpc = mPtr.get()->mBase; mHeadAnimationTime = std::shared_ptr(new HeadAnimationTime(mPtr)); mWeaponAnimationTime = std::shared_ptr(new WeaponAnimationTime(this)); for(size_t i = 0;i < ESM::PRT_Count;i++) { mPartslots[i] = -1; //each slot is empty mPartPriorities[i] = 0; } std::fill(mSounds.begin(), mSounds.end(), nullptr); updateNpcBase(); } void NpcAnimation::setViewMode(NpcAnimation::ViewMode viewMode) { assert(viewMode != VM_HeadOnly); if(mViewMode == viewMode) return; mViewMode = viewMode; MWBase::Environment::get().getWorld()->scaleObject(mPtr, mPtr.getCellRef().getScale()); // apply race height after view change mAmmunition.reset(); rebuild(); setRenderBin(); } /// @brief A RenderBin callback to clear the depth buffer before rendering. class DepthClearCallback : public osgUtil::RenderBin::DrawCallback { public: DepthClearCallback() { mDepth = new osg::Depth; mDepth->setWriteMask(true); } void drawImplementation(osgUtil::RenderBin* bin, osg::RenderInfo& renderInfo, osgUtil::RenderLeaf*& previous) override { renderInfo.getState()->applyAttribute(mDepth); glClear(GL_DEPTH_BUFFER_BIT); bin->drawImplementation(renderInfo, previous); } osg::ref_ptr mDepth; }; /// Overrides Field of View to given value for rendering the subgraph. /// Must be added as cull callback. class OverrideFieldOfViewCallback : public osg::NodeCallback { public: OverrideFieldOfViewCallback(float fov) : mFov(fov) { } void operator()(osg::Node* node, osg::NodeVisitor* nv) override { osgUtil::CullVisitor* cv = static_cast(nv); float fov, aspect, zNear, zFar; if (cv->getProjectionMatrix()->getPerspective(fov, aspect, zNear, zFar)) { fov = mFov; osg::ref_ptr newProjectionMatrix = new osg::RefMatrix(); newProjectionMatrix->makePerspective(fov, aspect, zNear, zFar); osg::ref_ptr invertedOldMatrix = cv->getProjectionMatrix(); invertedOldMatrix = new osg::RefMatrix(osg::RefMatrix::inverse(*invertedOldMatrix)); osg::ref_ptr viewMatrix = new osg::RefMatrix(*cv->getModelViewMatrix()); viewMatrix->postMult(*newProjectionMatrix); viewMatrix->postMult(*invertedOldMatrix); cv->pushModelViewMatrix(viewMatrix, osg::Transform::ReferenceFrame::ABSOLUTE_RF); traverse(node, nv); cv->popModelViewMatrix(); } else traverse(node, nv); } private: float mFov; }; void NpcAnimation::setRenderBin() { if (mViewMode == VM_FirstPerson) { static bool prototypeAdded = false; if (!prototypeAdded) { osg::ref_ptr depthClearBin (new osgUtil::RenderBin); depthClearBin->setDrawCallback(new DepthClearCallback); osgUtil::RenderBin::addRenderBinPrototype("DepthClear", depthClearBin); prototypeAdded = true; } mObjectRoot->getOrCreateStateSet()->setRenderBinDetails(RenderBin_FirstPerson, "DepthClear", osg::StateSet::OVERRIDE_RENDERBIN_DETAILS); } else if (osg::StateSet* stateset = mObjectRoot->getStateSet()) stateset->setRenderBinToInherit(); } void NpcAnimation::rebuild() { mScabbard.reset(); mHolsteredShield.reset(); updateNpcBase(); MWBase::Environment::get().getMechanicsManager()->forceStateUpdate(mPtr); } int NpcAnimation::getSlot(const osg::NodePath &path) const { for (int i=0; igetNode().get()) != path.end()) { return mPartslots[i]; } } return -1; } void NpcAnimation::updateNpcBase() { clearAnimSources(); for(size_t i = 0;i < ESM::PRT_Count;i++) removeIndividualPart((ESM::PartReferenceType)i); const MWWorld::ESMStore &store = MWBase::Environment::get().getWorld()->getStore(); const ESM::Race *race = store.get().find(mNpc->mRace); NpcType curType = getNpcType(); bool isWerewolf = (curType == Type_Werewolf); bool isVampire = (curType == Type_Vampire); bool isFemale = !mNpc->isMale(); mHeadModel.clear(); mHairModel.clear(); std::string headName = isWerewolf ? "WerewolfHead" : mNpc->mHead; std::string hairName = isWerewolf ? "WerewolfHair" : mNpc->mHair; if (!headName.empty()) { const ESM::BodyPart* bp = store.get().search(headName); if (bp) mHeadModel = "meshes\\" + bp->mModel; else Log(Debug::Warning) << "Warning: Failed to load body part '" << headName << "'"; } if (!hairName.empty()) { const ESM::BodyPart* bp = store.get().search(hairName); if (bp) mHairModel = "meshes\\" + bp->mModel; else Log(Debug::Warning) << "Warning: Failed to load body part '" << hairName << "'"; } const std::string& vampireHead = getVampireHead(mNpc->mRace, isFemale); if (!isWerewolf && isVampire && !vampireHead.empty()) mHeadModel = vampireHead; bool is1stPerson = mViewMode == VM_FirstPerson; bool isBeast = (race->mData.mFlags & ESM::Race::Beast) != 0; std::string defaultSkeleton = SceneUtil::getActorSkeleton(is1stPerson, isFemale, isBeast, isWerewolf); defaultSkeleton = Misc::ResourceHelpers::correctActorModelPath(defaultSkeleton, mResourceSystem->getVFS()); std::string smodel = defaultSkeleton; if (!is1stPerson && !isWerewolf && !mNpc->mModel.empty()) smodel = Misc::ResourceHelpers::correctActorModelPath("meshes\\" + mNpc->mModel, mResourceSystem->getVFS()); setObjectRoot(smodel, true, true, false); updateParts(); if(!is1stPerson) { const std::string base = Settings::Manager::getString("xbaseanim", "Models"); if (smodel != base && !isWerewolf) addAnimSource(base, smodel); if (smodel != defaultSkeleton && base != defaultSkeleton) addAnimSource(defaultSkeleton, smodel); addAnimSource(smodel, smodel); if(!isWerewolf && Misc::StringUtils::lowerCase(mNpc->mRace).find("argonian") != std::string::npos) addAnimSource("meshes\\xargonian_swimkna.nif", smodel); } else { const std::string base = Settings::Manager::getString("xbaseanim1st", "Models"); if (smodel != base && !isWerewolf) addAnimSource(base, smodel); addAnimSource(smodel, smodel); mObjectRoot->setNodeMask(Mask_FirstPerson); mObjectRoot->addCullCallback(new OverrideFieldOfViewCallback(mFirstPersonFieldOfView)); } mWeaponAnimationTime->updateStartTime(); } std::string NpcAnimation::getShieldMesh(MWWorld::ConstPtr shield) const { std::string mesh = shield.getClass().getModel(shield); const ESM::Armor *armor = shield.get()->mBase; const std::vector& bodyparts = armor->mParts.mParts; // Try to recover the body part model, use ground model as a fallback otherwise. if (!bodyparts.empty()) mesh = getShieldBodypartMesh(bodyparts, !mNpc->isMale()); if (mesh.empty()) return std::string(); std::string holsteredName = mesh; holsteredName = holsteredName.replace(holsteredName.size()-4, 4, "_sh.nif"); if(mResourceSystem->getVFS()->exists(holsteredName)) { osg::ref_ptr shieldTemplate = mResourceSystem->getSceneManager()->getInstance(holsteredName); SceneUtil::FindByNameVisitor findVisitor ("Bip01 Sheath"); shieldTemplate->accept(findVisitor); osg::ref_ptr sheathNode = findVisitor.mFoundNode; if(!sheathNode) return std::string(); } return mesh; } void NpcAnimation::updateParts() { if (!mObjectRoot.get()) return; NpcType curType = getNpcType(); if (curType != mNpcType) { mNpcType = curType; rebuild(); return; } static const struct { int mSlot; int mBasePriority; } slotlist[] = { // FIXME: Priority is based on the number of reserved slots. There should be a better way. { MWWorld::InventoryStore::Slot_Robe, 11 }, { MWWorld::InventoryStore::Slot_Skirt, 3 }, { MWWorld::InventoryStore::Slot_Helmet, 0 }, { MWWorld::InventoryStore::Slot_Cuirass, 0 }, { MWWorld::InventoryStore::Slot_Greaves, 0 }, { MWWorld::InventoryStore::Slot_LeftPauldron, 0 }, { MWWorld::InventoryStore::Slot_RightPauldron, 0 }, { MWWorld::InventoryStore::Slot_Boots, 0 }, { MWWorld::InventoryStore::Slot_LeftGauntlet, 0 }, { MWWorld::InventoryStore::Slot_RightGauntlet, 0 }, { MWWorld::InventoryStore::Slot_Shirt, 0 }, { MWWorld::InventoryStore::Slot_Pants, 0 }, { MWWorld::InventoryStore::Slot_CarriedLeft, 0 }, { MWWorld::InventoryStore::Slot_CarriedRight, 0 } }; static const size_t slotlistsize = sizeof(slotlist)/sizeof(slotlist[0]); bool wasArrowAttached = isArrowAttached(); mAmmunition.reset(); const MWWorld::InventoryStore& inv = mPtr.getClass().getInventoryStore(mPtr); for(size_t i = 0;i < slotlistsize && mViewMode != VM_HeadOnly;i++) { MWWorld::ConstContainerStoreIterator store = inv.getSlot(slotlist[i].mSlot); removePartGroup(slotlist[i].mSlot); if(store == inv.end()) continue; if(slotlist[i].mSlot == MWWorld::InventoryStore::Slot_Helmet) removeIndividualPart(ESM::PRT_Hair); int prio = 1; bool enchantedGlow = !store->getClass().getEnchantment(*store).empty(); osg::Vec4f glowColor = store->getClass().getEnchantmentColor(*store); if(store->getTypeName() == typeid(ESM::Clothing).name()) { prio = ((slotlist[i].mBasePriority+1)<<1) + 0; const ESM::Clothing *clothes = store->get()->mBase; addPartGroup(slotlist[i].mSlot, prio, clothes->mParts.mParts, enchantedGlow, &glowColor); } else if(store->getTypeName() == typeid(ESM::Armor).name()) { prio = ((slotlist[i].mBasePriority+1)<<1) + 1; const ESM::Armor *armor = store->get()->mBase; addPartGroup(slotlist[i].mSlot, prio, armor->mParts.mParts, enchantedGlow, &glowColor); } if(slotlist[i].mSlot == MWWorld::InventoryStore::Slot_Robe) { ESM::PartReferenceType parts[] = { ESM::PRT_Groin, ESM::PRT_Skirt, ESM::PRT_RLeg, ESM::PRT_LLeg, ESM::PRT_RUpperarm, ESM::PRT_LUpperarm, ESM::PRT_RKnee, ESM::PRT_LKnee, ESM::PRT_RForearm, ESM::PRT_LForearm, ESM::PRT_Cuirass }; size_t parts_size = sizeof(parts)/sizeof(parts[0]); for(size_t p = 0;p < parts_size;++p) reserveIndividualPart(parts[p], slotlist[i].mSlot, prio); } else if(slotlist[i].mSlot == MWWorld::InventoryStore::Slot_Skirt) { reserveIndividualPart(ESM::PRT_Groin, slotlist[i].mSlot, prio); reserveIndividualPart(ESM::PRT_RLeg, slotlist[i].mSlot, prio); reserveIndividualPart(ESM::PRT_LLeg, slotlist[i].mSlot, prio); } } if(mViewMode != VM_FirstPerson) { if(mPartPriorities[ESM::PRT_Head] < 1 && !mHeadModel.empty()) addOrReplaceIndividualPart(ESM::PRT_Head, -1,1, mHeadModel); if(mPartPriorities[ESM::PRT_Hair] < 1 && mPartPriorities[ESM::PRT_Head] <= 1 && !mHairModel.empty()) addOrReplaceIndividualPart(ESM::PRT_Hair, -1,1, mHairModel); } if(mViewMode == VM_HeadOnly) return; if(mPartPriorities[ESM::PRT_Shield] < 1) { MWWorld::ConstContainerStoreIterator store = inv.getSlot(MWWorld::InventoryStore::Slot_CarriedLeft); MWWorld::ConstPtr part; if(store != inv.end() && (part=*store).getTypeName() == typeid(ESM::Light).name()) { const ESM::Light *light = part.get()->mBase; addOrReplaceIndividualPart(ESM::PRT_Shield, MWWorld::InventoryStore::Slot_CarriedLeft, 1, "meshes\\"+light->mModel); if (mObjectParts[ESM::PRT_Shield]) addExtraLight(mObjectParts[ESM::PRT_Shield]->getNode()->asGroup(), light); } } showWeapons(mShowWeapons); showCarriedLeft(mShowCarriedLeft); bool isWerewolf = (getNpcType() == Type_Werewolf); std::string race = (isWerewolf ? "werewolf" : Misc::StringUtils::lowerCase(mNpc->mRace)); const std::vector &parts = getBodyParts(race, !mNpc->isMale(), mViewMode == VM_FirstPerson, isWerewolf); for(int part = ESM::PRT_Neck; part < ESM::PRT_Count; ++part) { if(mPartPriorities[part] < 1) { const ESM::BodyPart* bodypart = parts[part]; if(bodypart) addOrReplaceIndividualPart((ESM::PartReferenceType)part, -1, 1, "meshes\\"+bodypart->mModel); } } if (wasArrowAttached) attachArrow(); } PartHolderPtr NpcAnimation::insertBoundedPart(const std::string& model, const std::string& bonename, const std::string& bonefilter, bool enchantedGlow, osg::Vec4f* glowColor) { osg::ref_ptr instance = mResourceSystem->getSceneManager()->getInstance(model); const NodeMap& nodeMap = getNodeMap(); NodeMap::const_iterator found = nodeMap.find(Misc::StringUtils::lowerCase(bonename)); if (found == nodeMap.end()) throw std::runtime_error("Can't find attachment node " + bonename); osg::ref_ptr attached = SceneUtil::attach(instance, mObjectRoot, bonefilter, found->second); if (enchantedGlow) mGlowUpdater = SceneUtil::addEnchantedGlow(attached, mResourceSystem, *glowColor); return PartHolderPtr(new PartHolder(attached)); } osg::Vec3f NpcAnimation::runAnimation(float timepassed) { osg::Vec3f ret = Animation::runAnimation(timepassed); mHeadAnimationTime->update(timepassed); if (mFirstPersonNeckController) { if (mAccurateAiming) mAimingFactor = 1.f; else mAimingFactor = std::max(0.f, mAimingFactor - timepassed * 0.5f); float rotateFactor = 0.75f + 0.25f * mAimingFactor; mFirstPersonNeckController->setRotate(osg::Quat(mPtr.getRefData().getPosition().rot[0] * rotateFactor, osg::Vec3f(-1,0,0))); mFirstPersonNeckController->setOffset(mFirstPersonOffset); } WeaponAnimation::configureControllers(mPtr.getRefData().getPosition().rot[0] + getBodyPitchRadians()); return ret; } void NpcAnimation::removeIndividualPart(ESM::PartReferenceType type) { mPartPriorities[type] = 0; mPartslots[type] = -1; mObjectParts[type].reset(); if (mSounds[type] != nullptr && !mSoundsDisabled) { MWBase::Environment::get().getSoundManager()->stopSound(mSounds[type]); mSounds[type] = nullptr; } } void NpcAnimation::reserveIndividualPart(ESM::PartReferenceType type, int group, int priority) { if(priority > mPartPriorities[type]) { removeIndividualPart(type); mPartPriorities[type] = priority; mPartslots[type] = group; } } void NpcAnimation::removePartGroup(int group) { for(int i = 0; i < ESM::PRT_Count; i++) { if(mPartslots[i] == group) removeIndividualPart((ESM::PartReferenceType)i); } } bool NpcAnimation::isFirstPersonPart(const ESM::BodyPart* bodypart) { return bodypart->mId.size() >= 3 && bodypart->mId.substr(bodypart->mId.size()-3, 3) == "1st"; } bool NpcAnimation::isFemalePart(const ESM::BodyPart* bodypart) { return bodypart->mData.mFlags & ESM::BodyPart::BPF_Female; } bool NpcAnimation::addOrReplaceIndividualPart(ESM::PartReferenceType type, int group, int priority, const std::string &mesh, bool enchantedGlow, osg::Vec4f* glowColor) { if(priority <= mPartPriorities[type]) return false; removeIndividualPart(type); mPartslots[type] = group; mPartPriorities[type] = priority; try { std::string bonename = sPartList.at(type); if (type == ESM::PRT_Weapon) { const MWWorld::InventoryStore& inv = mPtr.getClass().getInventoryStore(mPtr); MWWorld::ConstContainerStoreIterator weapon = inv.getSlot(MWWorld::InventoryStore::Slot_CarriedRight); if(weapon != inv.end() && weapon->getTypeName() == typeid(ESM::Weapon).name()) { int weaponType = weapon->get()->mBase->mData.mType; const std::string weaponBonename = MWMechanics::getWeaponType(weaponType)->mAttachBone; if (weaponBonename != bonename) { const NodeMap& nodeMap = getNodeMap(); NodeMap::const_iterator found = nodeMap.find(Misc::StringUtils::lowerCase(weaponBonename)); if (found != nodeMap.end()) bonename = weaponBonename; } } } // PRT_Hair seems to be the only type that breaks consistency and uses a filter that's different from the attachment bone const std::string bonefilter = (type == ESM::PRT_Hair) ? "hair" : bonename; mObjectParts[type] = insertBoundedPart(mesh, bonename, bonefilter, enchantedGlow, glowColor); } catch (std::exception& e) { Log(Debug::Error) << "Error adding NPC part: " << e.what(); return false; } if (!mSoundsDisabled) { const MWWorld::InventoryStore& inv = mPtr.getClass().getInventoryStore(mPtr); MWWorld::ConstContainerStoreIterator csi = inv.getSlot(group < 0 ? MWWorld::InventoryStore::Slot_Helmet : group); if (csi != inv.end()) { const auto soundId = csi->getClass().getSound(*csi); if (!soundId.empty()) { mSounds[type] = MWBase::Environment::get().getSoundManager()->playSound3D(mPtr, soundId, 1.0f, 1.0f, MWSound::Type::Sfx, MWSound::PlayMode::Loop ); } } } osg::Node* node = mObjectParts[type]->getNode(); if (node->getNumChildrenRequiringUpdateTraversal() > 0) { std::shared_ptr src; if (type == ESM::PRT_Head) { src = mHeadAnimationTime; if (node->getUserDataContainer()) { for (unsigned int i=0; igetUserDataContainer()->getNumUserObjects(); ++i) { osg::Object* obj = node->getUserDataContainer()->getUserObject(i); if (SceneUtil::TextKeyMapHolder* keys = dynamic_cast(obj)) { for (const auto &key : keys->mTextKeys) { if (Misc::StringUtils::ciEqual(key.second, "talk: start")) mHeadAnimationTime->setTalkStart(key.first); if (Misc::StringUtils::ciEqual(key.second, "talk: stop")) mHeadAnimationTime->setTalkStop(key.first); if (Misc::StringUtils::ciEqual(key.second, "blink: start")) mHeadAnimationTime->setBlinkStart(key.first); if (Misc::StringUtils::ciEqual(key.second, "blink: stop")) mHeadAnimationTime->setBlinkStop(key.first); } break; } } } } else if (type == ESM::PRT_Weapon) src = mWeaponAnimationTime; else src.reset(new NullAnimationTime); SceneUtil::AssignControllerSourcesVisitor assignVisitor(src); node->accept(assignVisitor); } return true; } void NpcAnimation::addPartGroup(int group, int priority, const std::vector &parts, bool enchantedGlow, osg::Vec4f* glowColor) { const MWWorld::ESMStore &store = MWBase::Environment::get().getWorld()->getStore(); const MWWorld::Store &partStore = store.get(); const char *ext = (mViewMode == VM_FirstPerson) ? ".1st" : ""; for(const ESM::PartReference& part : parts) { const ESM::BodyPart *bodypart = nullptr; if(!mNpc->isMale() && !part.mFemale.empty()) { bodypart = partStore.search(part.mFemale+ext); if(!bodypart && mViewMode == VM_FirstPerson) { bodypart = partStore.search(part.mFemale); if(bodypart && !(bodypart->mData.mPart == ESM::BodyPart::MP_Hand || bodypart->mData.mPart == ESM::BodyPart::MP_Wrist || bodypart->mData.mPart == ESM::BodyPart::MP_Forearm || bodypart->mData.mPart == ESM::BodyPart::MP_Upperarm)) bodypart = nullptr; } else if (!bodypart) Log(Debug::Warning) << "Warning: Failed to find body part '" << part.mFemale << "'"; } if(!bodypart && !part.mMale.empty()) { bodypart = partStore.search(part.mMale+ext); if(!bodypart && mViewMode == VM_FirstPerson) { bodypart = partStore.search(part.mMale); if(bodypart && !(bodypart->mData.mPart == ESM::BodyPart::MP_Hand || bodypart->mData.mPart == ESM::BodyPart::MP_Wrist || bodypart->mData.mPart == ESM::BodyPart::MP_Forearm || bodypart->mData.mPart == ESM::BodyPart::MP_Upperarm)) bodypart = nullptr; } else if (!bodypart) Log(Debug::Warning) << "Warning: Failed to find body part '" << part.mMale << "'"; } if(bodypart) addOrReplaceIndividualPart((ESM::PartReferenceType)part.mPart, group, priority, "meshes\\"+bodypart->mModel, enchantedGlow, glowColor); else reserveIndividualPart((ESM::PartReferenceType)part.mPart, group, priority); } } void NpcAnimation::addControllers() { Animation::addControllers(); mFirstPersonNeckController = nullptr; WeaponAnimation::deleteControllers(); if (mViewMode == VM_FirstPerson) { NodeMap::iterator found = mNodeMap.find("bip01 neck"); if (found != mNodeMap.end()) { osg::MatrixTransform* node = found->second.get(); mFirstPersonNeckController = new NeckController(mObjectRoot.get()); node->addUpdateCallback(mFirstPersonNeckController); mActiveControllers.emplace_back(node, mFirstPersonNeckController); } } else if (mViewMode == VM_Normal) { WeaponAnimation::addControllers(mNodeMap, mActiveControllers, mObjectRoot.get()); } } void NpcAnimation::showWeapons(bool showWeapon) { mShowWeapons = showWeapon; mAmmunition.reset(); if(showWeapon) { const MWWorld::InventoryStore& inv = mPtr.getClass().getInventoryStore(mPtr); MWWorld::ConstContainerStoreIterator weapon = inv.getSlot(MWWorld::InventoryStore::Slot_CarriedRight); if(weapon != inv.end()) { osg::Vec4f glowColor = weapon->getClass().getEnchantmentColor(*weapon); std::string mesh = weapon->getClass().getModel(*weapon); addOrReplaceIndividualPart(ESM::PRT_Weapon, MWWorld::InventoryStore::Slot_CarriedRight, 1, mesh, !weapon->getClass().getEnchantment(*weapon).empty(), &glowColor); // Crossbows start out with a bolt attached if (weapon->getTypeName() == typeid(ESM::Weapon).name() && weapon->get()->mBase->mData.mType == ESM::Weapon::MarksmanCrossbow) { int ammotype = MWMechanics::getWeaponType(ESM::Weapon::MarksmanCrossbow)->mAmmoType; MWWorld::ConstContainerStoreIterator ammo = inv.getSlot(MWWorld::InventoryStore::Slot_Ammunition); if (ammo != inv.end() && ammo->get()->mBase->mData.mType == ammotype) attachArrow(); } } } else { removeIndividualPart(ESM::PRT_Weapon); // If we remove/hide weapon from player, we should reset attack animation as well if (mPtr == MWMechanics::getPlayer()) MWBase::Environment::get().getWorld()->getPlayer().setAttackingOrSpell(false); } updateHolsteredWeapon(!mShowWeapons); updateQuiver(); } void NpcAnimation::showCarriedLeft(bool show) { mShowCarriedLeft = show; const MWWorld::InventoryStore& inv = mPtr.getClass().getInventoryStore(mPtr); MWWorld::ConstContainerStoreIterator iter = inv.getSlot(MWWorld::InventoryStore::Slot_CarriedLeft); if(show && iter != inv.end()) { osg::Vec4f glowColor = iter->getClass().getEnchantmentColor(*iter); std::string mesh = iter->getClass().getModel(*iter); // For shields we must try to use the body part model if (iter->getTypeName() == typeid(ESM::Armor).name()) { const ESM::Armor *armor = iter->get()->mBase; const std::vector& bodyparts = armor->mParts.mParts; if (!bodyparts.empty()) mesh = getShieldBodypartMesh(bodyparts, !mNpc->isMale()); } if (mesh.empty() || addOrReplaceIndividualPart(ESM::PRT_Shield, MWWorld::InventoryStore::Slot_CarriedLeft, 1, mesh, !iter->getClass().getEnchantment(*iter).empty(), &glowColor)) { if (mesh.empty()) reserveIndividualPart(ESM::PRT_Shield, MWWorld::InventoryStore::Slot_CarriedLeft, 1); if (iter->getTypeName() == typeid(ESM::Light).name() && mObjectParts[ESM::PRT_Shield]) addExtraLight(mObjectParts[ESM::PRT_Shield]->getNode()->asGroup(), iter->get()->mBase); } } else removeIndividualPart(ESM::PRT_Shield); updateHolsteredShield(mShowCarriedLeft); } void NpcAnimation::attachArrow() { WeaponAnimation::attachArrow(mPtr); const MWWorld::InventoryStore& inv = mPtr.getClass().getInventoryStore(mPtr); MWWorld::ConstContainerStoreIterator ammo = inv.getSlot(MWWorld::InventoryStore::Slot_Ammunition); if (ammo != inv.end() && !ammo->getClass().getEnchantment(*ammo).empty()) { osg::Group* bone = getArrowBone(); if (bone != nullptr && bone->getNumChildren()) SceneUtil::addEnchantedGlow(bone->getChild(0), mResourceSystem, ammo->getClass().getEnchantmentColor(*ammo)); } updateQuiver(); } void NpcAnimation::detachArrow() { WeaponAnimation::detachArrow(mPtr); updateQuiver(); } void NpcAnimation::releaseArrow(float attackStrength) { WeaponAnimation::releaseArrow(mPtr, attackStrength); updateQuiver(); } osg::Group* NpcAnimation::getArrowBone() { PartHolderPtr part = mObjectParts[ESM::PRT_Weapon]; if (!part) return nullptr; const MWWorld::InventoryStore& inv = mPtr.getClass().getInventoryStore(mPtr); MWWorld::ConstContainerStoreIterator weapon = inv.getSlot(MWWorld::InventoryStore::Slot_CarriedRight); if(weapon == inv.end() || weapon->getTypeName() != typeid(ESM::Weapon).name()) return nullptr; int type = weapon->get()->mBase->mData.mType; int ammoType = MWMechanics::getWeaponType(type)->mAmmoType; // Try to find and attachment bone in actor's skeleton, otherwise fall back to the ArrowBone in weapon's mesh osg::Group* bone = getBoneByName(MWMechanics::getWeaponType(ammoType)->mAttachBone); if (bone == nullptr) { SceneUtil::FindByNameVisitor findVisitor ("ArrowBone"); part->getNode()->accept(findVisitor); bone = findVisitor.mFoundNode; } return bone; } osg::Node* NpcAnimation::getWeaponNode() { PartHolderPtr part = mObjectParts[ESM::PRT_Weapon]; if (!part) return nullptr; return part->getNode(); } Resource::ResourceSystem* NpcAnimation::getResourceSystem() { return mResourceSystem; } void NpcAnimation::permanentEffectAdded(const ESM::MagicEffect *magicEffect, bool isNew) { // During first auto equip, we don't play any sounds. // Basically we don't want sounds when the actor is first loaded, // the items should appear as if they'd always been equipped. if (isNew) { static const std::string schools[] = { "alteration", "conjuration", "destruction", "illusion", "mysticism", "restoration" }; MWBase::SoundManager *sndMgr = MWBase::Environment::get().getSoundManager(); if(!magicEffect->mHitSound.empty()) sndMgr->playSound3D(mPtr, magicEffect->mHitSound, 1.0f, 1.0f); else sndMgr->playSound3D(mPtr, schools[magicEffect->mData.mSchool]+" hit", 1.0f, 1.0f); } if (!magicEffect->mHit.empty()) { const ESM::Static* castStatic = MWBase::Environment::get().getWorld()->getStore().get().find (magicEffect->mHit); bool loop = (magicEffect->mData.mFlags & ESM::MagicEffect::ContinuousVfx) != 0; // Don't play particle VFX unless the effect is new or it should be looping. if (isNew || loop) addEffect("meshes\\" + castStatic->mModel, magicEffect->mIndex, loop, "", magicEffect->mParticle); } } void NpcAnimation::enableHeadAnimation(bool enable) { mHeadAnimationTime->setEnabled(enable); } void NpcAnimation::setWeaponGroup(const std::string &group, bool relativeDuration) { mWeaponAnimationTime->setGroup(group, relativeDuration); } void NpcAnimation::equipmentChanged() { static const bool shieldSheathing = Settings::Manager::getBool("shield sheathing", "Game"); if (shieldSheathing) { int weaptype = ESM::Weapon::None; MWMechanics::getActiveWeapon(mPtr, &weaptype); showCarriedLeft(updateCarriedLeftVisible(weaptype)); } updateParts(); } void NpcAnimation::setVampire(bool vampire) { if (mNpcType == Type_Werewolf) // we can't have werewolf vampires, can we return; if ((mNpcType == Type_Vampire) != vampire) { if (mPtr == MWMechanics::getPlayer()) MWBase::Environment::get().getWorld()->reattachPlayerCamera(); else rebuild(); } } void NpcAnimation::setFirstPersonOffset(const osg::Vec3f &offset) { mFirstPersonOffset = offset; } void NpcAnimation::updatePtr(const MWWorld::Ptr &updated) { Animation::updatePtr(updated); mHeadAnimationTime->updatePtr(updated); } // Remember body parts so we only have to search through the store once for each race/gender/viewmode combination typedef std::map< std::pair,std::vector > RaceMapping; static RaceMapping sRaceMapping; const std::vector& NpcAnimation::getBodyParts(const std::string &race, bool female, bool firstPerson, bool werewolf) { static const int Flag_FirstPerson = 1<<1; static const int Flag_Female = 1<<0; int flags = (werewolf ? -1 : 0); if(female) flags |= Flag_Female; if(firstPerson) flags |= Flag_FirstPerson; RaceMapping::iterator found = sRaceMapping.find(std::make_pair(race, flags)); if (found != sRaceMapping.end()) return found->second; else { std::vector& parts = sRaceMapping[std::make_pair(race, flags)]; typedef std::multimap BodyPartMapType; static const BodyPartMapType sBodyPartMap = { {ESM::BodyPart::MP_Neck, ESM::PRT_Neck}, {ESM::BodyPart::MP_Chest, ESM::PRT_Cuirass}, {ESM::BodyPart::MP_Groin, ESM::PRT_Groin}, {ESM::BodyPart::MP_Hand, ESM::PRT_RHand}, {ESM::BodyPart::MP_Hand, ESM::PRT_LHand}, {ESM::BodyPart::MP_Wrist, ESM::PRT_RWrist}, {ESM::BodyPart::MP_Wrist, ESM::PRT_LWrist}, {ESM::BodyPart::MP_Forearm, ESM::PRT_RForearm}, {ESM::BodyPart::MP_Forearm, ESM::PRT_LForearm}, {ESM::BodyPart::MP_Upperarm, ESM::PRT_RUpperarm}, {ESM::BodyPart::MP_Upperarm, ESM::PRT_LUpperarm}, {ESM::BodyPart::MP_Foot, ESM::PRT_RFoot}, {ESM::BodyPart::MP_Foot, ESM::PRT_LFoot}, {ESM::BodyPart::MP_Ankle, ESM::PRT_RAnkle}, {ESM::BodyPart::MP_Ankle, ESM::PRT_LAnkle}, {ESM::BodyPart::MP_Knee, ESM::PRT_RKnee}, {ESM::BodyPart::MP_Knee, ESM::PRT_LKnee}, {ESM::BodyPart::MP_Upperleg, ESM::PRT_RLeg}, {ESM::BodyPart::MP_Upperleg, ESM::PRT_LLeg}, {ESM::BodyPart::MP_Tail, ESM::PRT_Tail} }; parts.resize(ESM::PRT_Count, nullptr); if (werewolf) return parts; const MWWorld::ESMStore &store = MWBase::Environment::get().getWorld()->getStore(); for(const ESM::BodyPart& bodypart : store.get()) { if (bodypart.mData.mFlags & ESM::BodyPart::BPF_NotPlayable) continue; if (bodypart.mData.mType != ESM::BodyPart::MT_Skin) continue; if (!Misc::StringUtils::ciEqual(bodypart.mRace, race)) continue; bool partFirstPerson = isFirstPersonPart(&bodypart); bool isHand = bodypart.mData.mPart == ESM::BodyPart::MP_Hand || bodypart.mData.mPart == ESM::BodyPart::MP_Wrist || bodypart.mData.mPart == ESM::BodyPart::MP_Forearm || bodypart.mData.mPart == ESM::BodyPart::MP_Upperarm; bool isSameGender = isFemalePart(&bodypart) == female; /* A fallback for the arms if 1st person is missing: 1. Try to use 3d person skin for same gender 2. Try to use 1st person skin for male, if female == true 3. Try to use 3d person skin for male, if female == true A fallback in another cases: allow to use male bodyparts, if female == true */ if (firstPerson && isHand && !partFirstPerson) { // Allow 3rd person skins as a fallback for the arms if 1st person is missing BodyPartMapType::const_iterator bIt = sBodyPartMap.lower_bound(BodyPartMapType::key_type(bodypart.mData.mPart)); while(bIt != sBodyPartMap.end() && bIt->first == bodypart.mData.mPart) { // If we have no fallback bodypart now and bodypart is for same gender (1) if(!parts[bIt->second] && isSameGender) parts[bIt->second] = &bodypart; // If we have fallback bodypart for other gender and found fallback for current gender (1) else if(isSameGender && isFemalePart(parts[bIt->second]) != female) parts[bIt->second] = &bodypart; // If we have no fallback bodypart and searching for female bodyparts (3) else if(!parts[bIt->second] && female) parts[bIt->second] = &bodypart; ++bIt; } continue; } // Don't allow to use podyparts for a different view if (partFirstPerson != firstPerson) continue; if (female && !isFemalePart(&bodypart)) { // Allow male parts as fallback for females if female parts are missing BodyPartMapType::const_iterator bIt = sBodyPartMap.lower_bound(BodyPartMapType::key_type(bodypart.mData.mPart)); while(bIt != sBodyPartMap.end() && bIt->first == bodypart.mData.mPart) { // If we have no fallback bodypart now if(!parts[bIt->second]) parts[bIt->second] = &bodypart; // If we have 3d person fallback bodypart for hand and 1st person fallback found (2) else if(isHand && !isFirstPersonPart(parts[bIt->second]) && partFirstPerson) parts[bIt->second] = &bodypart; ++bIt; } continue; } // Don't allow to use podyparts for another gender if (female != isFemalePart(&bodypart)) continue; // Use properly found bodypart, replacing fallbacks BodyPartMapType::const_iterator bIt = sBodyPartMap.lower_bound(BodyPartMapType::key_type(bodypart.mData.mPart)); while(bIt != sBodyPartMap.end() && bIt->first == bodypart.mData.mPart) { parts[bIt->second] = &bodypart; ++bIt; } } return parts; } } void NpcAnimation::setAccurateAiming(bool enabled) { mAccurateAiming = enabled; } bool NpcAnimation::isArrowAttached() const { return mAmmunition != nullptr; } } ================================================ FILE: apps/openmw/mwrender/npcanimation.hpp ================================================ #ifndef GAME_RENDER_NPCANIMATION_H #define GAME_RENDER_NPCANIMATION_H #include "animation.hpp" #include "../mwworld/inventorystore.hpp" #include "actoranimation.hpp" #include "weaponanimation.hpp" #include namespace ESM { struct NPC; struct BodyPart; } namespace MWSound { class Sound; } namespace MWRender { class NeckController; class HeadAnimationTime; class NpcAnimation : public ActorAnimation, public WeaponAnimation, public MWWorld::InventoryStoreListener { public: void equipmentChanged() override; void permanentEffectAdded(const ESM::MagicEffect *magicEffect, bool isNew) override; public: typedef std::map PartBoneMap; enum ViewMode { VM_Normal, VM_FirstPerson, VM_HeadOnly }; private: static const PartBoneMap sPartList; // Bounded Parts PartHolderPtr mObjectParts[ESM::PRT_Count]; std::array mSounds; const ESM::NPC *mNpc; std::string mHeadModel; std::string mHairModel; ViewMode mViewMode; bool mShowWeapons; bool mShowCarriedLeft; enum NpcType { Type_Normal, Type_Werewolf, Type_Vampire }; NpcType mNpcType; int mPartslots[ESM::PRT_Count]; //Each part slot is taken by clothing, armor, or is empty int mPartPriorities[ESM::PRT_Count]; osg::Vec3f mFirstPersonOffset; // Field of view to use when rendering first person meshes float mFirstPersonFieldOfView; std::shared_ptr mHeadAnimationTime; std::shared_ptr mWeaponAnimationTime; bool mSoundsDisabled; bool mAccurateAiming; float mAimingFactor; void updateNpcBase(); NpcType getNpcType() const; PartHolderPtr insertBoundedPart(const std::string &model, const std::string &bonename, const std::string &bonefilter, bool enchantedGlow, osg::Vec4f* glowColor=nullptr); void removeIndividualPart(ESM::PartReferenceType type); void reserveIndividualPart(ESM::PartReferenceType type, int group, int priority); bool addOrReplaceIndividualPart(ESM::PartReferenceType type, int group, int priority, const std::string &mesh, bool enchantedGlow=false, osg::Vec4f* glowColor=nullptr); void removePartGroup(int group); void addPartGroup(int group, int priority, const std::vector &parts, bool enchantedGlow=false, osg::Vec4f* glowColor=nullptr); void setRenderBin(); osg::ref_ptr mFirstPersonNeckController; static bool isFirstPersonPart(const ESM::BodyPart* bodypart); static bool isFemalePart(const ESM::BodyPart* bodypart); static NpcType getNpcType(const MWWorld::Ptr& ptr); protected: void addControllers() override; bool isArrowAttached() const override; std::string getShieldMesh(MWWorld::ConstPtr shield) const override; public: /** * @param ptr * @param disableListener Don't listen for equipment changes and magic effects. InventoryStore only supports * one listener at a time, so you shouldn't do this if creating several NpcAnimations * for the same Ptr, eg preview dolls for the player. * Those need to be manually rendered anyway. * @param disableSounds Same as \a disableListener but for playing items sounds * @param viewMode */ NpcAnimation(const MWWorld::Ptr& ptr, osg::ref_ptr parentNode, Resource::ResourceSystem* resourceSystem, bool disableSounds = false, ViewMode viewMode=VM_Normal, float firstPersonFieldOfView=55.f); virtual ~NpcAnimation(); void enableHeadAnimation(bool enable) override; /// 1: the first person meshes follow the camera's rotation completely /// 0: the first person meshes follow the camera with a reduced factor, so you can look down at your own hands void setAccurateAiming(bool enabled) override; void setWeaponGroup(const std::string& group, bool relativeDuration) override; osg::Vec3f runAnimation(float timepassed) override; /// A relative factor (0-1) that decides if and how much the skeleton should be pitched /// to indicate the facing orientation of the character. void setPitchFactor(float factor) override { mPitchFactor = factor; } void showWeapons(bool showWeapon) override; bool getCarriedLeftShown() const override { return mShowCarriedLeft; } void showCarriedLeft(bool show) override; void attachArrow() override; void detachArrow() override; void releaseArrow(float attackStrength) override; osg::Group* getArrowBone() override; osg::Node* getWeaponNode() override; Resource::ResourceSystem* getResourceSystem() override; // WeaponAnimation void showWeapon(bool show) override { showWeapons(show); } void setViewMode(ViewMode viewMode); void updateParts(); /// Rebuilds the NPC, updating their root model, animation sources, and equipment. void rebuild(); /// Get the inventory slot that the given node path leads into, or -1 if not found. int getSlot(const osg::NodePath& path) const; void setVampire(bool vampire) override; /// Set a translation offset (in object root space) to apply to meshes when in first person mode. void setFirstPersonOffset(const osg::Vec3f& offset); void updatePtr(const MWWorld::Ptr& updated) override; /// Get a list of body parts that may be used by an NPC of given race and gender. /// @note This is a fixed size list, one list item for each ESM::PartReferenceType, may contain nullptr body parts. static const std::vector& getBodyParts(const std::string& raceId, bool female, bool firstperson, bool werewolf); }; } #endif ================================================ FILE: apps/openmw/mwrender/objectpaging.cpp ================================================ #include "objectpaging.hpp" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "apps/openmw/mwworld/esmstore.hpp" #include "apps/openmw/mwbase/environment.hpp" #include "apps/openmw/mwbase/world.hpp" #include "vismask.hpp" namespace MWRender { bool typeFilter(int type, bool far) { switch (type) { case ESM::REC_STAT: case ESM::REC_ACTI: case ESM::REC_DOOR: return true; case ESM::REC_CONT: return !far; default: return false; } } std::string getModel(int type, const std::string& id, const MWWorld::ESMStore& store) { switch (type) { case ESM::REC_STAT: return store.get().searchStatic(id)->mModel; case ESM::REC_ACTI: return store.get().searchStatic(id)->mModel; case ESM::REC_DOOR: return store.get().searchStatic(id)->mModel; case ESM::REC_CONT: return store.get().searchStatic(id)->mModel; default: return std::string(); } } osg::ref_ptr ObjectPaging::getChunk(float size, const osg::Vec2f& center, unsigned char lod, unsigned int lodFlags, bool activeGrid, const osg::Vec3f& viewPoint, bool compile) { if (activeGrid && !mActiveGrid) return nullptr; ChunkId id = std::make_tuple(center, size, activeGrid); osg::ref_ptr obj = mCache->getRefFromObjectCache(id); if (obj) return obj->asNode(); else { osg::ref_ptr node = createChunk(size, center, activeGrid, viewPoint, compile); mCache->addEntryToObjectCache(id, node.get()); return node; } } class CanOptimizeCallback : public SceneUtil::Optimizer::IsOperationPermissibleForObjectCallback { public: bool isOperationPermissibleForObjectImplementation(const SceneUtil::Optimizer* optimizer, const osg::Drawable* node,unsigned int option) const override { return true; } bool isOperationPermissibleForObjectImplementation(const SceneUtil::Optimizer* optimizer, const osg::Node* node,unsigned int option) const override { return (node->getDataVariance() != osg::Object::DYNAMIC); } }; class CopyOp : public osg::CopyOp { public: bool mOptimizeBillboards = true; float mSqrDistance = 0.f; osg::Vec3f mViewVector; osg::Node::NodeMask mCopyMask = ~0u; mutable std::vector mNodePath; void copy(const osg::Node* toCopy, osg::Group* attachTo) { const osg::Group* groupToCopy = toCopy->asGroup(); if (toCopy->getStateSet() || toCopy->asTransform() || !groupToCopy) attachTo->addChild(operator()(toCopy)); else { for (unsigned int i=0; igetNumChildren(); ++i) attachTo->addChild(operator()(groupToCopy->getChild(i))); } } osg::Node* operator() (const osg::Node* node) const override { if (!(node->getNodeMask() & mCopyMask)) return nullptr; if (const osg::Drawable* d = node->asDrawable()) return operator()(d); if (dynamic_cast(node)) return nullptr; if (dynamic_cast(node)) return nullptr; if (const osg::Switch* sw = node->asSwitch()) { osg::Group* n = new osg::Group; for (unsigned int i=0; igetNumChildren(); ++i) if (sw->getValue(i)) n->addChild(operator()(sw->getChild(i))); n->setDataVariance(osg::Object::STATIC); return n; } if (const osg::LOD* lod = dynamic_cast(node)) { osg::Group* n = new osg::Group; for (unsigned int i=0; igetNumChildren(); ++i) if (lod->getMinRange(i) * lod->getMinRange(i) <= mSqrDistance && mSqrDistance < lod->getMaxRange(i) * lod->getMaxRange(i)) n->addChild(operator()(lod->getChild(i))); n->setDataVariance(osg::Object::STATIC); return n; } mNodePath.push_back(node); osg::Node* cloned = static_cast(node->clone(*this)); cloned->setDataVariance(osg::Object::STATIC); cloned->setUserDataContainer(nullptr); cloned->setName(""); mNodePath.pop_back(); handleCallbacks(node, cloned); return cloned; } void handleCallbacks(const osg::Node* node, osg::Node *cloned) const { for (const osg::Callback* callback = node->getCullCallback(); callback != nullptr; callback = callback->getNestedCallback()) { if (callback->className() == std::string("BillboardCallback")) { if (mOptimizeBillboards) { handleBillboard(cloned); continue; } else cloned->setDataVariance(osg::Object::DYNAMIC); } if (node->getCullCallback()->getNestedCallback()) { osg::Callback *clonedCallback = osg::clone(callback, osg::CopyOp::SHALLOW_COPY); clonedCallback->setNestedCallback(nullptr); cloned->addCullCallback(clonedCallback); } else cloned->addCullCallback(const_cast(callback)); } } void handleBillboard(osg::Node* node) const { osg::Transform* transform = node->asTransform(); if (!transform) return; osg::MatrixTransform* matrixTransform = transform->asMatrixTransform(); if (!matrixTransform) return; osg::Matrix worldToLocal = osg::Matrix::identity(); for (auto pathNode : mNodePath) if (const osg::Transform* t = pathNode->asTransform()) t->computeWorldToLocalMatrix(worldToLocal, nullptr); worldToLocal = osg::Matrix::orthoNormal(worldToLocal); osg::Matrix billboardMatrix; osg::Vec3f viewVector = -(mViewVector + worldToLocal.getTrans()); viewVector.normalize(); osg::Vec3f right = viewVector ^ osg::Vec3f(0,0,1); right.normalize(); osg::Vec3f up = right ^ viewVector; up.normalize(); billboardMatrix.makeLookAt(osg::Vec3f(0,0,0), viewVector, up); billboardMatrix.invert(billboardMatrix); const osg::Matrix& oldMatrix = matrixTransform->getMatrix(); float mag[3]; // attempt to preserve scale for (int i=0;i<3;++i) mag[i] = std::sqrt(oldMatrix(0,i) * oldMatrix(0,i) + oldMatrix(1,i) * oldMatrix(1,i) + oldMatrix(2,i) * oldMatrix(2,i)); osg::Matrix newMatrix; worldToLocal.setTrans(0,0,0); newMatrix *= worldToLocal; newMatrix.preMult(billboardMatrix); newMatrix.preMultScale(osg::Vec3f(mag[0], mag[1], mag[2])); newMatrix.setTrans(oldMatrix.getTrans()); matrixTransform->setMatrix(newMatrix); } osg::Drawable* operator() (const osg::Drawable* drawable) const override { if (!(drawable->getNodeMask() & mCopyMask)) return nullptr; if (dynamic_cast(drawable)) return nullptr; if (const SceneUtil::RigGeometry* rig = dynamic_cast(drawable)) return operator()(rig->getSourceGeometry()); if (const SceneUtil::MorphGeometry* morph = dynamic_cast(drawable)) return operator()(morph->getSourceGeometry()); if (getCopyFlags() & DEEP_COPY_DRAWABLES) { osg::Drawable* d = static_cast(drawable->clone(*this)); d->setDataVariance(osg::Object::STATIC); d->setUserDataContainer(nullptr); d->setName(""); return d; } else return const_cast(drawable); } osg::Callback* operator() (const osg::Callback* callback) const override { return nullptr; } }; class RefnumSet : public osg::Object { public: RefnumSet(){} RefnumSet(const RefnumSet& copy, const osg::CopyOp&) : mRefnums(copy.mRefnums) {} META_Object(MWRender, RefnumSet) std::set mRefnums; }; class AnalyzeVisitor : public osg::NodeVisitor { public: AnalyzeVisitor(osg::Node::NodeMask analyzeMask) : osg::NodeVisitor(TRAVERSE_ALL_CHILDREN) , mCurrentStateSet(nullptr) , mCurrentDistance(0.f) , mAnalyzeMask(analyzeMask) {} typedef std::unordered_map StateSetCounter; struct Result { StateSetCounter mStateSetCounter; unsigned int mNumVerts = 0; }; void apply(osg::Node& node) override { if (!(node.getNodeMask() & mAnalyzeMask)) return; if (node.getStateSet()) mCurrentStateSet = node.getStateSet(); if (osg::Switch* sw = node.asSwitch()) { for (unsigned int i=0; igetNumChildren(); ++i) if (sw->getValue(i)) traverse(*sw->getChild(i)); return; } if (osg::LOD* lod = dynamic_cast(&node)) { for (unsigned int i=0; igetNumChildren(); ++i) if (lod->getMinRange(i) * lod->getMinRange(i) <= mCurrentDistance && mCurrentDistance < lod->getMaxRange(i) * lod->getMaxRange(i)) traverse(*lod->getChild(i)); return; } traverse(node); } void apply(osg::Geometry& geom) override { if (!(geom.getNodeMask() & mAnalyzeMask)) return; if (osg::Array* array = geom.getVertexArray()) mResult.mNumVerts += array->getNumElements(); ++mResult.mStateSetCounter[mCurrentStateSet]; ++mGlobalStateSetCounter[mCurrentStateSet]; } Result retrieveResult() { Result result = mResult; mResult = Result(); mCurrentStateSet = nullptr; return result; } void addInstance(const Result& result) { for (auto pair : result.mStateSetCounter) mGlobalStateSetCounter[pair.first] += pair.second; } float getMergeBenefit(const Result& result) { if (result.mStateSetCounter.empty()) return 1; float mergeBenefit = 0; for (auto pair : result.mStateSetCounter) { mergeBenefit += mGlobalStateSetCounter[pair.first]; } mergeBenefit /= result.mStateSetCounter.size(); return mergeBenefit; } Result mResult; osg::StateSet* mCurrentStateSet; StateSetCounter mGlobalStateSetCounter; float mCurrentDistance; osg::Node::NodeMask mAnalyzeMask; }; class DebugVisitor : public osg::NodeVisitor { public: DebugVisitor() : osg::NodeVisitor(TRAVERSE_ALL_CHILDREN) {} void apply(osg::Drawable& node) override { osg::ref_ptr m (new osg::Material); osg::Vec4f color(Misc::Rng::rollProbability(), Misc::Rng::rollProbability(), Misc::Rng::rollProbability(), 0.f); color.normalize(); m->setDiffuse(osg::Material::FRONT_AND_BACK, osg::Vec4f(0.1f,0.1f,0.1f,1.f)); m->setAmbient(osg::Material::FRONT_AND_BACK, osg::Vec4f(0.1f,0.1f,0.1f,1.f)); m->setColorMode(osg::Material::OFF); m->setEmission(osg::Material::FRONT_AND_BACK, osg::Vec4f(color)); osg::ref_ptr stateset = node.getStateSet() ? osg::clone(node.getStateSet(), osg::CopyOp::SHALLOW_COPY) : new osg::StateSet; stateset->setAttribute(m); stateset->addUniform(new osg::Uniform("colorMode", 0)); stateset->addUniform(new osg::Uniform("emissiveMult", 1.f)); node.setStateSet(stateset); } }; class AddRefnumMarkerVisitor : public osg::NodeVisitor { public: AddRefnumMarkerVisitor(const ESM::RefNum &refnum) : osg::NodeVisitor(TRAVERSE_ALL_CHILDREN), mRefnum(refnum) {} ESM::RefNum mRefnum; void apply(osg::Geometry &node) override { osg::ref_ptr marker (new RefnumMarker); marker->mRefnum = mRefnum; if (osg::Array* array = node.getVertexArray()) marker->mNumVertices = array->getNumElements(); node.getOrCreateUserDataContainer()->addUserObject(marker); } }; ObjectPaging::ObjectPaging(Resource::SceneManager* sceneManager) : GenericResourceManager(nullptr) , mSceneManager(sceneManager) , mRefTrackerLocked(false) { mActiveGrid = Settings::Manager::getBool("object paging active grid", "Terrain"); mDebugBatches = Settings::Manager::getBool("object paging debug batches", "Terrain"); mMergeFactor = Settings::Manager::getFloat("object paging merge factor", "Terrain"); mMinSize = Settings::Manager::getFloat("object paging min size", "Terrain"); mMinSizeMergeFactor = Settings::Manager::getFloat("object paging min size merge factor", "Terrain"); mMinSizeCostMultiplier = Settings::Manager::getFloat("object paging min size cost multiplier", "Terrain"); } osg::ref_ptr ObjectPaging::createChunk(float size, const osg::Vec2f& center, bool activeGrid, const osg::Vec3f& viewPoint, bool compile) { osg::Vec2i startCell = osg::Vec2i(std::floor(center.x() - size/2.f), std::floor(center.y() - size/2.f)); osg::Vec3f worldCenter = osg::Vec3f(center.x(), center.y(), 0)*ESM::Land::REAL_SIZE; osg::Vec3f relativeViewPoint = viewPoint - worldCenter; std::map refs; std::vector esm; const MWWorld::ESMStore& store = MWBase::Environment::get().getWorld()->getStore(); for (int cellX = startCell.x(); cellX < startCell.x() + size; ++cellX) { for (int cellY = startCell.y(); cellY < startCell.y() + size; ++cellY) { const ESM::Cell* cell = store.get().searchStatic(cellX, cellY); if (!cell) continue; for (size_t i=0; imContextList.size(); ++i) { try { unsigned int index = cell->mContextList[i].index; if (esm.size()<=index) esm.resize(index+1); cell->restore(esm[index], i); ESM::CellRef ref; ref.mRefNum.mContentFile = ESM::RefNum::RefNum_NoContentFile; bool deleted = false; while(cell->getNextRef(esm[index], ref, deleted)) { if (std::find(cell->mMovedRefs.begin(), cell->mMovedRefs.end(), ref.mRefNum) != cell->mMovedRefs.end()) continue; Misc::StringUtils::lowerCaseInPlace(ref.mRefID); 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); } } catch (std::exception&) { continue; } } for (auto [ref, deleted] : cell->mLeasedRefs) { if (deleted) { refs.erase(ref.mRefNum); continue; } Misc::StringUtils::lowerCaseInPlace(ref.mRefID); int type = store.findStatic(ref.mRefID); if (!typeFilter(type,size>=2)) continue; refs[ref.mRefNum] = std::move(ref); } } } if (activeGrid) { std::lock_guard lock(mRefTrackerMutex); for (auto ref : getRefTracker().mBlacklist) refs.erase(ref); } osg::Vec2f minBound = (center - osg::Vec2f(size/2.f, size/2.f)); osg::Vec2f maxBound = (center + osg::Vec2f(size/2.f, size/2.f)); struct InstanceList { std::vector mInstances; AnalyzeVisitor::Result mAnalyzeResult; bool mNeedCompile = false; }; typedef std::map, InstanceList> NodeMap; NodeMap nodes; osg::ref_ptr refnumSet = activeGrid ? new RefnumSet : nullptr; // Mask_UpdateVisitor is used in such cases in NIF loader: // 1. For collision nodes, which is not supposed to be rendered. // 2. For nodes masked via Flag_Hidden (VisController can change this flag value at runtime). // Since ObjectPaging does not handle VisController, we can just ignore both types of nodes. constexpr auto copyMask = ~Mask_UpdateVisitor; AnalyzeVisitor analyzeVisitor(copyMask); analyzeVisitor.mCurrentDistance = (viewPoint - worldCenter).length2(); float minSize = mMinSize; if (mMinSizeMergeFactor) minSize *= mMinSizeMergeFactor; for (const auto& pair : refs) { const ESM::CellRef& ref = pair.second; osg::Vec3f pos = ref.mPos.asVec3(); if (size < 1.f) { osg::Vec3f cellPos = pos / ESM::Land::REAL_SIZE; if ((minBound.x() > std::floor(minBound.x()) && cellPos.x() < minBound.x()) || (minBound.y() > std::floor(minBound.y()) && cellPos.y() < minBound.y()) || (maxBound.x() < std::ceil(maxBound.x()) && cellPos.x() >= maxBound.x()) || (minBound.y() < std::ceil(maxBound.y()) && cellPos.y() >= maxBound.y())) continue; } float dSqr = (viewPoint - pos).length2(); if (!activeGrid) { std::lock_guard lock(mSizeCacheMutex); SizeCache::iterator found = mSizeCache.find(pair.first); if (found != mSizeCache.end() && found->second < dSqr*minSize*minSize) continue; } if (ref.mRefID == "prisonmarker" || ref.mRefID == "divinemarker" || ref.mRefID == "templemarker" || ref.mRefID == "northmarker") continue; // marker objects that have a hardcoded function in the game logic, should be hidden from the player int type = store.findStatic(ref.mRefID); std::string model = getModel(type, ref.mRefID, store); if (model.empty()) continue; model = "meshes/" + model; if (activeGrid && type != ESM::REC_STAT) { model = Misc::ResourceHelpers::correctActorModelPath(model, mSceneManager->getVFS()); std::string kfname = Misc::StringUtils::lowerCase(model); if(kfname.size() > 4 && kfname.compare(kfname.size()-4, 4, ".nif") == 0) { kfname.replace(kfname.size()-4, 4, ".kf"); if (mSceneManager->getVFS()->exists(kfname)) continue; } } osg::ref_ptr cnode = mSceneManager->getTemplate(model, false); if (activeGrid) { if (cnode->getNumChildrenRequiringUpdateTraversal() > 0 || SceneUtil::hasUserDescription(cnode, Constants::NightDayLabel) || SceneUtil::hasUserDescription(cnode, Constants::HerbalismLabel)) continue; else refnumSet->mRefnums.insert(pair.first); } { std::lock_guard lock(mRefTrackerMutex); if (getRefTracker().mDisabled.count(pair.first)) continue; } float radius2 = cnode->getBound().radius2() * ref.mScale*ref.mScale; if (radius2 < dSqr*minSize*minSize && !activeGrid) { std::lock_guard lock(mSizeCacheMutex); mSizeCache[pair.first] = radius2; continue; } auto emplaced = nodes.emplace(cnode, InstanceList()); if (emplaced.second) { const_cast(cnode.get())->accept(analyzeVisitor); // const-trickery required because there is no const version of NodeVisitor emplaced.first->second.mAnalyzeResult = analyzeVisitor.retrieveResult(); emplaced.first->second.mNeedCompile = compile && cnode->referenceCount() <= 3; } else analyzeVisitor.addInstance(emplaced.first->second.mAnalyzeResult); emplaced.first->second.mInstances.push_back(&ref); } osg::ref_ptr group = new osg::Group; osg::ref_ptr mergeGroup = new osg::Group; osg::ref_ptr templateRefs = new Resource::TemplateMultiRef; osgUtil::StateToCompile stateToCompile(0, nullptr); CopyOp copyop; copyop.mCopyMask = copyMask; for (const auto& pair : nodes) { const osg::Node* cnode = pair.first; const AnalyzeVisitor::Result& analyzeResult = pair.second.mAnalyzeResult; float mergeCost = analyzeResult.mNumVerts * size; float mergeBenefit = analyzeVisitor.getMergeBenefit(analyzeResult) * mMergeFactor; bool merge = mergeBenefit > mergeCost; float minSizeMerged = mMinSize; float factor2 = mergeBenefit > 0 ? std::min(1.f, mergeCost * mMinSizeCostMultiplier / mergeBenefit) : 1; float minSizeMergeFactor2 = (1-factor2) * mMinSizeMergeFactor + factor2; if (minSizeMergeFactor2 > 0) minSizeMerged *= minSizeMergeFactor2; unsigned int numinstances = 0; for (auto cref : pair.second.mInstances) { const ESM::CellRef& ref = *cref; osg::Vec3f pos = ref.mPos.asVec3(); if (!activeGrid && minSizeMerged != minSize && cnode->getBound().radius2() * cref->mScale*cref->mScale < (viewPoint-pos).length2()*minSizeMerged*minSizeMerged) continue; osg::Matrixf matrix; matrix.preMultTranslate(pos - worldCenter); matrix.preMultRotate( osg::Quat(ref.mPos.rot[2], osg::Vec3f(0,0,-1)) * osg::Quat(ref.mPos.rot[1], osg::Vec3f(0,-1,0)) * osg::Quat(ref.mPos.rot[0], osg::Vec3f(-1,0,0)) ); matrix.preMultScale(osg::Vec3f(ref.mScale, ref.mScale, ref.mScale)); osg::ref_ptr trans = new osg::MatrixTransform(matrix); trans->setDataVariance(osg::Object::STATIC); copyop.setCopyFlags(merge ? osg::CopyOp::DEEP_COPY_NODES|osg::CopyOp::DEEP_COPY_DRAWABLES : osg::CopyOp::DEEP_COPY_NODES); copyop.mOptimizeBillboards = (size > 1/4.f); copyop.mNodePath.push_back(trans); copyop.mSqrDistance = (viewPoint - pos).length2(); copyop.mViewVector = (viewPoint - worldCenter); copyop.copy(cnode, trans); copyop.mNodePath.pop_back(); if (activeGrid) { if (merge) { AddRefnumMarkerVisitor visitor(ref.mRefNum); trans->accept(visitor); } else { osg::ref_ptr marker = new RefnumMarker; marker->mRefnum = ref.mRefNum; trans->getOrCreateUserDataContainer()->addUserObject(marker); } } osg::Group* attachTo = merge ? mergeGroup : group; attachTo->addChild(trans); ++numinstances; } if (numinstances > 0) { // add a ref to the original template, to hint to the cache that it's still being used and should be kept in cache templateRefs->addRef(cnode); if (pair.second.mNeedCompile) { int mode = osgUtil::GLObjectsVisitor::COMPILE_STATE_ATTRIBUTES; if (!merge) mode |= osgUtil::GLObjectsVisitor::COMPILE_DISPLAY_LISTS; stateToCompile._mode = mode; const_cast(cnode)->accept(stateToCompile); } } } if (mergeGroup->getNumChildren()) { SceneUtil::Optimizer optimizer; if (size > 1/8.f) { optimizer.setViewPoint(relativeViewPoint); optimizer.setMergeAlphaBlending(true); } optimizer.setIsOperationPermissibleForObjectCallback(new CanOptimizeCallback); unsigned int options = SceneUtil::Optimizer::FLATTEN_STATIC_TRANSFORMS|SceneUtil::Optimizer::REMOVE_REDUNDANT_NODES|SceneUtil::Optimizer::MERGE_GEOMETRY; mSceneManager->shareState(mergeGroup); optimizer.optimize(mergeGroup, options); group->addChild(mergeGroup); if (mDebugBatches) { DebugVisitor dv; mergeGroup->accept(dv); } if (compile) { stateToCompile._mode = osgUtil::GLObjectsVisitor::COMPILE_DISPLAY_LISTS; mergeGroup->accept(stateToCompile); } } auto ico = mSceneManager->getIncrementalCompileOperation(); if (!stateToCompile.empty() && ico) { auto compileSet = new osgUtil::IncrementalCompileOperation::CompileSet(group); compileSet->buildCompileMap(ico->getContextSet(), stateToCompile); ico->add(compileSet, false); } group->getBound(); group->setNodeMask(Mask_Static); osg::UserDataContainer* udc = group->getOrCreateUserDataContainer(); if (activeGrid) { udc->addUserObject(refnumSet); group->addCullCallback(new SceneUtil::LightListCallback); } udc->addUserObject(templateRefs); return group; } unsigned int ObjectPaging::getNodeMask() { return Mask_Static; } struct ClearCacheFunctor { void operator()(MWRender::ChunkId id, osg::Object* obj) { if (intersects(id, mPosition)) mToClear.insert(id); } bool intersects(ChunkId id, osg::Vec3f pos) { if (mActiveGridOnly && !std::get<2>(id)) return false; pos /= ESM::Land::REAL_SIZE; clampToCell(pos); osg::Vec2f center = std::get<0>(id); float halfSize = std::get<1>(id)/2; return pos.x() >= center.x()-halfSize && pos.y() >= center.y()-halfSize && pos.x() <= center.x()+halfSize && pos.y() <= center.y()+halfSize; } 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(); } osg::Vec3f mPosition; osg::Vec2i mCell; std::set mToClear; bool mActiveGridOnly = false; }; bool ObjectPaging::enableObject(int type, const ESM::RefNum & refnum, const osg::Vec3f& pos, const osg::Vec2i& cell, bool enabled) { if (!typeFilter(type, false)) return false; { std::lock_guard lock(mRefTrackerMutex); if (enabled && !getWritableRefTracker().mDisabled.erase(refnum)) return false; if (!enabled && !getWritableRefTracker().mDisabled.insert(refnum).second) return false; if (mRefTrackerLocked) return false; } ClearCacheFunctor ccf; ccf.mPosition = pos; ccf.mCell = cell; mCache->call(ccf); if (ccf.mToClear.empty()) return false; for (const auto& chunk : ccf.mToClear) mCache->removeFromObjectCache(chunk); return true; } bool ObjectPaging::blacklistObject(int type, const ESM::RefNum & refnum, const osg::Vec3f& pos, const osg::Vec2i& cell) { if (!typeFilter(type, false)) return false; { std::lock_guard lock(mRefTrackerMutex); if (!getWritableRefTracker().mBlacklist.insert(refnum).second) return false; if (mRefTrackerLocked) return false; } ClearCacheFunctor ccf; ccf.mPosition = pos; ccf.mCell = cell; ccf.mActiveGridOnly = true; mCache->call(ccf); if (ccf.mToClear.empty()) return false; for (const auto& chunk : ccf.mToClear) mCache->removeFromObjectCache(chunk); return true; } void ObjectPaging::clear() { std::lock_guard lock(mRefTrackerMutex); mRefTrackerNew.mDisabled.clear(); mRefTrackerNew.mBlacklist.clear(); mRefTrackerLocked = true; } bool ObjectPaging::unlockCache() { if (!mRefTrackerLocked) return false; { std::lock_guard lock(mRefTrackerMutex); mRefTrackerLocked = false; if (mRefTracker == mRefTrackerNew) return false; else mRefTracker = mRefTrackerNew; } mCache->clear(); return true; } struct GetRefnumsFunctor { GetRefnumsFunctor(std::set& output) : mOutput(output) {} void operator()(MWRender::ChunkId chunkId, osg::Object* obj) { if (!std::get<2>(chunkId)) return; const osg::Vec2f& center = std::get<0>(chunkId); bool activeGrid = (center.x() > mActiveGrid.x() || center.y() > mActiveGrid.y() || center.x() < mActiveGrid.z() || center.y() < mActiveGrid.w()); if (!activeGrid) return; osg::UserDataContainer* udc = obj->getUserDataContainer(); if (udc && udc->getNumUserObjects()) { RefnumSet* refnums = dynamic_cast(udc->getUserObject(0)); if (!refnums) return; mOutput.insert(refnums->mRefnums.begin(), refnums->mRefnums.end()); } } osg::Vec4i mActiveGrid; std::set& mOutput; }; void ObjectPaging::getPagedRefnums(const osg::Vec4i &activeGrid, std::set &out) { GetRefnumsFunctor grf(out); grf.mActiveGrid = activeGrid; mCache->call(grf); } void ObjectPaging::reportStats(unsigned int frameNumber, osg::Stats *stats) const { stats->setAttribute(frameNumber, "Object Chunk", mCache->getCacheSize()); } } ================================================ FILE: apps/openmw/mwrender/objectpaging.hpp ================================================ #ifndef OPENMW_MWRENDER_OBJECTPAGING_H #define OPENMW_MWRENDER_OBJECTPAGING_H #include #include #include #include namespace Resource { class SceneManager; } namespace MWWorld { class ESMStore; } namespace MWRender { typedef std::tuple ChunkId; // Center, Size, ActiveGrid class ObjectPaging : public Resource::GenericResourceManager, public Terrain::QuadTreeWorld::ChunkManager { public: ObjectPaging(Resource::SceneManager* sceneManager); ~ObjectPaging() = default; osg::ref_ptr getChunk(float size, const osg::Vec2f& center, unsigned char lod, unsigned int lodFlags, bool activeGrid, const osg::Vec3f& viewPoint, bool compile) override; osg::ref_ptr createChunk(float size, const osg::Vec2f& center, bool activeGrid, const osg::Vec3f& viewPoint, bool compile); unsigned int getNodeMask() override; /// @return true if view needs rebuild bool enableObject(int type, const ESM::RefNum & refnum, const osg::Vec3f& pos, const osg::Vec2i& cell, bool enabled); /// @return true if view needs rebuild bool blacklistObject(int type, const ESM::RefNum & refnum, const osg::Vec3f& pos, const osg::Vec2i& cell); void clear(); /// Must be called after clear() before rendering starts. /// @return true if view needs rebuild bool unlockCache(); void reportStats(unsigned int frameNumber, osg::Stats* stats) const override; void getPagedRefnums(const osg::Vec4i &activeGrid, std::set &out); private: Resource::SceneManager* mSceneManager; bool mActiveGrid; bool mDebugBatches; float mMergeFactor; float mMinSize; float mMinSizeMergeFactor; float mMinSizeCostMultiplier; std::mutex mRefTrackerMutex; struct RefTracker { std::set mDisabled; std::set mBlacklist; bool operator==(const RefTracker&other) const { return mDisabled == other.mDisabled && mBlacklist == other.mBlacklist; } }; RefTracker mRefTracker; RefTracker mRefTrackerNew; bool mRefTrackerLocked; const RefTracker& getRefTracker() const { return mRefTracker; } RefTracker& getWritableRefTracker() { return mRefTrackerLocked ? mRefTrackerNew : mRefTracker; } std::mutex mSizeCacheMutex; typedef std::map SizeCache; SizeCache mSizeCache; }; class RefnumMarker : public osg::Object { public: RefnumMarker() : mNumVertices(0) { mRefnum.unset(); } RefnumMarker(const RefnumMarker ©, osg::CopyOp co) : mRefnum(copy.mRefnum), mNumVertices(copy.mNumVertices) {} META_Object(MWRender, RefnumMarker) ESM::RefNum mRefnum; unsigned int mNumVertices; }; } #endif ================================================ FILE: apps/openmw/mwrender/objects.cpp ================================================ #include "objects.hpp" #include #include #include #include #include "../mwworld/ptr.hpp" #include "../mwworld/class.hpp" #include "animation.hpp" #include "npcanimation.hpp" #include "creatureanimation.hpp" #include "vismask.hpp" namespace MWRender { Objects::Objects(Resource::ResourceSystem* resourceSystem, osg::ref_ptr rootNode, SceneUtil::UnrefQueue* unrefQueue) : mRootNode(rootNode) , mResourceSystem(resourceSystem) , mUnrefQueue(unrefQueue) { } Objects::~Objects() { mObjects.clear(); for (CellMap::iterator iter = mCellSceneNodes.begin(); iter != mCellSceneNodes.end(); ++iter) iter->second->getParent(0)->removeChild(iter->second); mCellSceneNodes.clear(); } void Objects::insertBegin(const MWWorld::Ptr& ptr) { assert(mObjects.find(ptr) == mObjects.end()); osg::ref_ptr cellnode; CellMap::iterator found = mCellSceneNodes.find(ptr.getCell()); if (found == mCellSceneNodes.end()) { cellnode = new osg::Group; cellnode->setName("Cell Root"); mRootNode->addChild(cellnode); mCellSceneNodes[ptr.getCell()] = cellnode; } else cellnode = found->second; osg::ref_ptr insert (new SceneUtil::PositionAttitudeTransform); cellnode->addChild(insert); insert->getOrCreateUserDataContainer()->addUserObject(new PtrHolder(ptr)); const float *f = ptr.getRefData().getPosition().pos; insert->setPosition(osg::Vec3(f[0], f[1], f[2])); const float scale = ptr.getCellRef().getScale(); osg::Vec3f scaleVec(scale, scale, scale); ptr.getClass().adjustScale(ptr, scaleVec, true); insert->setScale(scaleVec); ptr.getRefData().setBaseNode(insert); } void Objects::insertModel(const MWWorld::Ptr &ptr, const std::string &mesh, bool animated, bool allowLight) { insertBegin(ptr); ptr.getRefData().getBaseNode()->setNodeMask(Mask_Object); osg::ref_ptr anim (new ObjectAnimation(ptr, mesh, mResourceSystem, animated, allowLight)); mObjects.insert(std::make_pair(ptr, anim)); } void Objects::insertCreature(const MWWorld::Ptr &ptr, const std::string &mesh, bool weaponsShields) { insertBegin(ptr); ptr.getRefData().getBaseNode()->setNodeMask(Mask_Actor); // CreatureAnimation osg::ref_ptr anim; if (weaponsShields) anim = new CreatureWeaponAnimation(ptr, mesh, mResourceSystem); else anim = new CreatureAnimation(ptr, mesh, mResourceSystem); if (mObjects.insert(std::make_pair(ptr, anim)).second) ptr.getClass().getContainerStore(ptr).setContListener(static_cast(anim.get())); } void Objects::insertNPC(const MWWorld::Ptr &ptr) { insertBegin(ptr); ptr.getRefData().getBaseNode()->setNodeMask(Mask_Actor); osg::ref_ptr anim (new NpcAnimation(ptr, osg::ref_ptr(ptr.getRefData().getBaseNode()), mResourceSystem)); if (mObjects.insert(std::make_pair(ptr, anim)).second) { ptr.getClass().getInventoryStore(ptr).setInvListener(anim.get(), ptr); ptr.getClass().getInventoryStore(ptr).setContListener(anim.get()); } } bool Objects::removeObject (const MWWorld::Ptr& ptr) { if(!ptr.getRefData().getBaseNode()) return true; PtrAnimationMap::iterator iter = mObjects.find(ptr); if(iter != mObjects.end()) { if (mUnrefQueue.get()) mUnrefQueue->push(iter->second); mObjects.erase(iter); if (ptr.getClass().isActor()) { if (ptr.getClass().hasInventoryStore(ptr)) ptr.getClass().getInventoryStore(ptr).setInvListener(nullptr, ptr); ptr.getClass().getContainerStore(ptr).setContListener(nullptr); } ptr.getRefData().getBaseNode()->getParent(0)->removeChild(ptr.getRefData().getBaseNode()); ptr.getRefData().setBaseNode(nullptr); return true; } return false; } void Objects::removeCell(const MWWorld::CellStore* store) { for(PtrAnimationMap::iterator iter = mObjects.begin();iter != mObjects.end();) { MWWorld::Ptr ptr = iter->second->getPtr(); if(ptr.getCell() == store) { if (mUnrefQueue.get()) mUnrefQueue->push(iter->second); if (ptr.getClass().isNpc() && ptr.getRefData().getCustomData()) { MWWorld::InventoryStore& invStore = ptr.getClass().getInventoryStore(ptr); invStore.setInvListener(nullptr, ptr); invStore.setContListener(nullptr); } mObjects.erase(iter++); } else ++iter; } CellMap::iterator cell = mCellSceneNodes.find(store); if(cell != mCellSceneNodes.end()) { cell->second->getParent(0)->removeChild(cell->second); if (mUnrefQueue.get()) mUnrefQueue->push(cell->second); mCellSceneNodes.erase(cell); } } void Objects::updatePtr(const MWWorld::Ptr &old, const MWWorld::Ptr &cur) { osg::Node* objectNode = cur.getRefData().getBaseNode(); if (!objectNode) return; MWWorld::CellStore *newCell = cur.getCell(); osg::Group* cellnode; if(mCellSceneNodes.find(newCell) == mCellSceneNodes.end()) { cellnode = new osg::Group; mRootNode->addChild(cellnode); mCellSceneNodes[newCell] = cellnode; } else { cellnode = mCellSceneNodes[newCell]; } osg::UserDataContainer* userDataContainer = objectNode->getUserDataContainer(); if (userDataContainer) for (unsigned int i=0; igetNumUserObjects(); ++i) { if (dynamic_cast(userDataContainer->getUserObject(i))) userDataContainer->setUserObject(i, new PtrHolder(cur)); } if (objectNode->getNumParents()) objectNode->getParent(0)->removeChild(objectNode); cellnode->addChild(objectNode); PtrAnimationMap::iterator iter = mObjects.find(old); if(iter != mObjects.end()) { osg::ref_ptr anim = iter->second; mObjects.erase(iter); anim->updatePtr(cur); mObjects[cur] = anim; } } Animation* Objects::getAnimation(const MWWorld::Ptr &ptr) { PtrAnimationMap::const_iterator iter = mObjects.find(ptr); if(iter != mObjects.end()) return iter->second; return nullptr; } const Animation* Objects::getAnimation(const MWWorld::ConstPtr &ptr) const { PtrAnimationMap::const_iterator iter = mObjects.find(ptr); if(iter != mObjects.end()) return iter->second; return nullptr; } } ================================================ FILE: apps/openmw/mwrender/objects.hpp ================================================ #ifndef GAME_RENDER_OBJECTS_H #define GAME_RENDER_OBJECTS_H #include #include #include #include #include "../mwworld/ptr.hpp" namespace osg { class Group; } namespace Resource { class ResourceSystem; } namespace MWWorld { class CellStore; } namespace SceneUtil { class UnrefQueue; } namespace MWRender{ class Animation; class PtrHolder : public osg::Object { public: PtrHolder(const MWWorld::Ptr& ptr) : mPtr(ptr) { } PtrHolder() { } PtrHolder(const PtrHolder& copy, const osg::CopyOp& copyop) : mPtr(copy.mPtr) { } META_Object(MWRender, PtrHolder) MWWorld::Ptr mPtr; }; class Objects{ typedef std::map > PtrAnimationMap; typedef std::map > CellMap; CellMap mCellSceneNodes; PtrAnimationMap mObjects; osg::ref_ptr mRootNode; Resource::ResourceSystem* mResourceSystem; osg::ref_ptr mUnrefQueue; void insertBegin(const MWWorld::Ptr& ptr); public: Objects(Resource::ResourceSystem* resourceSystem, osg::ref_ptr rootNode, SceneUtil::UnrefQueue* unrefQueue); ~Objects(); /// @param animated Attempt to load separate keyframes from a .kf file matching the model file? /// @param allowLight If false, no lights will be created, and particles systems will be removed. void insertModel(const MWWorld::Ptr& ptr, const std::string &model, bool animated=false, bool allowLight=true); void insertNPC(const MWWorld::Ptr& ptr); void insertCreature (const MWWorld::Ptr& ptr, const std::string& model, bool weaponsShields); Animation* getAnimation(const MWWorld::Ptr &ptr); const Animation* getAnimation(const MWWorld::ConstPtr &ptr) const; bool removeObject (const MWWorld::Ptr& ptr); ///< \return found? void removeCell(const MWWorld::CellStore* store); /// Updates containing cell for object rendering data void updatePtr(const MWWorld::Ptr &old, const MWWorld::Ptr &cur); private: void operator = (const Objects&); Objects(const Objects&); }; } #endif ================================================ FILE: apps/openmw/mwrender/pathgrid.cpp ================================================ #include "pathgrid.hpp" #include #include #include #include #include #include #include #include "../mwbase/world.hpp" // these includes can be removed once the static-hack is gone #include "../mwbase/environment.hpp" #include "../mwworld/cellstore.hpp" #include "../mwworld/esmstore.hpp" #include "../mwmechanics/pathfinding.hpp" #include "vismask.hpp" namespace MWRender { Pathgrid::Pathgrid(osg::ref_ptr root) : mPathgridEnabled(false) , mRootNode(root) , mPathGridRoot(nullptr) , mInteriorPathgridNode(nullptr) { } Pathgrid::~Pathgrid() { if (mPathgridEnabled) { togglePathgrid(); } } bool Pathgrid::toggleRenderMode (int mode){ switch (mode) { case Render_Pathgrid: togglePathgrid(); return mPathgridEnabled; default: return false; } return false; } void Pathgrid::addCell(const MWWorld::CellStore *store) { mActiveCells.push_back(store); if (mPathgridEnabled) enableCellPathgrid(store); } void Pathgrid::removeCell(const MWWorld::CellStore *store) { mActiveCells.erase(std::remove(mActiveCells.begin(), mActiveCells.end(), store), mActiveCells.end()); if (mPathgridEnabled) disableCellPathgrid(store); } void Pathgrid::togglePathgrid() { mPathgridEnabled = !mPathgridEnabled; if (mPathgridEnabled) { // add path grid meshes to already loaded cells mPathGridRoot = new osg::Group; mPathGridRoot->setNodeMask(Mask_Debug); mRootNode->addChild(mPathGridRoot); for(const MWWorld::CellStore* cell : mActiveCells) { enableCellPathgrid(cell); } } else { // remove path grid meshes from already loaded cells for(const MWWorld::CellStore* cell : mActiveCells) { disableCellPathgrid(cell); } if (mPathGridRoot) { mRootNode->removeChild(mPathGridRoot); mPathGridRoot = nullptr; } } } void Pathgrid::enableCellPathgrid(const MWWorld::CellStore *store) { MWBase::World* world = MWBase::Environment::get().getWorld(); const ESM::Pathgrid *pathgrid = world->getStore().get().search(*store->getCell()); if (!pathgrid) return; osg::Vec3f cellPathGridPos(0, 0, 0); Misc::CoordinateConverter(store->getCell()).toWorld(cellPathGridPos); osg::ref_ptr cellPathGrid = new osg::PositionAttitudeTransform; cellPathGrid->setPosition(cellPathGridPos); osg::ref_ptr geometry = SceneUtil::createPathgridGeometry(*pathgrid); cellPathGrid->addChild(geometry); mPathGridRoot->addChild(cellPathGrid); if (store->getCell()->isExterior()) { mExteriorPathgridNodes[std::make_pair(store->getCell()->getGridX(), store->getCell()->getGridY())] = cellPathGrid; } else { assert(mInteriorPathgridNode == nullptr); mInteriorPathgridNode = cellPathGrid; } } void Pathgrid::disableCellPathgrid(const MWWorld::CellStore *store) { if (store->getCell()->isExterior()) { ExteriorPathgridNodes::iterator it = mExteriorPathgridNodes.find(std::make_pair(store->getCell()->getGridX(), store->getCell()->getGridY())); if (it != mExteriorPathgridNodes.end()) { mPathGridRoot->removeChild(it->second); mExteriorPathgridNodes.erase(it); } } else { if (mInteriorPathgridNode) { mPathGridRoot->removeChild(mInteriorPathgridNode); mInteriorPathgridNode = nullptr; } } } } ================================================ FILE: apps/openmw/mwrender/pathgrid.hpp ================================================ #ifndef GAME_RENDER_MWSCENE_H #define GAME_RENDER_MWSCENE_H #include #include #include #include namespace ESM { struct Pathgrid; } namespace osg { class Group; class Geometry; } namespace MWWorld { class Ptr; class CellStore; } namespace MWRender { class Pathgrid { bool mPathgridEnabled; void togglePathgrid(); typedef std::vector CellList; CellList mActiveCells; osg::ref_ptr mRootNode; osg::ref_ptr mPathGridRoot; typedef std::map, osg::ref_ptr > ExteriorPathgridNodes; ExteriorPathgridNodes mExteriorPathgridNodes; osg::ref_ptr mInteriorPathgridNode; void enableCellPathgrid(const MWWorld::CellStore *store); void disableCellPathgrid(const MWWorld::CellStore *store); public: Pathgrid(osg::ref_ptr root); ~Pathgrid(); bool toggleRenderMode (int mode); void addCell(const MWWorld::CellStore* store); void removeCell(const MWWorld::CellStore* store); }; } #endif ================================================ FILE: apps/openmw/mwrender/recastmesh.cpp ================================================ #include "recastmesh.hpp" #include #include #include "vismask.hpp" namespace MWRender { RecastMesh::RecastMesh(const osg::ref_ptr& root, bool enabled) : mRootNode(root) , mEnabled(enabled) { } RecastMesh::~RecastMesh() { if (mEnabled) disable(); } bool RecastMesh::toggle() { if (mEnabled) disable(); else enable(); return mEnabled; } void RecastMesh::update(const DetourNavigator::RecastMeshTiles& tiles, const DetourNavigator::Settings& settings) { if (!mEnabled) return; for (auto it = mGroups.begin(); it != mGroups.end();) { const auto tile = tiles.find(it->first); if (tile == tiles.end()) { mRootNode->removeChild(it->second.mValue); it = mGroups.erase(it); continue; } if (it->second.mGeneration != tile->second->getGeneration() || it->second.mRevision != tile->second->getRevision()) { const auto group = SceneUtil::createRecastMeshGroup(*tile->second, settings); group->setNodeMask(Mask_Debug); mRootNode->removeChild(it->second.mValue); mRootNode->addChild(group); it->second.mValue = group; it->second.mGeneration = tile->second->getGeneration(); it->second.mRevision = tile->second->getRevision(); continue; } ++it; } for (const auto& tile : tiles) { if (mGroups.count(tile.first)) continue; const auto group = SceneUtil::createRecastMeshGroup(*tile.second, settings); group->setNodeMask(Mask_Debug); mGroups.emplace(tile.first, Group {tile.second->getGeneration(), tile.second->getRevision(), group}); mRootNode->addChild(group); } } void RecastMesh::reset() { std::for_each(mGroups.begin(), mGroups.end(), [&] (const auto& v) { mRootNode->removeChild(v.second.mValue); }); mGroups.clear(); } void RecastMesh::enable() { std::for_each(mGroups.begin(), mGroups.end(), [&] (const auto& v) { mRootNode->addChild(v.second.mValue); }); mEnabled = true; } void RecastMesh::disable() { std::for_each(mGroups.begin(), mGroups.end(), [&] (const auto& v) { mRootNode->removeChild(v.second.mValue); }); mEnabled = false; } } ================================================ FILE: apps/openmw/mwrender/recastmesh.hpp ================================================ #ifndef OPENMW_MWRENDER_RECASTMESH_H #define OPENMW_MWRENDER_RECASTMESH_H #include #include #include namespace osg { class Group; class Geometry; } namespace MWRender { class RecastMesh { public: RecastMesh(const osg::ref_ptr& root, bool enabled); ~RecastMesh(); bool toggle(); void update(const DetourNavigator::RecastMeshTiles& recastMeshTiles, const DetourNavigator::Settings& settings); void reset(); void enable(); void disable(); bool isEnabled() const { return mEnabled; } private: struct Group { std::size_t mGeneration; std::size_t mRevision; osg::ref_ptr mValue; }; osg::ref_ptr mRootNode; bool mEnabled; std::map mGroups; }; } #endif ================================================ FILE: apps/openmw/mwrender/renderbin.hpp ================================================ #ifndef OPENMW_MWRENDER_RENDERBIN_H #define OPENMW_MWRENDER_RENDERBIN_H namespace MWRender { /// Defines the render bin numbers used in the OpenMW scene graph. The bin with the lowest number is rendered first. enum RenderBins { RenderBin_Sky = -1, RenderBin_Default = 0, // osg::StateSet::OPAQUE_BIN RenderBin_Water = 9, RenderBin_DepthSorted = 10, // osg::StateSet::TRANSPARENT_BIN RenderBin_OcclusionQuery = 11, RenderBin_FirstPerson = 12, RenderBin_SunGlare = 13 }; } #endif ================================================ FILE: apps/openmw/mwrender/renderinginterface.hpp ================================================ #ifndef GAME_RENDERING_INTERFACE_H #define GAME_RENDERING_INTERFACE_H namespace MWRender { class Objects; class Actors; class RenderingInterface { public: virtual MWRender::Objects& getObjects() = 0; virtual ~RenderingInterface(){} }; } #endif ================================================ FILE: apps/openmw/mwrender/renderingmanager.cpp ================================================ #include "renderingmanager.hpp" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "../mwworld/cellstore.hpp" #include "../mwworld/class.hpp" #include "../mwgui/loadingscreen.hpp" #include "../mwbase/windowmanager.hpp" #include "../mwmechanics/actorutil.hpp" #include "sky.hpp" #include "effectmanager.hpp" #include "npcanimation.hpp" #include "vismask.hpp" #include "pathgrid.hpp" #include "camera.hpp" #include "viewovershoulder.hpp" #include "water.hpp" #include "terrainstorage.hpp" #include "navmesh.hpp" #include "actorspaths.hpp" #include "recastmesh.hpp" #include "fogmanager.hpp" #include "objectpaging.hpp" #include "screenshotmanager.hpp" #include "groundcover.hpp" namespace MWRender { class StateUpdater : public SceneUtil::StateSetUpdater { public: StateUpdater() : mFogStart(0.f) , mFogEnd(0.f) , mWireframe(false) { } void setDefaults(osg::StateSet *stateset) override { osg::LightModel* lightModel = new osg::LightModel; stateset->setAttribute(lightModel, osg::StateAttribute::ON); osg::Fog* fog = new osg::Fog; fog->setMode(osg::Fog::LINEAR); stateset->setAttributeAndModes(fog, osg::StateAttribute::ON); if (mWireframe) { osg::PolygonMode* polygonmode = new osg::PolygonMode; polygonmode->setMode(osg::PolygonMode::FRONT_AND_BACK, osg::PolygonMode::LINE); stateset->setAttributeAndModes(polygonmode, osg::StateAttribute::ON); } else stateset->removeAttribute(osg::StateAttribute::POLYGONMODE); } void apply(osg::StateSet* stateset, osg::NodeVisitor*) override { osg::LightModel* lightModel = static_cast(stateset->getAttribute(osg::StateAttribute::LIGHTMODEL)); lightModel->setAmbientIntensity(mAmbientColor); osg::Fog* fog = static_cast(stateset->getAttribute(osg::StateAttribute::FOG)); fog->setColor(mFogColor); fog->setStart(mFogStart); fog->setEnd(mFogEnd); } void setAmbientColor(const osg::Vec4f& col) { mAmbientColor = col; } void setFogColor(const osg::Vec4f& col) { mFogColor = col; } void setFogStart(float start) { mFogStart = start; } void setFogEnd(float end) { mFogEnd = end; } void setWireframe(bool wireframe) { if (mWireframe != wireframe) { mWireframe = wireframe; reset(); } } bool getWireframe() const { return mWireframe; } private: osg::Vec4f mAmbientColor; osg::Vec4f mFogColor; float mFogStart; float mFogEnd; bool mWireframe; }; class PreloadCommonAssetsWorkItem : public SceneUtil::WorkItem { public: PreloadCommonAssetsWorkItem(Resource::ResourceSystem* resourceSystem) : mResourceSystem(resourceSystem) { } void doWork() override { try { for (std::vector::const_iterator it = mModels.begin(); it != mModels.end(); ++it) mResourceSystem->getSceneManager()->cacheInstance(*it); for (std::vector::const_iterator it = mTextures.begin(); it != mTextures.end(); ++it) mResourceSystem->getImageManager()->getImage(*it); for (std::vector::const_iterator it = mKeyframes.begin(); it != mKeyframes.end(); ++it) mResourceSystem->getKeyframeManager()->get(*it); } catch (std::exception&) { // ignore error (will be shown when these are needed proper) } } std::vector mModels; std::vector mTextures; std::vector mKeyframes; private: Resource::ResourceSystem* mResourceSystem; }; RenderingManager::RenderingManager(osgViewer::Viewer* viewer, osg::ref_ptr rootNode, Resource::ResourceSystem* resourceSystem, SceneUtil::WorkQueue* workQueue, const std::string& resourcePath, DetourNavigator::Navigator& navigator) : mViewer(viewer) , mRootNode(rootNode) , mResourceSystem(resourceSystem) , mWorkQueue(workQueue) , mUnrefQueue(new SceneUtil::UnrefQueue) , mNavigator(navigator) , mMinimumAmbientLuminance(0.f) , mNightEyeFactor(0.f) , mFieldOfViewOverridden(false) , mFieldOfViewOverride(0.f) { auto lightingMethod = SceneUtil::LightManager::getLightingMethodFromString(Settings::Manager::getString("lighting method", "Shaders")); resourceSystem->getSceneManager()->setParticleSystemMask(MWRender::Mask_ParticleSystem); resourceSystem->getSceneManager()->setShaderPath(resourcePath + "/shaders"); // Shadows and radial fog have problems with fixed-function mode bool forceShaders = Settings::Manager::getBool("radial fog", "Shaders") || Settings::Manager::getBool("force shaders", "Shaders") || Settings::Manager::getBool("enable shadows", "Shadows") || lightingMethod != SceneUtil::LightingMethod::FFP; resourceSystem->getSceneManager()->setForceShaders(forceShaders); // FIXME: calling dummy method because terrain needs to know whether lighting is clamped resourceSystem->getSceneManager()->setClampLighting(Settings::Manager::getBool("clamp lighting", "Shaders")); resourceSystem->getSceneManager()->setAutoUseNormalMaps(Settings::Manager::getBool("auto use object normal maps", "Shaders")); resourceSystem->getSceneManager()->setNormalMapPattern(Settings::Manager::getString("normal map pattern", "Shaders")); resourceSystem->getSceneManager()->setNormalHeightMapPattern(Settings::Manager::getString("normal height map pattern", "Shaders")); resourceSystem->getSceneManager()->setAutoUseSpecularMaps(Settings::Manager::getBool("auto use object specular maps", "Shaders")); resourceSystem->getSceneManager()->setSpecularMapPattern(Settings::Manager::getString("specular map pattern", "Shaders")); resourceSystem->getSceneManager()->setApplyLightingToEnvMaps(Settings::Manager::getBool("apply lighting to environment maps", "Shaders")); resourceSystem->getSceneManager()->setConvertAlphaTestToAlphaToCoverage(Settings::Manager::getBool("antialias alpha test", "Shaders") && Settings::Manager::getInt("antialiasing", "Video") > 1); // Let LightManager choose which backend to use based on our hint. For methods besides legacy lighting, this depends on support for various OpenGL extensions. osg::ref_ptr sceneRoot = new SceneUtil::LightManager(lightingMethod == SceneUtil::LightingMethod::FFP); resourceSystem->getSceneManager()->getShaderManager().setLightingMethod(sceneRoot->getLightingMethod()); resourceSystem->getSceneManager()->setLightingMethod(sceneRoot->getLightingMethod()); resourceSystem->getSceneManager()->setSupportedLightingMethods(sceneRoot->getSupportedLightingMethods()); mMinimumAmbientLuminance = std::clamp(Settings::Manager::getFloat("minimum interior brightness", "Shaders"), 0.f, 1.f); sceneRoot->setLightingMask(Mask_Lighting); mSceneRoot = sceneRoot; sceneRoot->setStartLight(1); sceneRoot->setNodeMask(Mask_Scene); sceneRoot->setName("Scene Root"); int shadowCastingTraversalMask = Mask_Scene; if (Settings::Manager::getBool("actor shadows", "Shadows")) shadowCastingTraversalMask |= Mask_Actor; if (Settings::Manager::getBool("player shadows", "Shadows")) shadowCastingTraversalMask |= Mask_Player; if (Settings::Manager::getBool("terrain shadows", "Shadows")) shadowCastingTraversalMask |= Mask_Terrain; int indoorShadowCastingTraversalMask = shadowCastingTraversalMask; if (Settings::Manager::getBool("object shadows", "Shadows")) shadowCastingTraversalMask |= (Mask_Object|Mask_Static); mShadowManager.reset(new SceneUtil::ShadowManager(sceneRoot, mRootNode, shadowCastingTraversalMask, indoorShadowCastingTraversalMask, mResourceSystem->getSceneManager()->getShaderManager())); Shader::ShaderManager::DefineMap shadowDefines = mShadowManager->getShadowDefines(); Shader::ShaderManager::DefineMap lightDefines = sceneRoot->getLightDefines(); Shader::ShaderManager::DefineMap globalDefines = mResourceSystem->getSceneManager()->getShaderManager().getGlobalDefines(); for (auto itr = shadowDefines.begin(); itr != shadowDefines.end(); itr++) globalDefines[itr->first] = itr->second; globalDefines["forcePPL"] = Settings::Manager::getBool("force per pixel lighting", "Shaders") ? "1" : "0"; globalDefines["clamp"] = Settings::Manager::getBool("clamp lighting", "Shaders") ? "1" : "0"; globalDefines["preLightEnv"] = Settings::Manager::getBool("apply lighting to environment maps", "Shaders") ? "1" : "0"; globalDefines["radialFog"] = Settings::Manager::getBool("radial fog", "Shaders") ? "1" : "0"; globalDefines["useGPUShader4"] = "0"; for (auto itr = lightDefines.begin(); itr != lightDefines.end(); itr++) globalDefines[itr->first] = itr->second; // Refactor this at some point - most shaders don't care about these defines float groundcoverDistance = std::max(0.f, Settings::Manager::getFloat("rendering distance", "Groundcover")); globalDefines["groundcoverFadeStart"] = std::to_string(groundcoverDistance * 0.9f); globalDefines["groundcoverFadeEnd"] = std::to_string(groundcoverDistance); globalDefines["groundcoverStompMode"] = std::to_string(std::clamp(Settings::Manager::getInt("stomp mode", "Groundcover"), 0, 2)); globalDefines["groundcoverStompIntensity"] = std::to_string(std::clamp(Settings::Manager::getInt("stomp intensity", "Groundcover"), 0, 2)); // It is unnecessary to stop/start the viewer as no frames are being rendered yet. mResourceSystem->getSceneManager()->getShaderManager().setGlobalDefines(globalDefines); mNavMesh.reset(new NavMesh(mRootNode, Settings::Manager::getBool("enable nav mesh render", "Navigator"))); mActorsPaths.reset(new ActorsPaths(mRootNode, Settings::Manager::getBool("enable agents paths render", "Navigator"))); mRecastMesh.reset(new RecastMesh(mRootNode, Settings::Manager::getBool("enable recast mesh render", "Navigator"))); mPathgrid.reset(new Pathgrid(mRootNode)); mObjects.reset(new Objects(mResourceSystem, sceneRoot, mUnrefQueue.get())); if (getenv("OPENMW_DONT_PRECOMPILE") == nullptr) { mViewer->setIncrementalCompileOperation(new osgUtil::IncrementalCompileOperation); mViewer->getIncrementalCompileOperation()->setTargetFrameRate(Settings::Manager::getFloat("target framerate", "Cells")); } mResourceSystem->getSceneManager()->setIncrementalCompileOperation(mViewer->getIncrementalCompileOperation()); mEffectManager.reset(new EffectManager(sceneRoot, mResourceSystem)); const std::string normalMapPattern = Settings::Manager::getString("normal map pattern", "Shaders"); const std::string heightMapPattern = Settings::Manager::getString("normal height map pattern", "Shaders"); const std::string specularMapPattern = Settings::Manager::getString("terrain specular map pattern", "Shaders"); const bool useTerrainNormalMaps = Settings::Manager::getBool("auto use terrain normal maps", "Shaders"); const bool useTerrainSpecularMaps = Settings::Manager::getBool("auto use terrain specular maps", "Shaders"); mTerrainStorage.reset(new TerrainStorage(mResourceSystem, normalMapPattern, heightMapPattern, useTerrainNormalMaps, specularMapPattern, useTerrainSpecularMaps)); const float lodFactor = Settings::Manager::getFloat("lod factor", "Terrain"); if (Settings::Manager::getBool("distant terrain", "Terrain")) { const int compMapResolution = Settings::Manager::getInt("composite map resolution", "Terrain"); int compMapPower = Settings::Manager::getInt("composite map level", "Terrain"); compMapPower = std::max(-3, compMapPower); float compMapLevel = pow(2, compMapPower); const int vertexLodMod = Settings::Manager::getInt("vertex lod mod", "Terrain"); float maxCompGeometrySize = Settings::Manager::getFloat("max composite geometry size", "Terrain"); maxCompGeometrySize = std::max(maxCompGeometrySize, 1.f); mTerrain.reset(new Terrain::QuadTreeWorld( sceneRoot, mRootNode, mResourceSystem, mTerrainStorage.get(), Mask_Terrain, Mask_PreCompile, Mask_Debug, compMapResolution, compMapLevel, lodFactor, vertexLodMod, maxCompGeometrySize)); if (Settings::Manager::getBool("object paging", "Terrain")) { mObjectPaging.reset(new ObjectPaging(mResourceSystem->getSceneManager())); static_cast(mTerrain.get())->addChunkManager(mObjectPaging.get()); mResourceSystem->addResourceManager(mObjectPaging.get()); } } else mTerrain.reset(new Terrain::TerrainGrid(sceneRoot, mRootNode, mResourceSystem, mTerrainStorage.get(), Mask_Terrain, Mask_PreCompile, Mask_Debug)); mTerrain->setTargetFrameRate(Settings::Manager::getFloat("target framerate", "Cells")); mTerrain->setWorkQueue(mWorkQueue.get()); if (Settings::Manager::getBool("enabled", "Groundcover")) { osg::ref_ptr groundcoverRoot = new osg::Group; groundcoverRoot->setNodeMask(Mask_Groundcover); groundcoverRoot->setName("Groundcover Root"); sceneRoot->addChild(groundcoverRoot); mGroundcoverUpdater = new GroundcoverUpdater; groundcoverRoot->addUpdateCallback(mGroundcoverUpdater); float chunkSize = Settings::Manager::getFloat("min chunk size", "Groundcover"); if (chunkSize >= 1.0f) chunkSize = 1.0f; else if (chunkSize >= 0.5f) chunkSize = 0.5f; else if (chunkSize >= 0.25f) chunkSize = 0.25f; else if (chunkSize != 0.125f) chunkSize = 0.125f; float density = Settings::Manager::getFloat("density", "Groundcover"); density = std::clamp(density, 0.f, 1.f); mGroundcoverWorld.reset(new Terrain::QuadTreeWorld(groundcoverRoot, mTerrainStorage.get(), Mask_Groundcover, lodFactor, chunkSize)); mGroundcover.reset(new Groundcover(mResourceSystem->getSceneManager(), density)); static_cast(mGroundcoverWorld.get())->addChunkManager(mGroundcover.get()); mResourceSystem->addResourceManager(mGroundcover.get()); // Groundcover it is handled in the same way indifferently from if it is from active grid or from distant cell. // Use a stub grid to avoid splitting between chunks for active grid and chunks for distant cells. mGroundcoverWorld->setActiveGrid(osg::Vec4i(0, 0, 0, 0)); } // water goes after terrain for correct waterculling order 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())); mViewer->setLightingMode(osgViewer::View::NO_LIGHT); osg::ref_ptr source = new osg::LightSource; source->setNodeMask(Mask_Lighting); mSunLight = new osg::Light; source->setLight(mSunLight); mSunLight->setDiffuse(osg::Vec4f(0,0,0,1)); mSunLight->setAmbient(osg::Vec4f(0,0,0,1)); mSunLight->setSpecular(osg::Vec4f(0,0,0,0)); mSunLight->setConstantAttenuation(1.f); sceneRoot->setSunlight(mSunLight); sceneRoot->addChild(source); sceneRoot->getOrCreateStateSet()->setMode(GL_CULL_FACE, osg::StateAttribute::ON); sceneRoot->getOrCreateStateSet()->setMode(GL_LIGHTING, osg::StateAttribute::ON); sceneRoot->getOrCreateStateSet()->setMode(GL_NORMALIZE, osg::StateAttribute::ON); osg::ref_ptr defaultMat (new osg::Material); defaultMat->setColorMode(osg::Material::OFF); defaultMat->setAmbient(osg::Material::FRONT_AND_BACK, osg::Vec4f(1,1,1,1)); 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)); sceneRoot->getOrCreateStateSet()->setAttribute(defaultMat); mFog.reset(new FogManager()); mSky.reset(new SkyManager(sceneRoot, resourceSystem->getSceneManager())); mSky->setCamera(mViewer->getCamera()); source->setStateSetModes(*mRootNode->getOrCreateStateSet(), osg::StateAttribute::ON); mStateUpdater = new StateUpdater; sceneRoot->addUpdateCallback(mStateUpdater); osg::Camera::CullingMode cullingMode = osg::Camera::DEFAULT_CULLING|osg::Camera::FAR_PLANE_CULLING; if (!Settings::Manager::getBool("small feature culling", "Camera")) cullingMode &= ~(osg::CullStack::SMALL_FEATURE_CULLING); else { mViewer->getCamera()->setSmallFeatureCullingPixelSize(Settings::Manager::getFloat("small feature culling pixel size", "Camera")); cullingMode |= osg::CullStack::SMALL_FEATURE_CULLING; } mViewer->getCamera()->setCullingMode( cullingMode ); mViewer->getCamera()->setComputeNearFarMode(osg::Camera::DO_NOT_COMPUTE_NEAR_FAR); mViewer->getCamera()->setCullingMode(cullingMode); mViewer->getCamera()->setCullMask(~(Mask_UpdateVisitor|Mask_SimpleWater)); NifOsg::Loader::setHiddenNodeMask(Mask_UpdateVisitor); NifOsg::Loader::setIntersectionDisabledNodeMask(Mask_Effect); Nif::NIFFile::setLoadUnsupportedFiles(Settings::Manager::getBool("load unsupported nif files", "Models")); mNearClip = Settings::Manager::getFloat("near clip", "Camera"); mViewDistance = Settings::Manager::getFloat("viewing distance", "Camera"); float fov = Settings::Manager::getFloat("field of view", "Camera"); mFieldOfView = std::min(std::max(1.f, fov), 179.f); 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("near", mNearClip)); mRootNode->getOrCreateStateSet()->addUniform(new osg::Uniform("far", mViewDistance)); mRootNode->getOrCreateStateSet()->addUniform(new osg::Uniform("simpleWater", false)); // Hopefully, anything genuinely requiring the default alpha func of GL_ALWAYS explicitly sets it mRootNode->getOrCreateStateSet()->setAttribute(Shader::RemovedAlphaFunc::getInstance(GL_ALWAYS)); // The transparent renderbin sets alpha testing on because that was faster on old GPUs. It's now slower and breaks things. mRootNode->getOrCreateStateSet()->setMode(GL_ALPHA_TEST, osg::StateAttribute::OFF); mUniformNear = mRootNode->getOrCreateStateSet()->getUniform("near"); mUniformFar = mRootNode->getOrCreateStateSet()->getUniform("far"); updateProjectionMatrix(); } RenderingManager::~RenderingManager() { // let background loading thread finish before we delete anything else mWorkQueue = nullptr; } osgUtil::IncrementalCompileOperation* RenderingManager::getIncrementalCompileOperation() { return mViewer->getIncrementalCompileOperation(); } MWRender::Objects& RenderingManager::getObjects() { return *mObjects.get(); } Resource::ResourceSystem* RenderingManager::getResourceSystem() { return mResourceSystem; } SceneUtil::WorkQueue* RenderingManager::getWorkQueue() { return mWorkQueue.get(); } SceneUtil::UnrefQueue* RenderingManager::getUnrefQueue() { return mUnrefQueue.get(); } Terrain::World* RenderingManager::getTerrain() { return mTerrain.get(); } void RenderingManager::preloadCommonAssets() { osg::ref_ptr workItem (new PreloadCommonAssetsWorkItem(mResourceSystem)); mSky->listAssetsToPreload(workItem->mModels, workItem->mTextures); mWater->listAssetsToPreload(workItem->mTextures); workItem->mModels.push_back(Settings::Manager::getString("xbaseanim", "Models")); workItem->mModels.push_back(Settings::Manager::getString("xbaseanim1st", "Models")); workItem->mModels.push_back(Settings::Manager::getString("xbaseanimfemale", "Models")); workItem->mModels.push_back(Settings::Manager::getString("xargonianswimkna", "Models")); workItem->mKeyframes.push_back(Settings::Manager::getString("xbaseanimkf", "Models")); workItem->mKeyframes.push_back(Settings::Manager::getString("xbaseanim1stkf", "Models")); workItem->mKeyframes.push_back(Settings::Manager::getString("xbaseanimfemalekf", "Models")); workItem->mKeyframes.push_back(Settings::Manager::getString("xargonianswimknakf", "Models")); workItem->mTextures.emplace_back("textures/_land_default.dds"); mWorkQueue->addWorkItem(workItem); } double RenderingManager::getReferenceTime() const { return mViewer->getFrameStamp()->getReferenceTime(); } osg::Group* RenderingManager::getLightRoot() { return mSceneRoot.get(); } void RenderingManager::setNightEyeFactor(float factor) { if (factor != mNightEyeFactor) { mNightEyeFactor = factor; updateAmbient(); } } void RenderingManager::setAmbientColour(const osg::Vec4f &colour) { mAmbientColor = colour; updateAmbient(); } void RenderingManager::skySetDate(int day, int month) { mSky->setDate(day, month); } int RenderingManager::skyGetMasserPhase() const { return mSky->getMasserPhase(); } int RenderingManager::skyGetSecundaPhase() const { return mSky->getSecundaPhase(); } void RenderingManager::skySetMoonColour(bool red) { mSky->setMoonColour(red); } void RenderingManager::configureAmbient(const ESM::Cell *cell) { bool needsAdjusting = false; if (mResourceSystem->getSceneManager()->getLightingMethod() != SceneUtil::LightingMethod::FFP) needsAdjusting = !cell->isExterior() && !(cell->mData.mFlags & ESM::Cell::QuasiEx); auto ambient = SceneUtil::colourFromRGB(cell->mAmbi.mAmbient); if (needsAdjusting) { constexpr float pR = 0.2126; constexpr float pG = 0.7152; constexpr float pB = 0.0722; // we already work in linear RGB so no conversions are needed for the luminosity function float relativeLuminance = pR*ambient.r() + pG*ambient.g() + pB*ambient.b(); if (relativeLuminance < mMinimumAmbientLuminance) { // brighten ambient so it reaches the minimum threshold but no more, we want to mess with content data as least we can float targetBrightnessIncreaseFactor = mMinimumAmbientLuminance / relativeLuminance; if (ambient.r() == 0.f && ambient.g() == 0.f && ambient.b() == 0.f) ambient = osg::Vec4(mMinimumAmbientLuminance, mMinimumAmbientLuminance, mMinimumAmbientLuminance, ambient.a()); else ambient *= targetBrightnessIncreaseFactor; } } setAmbientColour(ambient); osg::Vec4f diffuse = SceneUtil::colourFromRGB(cell->mAmbi.mSunlight); mSunLight->setDiffuse(diffuse); mSunLight->setSpecular(diffuse); mSunLight->setPosition(osg::Vec4f(-0.15f, 0.15f, 1.f, 0.f)); } void RenderingManager::setSunColour(const osg::Vec4f& diffuse, const osg::Vec4f& specular) { // need to wrap this in a StateUpdater? mSunLight->setDiffuse(diffuse); mSunLight->setSpecular(specular); } void RenderingManager::setSunDirection(const osg::Vec3f &direction) { osg::Vec3 position = direction * -1; // need to wrap this in a StateUpdater? mSunLight->setPosition(osg::Vec4(position.x(), position.y(), position.z(), 0)); mSky->setSunDirection(position); } void RenderingManager::addCell(const MWWorld::CellStore *store) { mPathgrid->addCell(store); mWater->changeCell(store); if (store->getCell()->isExterior()) { mTerrain->loadCell(store->getCell()->getGridX(), store->getCell()->getGridY()); if (mGroundcoverWorld) mGroundcoverWorld->loadCell(store->getCell()->getGridX(), store->getCell()->getGridY()); } } void RenderingManager::removeCell(const MWWorld::CellStore *store) { mPathgrid->removeCell(store); mActorsPaths->removeCell(store); mObjects->removeCell(store); if (store->getCell()->isExterior()) { mTerrain->unloadCell(store->getCell()->getGridX(), store->getCell()->getGridY()); if (mGroundcoverWorld) mGroundcoverWorld->unloadCell(store->getCell()->getGridX(), store->getCell()->getGridY()); } mWater->removeCell(store); } void RenderingManager::enableTerrain(bool enable) { if (!enable) mWater->setCullCallback(nullptr); mTerrain->enable(enable); if (mGroundcoverWorld) mGroundcoverWorld->enable(enable); } void RenderingManager::setSkyEnabled(bool enabled) { mSky->setEnabled(enabled); if (enabled) mShadowManager->enableOutdoorMode(); else mShadowManager->enableIndoorMode(); } bool RenderingManager::toggleBorders() { bool borders = !mTerrain->getBordersVisible(); mTerrain->setBordersVisible(borders); return borders; } bool RenderingManager::toggleRenderMode(RenderMode mode) { if (mode == Render_CollisionDebug || mode == Render_Pathgrid) return mPathgrid->toggleRenderMode(mode); else if (mode == Render_Wireframe) { bool wireframe = !mStateUpdater->getWireframe(); mStateUpdater->setWireframe(wireframe); return wireframe; } else if (mode == Render_Water) { return mWater->toggle(); } else if (mode == Render_Scene) { unsigned int mask = mViewer->getCamera()->getCullMask(); bool enabled = mask&Mask_Scene; enabled = !enabled; if (enabled) mask |= Mask_Scene; else mask &= ~Mask_Scene; mViewer->getCamera()->setCullMask(mask); return enabled; } else if (mode == Render_NavMesh) { return mNavMesh->toggle(); } else if (mode == Render_ActorsPaths) { return mActorsPaths->toggle(); } else if (mode == Render_RecastMesh) { return mRecastMesh->toggle(); } return false; } void RenderingManager::configureFog(const ESM::Cell *cell) { mFog->configure(mViewDistance, cell); } void RenderingManager::configureFog(float fogDepth, float underwaterFog, float dlFactor, float dlOffset, const osg::Vec4f &color) { mFog->configure(mViewDistance, fogDepth, underwaterFog, dlFactor, dlOffset, color); } SkyManager* RenderingManager::getSkyManager() { return mSky.get(); } void RenderingManager::update(float dt, bool paused) { reportStats(); mUnrefQueue->flush(mWorkQueue.get()); float rainIntensity = mSky->getPrecipitationAlpha(); mWater->setRainIntensity(rainIntensity); if (!paused) { mEffectManager->update(dt); mSky->update(dt); mWater->update(dt); if (mGroundcoverUpdater) { const MWWorld::Ptr& player = mPlayerAnimation->getPtr(); osg::Vec3f playerPos(player.getRefData().getPosition().asVec3()); float windSpeed = mSky->getBaseWindSpeed(); mGroundcoverUpdater->setWindSpeed(windSpeed); mGroundcoverUpdater->setPlayerPos(playerPos); } } 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); mStateUpdater->setFogStart(mFog->getFogStart(isUnderwater)); mStateUpdater->setFogEnd(mFog->getFogEnd(isUnderwater)); setFogColor(mFog->getFogColor(isUnderwater)); } void RenderingManager::updatePlayerPtr(const MWWorld::Ptr &ptr) { if(mPlayerAnimation.get()) { setupPlayer(ptr); mPlayerAnimation->updatePtr(ptr); } mCamera->attachTo(ptr); } void RenderingManager::removePlayer(const MWWorld::Ptr &player) { mWater->removeEmitter(player); } void RenderingManager::rotateObject(const MWWorld::Ptr &ptr, const osg::Quat& rot) { if(ptr == mCamera->getTrackingPtr() && !mCamera->isVanityOrPreviewModeEnabled()) { mCamera->rotateCameraToTrackingPtr(); } ptr.getRefData().getBaseNode()->setAttitude(rot); } void RenderingManager::moveObject(const MWWorld::Ptr &ptr, const osg::Vec3f &pos) { ptr.getRefData().getBaseNode()->setPosition(pos); } void RenderingManager::scaleObject(const MWWorld::Ptr &ptr, const osg::Vec3f &scale) { ptr.getRefData().getBaseNode()->setScale(scale); if (ptr == mCamera->getTrackingPtr()) // update height of camera mCamera->processViewChange(); } void RenderingManager::removeObject(const MWWorld::Ptr &ptr) { mActorsPaths->remove(ptr); mObjects->removeObject(ptr); mWater->removeEmitter(ptr); } void RenderingManager::setWaterEnabled(bool enabled) { mWater->setEnabled(enabled); mSky->setWaterEnabled(enabled); } void RenderingManager::setWaterHeight(float height) { mWater->setCullCallback(mTerrain->getHeightCullCallback(height, Mask_Water)); mWater->setHeight(height); mSky->setWaterHeight(height); } void RenderingManager::screenshot(osg::Image* image, int w, int h) { mScreenshotManager->screenshot(image, w, h); } bool RenderingManager::screenshot360(osg::Image* image) { if (mCamera->isVanityOrPreviewModeEnabled()) { Log(Debug::Warning) << "Spherical screenshots are not allowed in preview mode."; return false; } unsigned int maskBackup = mPlayerAnimation->getObjectRoot()->getNodeMask(); if (mCamera->isFirstPerson()) mPlayerAnimation->getObjectRoot()->setNodeMask(0); mScreenshotManager->screenshot360(image); mPlayerAnimation->getObjectRoot()->setNodeMask(maskBackup); return true; } osg::Vec4f RenderingManager::getScreenBounds(const osg::BoundingBox &worldbb) { if (!worldbb.valid()) return osg::Vec4f(); osg::Matrix viewProj = mViewer->getCamera()->getViewMatrix() * mViewer->getCamera()->getProjectionMatrix(); float min_x = 1.0f, max_x = 0.0f, min_y = 1.0f, max_y = 0.0f; for (int i=0; i<8; ++i) { osg::Vec3f corner = worldbb.corner(i); corner = corner * viewProj; float x = (corner.x() + 1.f) * 0.5f; float y = (corner.y() - 1.f) * (-0.5f); if (x < min_x) min_x = x; if (x > max_x) max_x = x; if (y < min_y) min_y = y; if (y > max_y) max_y = y; } return osg::Vec4f(min_x, min_y, max_x, max_y); } RenderingManager::RayResult getIntersectionResult (osgUtil::LineSegmentIntersector* intersector) { RenderingManager::RayResult result; result.mHit = false; result.mHitRefnum.unset(); result.mRatio = 0; if (intersector->containsIntersections()) { result.mHit = true; osgUtil::LineSegmentIntersector::Intersection intersection = intersector->getFirstIntersection(); result.mHitPointWorld = intersection.getWorldIntersectPoint(); result.mHitNormalWorld = intersection.getWorldIntersectNormal(); result.mRatio = intersection.ratio; PtrHolder* ptrHolder = nullptr; std::vector refnumMarkers; for (osg::NodePath::const_iterator it = intersection.nodePath.begin(); it != intersection.nodePath.end(); ++it) { osg::UserDataContainer* userDataContainer = (*it)->getUserDataContainer(); if (!userDataContainer) continue; for (unsigned int i=0; igetNumUserObjects(); ++i) { if (PtrHolder* p = dynamic_cast(userDataContainer->getUserObject(i))) ptrHolder = p; if (RefnumMarker* r = dynamic_cast(userDataContainer->getUserObject(i))) refnumMarkers.push_back(r); } } if (ptrHolder) result.mHitObject = ptrHolder->mPtr; unsigned int vertexCounter = 0; for (unsigned int i=0; imNumVertices || (intersectionIndex >= vertexCounter && intersectionIndex < vertexCounter + refnumMarkers[i]->mNumVertices)) { result.mHitRefnum = refnumMarkers[i]->mRefnum; break; } vertexCounter += refnumMarkers[i]->mNumVertices; } } return result; } osg::ref_ptr RenderingManager::getIntersectionVisitor(osgUtil::Intersector *intersector, bool ignorePlayer, bool ignoreActors) { if (!mIntersectionVisitor) mIntersectionVisitor = new osgUtil::IntersectionVisitor; mIntersectionVisitor->setTraversalNumber(mViewer->getFrameStamp()->getFrameNumber()); mIntersectionVisitor->setFrameStamp(mViewer->getFrameStamp()); mIntersectionVisitor->setIntersector(intersector); unsigned int mask = ~0u; mask &= ~(Mask_RenderToTexture|Mask_Sky|Mask_Debug|Mask_Effect|Mask_Water|Mask_SimpleWater|Mask_Groundcover); if (ignorePlayer) mask &= ~(Mask_Player); if (ignoreActors) mask &= ~(Mask_Actor|Mask_Player); mIntersectionVisitor->setTraversalMask(mask); return mIntersectionVisitor; } RenderingManager::RayResult RenderingManager::castRay(const osg::Vec3f& origin, const osg::Vec3f& dest, bool ignorePlayer, bool ignoreActors) { osg::ref_ptr intersector (new osgUtil::LineSegmentIntersector(osgUtil::LineSegmentIntersector::MODEL, origin, dest)); intersector->setIntersectionLimit(osgUtil::LineSegmentIntersector::LIMIT_NEAREST); mRootNode->accept(*getIntersectionVisitor(intersector, ignorePlayer, ignoreActors)); return getIntersectionResult(intersector); } RenderingManager::RayResult RenderingManager::castCameraToViewportRay(const float nX, const float nY, float maxDistance, bool ignorePlayer, bool ignoreActors) { osg::ref_ptr intersector (new osgUtil::LineSegmentIntersector(osgUtil::LineSegmentIntersector::PROJECTION, nX * 2.f - 1.f, nY * (-2.f) + 1.f)); osg::Vec3d dist (0.f, 0.f, -maxDistance); dist = dist * mViewer->getCamera()->getProjectionMatrix(); osg::Vec3d end = intersector->getEnd(); end.z() = dist.z(); intersector->setEnd(end); intersector->setIntersectionLimit(osgUtil::LineSegmentIntersector::LIMIT_NEAREST); mViewer->getCamera()->accept(*getIntersectionVisitor(intersector, ignorePlayer, ignoreActors)); return getIntersectionResult(intersector); } void RenderingManager::updatePtr(const MWWorld::Ptr &old, const MWWorld::Ptr &updated) { mObjects->updatePtr(old, updated); mActorsPaths->updatePtr(old, updated); } void RenderingManager::spawnEffect(const std::string &model, const std::string &texture, const osg::Vec3f &worldPosition, float scale, bool isMagicVFX) { mEffectManager->addEffect(model, texture, worldPosition, scale, isMagicVFX); } void RenderingManager::notifyWorldSpaceChanged() { mEffectManager->clear(); mWater->clearRipples(); } void RenderingManager::clear() { mSky->setMoonColour(false); notifyWorldSpaceChanged(); if (mObjectPaging) mObjectPaging->clear(); } MWRender::Animation* RenderingManager::getAnimation(const MWWorld::Ptr &ptr) { if (mPlayerAnimation.get() && ptr == mPlayerAnimation->getPtr()) return mPlayerAnimation.get(); return mObjects->getAnimation(ptr); } const MWRender::Animation* RenderingManager::getAnimation(const MWWorld::ConstPtr &ptr) const { if (mPlayerAnimation.get() && ptr == mPlayerAnimation->getPtr()) return mPlayerAnimation.get(); return mObjects->getAnimation(ptr); } void RenderingManager::setupPlayer(const MWWorld::Ptr &player) { if (!mPlayerNode) { mPlayerNode = new SceneUtil::PositionAttitudeTransform; mPlayerNode->setNodeMask(Mask_Player); mPlayerNode->setName("Player Root"); mSceneRoot->addChild(mPlayerNode); } mPlayerNode->setUserDataContainer(new osg::DefaultUserDataContainer); mPlayerNode->getUserDataContainer()->addUserObject(new PtrHolder(player)); player.getRefData().setBaseNode(mPlayerNode); mWater->removeEmitter(player); mWater->addEmitter(player); } void RenderingManager::renderPlayer(const MWWorld::Ptr &player) { mPlayerAnimation = new NpcAnimation(player, player.getRefData().getBaseNode(), mResourceSystem, 0, NpcAnimation::VM_Normal, mFirstPersonFieldOfView); mCamera->setAnimation(mPlayerAnimation.get()); mCamera->attachTo(player); } void RenderingManager::rebuildPtr(const MWWorld::Ptr &ptr) { NpcAnimation *anim = nullptr; if(ptr == mPlayerAnimation->getPtr()) anim = mPlayerAnimation.get(); else anim = dynamic_cast(mObjects->getAnimation(ptr)); if(anim) { anim->rebuild(); if(mCamera->getTrackingPtr() == ptr) { mCamera->attachTo(ptr); mCamera->setAnimation(anim); } } } void RenderingManager::addWaterRippleEmitter(const MWWorld::Ptr &ptr) { mWater->addEmitter(ptr); } void RenderingManager::removeWaterRippleEmitter(const MWWorld::Ptr &ptr) { mWater->removeEmitter(ptr); } void RenderingManager::emitWaterRipple(const osg::Vec3f &pos) { mWater->emitRipple(pos); } void RenderingManager::updateProjectionMatrix() { double aspect = mViewer->getCamera()->getViewport()->aspectRatio(); float fov = mFieldOfView; if (mFieldOfViewOverridden) fov = mFieldOfViewOverride; mViewer->getCamera()->setProjectionMatrixAsPerspective(fov, aspect, mNearClip, mViewDistance); mUniformNear->set(mNearClip); mUniformFar->set(mViewDistance); // 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. fov = std::min(mFieldOfView, 140.f); float distanceMult = std::cos(osg::DegreesToRadians(fov)/2.f); mTerrain->setViewDistance(mViewDistance * (distanceMult ? 1.f/distanceMult : 1.f)); if (mGroundcoverWorld) { float groundcoverDistance = std::max(0.f, Settings::Manager::getFloat("rendering distance", "Groundcover")); mGroundcoverWorld->setViewDistance(groundcoverDistance * (distanceMult ? 1.f/distanceMult : 1.f)); } } void RenderingManager::updateTextureFiltering() { mViewer->stopThreading(); mResourceSystem->getSceneManager()->setFilterSettings( Settings::Manager::getString("texture mag filter", "General"), Settings::Manager::getString("texture min filter", "General"), Settings::Manager::getString("texture mipmap", "General"), Settings::Manager::getInt("anisotropy", "General") ); mTerrain->updateTextureFiltering(); mViewer->startThreading(); } void RenderingManager::updateAmbient() { osg::Vec4f color = mAmbientColor; if (mNightEyeFactor > 0.f) color += osg::Vec4f(0.7, 0.7, 0.7, 0.0) * mNightEyeFactor; mStateUpdater->setAmbientColor(color); } void RenderingManager::setFogColor(const osg::Vec4f &color) { mViewer->getCamera()->setClearColor(color); mStateUpdater->setFogColor(color); } void RenderingManager::reportStats() const { osg::Stats* stats = mViewer->getViewerStats(); unsigned int frameNumber = mViewer->getFrameStamp()->getFrameNumber(); if (stats->collectStats("resource")) { stats->setAttribute(frameNumber, "UnrefQueue", mUnrefQueue->getNumItems()); mTerrain->reportStats(frameNumber, stats); } } void RenderingManager::processChangedSettings(const Settings::CategorySettingVector &changed) { 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(); } 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(); } else if (it->first == "General" && (it->second == "texture filter" || it->second == "texture mipmap" || it->second == "anisotropy")) { updateTextureFiltering(); } else if (it->first == "Water") { mWater->processChangedSettings(changed); } else if (it->first == "Shaders" && it->second == "minimum interior brightness") { mMinimumAmbientLuminance = std::clamp(Settings::Manager::getFloat("minimum interior brightness", "Shaders"), 0.f, 1.f); if (MWMechanics::getPlayer().isInCell()) configureAmbient(MWMechanics::getPlayer().getCell()->getCell()); } else if (it->first == "Shaders" && (it->second == "light bounds multiplier" || it->second == "maximum light distance" || it->second == "light fade start" || it->second == "max lights")) { auto* lightManager = static_cast(getLightRoot()); lightManager->processChangedSettings(changed); if (it->second == "max lights" && !lightManager->usingFFP()) { mViewer->stopThreading(); lightManager->updateMaxLights(); auto defines = mResourceSystem->getSceneManager()->getShaderManager().getGlobalDefines(); for (const auto& [name, key] : lightManager->getLightDefines()) defines[name] = key; mResourceSystem->getSceneManager()->getShaderManager().setGlobalDefines(defines); mSceneRoot->removeUpdateCallback(mStateUpdater); mStateUpdater = new StateUpdater; mSceneRoot->addUpdateCallback(mStateUpdater); mStateUpdater->setFogEnd(mViewDistance); updateAmbient(); mViewer->startThreading(); } } } } float RenderingManager::getNearClipDistance() const { return mNearClip; } float RenderingManager::getTerrainHeightAt(const osg::Vec3f &pos) { return mTerrain->getHeightAt(pos); } void RenderingManager::overrideFieldOfView(float val) { if (mFieldOfViewOverridden != true || mFieldOfViewOverride != val) { mFieldOfViewOverridden = true; mFieldOfViewOverride = val; updateProjectionMatrix(); } } osg::Vec3f RenderingManager::getHalfExtents(const MWWorld::ConstPtr& object) const { osg::Vec3f halfExtents(0, 0, 0); std::string modelName = object.getClass().getModel(object); if (modelName.empty()) return halfExtents; osg::ref_ptr node = mResourceSystem->getSceneManager()->getTemplate(modelName); osg::ComputeBoundsVisitor computeBoundsVisitor; computeBoundsVisitor.setTraversalMask(~(MWRender::Mask_ParticleSystem|MWRender::Mask_Effect)); const_cast(node.get())->accept(computeBoundsVisitor); osg::BoundingBox bounds = computeBoundsVisitor.getBoundingBox(); if (bounds.valid()) { halfExtents[0] = std::abs(bounds.xMax() - bounds.xMin()) / 2.f; halfExtents[1] = std::abs(bounds.yMax() - bounds.yMin()) / 2.f; halfExtents[2] = std::abs(bounds.zMax() - bounds.zMin()) / 2.f; } return halfExtents; } void RenderingManager::resetFieldOfView() { if (mFieldOfViewOverridden == true) { mFieldOfViewOverridden = false; updateProjectionMatrix(); } } void RenderingManager::exportSceneGraph(const MWWorld::Ptr &ptr, const std::string &filename, const std::string &format) { osg::Node* node = mViewer->getSceneData(); if (!ptr.isEmpty()) node = ptr.getRefData().getBaseNode(); SceneUtil::writeScene(node, filename, format); } LandManager *RenderingManager::getLandManager() const { return mTerrainStorage->getLandManager(); } void RenderingManager::updateActorPath(const MWWorld::ConstPtr& actor, const std::deque& path, const osg::Vec3f& halfExtents, const osg::Vec3f& start, const osg::Vec3f& end) const { mActorsPaths->update(actor, path, halfExtents, start, end, mNavigator.getSettings()); } void RenderingManager::removeActorPath(const MWWorld::ConstPtr& actor) const { mActorsPaths->remove(actor); } void RenderingManager::setNavMeshNumber(const std::size_t value) { mNavMeshNumber = value; } void RenderingManager::updateNavMesh() { if (!mNavMesh->isEnabled()) return; const auto navMeshes = mNavigator.getNavMeshes(); auto it = navMeshes.begin(); for (std::size_t i = 0; it != navMeshes.end() && i < mNavMeshNumber; ++i) ++it; if (it == navMeshes.end()) { mNavMesh->reset(); } else { try { const auto locked = it->second->lockConst(); mNavMesh->update(locked->getImpl(), mNavMeshNumber, locked->getGeneration(), locked->getNavMeshRevision(), mNavigator.getSettings()); } catch (const std::exception& e) { Log(Debug::Error) << "NavMesh render update exception: " << e.what(); } } } void RenderingManager::updateRecastMesh() { if (!mRecastMesh->isEnabled()) return; mRecastMesh->update(mNavigator.getRecastMeshTiles(), mNavigator.getSettings()); } void RenderingManager::setActiveGrid(const osg::Vec4i &grid) { mTerrain->setActiveGrid(grid); } bool RenderingManager::pagingEnableObject(int type, const MWWorld::ConstPtr& ptr, bool enabled) { if (!ptr.isInCell() || !ptr.getCell()->isExterior() || !mObjectPaging) return false; if (mObjectPaging->enableObject(type, ptr.getCellRef().getRefNum(), ptr.getCellRef().getPosition().asVec3(), osg::Vec2i(ptr.getCell()->getCell()->getGridX(), ptr.getCell()->getCell()->getGridY()), enabled)) { mTerrain->rebuildViews(); return true; } return false; } void RenderingManager::pagingBlacklistObject(int type, const MWWorld::ConstPtr &ptr) { if (!ptr.isInCell() || !ptr.getCell()->isExterior() || !mObjectPaging) return; const ESM::RefNum & refnum = ptr.getCellRef().getRefNum(); if (!refnum.hasContentFile()) return; if (mObjectPaging->blacklistObject(type, refnum, ptr.getCellRef().getPosition().asVec3(), osg::Vec2i(ptr.getCell()->getCell()->getGridX(), ptr.getCell()->getCell()->getGridY()))) mTerrain->rebuildViews(); } bool RenderingManager::pagingUnlockCache() { if (mObjectPaging && mObjectPaging->unlockCache()) { mTerrain->rebuildViews(); return true; } return false; } void RenderingManager::getPagedRefnums(const osg::Vec4i &activeGrid, std::set &out) { if (mObjectPaging) mObjectPaging->getPagedRefnums(activeGrid, out); } } ================================================ FILE: apps/openmw/mwrender/renderingmanager.hpp ================================================ #ifndef OPENMW_MWRENDER_RENDERINGMANAGER_H #define OPENMW_MWRENDER_RENDERINGMANAGER_H #include #include #include #include #include #include "objects.hpp" #include "renderinginterface.hpp" #include "rendermode.hpp" #include #include namespace osg { class Group; class PositionAttitudeTransform; } namespace osgUtil { class IntersectionVisitor; class Intersector; } namespace Resource { class ResourceSystem; } namespace osgViewer { class Viewer; } namespace ESM { struct Cell; struct RefNum; } namespace Terrain { class World; } namespace Fallback { class Map; } namespace SceneUtil { class ShadowManager; class WorkQueue; class UnrefQueue; } namespace DetourNavigator { struct Navigator; struct Settings; } namespace MWRender { class GroundcoverUpdater; class StateUpdater; class EffectManager; class ScreenshotManager; class FogManager; class SkyManager; class NpcAnimation; class Pathgrid; class Camera; class ViewOverShoulderController; class Water; class TerrainStorage; class LandManager; class NavMesh; class ActorsPaths; class RecastMesh; class ObjectPaging; class Groundcover; class RenderingManager : public MWRender::RenderingInterface { public: RenderingManager(osgViewer::Viewer* viewer, osg::ref_ptr rootNode, Resource::ResourceSystem* resourceSystem, SceneUtil::WorkQueue* workQueue, const std::string& resourcePath, DetourNavigator::Navigator& navigator); ~RenderingManager(); osgUtil::IncrementalCompileOperation* getIncrementalCompileOperation(); MWRender::Objects& getObjects() override; Resource::ResourceSystem* getResourceSystem(); SceneUtil::WorkQueue* getWorkQueue(); SceneUtil::UnrefQueue* getUnrefQueue(); Terrain::World* getTerrain(); osg::Uniform* mUniformNear; osg::Uniform* mUniformFar; void preloadCommonAssets(); double getReferenceTime() const; osg::Group* getLightRoot(); void setNightEyeFactor(float factor); void setAmbientColour(const osg::Vec4f& colour); void skySetDate(int day, int month); int skyGetMasserPhase() const; int skyGetSecundaPhase() const; void skySetMoonColour(bool red); void setSunDirection(const osg::Vec3f& direction); void setSunColour(const osg::Vec4f& diffuse, const osg::Vec4f& specular); void configureAmbient(const ESM::Cell* cell); void configureFog(const ESM::Cell* cell); void configureFog(float fogDepth, float underwaterFog, float dlFactor, float dlOffset, const osg::Vec4f& colour); void addCell(const MWWorld::CellStore* store); void removeCell(const MWWorld::CellStore* store); void enableTerrain(bool enable); void updatePtr(const MWWorld::Ptr& old, const MWWorld::Ptr& updated); void rotateObject(const MWWorld::Ptr& ptr, const osg::Quat& rot); void moveObject(const MWWorld::Ptr& ptr, const osg::Vec3f& pos); void scaleObject(const MWWorld::Ptr& ptr, const osg::Vec3f& scale); void removeObject(const MWWorld::Ptr& ptr); void setWaterEnabled(bool enabled); void setWaterHeight(float level); /// Take a screenshot of w*h onto the given image, not including the GUI. void screenshot(osg::Image* image, int w, int h); bool screenshot360(osg::Image* image); struct RayResult { bool mHit; osg::Vec3f mHitNormalWorld; osg::Vec3f mHitPointWorld; MWWorld::Ptr mHitObject; ESM::RefNum mHitRefnum; float mRatio; }; RayResult castRay(const osg::Vec3f& origin, const osg::Vec3f& dest, bool ignorePlayer, bool ignoreActors=false); /// Return the object under the mouse cursor / crosshair position, given by nX and nY normalized screen coordinates, /// where (0,0) is the top left corner. RayResult castCameraToViewportRay(const float nX, const float nY, float maxDistance, bool ignorePlayer, bool ignoreActors=false); /// Get the bounding box of the given object in screen coordinates as (minX, minY, maxX, maxY), with (0,0) being the top left corner. osg::Vec4f getScreenBounds(const osg::BoundingBox &worldbb); void setSkyEnabled(bool enabled); bool toggleRenderMode(RenderMode mode); SkyManager* getSkyManager(); void spawnEffect(const std::string &model, const std::string &texture, const osg::Vec3f &worldPosition, float scale = 1.f, bool isMagicVFX = true); /// Clear all savegame-specific data void clear(); /// Clear all worldspace-specific data void notifyWorldSpaceChanged(); void update(float dt, bool paused); Animation* getAnimation(const MWWorld::Ptr& ptr); const Animation* getAnimation(const MWWorld::ConstPtr& ptr) const; void addWaterRippleEmitter(const MWWorld::Ptr& ptr); void removeWaterRippleEmitter(const MWWorld::Ptr& ptr); void emitWaterRipple(const osg::Vec3f& pos); void updatePlayerPtr(const MWWorld::Ptr &ptr); void removePlayer(const MWWorld::Ptr& player); void setupPlayer(const MWWorld::Ptr& player); void renderPlayer(const MWWorld::Ptr& player); void rebuildPtr(const MWWorld::Ptr& ptr); void processChangedSettings(const Settings::CategorySettingVector& settings); float getNearClipDistance() const; float getTerrainHeightAt(const osg::Vec3f& pos); // 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); /// reset a previous overrideFieldOfView() call, i.e. revert to field of view specified in the settings file. void resetFieldOfView(); osg::Vec3f getHalfExtents(const MWWorld::ConstPtr& object) const; void exportSceneGraph(const MWWorld::Ptr& ptr, const std::string& filename, const std::string& format); LandManager* getLandManager() const; bool toggleBorders(); void updateActorPath(const MWWorld::ConstPtr& actor, const std::deque& path, const osg::Vec3f& halfExtents, const osg::Vec3f& start, const osg::Vec3f& end) const; void removeActorPath(const MWWorld::ConstPtr& actor) const; void setNavMeshNumber(const std::size_t value); void setActiveGrid(const osg::Vec4i &grid); bool pagingEnableObject(int type, const MWWorld::ConstPtr& ptr, bool enabled); void pagingBlacklistObject(int type, const MWWorld::ConstPtr &ptr); bool pagingUnlockCache(); void getPagedRefnums(const osg::Vec4i &activeGrid, std::set &out); private: void updateProjectionMatrix(); void updateTextureFiltering(); void updateAmbient(); void setFogColor(const osg::Vec4f& color); void updateThirdPersonViewMode(); void reportStats() const; void updateNavMesh(); void updateRecastMesh(); osg::ref_ptr getIntersectionVisitor(osgUtil::Intersector* intersector, bool ignorePlayer, bool ignoreActors); osg::ref_ptr mIntersectionVisitor; osg::ref_ptr mViewer; osg::ref_ptr mRootNode; osg::ref_ptr mSceneRoot; Resource::ResourceSystem* mResourceSystem; osg::ref_ptr mGroundcoverUpdater; osg::ref_ptr mWorkQueue; osg::ref_ptr mUnrefQueue; osg::ref_ptr mSunLight; DetourNavigator::Navigator& mNavigator; std::unique_ptr mNavMesh; std::size_t mNavMeshNumber = 0; std::unique_ptr mActorsPaths; std::unique_ptr mRecastMesh; std::unique_ptr mPathgrid; std::unique_ptr mObjects; std::unique_ptr mWater; std::unique_ptr mTerrain; std::unique_ptr mGroundcoverWorld; std::unique_ptr mTerrainStorage; std::unique_ptr mObjectPaging; std::unique_ptr mGroundcover; std::unique_ptr mSky; std::unique_ptr mFog; std::unique_ptr mScreenshotManager; std::unique_ptr mEffectManager; std::unique_ptr mShadowManager; osg::ref_ptr mPlayerAnimation; osg::ref_ptr mPlayerNode; std::unique_ptr mCamera; std::unique_ptr mViewOverShoulderController; osg::Vec3f mCurrentCameraPos; osg::ref_ptr mStateUpdater; osg::Vec4f mAmbientColor; float mMinimumAmbientLuminance; float mNightEyeFactor; float mNearClip; float mViewDistance; bool mFieldOfViewOverridden; float mFieldOfViewOverride; float mFieldOfView; float mFirstPersonFieldOfView; void operator = (const RenderingManager&); RenderingManager(const RenderingManager&); }; } #endif ================================================ FILE: apps/openmw/mwrender/rendermode.hpp ================================================ #ifndef OPENMW_MWRENDER_RENDERMODE_H #define OPENMW_MWRENDER_RENDERMODE_H namespace MWRender { enum RenderMode { Render_CollisionDebug, Render_Wireframe, Render_Pathgrid, Render_Water, Render_Scene, Render_NavMesh, Render_ActorsPaths, Render_RecastMesh, }; } #endif ================================================ FILE: apps/openmw/mwrender/ripplesimulation.cpp ================================================ #include "ripplesimulation.hpp" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "vismask.hpp" #include "../mwbase/world.hpp" #include "../mwbase/environment.hpp" #include "../mwmechanics/actorutil.hpp" namespace { void createWaterRippleStateSet(Resource::ResourceSystem* resourceSystem,osg::Node* node) { int rippleFrameCount = Fallback::Map::getInt("Water_RippleFrameCount"); if (rippleFrameCount <= 0) return; const std::string& tex = Fallback::Map::getString("Water_RippleTexture"); std::vector > textures; for (int i=0; i tex2 (new osg::Texture2D(resourceSystem->getImageManager()->getImage(texname.str()))); tex2->setWrap(osg::Texture::WRAP_S, osg::Texture::REPEAT); tex2->setWrap(osg::Texture::WRAP_T, osg::Texture::REPEAT); resourceSystem->getSceneManager()->applyFilterSettings(tex2); textures.push_back(tex2); } osg::ref_ptr controller (new NifOsg::FlipController(0, 0.3f/rippleFrameCount, textures)); controller->setSource(std::shared_ptr(new SceneUtil::FrameTimeSource)); node->addUpdateCallback(controller); osg::ref_ptr stateset (new osg::StateSet); stateset->setMode(GL_BLEND, osg::StateAttribute::ON); stateset->setMode(GL_CULL_FACE, osg::StateAttribute::OFF); stateset->setTextureAttributeAndModes(0, textures[0], osg::StateAttribute::ON); osg::ref_ptr depth (new osg::Depth); depth->setWriteMask(false); stateset->setAttributeAndModes(depth, osg::StateAttribute::ON); osg::ref_ptr polygonOffset (new osg::PolygonOffset); polygonOffset->setUnits(-1); polygonOffset->setFactor(-1); stateset->setAttributeAndModes(polygonOffset, osg::StateAttribute::ON); stateset->setRenderingHint(osg::StateSet::TRANSPARENT_BIN); osg::ref_ptr mat (new osg::Material); mat->setDiffuse(osg::Material::FRONT_AND_BACK, osg::Vec4f(0.f, 0.f, 0.f, 1.f)); mat->setAmbient(osg::Material::FRONT_AND_BACK, osg::Vec4f(1.f, 1.f, 1.f, 1.f)); mat->setEmission(osg::Material::FRONT_AND_BACK, osg::Vec4f(0.f, 0.f, 0.f, 1.f)); mat->setSpecular(osg::Material::FRONT_AND_BACK, osg::Vec4f(0.f, 0.f, 0.f, 0.f)); mat->setColorMode(osg::Material::DIFFUSE); stateset->setAttributeAndModes(mat, osg::StateAttribute::ON); node->setStateSet(stateset); } } namespace MWRender { RippleSimulation::RippleSimulation(osg::Group *parent, Resource::ResourceSystem* resourceSystem) : mParent(parent) { mParticleSystem = new osgParticle::ParticleSystem; mParticleSystem->setParticleAlignment(osgParticle::ParticleSystem::FIXED); mParticleSystem->setAlignVectorX(osg::Vec3f(1,0,0)); mParticleSystem->setAlignVectorY(osg::Vec3f(0,1,0)); osgParticle::Particle& particleTemplate = mParticleSystem->getDefaultParticleTemplate(); particleTemplate.setSizeRange(osgParticle::rangef(15, 180)); particleTemplate.setColorRange(osgParticle::rangev4(osg::Vec4f(1,1,1,0.7), osg::Vec4f(1,1,1,0.7))); particleTemplate.setAlphaRange(osgParticle::rangef(1.f, 0.f)); particleTemplate.setAngularVelocity(osg::Vec3f(0,0,Fallback::Map::getFloat("Water_RippleRotSpeed"))); particleTemplate.setLifeTime(Fallback::Map::getFloat("Water_RippleLifetime")); osg::ref_ptr updater (new osgParticle::ParticleSystemUpdater); updater->addParticleSystem(mParticleSystem); mParticleNode = new osg::PositionAttitudeTransform; mParticleNode->setName("Ripple Root"); mParticleNode->addChild(updater); mParticleNode->addChild(mParticleSystem); mParticleNode->setNodeMask(Mask_Water); createWaterRippleStateSet(resourceSystem, mParticleNode); resourceSystem->getSceneManager()->recreateShaders(mParticleNode); mParent->addChild(mParticleNode); } RippleSimulation::~RippleSimulation() { mParent->removeChild(mParticleNode); } void RippleSimulation::update(float dt) { const MWBase::World* world = MWBase::Environment::get().getWorld(); for (Emitter& emitter : mEmitters) { MWWorld::ConstPtr& ptr = emitter.mPtr; if (ptr == MWBase::Environment::get().getWorld ()->getPlayerPtr()) { // fetch a new ptr (to handle cell change etc) // for non-player actors this is done in updateObjectCell ptr = MWBase::Environment::get().getWorld ()->getPlayerPtr(); } osg::Vec3f currentPos (ptr.getRefData().getPosition().asVec3()); bool shouldEmit = (world->isUnderwater(ptr.getCell(), currentPos) && !world->isSubmerged(ptr)) || world->isWalkingOnWater(ptr); if (shouldEmit && (currentPos - emitter.mLastEmitPosition).length() > 10) { emitter.mLastEmitPosition = currentPos; currentPos.z() = mParticleNode->getPosition().z(); if (mParticleSystem->numParticles()-mParticleSystem->numDeadParticles() > 500) continue; // TODO: remove the oldest particle to make room? emitRipple(currentPos); } } } void RippleSimulation::addEmitter(const MWWorld::ConstPtr& ptr, float scale, float force) { Emitter newEmitter; newEmitter.mPtr = ptr; newEmitter.mScale = scale; newEmitter.mForce = force; newEmitter.mLastEmitPosition = osg::Vec3f(0,0,0); mEmitters.push_back (newEmitter); } void RippleSimulation::removeEmitter (const MWWorld::ConstPtr& ptr) { for (std::vector::iterator it = mEmitters.begin(); it != mEmitters.end(); ++it) { if (it->mPtr == ptr) { mEmitters.erase(it); return; } } } void RippleSimulation::updateEmitterPtr (const MWWorld::ConstPtr& old, const MWWorld::ConstPtr& ptr) { for (std::vector::iterator it = mEmitters.begin(); it != mEmitters.end(); ++it) { if (it->mPtr == old) { it->mPtr = ptr; return; } } } void RippleSimulation::removeCell(const MWWorld::CellStore *store) { for (std::vector::iterator it = mEmitters.begin(); it != mEmitters.end();) { if ((it->mPtr.isInCell() && it->mPtr.getCell() == store) && it->mPtr != MWMechanics::getPlayer()) { it = mEmitters.erase(it); } else ++it; } } void RippleSimulation::emitRipple(const osg::Vec3f &pos) { if (std::abs(pos.z() - mParticleNode->getPosition().z()) < 20) { osgParticle::ParticleSystem::ScopedWriteLock lock(*mParticleSystem->getReadWriteMutex()); osgParticle::Particle* p = mParticleSystem->createParticle(nullptr); p->setPosition(osg::Vec3f(pos.x(), pos.y(), 0.f)); p->setAngle(osg::Vec3f(0,0, Misc::Rng::rollProbability() * osg::PI * 2 - osg::PI)); } } void RippleSimulation::setWaterHeight(float height) { mParticleNode->setPosition(osg::Vec3f(0,0,height)); } void RippleSimulation::clear() { for (int i=0; inumParticles(); ++i) mParticleSystem->destroyParticle(i); } } ================================================ FILE: apps/openmw/mwrender/ripplesimulation.hpp ================================================ #ifndef OPENMW_MWRENDER_RIPPLESIMULATION_H #define OPENMW_MWRENDER_RIPPLESIMULATION_H #include #include "../mwworld/ptr.hpp" namespace osg { class Group; class PositionAttitudeTransform; } namespace osgParticle { class ParticleSystem; } namespace Resource { class ResourceSystem; } namespace Fallback { class Map; } namespace MWRender { struct Emitter { MWWorld::ConstPtr mPtr; osg::Vec3f mLastEmitPosition; float mScale; float mForce; }; class RippleSimulation { public: RippleSimulation(osg::Group* parent, Resource::ResourceSystem* resourceSystem); ~RippleSimulation(); /// @param dt Time since the last frame void update(float dt); /// adds an emitter, position will be tracked automatically void addEmitter (const MWWorld::ConstPtr& ptr, float scale = 1.f, float force = 1.f); void removeEmitter (const MWWorld::ConstPtr& ptr); void updateEmitterPtr (const MWWorld::ConstPtr& old, const MWWorld::ConstPtr& ptr); void removeCell(const MWWorld::CellStore* store); void emitRipple(const osg::Vec3f& pos); /// Change the height of the water surface, thus moving all ripples with it void setWaterHeight(float height); /// Remove all active ripples void clear(); private: osg::ref_ptr mParent; osg::ref_ptr mParticleSystem; osg::ref_ptr mParticleNode; std::vector mEmitters; }; } #endif ================================================ FILE: apps/openmw/mwrender/rotatecontroller.cpp ================================================ #include "rotatecontroller.hpp" #include namespace MWRender { RotateController::RotateController(osg::Node *relativeTo) : mEnabled(true) , mRelativeTo(relativeTo) { } void RotateController::setEnabled(bool enabled) { mEnabled = enabled; } void RotateController::setRotate(const osg::Quat &rotate) { mRotate = rotate; } void RotateController::operator()(osg::Node *node, osg::NodeVisitor *nv) { if (!mEnabled) { traverse(node, nv); return; } osg::MatrixTransform* transform = static_cast(node); osg::Matrix matrix = transform->getMatrix(); osg::Quat worldOrient = getWorldOrientation(node); osg::Quat orient = worldOrient * mRotate * worldOrient.inverse() * matrix.getRotate(); matrix.setRotate(orient); transform->setMatrix(matrix); traverse(node,nv); } osg::Quat RotateController::getWorldOrientation(osg::Node *node) { // this could be optimized later, we just need the world orientation, not the full matrix osg::NodePathList nodepaths = node->getParentalNodePaths(mRelativeTo); osg::Quat worldOrient; if (!nodepaths.empty()) { osg::Matrixf worldMat = osg::computeLocalToWorld(nodepaths[0]); worldOrient = worldMat.getRotate(); } return worldOrient; } } ================================================ FILE: apps/openmw/mwrender/rotatecontroller.hpp ================================================ #ifndef OPENMW_MWRENDER_ROTATECONTROLLER_H #define OPENMW_MWRENDER_ROTATECONTROLLER_H #include #include namespace MWRender { /// Applies a rotation in \a relativeTo's space. /// @note Assumes that the node being rotated has its "original" orientation set every frame by a different controller. /// The rotation is then applied on top of that orientation. /// @note Must be set on a MatrixTransform. class RotateController : public osg::NodeCallback { public: RotateController(osg::Node* relativeTo); void setEnabled(bool enabled); void setRotate(const osg::Quat& rotate); void operator()(osg::Node* node, osg::NodeVisitor* nv) override; protected: osg::Quat getWorldOrientation(osg::Node* node); bool mEnabled; osg::Quat mRotate; osg::Node* mRelativeTo; }; } #endif ================================================ FILE: apps/openmw/mwrender/screenshotmanager.cpp ================================================ #include "screenshotmanager.hpp" #include #include #include #include #include #include #include #include #include #include #include #include "../mwgui/loadingscreen.hpp" #include "../mwbase/environment.hpp" #include "../mwbase/windowmanager.hpp" #include "util.hpp" #include "vismask.hpp" #include "water.hpp" namespace MWRender { enum Screenshot360Type { Spherical, Cylindrical, Planet, RawCubemap }; class NotifyDrawCompletedCallback : public osg::Camera::DrawCallback { public: NotifyDrawCompletedCallback() : mDone(false), mFrame(0) { } void operator () (osg::RenderInfo& renderInfo) const override { std::lock_guard lock(mMutex); if (renderInfo.getState()->getFrameStamp()->getFrameNumber() >= mFrame && !mDone) { mDone = true; mCondition.notify_one(); } } void waitTillDone() { std::unique_lock lock(mMutex); if (mDone) return; mCondition.wait(lock); } void reset(unsigned int frame) { std::lock_guard lock(mMutex); mDone = false; mFrame = frame; } mutable std::condition_variable mCondition; mutable std::mutex mMutex; mutable bool mDone; unsigned int mFrame; }; class ReadImageFromFramebufferCallback : public osg::Drawable::DrawCallback { public: ReadImageFromFramebufferCallback(osg::Image* image, int width, int height) : mWidth(width), mHeight(height), mImage(image) { } void drawImplementation(osg::RenderInfo& renderInfo,const osg::Drawable* /*drawable*/) const override { int screenW = renderInfo.getCurrentCamera()->getViewport()->width(); int screenH = renderInfo.getCurrentCamera()->getViewport()->height(); double imageaspect = (double)mWidth/(double)mHeight; int leftPadding = std::max(0, static_cast(screenW - screenH * imageaspect) / 2); int topPadding = std::max(0, static_cast(screenH - screenW / imageaspect) / 2); int width = screenW - leftPadding*2; int height = screenH - topPadding*2; mImage->readPixels(leftPadding, topPadding, width, height, GL_RGB, GL_UNSIGNED_BYTE); mImage->scaleImage(mWidth, mHeight, 1); } private: int mWidth; int mHeight; osg::ref_ptr mImage; }; ScreenshotManager::ScreenshotManager(osgViewer::Viewer* viewer, osg::ref_ptr rootNode, osg::ref_ptr sceneRoot, Resource::ResourceSystem* resourceSystem, Water* water) : mViewer(viewer) , mRootNode(rootNode) , mSceneRoot(sceneRoot) , mDrawCompleteCallback(new NotifyDrawCompletedCallback) , mResourceSystem(resourceSystem) , mWater(water) { } ScreenshotManager::~ScreenshotManager() { } void ScreenshotManager::screenshot(osg::Image* image, int w, int h) { osg::Camera* camera = mViewer->getCamera(); osg::ref_ptr tempDrw = new osg::Drawable; tempDrw->setDrawCallback(new ReadImageFromFramebufferCallback(image, w, h)); tempDrw->setCullingActive(false); tempDrw->getOrCreateStateSet()->setRenderBinDetails(100, "RenderBin", osg::StateSet::USE_RENDERBIN_DETAILS); // so its after all scene bins but before POST_RENDER gui camera camera->addChild(tempDrw); traversalsAndWait(mViewer->getFrameStamp()->getFrameNumber()); // now that we've "used up" the current frame, get a fresh frame number for the next frame() following after the screenshot is completed mViewer->advance(mViewer->getFrameStamp()->getSimulationTime()); camera->removeChild(tempDrw); } bool ScreenshotManager::screenshot360(osg::Image* image) { int screenshotW = mViewer->getCamera()->getViewport()->width(); int screenshotH = mViewer->getCamera()->getViewport()->height(); Screenshot360Type screenshotMapping = Spherical; const std::string& settingStr = Settings::Manager::getString("screenshot type", "Video"); std::vector settingArgs; Misc::StringUtils::split(settingStr, settingArgs); if (settingArgs.size() > 0) { std::string typeStrings[4] = {"spherical", "cylindrical", "planet", "cubemap"}; bool found = false; for (int i = 0; i < 4; ++i) { if (settingArgs[0].compare(typeStrings[i]) == 0) { screenshotMapping = static_cast(i); found = true; break; } } if (!found) { Log(Debug::Warning) << "Wrong screenshot type: " << settingArgs[0] << "."; return false; } } // planet mapping needs higher resolution int cubeSize = screenshotMapping == Planet ? screenshotW : screenshotW / 2; if (settingArgs.size() > 1) screenshotW = std::min(10000, std::atoi(settingArgs[1].c_str())); if (settingArgs.size() > 2) screenshotH = std::min(10000, std::atoi(settingArgs[2].c_str())); if (settingArgs.size() > 3) cubeSize = std::min(5000, std::atoi(settingArgs[3].c_str())); bool rawCubemap = screenshotMapping == RawCubemap; if (rawCubemap) screenshotW = cubeSize * 6; // the image will consist of 6 cube sides in a row else if (screenshotMapping == Planet) screenshotH = screenshotW; // use square resolution for planet mapping std::vector> images; for (int i = 0; i < 6; ++i) images.push_back(new osg::Image); osg::Vec3 directions[6] = { rawCubemap ? osg::Vec3(1,0,0) : osg::Vec3(0,0,1), osg::Vec3(0,0,-1), osg::Vec3(-1,0,0), rawCubemap ? osg::Vec3(0,0,1) : osg::Vec3(1,0,0), osg::Vec3(0,1,0), osg::Vec3(0,-1,0)}; double rotations[] = { -osg::PI / 2.0, osg::PI / 2.0, osg::PI, 0, osg::PI / 2.0, osg::PI / 2.0 }; for (int i = 0; i < 6; ++i) // for each cubemap side { osg::Matrixd transform = osg::Matrixd::rotate(osg::Vec3(0,0,-1), directions[i]); if (!rawCubemap) transform *= osg::Matrixd::rotate(rotations[i],osg::Vec3(0,0,-1)); osg::Image *sideImage = images[i].get(); makeCubemapScreenshot(sideImage, cubeSize, cubeSize, transform); if (!rawCubemap) sideImage->flipHorizontal(); } if (rawCubemap) // for raw cubemap don't run on GPU, just merge the images { image->allocateImage(cubeSize * 6,cubeSize,images[0]->r(),images[0]->getPixelFormat(),images[0]->getDataType()); for (int i = 0; i < 6; ++i) osg::copyImage(images[i].get(),0,0,0,images[i]->s(),images[i]->t(),images[i]->r(),image,i * cubeSize,0,0); return true; } // run on GPU now: osg::ref_ptr cubeTexture (new osg::TextureCubeMap); cubeTexture->setResizeNonPowerOfTwoHint(false); cubeTexture->setFilter(osg::Texture::MIN_FILTER,osg::Texture::NEAREST); cubeTexture->setFilter(osg::Texture::MAG_FILTER,osg::Texture::NEAREST); cubeTexture->setWrap(osg::Texture::WRAP_S, osg::Texture::CLAMP_TO_EDGE); cubeTexture->setWrap(osg::Texture::WRAP_T, osg::Texture::CLAMP_TO_EDGE); for (int i = 0; i < 6; ++i) cubeTexture->setImage(i, images[i].get()); osg::ref_ptr screenshotCamera(new osg::Camera); osg::ref_ptr quad(new osg::ShapeDrawable(new osg::Box(osg::Vec3(0,0,0), 2.0))); std::map defineMap; Shader::ShaderManager& shaderMgr = mResourceSystem->getSceneManager()->getShaderManager(); osg::ref_ptr fragmentShader(shaderMgr.getShader("s360_fragment.glsl", defineMap,osg::Shader::FRAGMENT)); osg::ref_ptr vertexShader(shaderMgr.getShader("s360_vertex.glsl", defineMap, osg::Shader::VERTEX)); osg::ref_ptr stateset = new osg::StateSet; osg::ref_ptr program(new osg::Program); program->addShader(fragmentShader); program->addShader(vertexShader); stateset->setAttributeAndModes(program, osg::StateAttribute::ON); stateset->addUniform(new osg::Uniform("cubeMap", 0)); stateset->addUniform(new osg::Uniform("mapping", screenshotMapping)); stateset->setTextureAttributeAndModes(0, cubeTexture, osg::StateAttribute::ON); quad->setStateSet(stateset); quad->setUpdateCallback(nullptr); screenshotCamera->addChild(quad); renderCameraToImage(screenshotCamera, image, screenshotW, screenshotH); return true; } void ScreenshotManager::traversalsAndWait(unsigned int frame) { // Ref https://gitlab.com/OpenMW/openmw/-/issues/6013 mDrawCompleteCallback->reset(frame); mViewer->getCamera()->setFinalDrawCallback(mDrawCompleteCallback); mViewer->eventTraversal(); mViewer->updateTraversal(); mViewer->renderingTraversals(); mDrawCompleteCallback->waitTillDone(); } void ScreenshotManager::renderCameraToImage(osg::Camera *camera, osg::Image *image, int w, int h) { camera->setNodeMask(Mask_RenderToTexture); camera->attach(osg::Camera::COLOR_BUFFER, image); camera->setRenderOrder(osg::Camera::PRE_RENDER); camera->setReferenceFrame(osg::Camera::ABSOLUTE_RF); camera->setRenderTargetImplementation(osg::Camera::FRAME_BUFFER_OBJECT,osg::Camera::PIXEL_BUFFER_RTT); camera->setViewport(0, 0, w, h); osg::ref_ptr texture (new osg::Texture2D); texture->setInternalFormat(GL_RGB); texture->setTextureSize(w,h); texture->setResizeNonPowerOfTwoHint(false); texture->setFilter(osg::Texture::MIN_FILTER, osg::Texture::LINEAR); texture->setFilter(osg::Texture::MAG_FILTER, osg::Texture::LINEAR); camera->attach(osg::Camera::COLOR_BUFFER,texture); image->setDataType(GL_UNSIGNED_BYTE); image->setPixelFormat(texture->getInternalFormat()); mRootNode->addChild(camera); MWBase::Environment::get().getWindowManager()->getLoadingScreen()->loadingOn(false); // The draw needs to complete before we can copy back our image. traversalsAndWait(0); MWBase::Environment::get().getWindowManager()->getLoadingScreen()->loadingOff(); // now that we've "used up" the current frame, get a fresh framenumber for the next frame() following after the screenshot is completed mViewer->advance(mViewer->getFrameStamp()->getSimulationTime()); camera->removeChildren(0, camera->getNumChildren()); mRootNode->removeChild(camera); } void ScreenshotManager::makeCubemapScreenshot(osg::Image *image, int w, int h, osg::Matrixd cameraTransform) { osg::ref_ptr rttCamera (new osg::Camera); float nearClip = Settings::Manager::getFloat("near clip", "Camera"); float viewDistance = Settings::Manager::getFloat("viewing distance", "Camera"); // each cubemap side sees 90 degrees rttCamera->setProjectionMatrixAsPerspective(90.0, w/float(h), nearClip, viewDistance); rttCamera->setViewMatrix(mViewer->getCamera()->getViewMatrix() * cameraTransform); rttCamera->setUpdateCallback(new NoTraverseCallback); rttCamera->addChild(mSceneRoot); rttCamera->addChild(mWater->getReflectionCamera()); rttCamera->addChild(mWater->getRefractionCamera()); rttCamera->setCullMask(mViewer->getCamera()->getCullMask() & (~Mask_GUI)); rttCamera->setClearMask(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); renderCameraToImage(rttCamera.get(),image,w,h); } } ================================================ FILE: apps/openmw/mwrender/screenshotmanager.hpp ================================================ #ifndef MWRENDER_SCREENSHOTMANAGER_H #define MWRENDER_SCREENSHOTMANAGER_H #include #include #include #include namespace Resource { class ResourceSystem; } namespace MWRender { class Water; class NotifyDrawCompletedCallback; class ScreenshotManager { public: ScreenshotManager(osgViewer::Viewer* viewer, osg::ref_ptr rootNode, osg::ref_ptr sceneRoot, Resource::ResourceSystem* resourceSystem, Water* water); ~ScreenshotManager(); void screenshot(osg::Image* image, int w, int h); bool screenshot360(osg::Image* image); private: osg::ref_ptr mViewer; osg::ref_ptr mRootNode; osg::ref_ptr mSceneRoot; osg::ref_ptr mDrawCompleteCallback; Resource::ResourceSystem* mResourceSystem; Water* mWater; void traversalsAndWait(unsigned int frame); void renderCameraToImage(osg::Camera *camera, osg::Image *image, int w, int h); void makeCubemapScreenshot(osg::Image* image, int w, int h, osg::Matrixd cameraTransform=osg::Matrixd()); }; } #endif ================================================ FILE: apps/openmw/mwrender/sky.cpp ================================================ #include "sky.hpp" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "../mwbase/environment.hpp" #include "../mwbase/world.hpp" #include "vismask.hpp" #include "renderbin.hpp" namespace { osg::ref_ptr createAlphaTrackingUnlitMaterial() { osg::ref_ptr mat = new osg::Material; mat->setDiffuse(osg::Material::FRONT_AND_BACK, osg::Vec4f(0.f, 0.f, 0.f, 1.f)); mat->setAmbient(osg::Material::FRONT_AND_BACK, osg::Vec4f(0.f, 0.f, 0.f, 1.f)); mat->setEmission(osg::Material::FRONT_AND_BACK, osg::Vec4f(1.f, 1.f, 1.f, 1.f)); mat->setSpecular(osg::Material::FRONT_AND_BACK, osg::Vec4f(0.f, 0.f, 0.f, 0.f)); mat->setColorMode(osg::Material::DIFFUSE); return mat; } osg::ref_ptr createUnlitMaterial() { osg::ref_ptr mat = new osg::Material; mat->setDiffuse(osg::Material::FRONT_AND_BACK, osg::Vec4f(0.f, 0.f, 0.f, 1.f)); mat->setAmbient(osg::Material::FRONT_AND_BACK, osg::Vec4f(0.f, 0.f, 0.f, 1.f)); mat->setEmission(osg::Material::FRONT_AND_BACK, osg::Vec4f(1.f, 1.f, 1.f, 1.f)); mat->setSpecular(osg::Material::FRONT_AND_BACK, osg::Vec4f(0.f, 0.f, 0.f, 0.f)); mat->setColorMode(osg::Material::OFF); return mat; } osg::ref_ptr createTexturedQuad(int numUvSets=1) { osg::ref_ptr geom = new osg::Geometry; osg::ref_ptr verts = new osg::Vec3Array; verts->push_back(osg::Vec3f(-0.5, -0.5, 0)); verts->push_back(osg::Vec3f(-0.5, 0.5, 0)); verts->push_back(osg::Vec3f(0.5, 0.5, 0)); verts->push_back(osg::Vec3f(0.5, -0.5, 0)); geom->setVertexArray(verts); osg::ref_ptr texcoords = new osg::Vec2Array; texcoords->push_back(osg::Vec2f(0, 0)); texcoords->push_back(osg::Vec2f(0, 1)); texcoords->push_back(osg::Vec2f(1, 1)); texcoords->push_back(osg::Vec2f(1, 0)); osg::ref_ptr colors = new osg::Vec4Array; colors->push_back(osg::Vec4(1.f, 1.f, 1.f, 1.f)); geom->setColorArray(colors, osg::Array::BIND_OVERALL); for (int i=0; isetTexCoordArray(i, texcoords, osg::Array::BIND_PER_VERTEX); geom->addPrimitiveSet(new osg::DrawArrays(osg::PrimitiveSet::QUADS,0,4)); return geom; } } namespace MWRender { class AtmosphereUpdater : public SceneUtil::StateSetUpdater { public: void setEmissionColor(const osg::Vec4f& emissionColor) { mEmissionColor = emissionColor; } protected: void setDefaults(osg::StateSet* stateset) override { stateset->setAttributeAndModes(createAlphaTrackingUnlitMaterial(), osg::StateAttribute::ON|osg::StateAttribute::OVERRIDE); } void apply(osg::StateSet* stateset, osg::NodeVisitor* /*nv*/) override { osg::Material* mat = static_cast(stateset->getAttribute(osg::StateAttribute::MATERIAL)); mat->setEmission(osg::Material::FRONT_AND_BACK, mEmissionColor); } private: osg::Vec4f mEmissionColor; }; class AtmosphereNightUpdater : public SceneUtil::StateSetUpdater { public: AtmosphereNightUpdater(Resource::ImageManager* imageManager) { // we just need a texture, its contents don't really matter mTexture = new osg::Texture2D(imageManager->getWarningImage()); } void setFade(const float fade) { mColor.a() = fade; } protected: void setDefaults(osg::StateSet* stateset) override { osg::ref_ptr texEnv (new osg::TexEnvCombine); texEnv->setCombine_Alpha(osg::TexEnvCombine::MODULATE); texEnv->setSource0_Alpha(osg::TexEnvCombine::PREVIOUS); texEnv->setSource1_Alpha(osg::TexEnvCombine::CONSTANT); texEnv->setCombine_RGB(osg::TexEnvCombine::REPLACE); texEnv->setSource0_RGB(osg::TexEnvCombine::PREVIOUS); stateset->setTextureAttributeAndModes(1, mTexture, osg::StateAttribute::ON|osg::StateAttribute::OVERRIDE); stateset->setTextureAttributeAndModes(1, texEnv, osg::StateAttribute::ON|osg::StateAttribute::OVERRIDE); } void apply(osg::StateSet* stateset, osg::NodeVisitor* /*nv*/) override { osg::TexEnvCombine* texEnv = static_cast(stateset->getTextureAttribute(1, osg::StateAttribute::TEXENV)); texEnv->setConstantColor(mColor); } osg::ref_ptr mTexture; osg::Vec4f mColor; }; class CloudUpdater : public SceneUtil::StateSetUpdater { public: CloudUpdater() : mAnimationTimer(0.f) , mOpacity(0.f) { } void setAnimationTimer(float timer) { mAnimationTimer = timer; } void setTexture(osg::ref_ptr texture) { mTexture = texture; } void setEmissionColor(const osg::Vec4f& emissionColor) { mEmissionColor = emissionColor; } void setOpacity(float opacity) { mOpacity = opacity; } protected: void setDefaults(osg::StateSet *stateset) override { osg::ref_ptr texmat (new osg::TexMat); stateset->setTextureAttributeAndModes(0, texmat, osg::StateAttribute::ON); stateset->setTextureAttributeAndModes(1, texmat, osg::StateAttribute::ON); stateset->setAttribute(createAlphaTrackingUnlitMaterial(), osg::StateAttribute::ON|osg::StateAttribute::OVERRIDE); // need to set opacity on a separate texture unit, diffuse alpha is used by the vertex colors already osg::ref_ptr texEnvCombine (new osg::TexEnvCombine); texEnvCombine->setSource0_RGB(osg::TexEnvCombine::PREVIOUS); texEnvCombine->setSource0_Alpha(osg::TexEnvCombine::PREVIOUS); texEnvCombine->setSource1_Alpha(osg::TexEnvCombine::CONSTANT); texEnvCombine->setConstantColor(osg::Vec4f(1,1,1,1)); texEnvCombine->setCombine_Alpha(osg::TexEnvCombine::MODULATE); texEnvCombine->setCombine_RGB(osg::TexEnvCombine::REPLACE); stateset->setTextureAttributeAndModes(1, texEnvCombine, osg::StateAttribute::ON); stateset->setTextureMode(0, GL_TEXTURE_2D, osg::StateAttribute::ON|osg::StateAttribute::OVERRIDE); stateset->setTextureMode(1, GL_TEXTURE_2D, osg::StateAttribute::ON|osg::StateAttribute::OVERRIDE); } void apply(osg::StateSet *stateset, osg::NodeVisitor *nv) override { osg::TexMat* texMat = static_cast(stateset->getTextureAttribute(0, osg::StateAttribute::TEXMAT)); texMat->setMatrix(osg::Matrix::translate(osg::Vec3f(0, -mAnimationTimer, 0.f))); stateset->setTextureAttribute(0, mTexture, osg::StateAttribute::ON|osg::StateAttribute::OVERRIDE); stateset->setTextureAttribute(1, mTexture, osg::StateAttribute::ON|osg::StateAttribute::OVERRIDE); osg::Material* mat = static_cast(stateset->getAttribute(osg::StateAttribute::MATERIAL)); mat->setEmission(osg::Material::FRONT_AND_BACK, mEmissionColor); osg::TexEnvCombine* texEnvCombine = static_cast(stateset->getTextureAttribute(1, osg::StateAttribute::TEXENV)); texEnvCombine->setConstantColor(osg::Vec4f(1,1,1,mOpacity)); } private: float mAnimationTimer; osg::ref_ptr mTexture; osg::Vec4f mEmissionColor; float mOpacity; }; /// Transform that removes the eyepoint of the modelview matrix, /// i.e. its children are positioned relative to the camera. class CameraRelativeTransform : public osg::Transform { public: CameraRelativeTransform() { // Culling works in node-local space, not in camera space, so we can't cull this node correctly // That's not a problem though, children of this node can be culled just fine // Just make sure you do not place a CameraRelativeTransform deep in the scene graph setCullingActive(false); addCullCallback(new CullCallback); } CameraRelativeTransform(const CameraRelativeTransform& copy, const osg::CopyOp& copyop) : osg::Transform(copy, copyop) { } META_Node(MWRender, CameraRelativeTransform) const osg::Vec3f& getLastViewPoint() const { return mViewPoint; } bool computeLocalToWorldMatrix(osg::Matrix& matrix, osg::NodeVisitor* nv) const override { if (nv->getVisitorType() == osg::NodeVisitor::CULL_VISITOR) { mViewPoint = static_cast(nv)->getViewPoint(); } if (_referenceFrame==RELATIVE_RF) { matrix.setTrans(osg::Vec3f(0.f,0.f,0.f)); return false; } else // absolute { matrix.makeIdentity(); return true; } } osg::BoundingSphere computeBound() const override { return osg::BoundingSphere(osg::Vec3f(0,0,0), 0); } class CullCallback : public osg::NodeCallback { public: void operator() (osg::Node* node, osg::NodeVisitor* nv) override { osgUtil::CullVisitor* cv = static_cast(nv); // XXX have to remove unwanted culling plane of the water reflection camera // Remove all planes that aren't from the standard frustum unsigned int numPlanes = 4; if (cv->getCullingMode() & osg::CullSettings::NEAR_PLANE_CULLING) ++numPlanes; if (cv->getCullingMode() & osg::CullSettings::FAR_PLANE_CULLING) ++numPlanes; unsigned int mask = 0x1; unsigned int resultMask = cv->getProjectionCullingStack().back().getFrustum().getResultMask(); for (unsigned int i=0; igetProjectionCullingStack().back().getFrustum().getPlaneList().size(); ++i) { if (i >= numPlanes) { // turn off this culling plane resultMask &= (~mask); } mask <<= 1; } cv->getProjectionCullingStack().back().getFrustum().setResultMask(resultMask); cv->getCurrentCullingSet().getFrustum().setResultMask(resultMask); cv->getProjectionCullingStack().back().pushCurrentMask(); cv->getCurrentCullingSet().pushCurrentMask(); traverse(node, nv); cv->getProjectionCullingStack().back().popCurrentMask(); cv->getCurrentCullingSet().popCurrentMask(); } }; private: // viewPoint for the current frame mutable osg::Vec3f mViewPoint; }; class ModVertexAlphaVisitor : public osg::NodeVisitor { public: ModVertexAlphaVisitor(int meshType) : osg::NodeVisitor(TRAVERSE_ALL_CHILDREN) , mMeshType(meshType) { } void apply(osg::Drawable& drw) override { osg::Geometry* geom = drw.asGeometry(); if (!geom) return; osg::ref_ptr colors = new osg::Vec4Array(geom->getVertexArray()->getNumElements()); for (unsigned int i=0; isize(); ++i) { float alpha = 1.f; if (mMeshType == 0) alpha = (i%2) ? 0.f : 1.f; // this is a cylinder, so every second vertex belongs to the bottom-most row else if (mMeshType == 1) { if (i>= 49 && i <= 64) alpha = 0.f; // bottom-most row else if (i>= 33 && i <= 48) alpha = 0.25098; // second row else alpha = 1.f; } else if (mMeshType == 2) { if (geom->getColorArray()) { osg::Vec4Array* origColors = static_cast(geom->getColorArray()); alpha = ((*origColors)[i].x() == 1.f) ? 1.f : 0.f; } else alpha = 1.f; } (*colors)[i] = osg::Vec4f(0.f, 0.f, 0.f, alpha); } geom->setColorArray(colors, osg::Array::BIND_PER_VERTEX); } private: int mMeshType; }; /// @brief Hides the node subgraph if the eye point is below water. /// @note Must be added as cull callback. /// @note Meant to be used on a node that is child of a CameraRelativeTransform. /// The current view point must be retrieved by the CameraRelativeTransform since we can't get it anymore once we are in camera-relative space. class UnderwaterSwitchCallback : public osg::NodeCallback { public: UnderwaterSwitchCallback(CameraRelativeTransform* cameraRelativeTransform) : mCameraRelativeTransform(cameraRelativeTransform) , mEnabled(true) , mWaterLevel(0.f) { } bool isUnderwater() { osg::Vec3f viewPoint = mCameraRelativeTransform->getLastViewPoint(); return mEnabled && viewPoint.z() < mWaterLevel; } void operator()(osg::Node* node, osg::NodeVisitor* nv) override { if (isUnderwater()) return; traverse(node, nv); } void setEnabled(bool enabled) { mEnabled = enabled; } void setWaterLevel(float waterLevel) { mWaterLevel = waterLevel; } private: osg::ref_ptr mCameraRelativeTransform; bool mEnabled; float mWaterLevel; }; /// A base class for the sun and moons. class CelestialBody { public: CelestialBody(osg::Group* parentNode, float scaleFactor, int numUvSets, unsigned int visibleMask=~0u) : mVisibleMask(visibleMask) { mGeom = createTexturedQuad(numUvSets); mTransform = new osg::PositionAttitudeTransform; mTransform->setNodeMask(mVisibleMask); mTransform->setScale(osg::Vec3f(450,450,450) * scaleFactor); mTransform->addChild(mGeom); parentNode->addChild(mTransform); } virtual ~CelestialBody() {} virtual void adjustTransparency(const float ratio) = 0; void setVisible(bool visible) { mTransform->setNodeMask(visible ? mVisibleMask : 0); } protected: unsigned int mVisibleMask; static const float mDistance; osg::ref_ptr mTransform; osg::ref_ptr mGeom; }; const float CelestialBody::mDistance = 1000.0f; class Sun : public CelestialBody { public: Sun(osg::Group* parentNode, Resource::ImageManager& imageManager) : CelestialBody(parentNode, 1.0f, 1, Mask_Sun) , mUpdater(new Updater) { mTransform->addUpdateCallback(mUpdater); osg::ref_ptr 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); mGeom->getOrCreateStateSet()->setTextureAttributeAndModes(0, sunTex, osg::StateAttribute::ON); osg::ref_ptr queryNode (new osg::Group); // Need to render after the world geometry so we can correctly test for occlusions osg::StateSet* stateset = queryNode->getOrCreateStateSet(); 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 alphaFunc (new osg::AlphaFunc); alphaFunc->setFunction(osg::AlphaFunc::GREATER, 0.8); stateset->setAttributeAndModes(alphaFunc, osg::StateAttribute::ON); stateset->setTextureAttributeAndModes(0, sunTex, osg::StateAttribute::ON); stateset->setAttributeAndModes(createUnlitMaterial(), osg::StateAttribute::ON); // Disable writing to the color buffer. We are using this geometry for visibility tests only. osg::ref_ptr colormask (new osg::ColorMask(0, 0, 0, 0)); stateset->setAttributeAndModes(colormask, osg::StateAttribute::ON); mTransform->addChild(queryNode); mOcclusionQueryVisiblePixels = createOcclusionQueryNode(queryNode, true); mOcclusionQueryTotalPixels = createOcclusionQueryNode(queryNode, false); createSunFlash(imageManager); createSunGlare(); } ~Sun() { mTransform->removeUpdateCallback(mUpdater); destroySunFlash(); destroySunGlare(); } void setColor(const osg::Vec4f& color) { mUpdater->mColor.r() = color.r(); mUpdater->mColor.g() = color.g(); mUpdater->mColor.b() = color.b(); } void adjustTransparency(const float ratio) override { mUpdater->mColor.a() = ratio; if (mSunGlareCallback) mSunGlareCallback->setGlareView(ratio); if (mSunFlashCallback) mSunFlashCallback->setGlareView(ratio); } void setDirection(const osg::Vec3f& direction) { osg::Vec3f normalizedDirection = direction / direction.length(); mTransform->setPosition(normalizedDirection * mDistance); osg::Quat quat; quat.makeRotate(osg::Vec3f(0.0f, 0.0f, 1.0f), normalizedDirection); mTransform->setAttitude(quat); } void setGlareTimeOfDayFade(float val) { if (mSunGlareCallback) mSunGlareCallback->setTimeOfDayFade(val); } private: class DummyComputeBoundCallback : public osg::Node::ComputeBoundingSphereCallback { public: osg::BoundingSphere computeBound(const osg::Node& node) const override { return osg::BoundingSphere(); } }; /// @param queryVisible If true, queries the amount of visible pixels. If false, queries the total amount of pixels. osg::ref_ptr createOcclusionQueryNode(osg::Group* parent, bool queryVisible) { osg::ref_ptr oqn = new osg::OcclusionQueryNode; oqn->setQueriesEnabled(true); #if OSG_VERSION_GREATER_OR_EQUAL(3, 6, 5) // With OSG 3.6.5, the method of providing user defined query geometry has been completely replaced osg::ref_ptr queryGeom = new osg::QueryGeometry(oqn->getName()); #else osg::ref_ptr queryGeom = oqn->getQueryGeometry(); #endif // Make it fast! A DYNAMIC query geometry means we can't break frame until the flare is rendered (which is rendered after all the other geometry, // so that would be pretty bad). STATIC should be safe, since our node's local bounds are static, thus computeBounds() which modifies the queryGeometry // is only called once. // Note the debug geometry setDebugDisplay(true) is always DYNAMIC and that can't be changed, not a big deal. queryGeom->setDataVariance(osg::Object::STATIC); // Set up the query geometry to match the actual sun's rendering shape. osg::OcclusionQueryNode wasn't originally intended to allow this, // normally it would automatically adjust the query geometry to match the sub graph's bounding box. The below hack is needed to // circumvent this. queryGeom->setVertexArray(mGeom->getVertexArray()); queryGeom->setTexCoordArray(0, mGeom->getTexCoordArray(0), osg::Array::BIND_PER_VERTEX); queryGeom->removePrimitiveSet(0, queryGeom->getNumPrimitiveSets()); queryGeom->addPrimitiveSet(mGeom->getPrimitiveSet(0)); // Hack to disable unwanted awful code inside OcclusionQueryNode::computeBound. oqn->setComputeBoundingSphereCallback(new DummyComputeBoundCallback); // Still need a proper bounding sphere. oqn->setInitialBound(queryGeom->getBound()); #if OSG_VERSION_GREATER_OR_EQUAL(3, 6, 5) oqn->setQueryGeometry(queryGeom.release()); #endif osg::StateSet* queryStateSet = new osg::StateSet; if (queryVisible) { osg::ref_ptr depth (new osg::Depth); depth->setFunction(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. depth->setZNear(1.0); depth->setZFar(1.0); depth->setWriteMask(false); queryStateSet->setAttributeAndModes(depth, osg::StateAttribute::ON); } else { queryStateSet->setMode(GL_DEPTH_TEST, osg::StateAttribute::OFF); } oqn->setQueryStateSet(queryStateSet); parent->addChild(oqn); return oqn; } void createSunFlash(Resource::ImageManager& imageManager) { osg::ref_ptr tex (new osg::Texture2D(imageManager.getImage("textures/tx_sun_flash_grey_05.dds"))); tex->setWrap(osg::Texture::WRAP_S, osg::Texture::CLAMP_TO_EDGE); tex->setWrap(osg::Texture::WRAP_T, osg::Texture::CLAMP_TO_EDGE); osg::ref_ptr transform (new osg::PositionAttitudeTransform); const float scale = 2.6f; transform->setScale(osg::Vec3f(scale,scale,scale)); mTransform->addChild(transform); osg::ref_ptr geom = createTexturedQuad(); transform->addChild(geom); osg::StateSet* stateset = geom->getOrCreateStateSet(); stateset->setTextureAttributeAndModes(0, tex, osg::StateAttribute::ON); stateset->setMode(GL_DEPTH_TEST, osg::StateAttribute::OFF); stateset->setRenderBinDetails(RenderBin_SunGlare, "RenderBin"); stateset->setNestRenderBins(false); mSunFlashNode = transform; mSunFlashCallback = new SunFlashCallback(mOcclusionQueryVisiblePixels, mOcclusionQueryTotalPixels); mSunFlashNode->addCullCallback(mSunFlashCallback); } void destroySunFlash() { if (mSunFlashNode) { mSunFlashNode->removeCullCallback(mSunFlashCallback); mSunFlashCallback = nullptr; } } void createSunGlare() { osg::ref_ptr camera (new osg::Camera); camera->setProjectionMatrix(osg::Matrix::identity()); camera->setReferenceFrame(osg::Transform::ABSOLUTE_RF); // add to skyRoot instead? camera->setViewMatrix(osg::Matrix::identity()); camera->setClearMask(0); camera->setRenderOrder(osg::Camera::NESTED_RENDER); camera->setAllowEventFocus(false); osg::ref_ptr geom = osg::createTexturedQuadGeometry(osg::Vec3f(-1,-1,0), osg::Vec3f(2,0,0), osg::Vec3f(0,2,0)); camera->addChild(geom); osg::StateSet* stateset = geom->getOrCreateStateSet(); stateset->setRenderBinDetails(RenderBin_SunGlare, "RenderBin"); stateset->setNestRenderBins(false); stateset->setMode(GL_DEPTH_TEST, osg::StateAttribute::OFF); // set up additive blending osg::ref_ptr blendFunc (new osg::BlendFunc); blendFunc->setSource(osg::BlendFunc::SRC_ALPHA); blendFunc->setDestination(osg::BlendFunc::ONE); stateset->setAttributeAndModes(blendFunc, osg::StateAttribute::ON); mSunGlareCallback = new SunGlareCallback(mOcclusionQueryVisiblePixels, mOcclusionQueryTotalPixels, mTransform); mSunGlareNode = camera; mSunGlareNode->addCullCallback(mSunGlareCallback); mTransform->addChild(camera); } void destroySunGlare() { if (mSunGlareNode) { mSunGlareNode->removeCullCallback(mSunGlareCallback); mSunGlareCallback = nullptr; } } class Updater : public SceneUtil::StateSetUpdater { public: osg::Vec4f mColor; Updater() : mColor(1.f, 1.f, 1.f, 1.f) { } void setDefaults(osg::StateSet* stateset) override { stateset->setAttributeAndModes(createUnlitMaterial(), osg::StateAttribute::ON); } void apply(osg::StateSet* stateset, osg::NodeVisitor*) override { osg::Material* mat = static_cast(stateset->getAttribute(osg::StateAttribute::MATERIAL)); mat->setDiffuse(osg::Material::FRONT_AND_BACK, osg::Vec4f(0,0,0,mColor.a())); mat->setEmission(osg::Material::FRONT_AND_BACK, osg::Vec4f(mColor.r(), mColor.g(), mColor.b(), 1)); } }; class OcclusionCallback : public osg::NodeCallback { public: OcclusionCallback(osg::ref_ptr oqnVisible, osg::ref_ptr oqnTotal) : mOcclusionQueryVisiblePixels(oqnVisible) , mOcclusionQueryTotalPixels(oqnTotal) { } protected: float getVisibleRatio (osg::Camera* camera) { int visible = mOcclusionQueryVisiblePixels->getQueryGeometry()->getNumPixels(camera); int total = mOcclusionQueryTotalPixels->getQueryGeometry()->getNumPixels(camera); float visibleRatio = 0.f; if (total > 0) visibleRatio = static_cast(visible) / static_cast(total); float dt = MWBase::Environment::get().getFrameDuration(); float lastRatio = mLastRatio[osg::observer_ptr(camera)]; float change = dt*10; if (visibleRatio > lastRatio) visibleRatio = std::min(visibleRatio, lastRatio + change); else visibleRatio = std::max(visibleRatio, lastRatio - change); mLastRatio[osg::observer_ptr(camera)] = visibleRatio; return visibleRatio; } private: osg::ref_ptr mOcclusionQueryVisiblePixels; osg::ref_ptr mOcclusionQueryTotalPixels; std::map, float> mLastRatio; }; /// SunFlashCallback handles fading/scaling of a node depending on occlusion query result. Must be attached as a cull callback. class SunFlashCallback : public OcclusionCallback { public: SunFlashCallback(osg::ref_ptr oqnVisible, osg::ref_ptr oqnTotal) : OcclusionCallback(oqnVisible, oqnTotal) , mGlareView(1.f) { } void operator()(osg::Node* node, osg::NodeVisitor* nv) override { osgUtil::CullVisitor* cv = static_cast(nv); float visibleRatio = getVisibleRatio(cv->getCurrentCamera()); osg::ref_ptr stateset; if (visibleRatio > 0.f) { const float fadeThreshold = 0.1; if (visibleRatio < fadeThreshold) { float fade = 1.f - (fadeThreshold - visibleRatio) / fadeThreshold; osg::ref_ptr mat (createUnlitMaterial()); mat->setDiffuse(osg::Material::FRONT_AND_BACK, osg::Vec4f(0,0,0,fade*mGlareView)); stateset = new osg::StateSet; stateset->setAttributeAndModes(mat, osg::StateAttribute::ON|osg::StateAttribute::OVERRIDE); } const float threshold = 0.6; visibleRatio = visibleRatio * (1.f - threshold) + threshold; } float scale = visibleRatio; if (scale == 0.f) { // no traverse return; } else { osg::Matrix modelView = *cv->getModelViewMatrix(); modelView.preMultScale(osg::Vec3f(visibleRatio, visibleRatio, visibleRatio)); if (stateset) cv->pushStateSet(stateset); cv->pushModelViewMatrix(new osg::RefMatrix(modelView), osg::Transform::RELATIVE_RF); traverse(node, nv); cv->popModelViewMatrix(); if (stateset) cv->popStateSet(); } } void setGlareView(float value) { mGlareView = value; } private: float mGlareView; }; /// SunGlareCallback controls a full-screen glare effect depending on occlusion query result and the angle between sun and camera. /// Must be attached as a cull callback to the node above the glare node. class SunGlareCallback : public OcclusionCallback { public: SunGlareCallback(osg::ref_ptr oqnVisible, osg::ref_ptr oqnTotal, osg::ref_ptr sunTransform) : OcclusionCallback(oqnVisible, oqnTotal) , mSunTransform(sunTransform) , mTimeOfDayFade(1.f) , mGlareView(1.f) { mColor = Fallback::Map::getColour("Weather_Sun_Glare_Fader_Color"); mSunGlareFaderMax = Fallback::Map::getFloat("Weather_Sun_Glare_Fader_Max"); mSunGlareFaderAngleMax = Fallback::Map::getFloat("Weather_Sun_Glare_Fader_Angle_Max"); // Replicating a design flaw in MW. The color was being set on both ambient and emissive properties, which multiplies the result by two, // then finally gets clamped by the fixed function pipeline. With the default INI settings, only the red component gets clamped, // so the resulting color looks more orange than red. mColor *= 2; for (int i=0; i<3; ++i) mColor[i] = std::min(1.f, mColor[i]); } void operator ()(osg::Node* node, osg::NodeVisitor* nv) override { osgUtil::CullVisitor* cv = static_cast(nv); float angleRadians = getAngleToSunInRadians(*cv->getCurrentRenderStage()->getInitialViewMatrix()); float visibleRatio = getVisibleRatio(cv->getCurrentCamera()); const float angleMaxRadians = osg::DegreesToRadians(mSunGlareFaderAngleMax); float value = 1.f - std::min(1.f, angleRadians / angleMaxRadians); float fade = value * mSunGlareFaderMax; fade *= mTimeOfDayFade * mGlareView * visibleRatio; if (fade == 0.f) { // no traverse return; } else { osg::ref_ptr stateset (new osg::StateSet); osg::ref_ptr mat (createUnlitMaterial()); mat->setDiffuse(osg::Material::FRONT_AND_BACK, osg::Vec4f(0,0,0,fade)); mat->setEmission(osg::Material::FRONT_AND_BACK, mColor); stateset->setAttributeAndModes(mat, osg::StateAttribute::ON); cv->pushStateSet(stateset); traverse(node, nv); cv->popStateSet(); } } void setTimeOfDayFade(float val) { mTimeOfDayFade = val; } void setGlareView(float glareView) { mGlareView = glareView; } private: float getAngleToSunInRadians(const osg::Matrix& viewMatrix) const { osg::Vec3d eye, center, up; viewMatrix.getLookAt(eye, center, up); osg::Vec3d forward = center - eye; osg::Vec3d sun = mSunTransform->getPosition(); forward.normalize(); sun.normalize(); float angleRadians = std::acos(forward * sun); return angleRadians; } osg::ref_ptr mSunTransform; float mTimeOfDayFade; float mGlareView; osg::Vec4f mColor; float mSunGlareFaderMax; float mSunGlareFaderAngleMax; }; osg::ref_ptr mUpdater; osg::ref_ptr mSunFlashCallback; osg::ref_ptr mSunFlashNode; osg::ref_ptr mSunGlareCallback; osg::ref_ptr mSunGlareNode; osg::ref_ptr mOcclusionQueryVisiblePixels; osg::ref_ptr mOcclusionQueryTotalPixels; }; class Moon : public CelestialBody { public: enum Type { Type_Masser = 0, Type_Secunda }; Moon(osg::Group* parentNode, Resource::ImageManager& imageManager, float scaleFactor, Type type) : CelestialBody(parentNode, scaleFactor, 2) , mType(type) , mPhase(MoonState::Phase::Unspecified) , mUpdater(new Updater(imageManager)) { setPhase(MoonState::Phase::Full); setVisible(true); mGeom->addUpdateCallback(mUpdater); } ~Moon() { mGeom->removeUpdateCallback(mUpdater); } void adjustTransparency(const float ratio) override { mUpdater->mTransparency *= ratio; } void setState(const MoonState& state) { float radsX = ((state.mRotationFromHorizon) * static_cast(osg::PI)) / 180.0f; float radsZ = ((state.mRotationFromNorth) * static_cast(osg::PI)) / 180.0f; osg::Quat rotX(radsX, osg::Vec3f(1.0f, 0.0f, 0.0f)); osg::Quat rotZ(radsZ, osg::Vec3f(0.0f, 0.0f, 1.0f)); osg::Vec3f direction = rotX * rotZ * osg::Vec3f(0.0f, 1.0f, 0.0f); mTransform->setPosition(direction * mDistance); // The moon quad is initially oriented facing down, so we need to offset its X-axis // rotation to rotate it to face the camera when sitting at the horizon. osg::Quat attX((-static_cast(osg::PI) / 2.0f) + radsX, osg::Vec3f(1.0f, 0.0f, 0.0f)); mTransform->setAttitude(attX * rotZ); setPhase(state.mPhase); mUpdater->mTransparency = state.mMoonAlpha; mUpdater->mShadowBlend = state.mShadowBlend; } void setAtmosphereColor(const osg::Vec4f& color) { mUpdater->mAtmosphereColor = color; } void setColor(const osg::Vec4f& color) { mUpdater->mMoonColor = color; } unsigned int getPhaseInt() const { if (mPhase == MoonState::Phase::New) return 0; else if (mPhase == MoonState::Phase::WaxingCrescent) return 1; else if (mPhase == MoonState::Phase::WaningCrescent) return 1; else if (mPhase == MoonState::Phase::FirstQuarter) return 2; else if (mPhase == MoonState::Phase::ThirdQuarter) return 2; else if (mPhase == MoonState::Phase::WaxingGibbous) return 3; else if (mPhase == MoonState::Phase::WaningGibbous) return 3; else if (mPhase == MoonState::Phase::Full) return 4; return 0; } private: struct Updater : public SceneUtil::StateSetUpdater { Resource::ImageManager& mImageManager; osg::ref_ptr mPhaseTex; osg::ref_ptr mCircleTex; float mTransparency; float mShadowBlend; osg::Vec4f mAtmosphereColor; osg::Vec4f mMoonColor; Updater(Resource::ImageManager& imageManager) : mImageManager(imageManager) , mPhaseTex() , mCircleTex() , mTransparency(1.0f) , mShadowBlend(1.0f) , mAtmosphereColor(1.0f, 1.0f, 1.0f, 1.0f) , mMoonColor(1.0f, 1.0f, 1.0f, 1.0f) { } void setDefaults(osg::StateSet* stateset) override { stateset->setTextureAttributeAndModes(0, mPhaseTex, osg::StateAttribute::ON); osg::ref_ptr texEnv = new osg::TexEnvCombine; texEnv->setCombine_RGB(osg::TexEnvCombine::MODULATE); texEnv->setSource0_RGB(osg::TexEnvCombine::CONSTANT); texEnv->setSource1_RGB(osg::TexEnvCombine::TEXTURE); texEnv->setConstantColor(osg::Vec4f(1.f, 0.f, 0.f, 1.f)); // mShadowBlend * mMoonColor stateset->setTextureAttributeAndModes(0, texEnv, osg::StateAttribute::ON); stateset->setTextureAttributeAndModes(1, mCircleTex, osg::StateAttribute::ON); osg::ref_ptr texEnv2 = new osg::TexEnvCombine; texEnv2->setCombine_RGB(osg::TexEnvCombine::ADD); texEnv2->setCombine_Alpha(osg::TexEnvCombine::MODULATE); texEnv2->setSource0_Alpha(osg::TexEnvCombine::TEXTURE); texEnv2->setSource1_Alpha(osg::TexEnvCombine::CONSTANT); texEnv2->setSource0_RGB(osg::TexEnvCombine::PREVIOUS); texEnv2->setSource1_RGB(osg::TexEnvCombine::CONSTANT); texEnv2->setConstantColor(osg::Vec4f(0.f, 0.f, 0.f, 1.f)); // mAtmosphereColor.rgb, mTransparency stateset->setTextureAttributeAndModes(1, texEnv2, osg::StateAttribute::ON); stateset->setAttributeAndModes(createUnlitMaterial(), osg::StateAttribute::ON|osg::StateAttribute::OVERRIDE); } void apply(osg::StateSet* stateset, osg::NodeVisitor*) override { osg::TexEnvCombine* texEnv = static_cast(stateset->getTextureAttribute(0, osg::StateAttribute::TEXENV)); texEnv->setConstantColor(mMoonColor * mShadowBlend); osg::TexEnvCombine* texEnv2 = static_cast(stateset->getTextureAttribute(1, osg::StateAttribute::TEXENV)); texEnv2->setConstantColor(osg::Vec4f(mAtmosphereColor.x(), mAtmosphereColor.y(), mAtmosphereColor.z(), mTransparency)); } void setTextures(const std::string& phaseTex, const std::string& circleTex) { mPhaseTex = new osg::Texture2D(mImageManager.getImage(phaseTex)); mPhaseTex->setWrap(osg::Texture::WRAP_S, osg::Texture::CLAMP_TO_EDGE); mPhaseTex->setWrap(osg::Texture::WRAP_T, osg::Texture::CLAMP_TO_EDGE); mCircleTex = new osg::Texture2D(mImageManager.getImage(circleTex)); mCircleTex->setWrap(osg::Texture::WRAP_S, osg::Texture::CLAMP_TO_EDGE); mCircleTex->setWrap(osg::Texture::WRAP_T, osg::Texture::CLAMP_TO_EDGE); reset(); } }; Type mType; MoonState::Phase mPhase; osg::ref_ptr mUpdater; void setPhase(const MoonState::Phase& phase) { if(mPhase == phase) return; mPhase = phase; std::string textureName = "textures/tx_"; if (mType == Moon::Type_Secunda) textureName += "secunda_"; else textureName += "masser_"; if (phase == MoonState::Phase::New) textureName += "new"; else if(phase == MoonState::Phase::WaxingCrescent) textureName += "one_wax"; else if(phase == MoonState::Phase::FirstQuarter) textureName += "half_wax"; else if(phase == MoonState::Phase::WaxingGibbous) textureName += "three_wax"; else if(phase == MoonState::Phase::WaningCrescent) textureName += "one_wan"; else if(phase == MoonState::Phase::ThirdQuarter) textureName += "half_wan"; else if(phase == MoonState::Phase::WaningGibbous) textureName += "three_wan"; else if(phase == MoonState::Phase::Full) textureName += "full"; textureName += ".dds"; if (mType == Moon::Type_Secunda) mUpdater->setTextures(textureName, "textures/tx_mooncircle_full_s.dds"); else mUpdater->setTextures(textureName, "textures/tx_mooncircle_full_m.dds"); } }; SkyManager::SkyManager(osg::Group* parentNode, Resource::SceneManager* sceneManager) : mSceneManager(sceneManager) , mCamera(nullptr) , mAtmosphereNightRoll(0.f) , mCreated(false) , mIsStorm(false) , mDay(0) , mMonth(0) , mCloudAnimationTimer(0.f) , mRainTimer(0.f) , mStormDirection(0,1,0) , mClouds() , mNextClouds() , mCloudBlendFactor(0.0f) , mCloudSpeed(0.0f) , mStarsOpacity(0.0f) , mRemainingTransitionTime(0.0f) , mRainEnabled(false) , mRainSpeed(0) , mRainDiameter(0) , mRainMinHeight(0) , mRainMaxHeight(0) , mRainEntranceSpeed(1) , mRainMaxRaindrops(0) , mWindSpeed(0.f) , mBaseWindSpeed(0.f) , mEnabled(true) , mSunEnabled(true) , mPrecipitationAlpha(0.f) { osg::ref_ptr skyroot (new CameraRelativeTransform); skyroot->setName("Sky Root"); // Assign empty program to specify we don't want shaders // The shaders generated by the SceneManager can't handle everything we need skyroot->getOrCreateStateSet()->setAttributeAndModes(new osg::Program(), osg::StateAttribute::OVERRIDE|osg::StateAttribute::PROTECTED|osg::StateAttribute::ON); SceneUtil::ShadowManager::disableShadowsForStateSet(skyroot->getOrCreateStateSet()); skyroot->setNodeMask(Mask_Sky); parentNode->addChild(skyroot); mRootNode = skyroot; mEarlyRenderBinRoot = new osg::Group; // render before the world is rendered mEarlyRenderBinRoot->getOrCreateStateSet()->setRenderBinDetails(RenderBin_Sky, "RenderBin"); // Prevent unwanted clipping by water reflection camera's clipping plane mEarlyRenderBinRoot->getOrCreateStateSet()->setMode(GL_CLIP_PLANE0, osg::StateAttribute::OFF); mRootNode->addChild(mEarlyRenderBinRoot); mUnderwaterSwitch = new UnderwaterSwitchCallback(skyroot); } void SkyManager::create() { assert(!mCreated); mAtmosphereDay = mSceneManager->getInstance(Settings::Manager::getString("skyatmosphere", "Models"), mEarlyRenderBinRoot); ModVertexAlphaVisitor modAtmosphere(0); mAtmosphereDay->accept(modAtmosphere); mAtmosphereUpdater = new AtmosphereUpdater; mAtmosphereDay->addUpdateCallback(mAtmosphereUpdater); mAtmosphereNightNode = new osg::PositionAttitudeTransform; mAtmosphereNightNode->setNodeMask(0); mEarlyRenderBinRoot->addChild(mAtmosphereNightNode); osg::ref_ptr atmosphereNight; if (mSceneManager->getVFS()->exists(Settings::Manager::getString("skynight02", "Models"))) atmosphereNight = mSceneManager->getInstance(Settings::Manager::getString("skynight02", "Models"), mAtmosphereNightNode); else atmosphereNight = mSceneManager->getInstance(Settings::Manager::getString("skynight01", "Models"), mAtmosphereNightNode); atmosphereNight->getOrCreateStateSet()->setAttributeAndModes(createAlphaTrackingUnlitMaterial(), osg::StateAttribute::ON|osg::StateAttribute::OVERRIDE); ModVertexAlphaVisitor modStars(2); atmosphereNight->accept(modStars); mAtmosphereNightUpdater = new AtmosphereNightUpdater(mSceneManager->getImageManager()); atmosphereNight->addUpdateCallback(mAtmosphereNightUpdater); mSun.reset(new Sun(mEarlyRenderBinRoot, *mSceneManager->getImageManager())); mMasser.reset(new Moon(mEarlyRenderBinRoot, *mSceneManager->getImageManager(), Fallback::Map::getFloat("Moons_Masser_Size")/125, Moon::Type_Masser)); mSecunda.reset(new Moon(mEarlyRenderBinRoot, *mSceneManager->getImageManager(), Fallback::Map::getFloat("Moons_Secunda_Size")/125, Moon::Type_Secunda)); mCloudNode = new osg::PositionAttitudeTransform; mEarlyRenderBinRoot->addChild(mCloudNode); mCloudMesh = mSceneManager->getInstance(Settings::Manager::getString("skyclouds", "Models"), mCloudNode); ModVertexAlphaVisitor modClouds(1); mCloudMesh->accept(modClouds); mCloudUpdater = new CloudUpdater; mCloudUpdater->setOpacity(1.f); mCloudMesh->addUpdateCallback(mCloudUpdater); mCloudMesh2 = mSceneManager->getInstance(Settings::Manager::getString("skyclouds", "Models"), mCloudNode); mCloudMesh2->accept(modClouds); mCloudUpdater2 = new CloudUpdater; mCloudUpdater2->setOpacity(0.f); mCloudMesh2->addUpdateCallback(mCloudUpdater2); mCloudMesh2->setNodeMask(0); osg::ref_ptr depth = new osg::Depth; depth->setWriteMask(false); mEarlyRenderBinRoot->getOrCreateStateSet()->setAttributeAndModes(depth, osg::StateAttribute::ON); mEarlyRenderBinRoot->getOrCreateStateSet()->setMode(GL_BLEND, osg::StateAttribute::ON); mEarlyRenderBinRoot->getOrCreateStateSet()->setMode(GL_FOG, osg::StateAttribute::OFF); mMoonScriptColor = Fallback::Map::getColour("Moons_Script_Color"); mCreated = true; } class RainCounter : public osgParticle::ConstantRateCounter { public: int numParticlesToCreate(double dt) const override { // limit dt to avoid large particle emissions if there are jumps in the simulation time // 0.2 seconds is the same cap as used in Engine's frame loop dt = std::min(dt, 0.2); return ConstantRateCounter::numParticlesToCreate(dt); } }; class RainShooter : public osgParticle::Shooter { public: RainShooter() : mAngle(0.f) { } void shoot(osgParticle::Particle* particle) const override { particle->setVelocity(mVelocity); particle->setAngle(osg::Vec3f(-mAngle, 0, (Misc::Rng::rollProbability() * 2 - 1) * osg::PI)); } void setVelocity(const osg::Vec3f& velocity) { mVelocity = velocity; } void setAngle(float angle) { mAngle = angle; } osg::Object* cloneType() const override { return new RainShooter; } osg::Object* clone(const osg::CopyOp &) const override { return new RainShooter(*this); } private: osg::Vec3f mVelocity; float mAngle; }; // Updater for alpha value on a node's StateSet. Assumes the node has an existing Material StateAttribute. class AlphaFader : public SceneUtil::StateSetUpdater { public: /// @param alpha the variable alpha value is recovered from AlphaFader(float& alpha) : mAlpha(alpha) { } void setDefaults(osg::StateSet* stateset) override { // need to create a deep copy of StateAttributes we will modify osg::Material* mat = static_cast(stateset->getAttribute(osg::StateAttribute::MATERIAL)); stateset->setAttribute(osg::clone(mat, osg::CopyOp::DEEP_COPY_ALL), osg::StateAttribute::ON); } void apply(osg::StateSet* stateset, osg::NodeVisitor* nv) override { osg::Material* mat = static_cast(stateset->getAttribute(osg::StateAttribute::MATERIAL)); mat->setDiffuse(osg::Material::FRONT_AND_BACK, osg::Vec4f(0,0,0,mAlpha)); } // Helper for adding AlphaFaders to a subgraph class SetupVisitor : public osg::NodeVisitor { public: SetupVisitor(float &alpha) : osg::NodeVisitor(TRAVERSE_ALL_CHILDREN) , mAlpha(alpha) { } void apply(osg::Node &node) override { if (osg::StateSet* stateset = node.getStateSet()) { if (stateset->getAttribute(osg::StateAttribute::MATERIAL)) { SceneUtil::CompositeStateSetUpdater* composite = nullptr; osg::Callback* callback = node.getUpdateCallback(); while (callback) { composite = dynamic_cast(callback); if (composite) break; callback = callback->getNestedCallback(); } osg::ref_ptr alphaFader (new AlphaFader(mAlpha)); if (composite) composite->addController(alphaFader); else node.addUpdateCallback(alphaFader); } } traverse(node); } private: float &mAlpha; }; protected: float &mAlpha; }; void SkyManager::setCamera(osg::Camera *camera) { mCamera = camera; } class WrapAroundOperator : public osgParticle::Operator { public: WrapAroundOperator(osg::Camera *camera, const osg::Vec3 &wrapRange): osgParticle::Operator() { mCamera = camera; mWrapRange = wrapRange; mHalfWrapRange = mWrapRange / 2.0; mPreviousCameraPosition = getCameraPosition(); } osg::Object *cloneType() const override { return nullptr; } osg::Object *clone(const osg::CopyOp &op) const override { return nullptr; } void operate(osgParticle::Particle *P, double dt) override { } void operateParticles(osgParticle::ParticleSystem *ps, double dt) override { osg::Vec3 position = getCameraPosition(); osg::Vec3 positionDifference = position - mPreviousCameraPosition; osg::Matrix toWorld, toLocal; std::vector worldMatrices = ps->getWorldMatrices(); if (!worldMatrices.empty()) { toWorld = worldMatrices[0]; toLocal.invert(toWorld); } for (int i = 0; i < ps->numParticles(); ++i) { osgParticle::Particle *p = ps->getParticle(i); p->setPosition(toWorld.preMult(p->getPosition())); p->setPosition(p->getPosition() - positionDifference); for (int j = 0; j < 3; ++j) // wrap-around in all 3 dimensions { osg::Vec3 pos = p->getPosition(); if (pos[j] < -mHalfWrapRange[j]) pos[j] = mHalfWrapRange[j] + fmod(pos[j] - mHalfWrapRange[j],mWrapRange[j]); else if (pos[j] > mHalfWrapRange[j]) pos[j] = fmod(pos[j] + mHalfWrapRange[j],mWrapRange[j]) - mHalfWrapRange[j]; p->setPosition(pos); } p->setPosition(toLocal.preMult(p->getPosition())); } mPreviousCameraPosition = position; } protected: osg::Camera *mCamera; osg::Vec3 mPreviousCameraPosition; osg::Vec3 mWrapRange; osg::Vec3 mHalfWrapRange; osg::Vec3 getCameraPosition() { return mCamera->getInverseViewMatrix().getTrans(); } }; class WeatherAlphaOperator : public osgParticle::Operator { public: WeatherAlphaOperator(float& alpha, bool rain) : mAlpha(alpha) , mIsRain(rain) { } osg::Object *cloneType() const override { return nullptr; } osg::Object *clone(const osg::CopyOp &op) const override { return nullptr; } void operate(osgParticle::Particle *particle, double dt) override { constexpr float rainThreshold = 0.6f; // Rain_Threshold? const float alpha = mIsRain ? mAlpha * rainThreshold : mAlpha; particle->setAlphaRange(osgParticle::rangef(alpha, alpha)); } private: float &mAlpha; bool mIsRain; }; void SkyManager::createRain() { if (mRainNode) return; mRainNode = new osg::Group; mRainParticleSystem = new NifOsg::ParticleSystem; osg::Vec3 rainRange = osg::Vec3(mRainDiameter, mRainDiameter, (mRainMinHeight+mRainMaxHeight)/2.f); mRainParticleSystem->setParticleAlignment(osgParticle::ParticleSystem::FIXED); mRainParticleSystem->setAlignVectorX(osg::Vec3f(0.1,0,0)); mRainParticleSystem->setAlignVectorY(osg::Vec3f(0,0,1)); osg::ref_ptr stateset (mRainParticleSystem->getOrCreateStateSet()); osg::ref_ptr raindropTex (new osg::Texture2D(mSceneManager->getImageManager()->getImage("textures/tx_raindrop_01.dds"))); raindropTex->setWrap(osg::Texture::WRAP_S, osg::Texture::CLAMP_TO_EDGE); raindropTex->setWrap(osg::Texture::WRAP_T, osg::Texture::CLAMP_TO_EDGE); stateset->setTextureAttributeAndModes(0, raindropTex, osg::StateAttribute::ON); stateset->setNestRenderBins(false); stateset->setRenderingHint(osg::StateSet::TRANSPARENT_BIN); stateset->setMode(GL_CULL_FACE, osg::StateAttribute::OFF); stateset->setMode(GL_BLEND, osg::StateAttribute::ON); osg::ref_ptr mat (new osg::Material); mat->setAmbient(osg::Material::FRONT_AND_BACK, osg::Vec4f(1,1,1,1)); mat->setDiffuse(osg::Material::FRONT_AND_BACK, osg::Vec4f(1,1,1,1)); mat->setColorMode(osg::Material::AMBIENT_AND_DIFFUSE); stateset->setAttributeAndModes(mat, osg::StateAttribute::ON); osgParticle::Particle& particleTemplate = mRainParticleSystem->getDefaultParticleTemplate(); particleTemplate.setSizeRange(osgParticle::rangef(5.f, 15.f)); particleTemplate.setAlphaRange(osgParticle::rangef(1.f, 1.f)); particleTemplate.setLifeTime(1); osg::ref_ptr emitter (new osgParticle::ModularEmitter); emitter->setParticleSystem(mRainParticleSystem); osg::ref_ptr placer (new osgParticle::BoxPlacer); placer->setXRange(-rainRange.x() / 2, rainRange.x() / 2); placer->setYRange(-rainRange.y() / 2, rainRange.y() / 2); placer->setZRange(-rainRange.z() / 2, rainRange.z() / 2); emitter->setPlacer(placer); mPlacer = placer; // FIXME: vanilla engine does not use a particle system to handle rain, it uses a NIF-file with 20 raindrops in it. // It spawns the (maxRaindrops-getParticleSystem()->numParticles())*dt/rainEntranceSpeed batches every frame (near 1-2). // Since the rain is a regular geometry, it produces water ripples, also in theory it can be removed if collides with something. osg::ref_ptr counter (new RainCounter); counter->setNumberOfParticlesPerSecondToCreate(mRainMaxRaindrops/mRainEntranceSpeed*20); emitter->setCounter(counter); mCounter = counter; osg::ref_ptr shooter (new RainShooter); mRainShooter = shooter; emitter->setShooter(shooter); osg::ref_ptr updater (new osgParticle::ParticleSystemUpdater); updater->addParticleSystem(mRainParticleSystem); osg::ref_ptr program (new osgParticle::ModularProgram); program->addOperator(new WrapAroundOperator(mCamera,rainRange)); program->addOperator(new WeatherAlphaOperator(mPrecipitationAlpha, true)); program->setParticleSystem(mRainParticleSystem); mRainNode->addChild(program); mRainNode->addChild(emitter); mRainNode->addChild(mRainParticleSystem); mRainNode->addChild(updater); // Note: if we ever switch to regular geometry rain, it'll need to use an AlphaFader. mRainNode->addCullCallback(mUnderwaterSwitch); mRainNode->setNodeMask(Mask_WeatherParticles); mRootNode->addChild(mRainNode); } void SkyManager::destroyRain() { if (!mRainNode) return; mRootNode->removeChild(mRainNode); mRainNode = nullptr; mPlacer = nullptr; mCounter = nullptr; mRainParticleSystem = nullptr; mRainShooter = nullptr; } SkyManager::~SkyManager() { if (mRootNode) { mRootNode->getParent(0)->removeChild(mRootNode); mRootNode = nullptr; } } int SkyManager::getMasserPhase() const { if (!mCreated) return 0; return mMasser->getPhaseInt(); } int SkyManager::getSecundaPhase() const { if (!mCreated) return 0; return mSecunda->getPhaseInt(); } bool SkyManager::isEnabled() { return mEnabled; } bool SkyManager::hasRain() const { return mRainNode != nullptr; } float SkyManager::getPrecipitationAlpha() const { if (mEnabled && !mIsStorm && (hasRain() || mParticleNode)) return mPrecipitationAlpha; return 0.f; } void SkyManager::update(float duration) { if (!mEnabled) return; switchUnderwaterRain(); if (mIsStorm) { osg::Quat quat; quat.makeRotate(osg::Vec3f(0,1,0), mStormDirection); mCloudNode->setAttitude(quat); if (mParticleNode) { // Morrowind deliberately rotates the blizzard mesh, so so should we. if (mCurrentParticleEffect == Settings::Manager::getString("weatherblizzard", "Models")) quat.makeRotate(osg::Vec3f(-1,0,0), mStormDirection); mParticleNode->setAttitude(quat); } } else mCloudNode->setAttitude(osg::Quat()); // UV Scroll the clouds mCloudAnimationTimer += duration * mCloudSpeed * 0.003; mCloudUpdater->setAnimationTimer(mCloudAnimationTimer); mCloudUpdater2->setAnimationTimer(mCloudAnimationTimer); // rotate the stars by 360 degrees every 4 days mAtmosphereNightRoll += MWBase::Environment::get().getWorld()->getTimeScaleFactor()*duration*osg::DegreesToRadians(360.f) / (3600*96.f); if (mAtmosphereNightNode->getNodeMask() != 0) mAtmosphereNightNode->setAttitude(osg::Quat(mAtmosphereNightRoll, osg::Vec3f(0,0,1))); } void SkyManager::setEnabled(bool enabled) { if (enabled && !mCreated) create(); mRootNode->setNodeMask(enabled ? Mask_Sky : 0u); mEnabled = enabled; } void SkyManager::setMoonColour (bool red) { if (!mCreated) return; mSecunda->setColor(red ? mMoonScriptColor : osg::Vec4f(1,1,1,1)); } void SkyManager::updateRainParameters() { if (mRainShooter) { float angle = -std::atan(mWindSpeed/50.f); mRainShooter->setVelocity(osg::Vec3f(0, mRainSpeed*std::sin(angle), -mRainSpeed/std::cos(angle))); mRainShooter->setAngle(angle); osg::Vec3 rainRange = osg::Vec3(mRainDiameter, mRainDiameter, (mRainMinHeight+mRainMaxHeight)/2.f); mPlacer->setXRange(-rainRange.x() / 2, rainRange.x() / 2); mPlacer->setYRange(-rainRange.y() / 2, rainRange.y() / 2); mPlacer->setZRange(-rainRange.z() / 2, rainRange.z() / 2); mCounter->setNumberOfParticlesPerSecondToCreate(mRainMaxRaindrops/mRainEntranceSpeed*20); } } void SkyManager::switchUnderwaterRain() { if (!mRainParticleSystem) return; bool freeze = mUnderwaterSwitch->isUnderwater(); mRainParticleSystem->setFrozen(freeze); } void SkyManager::setWeather(const WeatherResult& weather) { if (!mCreated) return; mRainEntranceSpeed = weather.mRainEntranceSpeed; mRainMaxRaindrops = weather.mRainMaxRaindrops; mRainDiameter = weather.mRainDiameter; mRainMinHeight = weather.mRainMinHeight; mRainMaxHeight = weather.mRainMaxHeight; mRainSpeed = weather.mRainSpeed; mWindSpeed = weather.mWindSpeed; mBaseWindSpeed = weather.mBaseWindSpeed; if (mRainEffect != weather.mRainEffect) { mRainEffect = weather.mRainEffect; if (!mRainEffect.empty()) { createRain(); } else { destroyRain(); } } updateRainParameters(); mIsStorm = weather.mIsStorm; if (mCurrentParticleEffect != weather.mParticleEffect) { mCurrentParticleEffect = weather.mParticleEffect; // cleanup old particles if (mParticleEffect) { mParticleNode->removeChild(mParticleEffect); mParticleEffect = nullptr; } if (mCurrentParticleEffect.empty()) { if (mParticleNode) { mRootNode->removeChild(mParticleNode); mParticleNode = nullptr; } } else { if (!mParticleNode) { mParticleNode = new osg::PositionAttitudeTransform; mParticleNode->addCullCallback(mUnderwaterSwitch); mParticleNode->setNodeMask(Mask_WeatherParticles); mRootNode->addChild(mParticleNode); } mParticleEffect = mSceneManager->getInstance(mCurrentParticleEffect, mParticleNode); SceneUtil::AssignControllerSourcesVisitor assignVisitor(std::shared_ptr(new SceneUtil::FrameTimeSource)); mParticleEffect->accept(assignVisitor); AlphaFader::SetupVisitor alphaFaderSetupVisitor(mPrecipitationAlpha); mParticleEffect->accept(alphaFaderSetupVisitor); SceneUtil::DisableFreezeOnCullVisitor disableFreezeOnCullVisitor; mParticleEffect->accept(disableFreezeOnCullVisitor); SceneUtil::FindByClassVisitor findPSVisitor(std::string("ParticleSystem")); mParticleEffect->accept(findPSVisitor); for (unsigned int i = 0; i < findPSVisitor.mFoundNodes.size(); ++i) { osgParticle::ParticleSystem *ps = static_cast(findPSVisitor.mFoundNodes[i]); osg::ref_ptr program (new osgParticle::ModularProgram); if (!mIsStorm) program->addOperator(new WrapAroundOperator(mCamera,osg::Vec3(1024,1024,800))); program->addOperator(new WeatherAlphaOperator(mPrecipitationAlpha, false)); program->setParticleSystem(ps); mParticleNode->addChild(program); } } } if (mClouds != weather.mCloudTexture) { mClouds = weather.mCloudTexture; std::string texture = Misc::ResourceHelpers::correctTexturePath(mClouds, mSceneManager->getVFS()); osg::ref_ptr cloudTex (new osg::Texture2D(mSceneManager->getImageManager()->getImage(texture))); cloudTex->setWrap(osg::Texture::WRAP_S, osg::Texture::REPEAT); cloudTex->setWrap(osg::Texture::WRAP_T, osg::Texture::REPEAT); mCloudUpdater->setTexture(cloudTex); } if (mNextClouds != weather.mNextCloudTexture) { mNextClouds = weather.mNextCloudTexture; if (!mNextClouds.empty()) { std::string texture = Misc::ResourceHelpers::correctTexturePath(mNextClouds, mSceneManager->getVFS()); osg::ref_ptr cloudTex (new osg::Texture2D(mSceneManager->getImageManager()->getImage(texture))); cloudTex->setWrap(osg::Texture::WRAP_S, osg::Texture::REPEAT); cloudTex->setWrap(osg::Texture::WRAP_T, osg::Texture::REPEAT); mCloudUpdater2->setTexture(cloudTex); } } if (mCloudBlendFactor != weather.mCloudBlendFactor) { mCloudBlendFactor = weather.mCloudBlendFactor; mCloudUpdater->setOpacity((1.f-mCloudBlendFactor)); mCloudUpdater2->setOpacity(mCloudBlendFactor); mCloudMesh2->setNodeMask(mCloudBlendFactor > 0.f ? ~0u : 0); } if (mCloudColour != weather.mFogColor) { osg::Vec4f clr (weather.mFogColor); clr += osg::Vec4f(0.13f, 0.13f, 0.13f, 0.f); mCloudUpdater->setEmissionColor(clr); mCloudUpdater2->setEmissionColor(clr); mCloudColour = weather.mFogColor; } if (mSkyColour != weather.mSkyColor) { mSkyColour = weather.mSkyColor; mAtmosphereUpdater->setEmissionColor(mSkyColour); mMasser->setAtmosphereColor(mSkyColour); mSecunda->setAtmosphereColor(mSkyColour); } if (mFogColour != weather.mFogColor) { mFogColour = weather.mFogColor; } mCloudSpeed = weather.mCloudSpeed; mMasser->adjustTransparency(weather.mGlareView); mSecunda->adjustTransparency(weather.mGlareView); mSun->setColor(weather.mSunDiscColor); mSun->adjustTransparency(weather.mGlareView * weather.mSunDiscColor.a()); float nextStarsOpacity = weather.mNightFade * weather.mGlareView; if (weather.mNight && mStarsOpacity != nextStarsOpacity) { mStarsOpacity = nextStarsOpacity; mAtmosphereNightUpdater->setFade(mStarsOpacity); } mAtmosphereNightNode->setNodeMask(weather.mNight ? ~0u : 0); mPrecipitationAlpha = weather.mPrecipitationAlpha; } float SkyManager::getBaseWindSpeed() const { if (!mCreated) return 0.f; return mBaseWindSpeed; } void SkyManager::sunEnable() { if (!mCreated) return; mSun->setVisible(true); } void SkyManager::sunDisable() { if (!mCreated) return; mSun->setVisible(false); } void SkyManager::setStormDirection(const osg::Vec3f &direction) { mStormDirection = direction; } void SkyManager::setSunDirection(const osg::Vec3f& direction) { if (!mCreated) return; mSun->setDirection(direction); } void SkyManager::setMasserState(const MoonState& state) { if(!mCreated) return; mMasser->setState(state); } void SkyManager::setSecundaState(const MoonState& state) { if(!mCreated) return; mSecunda->setState(state); } void SkyManager::setDate(int day, int month) { mDay = day; mMonth = month; } void SkyManager::setGlareTimeOfDayFade(float val) { mSun->setGlareTimeOfDayFade(val); } void SkyManager::setWaterHeight(float height) { mUnderwaterSwitch->setWaterLevel(height); } void SkyManager::listAssetsToPreload(std::vector& models, std::vector& textures) { models.emplace_back(Settings::Manager::getString("skyatmosphere", "Models")); if (mSceneManager->getVFS()->exists(Settings::Manager::getString("skynight02", "Models"))) models.emplace_back(Settings::Manager::getString("skynight02", "Models")); models.emplace_back(Settings::Manager::getString("skynight01", "Models")); models.emplace_back(Settings::Manager::getString("skyclouds", "Models")); models.emplace_back(Settings::Manager::getString("weatherashcloud", "Models")); models.emplace_back(Settings::Manager::getString("weatherblightcloud", "Models")); models.emplace_back(Settings::Manager::getString("weathersnow", "Models")); models.emplace_back(Settings::Manager::getString("weatherblizzard", "Models")); textures.emplace_back("textures/tx_mooncircle_full_s.dds"); textures.emplace_back("textures/tx_mooncircle_full_m.dds"); textures.emplace_back("textures/tx_masser_new.dds"); textures.emplace_back("textures/tx_masser_one_wax.dds"); textures.emplace_back("textures/tx_masser_half_wax.dds"); textures.emplace_back("textures/tx_masser_three_wax.dds"); textures.emplace_back("textures/tx_masser_one_wan.dds"); textures.emplace_back("textures/tx_masser_half_wan.dds"); textures.emplace_back("textures/tx_masser_three_wan.dds"); textures.emplace_back("textures/tx_masser_full.dds"); textures.emplace_back("textures/tx_secunda_new.dds"); textures.emplace_back("textures/tx_secunda_one_wax.dds"); textures.emplace_back("textures/tx_secunda_half_wax.dds"); textures.emplace_back("textures/tx_secunda_three_wax.dds"); textures.emplace_back("textures/tx_secunda_one_wan.dds"); textures.emplace_back("textures/tx_secunda_half_wan.dds"); textures.emplace_back("textures/tx_secunda_three_wan.dds"); textures.emplace_back("textures/tx_secunda_full.dds"); textures.emplace_back("textures/tx_sun_05.dds"); textures.emplace_back("textures/tx_sun_flash_grey_05.dds"); textures.emplace_back("textures/tx_raindrop_01.dds"); } void SkyManager::setWaterEnabled(bool enabled) { mUnderwaterSwitch->setEnabled(enabled); } } ================================================ FILE: apps/openmw/mwrender/sky.hpp ================================================ #ifndef OPENMW_MWRENDER_SKY_H #define OPENMW_MWRENDER_SKY_H #include #include #include #include #include namespace osg { class Camera; } namespace osg { class Group; class Node; class Material; class PositionAttitudeTransform; } namespace osgParticle { class ParticleSystem; class BoxPlacer; } namespace Resource { class SceneManager; } namespace MWRender { class AtmosphereUpdater; class AtmosphereNightUpdater; class CloudUpdater; class Sun; class Moon; class RainCounter; class RainShooter; class RainFader; class AlphaFader; class UnderwaterSwitchCallback; struct WeatherResult { std::string mCloudTexture; std::string mNextCloudTexture; float mCloudBlendFactor; osg::Vec4f mFogColor; osg::Vec4f mAmbientColor; osg::Vec4f mSkyColor; // sun light color osg::Vec4f mSunColor; // alpha is the sun transparency osg::Vec4f mSunDiscColor; float mFogDepth; float mDLFogFactor; float mDLFogOffset; float mWindSpeed; float mBaseWindSpeed; float mCurrentWindSpeed; float mNextWindSpeed; float mCloudSpeed; float mGlareView; bool mNight; // use night skybox float mNightFade; // fading factor for night skybox bool mIsStorm; std::string mAmbientLoopSoundID; float mAmbientSoundVolume; std::string mParticleEffect; std::string mRainEffect; float mPrecipitationAlpha; float mRainDiameter; float mRainMinHeight; float mRainMaxHeight; float mRainSpeed; float mRainEntranceSpeed; int mRainMaxRaindrops; }; struct MoonState { enum class Phase { Full = 0, WaningGibbous, ThirdQuarter, WaningCrescent, New, WaxingCrescent, FirstQuarter, WaxingGibbous, Unspecified }; float mRotationFromHorizon; float mRotationFromNorth; Phase mPhase; float mShadowBlend; float mMoonAlpha; }; ///@brief The SkyManager handles rendering of the sky domes, celestial bodies as well as other objects that need to be rendered /// relative to the camera (e.g. weather particle effects) class SkyManager { public: SkyManager(osg::Group* parentNode, Resource::SceneManager* sceneManager); ~SkyManager(); void update(float duration); void setEnabled(bool enabled); void setHour (double hour); ///< will be called even when sky is disabled. void setDate (int day, int month); ///< will be called even when sky is disabled. int getMasserPhase() const; ///< 0 new moon, 1 waxing or waning cresecent, 2 waxing or waning half, /// 3 waxing or waning gibbous, 4 full moon int getSecundaPhase() const; ///< 0 new moon, 1 waxing or waning cresecent, 2 waxing or waning half, /// 3 waxing or waning gibbous, 4 full moon void setMoonColour (bool red); ///< change Secunda colour to red void setWeather(const WeatherResult& weather); void sunEnable(); void sunDisable(); bool isEnabled(); bool hasRain() const; float getPrecipitationAlpha() const; void setRainSpeed(float speed); void setStormDirection(const osg::Vec3f& direction); void setSunDirection(const osg::Vec3f& direction); void setMasserState(const MoonState& state); void setSecundaState(const MoonState& state); void setGlareTimeOfDayFade(float val); /// Enable or disable the water plane (used to remove underwater weather particles) void setWaterEnabled(bool enabled); /// Set height of water plane (used to remove underwater weather particles) void setWaterHeight(float height); void listAssetsToPreload(std::vector& models, std::vector& textures); void setCamera(osg::Camera *camera); float getBaseWindSpeed() const; private: void create(); ///< no need to call this, automatically done on first enable() void createRain(); void destroyRain(); void switchUnderwaterRain(); void updateRainParameters(); Resource::SceneManager* mSceneManager; osg::Camera *mCamera; osg::ref_ptr mRootNode; osg::ref_ptr mEarlyRenderBinRoot; osg::ref_ptr mParticleNode; osg::ref_ptr mParticleEffect; osg::ref_ptr mUnderwaterSwitch; osg::ref_ptr mCloudNode; osg::ref_ptr mCloudUpdater; osg::ref_ptr mCloudUpdater2; osg::ref_ptr mCloudMesh; osg::ref_ptr mCloudMesh2; osg::ref_ptr mAtmosphereDay; osg::ref_ptr mAtmosphereNightNode; float mAtmosphereNightRoll; osg::ref_ptr mAtmosphereNightUpdater; osg::ref_ptr mAtmosphereUpdater; std::unique_ptr mSun; std::unique_ptr mMasser; std::unique_ptr mSecunda; osg::ref_ptr mRainNode; osg::ref_ptr mRainParticleSystem; osg::ref_ptr mPlacer; osg::ref_ptr mCounter; osg::ref_ptr mRainShooter; bool mCreated; bool mIsStorm; int mDay; int mMonth; float mCloudAnimationTimer; float mRainTimer; osg::Vec3f mStormDirection; // remember some settings so we don't have to apply them again if they didn't change std::string mClouds; std::string mNextClouds; float mCloudBlendFactor; float mCloudSpeed; float mStarsOpacity; osg::Vec4f mCloudColour; osg::Vec4f mSkyColour; osg::Vec4f mFogColour; std::string mCurrentParticleEffect; float mRemainingTransitionTime; bool mRainEnabled; std::string mRainEffect; float mRainSpeed; float mRainDiameter; float mRainMinHeight; float mRainMaxHeight; float mRainEntranceSpeed; int mRainMaxRaindrops; float mWindSpeed; float mBaseWindSpeed; bool mEnabled; bool mSunEnabled; float mPrecipitationAlpha; osg::Vec4f mMoonScriptColor; }; } #endif // GAME_RENDER_SKY_H ================================================ FILE: apps/openmw/mwrender/terrainstorage.cpp ================================================ #include "terrainstorage.hpp" #include "../mwbase/world.hpp" #include "../mwbase/environment.hpp" #include "../mwworld/esmstore.hpp" #include "landmanager.hpp" namespace MWRender { TerrainStorage::TerrainStorage(Resource::ResourceSystem* resourceSystem, const std::string& normalMapPattern, const std::string& normalHeightMapPattern, bool autoUseNormalMaps, const std::string& specularMapPattern, bool autoUseSpecularMaps) : ESMTerrain::Storage(resourceSystem->getVFS(), normalMapPattern, normalHeightMapPattern, autoUseNormalMaps, specularMapPattern, autoUseSpecularMaps) , mLandManager(new LandManager(ESM::Land::DATA_VCLR|ESM::Land::DATA_VHGT|ESM::Land::DATA_VNML|ESM::Land::DATA_VTEX)) , mResourceSystem(resourceSystem) { mResourceSystem->addResourceManager(mLandManager.get()); } TerrainStorage::~TerrainStorage() { mResourceSystem->removeResourceManager(mLandManager.get()); } bool TerrainStorage::hasData(int cellX, int cellY) { const MWWorld::ESMStore &esmStore = MWBase::Environment::get().getWorld()->getStore(); const ESM::Land* land = esmStore.get().search(cellX, cellY); return land != nullptr; } void TerrainStorage::getBounds(float& minX, float& maxX, float& minY, float& maxY) { minX = 0, minY = 0, maxX = 0, maxY = 0; const MWWorld::ESMStore &esmStore = MWBase::Environment::get().getWorld()->getStore(); MWWorld::Store::iterator it = esmStore.get().begin(); for (; it != esmStore.get().end(); ++it) { if (it->mX < minX) minX = static_cast(it->mX); if (it->mX > maxX) maxX = static_cast(it->mX); if (it->mY < minY) minY = static_cast(it->mY); if (it->mY > maxY) maxY = static_cast(it->mY); } // since grid coords are at cell origin, we need to add 1 cell maxX += 1; maxY += 1; } LandManager *TerrainStorage::getLandManager() const { return mLandManager.get(); } osg::ref_ptr TerrainStorage::getLand(int cellX, int cellY) { return mLandManager->getLand(cellX, cellY); } const ESM::LandTexture* TerrainStorage::getLandTexture(int index, short plugin) { const MWWorld::ESMStore &esmStore = MWBase::Environment::get().getWorld()->getStore(); return esmStore.get().search(index, plugin); } } ================================================ FILE: apps/openmw/mwrender/terrainstorage.hpp ================================================ #ifndef MWRENDER_TERRAINSTORAGE_H #define MWRENDER_TERRAINSTORAGE_H #include #include #include namespace MWRender { class LandManager; /// @brief Connects the ESM Store used in OpenMW with the ESMTerrain storage. class TerrainStorage : public ESMTerrain::Storage { public: TerrainStorage(Resource::ResourceSystem* resourceSystem, const std::string& normalMapPattern = "", const std::string& normalHeightMapPattern = "", bool autoUseNormalMaps = false, const std::string& specularMapPattern = "", bool autoUseSpecularMaps = false); ~TerrainStorage(); osg::ref_ptr getLand (int cellX, int cellY) override; const ESM::LandTexture* getLandTexture(int index, short plugin) override; bool hasData(int cellX, int cellY) override; /// Get bounds of the whole terrain in cell units void getBounds(float& minX, float& maxX, float& minY, float& maxY) override; LandManager* getLandManager() const; private: std::unique_ptr mLandManager; Resource::ResourceSystem* mResourceSystem; }; } #endif ================================================ FILE: apps/openmw/mwrender/util.cpp ================================================ #include "util.hpp" #include #include #include #include #include #include namespace MWRender { class TextureOverrideVisitor : public osg::NodeVisitor { public: TextureOverrideVisitor(const std::string& texture, Resource::ResourceSystem* resourcesystem) : osg::NodeVisitor(TRAVERSE_ALL_CHILDREN) , mTexture(texture) , mResourcesystem(resourcesystem) { } void apply(osg::Node& node) override { int index = 0; osg::ref_ptr nodePtr(&node); if (node.getUserValue("overrideFx", index)) { if (index == 1) overrideTexture(mTexture, mResourcesystem, nodePtr); } traverse(node); } std::string mTexture; Resource::ResourceSystem* mResourcesystem; }; void overrideFirstRootTexture(const std::string &texture, Resource::ResourceSystem *resourceSystem, osg::ref_ptr node) { TextureOverrideVisitor overrideVisitor(texture, resourceSystem); node->accept(overrideVisitor); } void overrideTexture(const std::string &texture, Resource::ResourceSystem *resourceSystem, osg::ref_ptr node) { if (texture.empty()) return; std::string correctedTexture = Misc::ResourceHelpers::correctTexturePath(texture, resourceSystem->getVFS()); // Not sure if wrap settings should be pulled from the overridden texture? osg::ref_ptr tex = new osg::Texture2D(resourceSystem->getImageManager()->getImage(correctedTexture)); tex->setWrap(osg::Texture::WRAP_S, osg::Texture::CLAMP_TO_EDGE); tex->setWrap(osg::Texture::WRAP_T, osg::Texture::CLAMP_TO_EDGE); tex->setName("diffuseMap"); osg::ref_ptr stateset; if (node->getStateSet()) stateset = new osg::StateSet(*node->getStateSet(), osg::CopyOp::SHALLOW_COPY); else stateset = new osg::StateSet; stateset->setTextureAttribute(0, tex, osg::StateAttribute::OVERRIDE); node->setStateSet(stateset); } } ================================================ FILE: apps/openmw/mwrender/util.hpp ================================================ #ifndef OPENMW_MWRENDER_UTIL_H #define OPENMW_MWRENDER_UTIL_H #include #include #include namespace osg { class Node; } namespace Resource { class ResourceSystem; } namespace MWRender { // Overrides the texture of nodes in the mesh that had the same NiTexturingProperty as the first NiTexturingProperty of the .NIF file's root node, // if it had a NiTexturingProperty. Used for applying "particle textures" to magic effects. void overrideFirstRootTexture(const std::string &texture, Resource::ResourceSystem *resourceSystem, osg::ref_ptr node); void overrideTexture(const std::string& texture, Resource::ResourceSystem* resourceSystem, osg::ref_ptr node); // Node callback to entirely skip the traversal. class NoTraverseCallback : public osg::NodeCallback { public: void operator()(osg::Node* node, osg::NodeVisitor* nv) override { // no traverse() } }; } #endif ================================================ FILE: apps/openmw/mwrender/viewovershoulder.cpp ================================================ #include "viewovershoulder.hpp" #include #include #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; } } ================================================ FILE: apps/openmw/mwrender/viewovershoulder.hpp ================================================ #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 ================================================ FILE: apps/openmw/mwrender/vismask.hpp ================================================ #ifndef OPENMW_MWRENDER_VISMASK_H #define OPENMW_MWRENDER_VISMASK_H namespace MWRender { /// Node masks used for controlling visibility of game objects. /// @par Any node in the OSG scene graph can have a node mask. When traversing the scene graph, /// the node visitor's traversal mask is bitwise AND'ed with the node mask. If the result of this test is /// 0, then the node and all its child nodes are not processed. /// @par Important traversal masks are the camera's cull mask (determines what is visible), /// the update visitor mask (what is updated) and the intersection visitor mask (what is /// selectable through mouse clicks or other intersection tests). /// @par In practice, it can be useful to make a "hierarchy" out of the node masks - e.g. in OpenMW, /// all 3D rendering nodes are child of a Scene Root node with Mask_Scene. When we do not want 3D rendering, /// we can just omit Mask_Scene from the traversal mask, and do not need to omit all the individual /// element masks (water, sky, terrain, etc.) since the traversal will already have stopped at the Scene root node. /// @par The comments within the VisMask enum should give some hints as to what masks are commonly "child" of /// another mask, or what type of node this mask is usually set on. /// @note The mask values are not serialized within models, nor used in any other way that would break backwards /// compatibility if the enumeration values were to be changed. Feel free to change them when it makes sense. enum VisMask : unsigned int { Mask_UpdateVisitor = 0x1, // reserved for separating UpdateVisitors from CullVisitors // child of Scene Mask_Effect = (1<<1), Mask_Debug = (1<<2), Mask_Actor = (1<<3), Mask_Player = (1<<4), Mask_Sky = (1<<5), Mask_Water = (1<<6), // choose Water or SimpleWater depending on detail required Mask_SimpleWater = (1<<7), Mask_Terrain = (1<<8), Mask_FirstPerson = (1<<9), Mask_Object = (1<<10), Mask_Static = (1<<11), // child of Sky Mask_Sun = (1<<12), Mask_WeatherParticles = (1<<13), // top level masks Mask_Scene = (1<<14), Mask_GUI = (1<<15), // Set on a ParticleSystem Drawable Mask_ParticleSystem = (1<<16), // Set on cameras within the main scene graph Mask_RenderToTexture = (1<<17), Mask_PreCompile = (1<<18), // Set on a camera's cull mask to enable the LightManager Mask_Lighting = (1<<19), Mask_Groundcover = (1<<20), }; } #endif ================================================ FILE: apps/openmw/mwrender/water.cpp ================================================ #include "water.hpp" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "../mwworld/cellstore.hpp" #include "vismask.hpp" #include "ripplesimulation.hpp" #include "renderbin.hpp" #include "util.hpp" namespace MWRender { // -------------------------------------------------------------------------------------------------------------------------------- /// @brief Allows to cull and clip meshes that are below a plane. Useful for reflection & refraction camera effects. /// Also handles flipping of the plane when the eye point goes below it. /// To use, simply create the scene as subgraph of this node, then do setPlane(const osg::Plane& plane); class ClipCullNode : public osg::Group { class PlaneCullCallback : public osg::NodeCallback { public: /// @param cullPlane The culling plane (in world space). PlaneCullCallback(const osg::Plane* cullPlane) : osg::NodeCallback() , mCullPlane(cullPlane) { } void operator()(osg::Node* node, osg::NodeVisitor* nv) override { osgUtil::CullVisitor* cv = static_cast(nv); osg::Polytope::PlaneList origPlaneList = cv->getProjectionCullingStack().back().getFrustum().getPlaneList(); osg::Plane plane = *mCullPlane; plane.transform(*cv->getCurrentRenderStage()->getInitialViewMatrix()); osg::Vec3d eyePoint = cv->getEyePoint(); if (mCullPlane->intersect(osg::BoundingSphere(osg::Vec3d(0,0,eyePoint.z()), 0)) > 0) plane.flip(); cv->getProjectionCullingStack().back().getFrustum().add(plane); traverse(node, nv); // undo cv->getProjectionCullingStack().back().getFrustum().set(origPlaneList); } private: const osg::Plane* mCullPlane; }; class FlipCallback : public osg::NodeCallback { public: FlipCallback(const osg::Plane* cullPlane) : mCullPlane(cullPlane) { } void operator()(osg::Node* node, osg::NodeVisitor* nv) override { osgUtil::CullVisitor* cv = static_cast(nv); osg::Vec3d eyePoint = cv->getEyePoint(); osg::RefMatrix* modelViewMatrix = new osg::RefMatrix(*cv->getModelViewMatrix()); // apply the height of the plane // we can't apply this height in the addClipPlane() since the "flip the below graph" function would otherwise flip the height as well modelViewMatrix->preMultTranslate(mCullPlane->getNormal() * ((*mCullPlane)[3] * -1)); // flip the below graph if the eye point is above the plane if (mCullPlane->intersect(osg::BoundingSphere(osg::Vec3d(0,0,eyePoint.z()), 0)) > 0) { modelViewMatrix->preMultScale(osg::Vec3(1,1,-1)); } // move the plane back along its normal a little bit to prevent bleeding at the water shore const float clipFudge = -5; modelViewMatrix->preMultTranslate(mCullPlane->getNormal() * clipFudge); cv->pushModelViewMatrix(modelViewMatrix, osg::Transform::RELATIVE_RF); traverse(node, nv); cv->popModelViewMatrix(); } private: const osg::Plane* mCullPlane; }; public: ClipCullNode() { addCullCallback (new PlaneCullCallback(&mPlane)); mClipNodeTransform = new osg::Group; mClipNodeTransform->addCullCallback(new FlipCallback(&mPlane)); osg::Group::addChild(mClipNodeTransform); mClipNode = new osg::ClipNode; mClipNodeTransform->addChild(mClipNode); } void setPlane (const osg::Plane& plane) { if (plane == mPlane) return; mPlane = plane; mClipNode->getClipPlaneList().clear(); mClipNode->addClipPlane(new osg::ClipPlane(0, osg::Plane(mPlane.getNormal(), 0))); // mPlane.d() applied in FlipCallback mClipNode->setStateSetModes(*getOrCreateStateSet(), osg::StateAttribute::ON); mClipNode->setCullingActive(false); } private: osg::ref_ptr mClipNodeTransform; osg::ref_ptr mClipNode; osg::Plane mPlane; }; /// This callback on the Camera has the effect of a RELATIVE_RF_INHERIT_VIEWPOINT transform mode (which does not exist in OSG). /// We want to keep the View Point of the parent camera so we will not have to recreate LODs. class InheritViewPointCallback : public osg::NodeCallback { public: InheritViewPointCallback() {} void operator()(osg::Node* node, osg::NodeVisitor* nv) override { osgUtil::CullVisitor* cv = static_cast(nv); osg::ref_ptr modelViewMatrix = new osg::RefMatrix(*cv->getModelViewMatrix()); cv->popModelViewMatrix(); cv->pushModelViewMatrix(modelViewMatrix, osg::Transform::ABSOLUTE_RF_INHERIT_VIEWPOINT); traverse(node, nv); } }; /// Moves water mesh away from the camera slightly if the camera gets too close on the Z axis. /// The offset works around graphics artifacts that occurred with the GL_DEPTH_CLAMP when the camera gets extremely close to the mesh (seen on NVIDIA at least). /// Must be added as a Cull callback. class FudgeCallback : public osg::NodeCallback { public: void operator()(osg::Node* node, osg::NodeVisitor* nv) override { osgUtil::CullVisitor* cv = static_cast(nv); const float fudge = 0.2; if (std::abs(cv->getEyeLocal().z()) < fudge) { float diff = fudge - cv->getEyeLocal().z(); osg::RefMatrix* modelViewMatrix = new osg::RefMatrix(*cv->getModelViewMatrix()); if (cv->getEyeLocal().z() > 0) modelViewMatrix->preMultTranslate(osg::Vec3f(0,0,-diff)); else modelViewMatrix->preMultTranslate(osg::Vec3f(0,0,diff)); cv->pushModelViewMatrix(modelViewMatrix, osg::Transform::RELATIVE_RF); traverse(node, nv); cv->popModelViewMatrix(); } else traverse(node, nv); } }; class RainIntensityUpdater : public SceneUtil::StateSetUpdater { public: RainIntensityUpdater() : mRainIntensity(0.f) { } void setRainIntensity(float rainIntensity) { mRainIntensity = rainIntensity; } protected: void setDefaults(osg::StateSet* stateset) override { osg::ref_ptr rainIntensityUniform = new osg::Uniform("rainIntensity", 0.0f); stateset->addUniform(rainIntensityUniform.get()); } void apply(osg::StateSet* stateset, osg::NodeVisitor* /*nv*/) override { osg::ref_ptr rainIntensityUniform = stateset->getUniform("rainIntensity"); if (rainIntensityUniform != nullptr) rainIntensityUniform->set(mRainIntensity); } private: float mRainIntensity; }; osg::ref_ptr readPngImage (const std::string& file) { // use boost in favor of osgDB::readImage, to handle utf-8 path issues on Windows boost::filesystem::ifstream inStream; inStream.open(file, std::ios_base::in | std::ios_base::binary); if (inStream.fail()) Log(Debug::Error) << "Error: Failed to open " << file; osgDB::ReaderWriter* reader = osgDB::Registry::instance()->getReaderWriterForExtension("png"); if (!reader) { Log(Debug::Error) << "Error: Failed to read " << file << ", no png readerwriter found"; return osg::ref_ptr(); } osgDB::ReaderWriter::ReadResult result = reader->readImage(inStream); if (!result.success()) Log(Debug::Error) << "Error: Failed to read " << file << ": " << result.message() << " code " << result.status(); return result.getImage(); } class Refraction : public osg::Camera { public: Refraction() { unsigned int rttSize = Settings::Manager::getInt("rtt size", "Water"); setRenderOrder(osg::Camera::PRE_RENDER, 1); setClearMask(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); setRenderTargetImplementation(osg::Camera::FRAME_BUFFER_OBJECT); setReferenceFrame(osg::Camera::RELATIVE_RF); setSmallFeatureCullingPixelSize(Settings::Manager::getInt("small feature culling pixel size", "Water")); osg::Camera::setName("RefractionCamera"); setCullCallback(new InheritViewPointCallback); setComputeNearFarMode(osg::CullSettings::DO_NOT_COMPUTE_NEAR_FAR); setCullMask(Mask_Effect|Mask_Scene|Mask_Object|Mask_Static|Mask_Terrain|Mask_Actor|Mask_ParticleSystem|Mask_Sky|Mask_Sun|Mask_Player|Mask_Lighting|Mask_Groundcover); setNodeMask(Mask_RenderToTexture); setViewport(0, 0, rttSize, rttSize); // No need for Update traversal since the scene is already updated as part of the main scene graph // A double update would mess with the light collection (in addition to being plain redundant) setUpdateCallback(new NoTraverseCallback); // No need for fog here, we are already applying fog on the water surface itself as well as underwater fog // assign large value to effectively turn off fog // shaders don't respect glDisable(GL_FOG) osg::ref_ptr fog (new osg::Fog); fog->setStart(10000000); fog->setEnd(10000000); getOrCreateStateSet()->setAttributeAndModes(fog, osg::StateAttribute::OFF|osg::StateAttribute::OVERRIDE); mClipCullNode = new ClipCullNode; osg::Camera::addChild(mClipCullNode); mRefractionTexture = new osg::Texture2D; mRefractionTexture->setTextureSize(rttSize, rttSize); mRefractionTexture->setWrap(osg::Texture::WRAP_S, osg::Texture::CLAMP_TO_EDGE); mRefractionTexture->setWrap(osg::Texture::WRAP_T, osg::Texture::CLAMP_TO_EDGE); mRefractionTexture->setInternalFormat(GL_RGB); mRefractionTexture->setFilter(osg::Texture::MIN_FILTER, osg::Texture::LINEAR); mRefractionTexture->setFilter(osg::Texture::MAG_FILTER, osg::Texture::LINEAR); SceneUtil::attachAlphaToCoverageFriendlyFramebufferToCamera(this, osg::Camera::COLOR_BUFFER, mRefractionTexture); mRefractionDepthTexture = new osg::Texture2D; mRefractionDepthTexture->setTextureSize(rttSize, rttSize); mRefractionDepthTexture->setSourceFormat(GL_DEPTH_COMPONENT); mRefractionDepthTexture->setInternalFormat(GL_DEPTH_COMPONENT24); mRefractionDepthTexture->setWrap(osg::Texture::WRAP_S, osg::Texture::CLAMP_TO_EDGE); mRefractionDepthTexture->setWrap(osg::Texture::WRAP_T, osg::Texture::CLAMP_TO_EDGE); mRefractionDepthTexture->setSourceType(GL_UNSIGNED_INT); mRefractionDepthTexture->setFilter(osg::Texture::MIN_FILTER, osg::Texture::LINEAR); mRefractionDepthTexture->setFilter(osg::Texture::MAG_FILTER, osg::Texture::LINEAR); attach(osg::Camera::DEPTH_BUFFER, mRefractionDepthTexture); if (Settings::Manager::getFloat("refraction scale", "Water") != 1) // TODO: to be removed with issue #5709 SceneUtil::ShadowManager::disableShadowsForStateSet(getOrCreateStateSet()); } void setScene(osg::Node* scene) { if (mScene) mClipCullNode->removeChild(mScene); mScene = scene; mClipCullNode->addChild(scene); } void setWaterLevel(float waterLevel) { const float refractionScale = std::min(1.0f,std::max(0.0f, Settings::Manager::getFloat("refraction scale", "Water"))); setViewMatrix(osg::Matrix::scale(1,1,refractionScale) * osg::Matrix::translate(0,0,(1.0 - refractionScale) * waterLevel)); mClipCullNode->setPlane(osg::Plane(osg::Vec3d(0,0,-1), osg::Vec3d(0,0, waterLevel))); } osg::Texture2D* getRefractionTexture() const { return mRefractionTexture.get(); } osg::Texture2D* getRefractionDepthTexture() const { return mRefractionDepthTexture.get(); } private: osg::ref_ptr mClipCullNode; osg::ref_ptr mRefractionTexture; osg::ref_ptr mRefractionDepthTexture; osg::ref_ptr mScene; }; class Reflection : public osg::Camera { public: Reflection(bool isInterior) { setRenderOrder(osg::Camera::PRE_RENDER); setClearMask(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); setRenderTargetImplementation(osg::Camera::FRAME_BUFFER_OBJECT); setReferenceFrame(osg::Camera::RELATIVE_RF); setSmallFeatureCullingPixelSize(Settings::Manager::getInt("small feature culling pixel size", "Water")); osg::Camera::setName("ReflectionCamera"); setCullCallback(new InheritViewPointCallback); setInterior(isInterior); setNodeMask(Mask_RenderToTexture); unsigned int rttSize = Settings::Manager::getInt("rtt size", "Water"); setViewport(0, 0, rttSize, rttSize); // No need for Update traversal since the mSceneRoot is already updated as part of the main scene graph // A double update would mess with the light collection (in addition to being plain redundant) setUpdateCallback(new NoTraverseCallback); mReflectionTexture = new osg::Texture2D; mReflectionTexture->setTextureSize(rttSize, rttSize); mReflectionTexture->setInternalFormat(GL_RGB); mReflectionTexture->setFilter(osg::Texture::MIN_FILTER, osg::Texture::LINEAR); mReflectionTexture->setFilter(osg::Texture::MAG_FILTER, osg::Texture::LINEAR); mReflectionTexture->setWrap(osg::Texture::WRAP_S, osg::Texture::CLAMP_TO_EDGE); mReflectionTexture->setWrap(osg::Texture::WRAP_T, osg::Texture::CLAMP_TO_EDGE); SceneUtil::attachAlphaToCoverageFriendlyFramebufferToCamera(this, osg::Camera::COLOR_BUFFER, mReflectionTexture); // XXX: should really flip the FrontFace on each renderable instead of forcing clockwise. osg::ref_ptr frontFace (new osg::FrontFace); frontFace->setMode(osg::FrontFace::CLOCKWISE); getOrCreateStateSet()->setAttributeAndModes(frontFace, osg::StateAttribute::ON); mClipCullNode = new ClipCullNode; osg::Camera::addChild(mClipCullNode); SceneUtil::ShadowManager::disableShadowsForStateSet(getOrCreateStateSet()); } void setInterior(bool isInterior) { int reflectionDetail = Settings::Manager::getInt("reflection detail", "Water"); reflectionDetail = std::min(5, std::max(isInterior ? 2 : 0, reflectionDetail)); unsigned int extraMask = 0; if(reflectionDetail >= 1) extraMask |= Mask_Terrain; if(reflectionDetail >= 2) extraMask |= Mask_Static; if(reflectionDetail >= 3) extraMask |= Mask_Effect|Mask_ParticleSystem|Mask_Object; if(reflectionDetail >= 4) extraMask |= Mask_Player|Mask_Actor; if(reflectionDetail >= 5) extraMask |= Mask_Groundcover; setCullMask(Mask_Scene|Mask_Sky|Mask_Lighting|extraMask); } void setWaterLevel(float waterLevel) { setViewMatrix(osg::Matrix::scale(1,1,-1) * osg::Matrix::translate(0,0,2 * waterLevel)); mClipCullNode->setPlane(osg::Plane(osg::Vec3d(0,0,1), osg::Vec3d(0,0,waterLevel))); } void setScene(osg::Node* scene) { if (mScene) mClipCullNode->removeChild(mScene); mScene = scene; mClipCullNode->addChild(scene); } osg::Texture2D* getReflectionTexture() const { return mReflectionTexture.get(); } private: osg::ref_ptr mReflectionTexture; osg::ref_ptr mClipCullNode; osg::ref_ptr mScene; }; /// DepthClampCallback enables GL_DEPTH_CLAMP for the current draw, if supported. class DepthClampCallback : public osg::Drawable::DrawCallback { public: void drawImplementation(osg::RenderInfo& renderInfo,const osg::Drawable* drawable) const override { static bool supported = osg::isGLExtensionOrVersionSupported(renderInfo.getState()->getContextID(), "GL_ARB_depth_clamp", 3.3); if (!supported) { drawable->drawImplementation(renderInfo); return; } glEnable(GL_DEPTH_CLAMP); drawable->drawImplementation(renderInfo); // restore default glDisable(GL_DEPTH_CLAMP); } }; Water::Water(osg::Group *parent, osg::Group* sceneRoot, Resource::ResourceSystem *resourceSystem, osgUtil::IncrementalCompileOperation *ico, const std::string& resourcePath) : mRainIntensityUpdater(nullptr) , mParent(parent) , mSceneRoot(sceneRoot) , mResourceSystem(resourceSystem) , mResourcePath(resourcePath) , mEnabled(true) , mToggled(true) , mTop(0) , mInterior(false) , mCullCallback(nullptr) { mSimulation.reset(new RippleSimulation(mSceneRoot, resourceSystem)); mWaterGeom = SceneUtil::createWaterGeometry(Constants::CellSizeInUnits*150, 40, 900); mWaterGeom->setDrawCallback(new DepthClampCallback); mWaterGeom->setNodeMask(Mask_Water); mWaterGeom->setDataVariance(osg::Object::STATIC); mWaterNode = new osg::PositionAttitudeTransform; mWaterNode->setName("Water Root"); mWaterNode->addChild(mWaterGeom); mWaterNode->addCullCallback(new FudgeCallback); // simple water fallback for the local map osg::ref_ptr geom2 (osg::clone(mWaterGeom.get(), osg::CopyOp::DEEP_COPY_NODES)); createSimpleWaterStateSet(geom2, Fallback::Map::getFloat("Water_Map_Alpha")); geom2->setNodeMask(Mask_SimpleWater); mWaterNode->addChild(geom2); mSceneRoot->addChild(mWaterNode); setHeight(mTop); updateWaterMaterial(); if (ico) ico->add(mWaterNode); } void Water::setCullCallback(osg::Callback* callback) { if (mCullCallback) { mWaterNode->removeCullCallback(mCullCallback); if (mReflection) mReflection->removeCullCallback(mCullCallback); if (mRefraction) mRefraction->removeCullCallback(mCullCallback); } mCullCallback = callback; if (callback) { mWaterNode->addCullCallback(callback); if (mReflection) mReflection->addCullCallback(callback); if (mRefraction) mRefraction->addCullCallback(callback); } } void Water::updateWaterMaterial() { if (mReflection) { mReflection->removeChildren(0, mReflection->getNumChildren()); mParent->removeChild(mReflection); mReflection = nullptr; } if (mRefraction) { mRefraction->removeChildren(0, mRefraction->getNumChildren()); mParent->removeChild(mRefraction); mRefraction = nullptr; } if (Settings::Manager::getBool("shader", "Water")) { mReflection = new Reflection(mInterior); mReflection->setWaterLevel(mTop); mReflection->setScene(mSceneRoot); if (mCullCallback) mReflection->addCullCallback(mCullCallback); mParent->addChild(mReflection); if (Settings::Manager::getBool("refraction", "Water")) { mRefraction = new Refraction; mRefraction->setWaterLevel(mTop); mRefraction->setScene(mSceneRoot); if (mCullCallback) mRefraction->addCullCallback(mCullCallback); mParent->addChild(mRefraction); } createShaderWaterStateSet(mWaterGeom, mReflection, mRefraction); } else createSimpleWaterStateSet(mWaterGeom, Fallback::Map::getFloat("Water_World_Alpha")); updateVisible(); } osg::Camera *Water::getReflectionCamera() { return mReflection; } osg::Camera *Water::getRefractionCamera() { return mRefraction; } void Water::createSimpleWaterStateSet(osg::Node* node, float alpha) { osg::ref_ptr stateset = SceneUtil::createSimpleWaterStateSet(alpha, MWRender::RenderBin_Water); node->setStateSet(stateset); node->setUpdateCallback(nullptr); mRainIntensityUpdater = nullptr; // Add animated textures std::vector > textures; int frameCount = std::max(0, std::min(Fallback::Map::getInt("Water_SurfaceFrameCount"), 320)); const std::string& texture = Fallback::Map::getString("Water_SurfaceTexture"); for (int i=0; i tex (new osg::Texture2D(mResourceSystem->getImageManager()->getImage(texname.str()))); tex->setWrap(osg::Texture::WRAP_S, osg::Texture::REPEAT); tex->setWrap(osg::Texture::WRAP_T, osg::Texture::REPEAT); textures.push_back(tex); } if (textures.empty()) return; float fps = Fallback::Map::getFloat("Water_SurfaceFPS"); osg::ref_ptr controller (new NifOsg::FlipController(0, 1.f/fps, textures)); controller->setSource(std::shared_ptr(new SceneUtil::FrameTimeSource)); node->setUpdateCallback(controller); stateset->setTextureAttributeAndModes(0, textures[0], osg::StateAttribute::ON); // use a shader to render the simple water, ensuring that fog is applied per pixel as required. // this could be removed if a more detailed water mesh, using some sort of paging solution, is implemented. Resource::SceneManager* sceneManager = mResourceSystem->getSceneManager(); bool oldValue = sceneManager->getForceShaders(); sceneManager->setForceShaders(true); sceneManager->recreateShaders(node); sceneManager->setForceShaders(oldValue); } void Water::createShaderWaterStateSet(osg::Node* node, Reflection* reflection, Refraction* refraction) { // use a define map to conditionally compile the shader std::map defineMap; defineMap.insert(std::make_pair(std::string("refraction_enabled"), std::string(refraction ? "1" : "0"))); Shader::ShaderManager& shaderMgr = mResourceSystem->getSceneManager()->getShaderManager(); osg::ref_ptr vertexShader (shaderMgr.getShader("water_vertex.glsl", defineMap, osg::Shader::VERTEX)); osg::ref_ptr fragmentShader (shaderMgr.getShader("water_fragment.glsl", defineMap, osg::Shader::FRAGMENT)); osg::ref_ptr normalMap (new osg::Texture2D(readPngImage(mResourcePath + "/shaders/water_nm.png"))); if (normalMap->getImage()) normalMap->getImage()->flipVertical(); normalMap->setWrap(osg::Texture::WRAP_S, osg::Texture::REPEAT); normalMap->setWrap(osg::Texture::WRAP_T, osg::Texture::REPEAT); normalMap->setMaxAnisotropy(16); normalMap->setFilter(osg::Texture::MIN_FILTER, osg::Texture::LINEAR_MIPMAP_LINEAR); normalMap->setFilter(osg::Texture::MAG_FILTER, osg::Texture::LINEAR); osg::ref_ptr shaderStateset = new osg::StateSet; shaderStateset->addUniform(new osg::Uniform("normalMap", 0)); shaderStateset->addUniform(new osg::Uniform("reflectionMap", 1)); shaderStateset->setTextureAttributeAndModes(0, normalMap, osg::StateAttribute::ON); shaderStateset->setTextureAttributeAndModes(1, reflection->getReflectionTexture(), osg::StateAttribute::ON); if (refraction) { shaderStateset->setTextureAttributeAndModes(2, refraction->getRefractionTexture(), osg::StateAttribute::ON); shaderStateset->setTextureAttributeAndModes(3, refraction->getRefractionDepthTexture(), osg::StateAttribute::ON); shaderStateset->addUniform(new osg::Uniform("refractionMap", 2)); shaderStateset->addUniform(new osg::Uniform("refractionDepthMap", 3)); shaderStateset->setRenderBinDetails(MWRender::RenderBin_Default, "RenderBin"); } else { shaderStateset->setMode(GL_BLEND, osg::StateAttribute::ON); shaderStateset->setRenderBinDetails(MWRender::RenderBin_Water, "RenderBin"); osg::ref_ptr depth (new osg::Depth); depth->setWriteMask(false); shaderStateset->setAttributeAndModes(depth, osg::StateAttribute::ON); } shaderStateset->setMode(GL_CULL_FACE, osg::StateAttribute::OFF); osg::ref_ptr program (new osg::Program); program->addShader(vertexShader); program->addShader(fragmentShader); auto method = mResourceSystem->getSceneManager()->getLightingMethod(); if (method == SceneUtil::LightingMethod::SingleUBO) program->addBindUniformBlock("LightBufferBinding", static_cast(Shader::UBOBinding::LightBuffer)); shaderStateset->setAttributeAndModes(program, osg::StateAttribute::ON); node->setStateSet(shaderStateset); mRainIntensityUpdater = new RainIntensityUpdater(); node->setUpdateCallback(mRainIntensityUpdater); } void Water::processChangedSettings(const Settings::CategorySettingVector& settings) { updateWaterMaterial(); } Water::~Water() { mParent->removeChild(mWaterNode); if (mReflection) { mReflection->removeChildren(0, mReflection->getNumChildren()); mParent->removeChild(mReflection); mReflection = nullptr; } if (mRefraction) { mRefraction->removeChildren(0, mRefraction->getNumChildren()); mParent->removeChild(mRefraction); mRefraction = nullptr; } } void Water::listAssetsToPreload(std::vector &textures) { int frameCount = std::max(0, std::min(Fallback::Map::getInt("Water_SurfaceFrameCount"), 320)); const std::string& texture = Fallback::Map::getString("Water_SurfaceTexture"); for (int i=0; igetCell()->isExterior(); bool wasInterior = mInterior; if (!isInterior) { mWaterNode->setPosition(getSceneNodeCoordinates(store->getCell()->mData.mX, store->getCell()->mData.mY)); mInterior = false; } else { mWaterNode->setPosition(osg::Vec3f(0,0,mTop)); mInterior = true; } if(mInterior != wasInterior && mReflection) mReflection->setInterior(mInterior); // create a new StateSet to prevent threading issues osg::ref_ptr nodeStateSet (new osg::StateSet); nodeStateSet->addUniform(new osg::Uniform("nodePosition", osg::Vec3f(mWaterNode->getPosition()))); mWaterNode->setStateSet(nodeStateSet); } void Water::setHeight(const float height) { mTop = height; mSimulation->setWaterHeight(height); osg::Vec3f pos = mWaterNode->getPosition(); pos.z() = height; mWaterNode->setPosition(pos); if (mReflection) mReflection->setWaterLevel(mTop); if (mRefraction) mRefraction->setWaterLevel(mTop); } void Water::setRainIntensity(float rainIntensity) { if (mRainIntensityUpdater) mRainIntensityUpdater->setRainIntensity(rainIntensity); } void Water::update(float dt) { mSimulation->update(dt); } void Water::updateVisible() { bool visible = mEnabled && mToggled; mWaterNode->setNodeMask(visible ? ~0u : 0u); if (mRefraction) mRefraction->setNodeMask(visible ? Mask_RenderToTexture : 0u); if (mReflection) mReflection->setNodeMask(visible ? Mask_RenderToTexture : 0u); } bool Water::toggle() { mToggled = !mToggled; updateVisible(); return mToggled; } bool Water::isUnderwater(const osg::Vec3f &pos) const { return pos.z() < mTop && mToggled && mEnabled; } osg::Vec3f Water::getSceneNodeCoordinates(int gridX, int gridY) { return osg::Vec3f(static_cast(gridX * Constants::CellSizeInUnits + (Constants::CellSizeInUnits / 2)), static_cast(gridY * Constants::CellSizeInUnits + (Constants::CellSizeInUnits / 2)), mTop); } void Water::addEmitter (const MWWorld::Ptr& ptr, float scale, float force) { mSimulation->addEmitter (ptr, scale, force); } void Water::removeEmitter (const MWWorld::Ptr& ptr) { mSimulation->removeEmitter (ptr); } void Water::updateEmitterPtr (const MWWorld::Ptr& old, const MWWorld::Ptr& ptr) { mSimulation->updateEmitterPtr(old, ptr); } void Water::emitRipple(const osg::Vec3f &pos) { mSimulation->emitRipple(pos); } void Water::removeCell(const MWWorld::CellStore *store) { mSimulation->removeCell(store); } void Water::clearRipples() { mSimulation->clear(); } } ================================================ FILE: apps/openmw/mwrender/water.hpp ================================================ #ifndef OPENMW_MWRENDER_WATER_H #define OPENMW_MWRENDER_WATER_H #include #include #include #include #include #include namespace osg { class Group; class PositionAttitudeTransform; class Geometry; class Node; } namespace osgUtil { class IncrementalCompileOperation; } namespace Resource { class ResourceSystem; } namespace MWWorld { class CellStore; class Ptr; } namespace Fallback { class Map; } namespace MWRender { class Refraction; class Reflection; class RippleSimulation; class RainIntensityUpdater; /// Water rendering class Water { osg::ref_ptr mRainIntensityUpdater; osg::ref_ptr mParent; osg::ref_ptr mSceneRoot; osg::ref_ptr mWaterNode; osg::ref_ptr mWaterGeom; Resource::ResourceSystem* mResourceSystem; osg::ref_ptr mIncrementalCompileOperation; std::unique_ptr mSimulation; osg::ref_ptr mRefraction; osg::ref_ptr mReflection; const std::string mResourcePath; bool mEnabled; bool mToggled; float mTop; bool mInterior; osg::Callback* mCullCallback; osg::Vec3f getSceneNodeCoordinates(int gridX, int gridY); void updateVisible(); void createSimpleWaterStateSet(osg::Node* node, float alpha); /// @param reflection the reflection camera (required) /// @param refraction the refraction camera (optional) void createShaderWaterStateSet(osg::Node* node, Reflection* reflection, Refraction* refraction); void updateWaterMaterial(); public: Water(osg::Group* parent, osg::Group* sceneRoot, Resource::ResourceSystem* resourceSystem, osgUtil::IncrementalCompileOperation* ico, const std::string& resourcePath); ~Water(); void setCullCallback(osg::Callback* callback); void listAssetsToPreload(std::vector& textures); void setEnabled(bool enabled); bool toggle(); bool isUnderwater(const osg::Vec3f& pos) const; /// adds an emitter, position will be tracked automatically using its scene node void addEmitter (const MWWorld::Ptr& ptr, float scale = 1.f, float force = 1.f); void removeEmitter (const MWWorld::Ptr& ptr); void updateEmitterPtr (const MWWorld::Ptr& old, const MWWorld::Ptr& ptr); void emitRipple(const osg::Vec3f& pos); void removeCell(const MWWorld::CellStore* store); ///< remove all emitters in this cell void clearRipples(); void changeCell(const MWWorld::CellStore* store); void setHeight(const float height); void setRainIntensity(const float rainIntensity); void update(float dt); osg::Camera *getReflectionCamera(); osg::Camera *getRefractionCamera(); void processChangedSettings(const Settings::CategorySettingVector& settings); }; } #endif ================================================ FILE: apps/openmw/mwrender/weaponanimation.cpp ================================================ #include "weaponanimation.hpp" #include #include #include /* Start of tes3mp addition Include additional headers for multiplayer purposes */ #include "../mwmp/MechanicsHelper.hpp" /* End of tes3mp addition */ #include "../mwbase/world.hpp" #include "../mwbase/environment.hpp" #include "../mwbase/soundmanager.hpp" #include "../mwworld/inventorystore.hpp" #include "../mwworld/class.hpp" #include "../mwworld/esmstore.hpp" #include "../mwmechanics/creaturestats.hpp" #include "../mwmechanics/combat.hpp" #include "../mwmechanics/weapontype.hpp" #include "animation.hpp" #include "rotatecontroller.hpp" namespace MWRender { float WeaponAnimationTime::getValue(osg::NodeVisitor*) { if (mWeaponGroup.empty()) return 0; float current = mAnimation->getCurrentTime(mWeaponGroup); if (current == -1) return 0; return current - mStartTime; } void WeaponAnimationTime::setGroup(const std::string &group, bool relativeTime) { mWeaponGroup = group; mRelativeTime = relativeTime; if (mRelativeTime) mStartTime = mAnimation->getStartTime(mWeaponGroup); else mStartTime = 0; } void WeaponAnimationTime::updateStartTime() { setGroup(mWeaponGroup, mRelativeTime); } WeaponAnimation::WeaponAnimation() : mPitchFactor(0) { } WeaponAnimation::~WeaponAnimation() { } void WeaponAnimation::attachArrow(MWWorld::Ptr actor) { const MWWorld::InventoryStore& inv = actor.getClass().getInventoryStore(actor); MWWorld::ConstContainerStoreIterator weaponSlot = inv.getSlot(MWWorld::InventoryStore::Slot_CarriedRight); if (weaponSlot == inv.end()) return; if (weaponSlot->getTypeName() != typeid(ESM::Weapon).name()) return; int type = weaponSlot->get()->mBase->mData.mType; ESM::WeaponType::Class weapclass = MWMechanics::getWeaponType(type)->mWeaponClass; if (weapclass == ESM::WeaponType::Thrown) { std::string soundid = weaponSlot->getClass().getUpSoundId(*weaponSlot); if(!soundid.empty()) { MWBase::SoundManager *sndMgr = MWBase::Environment::get().getSoundManager(); sndMgr->playSound3D(actor, soundid, 1.0f, 1.0f); } showWeapon(true); } else if (weapclass == ESM::WeaponType::Ranged) { osg::Group* parent = getArrowBone(); if (!parent) return; MWWorld::ConstContainerStoreIterator ammo = inv.getSlot(MWWorld::InventoryStore::Slot_Ammunition); if (ammo == inv.end()) return; std::string model = ammo->getClass().getModel(*ammo); osg::ref_ptr arrow = getResourceSystem()->getSceneManager()->getInstance(model, parent); mAmmunition = PartHolderPtr(new PartHolder(arrow)); } } void WeaponAnimation::detachArrow(MWWorld::Ptr actor) { mAmmunition.reset(); } void WeaponAnimation::releaseArrow(MWWorld::Ptr actor, float attackStrength) { MWWorld::InventoryStore& inv = actor.getClass().getInventoryStore(actor); MWWorld::ContainerStoreIterator weapon = inv.getSlot(MWWorld::InventoryStore::Slot_CarriedRight); if (weapon == inv.end()) return; if (weapon->getTypeName() != typeid(ESM::Weapon).name()) return; /* Start of tes3mp addition If this is an attack by a LocalPlayer or LocalActor, record its attackStrength and rangedWeaponId and prepare an attack packet for sending. Unlike melee attacks, ranged attacks require the weapon and ammo IDs to be recorded because players and actors can have multiple projectiles in the air at the same time. If it's an attack by a DedicatedPlayer or DedicatedActor, apply the attackStrength from their latest attack packet. */ mwmp::Attack *localAttack = MechanicsHelper::getLocalAttack(actor); if (localAttack) { localAttack->attackStrength = attackStrength; localAttack->rangedWeaponId = weapon->getCellRef().getRefId(); localAttack->shouldSend = true; } else { mwmp::Attack *dedicatedAttack = MechanicsHelper::getDedicatedAttack(actor); if (dedicatedAttack) attackStrength = dedicatedAttack->attackStrength; } /* End of tes3mp addition */ // The orientation of the launched projectile. Always the same as the actor orientation, even if the ArrowBone's orientation dictates otherwise. osg::Quat orient = osg::Quat(actor.getRefData().getPosition().rot[0], osg::Vec3f(-1,0,0)) * osg::Quat(actor.getRefData().getPosition().rot[2], osg::Vec3f(0,0,-1)); const MWWorld::Store &gmst = MWBase::Environment::get().getWorld()->getStore().get(); MWMechanics::applyFatigueLoss(actor, *weapon, attackStrength); if (MWMechanics::getWeaponType(weapon->get()->mBase->mData.mType)->mWeaponClass == ESM::WeaponType::Thrown) { /* Start of tes3mp addition If this is a local attack, clear the rangedAmmoId used for it */ if (localAttack) localAttack->rangedAmmoId = ""; /* End of tes3mp addition */ // Thrown weapons get detached now osg::Node* weaponNode = getWeaponNode(); if (!weaponNode) return; osg::NodePathList nodepaths = weaponNode->getParentalNodePaths(); if (nodepaths.empty()) return; osg::Vec3f launchPos = osg::computeLocalToWorld(nodepaths[0]).getTrans(); /* Start of tes3mp addition If the actor shooting this is a LocalPlayer or LocalActor, track their projectile origin so it can be sent in the next PlayerAttack or ActorAttack packet Otherwise, set the projectileOrigin for a DedicatedPlayer or DedicatedActor */ if (localAttack) { localAttack->projectileOrigin.origin[0] = launchPos.x(); localAttack->projectileOrigin.origin[1] = launchPos.y(); localAttack->projectileOrigin.origin[2] = launchPos.z(); localAttack->projectileOrigin.orientation[0] = orient.x(); localAttack->projectileOrigin.orientation[1] = orient.y(); localAttack->projectileOrigin.orientation[2] = orient.z(); localAttack->projectileOrigin.orientation[3] = orient.w(); } else { mwmp::Attack* dedicatedAttack = MechanicsHelper::getDedicatedAttack(actor); if (dedicatedAttack) { launchPos = osg::Vec3f(dedicatedAttack->projectileOrigin.origin[0], dedicatedAttack->projectileOrigin.origin[1], dedicatedAttack->projectileOrigin.origin[2]); orient = osg::Quat(dedicatedAttack->projectileOrigin.orientation[0], dedicatedAttack->projectileOrigin.orientation[1], dedicatedAttack->projectileOrigin.orientation[2], dedicatedAttack->projectileOrigin.orientation[3]); } } /* End of tes3mp addition */ float fThrownWeaponMinSpeed = gmst.find("fThrownWeaponMinSpeed")->mValue.getFloat(); float fThrownWeaponMaxSpeed = gmst.find("fThrownWeaponMaxSpeed")->mValue.getFloat(); float speed = fThrownWeaponMinSpeed + (fThrownWeaponMaxSpeed - fThrownWeaponMinSpeed) * attackStrength; MWWorld::Ptr weaponPtr = *weapon; MWBase::Environment::get().getWorld()->launchProjectile(actor, weaponPtr, launchPos, orient, weaponPtr, speed, attackStrength); showWeapon(false); inv.remove(*weapon, 1, actor); } else { // With bows and crossbows only the used arrow/bolt gets detached MWWorld::ContainerStoreIterator ammo = inv.getSlot(MWWorld::InventoryStore::Slot_Ammunition); if (ammo == inv.end()) return; if (!mAmmunition) return; /* Start of tes3mp addition If this is a local attack, record the rangedAmmoId used for it */ if (localAttack) localAttack->rangedAmmoId = ammo->getCellRef().getRefId(); /* End of tes3mp addition */ osg::ref_ptr ammoNode = mAmmunition->getNode(); osg::NodePathList nodepaths = ammoNode->getParentalNodePaths(); if (nodepaths.empty()) return; osg::Vec3f launchPos = osg::computeLocalToWorld(nodepaths[0]).getTrans(); /* Start of tes3mp addition If the actor shooting this is a LocalPlayer or LocalActor, track their projectile origin so it can be sent in the next PlayerAttack or ActorAttack packet Otherwise, set the projectileOrigin for a DedicatedPlayer or DedicatedActor */ if (localAttack) { localAttack->projectileOrigin.origin[0] = launchPos.x(); localAttack->projectileOrigin.origin[1] = launchPos.y(); localAttack->projectileOrigin.origin[2] = launchPos.z(); localAttack->projectileOrigin.orientation[0] = orient.x(); localAttack->projectileOrigin.orientation[1] = orient.y(); localAttack->projectileOrigin.orientation[2] = orient.z(); localAttack->projectileOrigin.orientation[3] = orient.w(); } else { mwmp::Attack* dedicatedAttack = MechanicsHelper::getDedicatedAttack(actor); if (dedicatedAttack) { launchPos = osg::Vec3f(dedicatedAttack->projectileOrigin.origin[0], dedicatedAttack->projectileOrigin.origin[1], dedicatedAttack->projectileOrigin.origin[2]); orient = osg::Quat(dedicatedAttack->projectileOrigin.orientation[0], dedicatedAttack->projectileOrigin.orientation[1], dedicatedAttack->projectileOrigin.orientation[2], dedicatedAttack->projectileOrigin.orientation[3]); } } /* End of tes3mp addition */ float fProjectileMinSpeed = gmst.find("fProjectileMinSpeed")->mValue.getFloat(); float fProjectileMaxSpeed = gmst.find("fProjectileMaxSpeed")->mValue.getFloat(); float speed = fProjectileMinSpeed + (fProjectileMaxSpeed - fProjectileMinSpeed) * attackStrength; MWWorld::Ptr weaponPtr = *weapon; MWWorld::Ptr ammoPtr = *ammo; MWBase::Environment::get().getWorld()->launchProjectile(actor, ammoPtr, launchPos, orient, weaponPtr, speed, attackStrength); inv.remove(ammoPtr, 1, actor); mAmmunition.reset(); } } void WeaponAnimation::addControllers(const std::map >& nodes, std::vector, osg::ref_ptr>> &map, osg::Node* objectRoot) { for (int i=0; i<2; ++i) { mSpineControllers[i] = nullptr; std::map >::const_iterator found = nodes.find(i == 0 ? "bip01 spine1" : "bip01 spine2"); if (found != nodes.end()) { osg::Node* node = found->second; mSpineControllers[i] = new RotateController(objectRoot); node->addUpdateCallback(mSpineControllers[i]); map.emplace_back(node, mSpineControllers[i]); } } } void WeaponAnimation::deleteControllers() { for (int i=0; i<2; ++i) mSpineControllers[i] = nullptr; } void WeaponAnimation::configureControllers(float characterPitchRadians) { if (mPitchFactor == 0.f || characterPitchRadians == 0.f) { setControllerEnabled(false); return; } float pitch = characterPitchRadians * mPitchFactor; osg::Quat rotate (pitch/2, osg::Vec3f(-1,0,0)); setControllerRotate(rotate); setControllerEnabled(true); } void WeaponAnimation::setControllerRotate(const osg::Quat& rotate) { for (int i=0; i<2; ++i) if (mSpineControllers[i]) mSpineControllers[i]->setRotate(rotate); } void WeaponAnimation::setControllerEnabled(bool enabled) { for (int i=0; i<2; ++i) if (mSpineControllers[i]) mSpineControllers[i]->setEnabled(enabled); } } ================================================ FILE: apps/openmw/mwrender/weaponanimation.hpp ================================================ #ifndef OPENMW_MWRENDER_WEAPONANIMATION_H #define OPENMW_MWRENDER_WEAPONANIMATION_H #include #include "../mwworld/ptr.hpp" #include "animation.hpp" namespace MWRender { class RotateController; class WeaponAnimationTime : public SceneUtil::ControllerSource { private: Animation* mAnimation; std::string mWeaponGroup; float mStartTime; bool mRelativeTime; public: WeaponAnimationTime(Animation* animation) : mAnimation(animation), mStartTime(0), mRelativeTime(false) {} void setGroup(const std::string& group, bool relativeTime); void updateStartTime(); float getValue(osg::NodeVisitor* nv) override; }; /// Handles attach & release of projectiles for ranged weapons class WeaponAnimation { public: WeaponAnimation(); virtual ~WeaponAnimation(); /// @note If no weapon (or an invalid weapon) is equipped, this function is a no-op. void attachArrow(MWWorld::Ptr actor); void detachArrow(MWWorld::Ptr actor); /// @note If no weapon (or an invalid weapon) is equipped, this function is a no-op. void releaseArrow(MWWorld::Ptr actor, float attackStrength); /// Add WeaponAnimation-related controllers to \a nodes and store the added controllers in \a map. void addControllers(const std::map >& nodes, std::vector, osg::ref_ptr>>& map, osg::Node* objectRoot); void deleteControllers(); /// Configure controllers, should be called every animation frame. void configureControllers(float characterPitchRadians); protected: PartHolderPtr mAmmunition; osg::ref_ptr mSpineControllers[2]; void setControllerRotate(const osg::Quat& rotate); void setControllerEnabled(bool enabled); virtual osg::Group* getArrowBone() = 0; virtual osg::Node* getWeaponNode() = 0; virtual Resource::ResourceSystem* getResourceSystem() = 0; virtual void showWeapon(bool show) = 0; /// A relative factor (0-1) that decides if and how much the skeleton should be pitched /// to indicate the facing orientation of the character, for ranged weapon aiming. float mPitchFactor; }; } #endif ================================================ FILE: apps/openmw/mwscript/aiextensions.cpp ================================================ #include "aiextensions.hpp" #include #include /* Start of tes3mp addition Include additional headers for multiplayer purposes */ #include #include "../mwmp/Main.hpp" #include "../mwmp/Networking.hpp" #include "../mwmp/ActorList.hpp" #include "../mwmp/MechanicsHelper.hpp" /* End of tes3mp addition */ #include #include #include #include #include #include "../mwworld/class.hpp" #include "../mwworld/esmstore.hpp" #include "../mwmechanics/creaturestats.hpp" #include "../mwmechanics/aiactivate.hpp" #include "../mwmechanics/aiescort.hpp" #include "../mwmechanics/aifollow.hpp" #include "../mwmechanics/aitravel.hpp" #include "../mwmechanics/aiwander.hpp" #include "../mwmechanics/aiface.hpp" #include "../mwbase/environment.hpp" #include "../mwbase/world.hpp" #include "../mwbase/mechanicsmanager.hpp" #include "../mwbase/soundmanager.hpp" #include "interpretercontext.hpp" #include "ref.hpp" namespace MWScript { namespace Ai { template class OpAiActivate : public Interpreter::Opcode1 { public: void execute (Interpreter::Runtime& runtime, unsigned int arg0) override { MWWorld::Ptr ptr = R()(runtime); std::string objectID = runtime.getStringLiteral (runtime[0].mInteger); runtime.pop(); // discard additional arguments (reset), because we have no idea what they mean. for (unsigned int i=0; i class OpAiTravel : public Interpreter::Opcode1 { public: void execute (Interpreter::Runtime& runtime, unsigned int arg0) override { MWWorld::Ptr ptr = R()(runtime); Interpreter::Type_Float x = runtime[0].mFloat; runtime.pop(); Interpreter::Type_Float y = runtime[0].mFloat; runtime.pop(); Interpreter::Type_Float z = runtime[0].mFloat; runtime.pop(); // discard additional arguments (reset), because we have no idea what they mean. for (unsigned int i=0; i class OpAiEscort : public Interpreter::Opcode1 { public: void execute (Interpreter::Runtime& runtime, unsigned int arg0) override { MWWorld::Ptr ptr = R()(runtime); std::string actorID = runtime.getStringLiteral (runtime[0].mInteger); runtime.pop(); Interpreter::Type_Float duration = runtime[0].mFloat; runtime.pop(); Interpreter::Type_Float x = runtime[0].mFloat; runtime.pop(); Interpreter::Type_Float y = runtime[0].mFloat; runtime.pop(); Interpreter::Type_Float z = runtime[0].mFloat; runtime.pop(); // discard additional arguments (reset), because we have no idea what they mean. for (unsigned int i=0; i(duration), x, y, z); ptr.getClass().getCreatureStats (ptr).getAiSequence().stack(escortPackage, ptr); Log(Debug::Info) << "AiEscort: " << x << ", " << y << ", " << z << ", " << duration; } }; template class OpAiEscortCell : public Interpreter::Opcode1 { public: void execute (Interpreter::Runtime& runtime, unsigned int arg0) override { MWWorld::Ptr ptr = R()(runtime); std::string actorID = runtime.getStringLiteral (runtime[0].mInteger); runtime.pop(); std::string cellID = runtime.getStringLiteral (runtime[0].mInteger); runtime.pop(); Interpreter::Type_Float duration = runtime[0].mFloat; runtime.pop(); Interpreter::Type_Float x = runtime[0].mFloat; runtime.pop(); Interpreter::Type_Float y = runtime[0].mFloat; runtime.pop(); Interpreter::Type_Float z = runtime[0].mFloat; runtime.pop(); // discard additional arguments (reset), because we have no idea what they mean. for (unsigned int i=0; igetStore().get().find(cellID); MWMechanics::AiEscort escortPackage(actorID, cellID, static_cast(duration), x, y, z); ptr.getClass().getCreatureStats (ptr).getAiSequence().stack(escortPackage, ptr); Log(Debug::Info) << "AiEscort: " << x << ", " << y << ", " << z << ", " << duration; } }; template class OpGetAiPackageDone : public Interpreter::Opcode0 { public: void execute (Interpreter::Runtime& runtime) override { MWWorld::Ptr ptr = R()(runtime); Interpreter::Type_Integer value = ptr.getClass().getCreatureStats (ptr).getAiSequence().isPackageDone(); runtime.push (value); } }; template class OpAiWander : public Interpreter::Opcode1 { public: void execute (Interpreter::Runtime& runtime, unsigned int arg0) override { MWWorld::Ptr ptr = R()(runtime); Interpreter::Type_Integer range = static_cast(runtime[0].mFloat); runtime.pop(); Interpreter::Type_Integer duration = static_cast(runtime[0].mFloat); runtime.pop(); Interpreter::Type_Integer time = static_cast(runtime[0].mFloat); runtime.pop(); // Chance for Idle is unused if (arg0) { --arg0; runtime.pop(); } std::vector idleList; bool repeat = false; // Chances for Idle2-Idle9 for(int i=2; i<=9 && arg0; ++i) { if(!repeat) repeat = true; Interpreter::Type_Integer idleValue = runtime[0].mInteger; idleValue = std::min(255, std::max(0, idleValue)); idleList.push_back(idleValue); runtime.pop(); --arg0; } if(arg0) { repeat = runtime[0].mInteger != 0; runtime.pop(); --arg0; } // discard additional arguments (reset), because we have no idea what they mean. for (unsigned int i=0; i class OpGetAiSetting : public Interpreter::Opcode0 { MWMechanics::CreatureStats::AiSetting mIndex; public: OpGetAiSetting(MWMechanics::CreatureStats::AiSetting index) : mIndex(index) {} void execute (Interpreter::Runtime& runtime) override { MWWorld::Ptr ptr = R()(runtime); runtime.push(ptr.getClass().getCreatureStats (ptr).getAiSetting (mIndex).getModified(false)); } }; template class OpModAiSetting : public Interpreter::Opcode0 { MWMechanics::CreatureStats::AiSetting mIndex; public: OpModAiSetting(MWMechanics::CreatureStats::AiSetting index) : mIndex(index) {} void execute (Interpreter::Runtime& runtime) override { MWWorld::Ptr ptr = R()(runtime); Interpreter::Type_Integer value = runtime[0].mInteger; runtime.pop(); int modified = ptr.getClass().getCreatureStats (ptr).getAiSetting (mIndex).getBase() + value; ptr.getClass().getCreatureStats (ptr).setAiSetting (mIndex, modified); ptr.getClass().setBaseAISetting(ptr.getCellRef().getRefId(), mIndex, modified); } }; template class OpSetAiSetting : public Interpreter::Opcode0 { MWMechanics::CreatureStats::AiSetting mIndex; public: OpSetAiSetting(MWMechanics::CreatureStats::AiSetting index) : mIndex(index) {} void execute (Interpreter::Runtime& runtime) override { MWWorld::Ptr ptr = R()(runtime); Interpreter::Type_Integer value = runtime[0].mInteger; runtime.pop(); /* Start of tes3mp addition Track the original stat value, to ensure we don't send repetitive packets to the server about its changes */ MWMechanics::Stat stat = ptr.getClass().getCreatureStats(ptr).getAiSetting(mIndex); int initialValue = stat.getBase(); /* End of tes3mp addition */ ptr.getClass().getCreatureStats(ptr).setAiSetting(mIndex, value); ptr.getClass().setBaseAISetting(ptr.getCellRef().getRefId(), mIndex, value); /* Start of tes3mp addition Setting an actor's AI_Fight to 100 is equivalent to starting combat with the local player, so send a combat packet regardless of whether we're the cell authority or not; the server can decide if it wants to comply with them by forwarding them to the cell authority */ if (stat.getBase() != initialValue && mIndex == MWMechanics::CreatureStats::AI_Fight && value == 100) { mwmp::ActorList *actorList = mwmp::Main::get().getNetworking()->getActorList(); actorList->reset(); actorList->cell = *ptr.getCell()->getCell(); actorList->addAiActor(ptr, MWBase::Environment::get().getWorld()->getPlayerPtr(), mwmp::BaseActorList::COMBAT); actorList->sendAiActors(); } /* End of tes3mp addition */ } }; template class OpAiFollow : public Interpreter::Opcode1 { public: void execute (Interpreter::Runtime& runtime, unsigned int arg0) override { MWWorld::Ptr ptr = R()(runtime); std::string actorID = runtime.getStringLiteral (runtime[0].mInteger); runtime.pop(); Interpreter::Type_Float duration = runtime[0].mFloat; runtime.pop(); Interpreter::Type_Float x = runtime[0].mFloat; runtime.pop(); Interpreter::Type_Float y = runtime[0].mFloat; runtime.pop(); Interpreter::Type_Float z = runtime[0].mFloat; runtime.pop(); // discard additional arguments (reset), because we have no idea what they mean. for (unsigned int i=0; isearchPtr(actorID, true); if (targetPtr) { mwmp::ActorList *actorList = mwmp::Main::get().getNetworking()->getActorList(); actorList->reset(); actorList->cell = *ptr.getCell()->getCell(); actorList->addAiActor(ptr, targetPtr, mwmp::BaseActorList::FOLLOW); actorList->sendAiActors(); } /* End of tes3mp addition */ } }; template class OpAiFollowCell : public Interpreter::Opcode1 { public: void execute (Interpreter::Runtime& runtime, unsigned int arg0) override { MWWorld::Ptr ptr = R()(runtime); std::string actorID = runtime.getStringLiteral (runtime[0].mInteger); runtime.pop(); std::string cellID = runtime.getStringLiteral (runtime[0].mInteger); runtime.pop(); Interpreter::Type_Float duration = runtime[0].mFloat; runtime.pop(); Interpreter::Type_Float x = runtime[0].mFloat; runtime.pop(); Interpreter::Type_Float y = runtime[0].mFloat; runtime.pop(); Interpreter::Type_Float z = runtime[0].mFloat; runtime.pop(); // discard additional arguments (reset), because we have no idea what they mean. for (unsigned int i=0; i class OpGetCurrentAIPackage : public Interpreter::Opcode0 { public: void execute (Interpreter::Runtime& runtime) override { MWWorld::Ptr ptr = R()(runtime); const auto value = static_cast(ptr.getClass().getCreatureStats (ptr).getAiSequence().getLastRunTypeId()); runtime.push (value); } }; template class OpGetDetected : public Interpreter::Opcode0 { public: void execute (Interpreter::Runtime& runtime) override { MWWorld::Ptr observer = R()(runtime, false); // required=false std::string actorID = runtime.getStringLiteral (runtime[0].mInteger); runtime.pop(); MWWorld::Ptr actor = MWBase::Environment::get().getWorld()->searchPtr(actorID, true, false); Interpreter::Type_Integer value = 0; if (!actor.isEmpty()) value = MWBase::Environment::get().getMechanicsManager()->isActorDetected(actor, observer); runtime.push (value); } }; template class OpGetLineOfSight : public Interpreter::Opcode0 { public: void execute (Interpreter::Runtime& runtime) override { MWWorld::Ptr source = R()(runtime); std::string actorID = runtime.getStringLiteral (runtime[0].mInteger); runtime.pop(); MWWorld::Ptr dest = MWBase::Environment::get().getWorld()->searchPtr(actorID, true, false); bool value = false; if (!dest.isEmpty() && source.getClass().isActor() && dest.getClass().isActor()) { value = MWBase::Environment::get().getWorld()->getLOS(source,dest); } runtime.push (value); } }; template class OpGetTarget : public Interpreter::Opcode0 { public: void execute (Interpreter::Runtime &runtime) override { MWWorld::Ptr actor = R()(runtime); 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 (!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)); } }; template class OpStartCombat : public Interpreter::Opcode0 { public: void execute (Interpreter::Runtime &runtime) override { MWWorld::Ptr actor = R()(runtime); std::string targetID = runtime.getStringLiteral (runtime[0].mInteger); runtime.pop(); MWWorld::Ptr target = MWBase::Environment::get().getWorld()->searchPtr(targetID, true, false); /* Start of tes3mp addition Track whether this actor is already in combat with its target, to ensure we don't send repetitive packets to the server */ bool alreadyInCombatWithTarget = !target.isEmpty() ? actor.getClass().getCreatureStats(actor).getAiSequence().isInCombat(target) : false; /* End of tes3mp addition */ if (!target.isEmpty()) MWBase::Environment::get().getMechanicsManager()->startCombat(actor, target); /* Start of tes3mp addition Send ActorAI packets when an actor starts combat, regardless of whether we're the cell authority or not; the server can decide if it wants to comply with them by forwarding them to the cell authority */ if (!target.isEmpty() && !alreadyInCombatWithTarget) { mwmp::ActorList *actorList = mwmp::Main::get().getNetworking()->getActorList(); actorList->reset(); actorList->cell = *actor.getCell()->getCell(); actorList->addAiActor(actor, target, mwmp::BaseActorList::COMBAT); actorList->sendAiActors(); } /* End of tes3mp addition */ } }; template class OpStopCombat : public Interpreter::Opcode0 { public: void execute (Interpreter::Runtime& runtime) override { MWWorld::Ptr actor = R()(runtime); MWMechanics::CreatureStats& creatureStats = actor.getClass().getCreatureStats(actor); creatureStats.getAiSequence().stopCombat(); } }; class OpToggleAI : public Interpreter::Opcode0 { public: void execute (Interpreter::Runtime& runtime) override { bool enabled = MWBase::Environment::get().getMechanicsManager()->toggleAI(); runtime.getContext().report (enabled ? "AI -> On" : "AI -> Off"); } }; template class OpFace : public Interpreter::Opcode0 { public: void execute(Interpreter::Runtime& runtime) override { MWWorld::Ptr actor = R()(runtime); Interpreter::Type_Float x = runtime[0].mFloat; runtime.pop(); Interpreter::Type_Float y = runtime[0].mFloat; runtime.pop(); MWMechanics::AiFace facePackage(x, y); actor.getClass().getCreatureStats(actor).getAiSequence().stack(facePackage, actor); } }; void installOpcodes (Interpreter::Interpreter& interpreter) { interpreter.installSegment3 (Compiler::Ai::opcodeAIActivate, new OpAiActivate); interpreter.installSegment3 (Compiler::Ai::opcodeAIActivateExplicit, new OpAiActivate); interpreter.installSegment3 (Compiler::Ai::opcodeAiTravel, new OpAiTravel); interpreter.installSegment3 (Compiler::Ai::opcodeAiTravelExplicit, new OpAiTravel); interpreter.installSegment3 (Compiler::Ai::opcodeAiEscort, new OpAiEscort); interpreter.installSegment3 (Compiler::Ai::opcodeAiEscortExplicit, new OpAiEscort); interpreter.installSegment3 (Compiler::Ai::opcodeAiEscortCell, new OpAiEscortCell); interpreter.installSegment3 (Compiler::Ai::opcodeAiEscortCellExplicit, new OpAiEscortCell); interpreter.installSegment3 (Compiler::Ai::opcodeAiWander, new OpAiWander); interpreter.installSegment3 (Compiler::Ai::opcodeAiWanderExplicit, new OpAiWander); interpreter.installSegment3 (Compiler::Ai::opcodeAiFollow, new OpAiFollow); interpreter.installSegment3 (Compiler::Ai::opcodeAiFollowExplicit, new OpAiFollow); interpreter.installSegment3 (Compiler::Ai::opcodeAiFollowCell, new OpAiFollowCell); interpreter.installSegment3 (Compiler::Ai::opcodeAiFollowCellExplicit, new OpAiFollowCell); interpreter.installSegment5 (Compiler::Ai::opcodeGetAiPackageDone, new OpGetAiPackageDone); interpreter.installSegment5 (Compiler::Ai::opcodeGetAiPackageDoneExplicit, new OpGetAiPackageDone); interpreter.installSegment5 (Compiler::Ai::opcodeGetCurrentAiPackage, new OpGetCurrentAIPackage); interpreter.installSegment5 (Compiler::Ai::opcodeGetCurrentAiPackageExplicit, new OpGetCurrentAIPackage); interpreter.installSegment5 (Compiler::Ai::opcodeGetDetected, new OpGetDetected); interpreter.installSegment5 (Compiler::Ai::opcodeGetDetectedExplicit, new OpGetDetected); interpreter.installSegment5 (Compiler::Ai::opcodeGetLineOfSight, new OpGetLineOfSight); interpreter.installSegment5 (Compiler::Ai::opcodeGetLineOfSightExplicit, new OpGetLineOfSight); interpreter.installSegment5 (Compiler::Ai::opcodeGetTarget, new OpGetTarget); interpreter.installSegment5 (Compiler::Ai::opcodeGetTargetExplicit, new OpGetTarget); interpreter.installSegment5 (Compiler::Ai::opcodeStartCombat, new OpStartCombat); interpreter.installSegment5 (Compiler::Ai::opcodeStartCombatExplicit, new OpStartCombat); interpreter.installSegment5 (Compiler::Ai::opcodeStopCombat, new OpStopCombat); interpreter.installSegment5 (Compiler::Ai::opcodeStopCombatExplicit, new OpStopCombat); interpreter.installSegment5 (Compiler::Ai::opcodeToggleAI, new OpToggleAI); interpreter.installSegment5 (Compiler::Ai::opcodeSetHello, new OpSetAiSetting(MWMechanics::CreatureStats::AiSetting::AI_Hello)); interpreter.installSegment5 (Compiler::Ai::opcodeSetHelloExplicit, new OpSetAiSetting(MWMechanics::CreatureStats::AiSetting::AI_Hello)); interpreter.installSegment5 (Compiler::Ai::opcodeSetFight, new OpSetAiSetting(MWMechanics::CreatureStats::AiSetting::AI_Fight)); interpreter.installSegment5 (Compiler::Ai::opcodeSetFightExplicit, new OpSetAiSetting(MWMechanics::CreatureStats::AiSetting::AI_Fight)); interpreter.installSegment5 (Compiler::Ai::opcodeSetFlee, new OpSetAiSetting(MWMechanics::CreatureStats::AiSetting::AI_Flee)); interpreter.installSegment5 (Compiler::Ai::opcodeSetFleeExplicit, new OpSetAiSetting(MWMechanics::CreatureStats::AiSetting::AI_Flee)); interpreter.installSegment5 (Compiler::Ai::opcodeSetAlarm, new OpSetAiSetting(MWMechanics::CreatureStats::AiSetting::AI_Alarm)); interpreter.installSegment5 (Compiler::Ai::opcodeSetAlarmExplicit, new OpSetAiSetting(MWMechanics::CreatureStats::AiSetting::AI_Alarm)); interpreter.installSegment5 (Compiler::Ai::opcodeModHello, new OpModAiSetting(MWMechanics::CreatureStats::AiSetting::AI_Hello)); interpreter.installSegment5 (Compiler::Ai::opcodeModHelloExplicit, new OpModAiSetting(MWMechanics::CreatureStats::AiSetting::AI_Hello)); interpreter.installSegment5 (Compiler::Ai::opcodeModFight, new OpModAiSetting(MWMechanics::CreatureStats::AiSetting::AI_Fight)); interpreter.installSegment5 (Compiler::Ai::opcodeModFightExplicit, new OpModAiSetting(MWMechanics::CreatureStats::AiSetting::AI_Fight)); interpreter.installSegment5 (Compiler::Ai::opcodeModFlee, new OpModAiSetting(MWMechanics::CreatureStats::AiSetting::AI_Flee)); interpreter.installSegment5 (Compiler::Ai::opcodeModFleeExplicit, new OpModAiSetting(MWMechanics::CreatureStats::AiSetting::AI_Flee)); interpreter.installSegment5 (Compiler::Ai::opcodeModAlarm, new OpModAiSetting(MWMechanics::CreatureStats::AiSetting::AI_Alarm)); interpreter.installSegment5 (Compiler::Ai::opcodeModAlarmExplicit, new OpModAiSetting(MWMechanics::CreatureStats::AiSetting::AI_Alarm)); interpreter.installSegment5 (Compiler::Ai::opcodeGetHello, new OpGetAiSetting(MWMechanics::CreatureStats::AiSetting::AI_Hello)); interpreter.installSegment5 (Compiler::Ai::opcodeGetHelloExplicit, new OpGetAiSetting(MWMechanics::CreatureStats::AiSetting::AI_Hello)); interpreter.installSegment5 (Compiler::Ai::opcodeGetFight, new OpGetAiSetting(MWMechanics::CreatureStats::AiSetting::AI_Fight)); interpreter.installSegment5 (Compiler::Ai::opcodeGetFightExplicit, new OpGetAiSetting(MWMechanics::CreatureStats::AiSetting::AI_Fight)); interpreter.installSegment5 (Compiler::Ai::opcodeGetFlee, new OpGetAiSetting(MWMechanics::CreatureStats::AiSetting::AI_Flee)); interpreter.installSegment5 (Compiler::Ai::opcodeGetFleeExplicit, new OpGetAiSetting(MWMechanics::CreatureStats::AiSetting::AI_Flee)); interpreter.installSegment5 (Compiler::Ai::opcodeGetAlarm, new OpGetAiSetting(MWMechanics::CreatureStats::AiSetting::AI_Alarm)); interpreter.installSegment5 (Compiler::Ai::opcodeGetAlarmExplicit, new OpGetAiSetting(MWMechanics::CreatureStats::AiSetting::AI_Alarm)); interpreter.installSegment5 (Compiler::Ai::opcodeFace, new OpFace); interpreter.installSegment5 (Compiler::Ai::opcodeFaceExplicit, new OpFace); } } } ================================================ FILE: apps/openmw/mwscript/aiextensions.hpp ================================================ #ifndef GAME_SCRIPT_AIEXTENSIONS_H #define GAME_SCRIPT_AIEXTENSIONS_H namespace Compiler { class Extensions; } namespace Interpreter { class Interpreter; } namespace MWScript { /// \brief AI-related script functionality namespace Ai { void installOpcodes (Interpreter::Interpreter& interpreter); } } #endif ================================================ FILE: apps/openmw/mwscript/animationextensions.cpp ================================================ #include "animationextensions.hpp" #include #include /* Start of tes3mp addition Include additional headers for multiplayer purposes */ #include "../mwmp/Main.hpp" #include "../mwmp/Networking.hpp" #include "../mwmp/ObjectList.hpp" #include "../mwmp/ScriptController.hpp" /* End of tes3mp addition */ #include "../mwworld/cellstore.hpp" #include "../mwworld/class.hpp" #include #include #include #include #include "../mwbase/environment.hpp" #include "../mwbase/mechanicsmanager.hpp" #include "interpretercontext.hpp" #include "ref.hpp" namespace MWScript { namespace Animation { template class OpSkipAnim : public Interpreter::Opcode0 { public: void execute (Interpreter::Runtime& runtime) override { MWWorld::Ptr ptr = R()(runtime); MWBase::Environment::get().getMechanicsManager()->skipAnimation (ptr); } }; template class OpPlayAnim : public Interpreter::Opcode1 { public: void execute (Interpreter::Runtime& runtime, unsigned int arg0) override { MWWorld::Ptr ptr = R()(runtime); if (!ptr.getRefData().isEnabled()) return; std::string group = runtime.getStringLiteral (runtime[0].mInteger); runtime.pop(); Interpreter::Type_Integer mode = 0; if (arg0==1) { mode = runtime[0].mInteger; runtime.pop(); if (mode<0 || mode>2) throw std::runtime_error ("animation mode out of range"); } /* Start of tes3mp addition Send an ID_OBJECT_ANIM_PLAY every time an animation is played for an object through an approved script */ if (mwmp::Main::isValidPacketScript(ptr.getClass().getScript(ptr))) { mwmp::ObjectList *objectList = mwmp::Main::get().getNetworking()->getObjectList(); objectList->reset(); objectList->packetOrigin = ScriptController::getPacketOriginFromContextType(runtime.getContext().getContextType()); objectList->originClientScript = runtime.getContext().getCurrentScriptName(); objectList->addObjectAnimPlay(ptr, group, mode); objectList->sendObjectAnimPlay(); } /* End of tes3mp addition */ MWBase::Environment::get().getMechanicsManager()->playAnimationGroup (ptr, group, mode, std::numeric_limits::max(), true); } }; template class OpLoopAnim : public Interpreter::Opcode1 { public: void execute (Interpreter::Runtime& runtime, unsigned int arg0) override { MWWorld::Ptr ptr = R()(runtime); if (!ptr.getRefData().isEnabled()) return; std::string group = runtime.getStringLiteral (runtime[0].mInteger); runtime.pop(); Interpreter::Type_Integer loops = runtime[0].mInteger; runtime.pop(); if (loops<0) throw std::runtime_error ("number of animation loops must be non-negative"); Interpreter::Type_Integer mode = 0; if (arg0==1) { mode = runtime[0].mInteger; runtime.pop(); if (mode<0 || mode>2) throw std::runtime_error ("animation mode out of range"); } MWBase::Environment::get().getMechanicsManager()->playAnimationGroup (ptr, group, mode, loops + 1, true); } }; void installOpcodes (Interpreter::Interpreter& interpreter) { interpreter.installSegment5 (Compiler::Animation::opcodeSkipAnim, new OpSkipAnim); interpreter.installSegment5 (Compiler::Animation::opcodeSkipAnimExplicit, new OpSkipAnim); interpreter.installSegment3 (Compiler::Animation::opcodePlayAnim, new OpPlayAnim); interpreter.installSegment3 (Compiler::Animation::opcodePlayAnimExplicit, new OpPlayAnim); interpreter.installSegment3 (Compiler::Animation::opcodeLoopAnim, new OpLoopAnim); interpreter.installSegment3 (Compiler::Animation::opcodeLoopAnimExplicit, new OpLoopAnim); } } } ================================================ FILE: apps/openmw/mwscript/animationextensions.hpp ================================================ #ifndef GAME_SCRIPT_ANIMATIONEXTENSIONS_H #define GAME_SCRIPT_ANIMATIONEXTENSIONS_H namespace Compiler { class Extensions; } namespace Interpreter { class Interpreter; } namespace MWScript { namespace Animation { void registerExtensions (Compiler::Extensions& extensions); void installOpcodes (Interpreter::Interpreter& interpreter); } } #endif ================================================ FILE: apps/openmw/mwscript/cellextensions.cpp ================================================ #include "cellextensions.hpp" #include #include "../mwworld/esmstore.hpp" #include #include #include #include #include "../mwworld/actionteleport.hpp" #include "../mwworld/cellstore.hpp" #include "../mwbase/environment.hpp" #include "../mwbase/statemanager.hpp" #include "../mwbase/windowmanager.hpp" #include "../mwbase/world.hpp" #include "../mwmechanics/actorutil.hpp" #include "interpretercontext.hpp" namespace MWScript { namespace Cell { class OpCellChanged : public Interpreter::Opcode0 { public: void execute (Interpreter::Runtime& runtime) override { runtime.push (MWBase::Environment::get().getWorld()->hasCellChanged() ? 1 : 0); } }; class OpTestCells : public Interpreter::Opcode0 { public: void execute (Interpreter::Runtime& runtime) override { if (MWBase::Environment::get().getStateManager()->getState() != MWBase::StateManager::State_NoGame) { runtime.getContext().report("Use TestCells from the main menu, when there is no active game session."); return; } bool wasConsole = MWBase::Environment::get().getWindowManager()->isConsoleMode(); if (wasConsole) MWBase::Environment::get().getWindowManager()->toggleConsole(); MWBase::Environment::get().getWorld()->testExteriorCells(); if (wasConsole) MWBase::Environment::get().getWindowManager()->toggleConsole(); } }; class OpTestInteriorCells : public Interpreter::Opcode0 { public: void execute (Interpreter::Runtime& runtime) override { if (MWBase::Environment::get().getStateManager()->getState() != MWBase::StateManager::State_NoGame) { runtime.getContext().report("Use TestInteriorCells from the main menu, when there is no active game session."); return; } bool wasConsole = MWBase::Environment::get().getWindowManager()->isConsoleMode(); if (wasConsole) MWBase::Environment::get().getWindowManager()->toggleConsole(); MWBase::Environment::get().getWorld()->testInteriorCells(); if (wasConsole) MWBase::Environment::get().getWindowManager()->toggleConsole(); } }; class OpCOC : public Interpreter::Opcode0 { public: void execute (Interpreter::Runtime& runtime) override { std::string cell = runtime.getStringLiteral (runtime[0].mInteger); runtime.pop(); ESM::Position pos; MWBase::World *world = MWBase::Environment::get().getWorld(); const MWWorld::Ptr playerPtr = world->getPlayerPtr(); if (world->findExteriorPosition(cell, pos)) { MWWorld::ActionTeleport("", pos, false).execute(playerPtr); world->adjustPosition(playerPtr, false); } else { // Change to interior even if findInteriorPosition() // yields false. In this case position will be zero-point. world->findInteriorPosition(cell, pos); MWWorld::ActionTeleport(cell, pos, false).execute(playerPtr); } } }; class OpCOE : public Interpreter::Opcode0 { public: void execute (Interpreter::Runtime& runtime) override { Interpreter::Type_Integer x = runtime[0].mInteger; runtime.pop(); Interpreter::Type_Integer y = runtime[0].mInteger; runtime.pop(); ESM::Position pos; MWBase::World *world = MWBase::Environment::get().getWorld(); const MWWorld::Ptr playerPtr = world->getPlayerPtr(); world->indexToPosition (x, y, pos.pos[0], pos.pos[1], true); pos.pos[2] = 0; pos.rot[0] = pos.rot[1] = pos.rot[2] = 0; MWWorld::ActionTeleport("", pos, false).execute(playerPtr); world->adjustPosition(playerPtr, false); } }; class OpGetInterior : public Interpreter::Opcode0 { public: void execute (Interpreter::Runtime& runtime) override { if (!MWMechanics::getPlayer().isInCell()) { runtime.push (0); return; } bool interior = !MWMechanics::getPlayer().getCell()->getCell()->isExterior(); runtime.push (interior ? 1 : 0); } }; class OpGetPCCell : public Interpreter::Opcode0 { public: void execute (Interpreter::Runtime& runtime) override { std::string name = runtime.getStringLiteral (runtime[0].mInteger); runtime.pop(); if (!MWMechanics::getPlayer().isInCell()) { runtime.push(0); return; } const MWWorld::CellStore *cell = MWMechanics::getPlayer().getCell(); std::string current = MWBase::Environment::get().getWorld()->getCellName(cell); Misc::StringUtils::lowerCaseInPlace(current); bool match = current.length()>=name.length() && current.substr (0, name.length())==name; runtime.push (match ? 1 : 0); } }; class OpGetWaterLevel : public Interpreter::Opcode0 { public: void execute (Interpreter::Runtime& runtime) override { if (!MWMechanics::getPlayer().isInCell()) { runtime.push(0.f); return; } MWWorld::CellStore *cell = MWMechanics::getPlayer().getCell(); if (cell->isExterior()) runtime.push(0.f); // vanilla oddity, return 0 even though water is actually at -1 else if (cell->getCell()->hasWater()) runtime.push (cell->getWaterLevel()); else runtime.push (-std::numeric_limits::max()); } }; class OpSetWaterLevel : public Interpreter::Opcode0 { public: void execute (Interpreter::Runtime& runtime) override { Interpreter::Type_Float level = runtime[0].mFloat; if (!MWMechanics::getPlayer().isInCell()) { return; } MWWorld::CellStore *cell = MWMechanics::getPlayer().getCell(); if (cell->getCell()->isExterior()) throw std::runtime_error("Can't set water level in exterior cell"); cell->setWaterLevel (level); MWBase::Environment::get().getWorld()->setWaterHeight (cell->getWaterLevel()); } }; class OpModWaterLevel : public Interpreter::Opcode0 { public: void execute (Interpreter::Runtime& runtime) override { Interpreter::Type_Float level = runtime[0].mFloat; if (!MWMechanics::getPlayer().isInCell()) { return; } MWWorld::CellStore *cell = MWMechanics::getPlayer().getCell(); if (cell->getCell()->isExterior()) throw std::runtime_error("Can't set water level in exterior cell"); cell->setWaterLevel (cell->getWaterLevel()+level); MWBase::Environment::get().getWorld()->setWaterHeight(cell->getWaterLevel()); } }; void installOpcodes (Interpreter::Interpreter& interpreter) { interpreter.installSegment5 (Compiler::Cell::opcodeCellChanged, new OpCellChanged); interpreter.installSegment5 (Compiler::Cell::opcodeTestCells, new OpTestCells); interpreter.installSegment5 (Compiler::Cell::opcodeTestInteriorCells, new OpTestInteriorCells); interpreter.installSegment5 (Compiler::Cell::opcodeCOC, new OpCOC); interpreter.installSegment5 (Compiler::Cell::opcodeCOE, new OpCOE); interpreter.installSegment5 (Compiler::Cell::opcodeGetInterior, new OpGetInterior); interpreter.installSegment5 (Compiler::Cell::opcodeGetPCCell, new OpGetPCCell); interpreter.installSegment5 (Compiler::Cell::opcodeGetWaterLevel, new OpGetWaterLevel); interpreter.installSegment5 (Compiler::Cell::opcodeSetWaterLevel, new OpSetWaterLevel); interpreter.installSegment5 (Compiler::Cell::opcodeModWaterLevel, new OpModWaterLevel); } } } ================================================ FILE: apps/openmw/mwscript/cellextensions.hpp ================================================ #ifndef GAME_SCRIPT_CELLEXTENSIONS_H #define GAME_SCRIPT_CELLEXTENSIONS_H namespace Compiler { class Extensions; } namespace Interpreter { class Interpreter; } namespace MWScript { /// \brief cell-related script functionality namespace Cell { void installOpcodes (Interpreter::Interpreter& interpreter); } } #endif ================================================ FILE: apps/openmw/mwscript/compilercontext.cpp ================================================ #include "compilercontext.hpp" #include "../mwworld/esmstore.hpp" #include #include #include "../mwbase/environment.hpp" #include "../mwbase/world.hpp" #include "../mwbase/scriptmanager.hpp" #include "../mwworld/ptr.hpp" #include "../mwworld/class.hpp" #include "../mwworld/manualref.hpp" namespace MWScript { CompilerContext::CompilerContext (Type type) : mType (type) {} bool CompilerContext::canDeclareLocals() const { return mType==Type_Full; } char CompilerContext::getGlobalType (const std::string& name) const { return MWBase::Environment::get().getWorld()->getGlobalVariableType (name); } std::pair CompilerContext::getMemberType (const std::string& name, const std::string& id) const { std::string script; bool reference = false; if (const ESM::Script *scriptRecord = MWBase::Environment::get().getWorld()->getStore().get().search (id)) { script = scriptRecord->mId; } else { MWWorld::ManualRef ref (MWBase::Environment::get().getWorld()->getStore(), id); script = ref.getPtr().getClass().getScript (ref.getPtr()); reference = true; } char type = ' '; if (!script.empty()) type = MWBase::Environment::get().getScriptManager()->getLocals (script).getType ( Misc::StringUtils::lowerCase (name)); return std::make_pair (type, reference); } bool CompilerContext::isId (const std::string& name) const { const MWWorld::ESMStore &store = MWBase::Environment::get().getWorld()->getStore(); return store.get().search (name) || store.get().search (name) || store.get().search (name) || store.get().search (name) || store.get().search (name) || store.get().search (name) || store.get().search (name) || store.get().search (name) || store.get().search (name) || store.get().search (name) || store.get().search (name) || store.get().search (name) || store.get().search (name) || store.get().search (name) || store.get().search (name) || store.get().search (name) || store.get().search (name) || store.get().search (name) || store.get().search (name) || store.get().search (name) || store.get().search (name); } bool CompilerContext::isJournalId (const std::string& name) const { const MWWorld::ESMStore &store = MWBase::Environment::get().getWorld()->getStore(); const ESM::Dialogue *topic = store.get().search (name); return topic && topic->mType==ESM::Dialogue::Journal; } } ================================================ FILE: apps/openmw/mwscript/compilercontext.hpp ================================================ #ifndef GAME_SCRIPT_COMPILERCONTEXT_H #define GAME_SCRIPT_COMPILERCONTEXT_H #include namespace MWScript { class CompilerContext : public Compiler::Context { public: enum Type { Type_Full, // global, local, targeted Type_Dialogue, Type_Console }; private: Type mType; public: CompilerContext (Type type); /// Is the compiler allowed to declare local variables? bool canDeclareLocals() const override; /// 'l: long, 's': short, 'f': float, ' ': does not exist. char getGlobalType (const std::string& name) const override; std::pair getMemberType (const std::string& name, const std::string& id) const override; ///< Return type of member variable \a name in script \a id or in script of reference of /// \a id /// \return first: 'l: long, 's': short, 'f': float, ' ': does not exist. /// second: true: script of reference bool isId (const std::string& name) const override; ///< Does \a name match an ID, that can be referenced? bool isJournalId (const std::string& name) const override; ///< Does \a name match a journal ID? }; } #endif ================================================ FILE: apps/openmw/mwscript/consoleextensions.cpp ================================================ #include "consoleextensions.hpp" #include namespace MWScript { namespace Console { void installOpcodes (Interpreter::Interpreter& interpreter) { } } } ================================================ FILE: apps/openmw/mwscript/consoleextensions.hpp ================================================ #ifndef GAME_SCRIPT_CONSOLEEXTENSIONS_H #define GAME_SCRIPT_CONSOLEEXTENSIONS_H namespace Compiler { class Extensions; } namespace Interpreter { class Interpreter; } namespace MWScript { /// \brief Script functionality limited to the console namespace Console { void installOpcodes (Interpreter::Interpreter& interpreter); } } #endif ================================================ FILE: apps/openmw/mwscript/containerextensions.cpp ================================================ #include "containerextensions.hpp" #include #include /* Start of tes3mp addition Include additional headers for multiplayer purposes */ #include "../mwmp/Main.hpp" #include "../mwmp/Networking.hpp" #include "../mwmp/LocalPlayer.hpp" #include "../mwmp/PlayerList.hpp" #include "../mwmp/ObjectList.hpp" #include "../mwmp/ScriptController.hpp" #include /* End of tes3mp addition */ #include #include #include #include #include #include #include "../mwbase/environment.hpp" #include "../mwbase/windowmanager.hpp" #include "../mwbase/world.hpp" #include "../mwclass/container.hpp" #include "../mwworld/action.hpp" #include "../mwworld/class.hpp" #include "../mwworld/containerstore.hpp" #include "../mwworld/inventorystore.hpp" #include "../mwworld/manualref.hpp" #include "../mwmechanics/actorutil.hpp" #include "../mwmechanics/levelledlist.hpp" #include "ref.hpp" namespace { void addToStore(const MWWorld::Ptr& itemPtr, int count, MWWorld::Ptr& ptr, MWWorld::ContainerStore& store, bool resolve = true) { if (itemPtr.getClass().getScript(itemPtr).empty()) { store.add (itemPtr, count, ptr, true, resolve); } else { // Adding just one item per time to make sure there isn't a stack of scripted items for (int i = 0; i < count; i++) store.add (itemPtr, 1, ptr, true, resolve); } } void addRandomToStore(const MWWorld::Ptr& itemPtr, int count, MWWorld::Ptr& owner, MWWorld::ContainerStore& store, bool topLevel = true) { if(itemPtr.getTypeName() == typeid(ESM::ItemLevList).name()) { const ESM::ItemLevList* levItemList = itemPtr.get()->mBase; if(topLevel && count > 1 && levItemList->mFlags & ESM::ItemLevList::Each) { for(int i = 0; i < count; i++) addRandomToStore(itemPtr, 1, owner, store, true); } else { std::string itemId = MWMechanics::getLevelledItem(itemPtr.get()->mBase, false); if (itemId.empty()) return; MWWorld::ManualRef manualRef(MWBase::Environment::get().getWorld()->getStore(), itemId, 1); addRandomToStore(manualRef.getPtr(), count, owner, store, false); } } else addToStore(itemPtr, count, owner, store); } } namespace MWScript { namespace Container { template class OpAddItem : public Interpreter::Opcode0 { public: void execute (Interpreter::Runtime& runtime) override { MWWorld::Ptr ptr = R()(runtime); std::string item = runtime.getStringLiteral (runtime[0].mInteger); runtime.pop(); Interpreter::Type_Integer count = runtime[0].mInteger; runtime.pop(); if (count<0) count = static_cast(count); // no-op if (count == 0) return; if(::Misc::StringUtils::ciEqual(item, "gold_005") || ::Misc::StringUtils::ciEqual(item, "gold_010") || ::Misc::StringUtils::ciEqual(item, "gold_025") || ::Misc::StringUtils::ciEqual(item, "gold_100")) item = "gold_001"; // Check if "item" can be placed in a container MWWorld::ManualRef manualRef(MWBase::Environment::get().getWorld()->getStore(), item, 1); MWWorld::Ptr itemPtr = manualRef.getPtr(); bool isLevelledList = itemPtr.getClass().getTypeName() == typeid(ESM::ItemLevList).name(); if(!isLevelledList) MWWorld::ContainerStore::getType(itemPtr); // Explicit calls to non-unique actors affect the base record if(!R::implicit && ptr.getClass().isActor() && MWBase::Environment::get().getWorld()->getStore().getRefCount(ptr.getCellRef().getRefId()) > 1) { ptr.getClass().modifyBaseInventory(ptr.getCellRef().getRefId(), item, count); return; } /* Start of tes3mp change (major) Allow unilateral item removal on this client from client scripts and dialogue (but not console commands) to prevent infinite loops in certain mods. Otherwise, expect the server's reply to our packet to do the removal instead, except for changes to player inventories which still require the PlayerInventory to be reworked. */ unsigned char packetOrigin = ScriptController::getPacketOriginFromContextType(runtime.getContext().getContextType()); if (ptr == MWBase::Environment::get().getWorld()->getPlayerPtr() || packetOrigin != mwmp::CLIENT_CONSOLE) { // Calls to unresolved containers affect the base record if (ptr.getClass().getTypeName() == typeid(ESM::Container).name() && (!ptr.getRefData().getCustomData() || !ptr.getClass().getContainerStore(ptr).isResolved())) { ptr.getClass().modifyBaseInventory(ptr.getCellRef().getRefId(), item, count); const ESM::Container* baseRecord = MWBase::Environment::get().getWorld()->getStore().get().find(ptr.getCellRef().getRefId()); const auto& ptrs = MWBase::Environment::get().getWorld()->getAll(ptr.getCellRef().getRefId()); for (const auto& container : ptrs) { // use the new base record container.get()->mBase = baseRecord; if (container.getRefData().getCustomData()) { auto& store = container.getClass().getContainerStore(container); if (isLevelledList) { if (store.isResolved()) { addRandomToStore(itemPtr, count, ptr, store); } } else addToStore(itemPtr, count, ptr, store, store.isResolved()); } } return; } MWWorld::ContainerStore& store = ptr.getClass().getContainerStore(ptr); if (isLevelledList) addRandomToStore(itemPtr, count, ptr, store); else addToStore(itemPtr, count, ptr, store); } /* End of tes3mp change (major) */ // Spawn a messagebox (only for items added to player's inventory and if player is talking to someone) if (ptr == MWBase::Environment::get().getWorld ()->getPlayerPtr() ) { // The two GMST entries below expand to strings informing the player of what, and how many of it has been added to their inventory std::string msgBox; std::string itemName = itemPtr.getClass().getName(itemPtr); if (count == 1) { msgBox = MyGUI::LanguageManager::getInstance().replaceTags("#{sNotifyMessage60}"); msgBox = ::Misc::StringUtils::format(msgBox, itemName); } else { msgBox = MyGUI::LanguageManager::getInstance().replaceTags("#{sNotifyMessage61}"); msgBox = ::Misc::StringUtils::format(msgBox, count, itemName); } MWBase::Environment::get().getWindowManager()->messageBox(msgBox, MWGui::ShowInDialogueMode_Only); } /* Start of tes3mp addition Send an ID_CONTAINER packet every time an item is added to a Ptr that doesn't belong to a DedicatedPlayer */ else if (mwmp::Main::get().getLocalPlayer()->isLoggedIn() && (!ptr.getClass().isActor() || !mwmp::PlayerList::isDedicatedPlayer(ptr))) { mwmp::ObjectList *objectList = mwmp::Main::get().getNetworking()->getObjectList(); objectList->reset(); objectList->packetOrigin = packetOrigin; objectList->originClientScript = runtime.getContext().getCurrentScriptName(); objectList->cell = *ptr.getCell()->getCell(); objectList->action = mwmp::BaseObjectList::ADD; objectList->containerSubAction = mwmp::BaseObjectList::NONE; mwmp::BaseObject baseObject = objectList->getBaseObjectFromPtr(ptr); objectList->addContainerItem(baseObject, item, count, 0); objectList->addBaseObject(baseObject); objectList->sendContainer(); } /* End of tes3mp addition */ } }; template class OpGetItemCount : public Interpreter::Opcode0 { public: void execute (Interpreter::Runtime& runtime) override { MWWorld::Ptr ptr = R()(runtime); std::string item = runtime.getStringLiteral (runtime[0].mInteger); runtime.pop(); if(::Misc::StringUtils::ciEqual(item, "gold_005") || ::Misc::StringUtils::ciEqual(item, "gold_010") || ::Misc::StringUtils::ciEqual(item, "gold_025") || ::Misc::StringUtils::ciEqual(item, "gold_100")) item = "gold_001"; MWWorld::ContainerStore& store = ptr.getClass().getContainerStore (ptr); runtime.push (store.count(item)); } }; template class OpRemoveItem : public Interpreter::Opcode0 { public: void execute (Interpreter::Runtime& runtime) override { MWWorld::Ptr ptr = R()(runtime); std::string item = runtime.getStringLiteral (runtime[0].mInteger); runtime.pop(); Interpreter::Type_Integer count = runtime[0].mInteger; runtime.pop(); if (count<0) throw std::runtime_error ("second argument for RemoveItem must be non-negative"); // no-op if (count == 0) return; if(::Misc::StringUtils::ciEqual(item, "gold_005") || ::Misc::StringUtils::ciEqual(item, "gold_010") || ::Misc::StringUtils::ciEqual(item, "gold_025") || ::Misc::StringUtils::ciEqual(item, "gold_100")) item = "gold_001"; // Explicit calls to non-unique actors affect the base record if(!R::implicit && ptr.getClass().isActor() && MWBase::Environment::get().getWorld()->getStore().getRefCount(ptr.getCellRef().getRefId()) > 1) { ptr.getClass().modifyBaseInventory(ptr.getCellRef().getRefId(), item, -count); return; } // Calls to unresolved containers affect the base record instead else if(ptr.getClass().getTypeName() == typeid(ESM::Container).name() && (!ptr.getRefData().getCustomData() || !ptr.getClass().getContainerStore(ptr).isResolved())) { ptr.getClass().modifyBaseInventory(ptr.getCellRef().getRefId(), item, -count); const ESM::Container* baseRecord = MWBase::Environment::get().getWorld()->getStore().get().find(ptr.getCellRef().getRefId()); const auto& ptrs = MWBase::Environment::get().getWorld()->getAll(ptr.getCellRef().getRefId()); for(const auto& container : ptrs) { container.get()->mBase = baseRecord; if(container.getRefData().getCustomData()) { auto& store = container.getClass().getContainerStore(container); // Note that unlike AddItem, RemoveItem only removes from unresolved containers if(!store.isResolved()) store.remove(item, count, ptr, false, false); } } return; } MWWorld::ContainerStore& store = ptr.getClass().getContainerStore (ptr); std::string itemName; for (MWWorld::ConstContainerStoreIterator iter(store.cbegin()); iter != store.cend(); ++iter) { if (::Misc::StringUtils::ciEqual(iter->getCellRef().getRefId(), item)) { itemName = iter->getClass().getName(*iter); break; } } /* Start of tes3mp change (major) Allow unilateral item removal on this client from client scripts and dialogue (but not console commands) to prevent infinite loops in certain mods. Otherwise, expect the server's reply to our packet to do the removal instead, except for changes to player inventories which still require the PlayerInventory to be reworked. */ unsigned char packetOrigin = ScriptController::getPacketOriginFromContextType(runtime.getContext().getContextType()); int numRemoved = 0; if (ptr == MWMechanics::getPlayer() || packetOrigin != mwmp::CLIENT_CONSOLE) numRemoved = store.remove(item, count, ptr); // Spawn a messagebox (only for items removed from player's inventory) if ((numRemoved > 0) && (ptr == MWMechanics::getPlayer())) { /* End of tes3mp change (major) */ // The two GMST entries below expand to strings informing the player of what, and how many of it has been removed from their inventory std::string msgBox; if (numRemoved > 1) { msgBox = MyGUI::LanguageManager::getInstance().replaceTags("#{sNotifyMessage63}"); msgBox = ::Misc::StringUtils::format(msgBox, numRemoved, itemName); } else { msgBox = MyGUI::LanguageManager::getInstance().replaceTags("#{sNotifyMessage62}"); msgBox = ::Misc::StringUtils::format(msgBox, itemName); } MWBase::Environment::get().getWindowManager()->messageBox(msgBox, MWGui::ShowInDialogueMode_Only); } /* Start of tes3mp addition Send an ID_CONTAINER packet every time an item is removed from a Ptr that doesn't belong to a DedicatedPlayer */ else if (mwmp::Main::get().getLocalPlayer()->isLoggedIn() && (!ptr.getClass().isActor() || !mwmp::PlayerList::isDedicatedPlayer(ptr))) { mwmp::ObjectList *objectList = mwmp::Main::get().getNetworking()->getObjectList(); objectList->reset(); objectList->packetOrigin = packetOrigin; objectList->originClientScript = runtime.getContext().getCurrentScriptName(); objectList->cell = *ptr.getCell()->getCell(); objectList->action = mwmp::BaseObjectList::REMOVE; objectList->containerSubAction = mwmp::BaseObjectList::NONE; mwmp::BaseObject baseObject = objectList->getBaseObjectFromPtr(ptr); objectList->addContainerItem(baseObject, item, 0, count); objectList->addBaseObject(baseObject); objectList->sendContainer(); } /* End of tes3mp addition */ } }; template class OpEquip : public Interpreter::Opcode0 { public: void execute(Interpreter::Runtime &runtime) override { MWWorld::Ptr ptr = R()(runtime); std::string item = runtime.getStringLiteral (runtime[0].mInteger); runtime.pop(); MWWorld::InventoryStore& invStore = ptr.getClass().getInventoryStore (ptr); MWWorld::ContainerStoreIterator it = invStore.begin(); for (; it != invStore.end(); ++it) { if (::Misc::StringUtils::ciEqual(it->getCellRef().getRefId(), item)) break; } if (it == invStore.end()) { it = ptr.getClass().getContainerStore (ptr).add (item, 1, ptr); Log(Debug::Warning) << "Implicitly adding one " << item << " to the inventory store of " << ptr.getCellRef().getRefId() << " to fulfill the requirements of Equip instruction"; } if (ptr == MWMechanics::getPlayer()) MWBase::Environment::get().getWindowManager()->useItem(*it, true); else { std::shared_ptr action = it->getClass().use(*it, true); action->execute(ptr, true); } } }; template class OpGetArmorType : public Interpreter::Opcode0 { public: void execute(Interpreter::Runtime &runtime) override { MWWorld::Ptr ptr = R()(runtime); Interpreter::Type_Integer location = runtime[0].mInteger; runtime.pop(); int slot; switch (location) { case 0: slot = MWWorld::InventoryStore::Slot_Helmet; break; case 1: slot = MWWorld::InventoryStore::Slot_Cuirass; break; case 2: slot = MWWorld::InventoryStore::Slot_LeftPauldron; break; case 3: slot = MWWorld::InventoryStore::Slot_RightPauldron; break; case 4: slot = MWWorld::InventoryStore::Slot_Greaves; break; case 5: slot = MWWorld::InventoryStore::Slot_Boots; break; case 6: slot = MWWorld::InventoryStore::Slot_LeftGauntlet; break; case 7: slot = MWWorld::InventoryStore::Slot_RightGauntlet; break; case 8: slot = MWWorld::InventoryStore::Slot_CarriedLeft; // shield break; case 9: slot = MWWorld::InventoryStore::Slot_LeftGauntlet; break; case 10: slot = MWWorld::InventoryStore::Slot_RightGauntlet; break; default: throw std::runtime_error ("armor index out of range"); } const MWWorld::InventoryStore& invStore = ptr.getClass().getInventoryStore (ptr); MWWorld::ConstContainerStoreIterator it = invStore.getSlot (slot); if (it == invStore.end() || it->getTypeName () != typeid(ESM::Armor).name()) { runtime.push(-1); return; } int skill = it->getClass().getEquipmentSkill (*it) ; if (skill == ESM::Skill::HeavyArmor) runtime.push(2); else if (skill == ESM::Skill::MediumArmor) runtime.push(1); else if (skill == ESM::Skill::LightArmor) runtime.push(0); else runtime.push(-1); } }; template class OpHasItemEquipped : public Interpreter::Opcode0 { public: void execute(Interpreter::Runtime &runtime) override { MWWorld::Ptr ptr = R()(runtime); std::string item = runtime.getStringLiteral (runtime[0].mInteger); runtime.pop(); const MWWorld::InventoryStore& invStore = ptr.getClass().getInventoryStore (ptr); for (int slot = 0; slot < MWWorld::InventoryStore::Slots; ++slot) { MWWorld::ConstContainerStoreIterator it = invStore.getSlot (slot); if (it != invStore.end() && ::Misc::StringUtils::ciEqual(it->getCellRef().getRefId(), item)) { runtime.push(1); return; } } runtime.push(0); } }; template class OpHasSoulGem : public Interpreter::Opcode0 { public: void execute(Interpreter::Runtime &runtime) override { MWWorld::Ptr ptr = R()(runtime); const std::string &name = runtime.getStringLiteral (runtime[0].mInteger); runtime.pop(); int count = 0; const MWWorld::InventoryStore& invStore = ptr.getClass().getInventoryStore (ptr); for (MWWorld::ConstContainerStoreIterator it = invStore.cbegin(MWWorld::ContainerStore::Type_Miscellaneous); it != invStore.cend(); ++it) { if (::Misc::StringUtils::ciEqual(it->getCellRef().getSoul(), name)) count += it->getRefData().getCount(); } runtime.push(count); } }; template class OpGetWeaponType : public Interpreter::Opcode0 { public: void execute(Interpreter::Runtime &runtime) override { MWWorld::Ptr ptr = R()(runtime); const MWWorld::InventoryStore& invStore = ptr.getClass().getInventoryStore (ptr); MWWorld::ConstContainerStoreIterator it = invStore.getSlot (MWWorld::InventoryStore::Slot_CarriedRight); if (it == invStore.end()) { runtime.push(-1); return; } else if (it->getTypeName() != typeid(ESM::Weapon).name()) { if (it->getTypeName() == typeid(ESM::Lockpick).name()) { runtime.push(-2); } else if (it->getTypeName() == typeid(ESM::Probe).name()) { runtime.push(-3); } else { runtime.push(-1); } return; } runtime.push(it->get()->mBase->mData.mType); } }; void installOpcodes (Interpreter::Interpreter& interpreter) { interpreter.installSegment5 (Compiler::Container::opcodeAddItem, new OpAddItem); interpreter.installSegment5 (Compiler::Container::opcodeAddItemExplicit, new OpAddItem); interpreter.installSegment5 (Compiler::Container::opcodeGetItemCount, new OpGetItemCount); interpreter.installSegment5 (Compiler::Container::opcodeGetItemCountExplicit, new OpGetItemCount); interpreter.installSegment5 (Compiler::Container::opcodeRemoveItem, new OpRemoveItem); interpreter.installSegment5 (Compiler::Container::opcodeRemoveItemExplicit, new OpRemoveItem); interpreter.installSegment5 (Compiler::Container::opcodeEquip, new OpEquip); interpreter.installSegment5 (Compiler::Container::opcodeEquipExplicit, new OpEquip); interpreter.installSegment5 (Compiler::Container::opcodeGetArmorType, new OpGetArmorType); interpreter.installSegment5 (Compiler::Container::opcodeGetArmorTypeExplicit, new OpGetArmorType); interpreter.installSegment5 (Compiler::Container::opcodeHasItemEquipped, new OpHasItemEquipped); interpreter.installSegment5 (Compiler::Container::opcodeHasItemEquippedExplicit, new OpHasItemEquipped); interpreter.installSegment5 (Compiler::Container::opcodeHasSoulGem, new OpHasSoulGem); interpreter.installSegment5 (Compiler::Container::opcodeHasSoulGemExplicit, new OpHasSoulGem); interpreter.installSegment5 (Compiler::Container::opcodeGetWeaponType, new OpGetWeaponType); interpreter.installSegment5 (Compiler::Container::opcodeGetWeaponTypeExplicit, new OpGetWeaponType); } } } ================================================ FILE: apps/openmw/mwscript/containerextensions.hpp ================================================ #ifndef GAME_SCRIPT_CONTAINEREXTENSIONS_H #define GAME_SCRIPT_CONTAINEREXTENSIONS_H namespace Compiler { class Extensions; } namespace Interpreter { class Interpreter; } namespace MWScript { /// \brief Container-related script functionality (chests, NPCs, creatures) namespace Container { void installOpcodes (Interpreter::Interpreter& interpreter); } } #endif ================================================ FILE: apps/openmw/mwscript/controlextensions.cpp ================================================ #include "controlextensions.hpp" #include #include #include #include /* Start of tes3mp addition Include additional headers for multiplayer purposes */ #include "../mwmp/Main.hpp" #include "../mwmp/LocalPlayer.hpp" /* End of tes3mp addition */ #include "../mwbase/environment.hpp" #include "../mwbase/inputmanager.hpp" #include "../mwbase/mechanicsmanager.hpp" #include "../mwbase/world.hpp" #include "../mwworld/class.hpp" #include "../mwworld/ptr.hpp" #include "interpretercontext.hpp" #include "ref.hpp" namespace MWScript { namespace Control { class OpSetControl : public Interpreter::Opcode0 { std::string mControl; bool mEnable; public: OpSetControl (const std::string& control, bool enable) : mControl (control), mEnable (enable) {} void execute (Interpreter::Runtime& runtime) override { MWBase::Environment::get() .getInputManager() ->toggleControlSwitch(mControl, mEnable); } }; class OpGetDisabled : public Interpreter::Opcode0 { std::string mControl; public: OpGetDisabled (const std::string& control) : mControl (control) {} void execute (Interpreter::Runtime& runtime) override { runtime.push(!MWBase::Environment::get().getInputManager()->getControlSwitch (mControl)); } }; class OpToggleCollision : public Interpreter::Opcode0 { public: void execute (Interpreter::Runtime& runtime) override { bool enabled = MWBase::Environment::get().getWorld()->toggleCollisionMode(); /* Start of tes3mp addition Update the LocalPlayer's tclState so it gets sent to the server */ mwmp::Main::get().getLocalPlayer()->hasTcl = !enabled; /* End of tes3mp addition */ runtime.getContext().report (enabled ? "Collision -> On" : "Collision -> Off"); } }; template class OpClearMovementFlag : public Interpreter::Opcode0 { MWMechanics::CreatureStats::Flag mFlag; public: OpClearMovementFlag (MWMechanics::CreatureStats::Flag flag) : mFlag (flag) {} void execute (Interpreter::Runtime& runtime) override { MWWorld::Ptr ptr = R()(runtime); ptr.getClass().getCreatureStats(ptr).setMovementFlag (mFlag, false); } }; template class OpSetMovementFlag : public Interpreter::Opcode0 { MWMechanics::CreatureStats::Flag mFlag; public: OpSetMovementFlag (MWMechanics::CreatureStats::Flag flag) : mFlag (flag) {} void execute (Interpreter::Runtime& runtime) override { MWWorld::Ptr ptr = R()(runtime); ptr.getClass().getCreatureStats(ptr).setMovementFlag (mFlag, true); } }; template class OpGetForceRun : public Interpreter::Opcode0 { public: void execute (Interpreter::Runtime& runtime) override { MWWorld::Ptr ptr = R()(runtime); MWMechanics::CreatureStats& stats = ptr.getClass().getCreatureStats (ptr); runtime.push (stats.getMovementFlag (MWMechanics::CreatureStats::Flag_ForceRun)); } }; template class OpGetForceJump : public Interpreter::Opcode0 { public: void execute (Interpreter::Runtime& runtime) override { MWWorld::Ptr ptr = R()(runtime); MWMechanics::CreatureStats& stats = ptr.getClass().getCreatureStats (ptr); runtime.push (stats.getMovementFlag (MWMechanics::CreatureStats::Flag_ForceJump)); } }; template class OpGetForceMoveJump : public Interpreter::Opcode0 { public: void execute (Interpreter::Runtime& runtime) override { MWWorld::Ptr ptr = R()(runtime); MWMechanics::CreatureStats& stats = ptr.getClass().getCreatureStats (ptr); runtime.push (stats.getMovementFlag (MWMechanics::CreatureStats::Flag_ForceMoveJump)); } }; template class OpGetForceSneak : public Interpreter::Opcode0 { public: void execute (Interpreter::Runtime& runtime) override { MWWorld::Ptr ptr = R()(runtime); MWMechanics::CreatureStats& stats = ptr.getClass().getCreatureStats(ptr); runtime.push (stats.getMovementFlag (MWMechanics::CreatureStats::Flag_ForceSneak)); } }; class OpGetPcRunning : public Interpreter::Opcode0 { public: void execute (Interpreter::Runtime& runtime) override { MWWorld::Ptr ptr = MWBase::Environment::get().getWorld()->getPlayerPtr(); MWMechanics::CreatureStats& stats = ptr.getClass().getCreatureStats(ptr); MWBase::World* world = MWBase::Environment::get().getWorld(); bool stanceOn = stats.getStance(MWMechanics::CreatureStats::Stance_Run); bool running = MWBase::Environment::get().getMechanicsManager()->isRunning(ptr); bool inair = !world->isOnGround(ptr) && !world->isSwimming(ptr) && !world->isFlying(ptr); runtime.push(stanceOn && (running || inair)); } }; class OpGetPcSneaking : public Interpreter::Opcode0 { public: void execute (Interpreter::Runtime& runtime) override { MWWorld::Ptr ptr = MWBase::Environment::get().getWorld()->getPlayerPtr(); runtime.push(MWBase::Environment::get().getMechanicsManager()->isSneaking(ptr)); } }; void installOpcodes (Interpreter::Interpreter& interpreter) { for (int i=0; i (MWMechanics::CreatureStats::Flag_ForceRun)); interpreter.installSegment5 (Compiler::Control::opcodeClearForceRunExplicit, new OpClearMovementFlag (MWMechanics::CreatureStats::Flag_ForceRun)); interpreter.installSegment5 (Compiler::Control::opcodeForceRun, new OpSetMovementFlag (MWMechanics::CreatureStats::Flag_ForceRun)); interpreter.installSegment5 (Compiler::Control::opcodeForceRunExplicit, new OpSetMovementFlag (MWMechanics::CreatureStats::Flag_ForceRun)); //Force Jump interpreter.installSegment5 (Compiler::Control::opcodeClearForceJump, new OpClearMovementFlag (MWMechanics::CreatureStats::Flag_ForceJump)); interpreter.installSegment5 (Compiler::Control::opcodeClearForceJumpExplicit, new OpClearMovementFlag (MWMechanics::CreatureStats::Flag_ForceJump)); interpreter.installSegment5 (Compiler::Control::opcodeForceJump, new OpSetMovementFlag (MWMechanics::CreatureStats::Flag_ForceJump)); interpreter.installSegment5 (Compiler::Control::opcodeForceJumpExplicit, new OpSetMovementFlag (MWMechanics::CreatureStats::Flag_ForceJump)); //Force MoveJump interpreter.installSegment5 (Compiler::Control::opcodeClearForceMoveJump, new OpClearMovementFlag (MWMechanics::CreatureStats::Flag_ForceMoveJump)); interpreter.installSegment5 (Compiler::Control::opcodeClearForceMoveJumpExplicit, new OpClearMovementFlag (MWMechanics::CreatureStats::Flag_ForceMoveJump)); interpreter.installSegment5 (Compiler::Control::opcodeForceMoveJump, new OpSetMovementFlag (MWMechanics::CreatureStats::Flag_ForceMoveJump)); interpreter.installSegment5 (Compiler::Control::opcodeForceMoveJumpExplicit, new OpSetMovementFlag (MWMechanics::CreatureStats::Flag_ForceMoveJump)); //Force Sneak interpreter.installSegment5 (Compiler::Control::opcodeClearForceSneak, new OpClearMovementFlag (MWMechanics::CreatureStats::Flag_ForceSneak)); interpreter.installSegment5 (Compiler::Control::opcodeClearForceSneakExplicit, new OpClearMovementFlag (MWMechanics::CreatureStats::Flag_ForceSneak)); interpreter.installSegment5 (Compiler::Control::opcodeForceSneak, new OpSetMovementFlag (MWMechanics::CreatureStats::Flag_ForceSneak)); interpreter.installSegment5 (Compiler::Control::opcodeForceSneakExplicit, new OpSetMovementFlag (MWMechanics::CreatureStats::Flag_ForceSneak)); interpreter.installSegment5 (Compiler::Control::opcodeGetPcRunning, new OpGetPcRunning); interpreter.installSegment5 (Compiler::Control::opcodeGetPcSneaking, new OpGetPcSneaking); interpreter.installSegment5 (Compiler::Control::opcodeGetForceRun, new OpGetForceRun); interpreter.installSegment5 (Compiler::Control::opcodeGetForceRunExplicit, new OpGetForceRun); interpreter.installSegment5 (Compiler::Control::opcodeGetForceJump, new OpGetForceJump); interpreter.installSegment5 (Compiler::Control::opcodeGetForceJumpExplicit, new OpGetForceJump); interpreter.installSegment5 (Compiler::Control::opcodeGetForceMoveJump, new OpGetForceMoveJump); interpreter.installSegment5 (Compiler::Control::opcodeGetForceMoveJumpExplicit, new OpGetForceMoveJump); interpreter.installSegment5 (Compiler::Control::opcodeGetForceSneak, new OpGetForceSneak); interpreter.installSegment5 (Compiler::Control::opcodeGetForceSneakExplicit, new OpGetForceSneak); } } } ================================================ FILE: apps/openmw/mwscript/controlextensions.hpp ================================================ #ifndef GAME_SCRIPT_CONTROLEXTENSIONS_H #define GAME_SCRIPT_CONTROLEXTENSIONS_H namespace Compiler { class Extensions; } namespace Interpreter { class Interpreter; } namespace MWScript { /// \brief player controls-related script functionality namespace Control { void installOpcodes (Interpreter::Interpreter& interpreter); } } #endif ================================================ FILE: apps/openmw/mwscript/dialogueextensions.cpp ================================================ #include "dialogueextensions.hpp" /* Start of tes3mp addition Include additional headers for multiplayer purposes */ #include "../mwbase/windowmanager.hpp" #include "../mwmp/Main.hpp" #include "../mwmp/LocalPlayer.hpp" /* End of tes3mp addition */ #include #include #include #include #include #include #include "../mwbase/environment.hpp" #include "../mwbase/dialoguemanager.hpp" #include "../mwbase/journal.hpp" #include "../mwbase/world.hpp" #include "../mwbase/windowmanager.hpp" #include "../mwworld/class.hpp" #include "../mwmechanics/npcstats.hpp" #include "interpretercontext.hpp" #include "ref.hpp" namespace MWScript { namespace Dialogue { template class OpJournal : public Interpreter::Opcode0 { public: void execute (Interpreter::Runtime& runtime) override { MWWorld::Ptr ptr = R()(runtime, false); // required=false if (ptr.isEmpty()) ptr = MWBase::Environment::get().getWorld()->getPlayerPtr(); std::string quest = runtime.getStringLiteral (runtime[0].mInteger); runtime.pop(); Interpreter::Type_Integer index = runtime[0].mInteger; runtime.pop(); // Invoking Journal with a non-existing index is allowed, and triggers no errors. Seriously? :( try { /* Start of tes3mp addition Send an ID_PLAYER_JOURNAL packet every time a new journal entry is added through a script */ if (mwmp::Main::get().getLocalPlayer()->isLoggedIn() && !MWBase::Environment::get().getJournal()->hasEntry(quest, index)) mwmp::Main::get().getLocalPlayer()->sendJournalEntry(quest, index, ptr); /* End of tes3mp addition */ MWBase::Environment::get().getJournal()->addEntry (quest, index, ptr); } catch (...) { if (MWBase::Environment::get().getJournal()->getJournalIndex(quest) < index) MWBase::Environment::get().getJournal()->setJournalIndex(quest, index); } } }; class OpSetJournalIndex : public Interpreter::Opcode0 { public: void execute (Interpreter::Runtime& runtime) override { std::string quest = runtime.getStringLiteral (runtime[0].mInteger); runtime.pop(); Interpreter::Type_Integer index = runtime[0].mInteger; runtime.pop(); MWBase::Environment::get().getJournal()->setJournalIndex (quest, index); /* Start of tes3mp addition Send an ID_PLAYER_JOURNAL packet every time a journal index is set through a script */ if (mwmp::Main::get().getLocalPlayer()->isLoggedIn()) mwmp::Main::get().getLocalPlayer()->sendJournalIndex(quest, index); /* End of tes3mp addition */ } }; class OpGetJournalIndex : public Interpreter::Opcode0 { public: void execute (Interpreter::Runtime& runtime) override { std::string quest = runtime.getStringLiteral (runtime[0].mInteger); runtime.pop(); int index = MWBase::Environment::get().getJournal()->getJournalIndex (quest); runtime.push (index); } }; class OpAddTopic : public Interpreter::Opcode0 { public: void execute (Interpreter::Runtime& runtime) override { std::string topic = runtime.getStringLiteral (runtime[0].mInteger); runtime.pop(); /* Start of tes3mp addition Send an ID_PLAYER_TOPIC packet every time a new topic is added through a script */ if (mwmp::Main::get().getLocalPlayer()->isLoggedIn() && MWBase::Environment::get().getDialogueManager()->isNewTopic(Misc::StringUtils::lowerCase(topic))) mwmp::Main::get().getLocalPlayer()->sendTopic(Misc::StringUtils::lowerCase(topic)); /* End of tes3mp addition */ MWBase::Environment::get().getDialogueManager()->addTopic(topic); } }; class OpChoice : public Interpreter::Opcode1 { public: void execute (Interpreter::Runtime& runtime, unsigned int arg0) override { MWBase::DialogueManager* dialogue = MWBase::Environment::get().getDialogueManager(); while(arg0>0) { std::string question = runtime.getStringLiteral (runtime[0].mInteger); runtime.pop(); arg0 = arg0 -1; Interpreter::Type_Integer choice = 1; if(arg0>0) { choice = runtime[0].mInteger; runtime.pop(); arg0 = arg0 -1; } dialogue->addChoice(question,choice); } } }; template class OpForceGreeting : public Interpreter::Opcode0 { public: void execute (Interpreter::Runtime& runtime) override { MWWorld::Ptr ptr = R()(runtime); if (!ptr.getRefData().isEnabled()) return; if (!ptr.getClass().isActor()) { const std::string error = "Warning: \"forcegreeting\" command works only for actors."; runtime.getContext().report(error); Log(Debug::Warning) << error; return; } /* Start of tes3mp change (major) Don't start a dialogue if the target is already engaged in one, thus preventing infinite greeting loops */ if (!MWBase::Environment::get().getWindowManager()->containsMode(MWGui::GM_Dialogue)) MWBase::Environment::get().getWindowManager()->pushGuiMode(MWGui::GM_Dialogue, ptr); /* End of tes3mp change (major) */ } }; class OpGoodbye : public Interpreter::Opcode0 { public: void execute(Interpreter::Runtime& runtime) override { MWBase::Environment::get().getDialogueManager()->goodbye(); } }; template class OpModReputation : public Interpreter::Opcode0 { public: void execute (Interpreter::Runtime& runtime) override { MWWorld::Ptr ptr = R()(runtime); Interpreter::Type_Integer value = runtime[0].mInteger; runtime.pop(); ptr.getClass().getNpcStats (ptr).setReputation (ptr.getClass().getNpcStats (ptr).getReputation () + value); } }; template class OpSetReputation : public Interpreter::Opcode0 { public: void execute (Interpreter::Runtime& runtime) override { MWWorld::Ptr ptr = R()(runtime); Interpreter::Type_Integer value = runtime[0].mInteger; runtime.pop(); ptr.getClass().getNpcStats (ptr).setReputation (value); } }; template class OpGetReputation : public Interpreter::Opcode0 { public: void execute (Interpreter::Runtime& runtime) override { MWWorld::Ptr ptr = R()(runtime); runtime.push (ptr.getClass().getNpcStats (ptr).getReputation ()); } }; template class OpSameFaction : public Interpreter::Opcode0 { public: void execute (Interpreter::Runtime& runtime) override { MWWorld::Ptr ptr = R()(runtime); MWWorld::Ptr player = MWBase::Environment::get().getWorld ()->getPlayerPtr(); runtime.push(player.getClass().getNpcStats (player).isInFaction(ptr.getClass().getPrimaryFaction(ptr))); } }; class OpModFactionReaction : public Interpreter::Opcode0 { public: void execute (Interpreter::Runtime& runtime) override { std::string faction1 = runtime.getStringLiteral (runtime[0].mInteger); runtime.pop(); std::string faction2 = runtime.getStringLiteral (runtime[0].mInteger); runtime.pop(); int modReaction = runtime[0].mInteger; runtime.pop(); MWBase::Environment::get().getDialogueManager()->modFactionReaction(faction1, faction2, modReaction); } }; class OpGetFactionReaction : public Interpreter::Opcode0 { public: void execute (Interpreter::Runtime& runtime) override { std::string faction1 = runtime.getStringLiteral (runtime[0].mInteger); runtime.pop(); std::string faction2 = runtime.getStringLiteral (runtime[0].mInteger); runtime.pop(); runtime.push(MWBase::Environment::get().getDialogueManager() ->getFactionReaction(faction1, faction2)); } }; class OpSetFactionReaction : public Interpreter::Opcode0 { public: void execute (Interpreter::Runtime& runtime) override { std::string faction1 = runtime.getStringLiteral (runtime[0].mInteger); runtime.pop(); std::string faction2 = runtime.getStringLiteral (runtime[0].mInteger); runtime.pop(); int newValue = runtime[0].mInteger; runtime.pop(); MWBase::Environment::get().getDialogueManager()->setFactionReaction(faction1, faction2, newValue); } }; template class OpClearInfoActor : public Interpreter::Opcode0 { public: void execute (Interpreter::Runtime& runtime) override { MWWorld::Ptr ptr = R()(runtime); MWBase::Environment::get().getDialogueManager()->clearInfoActor(ptr); } }; void installOpcodes (Interpreter::Interpreter& interpreter) { interpreter.installSegment5 (Compiler::Dialogue::opcodeJournal, new OpJournal); interpreter.installSegment5 (Compiler::Dialogue::opcodeJournalExplicit, new OpJournal); interpreter.installSegment5 (Compiler::Dialogue::opcodeSetJournalIndex, new OpSetJournalIndex); interpreter.installSegment5 (Compiler::Dialogue::opcodeGetJournalIndex, new OpGetJournalIndex); interpreter.installSegment5 (Compiler::Dialogue::opcodeAddTopic, new OpAddTopic); interpreter.installSegment3 (Compiler::Dialogue::opcodeChoice,new OpChoice); interpreter.installSegment5 (Compiler::Dialogue::opcodeForceGreeting, new OpForceGreeting); interpreter.installSegment5 (Compiler::Dialogue::opcodeForceGreetingExplicit, new OpForceGreeting); interpreter.installSegment5 (Compiler::Dialogue::opcodeGoodbye, new OpGoodbye); interpreter.installSegment5 (Compiler::Dialogue::opcodeGetReputation, new OpGetReputation); interpreter.installSegment5 (Compiler::Dialogue::opcodeSetReputation, new OpSetReputation); interpreter.installSegment5 (Compiler::Dialogue::opcodeModReputation, new OpModReputation); interpreter.installSegment5 (Compiler::Dialogue::opcodeSetReputationExplicit, new OpSetReputation); interpreter.installSegment5 (Compiler::Dialogue::opcodeModReputationExplicit, new OpModReputation); interpreter.installSegment5 (Compiler::Dialogue::opcodeGetReputationExplicit, new OpGetReputation); interpreter.installSegment5 (Compiler::Dialogue::opcodeSameFaction, new OpSameFaction); interpreter.installSegment5 (Compiler::Dialogue::opcodeSameFactionExplicit, new OpSameFaction); interpreter.installSegment5 (Compiler::Dialogue::opcodeModFactionReaction, new OpModFactionReaction); interpreter.installSegment5 (Compiler::Dialogue::opcodeSetFactionReaction, new OpSetFactionReaction); interpreter.installSegment5 (Compiler::Dialogue::opcodeGetFactionReaction, new OpGetFactionReaction); interpreter.installSegment5 (Compiler::Dialogue::opcodeClearInfoActor, new OpClearInfoActor); interpreter.installSegment5 (Compiler::Dialogue::opcodeClearInfoActorExplicit, new OpClearInfoActor); } } } ================================================ FILE: apps/openmw/mwscript/dialogueextensions.hpp ================================================ #ifndef GAME_SCRIPT_DIALOGUEEXTENSIONS_H #define GAME_SCRIPT_DIALOGUEEXTENSIONS_H namespace Compiler { class Extensions; } namespace Interpreter { class Interpreter; } namespace MWScript { /// \brief Dialogue/Journal-related script functionality namespace Dialogue { void installOpcodes (Interpreter::Interpreter& interpreter); } } #endif ================================================ FILE: apps/openmw/mwscript/docs/vmformat.txt ================================================ OpenMW Extensions: Segment 0: (not implemented yet) opcodes 0x20-0x3f unused Segment 1: (not implemented yet) opcodes 0x20-0x3f unused Segment 2: (not implemented yet) opcodes 0x200-0x3ff unused Segment 3: op 0x20000: AiTravel op 0x20001: AiTravel, explicit reference op 0x20002: AiEscort op 0x20003: AiEscort, explicit reference op 0x20004: Lock op 0x20005: Lock, explicit reference op 0x20006: PlayAnim op 0x20007: PlayAnim, explicit reference op 0x20008: LoopAnim op 0x20009: LoopAnim, explicit reference op 0x2000a: Choice op 0x2000b: PCRaiseRank op 0x2000c: PCLowerRank op 0x2000d: PCJoinFaction op 0x2000e: PCGetRank implicit op 0x2000f: PCGetRank explicit op 0x20010: AiWander op 0x20011: AiWander, explicit reference op 0x20012: GetPCFacRep op 0x20013: GetPCFacRep, explicit reference op 0x20014: SetPCFacRep op 0x20015: SetPCFacRep, explicit reference op 0x20016: ModPCFacRep op 0x20017: ModPCFacRep, explicit reference op 0x20018: PcExpelled op 0x20019: PcExpelled, explicit op 0x2001a: PcExpell op 0x2001b: PcExpell, explicit op 0x2001c: PcClearExpelled op 0x2001d: PcClearExpelled, explicit op 0x2001e: AIActivate op 0x2001f: AIActivate, explicit reference op 0x20020: AiEscortCell op 0x20021: AiEscortCell, explicit reference op 0x20022: AiFollow op 0x20023: AiFollow, explicit reference op 0x20024: AiFollowCell op 0x20025: AiFollowCell, explicit reference op 0x20026: ModRegion op 0x20027: RemoveSoulGem op 0x20028: RemoveSoulGem, explicit reference op 0x20029: PCRaiseRank, explicit reference op 0x2002a: PCLowerRank, explicit reference op 0x2002b: PCJoinFaction, explicit reference op 0x2002c: MenuTest op 0x2002d: BetaComment op 0x2002e: BetaComment, explicit reference op 0x2002f: ShowSceneGraph op 0x20030: ShowSceneGraph, explicit opcodes 0x20031-0x3ffff unused Segment 4: (not implemented yet) opcodes 0x200-0x3ff unused Segment 5: op 0x2000000: CellChanged op 0x2000001: Say op 0x2000002: SayDone op 0x2000003: StreamMusic op 0x2000004: PlaySound op 0x2000005: PlaySoundVP op 0x2000006: PlaySound3D op 0x2000007: PlaySound3DVP op 0x2000008: PlayLoopSound3D op 0x2000009: PlayLoopSound3DVP op 0x200000a: StopSound op 0x200000b: GetSoundPlaying op 0x200000c: XBox (always 0) op 0x200000d: OnActivate op 0x200000e: EnableBirthMenu op 0x200000f: EnableClassMenu op 0x2000010: EnableNameMenu op 0x2000011: EnableRaceMenu op 0x2000012: EnableStatsReviewMenu op 0x2000013: EnableInventoryMenu op 0x2000014: EnableMagicMenu op 0x2000015: EnableMapMenu op 0x2000016: EnableStatsMenu op 0x2000017: EnableRest op 0x2000018: ShowRestMenu op 0x2000019: Say, explicit reference op 0x200001a: SayDone, explicit reference op 0x200001b: PlaySound3D, explicit reference op 0x200001c: PlaySound3DVP, explicit reference op 0x200001d: PlayLoopSound3D, explicit reference op 0x200001e: PlayLoopSound3DVP, explicit reference op 0x200001f: StopSound, explicit reference op 0x2000020: GetSoundPlaying, explicit reference op 0x2000021: ToggleSky op 0x2000022: TurnMoonWhite op 0x2000023: TurnMoonRed op 0x2000024: GetMasserPhase op 0x2000025: GetSecundaPhase op 0x2000026: COC op 0x2000027-0x200002e: GetAttribute op 0x200002f-0x2000036: GetAttribute, explicit reference op 0x2000037-0x200003e: SetAttribute op 0x200003f-0x2000046: SetAttribute, explicit reference op 0x2000047-0x200004e: ModAttribute op 0x200004f-0x2000056: ModAttribute, explicit reference op 0x2000057-0x2000059: GetDynamic (health, magicka, fatigue) op 0x200005a-0x200005c: GetDynamic (health, magicka, fatigue), explicit reference op 0x200005d-0x200005f: SetDynamic (health, magicka, fatigue) op 0x2000060-0x2000062: SetDynamic (health, magicka, fatigue), explicit reference op 0x2000063-0x2000065: ModDynamic (health, magicka, fatigue) op 0x2000066-0x2000068: ModDynamic (health, magicka, fatigue), explicit reference op 0x2000069-0x200006b: ModDynamic (health, magicka, fatigue) op 0x200006c-0x200006e: ModDynamic (health, magicka, fatigue), explicit reference op 0x200006f-0x2000071: GetDynamic (health, magicka, fatigue) op 0x2000072-0x2000074: GetDynamic (health, magicka, fatigue), explicit reference op 0x2000075: Activate op 0x2000076: AddItem op 0x2000077: AddItem, explicit reference op 0x2000078: GetItemCount op 0x2000079: GetItemCount, explicit reference op 0x200007a: RemoveItem op 0x200007b: RemoveItem, explicit reference op 0x200007c: GetAiPackageDone op 0x200007d: GetAiPackageDone, explicit reference op 0x200007e-0x2000084: Enable Controls op 0x2000085-0x200008b: Disable Controls op 0x200008c: Unlock op 0x200008d: Unlock, explicit reference op 0x200008e-0x20000a8: GetSkill op 0x20000a9-0x20000c3: GetSkill, explicit reference op 0x20000c4-0x20000de: SetSkill op 0x20000df-0x20000f9: SetSkill, explicit reference op 0x20000fa-0x2000114: ModSkill op 0x2000115-0x200012f: ModSKill, explicit reference op 0x2000130: ToggleCollision op 0x2000131: GetInterior op 0x2000132: ToggleCollsionDebug op 0x2000133: Journal op 0x2000134: SetJournalIndex op 0x2000135: GetJournalIndex op 0x2000136: GetPCCell op 0x2000137: GetButtonPressed op 0x2000138: SkipAnim op 0x2000139: SkipAnim, expplicit reference op 0x200013a: AddTopic op 0x200013b: twf op 0x200013c: FadeIn op 0x200013d: FadeOut op 0x200013e: FadeTo op 0x200013f: GetCurrentWeather op 0x2000140: ChangeWeather op 0x2000141: GetWaterLevel op 0x2000142: SetWaterLevel op 0x2000143: ModWaterLevel op 0x2000144: ToggleWater, twa op 0x2000145: ToggleFogOfWar (tfow) op 0x2000146: TogglePathgrid op 0x2000147: AddSpell op 0x2000148: AddSpell, explicit reference op 0x2000149: RemoveSpell op 0x200014a: RemoveSpell, explicit reference op 0x200014b: GetSpell op 0x200014c: GetSpell, explicit reference op 0x200014d: ModDisposition op 0x200014e: ModDisposition, explicit reference op 0x200014f: ForceGreeting op 0x2000150: ForceGreeting, explicit reference op 0x2000151: ToggleFullHelp op 0x2000152: Goodbye op 0x2000153: DontSaveObject (left unimplemented) op 0x2000154: ClearForceRun op 0x2000155: ClearForceRun, explicit reference op 0x2000156: ForceRun op 0x2000157: ForceRun, explicit reference op 0x2000158: ClearForceSneak op 0x2000159: ClearForceSneak, explicit reference op 0x200015a: ForceSneak op 0x200015b: ForceSneak, explicit reference op 0x200015c: SetHello op 0x200015d: SetHello, explicit reference op 0x200015e: SetFight op 0x200015f: SetFight, explicit reference op 0x2000160: SetFlee op 0x2000161: SetFlee, explicit reference op 0x2000162: SetAlarm op 0x2000163: SetAlarm, explicit reference op 0x2000164: SetScale op 0x2000165: SetScale, explicit reference op 0x2000166: SetAngle op 0x2000167: SetAngle, explicit reference op 0x2000168: GetScale op 0x2000169: GetScale, explicit reference op 0x200016a: GetAngle op 0x200016b: GetAngle, explicit reference op 0x200016c: user1 (console only, requires --script-console switch) op 0x200016d: user2 (console only, requires --script-console switch) op 0x200016e: user3, explicit reference (console only, requires --script-console switch) op 0x200016f: user3 (implicit reference, console only, requires --script-console switch) op 0x2000170: user4, explicit reference (console only, requires --script-console switch) op 0x2000171: user4 (implicit reference, console only, requires --script-console switch) op 0x2000172: GetStartingAngle op 0x2000173: GetStartingAngle, explicit reference op 0x2000174: ToggleVanityMode op 0x2000175-0x200018B: Get controls disabled op 0x200018C: GetLevel op 0x200018D: GetLevel, explicit reference op 0x200018E: SetLevel op 0x200018F: SetLevel, explicit reference op 0x2000190: GetPos op 0x2000191: GetPosExplicit op 0x2000192: SetPos op 0x2000193: SetPosExplicit op 0x2000194: GetStartingPos op 0x2000195: GetStartingPosExplicit op 0x2000196: Position op 0x2000197: Position Explicit op 0x2000198: PositionCell op 0x2000199: PositionCell Explicit op 0x200019a: PlaceItemCell op 0x200019b: PlaceItem op 0x200019c: PlaceAtPc op 0x200019d: PlaceAtMe op 0x200019e: PlaceAtMe Explicit op 0x200019f: GetPcSleep op 0x20001a0: ShowMap op 0x20001a1: FillMap op 0x20001a2: WakeUpPc op 0x20001a3: GetDeadCount op 0x20001a4: SetDisposition op 0x20001a5: SetDisposition, Explicit op 0x20001a6: GetDisposition op 0x20001a7: GetDisposition, Explicit op 0x20001a8: CommonDisease op 0x20001a9: CommonDisease, explicit reference op 0x20001aa: BlightDisease op 0x20001ab: BlightDisease, explicit reference op 0x20001ac: ToggleCollisionBoxes op 0x20001ad: SetReputation op 0x20001ae: ModReputation op 0x20001af: SetReputation, explicit op 0x20001b0: ModReputation, explicit op 0x20001b1: GetReputation op 0x20001b2: GetReputation, explicit op 0x20001b3: Equip op 0x20001b4: Equip, explicit op 0x20001b5: SameFaction op 0x20001b6: SameFaction, explicit op 0x20001b7: ModHello op 0x20001b8: ModHello, explicit reference op 0x20001b9: ModFight op 0x20001ba: ModFight, explicit reference op 0x20001bb: ModFlee op 0x20001bc: ModFlee, explicit reference op 0x20001bd: ModAlarm op 0x20001be: ModAlarm, explicit reference op 0x20001bf: GetHello op 0x20001c0: GetHello, explicit reference op 0x20001c1: GetFight op 0x20001c2: GetFight, explicit reference op 0x20001c3: GetFlee op 0x20001c4: GetFlee, explicit reference op 0x20001c5: GetAlarm op 0x20001c6: GetAlarm, explicit reference op 0x20001c7: GetLocked op 0x20001c8: GetLocked, explicit reference op 0x20001c9: GetPcRunning op 0x20001ca: GetPcSneaking op 0x20001cb: GetForceRun op 0x20001cc: GetForceSneak op 0x20001cd: GetForceRun, explicit op 0x20001ce: GetForceSneak, explicit op 0x20001cf: GetEffect op 0x20001d0: GetEffect, explicit op 0x20001d1: GetArmorType op 0x20001d2: GetArmorType, explicit op 0x20001d3: GetAttacked op 0x20001d4: GetAttacked, explicit op 0x20001d5: HasItemEquipped op 0x20001d6: HasItemEquipped, explicit op 0x20001d7: GetWeaponDrawn op 0x20001d8: GetWeaponDrawn, explicit op 0x20001d9: GetRace op 0x20001da: GetRace, explicit op 0x20001db: GetSpellEffects op 0x20001dc: GetSpellEffects, explicit op 0x20001dd: GetCurrentTime op 0x20001de: HasSoulGem op 0x20001df: HasSoulGem, explicit op 0x20001e0: GetWeaponType op 0x20001e1: GetWeaponType, explicit op 0x20001e2: GetWerewolfKills op 0x20001e3: ModScale op 0x20001e4: ModScale, explicit op 0x20001e5: SetDelete op 0x20001e6: SetDelete, explicit op 0x20001e7: GetSquareRoot op 0x20001e8: RaiseRank op 0x20001e9: RaiseRank, explicit op 0x20001ea: LowerRank op 0x20001eb: LowerRank, explicit op 0x20001ec: GetPCCrimeLevel op 0x20001ed: SetPCCrimeLevel op 0x20001ee: ModPCCrimeLevel op 0x20001ef: GetCurrentAIPackage op 0x20001f0: GetCurrentAIPackage, explicit reference op 0x20001f1: GetDetected op 0x20001f2: GetDetected, explicit reference op 0x20001f3: AddSoulGem op 0x20001f4: AddSoulGem, explicit reference op 0x20001f5: unused op 0x20001f6: unused op 0x20001f7: PlayBink op 0x20001f8: Drop op 0x20001f9: Drop, explicit reference op 0x20001fa: DropSoulGem op 0x20001fb: DropSoulGem, explicit reference op 0x20001fc: OnDeath op 0x20001fd: IsWerewolf op 0x20001fe: IsWerewolf, explicit reference op 0x20001ff: Rotate op 0x2000200: Rotate, explicit reference op 0x2000201: RotateWorld op 0x2000202: RotateWorld, explicit reference op 0x2000203: SetAtStart op 0x2000204: SetAtStart, explicit op 0x2000205: OnDeath, explicit op 0x2000206: Move op 0x2000207: Move, explicit op 0x2000208: MoveWorld op 0x2000209: MoveWorld, explicit op 0x200020a: Fall op 0x200020b: Fall, explicit op 0x200020c: GetStandingPC op 0x200020d: GetStandingPC, explicit op 0x200020e: GetStandingActor op 0x200020f: GetStandingActor, explicit op 0x2000210: GetStartingAngle op 0x2000211: GetStartingAngle, explicit op 0x2000212: GetWindSpeed op 0x2000213: HitOnMe op 0x2000214: HitOnMe, explicit op 0x2000215: DisableTeleporting op 0x2000216: EnableTeleporting op 0x2000217: BecomeWerewolf op 0x2000218: BecomeWerewolfExplicit op 0x2000219: UndoWerewolf op 0x200021a: UndoWerewolfExplicit op 0x200021b: SetWerewolfAcrobatics op 0x200021c: SetWerewolfAcrobaticsExplicit op 0x200021d: ShowVars op 0x200021e: ShowVarsExplicit op 0x200021f: ToggleGodMode op 0x2000220: DisableLevitation op 0x2000221: EnableLevitation op 0x2000222: GetLineOfSight op 0x2000223: GetLineOfSightExplicit op 0x2000224: ToggleAI op 0x2000225: unused op 0x2000226: COE op 0x2000227: Cast op 0x2000228: Cast, explicit op 0x2000229: ExplodeSpell op 0x200022a: ExplodeSpell, explicit op 0x200022b: RemoveSpellEffects op 0x200022c: RemoveSpellEffects, explicit op 0x200022d: RemoveEffects op 0x200022e: RemoveEffects, explicit op 0x200022f: Resurrect op 0x2000230: Resurrect, explicit op 0x2000231: GetSpellReadied op 0x2000232: GetSpellReadied, explicit op 0x2000233: GetPcJumping op 0x2000234: ShowRestMenu, explicit op 0x2000235: GoToJail op 0x2000236: PayFine op 0x2000237: PayFineThief op 0x2000238: GetTarget op 0x2000239: GetTargetExplicit op 0x200023a: StartCombat op 0x200023b: StartCombatExplicit op 0x200023c: StopCombat op 0x200023d: StopCombatExplicit op 0x200023e: GetPcInJail op 0x200023f: GetPcTraveling op 0x2000240: onKnockout op 0x2000241: onKnockoutExplicit op 0x2000242: ModFactionReaction op 0x2000243: GetFactionReaction op 0x2000244: Activate, explicit op 0x2000245: ClearInfoActor op 0x2000246: ClearInfoActor, explicit op 0x2000247: (unused) op 0x2000248: (unused) op 0x2000249: OnMurder op 0x200024a: OnMurder, explicit op 0x200024b: ToggleMenus op 0x200024c: Face op 0x200024d: Face, explicit op 0x200024e: GetStat (dummy function) op 0x200024f: GetStat (dummy function), explicit op 0x2000250: GetCollidingPC op 0x2000251: GetCollidingPC, explicit op 0x2000252: GetCollidingActor op 0x2000253: GetCollidingActor, explicit op 0x2000254: HurtStandingActor op 0x2000255: HurtStandingActor, explicit op 0x2000256: HurtCollidingActor op 0x2000257: HurtCollidingActor, explicit op 0x2000258: ClearForceJump op 0x2000259: ClearForceJump, explicit reference op 0x200025a: ForceJump op 0x200025b: ForceJump, explicit reference op 0x200025c: ClearForceMoveJump op 0x200025d: ClearForceMoveJump, explicit reference op 0x200025e: ForceMoveJump op 0x200025f: ForceMoveJump, explicit reference op 0x2000260: GetForceJump op 0x2000261: GetForceJump, explicit reference op 0x2000262: GetForceMoveJump op 0x2000263: GetForceMoveJump, explicit reference op 0x2000264-0x200027b: GetMagicEffect op 0x200027c-0x2000293: GetMagicEffect, explicit op 0x2000294-0x20002ab: SetMagicEffect op 0x20002ac-0x20002c3: SetMagicEffect, explicit op 0x20002c4-0x20002db: ModMagicEffect op 0x20002dc-0x20002f3: ModMagicEffect, explicit op 0x20002f4: ResetActors op 0x20002f5: ToggleWorld op 0x20002f6: PCForce1stPerson op 0x20002f7: PCForce3rdPerson op 0x20002f8: PCGet3rdPerson op 0x20002f9: HitAttemptOnMe op 0x20002fa: HitAttemptOnMe, explicit op 0x20002fb: AddToLevCreature op 0x20002fc: RemoveFromLevCreature op 0x20002fd: AddToLevItem op 0x20002fe: RemoveFromLevItem op 0x20002ff: SetFactionReaction op 0x2000300: EnableLevelupMenu op 0x2000301: ToggleScripts op 0x2000302: Fixme op 0x2000303: Fixme, explicit op 0x2000304: Show op 0x2000305: Show, explicit op 0x2000306: OnActivate, explicit op 0x2000307: ToggleBorders, tb op 0x2000308: ToggleNavMesh op 0x2000309: ToggleActorsPaths op 0x200030a: SetNavMeshNumber op 0x200030b: Journal, explicit op 0x200030c: RepairedOnMe op 0x200030d: RepairedOnMe, explicit op 0x200030e: TestCells op 0x200030f: TestInteriorCells op 0x2000310: ToggleRecastMesh op 0x2000311: MenuMode op 0x2000312: Random op 0x2000313: ScriptRunning op 0x2000314: StartScript op 0x2000315: StopScript op 0x2000316: GetSecondsPassed op 0x2000317: Enable op 0x2000318: Disable op 0x2000319: GetDisabled op 0x200031a: Enable, explicit op 0x200031b: Disable, explicit op 0x200031c: GetDisabled, explicit op 0x200031d: StartScript, explicit op 0x200031e: GetDistance op 0x200031f: GetDistance, explicit opcodes 0x2000320-0x3ffffff unused ================================================ FILE: apps/openmw/mwscript/extensions.cpp ================================================ #include "extensions.hpp" #include #include #include "soundextensions.hpp" #include "cellextensions.hpp" #include "miscextensions.hpp" #include "guiextensions.hpp" #include "skyextensions.hpp" #include "statsextensions.hpp" #include "containerextensions.hpp" #include "aiextensions.hpp" #include "controlextensions.hpp" #include "dialogueextensions.hpp" #include "animationextensions.hpp" #include "transformationextensions.hpp" #include "consoleextensions.hpp" #include "userextensions.hpp" namespace MWScript { void installOpcodes (Interpreter::Interpreter& interpreter, bool consoleOnly) { Interpreter::installOpcodes (interpreter); Cell::installOpcodes (interpreter); Misc::installOpcodes (interpreter); Gui::installOpcodes (interpreter); Sound::installOpcodes (interpreter); Sky::installOpcodes (interpreter); Stats::installOpcodes (interpreter); Container::installOpcodes (interpreter); Ai::installOpcodes (interpreter); Control::installOpcodes (interpreter); Dialogue::installOpcodes (interpreter); Animation::installOpcodes (interpreter); Transformation::installOpcodes (interpreter); if (consoleOnly) { Console::installOpcodes (interpreter); User::installOpcodes (interpreter); } } } ================================================ FILE: apps/openmw/mwscript/extensions.hpp ================================================ #ifndef GAME_SCRIPT_EXTENSIONS_H #define GAME_SCRIPT_EXTENSIONS_H namespace Compiler { class Extensions; } namespace Interpreter { class Interpreter; } namespace MWScript { void installOpcodes (Interpreter::Interpreter& interpreter, bool consoleOnly = false); ///< \param consoleOnly include console only opcodes } #endif ================================================ FILE: apps/openmw/mwscript/globalscripts.cpp ================================================ #include "globalscripts.hpp" #include #include #include #include #include "../mwworld/class.hpp" #include "../mwworld/esmstore.hpp" #include "../mwbase/environment.hpp" #include "../mwbase/world.hpp" #include "../mwbase/scriptmanager.hpp" #include "interpretercontext.hpp" namespace { struct ScriptCreatingVisitor : public boost::static_visitor { ESM::GlobalScript operator()(const MWWorld::Ptr &ptr) const { ESM::GlobalScript script; script.mTargetRef.unset(); script.mRunning = false; if (!ptr.isEmpty()) { if (ptr.getCellRef().hasContentFile()) { script.mTargetId = ptr.getCellRef().getRefId(); script.mTargetRef = ptr.getCellRef().getRefNum(); } else if (MWBase::Environment::get().getWorld()->getPlayerPtr() == ptr) script.mTargetId = ptr.getCellRef().getRefId(); } return script; } ESM::GlobalScript operator()(const std::pair &pair) const { ESM::GlobalScript script; script.mTargetId = pair.second; script.mTargetRef = pair.first; script.mRunning = false; return script; } }; struct PtrGettingVisitor : public boost::static_visitor { const MWWorld::Ptr* operator()(const MWWorld::Ptr &ptr) const { return &ptr; } const MWWorld::Ptr* operator()(const std::pair &pair) const { return nullptr; } }; struct PtrResolvingVisitor : public boost::static_visitor { MWWorld::Ptr operator()(const MWWorld::Ptr &ptr) const { return ptr; } MWWorld::Ptr operator()(const std::pair &pair) const { if (pair.second.empty()) return MWWorld::Ptr(); else if(pair.first.hasContentFile()) return MWBase::Environment::get().getWorld()->searchPtrViaRefNum(pair.second, pair.first); return MWBase::Environment::get().getWorld()->searchPtr(pair.second, false); } }; class MatchPtrVisitor : public boost::static_visitor { const MWWorld::Ptr& mPtr; public: MatchPtrVisitor(const MWWorld::Ptr& ptr) : mPtr(ptr) {} bool operator()(const MWWorld::Ptr &ptr) const { return ptr == mPtr; } bool operator()(const std::pair &pair) const { return false; } }; } namespace MWScript { GlobalScriptDesc::GlobalScriptDesc() : mRunning (false) {} const MWWorld::Ptr* GlobalScriptDesc::getPtrIfPresent() const { return boost::apply_visitor(PtrGettingVisitor(), mTarget); } MWWorld::Ptr GlobalScriptDesc::getPtr() { MWWorld::Ptr ptr = boost::apply_visitor(PtrResolvingVisitor(), mTarget); mTarget = ptr; return ptr; } GlobalScripts::GlobalScripts (const MWWorld::ESMStore& store) : mStore (store) {} void GlobalScripts::addScript (const std::string& name, const MWWorld::Ptr& target) { const auto iter = mScripts.find (::Misc::StringUtils::lowerCase (name)); if (iter==mScripts.end()) { if (const ESM::Script *script = mStore.get().search(name)) { auto desc = std::make_shared(); MWWorld::Ptr ptr = target; desc->mTarget = ptr; desc->mRunning = true; desc->mLocals.configure (*script); mScripts.insert (std::make_pair(name, desc)); } else { Log(Debug::Error) << "Failed to add global script " << name << ": script record not found"; } } else if (!iter->second->mRunning) { iter->second->mRunning = true; MWWorld::Ptr ptr = target; iter->second->mTarget = ptr; } } void GlobalScripts::removeScript (const std::string& name) { const auto iter = mScripts.find (::Misc::StringUtils::lowerCase (name)); if (iter!=mScripts.end()) iter->second->mRunning = false; } bool GlobalScripts::isRunning (const std::string& name) const { const auto iter = mScripts.find (::Misc::StringUtils::lowerCase (name)); if (iter==mScripts.end()) return false; return iter->second->mRunning; } void GlobalScripts::run() { for (const auto& script : mScripts) { if (script.second->mRunning) { MWScript::InterpreterContext context(script.second); /* Start of tes3mp addition Mark this InterpreterContext as having a SCRIPT_GLOBAL context and as currently running the script with this name, so that packets sent by the Interpreter can have their origin determined by serverside scripts */ context.trackContextType(Interpreter::Context::SCRIPT_GLOBAL); context.trackCurrentScriptName(script.first); /* End of tes3mp addition */ if (!MWBase::Environment::get().getScriptManager()->run(script.first, context)) script.second->mRunning = false; } } } void GlobalScripts::clear() { mScripts.clear(); } void GlobalScripts::addStartup() { // make list of global scripts to be added std::vector scripts; scripts.emplace_back("main"); for (MWWorld::Store::iterator iter = mStore.get().begin(); iter != mStore.get().end(); ++iter) { scripts.push_back (iter->mId); } // add scripts for (std::vector::const_iterator iter (scripts.begin()); iter!=scripts.end(); ++iter) { try { addScript (*iter); } catch (const std::exception& exception) { Log(Debug::Error) << "Failed to add start script " << *iter << " because an exception has " << "been thrown: " << exception.what(); } } } int GlobalScripts::countSavedGameRecords() const { return mScripts.size(); } void GlobalScripts::write (ESM::ESMWriter& writer, Loading::Listener& progress) const { for (const auto& iter : mScripts) { ESM::GlobalScript script = boost::apply_visitor (ScriptCreatingVisitor(), iter.second->mTarget); script.mId = iter.first; iter.second->mLocals.write (script.mLocals, iter.first); script.mRunning = iter.second->mRunning ? 1 : 0; writer.startRecord (ESM::REC_GSCR); script.save (writer); writer.endRecord (ESM::REC_GSCR); } } bool GlobalScripts::readRecord (ESM::ESMReader& reader, uint32_t type, const std::map& contentFileMap) { if (type==ESM::REC_GSCR) { ESM::GlobalScript script; script.load (reader); if (script.mTargetRef.hasContentFile()) { auto iter = contentFileMap.find(script.mTargetRef.mContentFile); if (iter != contentFileMap.end()) script.mTargetRef.mContentFile = iter->second; } auto iter = mScripts.find (script.mId); if (iter==mScripts.end()) { if (const ESM::Script *scriptRecord = mStore.get().search (script.mId)) { try { auto desc = std::make_shared(); if (!script.mTargetId.empty()) { desc->mTarget = std::make_pair(script.mTargetRef, script.mTargetId); } desc->mLocals.configure (*scriptRecord); iter = mScripts.insert (std::make_pair (script.mId, desc)).first; } catch (const std::exception& exception) { Log(Debug::Error) << "Failed to add start script " << script.mId << " because an exception has been thrown: " << exception.what(); return true; } } else // script does not exist anymore return true; } iter->second->mRunning = script.mRunning!=0; iter->second->mLocals.read (script.mLocals, script.mId); return true; } return false; } Locals& GlobalScripts::getLocals (const std::string& name) { std::string name2 = ::Misc::StringUtils::lowerCase (name); auto iter = mScripts.find (name2); if (iter==mScripts.end()) { const ESM::Script *script = mStore.get().find (name); auto desc = std::make_shared(); desc->mLocals.configure (*script); iter = mScripts.insert (std::make_pair (name2, desc)).first; } return iter->second->mLocals; } const Locals* GlobalScripts::getLocalsIfPresent (const std::string& name) const { std::string name2 = ::Misc::StringUtils::lowerCase (name); auto iter = mScripts.find (name2); if (iter==mScripts.end()) return nullptr; return &iter->second->mLocals; } void GlobalScripts::updatePtrs(const MWWorld::Ptr& base, const MWWorld::Ptr& updated) { MatchPtrVisitor visitor(base); for (const auto& script : mScripts) { if (boost::apply_visitor (visitor, script.second->mTarget)) script.second->mTarget = updated; } } } ================================================ FILE: apps/openmw/mwscript/globalscripts.hpp ================================================ #ifndef GAME_SCRIPT_GLOBALSCRIPTS_H #define GAME_SCRIPT_GLOBALSCRIPTS_H #include #include #include #include #include #include #include "locals.hpp" #include "../mwworld/ptr.hpp" namespace ESM { class ESMWriter; class ESMReader; struct RefNum; } namespace Loading { class Listener; } namespace MWWorld { class ESMStore; } namespace MWScript { struct GlobalScriptDesc { bool mRunning; Locals mLocals; boost::variant > mTarget; // Used to start targeted script GlobalScriptDesc(); const MWWorld::Ptr* getPtrIfPresent() const; // Returns a Ptr if one has been resolved MWWorld::Ptr getPtr(); // Resolves mTarget to a Ptr and caches the (potentially empty) result }; class GlobalScripts { const MWWorld::ESMStore& mStore; std::map > mScripts; public: GlobalScripts (const MWWorld::ESMStore& store); void addScript (const std::string& name, const MWWorld::Ptr& target = MWWorld::Ptr()); void removeScript (const std::string& name); bool isRunning (const std::string& name) const; void run(); ///< run all active global scripts void clear(); void addStartup(); ///< Add startup script int countSavedGameRecords() const; void write (ESM::ESMWriter& writer, Loading::Listener& progress) const; bool readRecord (ESM::ESMReader& reader, uint32_t type, const std::map& contentFileMap); ///< Records for variables that do not exist are dropped silently. /// /// \return Known type? Locals& getLocals (const std::string& name); ///< If the script \a name has not been added as a global script yet, it is added /// automatically, but is not set to running state. const Locals* getLocalsIfPresent (const std::string& name) const; void updatePtrs(const MWWorld::Ptr& base, const MWWorld::Ptr& updated); ///< Update the Ptrs stored in mTarget. Should be called after the reference has been moved to a new cell. }; } #endif ================================================ FILE: apps/openmw/mwscript/guiextensions.cpp ================================================ #include "guiextensions.hpp" /* Start of tes3mp addition Include additional headers for multiplayer purposes */ #include "../mwmp/Main.hpp" #include "../mwmp/LocalPlayer.hpp" /* End of tes3mp addition */ #include #include #include #include #include "../mwworld/esmstore.hpp" #include "../mwbase/environment.hpp" #include "../mwbase/windowmanager.hpp" #include "../mwbase/world.hpp" #include "../mwbase/mechanicsmanager.hpp" #include "../mwmechanics/actorutil.hpp" #include "interpretercontext.hpp" #include "ref.hpp" namespace MWScript { namespace Gui { class OpEnableWindow : public Interpreter::Opcode0 { MWGui::GuiWindow mWindow; public: OpEnableWindow (MWGui::GuiWindow window) : mWindow (window) {} void execute (Interpreter::Runtime& runtime) override { MWBase::Environment::get().getWindowManager()->allow (mWindow); } }; class OpEnableRest : public Interpreter::Opcode0 { public: void execute (Interpreter::Runtime& runtime) override { MWBase::Environment::get().getWindowManager()->enableRest(); } }; template class OpShowRestMenu : public Interpreter::Opcode0 { public: void execute (Interpreter::Runtime& runtime) override { MWWorld::Ptr bed = R()(runtime, false); if (bed.isEmpty() || !MWBase::Environment::get().getMechanicsManager()->sleepInBed(MWMechanics::getPlayer(), bed)) /* Start of tes3mp change (minor) Prevent resting if it has been disabled by the server for the local player */ { if (!mwmp::Main::get().getLocalPlayer()->bedRestAllowed) MWBase::Environment::get().getWindowManager()->messageBox("You are not allowed to rest in beds."); else { mwmp::Main::get().getLocalPlayer()->isUsingBed = true; MWBase::Environment::get().getWindowManager()->pushGuiMode(MWGui::GM_Rest, bed); } } /* End of tes3mp change (minor) */ } }; class OpShowDialogue : public Interpreter::Opcode0 { MWGui::GuiMode mDialogue; public: OpShowDialogue (MWGui::GuiMode dialogue) : mDialogue (dialogue) {} void execute (Interpreter::Runtime& runtime) override { MWBase::Environment::get().getWindowManager()->pushGuiMode(mDialogue); } }; class OpGetButtonPressed : public Interpreter::Opcode0 { public: void execute (Interpreter::Runtime& runtime) override { runtime.push (MWBase::Environment::get().getWindowManager()->readPressedButton()); } }; class OpToggleFogOfWar : public Interpreter::Opcode0 { public: void execute (Interpreter::Runtime& runtime) override { runtime.getContext().report(MWBase::Environment::get().getWindowManager()->toggleFogOfWar() ? "Fog of war -> On" : "Fog of war -> Off"); } }; class OpToggleFullHelp : public Interpreter::Opcode0 { public: void execute (Interpreter::Runtime& runtime) override { runtime.getContext().report(MWBase::Environment::get().getWindowManager()->toggleFullHelp() ? "Full help -> On" : "Full help -> Off"); } }; class OpShowMap : public Interpreter::Opcode0 { public: void execute (Interpreter::Runtime& runtime) override { std::string cell = (runtime.getStringLiteral (runtime[0].mInteger)); ::Misc::StringUtils::lowerCaseInPlace(cell); runtime.pop(); // "Will match complete or partial cells, so ShowMap, "Vivec" will show cells Vivec and Vivec, Fred's House as well." // http://www.uesp.net/wiki/Tes3Mod:ShowMap const MWWorld::Store &cells = MWBase::Environment::get().getWorld()->getStore().get(); MWWorld::Store::iterator it = cells.extBegin(); for (; it != cells.extEnd(); ++it) { std::string name = it->mName; ::Misc::StringUtils::lowerCaseInPlace(name); if (name.find(cell) != std::string::npos) MWBase::Environment::get().getWindowManager()->addVisitedLocation ( it->mName, it->getGridX(), it->getGridY() ); } } }; class OpFillMap : public Interpreter::Opcode0 { public: void execute (Interpreter::Runtime& runtime) override { const MWWorld::Store &cells = MWBase::Environment::get().getWorld ()->getStore().get(); MWWorld::Store::iterator it = cells.extBegin(); for (; it != cells.extEnd(); ++it) { std::string name = it->mName; if (name != "") MWBase::Environment::get().getWindowManager()->addVisitedLocation ( name, it->getGridX(), it->getGridY() ); } } }; class OpMenuTest : public Interpreter::Opcode1 { public: void execute (Interpreter::Runtime& runtime, unsigned int arg0) override { int arg=0; if(arg0>0) { arg = runtime[0].mInteger; runtime.pop(); } if (arg == 0) { MWGui::GuiMode modes[] = { MWGui::GM_Inventory, MWGui::GM_Container }; for (int i=0; i<2; ++i) { if (MWBase::Environment::get().getWindowManager()->containsMode(modes[i])) MWBase::Environment::get().getWindowManager()->removeGuiMode(modes[i]); } } else { MWGui::GuiWindow gw = MWGui::GW_None; if (arg == 3) gw = MWGui::GW_Stats; if (arg == 4) gw = MWGui::GW_Inventory; if (arg == 5) gw = MWGui::GW_Magic; if (arg == 6) gw = MWGui::GW_Map; MWBase::Environment::get().getWindowManager()->pinWindow(gw); } } }; class OpToggleMenus : public Interpreter::Opcode0 { public: void execute(Interpreter::Runtime &runtime) override { bool state = MWBase::Environment::get().getWindowManager()->toggleHud(); runtime.getContext().report(state ? "GUI -> On" : "GUI -> Off"); if (!state) { while (MWBase::Environment::get().getWindowManager()->getMode() != MWGui::GM_None) // don't use isGuiMode, or we get an infinite loop for modal message boxes! MWBase::Environment::get().getWindowManager()->popGuiMode(); } } }; void installOpcodes (Interpreter::Interpreter& interpreter) { interpreter.installSegment5 (Compiler::Gui::opcodeEnableBirthMenu, new OpShowDialogue (MWGui::GM_Birth)); interpreter.installSegment5 (Compiler::Gui::opcodeEnableClassMenu, new OpShowDialogue (MWGui::GM_Class)); interpreter.installSegment5 (Compiler::Gui::opcodeEnableNameMenu, new OpShowDialogue (MWGui::GM_Name)); interpreter.installSegment5 (Compiler::Gui::opcodeEnableRaceMenu, new OpShowDialogue (MWGui::GM_Race)); interpreter.installSegment5 (Compiler::Gui::opcodeEnableStatsReviewMenu, new OpShowDialogue (MWGui::GM_Review)); interpreter.installSegment5 (Compiler::Gui::opcodeEnableLevelupMenu, new OpShowDialogue (MWGui::GM_Levelup)); interpreter.installSegment5 (Compiler::Gui::opcodeEnableInventoryMenu, new OpEnableWindow (MWGui::GW_Inventory)); interpreter.installSegment5 (Compiler::Gui::opcodeEnableMagicMenu, new OpEnableWindow (MWGui::GW_Magic)); interpreter.installSegment5 (Compiler::Gui::opcodeEnableMapMenu, new OpEnableWindow (MWGui::GW_Map)); interpreter.installSegment5 (Compiler::Gui::opcodeEnableStatsMenu, new OpEnableWindow (MWGui::GW_Stats)); interpreter.installSegment5 (Compiler::Gui::opcodeEnableRest, new OpEnableRest ()); interpreter.installSegment5 (Compiler::Gui::opcodeShowRestMenu, new OpShowRestMenu); interpreter.installSegment5 (Compiler::Gui::opcodeShowRestMenuExplicit, new OpShowRestMenu); interpreter.installSegment5 (Compiler::Gui::opcodeGetButtonPressed, new OpGetButtonPressed); interpreter.installSegment5 (Compiler::Gui::opcodeToggleFogOfWar, new OpToggleFogOfWar); interpreter.installSegment5 (Compiler::Gui::opcodeToggleFullHelp, new OpToggleFullHelp); interpreter.installSegment5 (Compiler::Gui::opcodeShowMap, new OpShowMap); interpreter.installSegment5 (Compiler::Gui::opcodeFillMap, new OpFillMap); interpreter.installSegment3 (Compiler::Gui::opcodeMenuTest, new OpMenuTest); interpreter.installSegment5 (Compiler::Gui::opcodeToggleMenus, new OpToggleMenus); } } } ================================================ FILE: apps/openmw/mwscript/guiextensions.hpp ================================================ #ifndef GAME_SCRIPT_GUIEXTENSIONS_H #define GAME_SCRIPT_GUIEXTENSIONS_H namespace Compiler { class Extensions; } namespace Interpreter { class Interpreter; } namespace MWScript { /// \brief GUI-related script functionality namespace Gui { void installOpcodes (Interpreter::Interpreter& interpreter); } } #endif ================================================ FILE: apps/openmw/mwscript/interpretercontext.cpp ================================================ #include "interpretercontext.hpp" #include #include #include /* Start of tes3mp addition Include additional headers for multiplayer purposes */ #include #include "../mwmp/Main.hpp" #include "../mwmp/Networking.hpp" #include "../mwmp/LocalPlayer.hpp" #include "../mwmp/ObjectList.hpp" #include "../mwmp/ScriptController.hpp" /* End of tes3mp addition */ #include "../mwworld/esmstore.hpp" #include "../mwbase/environment.hpp" #include "../mwbase/world.hpp" #include "../mwbase/scriptmanager.hpp" #include "../mwbase/windowmanager.hpp" #include "../mwbase/inputmanager.hpp" #include "../mwworld/action.hpp" #include "../mwworld/class.hpp" #include "../mwworld/cellstore.hpp" #include "../mwworld/containerstore.hpp" #include "../mwmechanics/npcstats.hpp" #include "locals.hpp" #include "globalscripts.hpp" namespace MWScript { /* Start of tes3mp addition Used for tracking and checking the type of this InterpreterContext, as well as its current script */ unsigned short InterpreterContext::getContextType() const { return mContextType; } std::string InterpreterContext::getCurrentScriptName() const { return mCurrentScriptName; } void InterpreterContext::trackContextType(unsigned short contextType) { mContextType = contextType; } void InterpreterContext::trackCurrentScriptName(const std::string& name) { mCurrentScriptName = name; } /* End of tes3mp addition */ const MWWorld::Ptr InterpreterContext::getReferenceImp ( const std::string& id, bool activeOnly, bool doThrow) const { if (!id.empty()) { return MWBase::Environment::get().getWorld()->getPtr (id, activeOnly); } else { if (mReference.isEmpty() && mGlobalScriptDesc) mReference = mGlobalScriptDesc->getPtr(); if (mReference.isEmpty() && doThrow) throw MissingImplicitRefError(); return mReference; } } const Locals& InterpreterContext::getMemberLocals (std::string& id, bool global) const { if (global) { return MWBase::Environment::get().getScriptManager()->getGlobalScripts(). getLocals (id); } else { const MWWorld::Ptr ptr = getReferenceImp (id, false); id = ptr.getClass().getScript (ptr); ptr.getRefData().setLocals ( *MWBase::Environment::get().getWorld()->getStore().get().find (id)); return ptr.getRefData().getLocals(); } } Locals& InterpreterContext::getMemberLocals (std::string& id, bool global) { if (global) { return MWBase::Environment::get().getScriptManager()->getGlobalScripts(). getLocals (id); } else { const MWWorld::Ptr ptr = getReferenceImp (id, false); id = ptr.getClass().getScript (ptr); ptr.getRefData().setLocals ( *MWBase::Environment::get().getWorld()->getStore().get().find (id)); return ptr.getRefData().getLocals(); } } MissingImplicitRefError::MissingImplicitRefError() : std::runtime_error("no implicit reference") {} int InterpreterContext::findLocalVariableIndex (const std::string& scriptId, const std::string& name, char type) const { int index = MWBase::Environment::get().getScriptManager()->getLocals (scriptId). searchIndex (type, name); if (index!=-1) return index; std::ostringstream stream; stream << "Failed to access "; switch (type) { case 's': stream << "short"; break; case 'l': stream << "long"; break; case 'f': stream << "float"; break; } stream << " member variable " << name << " in script " << scriptId; throw std::runtime_error (stream.str().c_str()); } InterpreterContext::InterpreterContext (MWScript::Locals *locals, const MWWorld::Ptr& reference) : mLocals (locals), mReference (reference) {} InterpreterContext::InterpreterContext (std::shared_ptr globalScriptDesc) : mLocals (&(globalScriptDesc->mLocals)) { const MWWorld::Ptr* ptr = globalScriptDesc->getPtrIfPresent(); // A nullptr here signifies that the script's target has not yet been resolved after loading the game. // Script targets are lazily resolved to MWWorld::Ptrs (which can, upon resolution, be empty) // because scripts started through dialogue often don't use their implicit target. if (ptr) mReference = *ptr; else mGlobalScriptDesc = globalScriptDesc; } int InterpreterContext::getLocalShort (int index) const { if (!mLocals) throw std::runtime_error ("local variables not available in this context"); return mLocals->mShorts.at (index); } int InterpreterContext::getLocalLong (int index) const { if (!mLocals) throw std::runtime_error ("local variables not available in this context"); return mLocals->mLongs.at (index); } float InterpreterContext::getLocalFloat (int index) const { if (!mLocals) throw std::runtime_error ("local variables not available in this context"); return mLocals->mFloats.at (index); } void InterpreterContext::setLocalShort (int index, int value) { if (!mLocals) throw std::runtime_error ("local variables not available in this context"); /* Start of tes3mp addition Avoid setting a local to a value it already is, preventing packet spam */ if (mLocals->mShorts.at(index) == value) return; /* End of tes3mp addition */ mLocals->mShorts.at (index) = value; /* Start of tes3mp addition Send an ID_CLIENT_SCRIPT_LOCAL packet when a local short changes its value if it is being set in a script that has been approved for packet sending */ if (sendPackets) { mwmp::ObjectList *objectList = mwmp::Main::get().getNetworking()->getObjectList(); objectList->reset(); objectList->packetOrigin = ScriptController::getPacketOriginFromContextType(getContextType()); objectList->originClientScript = getCurrentScriptName(); objectList->addClientScriptLocal(mReference, index, value, mwmp::VARIABLE_TYPE::SHORT); objectList->sendClientScriptLocal(); } /* End of tes3mp addition */ } void InterpreterContext::setLocalLong (int index, int value) { if (!mLocals) throw std::runtime_error ("local variables not available in this context"); /* Start of tes3mp addition Avoid setting a local to a value it already is, preventing packet spam */ if (mLocals->mLongs.at(index) == value) return; /* End of tes3mp addition */ mLocals->mLongs.at (index) = value; /* Start of tes3mp addition Send an ID_CLIENT_SCRIPT_LOCAL packet when a local long changes its value if it is being set in a script that has been approved for packet sending */ if (sendPackets) { mwmp::ObjectList *objectList = mwmp::Main::get().getNetworking()->getObjectList(); objectList->reset(); objectList->packetOrigin = ScriptController::getPacketOriginFromContextType(getContextType()); objectList->originClientScript = getCurrentScriptName(); objectList->addClientScriptLocal(mReference, index, value, mwmp::VARIABLE_TYPE::LONG); objectList->sendClientScriptLocal(); } /* End of tes3mp addition */ } void InterpreterContext::setLocalFloat (int index, float value) { if (!mLocals) throw std::runtime_error ("local variables not available in this context"); /* Start of tes3mp addition Avoid setting a local to a value it already is, preventing packet spam Additionally, record the old value to check it below when determining if it has changed enough to warrant sending a packet about it */ float oldValue = mLocals->mFloats.at(index); if (oldValue == value) return; /* End of tes3mp addition */ mLocals->mFloats.at (index) = value; /* Start of tes3mp addition Send an ID_CLIENT_SCRIPT_LOCAL packet when a local float changes its value if its value has changed enough and it is being set in a script that has been approved for packet sending */ if (floor(oldValue) != floor(value) && sendPackets) { mwmp::ObjectList *objectList = mwmp::Main::get().getNetworking()->getObjectList(); objectList->reset(); objectList->packetOrigin = ScriptController::getPacketOriginFromContextType(getContextType()); objectList->originClientScript = getCurrentScriptName(); objectList->addClientScriptLocal(mReference, index, value); objectList->sendClientScriptLocal(); } /* End of tes3mp addition */ } void InterpreterContext::messageBox (const std::string& message, const std::vector& buttons) { if (buttons.empty()) MWBase::Environment::get().getWindowManager()->messageBox (message); else MWBase::Environment::get().getWindowManager()->interactiveMessageBox(message, buttons); } void InterpreterContext::report (const std::string& message) { } int InterpreterContext::getGlobalShort (const std::string& name) const { return MWBase::Environment::get().getWorld()->getGlobalInt (name); } int InterpreterContext::getGlobalLong (const std::string& name) const { // a global long is internally a float. return MWBase::Environment::get().getWorld()->getGlobalInt (name); } float InterpreterContext::getGlobalFloat (const std::string& name) const { return MWBase::Environment::get().getWorld()->getGlobalFloat (name); } void InterpreterContext::setGlobalShort (const std::string& name, int value) { /* Start of tes3mp addition Avoid setting a global to a value it already is, preventing packet spam */ if (getGlobalShort(name) == value) return; /* End of tes3mp addition */ /* Start of tes3mp addition Send an ID_CLIENT_SCRIPT_GLOBAL packet when a global short changes its value if it is being set in a script that has been approved for packet sending or the global itself has been set to always be synchronized */ if (sendPackets || mwmp::Main::isValidPacketGlobal(name)) { mwmp::Main::get().getNetworking()->getWorldstate()->sendClientGlobal(name, value, mwmp::VARIABLE_TYPE::SHORT); } /* End of tes3mp addition */ MWBase::Environment::get().getWorld()->setGlobalInt (name, value); } void InterpreterContext::setGlobalLong (const std::string& name, int value) { /* Start of tes3mp addition Avoid setting a global to a value it already is, preventing packet spam */ if (getGlobalLong(name) == value) return; /* End of tes3mp addition */ /* Start of tes3mp addition Send an ID_CLIENT_SCRIPT_GLOBAL packet when a global long changes its value if it is being set in a script that has been approved for packet sending or the global itself has been set to always be synchronized */ if (sendPackets || mwmp::Main::isValidPacketGlobal(name)) { mwmp::Main::get().getNetworking()->getWorldstate()->sendClientGlobal(name, value, mwmp::VARIABLE_TYPE::LONG); } /* End of tes3mp addition */ MWBase::Environment::get().getWorld()->setGlobalInt (name, value); } void InterpreterContext::setGlobalFloat (const std::string& name, float value) { /* Start of tes3mp addition Avoid setting a global to a value it already is, preventing packet spam Additionally, record the old value to check it below when determining if it has changed enough to warrant sending a packet about it */ float oldValue = getGlobalFloat(name); if (oldValue == value) return; /* End of tes3mp addition */ /* Start of tes3mp addition Send an ID_CLIENT_SCRIPT_GLOBAL packet when a global float changes its value if its value has changed enough and it is being set in a script that has been approved for packet sending or the global itself has been set to always be synchronized */ if (floor(oldValue) != floor(value) && (sendPackets || mwmp::Main::isValidPacketGlobal(name))) { mwmp::Main::get().getNetworking()->getWorldstate()->sendClientGlobal(name, value); } /* End of tes3mp addition */ MWBase::Environment::get().getWorld()->setGlobalFloat (name, value); } std::vector InterpreterContext::getGlobals() const { const MWWorld::Store& globals = MWBase::Environment::get().getWorld()->getStore().get(); std::vector ids; for (auto& globalVariable : globals) { ids.emplace_back(globalVariable.mId); } return ids; } char InterpreterContext::getGlobalType (const std::string& name) const { MWBase::World *world = MWBase::Environment::get().getWorld(); return world->getGlobalVariableType(name); } std::string InterpreterContext::getActionBinding(const std::string& targetAction) const { MWBase::InputManager* input = MWBase::Environment::get().getInputManager(); std::vector actions = input->getActionKeySorting (); for (const int action : actions) { std::string desc = input->getActionDescription (action); if(desc == "") continue; if(desc == targetAction) { if(input->joystickLastUsed()) return input->getActionControllerBindingName(action); else return input->getActionKeyBindingName(action); } } return "None"; } std::string InterpreterContext::getActorName() const { const MWWorld::Ptr& ptr = getReferenceImp(); if (ptr.getClass().isNpc()) { const ESM::NPC* npc = ptr.get()->mBase; return npc->mName; } const ESM::Creature* creature = ptr.get()->mBase; return creature->mName; } std::string InterpreterContext::getNPCRace() const { ESM::NPC npc = *getReferenceImp().get()->mBase; const ESM::Race* race = MWBase::Environment::get().getWorld()->getStore().get().find(npc.mRace); return race->mName; } std::string InterpreterContext::getNPCClass() const { ESM::NPC npc = *getReferenceImp().get()->mBase; const ESM::Class* class_ = MWBase::Environment::get().getWorld()->getStore().get().find(npc.mClass); return class_->mName; } std::string InterpreterContext::getNPCFaction() const { ESM::NPC npc = *getReferenceImp().get()->mBase; const ESM::Faction* faction = MWBase::Environment::get().getWorld()->getStore().get().find(npc.mFaction); return faction->mName; } std::string InterpreterContext::getNPCRank() const { const MWWorld::Ptr& ptr = getReferenceImp(); std::string faction = ptr.getClass().getPrimaryFaction(ptr); if (faction.empty()) throw std::runtime_error("getNPCRank(): NPC is not in a faction"); int rank = ptr.getClass().getPrimaryFactionRank(ptr); if (rank < 0 || rank > 9) throw std::runtime_error("getNPCRank(): invalid rank"); MWBase::World *world = MWBase::Environment::get().getWorld(); const MWWorld::ESMStore &store = world->getStore(); const ESM::Faction *fact = store.get().find(faction); return fact->mRanks[rank]; } std::string InterpreterContext::getPCName() const { MWBase::World *world = MWBase::Environment::get().getWorld(); ESM::NPC player = *world->getPlayerPtr().get()->mBase; return player.mName; } std::string InterpreterContext::getPCRace() const { MWBase::World *world = MWBase::Environment::get().getWorld(); std::string race = world->getPlayerPtr().get()->mBase->mRace; return world->getStore().get().find(race)->mName; } std::string InterpreterContext::getPCClass() const { MWBase::World *world = MWBase::Environment::get().getWorld(); std::string class_ = world->getPlayerPtr().get()->mBase->mClass; return world->getStore().get().find(class_)->mName; } std::string InterpreterContext::getPCRank() const { MWBase::World *world = MWBase::Environment::get().getWorld(); MWWorld::Ptr player = world->getPlayerPtr(); std::string factionId = getReferenceImp().getClass().getPrimaryFaction(getReferenceImp()); if (factionId.empty()) throw std::runtime_error("getPCRank(): NPC is not in a faction"); const std::map& ranks = player.getClass().getNpcStats (player).getFactionRanks(); std::map::const_iterator it = ranks.find(Misc::StringUtils::lowerCase(factionId)); int rank = -1; if (it != ranks.end()) rank = it->second; // If you are not in the faction, PcRank returns the first rank, for whatever reason. // This is used by the dialogue when joining the Thieves Guild in Balmora. if (rank == -1) rank = 0; const MWWorld::ESMStore &store = world->getStore(); const ESM::Faction *faction = store.get().find(factionId); if(rank < 0 || rank > 9) // there are only 10 ranks return ""; return faction->mRanks[rank]; } std::string InterpreterContext::getPCNextRank() const { MWBase::World *world = MWBase::Environment::get().getWorld(); MWWorld::Ptr player = world->getPlayerPtr(); std::string factionId = getReferenceImp().getClass().getPrimaryFaction(getReferenceImp()); if (factionId.empty()) throw std::runtime_error("getPCNextRank(): NPC is not in a faction"); const std::map& ranks = player.getClass().getNpcStats (player).getFactionRanks(); std::map::const_iterator it = ranks.find(Misc::StringUtils::lowerCase(factionId)); int rank = -1; if (it != ranks.end()) rank = it->second; ++rank; // Next rank // if we are already at max rank, there is no next rank if (rank > 9) rank = 9; const MWWorld::ESMStore &store = world->getStore(); const ESM::Faction *faction = store.get().find(factionId); if(rank < 0) return ""; return faction->mRanks[rank]; } int InterpreterContext::getPCBounty() const { MWBase::World *world = MWBase::Environment::get().getWorld(); MWWorld::Ptr player = world->getPlayerPtr(); return player.getClass().getNpcStats (player).getBounty(); } std::string InterpreterContext::getCurrentCellName() const { return MWBase::Environment::get().getWorld()->getCellName(); } void InterpreterContext::executeActivation(MWWorld::Ptr ptr, MWWorld::Ptr actor) { std::shared_ptr action = (ptr.getClass().activate(ptr, actor)); action->execute (actor); if (action->getTarget() != MWWorld::Ptr() && action->getTarget() != ptr) { updatePtr(ptr, action->getTarget()); } } int InterpreterContext::getMemberShort (const std::string& id, const std::string& name, bool global) const { std::string scriptId (id); const Locals& locals = getMemberLocals (scriptId, global); return locals.mShorts[findLocalVariableIndex (scriptId, name, 's')]; } int InterpreterContext::getMemberLong (const std::string& id, const std::string& name, bool global) const { std::string scriptId (id); const Locals& locals = getMemberLocals (scriptId, global); return locals.mLongs[findLocalVariableIndex (scriptId, name, 'l')]; } float InterpreterContext::getMemberFloat (const std::string& id, const std::string& name, bool global) const { std::string scriptId (id); const Locals& locals = getMemberLocals (scriptId, global); return locals.mFloats[findLocalVariableIndex (scriptId, name, 'f')]; } void InterpreterContext::setMemberShort (const std::string& id, const std::string& name, int value, bool global) { std::string scriptId (id); Locals& locals = getMemberLocals (scriptId, global); /* Start of tes3mp change (minor) Declare an integer so it can be reused below for multiplayer script sync purposes */ int index = findLocalVariableIndex(scriptId, name, 's'); locals.mShorts[index] = value; /* End of tes3mp change (minor) */ /* Start of tes3mp addition Send an ID_SCRIPT_MEMBER_SHORT packet every time a member short changes its value in a script approved for packet sending */ if (sendPackets && !global) { mwmp::ObjectList *objectList = mwmp::Main::get().getNetworking()->getObjectList(); objectList->reset(); objectList->packetOrigin = ScriptController::getPacketOriginFromContextType(getContextType()); objectList->originClientScript = getCurrentScriptName(); objectList->addScriptMemberShort(id, index, value); objectList->sendScriptMemberShort(); } /* End of tes3mp addition */ } void InterpreterContext::setMemberLong (const std::string& id, const std::string& name, int value, bool global) { std::string scriptId (id); Locals& locals = getMemberLocals (scriptId, global); locals.mLongs[findLocalVariableIndex (scriptId, name, 'l')] = value; } void InterpreterContext::setMemberFloat (const std::string& id, const std::string& name, float value, bool global) { std::string scriptId (id); Locals& locals = getMemberLocals (scriptId, global); locals.mFloats[findLocalVariableIndex (scriptId, name, 'f')] = value; } MWWorld::Ptr InterpreterContext::getReference(bool required) { return getReferenceImp ("", true, required); } void InterpreterContext::updatePtr(const MWWorld::Ptr& base, const MWWorld::Ptr& updated) { if (!mReference.isEmpty() && base == mReference) { mReference = updated; if (mLocals == &base.getRefData().getLocals()) mLocals = &mReference.getRefData().getLocals(); } } } ================================================ FILE: apps/openmw/mwscript/interpretercontext.hpp ================================================ #ifndef GAME_SCRIPT_INTERPRETERCONTEXT_H #define GAME_SCRIPT_INTERPRETERCONTEXT_H #include #include #include #include "globalscripts.hpp" #include "../mwworld/ptr.hpp" namespace MWScript { class Locals; class MissingImplicitRefError : public std::runtime_error { public: MissingImplicitRefError(); }; class InterpreterContext : public Interpreter::Context { Locals *mLocals; mutable MWWorld::Ptr mReference; std::shared_ptr mGlobalScriptDesc; /// If \a id is empty, a reference the script is run from is returned or in case /// of a non-local script the reference derived from the target ID. const MWWorld::Ptr getReferenceImp (const std::string& id = "", bool activeOnly = false, bool doThrow=true) const; const Locals& getMemberLocals (std::string& id, bool global) const; ///< \a id is changed to the respective script ID, if \a id wasn't a script ID before Locals& getMemberLocals (std::string& id, bool global); ///< \a id is changed to the respective script ID, if \a id wasn't a script ID before /// Throws an exception if local variable can't be found. int findLocalVariableIndex (const std::string& scriptId, const std::string& name, char type) const; public: InterpreterContext (std::shared_ptr globalScriptDesc); InterpreterContext (MWScript::Locals *locals, const MWWorld::Ptr& reference); ///< The ownership of \a locals is not transferred. 0-pointer allowed. /* Start of tes3mp addition Useful boolean for setting whether scripts send packets, set to false by default to avoid massive packet spam */ bool sendPackets = false; /* End of tes3mp addition */ /* Start of tes3mp addition Used for tracking and checking the type of this InterpreterContext, as well as its current script */ unsigned short mContextType; std::string mCurrentScriptName = ""; virtual unsigned short getContextType() const; virtual std::string getCurrentScriptName() const; virtual void trackContextType(unsigned short contextType); virtual void trackCurrentScriptName(const std::string& name); /* End of tes3mp addition */ int getLocalShort (int index) const override; int getLocalLong (int index) const override; float getLocalFloat (int index) const override; void setLocalShort (int index, int value) override; void setLocalLong (int index, int value) override; void setLocalFloat (int index, float value) override; using Interpreter::Context::messageBox; void messageBox (const std::string& message, const std::vector& buttons) override; void report (const std::string& message) override; ///< By default, do nothing. int getGlobalShort (const std::string& name) const override; int getGlobalLong (const std::string& name) const override; float getGlobalFloat (const std::string& name) const override; void setGlobalShort (const std::string& name, int value) override; void setGlobalLong (const std::string& name, int value) override; void setGlobalFloat (const std::string& name, float value) override; std::vector getGlobals () const override; char getGlobalType (const std::string& name) const override; std::string getActionBinding(const std::string& action) const override; std::string getActorName() const override; std::string getNPCRace() const override; std::string getNPCClass() const override; std::string getNPCFaction() const override; std::string getNPCRank() const override; std::string getPCName() const override; std::string getPCRace() const override; std::string getPCClass() const override; std::string getPCRank() const override; std::string getPCNextRank() const override; int getPCBounty() const override; std::string getCurrentCellName() const override; void executeActivation(MWWorld::Ptr ptr, MWWorld::Ptr actor); ///< Execute the activation action for this ptr. If ptr is mActivated, mark activation as handled. int getMemberShort (const std::string& id, const std::string& name, bool global) const override; int getMemberLong (const std::string& id, const std::string& name, bool global) const override; float getMemberFloat (const std::string& id, const std::string& name, bool global) const override; void setMemberShort (const std::string& id, const std::string& name, int value, bool global) override; void setMemberLong (const std::string& id, const std::string& name, int value, bool global) override; void setMemberFloat (const std::string& id, const std::string& name, float value, bool global) override; MWWorld::Ptr getReference(bool required=true); ///< Reference, that the script is running from (can be empty) void updatePtr(const MWWorld::Ptr& base, const MWWorld::Ptr& updated); ///< Update the Ptr stored in mReference, if there is one stored there. Should be called after the reference has been moved to a new cell. }; } #endif ================================================ FILE: apps/openmw/mwscript/locals.cpp ================================================ #include "locals.hpp" #include "globalscripts.hpp" #include #include #include #include #include #include #include "../mwbase/environment.hpp" #include "../mwbase/scriptmanager.hpp" #include "../mwbase/world.hpp" #include "../mwworld/esmstore.hpp" namespace MWScript { void Locals::ensure (const std::string& scriptName) { if (!mInitialised) { const ESM::Script *script = MWBase::Environment::get().getWorld()->getStore(). get().find (scriptName); configure (*script); } } Locals::Locals() : mInitialised (false) {} bool Locals::configure (const ESM::Script& script) { if (mInitialised) return false; const Locals* global = MWBase::Environment::get().getScriptManager()->getGlobalScripts().getLocalsIfPresent(script.mId); if(global) { mShorts = global->mShorts; mLongs = global->mLongs; mFloats = global->mFloats; } else { const Compiler::Locals& locals = MWBase::Environment::get().getScriptManager()->getLocals (script.mId); mShorts.clear(); mShorts.resize (locals.get ('s').size(), 0); mLongs.clear(); mLongs.resize (locals.get ('l').size(), 0); mFloats.clear(); mFloats.resize (locals.get ('f').size(), 0); } mInitialised = true; return true; } bool Locals::isEmpty() const { return (mShorts.empty() && mLongs.empty() && mFloats.empty()); } bool Locals::hasVar(const std::string &script, const std::string &var) { try { ensure (script); const Compiler::Locals& locals = MWBase::Environment::get().getScriptManager()->getLocals(script); int index = locals.getIndex(var); return (index != -1); } catch (const Compiler::SourceException&) { return false; } } int Locals::getIntVar(const std::string &script, const std::string &var) { ensure (script); const Compiler::Locals& locals = MWBase::Environment::get().getScriptManager()->getLocals(script); int index = locals.getIndex(var); char type = locals.getType(var); if(index != -1) { switch(type) { case 's': return mShorts.at (index); case 'l': return mLongs.at (index); case 'f': return static_cast(mFloats.at(index)); default: return 0; } } return 0; } float Locals::getFloatVar(const std::string &script, const std::string &var) { ensure (script); const Compiler::Locals& locals = MWBase::Environment::get().getScriptManager()->getLocals(script); int index = locals.getIndex(var); char type = locals.getType(var); if(index != -1) { switch(type) { case 's': return mShorts.at (index); case 'l': return mLongs.at (index); case 'f': return mFloats.at(index); default: return 0; } } return 0; } bool Locals::setVarByInt(const std::string& script, const std::string& var, int val) { ensure (script); const Compiler::Locals& locals = MWBase::Environment::get().getScriptManager()->getLocals(script); int index = locals.getIndex(var); char type = locals.getType(var); if(index != -1) { switch(type) { case 's': mShorts.at (index) = val; break; case 'l': mLongs.at (index) = val; break; case 'f': mFloats.at(index) = static_cast(val); break; } return true; } return false; } bool Locals::write (ESM::Locals& locals, const std::string& script) const { if (!mInitialised) return false; try { const Compiler::Locals& declarations = MWBase::Environment::get().getScriptManager()->getLocals(script); for (int i=0; i<3; ++i) { char type = 0; switch (i) { case 0: type = 's'; break; case 1: type = 'l'; break; case 2: type = 'f'; break; } const std::vector& names = declarations.get (type); for (int i2=0; i2 (names.size()); ++i2) { ESM::Variant value; switch (i) { case 0: value.setType (ESM::VT_Int); value.setInteger (mShorts.at (i2)); break; case 1: value.setType (ESM::VT_Int); value.setInteger (mLongs.at (i2)); break; case 2: value.setType (ESM::VT_Float); value.setFloat (mFloats.at (i2)); break; } locals.mVariables.emplace_back (names[i2], value); } } } catch (const Compiler::SourceException&) { } return true; } void Locals::read (const ESM::Locals& locals, const std::string& script) { ensure (script); try { const Compiler::Locals& declarations = MWBase::Environment::get().getScriptManager()->getLocals(script); int index = 0, numshorts = 0, numlongs = 0; for (unsigned int v=0; v >::const_iterator iter = locals.mVariables.begin(); iter!=locals.mVariables.end(); ++iter,++index) { if (iter->first.empty()) { // no variable names available (this will happen for legacy, i.e. ESS-imported savegames only) try { if (index >= numshorts+numlongs) mFloats.at(index - (numshorts+numlongs)) = iter->second.getFloat(); else if (index >= numshorts) mLongs.at(index - numshorts) = iter->second.getInteger(); else mShorts.at(index) = iter->second.getInteger(); } catch (std::exception& e) { Log(Debug::Error) << "Failed to read local variable state for script '" << script << "' (legacy format): " << e.what() << "\nNum shorts: " << numshorts << " / " << mShorts.size() << " Num longs: " << numlongs << " / " << mLongs.size(); } } else { char type = declarations.getType (iter->first); int index2 = declarations.getIndex (iter->first); // silently ignore locals that don't exist anymore if (type == ' ' || index2 == -1) continue; try { switch (type) { case 's': mShorts.at (index2) = iter->second.getInteger(); break; case 'l': mLongs.at (index2) = iter->second.getInteger(); break; case 'f': mFloats.at (index2) = iter->second.getFloat(); break; } } catch (...) { // ignore type changes /// \todo write to log } } } } catch (const Compiler::SourceException&) { } } } ================================================ FILE: apps/openmw/mwscript/locals.hpp ================================================ #ifndef GAME_SCRIPT_LOCALS_H #define GAME_SCRIPT_LOCALS_H #include #include namespace ESM { class Script; struct Locals; } namespace MWScript { class Locals { bool mInitialised; void ensure (const std::string& scriptName); public: std::vector mShorts; std::vector mLongs; std::vector mFloats; Locals(); /// Are there any locals? /// /// \note Will return false, if locals have not been configured yet. bool isEmpty() const; /// \return Did the state of *this change from uninitialised to initialised? bool configure (const ESM::Script& script); /// @note var needs to be in lowercase /// /// \note Locals will be automatically configured first, if necessary bool setVarByInt(const std::string& script, const std::string& var, int val); /// \note Locals will be automatically configured first, if necessary // // \note If it can not be determined if the variable exists, the error will be // ignored and false will be returned. bool hasVar(const std::string& script, const std::string& var); /// if var does not exist, returns 0 /// @note var needs to be in lowercase /// /// \note Locals will be automatically configured first, if necessary int getIntVar (const std::string& script, const std::string& var); /// if var does not exist, returns 0 /// @note var needs to be in lowercase /// /// \note Locals will be automatically configured first, if necessary float getFloatVar (const std::string& script, const std::string& var); /// \note If locals have not been configured yet, no data is written. /// /// \return Locals written? bool write (ESM::Locals& locals, const std::string& script) const; /// \note Locals will be automatically configured first, if necessary void read (const ESM::Locals& locals, const std::string& script); }; } #endif ================================================ FILE: apps/openmw/mwscript/miscextensions.cpp ================================================ #include "miscextensions.hpp" #include #include /* Start of tes3mp addition Include additional headers for multiplayer purposes */ #include "../mwmp/Main.hpp" #include "../mwmp/Networking.hpp" #include "../mwmp/LocalPlayer.hpp" #include "../mwmp/ObjectList.hpp" #include "../mwmp/ScriptController.hpp" /* End of tes3mp addition */ #include #include #include #include #include #include #include #include #include #include #include #include #include "../mwbase/environment.hpp" #include "../mwbase/windowmanager.hpp" #include "../mwbase/scriptmanager.hpp" #include "../mwbase/soundmanager.hpp" #include "../mwbase/world.hpp" #include "../mwworld/class.hpp" #include "../mwworld/player.hpp" #include "../mwworld/containerstore.hpp" #include "../mwworld/inventorystore.hpp" #include "../mwworld/esmstore.hpp" #include "../mwworld/cellstore.hpp" #include "../mwworld/manualref.hpp" #include "../mwmechanics/aicast.hpp" #include "../mwmechanics/npcstats.hpp" #include "../mwmechanics/creaturestats.hpp" #include "../mwmechanics/spellcasting.hpp" #include "../mwmechanics/actorutil.hpp" #include "interpretercontext.hpp" #include "ref.hpp" namespace { void addToLevList(ESM::LevelledListBase* list, const std::string& itemId, int level) { for (auto& levelItem : list->mList) { if (levelItem.mLevel == level && itemId == levelItem.mId) return; } ESM::LevelledListBase::LevelItem item; item.mId = itemId; item.mLevel = level; list->mList.push_back(item); } void removeFromLevList(ESM::LevelledListBase* list, const std::string& itemId, int level) { // level of -1 removes all items with that itemId for (std::vector::iterator it = list->mList.begin(); it != list->mList.end();) { if (level != -1 && it->mLevel != level) { ++it; continue; } if (Misc::StringUtils::ciEqual(itemId, it->mId)) it = list->mList.erase(it); else ++it; } } } namespace MWScript { namespace Misc { class OpMenuMode : public Interpreter::Opcode0 { public: void execute (Interpreter::Runtime& runtime) override { /* Start of tes3mp change (major) Being in a menu should not pause scripts in multiplayer, so always return false */ //runtime.push (MWBase::Environment::get().getWindowManager()->isGuiMode()); runtime.push(false); /* End of tes3mp change (major) */ } }; class OpRandom : public Interpreter::Opcode0 { public: void execute (Interpreter::Runtime& runtime) override { Interpreter::Type_Integer limit = runtime[0].mInteger; runtime.pop(); if (limit<0) throw std::runtime_error ( "random: argument out of range (Don't be so negative!)"); runtime.push (static_cast(::Misc::Rng::rollDice(limit))); // [o, limit) } }; template class OpStartScript : public Interpreter::Opcode0 { public: void execute (Interpreter::Runtime& runtime) override { MWWorld::Ptr target = R()(runtime, false); std::string name = runtime.getStringLiteral (runtime[0].mInteger); runtime.pop(); MWBase::Environment::get().getScriptManager()->getGlobalScripts().addScript (name, target); } }; class OpScriptRunning : public Interpreter::Opcode0 { public: void execute (Interpreter::Runtime& runtime) override { std::string name = runtime.getStringLiteral (runtime[0].mInteger); runtime.pop(); runtime.push(MWBase::Environment::get().getScriptManager()->getGlobalScripts().isRunning (name)); } }; class OpStopScript : public Interpreter::Opcode0 { public: void execute (Interpreter::Runtime& runtime) override { std::string name = runtime.getStringLiteral (runtime[0].mInteger); runtime.pop(); MWBase::Environment::get().getScriptManager()->getGlobalScripts().removeScript (name); } }; class OpGetSecondsPassed : public Interpreter::Opcode0 { public: void execute (Interpreter::Runtime& runtime) override { runtime.push (MWBase::Environment::get().getFrameDuration()); } }; template class OpEnable : public Interpreter::Opcode0 { public: void execute (Interpreter::Runtime& runtime) override { MWWorld::Ptr ptr = R()(runtime); /* Start of tes3mp addition Send an ID_OBJECT_STATE packet whenever an object should be enabled, as long as the player is logged in on the server and if triggered from a clientside script our last packet regarding its state did not already attempt to enable it (to prevent packet spam) */ if (mwmp::Main::get().getLocalPlayer()->isLoggedIn() && ptr.isInCell()) { unsigned char packetOrigin = ScriptController::getPacketOriginFromContextType(runtime.getContext().getContextType()); if (packetOrigin == mwmp::CLIENT_CONSOLE || packetOrigin == mwmp::CLIENT_DIALOGUE || ptr.getRefData().getLastCommunicatedState() != MWWorld::RefData::StateCommunication::Enabled) { ptr.getRefData().setLastCommunicatedState(MWWorld::RefData::StateCommunication::Enabled); mwmp::ObjectList* objectList = mwmp::Main::get().getNetworking()->getObjectList(); objectList->reset(); objectList->packetOrigin = packetOrigin; objectList->originClientScript = runtime.getContext().getCurrentScriptName(); objectList->addObjectState(ptr, true); objectList->sendObjectState(); } } /* End of tes3mp addition */ /* Start of tes3mp change (major) Disable unilateral state enabling on this client and expect the server's reply to our packet to do it instead */ //MWBase::Environment::get().getWorld()->enable (ptr); /* End of tes3mp change (major) */ } }; template class OpDisable : public Interpreter::Opcode0 { public: void execute (Interpreter::Runtime& runtime) override { MWWorld::Ptr ptr = R()(runtime); /* Start of tes3mp addition Send an ID_OBJECT_STATE packet whenever an object should be disabled, as long as the player is logged in on the server and if triggered from a clientside script our last packet regarding its state did not already attempt to disable it (to prevent packet spam) */ if (mwmp::Main::get().getLocalPlayer()->isLoggedIn() && ptr.isInCell()) { unsigned char packetOrigin = ScriptController::getPacketOriginFromContextType(runtime.getContext().getContextType()); if (packetOrigin == mwmp::CLIENT_CONSOLE || packetOrigin == mwmp::CLIENT_DIALOGUE || ptr.getRefData().getLastCommunicatedState() != MWWorld::RefData::StateCommunication::Disabled) { ptr.getRefData().setLastCommunicatedState(MWWorld::RefData::StateCommunication::Disabled); mwmp::ObjectList *objectList = mwmp::Main::get().getNetworking()->getObjectList(); objectList->reset(); objectList->packetOrigin = packetOrigin; objectList->originClientScript = runtime.getContext().getCurrentScriptName(); objectList->addObjectState(ptr, false); objectList->sendObjectState(); } } /* End of tes3mp addition */ /* Start of tes3mp change (major) Disable unilateral state disabling on this client and expect the server's reply to our packet to do it instead */ //MWBase::Environment::get().getWorld()->disable (ptr); /* End of tes3mp change (major) */ } }; template class OpGetDisabled : public Interpreter::Opcode0 { public: void execute (Interpreter::Runtime& runtime) override { MWWorld::Ptr ptr = R()(runtime); runtime.push (!ptr.getRefData().isEnabled()); } }; class OpPlayBink : public Interpreter::Opcode0 { public: void execute (Interpreter::Runtime& runtime) override { std::string name = runtime.getStringLiteral (runtime[0].mInteger); runtime.pop(); bool allowSkipping = runtime[0].mInteger != 0; runtime.pop(); /* Start of tes3mp addition Send an ID_VIDEO_PLAY packet every time a video is played through a script */ if (mwmp::Main::get().getLocalPlayer()->isLoggedIn()) { mwmp::ObjectList *objectList = mwmp::Main::get().getNetworking()->getObjectList(); objectList->reset(); objectList->packetOrigin = ScriptController::getPacketOriginFromContextType(runtime.getContext().getContextType()); objectList->originClientScript = runtime.getContext().getCurrentScriptName(); objectList->addVideoPlay(name, allowSkipping); objectList->sendVideoPlay(); } /* End of tes3mp addition */ MWBase::Environment::get().getWindowManager()->playVideo (name, allowSkipping); } }; class OpGetPcSleep : public Interpreter::Opcode0 { public: void execute (Interpreter::Runtime& runtime) override { runtime.push (MWBase::Environment::get().getWindowManager ()->getPlayerSleeping()); } }; class OpGetPcJumping : public Interpreter::Opcode0 { public: void execute (Interpreter::Runtime& runtime) override { MWBase::World* world = MWBase::Environment::get().getWorld(); runtime.push (world->getPlayer().getJumping()); } }; class OpWakeUpPc : public Interpreter::Opcode0 { public: void execute (Interpreter::Runtime& runtime) override { MWBase::Environment::get().getWindowManager ()->wakeUpPlayer(); } }; class OpXBox : public Interpreter::Opcode0 { public: void execute (Interpreter::Runtime& runtime) override { runtime.push (0); } }; template class OpOnActivate : public Interpreter::Opcode0 { public: void execute (Interpreter::Runtime& runtime) override { MWWorld::Ptr ptr = R()(runtime); runtime.push (ptr.getRefData().onActivate()); } }; template class OpActivate : public Interpreter::Opcode0 { public: void execute (Interpreter::Runtime& runtime) override { InterpreterContext& context = static_cast (runtime.getContext()); MWWorld::Ptr ptr = R()(runtime); if (ptr.getRefData().activateByScript() || ptr.getContainerStore()) context.executeActivation(ptr, MWMechanics::getPlayer()); } }; template class OpLock : public Interpreter::Opcode1 { public: void execute (Interpreter::Runtime& runtime, unsigned int arg0) override { MWWorld::Ptr ptr = R()(runtime); Interpreter::Type_Integer lockLevel = ptr.getCellRef().getLockLevel(); if(lockLevel==0) { //no lock level was ever set, set to 100 as default lockLevel = 100; } if (arg0==1) { lockLevel = runtime[0].mInteger; runtime.pop(); } /* Start of tes3mp addition Send an ID_OBJECT_LOCK packet every time an object is locked through a script, as long as the lock level being set is not the one it already has */ if (mwmp::Main::get().getLocalPlayer()->isLoggedIn() && ptr.getCellRef().getLockLevel() != lockLevel) { mwmp::ObjectList *objectList = mwmp::Main::get().getNetworking()->getObjectList(); objectList->reset(); objectList->packetOrigin = ScriptController::getPacketOriginFromContextType(runtime.getContext().getContextType()); objectList->originClientScript = runtime.getContext().getCurrentScriptName(); objectList->addObjectLock(ptr, lockLevel); objectList->sendObjectLock(); } /* End of tes3mp addition */ /* Start of tes3mp change (major) Disable unilateral locking on this client and expect the server's reply to our packet to do it instead */ //ptr.getCellRef().lock (lockLevel); /* End of tes3mp change (major) */ // Instantly reset door to closed state // This is done when using Lock in scripts, but not when using Lock spells. if (ptr.getTypeName() == typeid(ESM::Door).name() && !ptr.getCellRef().getTeleport()) { MWBase::Environment::get().getWorld()->activateDoor(ptr, MWWorld::DoorState::Idle); } } }; template class OpUnlock : public Interpreter::Opcode0 { public: void execute (Interpreter::Runtime& runtime) override { MWWorld::Ptr ptr = R()(runtime); /* Start of tes3mp addition Send an ID_OBJECT_LOCK packet every time an object is unlocked through a script, as long as it's not already unlocked */ if (mwmp::Main::get().getLocalPlayer()->isLoggedIn() && ptr.getCellRef().getLockLevel() > 0) { mwmp::ObjectList *objectList = mwmp::Main::get().getNetworking()->getObjectList(); objectList->reset(); objectList->packetOrigin = ScriptController::getPacketOriginFromContextType(runtime.getContext().getContextType()); objectList->originClientScript = runtime.getContext().getCurrentScriptName(); objectList->addObjectLock(ptr, 0); objectList->sendObjectLock(); } /* End of tes3mp addition */ /* Start of tes3mp change (major) Disable unilateral unlocking on this client and expect the server's reply to our packet to do it instead */ //ptr.getCellRef().unlock (); /* End of tes3mp change (major) */ } }; class OpToggleCollisionDebug : public Interpreter::Opcode0 { public: void execute (Interpreter::Runtime& runtime) override { bool enabled = MWBase::Environment::get().getWorld()->toggleRenderMode (MWRender::Render_CollisionDebug); runtime.getContext().report (enabled ? "Collision Mesh Rendering -> On" : "Collision Mesh Rendering -> Off"); } }; class OpToggleCollisionBoxes : public Interpreter::Opcode0 { public: void execute (Interpreter::Runtime& runtime) override { bool enabled = MWBase::Environment::get().getWorld()->toggleRenderMode (MWRender::Render_CollisionDebug); runtime.getContext().report (enabled ? "Collision Mesh Rendering -> On" : "Collision Mesh Rendering -> Off"); } }; class OpToggleWireframe : public Interpreter::Opcode0 { public: void execute (Interpreter::Runtime& runtime) override { bool enabled = MWBase::Environment::get().getWorld()->toggleRenderMode (MWRender::Render_Wireframe); runtime.getContext().report (enabled ? "Wireframe Rendering -> On" : "Wireframe Rendering -> Off"); } }; class OpToggleBorders : public Interpreter::Opcode0 { public: void execute (Interpreter::Runtime& runtime) override { bool enabled = MWBase::Environment::get().getWorld()->toggleBorders(); runtime.getContext().report (enabled ? "Border Rendering -> On" : "Border Rendering -> Off"); } }; class OpTogglePathgrid : public Interpreter::Opcode0 { public: void execute (Interpreter::Runtime& runtime) override { bool enabled = MWBase::Environment::get().getWorld()->toggleRenderMode (MWRender::Render_Pathgrid); runtime.getContext().report (enabled ? "Path Grid rendering -> On" : "Path Grid Rendering -> Off"); } }; class OpFadeIn : public Interpreter::Opcode0 { public: void execute (Interpreter::Runtime& runtime) override { Interpreter::Type_Float time = runtime[0].mFloat; runtime.pop(); MWBase::Environment::get().getWindowManager()->fadeScreenIn(time, false); } }; class OpFadeOut : public Interpreter::Opcode0 { public: void execute (Interpreter::Runtime& runtime) override { Interpreter::Type_Float time = runtime[0].mFloat; runtime.pop(); MWBase::Environment::get().getWindowManager()->fadeScreenOut(time, false); } }; class OpFadeTo : public Interpreter::Opcode0 { public: void execute (Interpreter::Runtime& runtime) override { Interpreter::Type_Float alpha = runtime[0].mFloat; runtime.pop(); Interpreter::Type_Float time = runtime[0].mFloat; runtime.pop(); MWBase::Environment::get().getWindowManager()->fadeScreenTo(static_cast(alpha), time, false); } }; class OpToggleWater : public Interpreter::Opcode0 { public: void execute (Interpreter::Runtime& runtime) override { runtime.getContext().report(MWBase::Environment::get().getWorld()->toggleWater() ? "Water -> On" : "Water -> Off"); } }; class OpToggleWorld : public Interpreter::Opcode0 { public: void execute (Interpreter::Runtime& runtime) override { runtime.getContext().report(MWBase::Environment::get().getWorld()->toggleWorld() ? "World -> On" : "World -> Off"); } }; class OpDontSaveObject : public Interpreter::Opcode0 { public: void execute (Interpreter::Runtime& runtime) override { // We are ignoring the DontSaveObject statement for now. Probably not worth // bothering with. The incompatibility we are creating should be marginal at most. } }; class OpPcForce1stPerson : public Interpreter::Opcode0 { public: void execute (Interpreter::Runtime& runtime) override { if (!MWBase::Environment::get().getWorld()->isFirstPerson()) MWBase::Environment::get().getWorld()->togglePOV(true); } }; class OpPcForce3rdPerson : public Interpreter::Opcode0 { void execute (Interpreter::Runtime& runtime) override { if (MWBase::Environment::get().getWorld()->isFirstPerson()) MWBase::Environment::get().getWorld()->togglePOV(true); } }; class OpPcGet3rdPerson : public Interpreter::Opcode0 { public: void execute(Interpreter::Runtime& runtime) override { runtime.push(!MWBase::Environment::get().getWorld()->isFirstPerson()); } }; class OpToggleVanityMode : public Interpreter::Opcode0 { static bool sActivate; public: void execute(Interpreter::Runtime &runtime) override { MWBase::World *world = MWBase::Environment::get().getWorld(); if (world->toggleVanityMode(sActivate)) { runtime.getContext().report(sActivate ? "Vanity Mode -> On" : "Vanity Mode -> Off"); sActivate = !sActivate; } else { runtime.getContext().report("Vanity Mode -> No"); } } }; bool OpToggleVanityMode::sActivate = true; template class OpGetLocked : public Interpreter::Opcode0 { public: void execute (Interpreter::Runtime& runtime) override { MWWorld::Ptr ptr = R()(runtime); runtime.push (ptr.getCellRef().getLockLevel() > 0); } }; template class OpGetEffect : public Interpreter::Opcode0 { public: void execute (Interpreter::Runtime& runtime) override { MWWorld::Ptr ptr = R()(runtime); std::string effect = runtime.getStringLiteral(runtime[0].mInteger); runtime.pop(); if (!ptr.getClass().isActor()) { runtime.push(0); return; } char *end; long key = strtol(effect.c_str(), &end, 10); if(key < 0 || key > 32767 || *end != '\0') key = ESM::MagicEffect::effectStringToId(effect); const MWMechanics::CreatureStats& stats = ptr.getClass().getCreatureStats(ptr); MWMechanics::MagicEffects effects = stats.getSpells().getMagicEffects(); effects += stats.getActiveSpells().getMagicEffects(); if (ptr.getClass().hasInventoryStore(ptr) && !stats.isDeathAnimationFinished()) { MWWorld::InventoryStore& store = ptr.getClass().getInventoryStore(ptr); effects += store.getMagicEffects(); } for (const auto& activeEffect : effects) { if (activeEffect.first.mId == key && activeEffect.second.getModifier() > 0) { runtime.push(1); return; } } runtime.push(0); } }; template class OpAddSoulGem : public Interpreter::Opcode0 { public: void execute (Interpreter::Runtime& runtime) override { MWWorld::Ptr ptr = R()(runtime); std::string creature = runtime.getStringLiteral (runtime[0].mInteger); runtime.pop(); std::string gem = runtime.getStringLiteral (runtime[0].mInteger); runtime.pop(); if (!ptr.getClass().hasInventoryStore(ptr)) return; const MWWorld::ESMStore& store = MWBase::Environment::get().getWorld()->getStore(); store.get().find(creature); // This line throws an exception if it can't find the creature MWWorld::Ptr item = *ptr.getClass().getContainerStore(ptr).add(gem, 1, ptr); // Set the soul on just one of the gems, not the whole stack item.getContainerStore()->unstack(item, ptr); item.getCellRef().setSoul(creature); // Restack the gem with other gems with the same soul item.getContainerStore()->restack(item); } }; template class OpRemoveSoulGem : public Interpreter::Opcode1 { public: void execute (Interpreter::Runtime& runtime, unsigned int arg0) override { MWWorld::Ptr ptr = R()(runtime); std::string soul = runtime.getStringLiteral (runtime[0].mInteger); runtime.pop(); // throw away additional arguments for (unsigned int i=0; igetCellRef().getSoul(), soul)) { store.remove(*it, 1, ptr); return; } } } }; template class OpDrop : public Interpreter::Opcode0 { public: void execute (Interpreter::Runtime& runtime) override { MWWorld::Ptr ptr = R()(runtime); std::string item = runtime.getStringLiteral (runtime[0].mInteger); runtime.pop(); Interpreter::Type_Integer amount = runtime[0].mInteger; runtime.pop(); if (amount<0) throw std::runtime_error ("amount must be non-negative"); // no-op if (amount == 0) return; if (!ptr.getClass().isActor()) return; if (ptr.getClass().hasInventoryStore(ptr)) { // Prefer dropping unequipped items first; re-stack if possible by unequipping items before dropping them. MWWorld::InventoryStore& store = ptr.getClass().getInventoryStore(ptr); int numNotEquipped = store.count(item); for (int slot = 0; slot < MWWorld::InventoryStore::Slots; ++slot) { MWWorld::ConstContainerStoreIterator it = store.getSlot (slot); if (it != store.end() && ::Misc::StringUtils::ciEqual(it->getCellRef().getRefId(), item)) { numNotEquipped -= it->getRefData().getCount(); } } for (int slot = 0; slot < MWWorld::InventoryStore::Slots && amount > numNotEquipped; ++slot) { MWWorld::ContainerStoreIterator it = store.getSlot (slot); if (it != store.end() && ::Misc::StringUtils::ciEqual(it->getCellRef().getRefId(), item)) { int numToRemove = std::min(amount - numNotEquipped, it->getRefData().getCount()); store.unequipItemQuantity(*it, ptr, numToRemove); numNotEquipped += numToRemove; } } for (MWWorld::ContainerStoreIterator iter (store.begin()); iter!=store.end(); ++iter) { if (::Misc::StringUtils::ciEqual(iter->getCellRef().getRefId(), item) && !store.isEquipped(*iter)) { int removed = store.remove(*iter, amount, ptr); MWWorld::Ptr dropped = MWBase::Environment::get().getWorld()->dropObjectOnGround(ptr, *iter, removed); dropped.getCellRef().setOwner(""); amount -= removed; if (amount <= 0) break; } } } MWWorld::ManualRef ref(MWBase::Environment::get().getWorld()->getStore(), item, 1); MWWorld::Ptr itemPtr(ref.getPtr()); if (amount > 0) { if (itemPtr.getClass().getScript(itemPtr).empty()) { MWBase::Environment::get().getWorld()->dropObjectOnGround(ptr, itemPtr, amount); } else { // Dropping one item per time to prevent making stacks of scripted items for (int i = 0; i < amount; i++) MWBase::Environment::get().getWorld()->dropObjectOnGround(ptr, itemPtr, 1); } } MWBase::Environment::get().getSoundManager()->playSound3D(ptr, itemPtr.getClass().getDownSoundId(itemPtr), 1.f, 1.f); } }; template class OpDropSoulGem : public Interpreter::Opcode0 { public: void execute (Interpreter::Runtime& runtime) override { MWWorld::Ptr ptr = R()(runtime); std::string soul = runtime.getStringLiteral (runtime[0].mInteger); runtime.pop(); if (!ptr.getClass().hasInventoryStore(ptr)) return; MWWorld::InventoryStore& store = ptr.getClass().getInventoryStore(ptr); for (MWWorld::ContainerStoreIterator iter (store.begin()); iter!=store.end(); ++iter) { if (::Misc::StringUtils::ciEqual(iter->getCellRef().getSoul(), soul)) { MWBase::Environment::get().getWorld()->dropObjectOnGround(ptr, *iter, 1); store.remove(*iter, 1, ptr); break; } } } }; template class OpGetAttacked : public Interpreter::Opcode0 { public: void execute (Interpreter::Runtime& runtime) override { MWWorld::Ptr ptr = R()(runtime); runtime.push(ptr.getClass().getCreatureStats (ptr).getAttacked ()); } }; template class OpGetWeaponDrawn : public Interpreter::Opcode0 { public: void execute (Interpreter::Runtime& runtime) override { MWWorld::Ptr ptr = R()(runtime); runtime.push((ptr.getClass().hasInventoryStore(ptr) || ptr.getClass().isBipedal(ptr)) && ptr.getClass().getCreatureStats (ptr).getDrawState () == MWMechanics::DrawState_Weapon); } }; template class OpGetSpellReadied : public Interpreter::Opcode0 { public: void execute (Interpreter::Runtime& runtime) override { MWWorld::Ptr ptr = R()(runtime); runtime.push(ptr.getClass().getCreatureStats (ptr).getDrawState () == MWMechanics::DrawState_Spell); } }; template class OpGetSpellEffects : public Interpreter::Opcode0 { public: void execute (Interpreter::Runtime& runtime) override { MWWorld::Ptr ptr = R()(runtime); std::string id = runtime.getStringLiteral(runtime[0].mInteger); runtime.pop(); if (!ptr.getClass().isActor()) { runtime.push(0); return; } const MWMechanics::CreatureStats& stats = ptr.getClass().getCreatureStats(ptr); runtime.push(stats.getActiveSpells().isSpellActive(id) || stats.getSpells().isSpellActive(id)); } }; class OpGetCurrentTime : public Interpreter::Opcode0 { public: void execute (Interpreter::Runtime& runtime) override { runtime.push(MWBase::Environment::get().getWorld()->getTimeStamp().getHour()); } }; template class OpSetDelete : public Interpreter::Opcode0 { public: void execute (Interpreter::Runtime& runtime) override { MWWorld::Ptr ptr = R()(runtime); int parameter = runtime[0].mInteger; runtime.pop(); if (parameter == 1) { /* Start of tes3mp addition Send an ID_OBJECT_DELETE packet every time an object is deleted through a script, as long as we haven't already communicated a deletion for it */ if (mwmp::Main::get().getLocalPlayer()->isLoggedIn() && ptr.getRefData().getLastCommunicatedState() != MWWorld::RefData::StateCommunication::Deleted) { ptr.getRefData().setLastCommunicatedState(MWWorld::RefData::StateCommunication::Deleted); mwmp::ObjectList *objectList = mwmp::Main::get().getNetworking()->getObjectList(); objectList->reset(); objectList->packetOrigin = ScriptController::getPacketOriginFromContextType(runtime.getContext().getContextType()); objectList->originClientScript = runtime.getContext().getCurrentScriptName(); objectList->addObjectGeneric(ptr); objectList->sendObjectDelete(); } /* End of tes3mp addition */ /* Start of tes3mp change (major) Disable unilateral deletion on this client and expect the server's reply to our packet to do it instead */ //MWBase::Environment::get().getWorld()->deleteObject(ptr); /* End of tes3mp change (major) */ } else if (parameter == 0) MWBase::Environment::get().getWorld()->undeleteObject(ptr); else throw std::runtime_error("SetDelete: unexpected parameter"); } }; class OpGetSquareRoot : public Interpreter::Opcode0 { public: void execute (Interpreter::Runtime& runtime) override { float param = runtime[0].mFloat; runtime.pop(); runtime.push(std::sqrt (param)); } }; template class OpFall : public Interpreter::Opcode0 { public: void execute (Interpreter::Runtime& runtime) override { } }; template class OpGetStandingPc : public Interpreter::Opcode0 { public: void execute (Interpreter::Runtime& runtime) override { MWWorld::Ptr ptr = R()(runtime); runtime.push (MWBase::Environment::get().getWorld()->getPlayerStandingOn(ptr)); } }; template class OpGetStandingActor : public Interpreter::Opcode0 { public: void execute (Interpreter::Runtime& runtime) override { MWWorld::Ptr ptr = R()(runtime); runtime.push (MWBase::Environment::get().getWorld()->getActorStandingOn(ptr)); } }; template class OpGetCollidingPc : public Interpreter::Opcode0 { public: void execute (Interpreter::Runtime& runtime) override { MWWorld::Ptr ptr = R()(runtime); runtime.push (MWBase::Environment::get().getWorld()->getPlayerCollidingWith(ptr)); } }; template class OpGetCollidingActor : public Interpreter::Opcode0 { public: void execute (Interpreter::Runtime& runtime) override { MWWorld::Ptr ptr = R()(runtime); runtime.push (MWBase::Environment::get().getWorld()->getActorCollidingWith(ptr)); } }; template class OpHurtStandingActor : public Interpreter::Opcode0 { public: void execute (Interpreter::Runtime& runtime) override { MWWorld::Ptr ptr = R()(runtime); float healthDiffPerSecond = runtime[0].mFloat; runtime.pop(); MWBase::Environment::get().getWorld()->hurtStandingActors(ptr, healthDiffPerSecond); } }; template class OpHurtCollidingActor : public Interpreter::Opcode0 { public: void execute (Interpreter::Runtime& runtime) override { MWWorld::Ptr ptr = R()(runtime); float healthDiffPerSecond = runtime[0].mFloat; runtime.pop(); MWBase::Environment::get().getWorld()->hurtCollidingActors(ptr, healthDiffPerSecond); } }; class OpGetWindSpeed : public Interpreter::Opcode0 { public: void execute (Interpreter::Runtime& runtime) override { runtime.push(MWBase::Environment::get().getWorld()->getWindSpeed()); } }; template class OpHitOnMe : public Interpreter::Opcode0 { public: void execute (Interpreter::Runtime& runtime) override { MWWorld::Ptr ptr = R()(runtime); std::string objectID = runtime.getStringLiteral (runtime[0].mInteger); runtime.pop(); MWMechanics::CreatureStats &stats = ptr.getClass().getCreatureStats(ptr); runtime.push(::Misc::StringUtils::ciEqual(objectID, stats.getLastHitObject())); stats.setLastHitObject(std::string()); } }; template class OpHitAttemptOnMe : public Interpreter::Opcode0 { public: void execute (Interpreter::Runtime& runtime) override { MWWorld::Ptr ptr = R()(runtime); std::string objectID = runtime.getStringLiteral (runtime[0].mInteger); runtime.pop(); MWMechanics::CreatureStats &stats = ptr.getClass().getCreatureStats(ptr); runtime.push(::Misc::StringUtils::ciEqual(objectID, stats.getLastHitAttemptObject())); stats.setLastHitAttemptObject(std::string()); } }; template class OpEnableTeleporting : public Interpreter::Opcode0 { public: void execute (Interpreter::Runtime& runtime) override { MWBase::World *world = MWBase::Environment::get().getWorld(); world->enableTeleporting(Enable); } }; template class OpEnableLevitation : public Interpreter::Opcode0 { public: void execute (Interpreter::Runtime& runtime) override { MWBase::World *world = MWBase::Environment::get().getWorld(); world->enableLevitation(Enable); } }; template class OpShow : public Interpreter::Opcode0 { public: void execute (Interpreter::Runtime& runtime) override { MWWorld::Ptr ptr = R()(runtime, false); std::string var = runtime.getStringLiteral(runtime[0].mInteger); runtime.pop(); std::stringstream output; if (!ptr.isEmpty()) { const std::string& script = ptr.getClass().getScript(ptr); if (!script.empty()) { const Compiler::Locals& locals = MWBase::Environment::get().getScriptManager()->getLocals(script); char type = locals.getType(var); std::string refId = ptr.getCellRef().getRefId(); if (refId.find(' ') != std::string::npos) refId = '"' + refId + '"'; switch (type) { case 'l': case 's': output << refId << "." << var << " = " << ptr.getRefData().getLocals().getIntVar(script, var); break; case 'f': output << refId << "." << var << " = " << ptr.getRefData().getLocals().getFloatVar(script, var); break; } } } if (output.rdbuf()->in_avail() == 0) { MWBase::World *world = MWBase::Environment::get().getWorld(); char type = world->getGlobalVariableType (var); switch (type) { case 's': output << var << " = " << runtime.getContext().getGlobalShort (var); break; case 'l': output << var << " = " << runtime.getContext().getGlobalLong (var); break; case 'f': output << var << " = " << runtime.getContext().getGlobalFloat (var); break; default: output << "unknown variable"; } } runtime.getContext().report(output.str()); } }; template class OpShowVars : public Interpreter::Opcode0 { void printLocalVars(Interpreter::Runtime &runtime, const MWWorld::Ptr &ptr) { std::stringstream str; const std::string script = ptr.getClass().getScript(ptr); if(script.empty()) str<< ptr.getCellRef().getRefId()<<" does not have a script."; else { str<< "Local variables for "<getLocals(script); const std::vector *names = &complocals.get('s'); for(size_t i = 0;i < names->size();++i) { if(i >= locals.mShorts.size()) break; str<size();++i) { if(i >= locals.mLongs.size()) break; str<size();++i) { if(i >= locals.mFloats.size()) break; str< names = runtime.getContext().getGlobals(); for(size_t i = 0;i < names.size();++i) { char type = world->getGlobalVariableType (names[i]); str << std::endl << " " << names[i] << " = "; switch (type) { case 's': str << runtime.getContext().getGlobalShort (names[i]) << " (short)"; break; case 'l': str << runtime.getContext().getGlobalLong (names[i]) << " (long)"; break; case 'f': str << runtime.getContext().getGlobalFloat (names[i]) << " (float)"; break; default: str << ""; } } runtime.getContext().report (str.str()); } public: void execute(Interpreter::Runtime& runtime) override { MWWorld::Ptr ptr = R()(runtime, false); if (!ptr.isEmpty()) printLocalVars(runtime, ptr); else { // No reference, no problem. printGlobalVars(runtime); } } }; class OpToggleScripts : public Interpreter::Opcode0 { public: void execute (Interpreter::Runtime& runtime) override { bool enabled = MWBase::Environment::get().getWorld()->toggleScripts(); runtime.getContext().report(enabled ? "Scripts -> On" : "Scripts -> Off"); } }; class OpToggleGodMode : public Interpreter::Opcode0 { public: void execute (Interpreter::Runtime& runtime) override { bool enabled = MWBase::Environment::get().getWorld()->toggleGodMode(); runtime.getContext().report (enabled ? "God Mode -> On" : "God Mode -> Off"); } }; template class OpCast : public Interpreter::Opcode0 { public: void execute (Interpreter::Runtime& runtime) override { MWWorld::Ptr ptr = R()(runtime); std::string spellId = runtime.getStringLiteral (runtime[0].mInteger); runtime.pop(); std::string targetId = ::Misc::StringUtils::lowerCase(runtime.getStringLiteral (runtime[0].mInteger)); runtime.pop(); const ESM::Spell* spell = MWBase::Environment::get().getWorld()->getStore().get().search(spellId); if (!spell) { runtime.getContext().report("spellcasting failed: cannot find spell \""+spellId+"\""); return; } if (ptr == MWMechanics::getPlayer()) { MWBase::Environment::get().getWorld()->getPlayer().setSelectedSpell(spellId); return; } if (ptr.getClass().isActor()) { MWMechanics::AiCast castPackage(targetId, spellId, true); ptr.getClass().getCreatureStats (ptr).getAiSequence().stack(castPackage, ptr); return; } MWWorld::Ptr target = MWBase::Environment::get().getWorld()->searchPtr(targetId, false, false); if (target.isEmpty()) return; MWMechanics::CastSpell cast(ptr, target, false, true); cast.playSpellCastingEffects(spell->mId, false); cast.mHitPosition = target.getRefData().getPosition().asVec3(); cast.mAlwaysSucceed = true; cast.cast(spell); } }; template class OpExplodeSpell : public Interpreter::Opcode0 { public: void execute (Interpreter::Runtime& runtime) override { MWWorld::Ptr ptr = R()(runtime); std::string spellId = runtime.getStringLiteral (runtime[0].mInteger); runtime.pop(); const ESM::Spell* spell = MWBase::Environment::get().getWorld()->getStore().get().search(spellId); if (!spell) { runtime.getContext().report("spellcasting failed: cannot find spell \""+spellId+"\""); return; } if (ptr == MWMechanics::getPlayer()) { MWBase::Environment::get().getWorld()->getPlayer().setSelectedSpell(spellId); return; } if (ptr.getClass().isActor()) { MWMechanics::AiCast castPackage(ptr.getCellRef().getRefId(), spellId, true); ptr.getClass().getCreatureStats (ptr).getAiSequence().stack(castPackage, ptr); return; } MWMechanics::CastSpell cast(ptr, ptr, false, true); cast.mHitPosition = ptr.getRefData().getPosition().asVec3(); cast.mAlwaysSucceed = true; cast.cast(spell); } }; class OpGoToJail : public Interpreter::Opcode0 { public: void execute (Interpreter::Runtime& runtime) override { MWBase::World* world = MWBase::Environment::get().getWorld(); world->goToJail(); } }; class OpPayFine : public Interpreter::Opcode0 { public: void execute(Interpreter::Runtime &runtime) override { MWWorld::Ptr player = MWMechanics::getPlayer(); player.getClass().getNpcStats(player).setBounty(0); MWBase::Environment::get().getWorld()->confiscateStolenItems(player); MWBase::Environment::get().getWorld()->getPlayer().recordCrimeId(); } }; class OpPayFineThief : public Interpreter::Opcode0 { public: void execute(Interpreter::Runtime &runtime) override { MWWorld::Ptr player = MWMechanics::getPlayer(); player.getClass().getNpcStats(player).setBounty(0); MWBase::Environment::get().getWorld()->getPlayer().recordCrimeId(); } }; class OpGetPcInJail : public Interpreter::Opcode0 { public: void execute (Interpreter::Runtime &runtime) override { runtime.push (MWBase::Environment::get().getWorld()->isPlayerInJail()); } }; class OpGetPcTraveling : public Interpreter::Opcode0 { public: void execute (Interpreter::Runtime &runtime) override { runtime.push (MWBase::Environment::get().getWorld()->isPlayerTraveling()); } }; template class OpBetaComment : public Interpreter::Opcode1 { public: void execute(Interpreter::Runtime &runtime, unsigned int arg0) override { MWWorld::Ptr ptr = R()(runtime); std::stringstream msg; msg << "Report time: "; std::time_t currentTime = std::chrono::system_clock::to_time_t(std::chrono::system_clock::now()); msg << std::put_time(std::gmtime(¤tTime), "%Y.%m.%d %T UTC") << std::endl; msg << "Content file: "; if (!ptr.getCellRef().hasContentFile()) msg << "[None]" << std::endl; else { std::vector contentFiles = MWBase::Environment::get().getWorld()->getContentFiles(); msg << contentFiles.at (ptr.getCellRef().getRefNum().mContentFile) << std::endl; msg << "RefNum: " << ptr.getCellRef().getRefNum().mIndex << std::endl; } if (ptr.getRefData().isDeletedByContentFile()) msg << "[Deleted by content file]" << std::endl; if (!ptr.getRefData().getCount()) msg << "[Deleted]" << std::endl; msg << "RefID: " << ptr.getCellRef().getRefId() << std::endl; msg << "Memory address: " << ptr.getBase() << std::endl; if (ptr.isInCell()) { MWWorld::CellStore* cell = ptr.getCell(); msg << "Cell: " << MWBase::Environment::get().getWorld()->getCellName(cell) << std::endl; if (cell->getCell()->isExterior()) msg << "Grid: " << cell->getCell()->getGridX() << " " << cell->getCell()->getGridY() << std::endl; osg::Vec3f pos (ptr.getRefData().getPosition().asVec3()); msg << "Coordinates: " << pos.x() << " " << pos.y() << " " << pos.z() << std::endl; auto vfs = MWBase::Environment::get().getResourceSystem()->getVFS(); std::string model = ::Misc::ResourceHelpers::correctActorModelPath(ptr.getClass().getModel(ptr), vfs); msg << "Model: " << model << std::endl; if(!model.empty()) { const std::string archive = vfs->getArchive(model); if(!archive.empty()) msg << "(" << archive << ")" << std::endl; } if (!ptr.getClass().getScript(ptr).empty()) msg << "Script: " << ptr.getClass().getScript(ptr) << std::endl; } while (arg0 > 0) { std::string notes = runtime.getStringLiteral (runtime[0].mInteger); runtime.pop(); if (!notes.empty()) msg << "Notes: " << notes << std::endl; --arg0; } Log(Debug::Warning) << "\n" << msg.str(); runtime.getContext().report(msg.str()); } }; class OpAddToLevCreature : public Interpreter::Opcode0 { public: void execute(Interpreter::Runtime &runtime) override { const std::string& levId = runtime.getStringLiteral(runtime[0].mInteger); runtime.pop(); const std::string& creatureId = runtime.getStringLiteral(runtime[0].mInteger); runtime.pop(); int level = runtime[0].mInteger; runtime.pop(); ESM::CreatureLevList listCopy = *MWBase::Environment::get().getWorld()->getStore().get().find(levId); addToLevList(&listCopy, creatureId, level); MWBase::Environment::get().getWorld()->createOverrideRecord(listCopy); } }; class OpRemoveFromLevCreature : public Interpreter::Opcode0 { public: void execute(Interpreter::Runtime &runtime) override { const std::string& levId = runtime.getStringLiteral(runtime[0].mInteger); runtime.pop(); const std::string& creatureId = runtime.getStringLiteral(runtime[0].mInteger); runtime.pop(); int level = runtime[0].mInteger; runtime.pop(); ESM::CreatureLevList listCopy = *MWBase::Environment::get().getWorld()->getStore().get().find(levId); removeFromLevList(&listCopy, creatureId, level); MWBase::Environment::get().getWorld()->createOverrideRecord(listCopy); } }; class OpAddToLevItem : public Interpreter::Opcode0 { public: void execute(Interpreter::Runtime &runtime) override { const std::string& levId = runtime.getStringLiteral(runtime[0].mInteger); runtime.pop(); const std::string& itemId = runtime.getStringLiteral(runtime[0].mInteger); runtime.pop(); int level = runtime[0].mInteger; runtime.pop(); ESM::ItemLevList listCopy = *MWBase::Environment::get().getWorld()->getStore().get().find(levId); addToLevList(&listCopy, itemId, level); MWBase::Environment::get().getWorld()->createOverrideRecord(listCopy); } }; class OpRemoveFromLevItem : public Interpreter::Opcode0 { public: void execute(Interpreter::Runtime &runtime) override { const std::string& levId = runtime.getStringLiteral(runtime[0].mInteger); runtime.pop(); const std::string& itemId = runtime.getStringLiteral(runtime[0].mInteger); runtime.pop(); int level = runtime[0].mInteger; runtime.pop(); ESM::ItemLevList listCopy = *MWBase::Environment::get().getWorld()->getStore().get().find(levId); removeFromLevList(&listCopy, itemId, level); MWBase::Environment::get().getWorld()->createOverrideRecord(listCopy); } }; template class OpShowSceneGraph : public Interpreter::Opcode1 { public: void execute(Interpreter::Runtime &runtime, unsigned int arg0) override { MWWorld::Ptr ptr = R()(runtime, false); int confirmed = 0; if (arg0==1) { confirmed = runtime[0].mInteger; runtime.pop(); } if (ptr.isEmpty() && !confirmed) runtime.getContext().report("Exporting the entire scene graph will result in a large file. Confirm this action using 'showscenegraph 1' or select an object instead."); else { const std::string& filename = MWBase::Environment::get().getWorld()->exportSceneGraph(ptr); runtime.getContext().report("Wrote '" + filename + "'"); } } }; class OpToggleNavMesh : public Interpreter::Opcode0 { public: void execute (Interpreter::Runtime& runtime) override { bool enabled = MWBase::Environment::get().getWorld()->toggleRenderMode (MWRender::Render_NavMesh); runtime.getContext().report (enabled ? "Navigation Mesh Rendering -> On" : "Navigation Mesh Rendering -> Off"); } }; class OpToggleActorsPaths : public Interpreter::Opcode0 { public: void execute (Interpreter::Runtime& runtime) override { bool enabled = MWBase::Environment::get().getWorld()->toggleRenderMode (MWRender::Render_ActorsPaths); runtime.getContext().report (enabled ? "Agents Paths Rendering -> On" : "Agents Paths Rendering -> Off"); } }; class OpSetNavMeshNumberToRender : public Interpreter::Opcode0 { public: void execute (Interpreter::Runtime& runtime) override { const auto navMeshNumber = runtime[0].mInteger; runtime.pop(); if (navMeshNumber < 0) { runtime.getContext().report("Invalid navmesh number: use not less than zero values"); return; } MWBase::Environment::get().getWorld()->setNavMeshNumberToRender(static_cast(navMeshNumber)); } }; template class OpRepairedOnMe : public Interpreter::Opcode0 { public: void execute (Interpreter::Runtime& runtime) override { // Broken in vanilla and deliberately no-op. runtime.push(0); } }; class OpToggleRecastMesh : public Interpreter::Opcode0 { public: void execute (Interpreter::Runtime& runtime) override { bool enabled = MWBase::Environment::get().getWorld()->toggleRenderMode (MWRender::Render_RecastMesh); runtime.getContext().report (enabled ? "Recast Mesh Rendering -> On" : "Recast Mesh Rendering -> Off"); } }; void installOpcodes (Interpreter::Interpreter& interpreter) { interpreter.installSegment5 (Compiler::Misc::opcodeMenuMode, new OpMenuMode); interpreter.installSegment5 (Compiler::Misc::opcodeRandom, new OpRandom); interpreter.installSegment5 (Compiler::Misc::opcodeScriptRunning, new OpScriptRunning); interpreter.installSegment5 (Compiler::Misc::opcodeStartScript, new OpStartScript); interpreter.installSegment5 (Compiler::Misc::opcodeStartScriptExplicit, new OpStartScript); interpreter.installSegment5 (Compiler::Misc::opcodeStopScript, new OpStopScript); interpreter.installSegment5 (Compiler::Misc::opcodeGetSecondsPassed, new OpGetSecondsPassed); interpreter.installSegment5 (Compiler::Misc::opcodeEnable, new OpEnable); interpreter.installSegment5 (Compiler::Misc::opcodeEnableExplicit, new OpEnable); interpreter.installSegment5 (Compiler::Misc::opcodeDisable, new OpDisable); interpreter.installSegment5 (Compiler::Misc::opcodeDisableExplicit, new OpDisable); interpreter.installSegment5 (Compiler::Misc::opcodeGetDisabled, new OpGetDisabled); interpreter.installSegment5 (Compiler::Misc::opcodeGetDisabledExplicit, new OpGetDisabled); interpreter.installSegment5 (Compiler::Misc::opcodeXBox, new OpXBox); interpreter.installSegment5 (Compiler::Misc::opcodeOnActivate, new OpOnActivate); interpreter.installSegment5 (Compiler::Misc::opcodeOnActivateExplicit, new OpOnActivate); interpreter.installSegment5 (Compiler::Misc::opcodeActivate, new OpActivate); interpreter.installSegment5 (Compiler::Misc::opcodeActivateExplicit, new OpActivate); interpreter.installSegment3 (Compiler::Misc::opcodeLock, new OpLock); interpreter.installSegment3 (Compiler::Misc::opcodeLockExplicit, new OpLock); interpreter.installSegment5 (Compiler::Misc::opcodeUnlock, new OpUnlock); interpreter.installSegment5 (Compiler::Misc::opcodeUnlockExplicit, new OpUnlock); interpreter.installSegment5 (Compiler::Misc::opcodeToggleCollisionDebug, new OpToggleCollisionDebug); interpreter.installSegment5 (Compiler::Misc::opcodeToggleCollisionBoxes, new OpToggleCollisionBoxes); interpreter.installSegment5 (Compiler::Misc::opcodeToggleWireframe, new OpToggleWireframe); interpreter.installSegment5 (Compiler::Misc::opcodeFadeIn, new OpFadeIn); interpreter.installSegment5 (Compiler::Misc::opcodeFadeOut, new OpFadeOut); interpreter.installSegment5 (Compiler::Misc::opcodeFadeTo, new OpFadeTo); interpreter.installSegment5 (Compiler::Misc::opcodeTogglePathgrid, new OpTogglePathgrid); interpreter.installSegment5 (Compiler::Misc::opcodeToggleWater, new OpToggleWater); interpreter.installSegment5 (Compiler::Misc::opcodeToggleWorld, new OpToggleWorld); interpreter.installSegment5 (Compiler::Misc::opcodeDontSaveObject, new OpDontSaveObject); interpreter.installSegment5 (Compiler::Misc::opcodePcForce1stPerson, new OpPcForce1stPerson); interpreter.installSegment5 (Compiler::Misc::opcodePcForce3rdPerson, new OpPcForce3rdPerson); interpreter.installSegment5 (Compiler::Misc::opcodePcGet3rdPerson, new OpPcGet3rdPerson); interpreter.installSegment5 (Compiler::Misc::opcodeToggleVanityMode, new OpToggleVanityMode); interpreter.installSegment5 (Compiler::Misc::opcodeGetPcSleep, new OpGetPcSleep); interpreter.installSegment5 (Compiler::Misc::opcodeGetPcJumping, new OpGetPcJumping); interpreter.installSegment5 (Compiler::Misc::opcodeWakeUpPc, new OpWakeUpPc); interpreter.installSegment5 (Compiler::Misc::opcodePlayBink, new OpPlayBink); interpreter.installSegment5 (Compiler::Misc::opcodePayFine, new OpPayFine); interpreter.installSegment5 (Compiler::Misc::opcodePayFineThief, new OpPayFineThief); interpreter.installSegment5 (Compiler::Misc::opcodeGoToJail, new OpGoToJail); interpreter.installSegment5 (Compiler::Misc::opcodeGetLocked, new OpGetLocked); interpreter.installSegment5 (Compiler::Misc::opcodeGetLockedExplicit, new OpGetLocked); interpreter.installSegment5 (Compiler::Misc::opcodeGetEffect, new OpGetEffect); interpreter.installSegment5 (Compiler::Misc::opcodeGetEffectExplicit, new OpGetEffect); interpreter.installSegment5 (Compiler::Misc::opcodeAddSoulGem, new OpAddSoulGem); interpreter.installSegment5 (Compiler::Misc::opcodeAddSoulGemExplicit, new OpAddSoulGem); interpreter.installSegment3 (Compiler::Misc::opcodeRemoveSoulGem, new OpRemoveSoulGem); interpreter.installSegment3 (Compiler::Misc::opcodeRemoveSoulGemExplicit, new OpRemoveSoulGem); interpreter.installSegment5 (Compiler::Misc::opcodeDrop, new OpDrop); interpreter.installSegment5 (Compiler::Misc::opcodeDropExplicit, new OpDrop); interpreter.installSegment5 (Compiler::Misc::opcodeDropSoulGem, new OpDropSoulGem); interpreter.installSegment5 (Compiler::Misc::opcodeDropSoulGemExplicit, new OpDropSoulGem); interpreter.installSegment5 (Compiler::Misc::opcodeGetAttacked, new OpGetAttacked); interpreter.installSegment5 (Compiler::Misc::opcodeGetAttackedExplicit, new OpGetAttacked); interpreter.installSegment5 (Compiler::Misc::opcodeGetWeaponDrawn, new OpGetWeaponDrawn); interpreter.installSegment5 (Compiler::Misc::opcodeGetWeaponDrawnExplicit, new OpGetWeaponDrawn); interpreter.installSegment5 (Compiler::Misc::opcodeGetSpellReadied, new OpGetSpellReadied); interpreter.installSegment5 (Compiler::Misc::opcodeGetSpellReadiedExplicit, new OpGetSpellReadied); interpreter.installSegment5 (Compiler::Misc::opcodeGetSpellEffects, new OpGetSpellEffects); interpreter.installSegment5 (Compiler::Misc::opcodeGetSpellEffectsExplicit, new OpGetSpellEffects); interpreter.installSegment5 (Compiler::Misc::opcodeGetCurrentTime, new OpGetCurrentTime); interpreter.installSegment5 (Compiler::Misc::opcodeSetDelete, new OpSetDelete); interpreter.installSegment5 (Compiler::Misc::opcodeSetDeleteExplicit, new OpSetDelete); interpreter.installSegment5 (Compiler::Misc::opcodeGetSquareRoot, new OpGetSquareRoot); interpreter.installSegment5 (Compiler::Misc::opcodeFall, new OpFall); interpreter.installSegment5 (Compiler::Misc::opcodeFallExplicit, new OpFall); interpreter.installSegment5 (Compiler::Misc::opcodeGetStandingPc, new OpGetStandingPc); interpreter.installSegment5 (Compiler::Misc::opcodeGetStandingPcExplicit, new OpGetStandingPc); interpreter.installSegment5 (Compiler::Misc::opcodeGetStandingActor, new OpGetStandingActor); interpreter.installSegment5 (Compiler::Misc::opcodeGetStandingActorExplicit, new OpGetStandingActor); interpreter.installSegment5 (Compiler::Misc::opcodeGetCollidingPc, new OpGetCollidingPc); interpreter.installSegment5 (Compiler::Misc::opcodeGetCollidingPcExplicit, new OpGetCollidingPc); interpreter.installSegment5 (Compiler::Misc::opcodeGetCollidingActor, new OpGetCollidingActor); interpreter.installSegment5 (Compiler::Misc::opcodeGetCollidingActorExplicit, new OpGetCollidingActor); interpreter.installSegment5 (Compiler::Misc::opcodeHurtStandingActor, new OpHurtStandingActor); interpreter.installSegment5 (Compiler::Misc::opcodeHurtStandingActorExplicit, new OpHurtStandingActor); interpreter.installSegment5 (Compiler::Misc::opcodeHurtCollidingActor, new OpHurtCollidingActor); interpreter.installSegment5 (Compiler::Misc::opcodeHurtCollidingActorExplicit, new OpHurtCollidingActor); interpreter.installSegment5 (Compiler::Misc::opcodeGetWindSpeed, new OpGetWindSpeed); interpreter.installSegment5 (Compiler::Misc::opcodeHitOnMe, new OpHitOnMe); interpreter.installSegment5 (Compiler::Misc::opcodeHitOnMeExplicit, new OpHitOnMe); interpreter.installSegment5 (Compiler::Misc::opcodeHitAttemptOnMe, new OpHitAttemptOnMe); interpreter.installSegment5 (Compiler::Misc::opcodeHitAttemptOnMeExplicit, new OpHitAttemptOnMe); interpreter.installSegment5 (Compiler::Misc::opcodeDisableTeleporting, new OpEnableTeleporting); interpreter.installSegment5 (Compiler::Misc::opcodeEnableTeleporting, new OpEnableTeleporting); interpreter.installSegment5 (Compiler::Misc::opcodeShowVars, new OpShowVars); interpreter.installSegment5 (Compiler::Misc::opcodeShowVarsExplicit, new OpShowVars); interpreter.installSegment5 (Compiler::Misc::opcodeShow, new OpShow); interpreter.installSegment5 (Compiler::Misc::opcodeShowExplicit, new OpShow); interpreter.installSegment5 (Compiler::Misc::opcodeToggleGodMode, new OpToggleGodMode); interpreter.installSegment5 (Compiler::Misc::opcodeToggleScripts, new OpToggleScripts); interpreter.installSegment5 (Compiler::Misc::opcodeDisableLevitation, new OpEnableLevitation); interpreter.installSegment5 (Compiler::Misc::opcodeEnableLevitation, new OpEnableLevitation); interpreter.installSegment5 (Compiler::Misc::opcodeCast, new OpCast); interpreter.installSegment5 (Compiler::Misc::opcodeCastExplicit, new OpCast); interpreter.installSegment5 (Compiler::Misc::opcodeExplodeSpell, new OpExplodeSpell); interpreter.installSegment5 (Compiler::Misc::opcodeExplodeSpellExplicit, new OpExplodeSpell); interpreter.installSegment5 (Compiler::Misc::opcodeGetPcInJail, new OpGetPcInJail); interpreter.installSegment5 (Compiler::Misc::opcodeGetPcTraveling, new OpGetPcTraveling); interpreter.installSegment3 (Compiler::Misc::opcodeBetaComment, new OpBetaComment); interpreter.installSegment3 (Compiler::Misc::opcodeBetaCommentExplicit, new OpBetaComment); interpreter.installSegment5 (Compiler::Misc::opcodeAddToLevCreature, new OpAddToLevCreature); interpreter.installSegment5 (Compiler::Misc::opcodeRemoveFromLevCreature, new OpRemoveFromLevCreature); interpreter.installSegment5 (Compiler::Misc::opcodeAddToLevItem, new OpAddToLevItem); interpreter.installSegment5 (Compiler::Misc::opcodeRemoveFromLevItem, new OpRemoveFromLevItem); interpreter.installSegment3 (Compiler::Misc::opcodeShowSceneGraph, new OpShowSceneGraph); interpreter.installSegment3 (Compiler::Misc::opcodeShowSceneGraphExplicit, new OpShowSceneGraph); interpreter.installSegment5 (Compiler::Misc::opcodeToggleBorders, new OpToggleBorders); interpreter.installSegment5 (Compiler::Misc::opcodeToggleNavMesh, new OpToggleNavMesh); interpreter.installSegment5 (Compiler::Misc::opcodeToggleActorsPaths, new OpToggleActorsPaths); interpreter.installSegment5 (Compiler::Misc::opcodeSetNavMeshNumberToRender, new OpSetNavMeshNumberToRender); interpreter.installSegment5 (Compiler::Misc::opcodeRepairedOnMe, new OpRepairedOnMe); interpreter.installSegment5 (Compiler::Misc::opcodeRepairedOnMeExplicit, new OpRepairedOnMe); interpreter.installSegment5 (Compiler::Misc::opcodeToggleRecastMesh, new OpToggleRecastMesh); } } } ================================================ FILE: apps/openmw/mwscript/miscextensions.hpp ================================================ #ifndef GAME_SCRIPT_MISCEXTENSIONS_H #define GAME_SCRIPT_MISCEXTENSIONS_H namespace Compiler { class Extensions; } namespace Interpreter { class Interpreter; } namespace MWScript { namespace Misc { void installOpcodes (Interpreter::Interpreter& interpreter); } } #endif ================================================ FILE: apps/openmw/mwscript/ref.cpp ================================================ #include "ref.hpp" #include #include "../mwbase/environment.hpp" #include "../mwbase/world.hpp" #include "interpretercontext.hpp" MWWorld::Ptr MWScript::ExplicitRef::operator() (Interpreter::Runtime& runtime, bool required, bool activeOnly) const { std::string id = runtime.getStringLiteral(runtime[0].mInteger); runtime.pop(); if (required) return MWBase::Environment::get().getWorld()->getPtr(id, activeOnly); else return MWBase::Environment::get().getWorld()->searchPtr(id, activeOnly); } MWWorld::Ptr MWScript::ImplicitRef::operator() (Interpreter::Runtime& runtime, bool required, bool activeOnly) const { MWScript::InterpreterContext& context = static_cast (runtime.getContext()); return context.getReference(required); } ================================================ FILE: apps/openmw/mwscript/ref.hpp ================================================ #ifndef GAME_MWSCRIPT_REF_H #define GAME_MWSCRIPT_REF_H #include #include "../mwworld/ptr.hpp" namespace Interpreter { class Runtime; } namespace MWScript { struct ExplicitRef { static constexpr bool implicit = false; MWWorld::Ptr operator() (Interpreter::Runtime& runtime, bool required = true, bool activeOnly = false) const; }; struct ImplicitRef { static constexpr bool implicit = true; MWWorld::Ptr operator() (Interpreter::Runtime& runtime, bool required = true, bool activeOnly = false) const; }; } #endif ================================================ FILE: apps/openmw/mwscript/scriptmanagerimp.cpp ================================================ #include "scriptmanagerimp.hpp" #include #include #include #include #include #include #include #include #include #include #include #include "../mwworld/esmstore.hpp" #include "extensions.hpp" #include "interpretercontext.hpp" namespace MWScript { ScriptManager::ScriptManager (const MWWorld::ESMStore& store, Compiler::Context& compilerContext, int warningsMode, const std::vector& scriptBlacklist) : mErrorHandler(), mStore (store), mCompilerContext (compilerContext), mParser (mErrorHandler, mCompilerContext), mOpcodesInstalled (false), mGlobalScripts (store) { mErrorHandler.setWarningsMode (warningsMode); mScriptBlacklist.resize (scriptBlacklist.size()); std::transform (scriptBlacklist.begin(), scriptBlacklist.end(), mScriptBlacklist.begin(), Misc::StringUtils::lowerCase); std::sort (mScriptBlacklist.begin(), mScriptBlacklist.end()); } bool ScriptManager::compile (const std::string& name) { mParser.reset(); mErrorHandler.reset(); if (const ESM::Script *script = mStore.get().find (name)) { mErrorHandler.setContext(name); bool Success = true; try { std::istringstream input (script->mScriptText); Compiler::Scanner scanner (mErrorHandler, input, mCompilerContext.getExtensions()); scanner.scan (mParser); if (!mErrorHandler.isGood()) Success = false; } catch (const Compiler::SourceException&) { // error has already been reported via error handler Success = false; } catch (const std::exception& error) { Log(Debug::Error) << "Error: An exception has been thrown: " << error.what(); Success = false; } if (!Success) { Log(Debug::Error) << "Error: script compiling failed: " << name; } if (Success) { std::vector code; mParser.getCode(code); mScripts.emplace(name, CompiledScript(code, mParser.getLocals())); return true; } } return false; } bool ScriptManager::run (const std::string& name, Interpreter::Context& interpreterContext) { // compile script ScriptCollection::iterator iter = mScripts.find (name); if (iter==mScripts.end()) { if (!compile (name)) { // failed -> ignore script from now on. std::vector empty; mScripts.emplace(name, CompiledScript(empty, Compiler::Locals())); return false; } iter = mScripts.find (name); assert (iter!=mScripts.end()); } // execute script if (!iter->second.mByteCode.empty() && iter->second.mActive) try { if (!mOpcodesInstalled) { installOpcodes (mInterpreter); mOpcodesInstalled = true; } mInterpreter.run (&iter->second.mByteCode[0], iter->second.mByteCode.size(), interpreterContext); return true; } catch (const MissingImplicitRefError& e) { Log(Debug::Error) << "Execution of script " << name << " failed: " << e.what(); } catch (const std::exception& e) { Log(Debug::Error) << "Execution of script " << name << " failed: " << e.what(); iter->second.mActive = false; // don't execute again. } return false; } void ScriptManager::clear() { for (auto& script : mScripts) { script.second.mActive = true; } mGlobalScripts.clear(); } std::pair ScriptManager::compileAll() { int count = 0; int success = 0; for (auto& script : mStore.get()) { if (!std::binary_search (mScriptBlacklist.begin(), mScriptBlacklist.end(), Misc::StringUtils::lowerCase(script.mId))) { ++count; if (compile(script.mId)) ++success; } } return std::make_pair (count, success); } const Compiler::Locals& ScriptManager::getLocals (const std::string& name) { std::string name2 = Misc::StringUtils::lowerCase (name); { ScriptCollection::iterator iter = mScripts.find (name2); if (iter!=mScripts.end()) return iter->second.mLocals; } { std::map::iterator iter = mOtherLocals.find (name2); if (iter!=mOtherLocals.end()) return iter->second; } if (const ESM::Script *script = mStore.get().search (name2)) { Compiler::Locals locals; const Compiler::ContextOverride override(mErrorHandler, name2 + "[local variables]"); std::istringstream stream (script->mScriptText); Compiler::QuickFileParser parser (mErrorHandler, mCompilerContext, locals); Compiler::Scanner scanner (mErrorHandler, stream, mCompilerContext.getExtensions()); scanner.scan (parser); std::map::iterator iter = mOtherLocals.emplace(name2, locals).first; return iter->second; } throw std::logic_error ("script " + name + " does not exist"); } GlobalScripts& ScriptManager::getGlobalScripts() { return mGlobalScripts; } } ================================================ FILE: apps/openmw/mwscript/scriptmanagerimp.hpp ================================================ #ifndef GAME_SCRIPT_SCRIPTMANAGER_H #define GAME_SCRIPT_SCRIPTMANAGER_H #include #include #include #include #include #include #include "../mwbase/scriptmanager.hpp" #include "globalscripts.hpp" namespace MWWorld { class ESMStore; } namespace Compiler { class Context; } namespace Interpreter { class Context; class Interpreter; } namespace MWScript { class ScriptManager : public MWBase::ScriptManager { Compiler::StreamErrorHandler mErrorHandler; const MWWorld::ESMStore& mStore; Compiler::Context& mCompilerContext; Compiler::FileParser mParser; Interpreter::Interpreter mInterpreter; bool mOpcodesInstalled; struct CompiledScript { std::vector mByteCode; Compiler::Locals mLocals; bool mActive; CompiledScript(const std::vector& code, const Compiler::Locals& locals) { mByteCode = code; mLocals = locals; mActive = true; } }; typedef std::map ScriptCollection; ScriptCollection mScripts; GlobalScripts mGlobalScripts; std::map mOtherLocals; std::vector mScriptBlacklist; public: ScriptManager (const MWWorld::ESMStore& store, Compiler::Context& compilerContext, int warningsMode, const std::vector& scriptBlacklist); void clear() override; bool run (const std::string& name, Interpreter::Context& interpreterContext) override; ///< Run the script with the given name (compile first, if not compiled yet) bool compile (const std::string& name) override; ///< Compile script with the given namen /// \return Success? std::pair compileAll() override; ///< Compile all scripts /// \return count, success const Compiler::Locals& getLocals (const std::string& name) override; ///< Return locals for script \a name. GlobalScripts& getGlobalScripts() override; }; } #endif ================================================ FILE: apps/openmw/mwscript/skyextensions.cpp ================================================ #include "skyextensions.hpp" #include #include #include #include #include #include "../mwbase/environment.hpp" #include "../mwbase/world.hpp" #include "interpretercontext.hpp" namespace MWScript { namespace Sky { class OpToggleSky : public Interpreter::Opcode0 { public: void execute (Interpreter::Runtime& runtime) override { bool enabled = MWBase::Environment::get().getWorld()->toggleSky(); runtime.getContext().report (enabled ? "Sky -> On" : "Sky -> Off"); } }; class OpTurnMoonWhite : public Interpreter::Opcode0 { public: void execute (Interpreter::Runtime& runtime) override { MWBase::Environment::get().getWorld()->setMoonColour (false); } }; class OpTurnMoonRed : public Interpreter::Opcode0 { public: void execute (Interpreter::Runtime& runtime) override { MWBase::Environment::get().getWorld()->setMoonColour (true); } }; class OpGetMasserPhase : public Interpreter::Opcode0 { public: void execute (Interpreter::Runtime& runtime) override { runtime.push (MWBase::Environment::get().getWorld()->getMasserPhase()); } }; class OpGetSecundaPhase : public Interpreter::Opcode0 { public: void execute (Interpreter::Runtime& runtime) override { runtime.push (MWBase::Environment::get().getWorld()->getSecundaPhase()); } }; class OpGetCurrentWeather : public Interpreter::Opcode0 { public: void execute (Interpreter::Runtime& runtime) override { runtime.push (MWBase::Environment::get().getWorld()->getCurrentWeather()); } }; class OpChangeWeather : public Interpreter::Opcode0 { public: void execute (Interpreter::Runtime& runtime) override { std::string region = runtime.getStringLiteral (runtime[0].mInteger); runtime.pop(); Interpreter::Type_Integer id = runtime[0].mInteger; runtime.pop(); MWBase::Environment::get().getWorld()->changeWeather(region, id); } }; class OpModRegion : public Interpreter::Opcode1 { public: void execute (Interpreter::Runtime& runtime, unsigned int arg0) override { std::string region = runtime.getStringLiteral (runtime[0].mInteger); runtime.pop(); std::vector chances; chances.reserve(10); while(arg0 > 0) { chances.push_back(std::max(0, std::min(127, runtime[0].mInteger))); runtime.pop(); arg0--; } MWBase::Environment::get().getWorld()->modRegion(region, chances); } }; void installOpcodes (Interpreter::Interpreter& interpreter) { interpreter.installSegment5 (Compiler::Sky::opcodeToggleSky, new OpToggleSky); interpreter.installSegment5 (Compiler::Sky::opcodeTurnMoonWhite, new OpTurnMoonWhite); interpreter.installSegment5 (Compiler::Sky::opcodeTurnMoonRed, new OpTurnMoonRed); interpreter.installSegment5 (Compiler::Sky::opcodeGetMasserPhase, new OpGetMasserPhase); interpreter.installSegment5 (Compiler::Sky::opcodeGetSecundaPhase, new OpGetSecundaPhase); interpreter.installSegment5 (Compiler::Sky::opcodeGetCurrentWeather, new OpGetCurrentWeather); interpreter.installSegment5 (Compiler::Sky::opcodeChangeWeather, new OpChangeWeather); interpreter.installSegment3 (Compiler::Sky::opcodeModRegion, new OpModRegion); } } } ================================================ FILE: apps/openmw/mwscript/skyextensions.hpp ================================================ #ifndef GAME_SCRIPT_SKYEXTENSIONS_H #define GAME_SCRIPT_SKYEXTENSIONS_H namespace Compiler { class Extensions; } namespace Interpreter { class Interpreter; } namespace MWScript { /// \brief sky-related script functionality namespace Sky { void installOpcodes (Interpreter::Interpreter& interpreter); } } #endif ================================================ FILE: apps/openmw/mwscript/soundextensions.cpp ================================================ #include "soundextensions.hpp" /* Start of tes3mp addition Include additional headers for multiplayer purposes */ #include "../mwmp/Main.hpp" #include "../mwmp/Networking.hpp" #include "../mwmp/ObjectList.hpp" #include "../mwmp/ScriptController.hpp" /* End of tes3mp addition */ #include #include #include #include #include "../mwbase/environment.hpp" #include "../mwbase/world.hpp" #include "../mwbase/soundmanager.hpp" #include "../mwbase/windowmanager.hpp" #include "../mwworld/inventorystore.hpp" #include "../mwworld/class.hpp" #include "interpretercontext.hpp" #include "ref.hpp" namespace MWScript { namespace Sound { template class OpSay : public Interpreter::Opcode0 { public: void execute (Interpreter::Runtime& runtime) override { MWWorld::Ptr ptr = R()(runtime); MWScript::InterpreterContext& context = static_cast (runtime.getContext()); std::string file = runtime.getStringLiteral (runtime[0].mInteger); runtime.pop(); std::string text = runtime.getStringLiteral (runtime[0].mInteger); runtime.pop(); MWBase::Environment::get().getSoundManager()->say (ptr, file); if (MWBase::Environment::get().getWindowManager ()->getSubtitlesEnabled()) context.messageBox (text); } }; template class OpSayDone : public Interpreter::Opcode0 { public: void execute (Interpreter::Runtime& runtime) override { MWWorld::Ptr ptr = R()(runtime); runtime.push (MWBase::Environment::get().getSoundManager()->sayDone (ptr)); } }; class OpStreamMusic : public Interpreter::Opcode0 { public: void execute (Interpreter::Runtime& runtime) override { std::string sound = runtime.getStringLiteral (runtime[0].mInteger); runtime.pop(); /* Start of tes3mp addition Send an ID_MUSIC_PLAY packet every time new music is streamed through a script */ mwmp::ObjectList *objectList = mwmp::Main::get().getNetworking()->getObjectList(); objectList->reset(); objectList->packetOrigin = ScriptController::getPacketOriginFromContextType(runtime.getContext().getContextType()); objectList->originClientScript = runtime.getContext().getCurrentScriptName(); objectList->addMusicPlay(sound); objectList->sendMusicPlay(); /* End of tes3mp addition */ MWBase::Environment::get().getSoundManager()->streamMusic (sound); } }; class OpPlaySound : public Interpreter::Opcode0 { public: void execute (Interpreter::Runtime& runtime) override { std::string sound = runtime.getStringLiteral (runtime[0].mInteger); runtime.pop(); MWBase::Environment::get().getSoundManager()->playSound(sound, 1.0, 1.0, MWSound::Type::Sfx, MWSound::PlayMode::NoEnv); } }; class OpPlaySoundVP : public Interpreter::Opcode0 { public: void execute (Interpreter::Runtime& runtime) override { std::string sound = runtime.getStringLiteral (runtime[0].mInteger); runtime.pop(); Interpreter::Type_Float volume = runtime[0].mFloat; runtime.pop(); Interpreter::Type_Float pitch = runtime[0].mFloat; runtime.pop(); MWBase::Environment::get().getSoundManager()->playSound(sound, volume, pitch, MWSound::Type::Sfx, MWSound::PlayMode::NoEnv); } }; template class OpPlaySound3D : public Interpreter::Opcode0 { bool mLoop; public: OpPlaySound3D (bool loop) : mLoop (loop) {} void execute (Interpreter::Runtime& runtime) override { MWWorld::Ptr ptr = R()(runtime); std::string sound = runtime.getStringLiteral (runtime[0].mInteger); runtime.pop(); MWBase::Environment::get().getSoundManager()->playSound3D(ptr, sound, 1.0, 1.0, MWSound::Type::Sfx, mLoop ? MWSound::PlayMode::LoopRemoveAtDistance : MWSound::PlayMode::Normal); } }; template class OpPlaySoundVP3D : public Interpreter::Opcode0 { bool mLoop; public: OpPlaySoundVP3D (bool loop) : mLoop (loop) {} void execute (Interpreter::Runtime& runtime) override { MWWorld::Ptr ptr = R()(runtime); std::string sound = runtime.getStringLiteral (runtime[0].mInteger); runtime.pop(); Interpreter::Type_Float volume = runtime[0].mFloat; runtime.pop(); Interpreter::Type_Float pitch = runtime[0].mFloat; runtime.pop(); MWBase::Environment::get().getSoundManager()->playSound3D(ptr, sound, volume, pitch, MWSound::Type::Sfx, mLoop ? MWSound::PlayMode::LoopRemoveAtDistance : MWSound::PlayMode::Normal); } }; template class OpStopSound : public Interpreter::Opcode0 { public: void execute (Interpreter::Runtime& runtime) override { MWWorld::Ptr ptr = R()(runtime); std::string sound = runtime.getStringLiteral (runtime[0].mInteger); runtime.pop(); MWBase::Environment::get().getSoundManager()->stopSound3D (ptr, sound); } }; template class OpGetSoundPlaying : public Interpreter::Opcode0 { public: void execute (Interpreter::Runtime& runtime) override { MWWorld::Ptr ptr = R()(runtime); int index = runtime[0].mInteger; runtime.pop(); bool ret = MWBase::Environment::get().getSoundManager()->getSoundPlaying ( ptr, runtime.getStringLiteral (index)); // GetSoundPlaying called on an equipped item should also look for sounds played by the equipping actor. if (!ret && ptr.getContainerStore()) { MWWorld::Ptr cont = MWBase::Environment::get().getWorld()->findContainer(ptr); if (!cont.isEmpty() && cont.getClass().hasInventoryStore(cont) && cont.getClass().getInventoryStore(cont).isEquipped(ptr)) { ret = MWBase::Environment::get().getSoundManager()->getSoundPlaying ( cont, runtime.getStringLiteral (index)); } } runtime.push(ret); } }; void installOpcodes (Interpreter::Interpreter& interpreter) { interpreter.installSegment5 (Compiler::Sound::opcodeSay, new OpSay); interpreter.installSegment5 (Compiler::Sound::opcodeSayDone, new OpSayDone); interpreter.installSegment5 (Compiler::Sound::opcodeStreamMusic, new OpStreamMusic); interpreter.installSegment5 (Compiler::Sound::opcodePlaySound, new OpPlaySound); interpreter.installSegment5 (Compiler::Sound::opcodePlaySoundVP, new OpPlaySoundVP); interpreter.installSegment5 (Compiler::Sound::opcodePlaySound3D, new OpPlaySound3D (false)); interpreter.installSegment5 (Compiler::Sound::opcodePlaySound3DVP, new OpPlaySoundVP3D (false)); interpreter.installSegment5 (Compiler::Sound::opcodePlayLoopSound3D, new OpPlaySound3D (true)); interpreter.installSegment5 (Compiler::Sound::opcodePlayLoopSound3DVP, new OpPlaySoundVP3D (true)); interpreter.installSegment5 (Compiler::Sound::opcodeStopSound, new OpStopSound); interpreter.installSegment5 (Compiler::Sound::opcodeGetSoundPlaying, new OpGetSoundPlaying); interpreter.installSegment5 (Compiler::Sound::opcodeSayExplicit, new OpSay); interpreter.installSegment5 (Compiler::Sound::opcodeSayDoneExplicit, new OpSayDone); interpreter.installSegment5 (Compiler::Sound::opcodePlaySound3DExplicit, new OpPlaySound3D (false)); interpreter.installSegment5 (Compiler::Sound::opcodePlaySound3DVPExplicit, new OpPlaySoundVP3D (false)); interpreter.installSegment5 (Compiler::Sound::opcodePlayLoopSound3DExplicit, new OpPlaySound3D (true)); interpreter.installSegment5 (Compiler::Sound::opcodePlayLoopSound3DVPExplicit, new OpPlaySoundVP3D (true)); interpreter.installSegment5 (Compiler::Sound::opcodeStopSoundExplicit, new OpStopSound); interpreter.installSegment5 (Compiler::Sound::opcodeGetSoundPlayingExplicit, new OpGetSoundPlaying); } } } ================================================ FILE: apps/openmw/mwscript/soundextensions.hpp ================================================ #ifndef GAME_SCRIPT_SOUNDEXTENSIONS_H #define GAME_SCRIPT_SOUNDEXTENSIONS_H namespace Compiler { class Extensions; } namespace Interpreter { class Interpreter; } namespace MWScript { namespace Sound { // Script-extensions related to sound void installOpcodes (Interpreter::Interpreter& interpreter); } } #endif ================================================ FILE: apps/openmw/mwscript/statsextensions.cpp ================================================ #include "statsextensions.hpp" #include /* Start of tes3mp addition Include additional headers for multiplayer purposes */ #include "../mwmp/Main.hpp" #include "../mwmp/LocalPlayer.hpp" /* End of tes3mp addition */ #include #include "../mwworld/esmstore.hpp" #include #include #include #include #include #include #include "../mwbase/environment.hpp" #include "../mwbase/mechanicsmanager.hpp" #include "../mwbase/statemanager.hpp" #include "../mwbase/windowmanager.hpp" #include "../mwbase/world.hpp" #include "../mwworld/class.hpp" #include "../mwworld/player.hpp" #include "../mwmechanics/creaturestats.hpp" #include "../mwmechanics/npcstats.hpp" #include "../mwmechanics/actorutil.hpp" #include "../mwmechanics/spellcasting.hpp" #include "ref.hpp" namespace { std::string getDialogueActorFaction(MWWorld::ConstPtr actor) { std::string factionId = actor.getClass().getPrimaryFaction(actor); if (factionId.empty()) throw std::runtime_error ( "failed to determine dialogue actors faction (because actor is factionless)"); return factionId; } } namespace MWScript { namespace Stats { template class OpGetLevel : public Interpreter::Opcode0 { public: void execute (Interpreter::Runtime& runtime) override { MWWorld::Ptr ptr = R()(runtime); Interpreter::Type_Integer value = ptr.getClass() .getCreatureStats (ptr) .getLevel(); runtime.push (value); } }; template class OpSetLevel : public Interpreter::Opcode0 { public: void execute (Interpreter::Runtime& runtime) override { MWWorld::Ptr ptr = R()(runtime); Interpreter::Type_Integer value = runtime[0].mInteger; runtime.pop(); ptr.getClass() .getCreatureStats (ptr) .setLevel(value); } }; template class OpGetAttribute : public Interpreter::Opcode0 { int mIndex; public: OpGetAttribute (int index) : mIndex (index) {} void execute (Interpreter::Runtime& runtime) override { MWWorld::Ptr ptr = R()(runtime); Interpreter::Type_Float value = ptr.getClass() .getCreatureStats (ptr) .getAttribute(mIndex) .getModified(); runtime.push (value); } }; template class OpSetAttribute : public Interpreter::Opcode0 { int mIndex; public: OpSetAttribute (int index) : mIndex (index) {} void execute (Interpreter::Runtime& runtime) override { MWWorld::Ptr ptr = R()(runtime); Interpreter::Type_Float value = runtime[0].mFloat; runtime.pop(); MWMechanics::AttributeValue attribute = ptr.getClass().getCreatureStats(ptr).getAttribute(mIndex); attribute.setBase (value); ptr.getClass().getCreatureStats(ptr).setAttribute(mIndex, attribute); } }; template class OpModAttribute : public Interpreter::Opcode0 { int mIndex; public: OpModAttribute (int index) : mIndex (index) {} void execute (Interpreter::Runtime& runtime) override { MWWorld::Ptr ptr = R()(runtime); Interpreter::Type_Float value = runtime[0].mFloat; runtime.pop(); 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)); ptr.getClass().getCreatureStats(ptr).setAttribute(mIndex, attribute); } }; template class OpGetDynamic : public Interpreter::Opcode0 { int mIndex; public: OpGetDynamic (int index) : mIndex (index) {} void execute (Interpreter::Runtime& runtime) override { MWWorld::Ptr ptr = R()(runtime); Interpreter::Type_Float value; if (mIndex==0 && ptr.getClass().hasItemHealth (ptr)) { // health is a special case value = static_cast(ptr.getClass().getItemMaxHealth(ptr)); } else { value = ptr.getClass() .getCreatureStats(ptr) .getDynamic(mIndex) .getCurrent(); // GetMagicka shouldn't return negative values if(mIndex == 1 && value < 0) value = 0; } runtime.push (value); } }; template class OpSetDynamic : public Interpreter::Opcode0 { int mIndex; public: OpSetDynamic (int index) : mIndex (index) {} void execute (Interpreter::Runtime& runtime) override { MWWorld::Ptr ptr = R()(runtime); Interpreter::Type_Float value = runtime[0].mFloat; runtime.pop(); MWMechanics::DynamicStat stat (ptr.getClass().getCreatureStats (ptr) .getDynamic (mIndex)); stat.setModified (value, 0); stat.setCurrent(value); ptr.getClass().getCreatureStats (ptr).setDynamic (mIndex, stat); } }; template class OpModDynamic : public Interpreter::Opcode0 { int mIndex; public: OpModDynamic (int index) : mIndex (index) {} void execute (Interpreter::Runtime& runtime) override { int peek = R::implicit ? 0 : runtime[0].mInteger; MWWorld::Ptr ptr = R()(runtime); Interpreter::Type_Float diff = runtime[0].mFloat; runtime.pop(); // workaround broken endgame scripts that kill dagoth ur if (!R::implicit && ::Misc::StringUtils::ciEqual(ptr.getCellRef().getRefId(), "dagoth_ur_1")) { runtime.push (peek); if (R()(runtime, false, true).isEmpty()) { Log(Debug::Warning) << "Warning: Compensating for broken script in Morrowind.esm by " << "ignoring remote access to dagoth_ur_1"; return; } } MWMechanics::CreatureStats& stats = ptr.getClass().getCreatureStats (ptr); Interpreter::Type_Float current = stats.getDynamic(mIndex).getCurrent(); MWMechanics::DynamicStat stat (ptr.getClass().getCreatureStats (ptr) .getDynamic (mIndex)); stat.setModified (diff + stat.getModified(), 0); stat.setCurrentModified (diff + stat.getCurrentModified()); stat.setCurrent (diff + current); ptr.getClass().getCreatureStats (ptr).setDynamic (mIndex, stat); } }; template class OpModCurrentDynamic : public Interpreter::Opcode0 { int mIndex; public: OpModCurrentDynamic (int index) : mIndex (index) {} void execute (Interpreter::Runtime& runtime) override { MWWorld::Ptr ptr = R()(runtime); Interpreter::Type_Float diff = runtime[0].mFloat; runtime.pop(); MWMechanics::CreatureStats& stats = ptr.getClass().getCreatureStats (ptr); Interpreter::Type_Float current = stats.getDynamic(mIndex).getCurrent(); MWMechanics::DynamicStat stat (ptr.getClass().getCreatureStats (ptr) .getDynamic (mIndex)); bool allowDecreaseBelowZero = false; if (mIndex == 2) // Fatigue-specific logic { // For fatigue, a negative current value is allowed and means the actor will be knocked down allowDecreaseBelowZero = true; // Knock down the actor immediately if a non-positive new value is the case if (diff + current <= 0.f) ptr.getClass().getCreatureStats(ptr).setKnockedDown(true); } stat.setCurrent (diff + current, allowDecreaseBelowZero); ptr.getClass().getCreatureStats (ptr).setDynamic (mIndex, stat); } }; template class OpGetDynamicGetRatio : public Interpreter::Opcode0 { int mIndex; public: OpGetDynamicGetRatio (int index) : mIndex (index) {} void execute (Interpreter::Runtime& runtime) override { MWWorld::Ptr ptr = R()(runtime); MWMechanics::CreatureStats& stats = ptr.getClass().getCreatureStats (ptr); Interpreter::Type_Float value = 0; Interpreter::Type_Float max = stats.getDynamic(mIndex).getModified(); if (max>0) value = stats.getDynamic(mIndex).getCurrent() / max; runtime.push (value); } }; template class OpGetSkill : public Interpreter::Opcode0 { int mIndex; public: OpGetSkill (int index) : mIndex (index) {} void execute (Interpreter::Runtime& runtime) override { MWWorld::Ptr ptr = R()(runtime); Interpreter::Type_Float value = ptr.getClass().getSkill(ptr, mIndex); runtime.push (value); } }; template class OpSetSkill : public Interpreter::Opcode0 { int mIndex; public: OpSetSkill (int index) : mIndex (index) {} void execute (Interpreter::Runtime& runtime) override { MWWorld::Ptr ptr = R()(runtime); Interpreter::Type_Float value = runtime[0].mFloat; runtime.pop(); MWMechanics::NpcStats& stats = ptr.getClass().getNpcStats (ptr); stats.getSkill (mIndex).setBase (value); } }; template class OpModSkill : public Interpreter::Opcode0 { int mIndex; public: OpModSkill (int index) : mIndex (index) {} void execute (Interpreter::Runtime& runtime) override { MWWorld::Ptr ptr = R()(runtime); Interpreter::Type_Float value = runtime[0].mFloat; runtime.pop(); 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)); } }; class OpGetPCCrimeLevel : public Interpreter::Opcode0 { public: void execute (Interpreter::Runtime& runtime) override { MWBase::World *world = MWBase::Environment::get().getWorld(); MWWorld::Ptr player = world->getPlayerPtr(); runtime.push (static_cast (player.getClass().getNpcStats (player).getBounty())); } }; class OpSetPCCrimeLevel : public Interpreter::Opcode0 { public: void execute (Interpreter::Runtime& runtime) override { MWBase::World *world = MWBase::Environment::get().getWorld(); MWWorld::Ptr player = world->getPlayerPtr(); int bounty = static_cast(runtime[0].mFloat); runtime.pop(); player.getClass().getNpcStats (player).setBounty(bounty); if (bounty == 0) MWBase::Environment::get().getWorld()->getPlayer().recordCrimeId(); } }; class OpModPCCrimeLevel : public Interpreter::Opcode0 { public: void execute (Interpreter::Runtime& runtime) override { MWBase::World *world = MWBase::Environment::get().getWorld(); MWWorld::Ptr player = world->getPlayerPtr(); player.getClass().getNpcStats(player).setBounty(static_cast(runtime[0].mFloat) + player.getClass().getNpcStats(player).getBounty()); runtime.pop(); } }; template class OpAddSpell : public Interpreter::Opcode0 { public: void execute (Interpreter::Runtime& runtime) override { MWWorld::Ptr ptr = R()(runtime); std::string id = runtime.getStringLiteral (runtime[0].mInteger); runtime.pop(); const ESM::Spell* spell = MWBase::Environment::get().getWorld()->getStore().get().find (id); MWMechanics::CreatureStats& creatureStats = ptr.getClass().getCreatureStats(ptr); /* Start of tes3mp change (major) Only add the spell if the target doesn't already have it Send an ID_PLAYER_SPELLBOOK packet every time a player gains a spell here */ MWMechanics::Spells &spells = creatureStats.getSpells(); if (!spells.hasSpell(id)) { spells.add(id); if (mwmp::Main::get().getLocalPlayer()->isLoggedIn() && ptr == MWMechanics::getPlayer()) mwmp::Main::get().getLocalPlayer()->sendSpellChange(id, mwmp::SpellbookChanges::ADD); } /* End of tes3mp change (major) */ ESM::Spell::SpellType type = static_cast(spell->mData.mType); if (type != ESM::Spell::ST_Spell && type != ESM::Spell::ST_Power) { // Apply looping particles immediately for constant effects MWBase::Environment::get().getWorld()->applyLoopingParticles(ptr); } } }; template class OpRemoveSpell : public Interpreter::Opcode0 { public: void execute (Interpreter::Runtime& runtime) override { MWWorld::Ptr ptr = R()(runtime); std::string id = runtime.getStringLiteral (runtime[0].mInteger); runtime.pop(); MWMechanics::CreatureStats& creatureStats = ptr.getClass().getCreatureStats(ptr); /* Start of tes3mp change (major) Only remove the spell if the target has it */ MWMechanics::Spells& spells = creatureStats.getSpells(); if (!spells.hasSpell(id)) return; /* End of tes3mp change (major) */ // The spell may have an instant effect which must be handled before the spell's removal. for (const auto& effect : creatureStats.getSpells().getMagicEffects()) { if (effect.second.getMagnitude() <= 0) continue; MWMechanics::CastSpell cast(ptr, ptr); if (cast.applyInstantEffect(ptr, ptr, effect.first, effect.second.getMagnitude())) creatureStats.getSpells().purgeEffect(effect.first.mId); } MWBase::Environment::get().getMechanicsManager()->restoreStatsAfterCorprus(ptr, id); creatureStats.getSpells().remove (id); MWBase::WindowManager* wm = MWBase::Environment::get().getWindowManager(); if (ptr == MWMechanics::getPlayer() && id == wm->getSelectedSpell()) { wm->unsetSelectedSpell(); } /* Start of tes3mp change (major) Send an ID_PLAYER_SPELLBOOK packet every time a player loses a spell here */ if (mwmp::Main::get().getLocalPlayer()->isLoggedIn()) mwmp::Main::get().getLocalPlayer()->sendSpellChange(id, mwmp::SpellbookChanges::REMOVE); /* End of tes3mp change (major) */ } }; template class OpRemoveSpellEffects : public Interpreter::Opcode0 { public: void execute (Interpreter::Runtime& runtime) override { MWWorld::Ptr ptr = R()(runtime); std::string spellid = runtime.getStringLiteral (runtime[0].mInteger); runtime.pop(); ptr.getClass().getCreatureStats (ptr).getActiveSpells().removeEffects(spellid); ptr.getClass().getCreatureStats (ptr).getSpells().removeEffects(spellid); } }; template class OpRemoveEffects : public Interpreter::Opcode0 { public: void execute (Interpreter::Runtime& runtime) override { MWWorld::Ptr ptr = R()(runtime); Interpreter::Type_Integer effectId = runtime[0].mInteger; runtime.pop(); ptr.getClass().getCreatureStats (ptr).getActiveSpells().purgeEffect(effectId); } }; template class OpGetSpell : public Interpreter::Opcode0 { public: void execute (Interpreter::Runtime& runtime) override { MWWorld::Ptr ptr = R()(runtime); std::string id = runtime.getStringLiteral (runtime[0].mInteger); runtime.pop(); Interpreter::Type_Integer value = 0; if (ptr.getClass().isActor() && ptr.getClass().getCreatureStats(ptr).getSpells().hasSpell(id)) value = 1; runtime.push (value); } }; template class OpPCJoinFaction : public Interpreter::Opcode1 { public: void execute (Interpreter::Runtime& runtime, unsigned int arg0) override { MWWorld::ConstPtr actor = R()(runtime, false); std::string factionID = ""; if(arg0==0) { factionID = getDialogueActorFaction(actor); } else { factionID = runtime.getStringLiteral (runtime[0].mInteger); runtime.pop(); } ::Misc::StringUtils::lowerCaseInPlace(factionID); // Make sure this faction exists MWBase::Environment::get().getWorld()->getStore().get().find(factionID); if(factionID != "") { MWWorld::Ptr player = MWMechanics::getPlayer(); player.getClass().getNpcStats(player).joinFaction(factionID); /* Start of tes3mp addition Send an ID_PLAYER_FACTION packet every time a player joins a faction */ int newRank = player.getClass().getNpcStats(player).getFactionRanks().at(factionID); mwmp::Main::get().getLocalPlayer()->sendFactionRank(factionID, newRank); /* End of tes3mp addition */ } } }; template class OpPCRaiseRank : public Interpreter::Opcode1 { public: void execute (Interpreter::Runtime& runtime, unsigned int arg0) override { MWWorld::ConstPtr actor = R()(runtime, false); std::string factionID = ""; if(arg0==0) { factionID = getDialogueActorFaction(actor); } else { factionID = runtime.getStringLiteral (runtime[0].mInteger); runtime.pop(); } ::Misc::StringUtils::lowerCaseInPlace(factionID); // Make sure this faction exists MWBase::Environment::get().getWorld()->getStore().get().find(factionID); if(factionID != "") { MWWorld::Ptr player = MWMechanics::getPlayer(); if(player.getClass().getNpcStats(player).getFactionRanks().find(factionID) == player.getClass().getNpcStats(player).getFactionRanks().end()) { player.getClass().getNpcStats(player).joinFaction(factionID); } else { player.getClass().getNpcStats(player).raiseRank(factionID); } /* Start of tes3mp addition Send an ID_PLAYER_FACTION packet every time a player rises in a faction */ int newRank = player.getClass().getNpcStats(player).getFactionRanks().at(factionID); mwmp::Main::get().getLocalPlayer()->sendFactionRank(factionID, newRank); /* End of tes3mp addition */ } } }; template class OpPCLowerRank : public Interpreter::Opcode1 { public: void execute (Interpreter::Runtime& runtime, unsigned int arg0) override { MWWorld::ConstPtr actor = R()(runtime, false); std::string factionID = ""; if(arg0==0) { factionID = getDialogueActorFaction(actor); } else { factionID = runtime.getStringLiteral (runtime[0].mInteger); runtime.pop(); } ::Misc::StringUtils::lowerCaseInPlace(factionID); // Make sure this faction exists MWBase::Environment::get().getWorld()->getStore().get().find(factionID); if(factionID != "") { MWWorld::Ptr player = MWMechanics::getPlayer(); player.getClass().getNpcStats(player).lowerRank(factionID); /* Start of tes3mp addition Send an ID_PLAYER_FACTION packet every time a player falls in a faction */ int newRank = player.getClass().getNpcStats(player).getFactionRanks().at(factionID); mwmp::Main::get().getLocalPlayer()->sendFactionRank(factionID, newRank); /* End of tes3mp addition */ } } }; template class OpGetPCRank : public Interpreter::Opcode1 { public: void execute (Interpreter::Runtime& runtime, unsigned int arg0) override { MWWorld::ConstPtr ptr = R()(runtime, false); std::string factionID = ""; if(arg0 >0) { factionID = runtime.getStringLiteral (runtime[0].mInteger); runtime.pop(); } else { factionID = ptr.getClass().getPrimaryFaction(ptr); } ::Misc::StringUtils::lowerCaseInPlace(factionID); // Make sure this faction exists MWBase::Environment::get().getWorld()->getStore().get().find(factionID); MWWorld::Ptr player = MWMechanics::getPlayer(); if(factionID!="") { if(player.getClass().getNpcStats(player).getFactionRanks().find(factionID) != player.getClass().getNpcStats(player).getFactionRanks().end()) { runtime.push(player.getClass().getNpcStats(player).getFactionRanks().at(factionID)); } else { runtime.push(-1); } } else { runtime.push(-1); } } }; template class OpModDisposition : public Interpreter::Opcode0 { public: void execute (Interpreter::Runtime& runtime) override { MWWorld::Ptr ptr = R()(runtime); Interpreter::Type_Integer value = runtime[0].mInteger; runtime.pop(); if (ptr.getClass().isNpc()) ptr.getClass().getNpcStats (ptr).setBaseDisposition (ptr.getClass().getNpcStats (ptr).getBaseDisposition() + value); // else: must not throw exception (used by an Almalexia dialogue script) } }; template class OpSetDisposition : public Interpreter::Opcode0 { public: void execute (Interpreter::Runtime& runtime) override { MWWorld::Ptr ptr = R()(runtime); Interpreter::Type_Integer value = runtime[0].mInteger; runtime.pop(); if (ptr.getClass().isNpc()) ptr.getClass().getNpcStats (ptr).setBaseDisposition (value); } }; template class OpGetDisposition : public Interpreter::Opcode0 { public: void execute (Interpreter::Runtime& runtime) override { MWWorld::Ptr ptr = R()(runtime); if (!ptr.getClass().isNpc()) runtime.push(0); else runtime.push (MWBase::Environment::get().getMechanicsManager()->getDerivedDisposition(ptr)); } }; class OpGetDeadCount : public Interpreter::Opcode0 { public: void execute (Interpreter::Runtime& runtime) override { std::string id = runtime.getStringLiteral (runtime[0].mInteger); runtime[0].mInteger = MWBase::Environment::get().getMechanicsManager()->countDeaths (id); } }; template class OpGetPCFacRep : public Interpreter::Opcode1 { public: void execute (Interpreter::Runtime& runtime, unsigned int arg0) override { MWWorld::ConstPtr ptr = R()(runtime, false); std::string factionId; if (arg0==1) { factionId = runtime.getStringLiteral (runtime[0].mInteger); runtime.pop(); } else { factionId = getDialogueActorFaction(ptr); } if (factionId.empty()) throw std::runtime_error ("failed to determine faction"); ::Misc::StringUtils::lowerCaseInPlace (factionId); MWWorld::Ptr player = MWMechanics::getPlayer(); runtime.push ( player.getClass().getNpcStats (player).getFactionReputation (factionId)); } }; template class OpSetPCFacRep : public Interpreter::Opcode1 { public: void execute (Interpreter::Runtime& runtime, unsigned int arg0) override { MWWorld::ConstPtr ptr = R()(runtime, false); Interpreter::Type_Integer value = runtime[0].mInteger; runtime.pop(); std::string factionId; if (arg0==1) { factionId = runtime.getStringLiteral (runtime[0].mInteger); runtime.pop(); } else { factionId = getDialogueActorFaction(ptr); } if (factionId.empty()) throw std::runtime_error ("failed to determine faction"); ::Misc::StringUtils::lowerCaseInPlace (factionId); MWWorld::Ptr player = MWMechanics::getPlayer(); player.getClass().getNpcStats (player).setFactionReputation (factionId, value); /* Start of tes3mp addition Send an ID_PLAYER_FACTION packet every time a player's faction reputation changes */ mwmp::Main::get().getLocalPlayer()->sendFactionReputation(Misc::StringUtils::lowerCase(factionId), value); /* End of tes3mp addition */ } }; template class OpModPCFacRep : public Interpreter::Opcode1 { public: void execute (Interpreter::Runtime& runtime, unsigned int arg0) override { MWWorld::ConstPtr ptr = R()(runtime, false); Interpreter::Type_Integer value = runtime[0].mInteger; runtime.pop(); std::string factionId; if (arg0==1) { factionId = runtime.getStringLiteral (runtime[0].mInteger); runtime.pop(); } else { factionId = getDialogueActorFaction(ptr); } if (factionId.empty()) throw std::runtime_error ("failed to determine faction"); ::Misc::StringUtils::lowerCaseInPlace (factionId); MWWorld::Ptr player = MWMechanics::getPlayer(); player.getClass().getNpcStats (player).setFactionReputation (factionId, player.getClass().getNpcStats (player).getFactionReputation (factionId)+ value); /* Start of tes3mp addition Send an ID_PLAYER_FACTION packet every time a player's faction reputation changes */ int newReputation = player.getClass().getNpcStats(player).getFactionReputation(factionId); mwmp::Main::get().getLocalPlayer()->sendFactionReputation(Misc::StringUtils::lowerCase(factionId), newReputation); /* End of tes3mp addition */ } }; template class OpGetCommonDisease : public Interpreter::Opcode0 { public: void execute (Interpreter::Runtime& runtime) override { MWWorld::Ptr ptr = R()(runtime); runtime.push (ptr.getClass().getCreatureStats (ptr).hasCommonDisease()); } }; template class OpGetBlightDisease : public Interpreter::Opcode0 { public: void execute (Interpreter::Runtime& runtime) override { MWWorld::Ptr ptr = R()(runtime); runtime.push (ptr.getClass().getCreatureStats (ptr).hasBlightDisease()); } }; template class OpGetRace : public Interpreter::Opcode0 { public: void execute (Interpreter::Runtime& runtime) override { MWWorld::ConstPtr ptr = R()(runtime); std::string race = runtime.getStringLiteral(runtime[0].mInteger); ::Misc::StringUtils::lowerCaseInPlace(race); runtime.pop(); std::string npcRace = ptr.get()->mBase->mRace; ::Misc::StringUtils::lowerCaseInPlace(npcRace); runtime.push (npcRace == race); } }; class OpGetWerewolfKills : public Interpreter::Opcode0 { public: void execute (Interpreter::Runtime& runtime) override { MWWorld::Ptr ptr = MWBase::Environment::get().getWorld ()->getPlayerPtr(); runtime.push (ptr.getClass().getNpcStats (ptr).getWerewolfKills ()); } }; template class OpPcExpelled : public Interpreter::Opcode1 { public: void execute (Interpreter::Runtime& runtime, unsigned int arg0) override { MWWorld::ConstPtr ptr = R()(runtime, false); std::string factionID = ""; if(arg0 >0 ) { factionID = runtime.getStringLiteral (runtime[0].mInteger); runtime.pop(); } else { factionID = ptr.getClass().getPrimaryFaction(ptr); } ::Misc::StringUtils::lowerCaseInPlace(factionID); MWWorld::Ptr player = MWMechanics::getPlayer(); if(factionID!="") { runtime.push(player.getClass().getNpcStats(player).getExpelled(factionID)); } else { runtime.push(0); } } }; template class OpPcExpell : public Interpreter::Opcode1 { public: void execute (Interpreter::Runtime& runtime, unsigned int arg0) override { MWWorld::ConstPtr ptr = R()(runtime, false); std::string factionID = ""; if(arg0 >0 ) { factionID = runtime.getStringLiteral (runtime[0].mInteger); runtime.pop(); } else { factionID = ptr.getClass().getPrimaryFaction(ptr); } MWWorld::Ptr player = MWMechanics::getPlayer(); if(factionID!="") { player.getClass().getNpcStats(player).expell(factionID); /* Start of tes3mp addition Send an ID_PLAYER_FACTION packet every time a player is expelled from a faction */ mwmp::Main::get().getLocalPlayer()->sendFactionExpulsionState(Misc::StringUtils::lowerCase(factionID), true); /* End of tes3mp addition */ } } }; template class OpPcClearExpelled : public Interpreter::Opcode1 { public: void execute (Interpreter::Runtime& runtime, unsigned int arg0) override { MWWorld::ConstPtr ptr = R()(runtime, false); std::string factionID = ""; if(arg0 >0 ) { factionID = runtime.getStringLiteral (runtime[0].mInteger); runtime.pop(); } else { factionID = ptr.getClass().getPrimaryFaction(ptr); } MWWorld::Ptr player = MWMechanics::getPlayer(); if(factionID!="") player.getClass().getNpcStats(player).clearExpelled(factionID); /* Start of tes3mp addition Send an ID_PLAYER_FACTION packet every time a player is no longer expelled from a faction */ if (factionID != "") mwmp::Main::get().getLocalPlayer()->sendFactionExpulsionState(Misc::StringUtils::lowerCase(factionID), false); /* End of tes3mp addition */ } }; template class OpRaiseRank : public Interpreter::Opcode0 { public: void execute (Interpreter::Runtime& runtime) override { MWWorld::Ptr ptr = R()(runtime); std::string factionID = ptr.getClass().getPrimaryFaction(ptr); if(factionID.empty()) return; MWWorld::Ptr player = MWMechanics::getPlayer(); // no-op when executed on the player if (ptr == player) return; // If we already changed rank for this NPC, modify current rank in the NPC stats. // Otherwise take rank from base NPC record, increase it and put it to NPC data. int currentRank = ptr.getClass().getNpcStats(ptr).getFactionRank(factionID); if (currentRank >= 0) ptr.getClass().getNpcStats(ptr).raiseRank(factionID); else { int rank = ptr.getClass().getPrimaryFactionRank(ptr); rank++; ptr.getClass().getNpcStats(ptr).joinFaction(factionID); for (int i=0; i class OpLowerRank : public Interpreter::Opcode0 { public: void execute (Interpreter::Runtime& runtime) override { MWWorld::Ptr ptr = R()(runtime); std::string factionID = ptr.getClass().getPrimaryFaction(ptr); if(factionID.empty()) return; MWWorld::Ptr player = MWMechanics::getPlayer(); // no-op when executed on the player if (ptr == player) return; // If we already changed rank for this NPC, modify current rank in the NPC stats. // Otherwise take rank from base NPC record, decrease it and put it to NPC data. int currentRank = ptr.getClass().getNpcStats(ptr).getFactionRank(factionID); if (currentRank == 0) return; else if (currentRank > 0) ptr.getClass().getNpcStats(ptr).lowerRank(factionID); else { int rank = ptr.getClass().getPrimaryFactionRank(ptr); rank--; ptr.getClass().getNpcStats(ptr).joinFaction(factionID); for (int i=0; i class OpOnDeath : public Interpreter::Opcode0 { public: void execute (Interpreter::Runtime& runtime) override { MWWorld::Ptr ptr = R()(runtime); Interpreter::Type_Integer value = ptr.getClass().getCreatureStats (ptr).hasDied(); if (value) ptr.getClass().getCreatureStats (ptr).clearHasDied(); runtime.push (value); } }; template class OpOnMurder : public Interpreter::Opcode0 { public: void execute (Interpreter::Runtime& runtime) override { MWWorld::Ptr ptr = R()(runtime); Interpreter::Type_Integer value = ptr.getClass().getCreatureStats (ptr).hasBeenMurdered(); if (value) ptr.getClass().getCreatureStats (ptr).clearHasBeenMurdered(); runtime.push (value); } }; template class OpOnKnockout : public Interpreter::Opcode0 { public: void execute (Interpreter::Runtime& runtime) override { MWWorld::Ptr ptr = R()(runtime); Interpreter::Type_Integer value = ptr.getClass().getCreatureStats (ptr).getKnockedDownOneFrame(); runtime.push (value); } }; template class OpIsWerewolf : public Interpreter::Opcode0 { public: void execute (Interpreter::Runtime& runtime) override { MWWorld::Ptr ptr = R()(runtime); runtime.push(ptr.getClass().getNpcStats(ptr).isWerewolf()); } }; template class OpSetWerewolf : public Interpreter::Opcode0 { public: void execute (Interpreter::Runtime& runtime) override { MWWorld::Ptr ptr = R()(runtime); MWBase::Environment::get().getMechanicsManager()->setWerewolf(ptr, set); /* Start of tes3mp addition When the player's werewolf state changes, send an ID_PLAYER_SHAPESHIFT packet */ if (ptr == MWMechanics::getPlayer()) mwmp::Main::get().getLocalPlayer()->sendWerewolfState(set); /* End of tes3mp addition */ } }; template class OpSetWerewolfAcrobatics : public Interpreter::Opcode0 { public: void execute (Interpreter::Runtime& runtime) override { MWWorld::Ptr ptr = R()(runtime); MWBase::Environment::get().getMechanicsManager()->applyWerewolfAcrobatics(ptr); } }; template class OpResurrect : public Interpreter::Opcode0 { public: void execute (Interpreter::Runtime& runtime) override { MWWorld::Ptr ptr = R()(runtime); if (ptr == MWMechanics::getPlayer()) { MWBase::Environment::get().getMechanicsManager()->resurrect(ptr); if (MWBase::Environment::get().getStateManager()->getState() == MWBase::StateManager::State_Ended) MWBase::Environment::get().getStateManager()->resumeGame(); } else if (ptr.getClass().getCreatureStats(ptr).isDead()) { bool wasEnabled = ptr.getRefData().isEnabled(); MWBase::Environment::get().getWorld()->undeleteObject(ptr); MWBase::Environment::get().getWorld()->removeContainerScripts(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 (wasEnabled) MWBase::Environment::get().getWorld()->enable(ptr); } } }; template class OpGetStat : public Interpreter::Opcode0 { public: void execute (Interpreter::Runtime& runtime) override { // dummy runtime.push(0); } }; template class OpGetMagicEffect : public Interpreter::Opcode0 { int mPositiveEffect; int mNegativeEffect; public: OpGetMagicEffect (int positiveEffect, int negativeEffect) : mPositiveEffect(positiveEffect) , mNegativeEffect(negativeEffect) { } void execute (Interpreter::Runtime& runtime) override { MWWorld::Ptr ptr = R()(runtime); const MWMechanics::MagicEffects& effects = ptr.getClass().getCreatureStats(ptr).getMagicEffects(); float currentValue = effects.get(mPositiveEffect).getMagnitude(); if (mNegativeEffect != -1) currentValue -= effects.get(mNegativeEffect).getMagnitude(); // GetResist* should take in account elemental shields if (mPositiveEffect == ESM::MagicEffect::ResistFire) currentValue += effects.get(ESM::MagicEffect::FireShield).getMagnitude(); if (mPositiveEffect == ESM::MagicEffect::ResistShock) currentValue += effects.get(ESM::MagicEffect::LightningShield).getMagnitude(); if (mPositiveEffect == ESM::MagicEffect::ResistFrost) currentValue += effects.get(ESM::MagicEffect::FrostShield).getMagnitude(); int ret = static_cast(currentValue); runtime.push(ret); } }; template class OpSetMagicEffect : public Interpreter::Opcode0 { int mPositiveEffect; int mNegativeEffect; public: OpSetMagicEffect (int positiveEffect, int negativeEffect) : mPositiveEffect(positiveEffect) , mNegativeEffect(negativeEffect) { } void execute(Interpreter::Runtime &runtime) override { MWWorld::Ptr ptr = R()(runtime); MWMechanics::MagicEffects& effects = ptr.getClass().getCreatureStats(ptr).getMagicEffects(); float currentValue = effects.get(mPositiveEffect).getMagnitude(); if (mNegativeEffect != -1) currentValue -= effects.get(mNegativeEffect).getMagnitude(); // SetResist* should take in account elemental shields if (mPositiveEffect == ESM::MagicEffect::ResistFire) currentValue += effects.get(ESM::MagicEffect::FireShield).getMagnitude(); if (mPositiveEffect == ESM::MagicEffect::ResistShock) currentValue += effects.get(ESM::MagicEffect::LightningShield).getMagnitude(); if (mPositiveEffect == ESM::MagicEffect::ResistFrost) currentValue += effects.get(ESM::MagicEffect::FrostShield).getMagnitude(); int arg = runtime[0].mInteger; runtime.pop(); effects.modifyBase(mPositiveEffect, (arg - static_cast(currentValue))); } }; template class OpModMagicEffect : public Interpreter::Opcode0 { int mPositiveEffect; int mNegativeEffect; public: OpModMagicEffect (int positiveEffect, int negativeEffect) : mPositiveEffect(positiveEffect) , mNegativeEffect(negativeEffect) { } void execute(Interpreter::Runtime &runtime) override { MWWorld::Ptr ptr = R()(runtime); MWMechanics::CreatureStats& stats = ptr.getClass().getCreatureStats(ptr); int arg = runtime[0].mInteger; runtime.pop(); stats.getMagicEffects().modifyBase(mPositiveEffect, arg); } }; struct MagicEffect { int mPositiveEffect; int mNegativeEffect; }; void installOpcodes (Interpreter::Interpreter& interpreter) { for (int i=0; i (i)); interpreter.installSegment5 (Compiler::Stats::opcodeGetAttributeExplicit+i, new OpGetAttribute (i)); interpreter.installSegment5 (Compiler::Stats::opcodeSetAttribute+i, new OpSetAttribute (i)); interpreter.installSegment5 (Compiler::Stats::opcodeSetAttributeExplicit+i, new OpSetAttribute (i)); interpreter.installSegment5 (Compiler::Stats::opcodeModAttribute+i, new OpModAttribute (i)); interpreter.installSegment5 (Compiler::Stats::opcodeModAttributeExplicit+i, new OpModAttribute (i)); } for (int i=0; i (i)); interpreter.installSegment5 (Compiler::Stats::opcodeGetDynamicExplicit+i, new OpGetDynamic (i)); interpreter.installSegment5 (Compiler::Stats::opcodeSetDynamic+i, new OpSetDynamic (i)); interpreter.installSegment5 (Compiler::Stats::opcodeSetDynamicExplicit+i, new OpSetDynamic (i)); interpreter.installSegment5 (Compiler::Stats::opcodeModDynamic+i, new OpModDynamic (i)); interpreter.installSegment5 (Compiler::Stats::opcodeModDynamicExplicit+i, new OpModDynamic (i)); interpreter.installSegment5 (Compiler::Stats::opcodeModCurrentDynamic+i, new OpModCurrentDynamic (i)); interpreter.installSegment5 (Compiler::Stats::opcodeModCurrentDynamicExplicit+i, new OpModCurrentDynamic (i)); interpreter.installSegment5 (Compiler::Stats::opcodeGetDynamicGetRatio+i, new OpGetDynamicGetRatio (i)); interpreter.installSegment5 (Compiler::Stats::opcodeGetDynamicGetRatioExplicit+i, new OpGetDynamicGetRatio (i)); } for (int i=0; i (i)); interpreter.installSegment5 (Compiler::Stats::opcodeGetSkillExplicit+i, new OpGetSkill (i)); interpreter.installSegment5 (Compiler::Stats::opcodeSetSkill+i, new OpSetSkill (i)); interpreter.installSegment5 (Compiler::Stats::opcodeSetSkillExplicit+i, new OpSetSkill (i)); interpreter.installSegment5 (Compiler::Stats::opcodeModSkill+i, new OpModSkill (i)); interpreter.installSegment5 (Compiler::Stats::opcodeModSkillExplicit+i, new OpModSkill (i)); } interpreter.installSegment5 (Compiler::Stats::opcodeGetPCCrimeLevel, new OpGetPCCrimeLevel); interpreter.installSegment5 (Compiler::Stats::opcodeSetPCCrimeLevel, new OpSetPCCrimeLevel); interpreter.installSegment5 (Compiler::Stats::opcodeModPCCrimeLevel, new OpModPCCrimeLevel); interpreter.installSegment5 (Compiler::Stats::opcodeAddSpell, new OpAddSpell); interpreter.installSegment5 (Compiler::Stats::opcodeAddSpellExplicit, new OpAddSpell); interpreter.installSegment5 (Compiler::Stats::opcodeRemoveSpell, new OpRemoveSpell); interpreter.installSegment5 (Compiler::Stats::opcodeRemoveSpellExplicit, new OpRemoveSpell); interpreter.installSegment5 (Compiler::Stats::opcodeRemoveSpellEffects, new OpRemoveSpellEffects); interpreter.installSegment5 (Compiler::Stats::opcodeRemoveSpellEffectsExplicit, new OpRemoveSpellEffects); interpreter.installSegment5 (Compiler::Stats::opcodeResurrect, new OpResurrect); interpreter.installSegment5 (Compiler::Stats::opcodeResurrectExplicit, new OpResurrect); interpreter.installSegment5 (Compiler::Stats::opcodeRemoveEffects, new OpRemoveEffects); interpreter.installSegment5 (Compiler::Stats::opcodeRemoveEffectsExplicit, new OpRemoveEffects); interpreter.installSegment5 (Compiler::Stats::opcodeGetSpell, new OpGetSpell); interpreter.installSegment5 (Compiler::Stats::opcodeGetSpellExplicit, new OpGetSpell); interpreter.installSegment3(Compiler::Stats::opcodePCRaiseRank,new OpPCRaiseRank); interpreter.installSegment3(Compiler::Stats::opcodePCLowerRank,new OpPCLowerRank); interpreter.installSegment3(Compiler::Stats::opcodePCJoinFaction,new OpPCJoinFaction); interpreter.installSegment3(Compiler::Stats::opcodePCRaiseRankExplicit,new OpPCRaiseRank); interpreter.installSegment3(Compiler::Stats::opcodePCLowerRankExplicit,new OpPCLowerRank); interpreter.installSegment3(Compiler::Stats::opcodePCJoinFactionExplicit,new OpPCJoinFaction); interpreter.installSegment3(Compiler::Stats::opcodeGetPCRank,new OpGetPCRank); interpreter.installSegment3(Compiler::Stats::opcodeGetPCRankExplicit,new OpGetPCRank); interpreter.installSegment5(Compiler::Stats::opcodeModDisposition,new OpModDisposition); interpreter.installSegment5(Compiler::Stats::opcodeModDispositionExplicit,new OpModDisposition); interpreter.installSegment5(Compiler::Stats::opcodeSetDisposition,new OpSetDisposition); interpreter.installSegment5(Compiler::Stats::opcodeSetDispositionExplicit,new OpSetDisposition); interpreter.installSegment5(Compiler::Stats::opcodeGetDisposition,new OpGetDisposition); interpreter.installSegment5(Compiler::Stats::opcodeGetDispositionExplicit,new OpGetDisposition); interpreter.installSegment5 (Compiler::Stats::opcodeGetLevel, new OpGetLevel); interpreter.installSegment5 (Compiler::Stats::opcodeGetLevelExplicit, new OpGetLevel); interpreter.installSegment5 (Compiler::Stats::opcodeSetLevel, new OpSetLevel); interpreter.installSegment5 (Compiler::Stats::opcodeSetLevelExplicit, new OpSetLevel); interpreter.installSegment5 (Compiler::Stats::opcodeGetDeadCount, new OpGetDeadCount); interpreter.installSegment3 (Compiler::Stats::opcodeGetPCFacRep, new OpGetPCFacRep); interpreter.installSegment3 (Compiler::Stats::opcodeGetPCFacRepExplicit, new OpGetPCFacRep); interpreter.installSegment3 (Compiler::Stats::opcodeSetPCFacRep, new OpSetPCFacRep); interpreter.installSegment3 (Compiler::Stats::opcodeSetPCFacRepExplicit, new OpSetPCFacRep); interpreter.installSegment3 (Compiler::Stats::opcodeModPCFacRep, new OpModPCFacRep); interpreter.installSegment3 (Compiler::Stats::opcodeModPCFacRepExplicit, new OpModPCFacRep); interpreter.installSegment5 (Compiler::Stats::opcodeGetCommonDisease, new OpGetCommonDisease); interpreter.installSegment5 (Compiler::Stats::opcodeGetCommonDiseaseExplicit, new OpGetCommonDisease); interpreter.installSegment5 (Compiler::Stats::opcodeGetBlightDisease, new OpGetBlightDisease); interpreter.installSegment5 (Compiler::Stats::opcodeGetBlightDiseaseExplicit, new OpGetBlightDisease); interpreter.installSegment5 (Compiler::Stats::opcodeGetRace, new OpGetRace); interpreter.installSegment5 (Compiler::Stats::opcodeGetRaceExplicit, new OpGetRace); interpreter.installSegment5 (Compiler::Stats::opcodeGetWerewolfKills, new OpGetWerewolfKills); interpreter.installSegment3 (Compiler::Stats::opcodePcExpelled, new OpPcExpelled); interpreter.installSegment3 (Compiler::Stats::opcodePcExpelledExplicit, new OpPcExpelled); interpreter.installSegment3 (Compiler::Stats::opcodePcExpell, new OpPcExpell); interpreter.installSegment3 (Compiler::Stats::opcodePcExpellExplicit, new OpPcExpell); interpreter.installSegment3 (Compiler::Stats::opcodePcClearExpelled, new OpPcClearExpelled); interpreter.installSegment3 (Compiler::Stats::opcodePcClearExpelledExplicit, new OpPcClearExpelled); interpreter.installSegment5 (Compiler::Stats::opcodeRaiseRank, new OpRaiseRank); interpreter.installSegment5 (Compiler::Stats::opcodeRaiseRankExplicit, new OpRaiseRank); interpreter.installSegment5 (Compiler::Stats::opcodeLowerRank, new OpLowerRank); interpreter.installSegment5 (Compiler::Stats::opcodeLowerRankExplicit, new OpLowerRank); interpreter.installSegment5 (Compiler::Stats::opcodeOnDeath, new OpOnDeath); interpreter.installSegment5 (Compiler::Stats::opcodeOnDeathExplicit, new OpOnDeath); interpreter.installSegment5 (Compiler::Stats::opcodeOnMurder, new OpOnMurder); interpreter.installSegment5 (Compiler::Stats::opcodeOnMurderExplicit, new OpOnMurder); interpreter.installSegment5 (Compiler::Stats::opcodeOnKnockout, new OpOnKnockout); interpreter.installSegment5 (Compiler::Stats::opcodeOnKnockoutExplicit, new OpOnKnockout); interpreter.installSegment5 (Compiler::Stats::opcodeIsWerewolf, new OpIsWerewolf); interpreter.installSegment5 (Compiler::Stats::opcodeIsWerewolfExplicit, new OpIsWerewolf); interpreter.installSegment5 (Compiler::Stats::opcodeBecomeWerewolf, new OpSetWerewolf); interpreter.installSegment5 (Compiler::Stats::opcodeBecomeWerewolfExplicit, new OpSetWerewolf); interpreter.installSegment5 (Compiler::Stats::opcodeUndoWerewolf, new OpSetWerewolf); interpreter.installSegment5 (Compiler::Stats::opcodeUndoWerewolfExplicit, new OpSetWerewolf); interpreter.installSegment5 (Compiler::Stats::opcodeSetWerewolfAcrobatics, new OpSetWerewolfAcrobatics); interpreter.installSegment5 (Compiler::Stats::opcodeSetWerewolfAcrobaticsExplicit, new OpSetWerewolfAcrobatics); interpreter.installSegment5 (Compiler::Stats::opcodeGetStat, new OpGetStat); interpreter.installSegment5 (Compiler::Stats::opcodeGetStatExplicit, new OpGetStat); static const MagicEffect sMagicEffects[] = { { ESM::MagicEffect::ResistMagicka, ESM::MagicEffect::WeaknessToMagicka }, { ESM::MagicEffect::ResistFire, ESM::MagicEffect::WeaknessToFire }, { ESM::MagicEffect::ResistFrost, ESM::MagicEffect::WeaknessToFrost }, { ESM::MagicEffect::ResistShock, ESM::MagicEffect::WeaknessToShock }, { ESM::MagicEffect::ResistCommonDisease, ESM::MagicEffect::WeaknessToCommonDisease }, { ESM::MagicEffect::ResistBlightDisease, ESM::MagicEffect::WeaknessToBlightDisease }, { ESM::MagicEffect::ResistCorprusDisease, ESM::MagicEffect::WeaknessToCorprusDisease }, { ESM::MagicEffect::ResistPoison, ESM::MagicEffect::WeaknessToPoison }, { ESM::MagicEffect::ResistParalysis, -1 }, { ESM::MagicEffect::ResistNormalWeapons, ESM::MagicEffect::WeaknessToNormalWeapons }, { ESM::MagicEffect::WaterBreathing, -1 }, { ESM::MagicEffect::Chameleon, -1 }, { ESM::MagicEffect::WaterWalking, -1 }, { ESM::MagicEffect::SwiftSwim, -1 }, { ESM::MagicEffect::Jump, -1 }, { ESM::MagicEffect::Levitate, -1 }, { ESM::MagicEffect::Shield, -1 }, { ESM::MagicEffect::Sound, -1 }, { ESM::MagicEffect::Silence, -1 }, { ESM::MagicEffect::Blind, -1 }, { ESM::MagicEffect::Paralyze, -1 }, { ESM::MagicEffect::Invisibility, -1 }, { ESM::MagicEffect::FortifyAttack, -1 }, { ESM::MagicEffect::Sanctuary, -1 }, }; for (int i=0; i<24; ++i) { int positive = sMagicEffects[i].mPositiveEffect; int negative = sMagicEffects[i].mNegativeEffect; interpreter.installSegment5 (Compiler::Stats::opcodeGetMagicEffect+i, new OpGetMagicEffect (positive, negative)); interpreter.installSegment5 (Compiler::Stats::opcodeGetMagicEffectExplicit+i, new OpGetMagicEffect (positive, negative)); interpreter.installSegment5 (Compiler::Stats::opcodeSetMagicEffect+i, new OpSetMagicEffect (positive, negative)); interpreter.installSegment5 (Compiler::Stats::opcodeSetMagicEffectExplicit+i, new OpSetMagicEffect (positive, negative)); interpreter.installSegment5 (Compiler::Stats::opcodeModMagicEffect+i, new OpModMagicEffect (positive, negative)); interpreter.installSegment5 (Compiler::Stats::opcodeModMagicEffectExplicit+i, new OpModMagicEffect (positive, negative)); } } } } ================================================ FILE: apps/openmw/mwscript/statsextensions.hpp ================================================ #ifndef GAME_SCRIPT_STATSEXTENSIONS_H #define GAME_SCRIPT_STATSEXTENSIONS_H namespace Compiler { class Extensions; } namespace Interpreter { class Interpreter; } namespace MWScript { /// \brief stats-related script functionality (creatures and NPCs) namespace Stats { void installOpcodes (Interpreter::Interpreter& interpreter); } } #endif ================================================ FILE: apps/openmw/mwscript/transformationextensions.cpp ================================================ #include /* Start of tes3mp addition Include additional headers for multiplayer purposes */ #include #include "../mwbase/windowmanager.hpp" #include "../mwmp/Main.hpp" #include "../mwmp/Networking.hpp" #include "../mwmp/LocalPlayer.hpp" #include "../mwmp/PlayerList.hpp" #include "../mwmp/ObjectList.hpp" #include "../mwmp/CellController.hpp" #include "../mwmp/ScriptController.hpp" /* End of tes3mp addition */ #include #include #include #include #include #include #include "../mwbase/environment.hpp" #include "../mwbase/world.hpp" #include "../mwworld/cellstore.hpp" #include "../mwworld/class.hpp" #include "../mwworld/manualref.hpp" #include "../mwworld/player.hpp" #include "../mwmechanics/actorutil.hpp" #include "interpretercontext.hpp" #include "ref.hpp" namespace MWScript { namespace Transformation { void moveStandingActors(const MWWorld::Ptr &ptr, const osg::Vec3f& diff) { std::vector actors; MWBase::Environment::get().getWorld()->getActorsStandingOn (ptr, actors); for (auto& actor : actors) MWBase::Environment::get().getWorld()->moveObjectBy(actor, diff, false, false); } template class OpGetDistance : public Interpreter::Opcode0 { public: void execute (Interpreter::Runtime& runtime) override { MWWorld::Ptr from = R()(runtime); std::string name = runtime.getStringLiteral (runtime[0].mInteger); runtime.pop(); if (from.getContainerStore()) // is the object contained? { MWWorld::Ptr container = MWBase::Environment::get().getWorld()->findContainer(from); if (!container.isEmpty()) from = container; else { std::string error = "Failed to find the container of object '" + from.getCellRef().getRefId() + "'"; runtime.getContext().report(error); Log(Debug::Error) << error; runtime.push(0.f); return; } } const MWWorld::Ptr to = MWBase::Environment::get().getWorld()->searchPtr(name, false); if (to.isEmpty()) { std::string error = "Failed to find an instance of object '" + name + "'"; runtime.getContext().report(error); Log(Debug::Error) << error; runtime.push(0.f); return; } float distance; // If the objects are in different worldspaces, return a large value (just like vanilla) if (!to.isInCell() || !from.isInCell() || to.getCell()->getCell()->getCellId().mWorldspace != from.getCell()->getCell()->getCellId().mWorldspace) distance = std::numeric_limits::max(); else { double diff[3]; const float* const pos1 = to.getRefData().getPosition().pos; const float* const pos2 = from.getRefData().getPosition().pos; for (int i=0; i<3; ++i) diff[i] = pos1[i] - pos2[i]; distance = static_cast(std::sqrt(diff[0] * diff[0] + diff[1] * diff[1] + diff[2] * diff[2])); } runtime.push(distance); } }; template class OpSetScale : public Interpreter::Opcode0 { public: void execute (Interpreter::Runtime& runtime) override { MWWorld::Ptr ptr = R()(runtime); Interpreter::Type_Float scale = runtime[0].mFloat; runtime.pop(); /* Start of tes3mp addition Prevent players from changing their own scale Send an ID_OBJECT_SCALE every time an object's scale is changed through a script */ if (ptr == MWMechanics::getPlayer()) { MWBase::Environment::get().getWindowManager()-> messageBox("You can't change your own scale in multiplayer. Only the server can."); } else if (mwmp::Main::get().getLocalPlayer()->isLoggedIn() && ptr.isInCell() && ptr.getCellRef().getScale() != scale) { // Ignore attempts to change another player's scale if (mwmp::PlayerList::isDedicatedPlayer(ptr)) { MWBase::Environment::get().getWindowManager()-> messageBox("You can't change the scales of other players. Only the server can."); } else { mwmp::ObjectList *objectList = mwmp::Main::get().getNetworking()->getObjectList(); objectList->reset(); objectList->packetOrigin = ScriptController::getPacketOriginFromContextType(runtime.getContext().getContextType()); objectList->originClientScript = runtime.getContext().getCurrentScriptName(); objectList->addObjectScale(ptr, scale); objectList->sendObjectScale(); } } /* End of tes3mp addition */ /* Start of tes3mp change (major) Disable unilateral scaling on this client and expect the server's reply to our packet to do it instead */ //MWBase::Environment::get().getWorld()->scaleObject(ptr,scale); /* End of tes3mp change (major) */ } }; template class OpGetScale : public Interpreter::Opcode0 { public: void execute (Interpreter::Runtime& runtime) override { MWWorld::Ptr ptr = R()(runtime); runtime.push(ptr.getCellRef().getScale()); } }; template class OpModScale : public Interpreter::Opcode0 { public: void execute (Interpreter::Runtime& runtime) override { MWWorld::Ptr ptr = R()(runtime); Interpreter::Type_Float scale = runtime[0].mFloat; runtime.pop(); // add the parameter to the object's scale. MWBase::Environment::get().getWorld()->scaleObject(ptr,ptr.getCellRef().getScale() + scale); } }; template class OpSetAngle : public Interpreter::Opcode0 { public: void execute (Interpreter::Runtime& runtime) override { MWWorld::Ptr ptr = R()(runtime); std::string axis = runtime.getStringLiteral (runtime[0].mInteger); runtime.pop(); Interpreter::Type_Float angle = osg::DegreesToRadians(runtime[0].mFloat); runtime.pop(); float ax = ptr.getRefData().getPosition().rot[0]; float ay = ptr.getRefData().getPosition().rot[1]; float az = ptr.getRefData().getPosition().rot[2]; // XYZ axis use the inverse (XYZ) rotation order like vanilla SetAngle. // UWV axis use the standard (ZYX) rotation order like TESCS/OpenMW-CS and the rest of the game. if (axis == "x") MWBase::Environment::get().getWorld()->rotateObject(ptr,angle,ay,az,MWBase::RotationFlag_inverseOrder); else if (axis == "y") MWBase::Environment::get().getWorld()->rotateObject(ptr,ax,angle,az,MWBase::RotationFlag_inverseOrder); else if (axis == "z") MWBase::Environment::get().getWorld()->rotateObject(ptr,ax,ay,angle,MWBase::RotationFlag_inverseOrder); else if (axis == "u") MWBase::Environment::get().getWorld()->rotateObject(ptr,angle,ay,az,MWBase::RotationFlag_none); else if (axis == "w") MWBase::Environment::get().getWorld()->rotateObject(ptr,ax,angle,az,MWBase::RotationFlag_none); else if (axis == "v") MWBase::Environment::get().getWorld()->rotateObject(ptr,ax,ay,angle,MWBase::RotationFlag_none); } }; template class OpGetStartingAngle : public Interpreter::Opcode0 { public: void execute (Interpreter::Runtime& runtime) override { MWWorld::Ptr ptr = R()(runtime); std::string axis = runtime.getStringLiteral (runtime[0].mInteger); runtime.pop(); if (axis == "x") { runtime.push(osg::RadiansToDegrees(ptr.getCellRef().getPosition().rot[0])); } else if (axis == "y") { runtime.push(osg::RadiansToDegrees(ptr.getCellRef().getPosition().rot[1])); } else if (axis == "z") { runtime.push(osg::RadiansToDegrees(ptr.getCellRef().getPosition().rot[2])); } } }; template class OpGetAngle : public Interpreter::Opcode0 { public: void execute (Interpreter::Runtime& runtime) override { MWWorld::Ptr ptr = R()(runtime); std::string axis = runtime.getStringLiteral (runtime[0].mInteger); runtime.pop(); if (axis=="x") { runtime.push(osg::RadiansToDegrees(ptr.getRefData().getPosition().rot[0])); } else if (axis=="y") { runtime.push(osg::RadiansToDegrees(ptr.getRefData().getPosition().rot[1])); } else if (axis=="z") { runtime.push(osg::RadiansToDegrees(ptr.getRefData().getPosition().rot[2])); } } }; template class OpGetPos : public Interpreter::Opcode0 { public: void execute (Interpreter::Runtime& runtime) override { MWWorld::Ptr ptr = R()(runtime); std::string axis = runtime.getStringLiteral (runtime[0].mInteger); runtime.pop(); if(axis == "x") { runtime.push(ptr.getRefData().getPosition().pos[0]); } else if(axis == "y") { runtime.push(ptr.getRefData().getPosition().pos[1]); } else if(axis == "z") { runtime.push(ptr.getRefData().getPosition().pos[2]); } } }; template class OpSetPos : public Interpreter::Opcode0 { public: void execute (Interpreter::Runtime& runtime) override { MWWorld::Ptr ptr = R()(runtime); if (!ptr.isInCell()) return; std::string axis = runtime.getStringLiteral (runtime[0].mInteger); runtime.pop(); Interpreter::Type_Float pos = runtime[0].mFloat; runtime.pop(); // Note: SetPos does not skip weather transitions in vanilla engine, so we do not call setTeleported(true) here. const auto curPos = ptr.getRefData().getPosition().asVec3(); auto newPos = curPos; if(axis == "x") { newPos[0] = pos; } else if(axis == "y") { newPos[1] = pos; } else if(axis == "z") { // We should not place actors under ground if (ptr.getClass().isActor()) { float terrainHeight = -std::numeric_limits::max(); if (ptr.getCell()->isExterior()) terrainHeight = MWBase::Environment::get().getWorld()->getTerrainHeightAt(curPos); if (pos < terrainHeight) pos = terrainHeight; } newPos[2] = pos; } else { return; } dynamic_cast(runtime.getContext()).updatePtr(ptr, MWBase::Environment::get().getWorld()->moveObjectBy(ptr, newPos - curPos, true, true)); } }; template class OpGetStartingPos : public Interpreter::Opcode0 { public: void execute (Interpreter::Runtime& runtime) override { MWWorld::Ptr ptr = R()(runtime); std::string axis = runtime.getStringLiteral (runtime[0].mInteger); runtime.pop(); if(axis == "x") { runtime.push(ptr.getCellRef().getPosition().pos[0]); } else if(axis == "y") { runtime.push(ptr.getCellRef().getPosition().pos[1]); } else if(axis == "z") { runtime.push(ptr.getCellRef().getPosition().pos[2]); } } }; template class OpPositionCell : public Interpreter::Opcode0 { public: void execute (Interpreter::Runtime& runtime) override { MWWorld::Ptr ptr = R()(runtime); if (ptr.getContainerStore()) return; if (ptr == MWMechanics::getPlayer()) { MWBase::Environment::get().getWorld()->getPlayer().setTeleported(true); } Interpreter::Type_Float x = runtime[0].mFloat; runtime.pop(); Interpreter::Type_Float y = runtime[0].mFloat; runtime.pop(); Interpreter::Type_Float z = runtime[0].mFloat; runtime.pop(); Interpreter::Type_Float zRot = runtime[0].mFloat; runtime.pop(); std::string cellID = runtime.getStringLiteral (runtime[0].mInteger); runtime.pop(); MWWorld::CellStore* store = nullptr; try { store = MWBase::Environment::get().getWorld()->getInterior(cellID); } catch(std::exception&) { // cell not found, move to exterior instead (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"; runtime.getContext().report (error); Log(Debug::Warning) << error; } } if(store) { /* Start of tes3mp addition Track the original cell of this object in case we need to use it when sending a packet */ ESM::Cell originalCell = *ptr.getCell()->getCell(); /* End of tes3mp addition */ MWWorld::Ptr base = ptr; ptr = MWBase::Environment::get().getWorld()->moveObject(ptr,store,x,y,z); dynamic_cast(runtime.getContext()).updatePtr(base,ptr); /* Start of tes3mp addition Send ActorCellChange packets when actors are moved here, regardless of whether we're the cell authority or not; the server can decide if it wants to comply with them */ if (ptr.getClass().isActor() && !mwmp::Main::get().getCellController()->isSameCell(originalCell, *store->getCell())) { mwmp::BaseActor baseActor; baseActor.refNum = ptr.getCellRef().getRefNum().mIndex; baseActor.mpNum = ptr.getCellRef().getMpNum(); baseActor.cell = *store->getCell(); baseActor.position = ptr.getRefData().getPosition(); baseActor.isFollowerCellChange = true; mwmp::ActorList* actorList = mwmp::Main::get().getNetworking()->getActorList(); actorList->reset(); actorList->cell = originalCell; LOG_MESSAGE_SIMPLE(TimedLog::LOG_INFO, "Sending ID_ACTOR_CELL_CHANGE about %s %i-%i to server", ptr.getCellRef().getRefId().c_str(), baseActor.refNum, baseActor.mpNum); LOG_APPEND(TimedLog::LOG_INFO, "- Moved from %s to %s", actorList->cell.getDescription().c_str(), baseActor.cell.getDescription().c_str()); actorList->addCellChangeActor(baseActor); actorList->sendCellChangeActors(); } /* End of tes3mp addition */ float ax = ptr.getRefData().getPosition().rot[0]; float ay = ptr.getRefData().getPosition().rot[1]; // 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()) zRot = zRot/60.0f; MWBase::Environment::get().getWorld()->rotateObject(ptr,ax,ay,osg::DegreesToRadians(zRot)); ptr.getClass().adjustPosition(ptr, false); } } }; template class OpPosition : public Interpreter::Opcode0 { public: void execute (Interpreter::Runtime& runtime) override { MWWorld::Ptr ptr = R()(runtime); if (!ptr.isInCell()) return; if (ptr == MWMechanics::getPlayer()) { MWBase::Environment::get().getWorld()->getPlayer().setTeleported(true); } Interpreter::Type_Float x = runtime[0].mFloat; runtime.pop(); Interpreter::Type_Float y = runtime[0].mFloat; runtime.pop(); Interpreter::Type_Float z = runtime[0].mFloat; runtime.pop(); Interpreter::Type_Float zRot = runtime[0].mFloat; runtime.pop(); int cx,cy; MWBase::Environment::get().getWorld()->positionToIndex(x,y,cx,cy); // another morrowind oddity: player will be moved to the exterior cell at this location, // non-player actors will move within the cell they are in. MWWorld::Ptr base = ptr; if (ptr == MWMechanics::getPlayer()) { MWWorld::CellStore* cell = MWBase::Environment::get().getWorld()->getExterior(cx,cy); ptr = MWBase::Environment::get().getWorld()->moveObject(ptr,cell,x,y,z); } else { ptr = MWBase::Environment::get().getWorld()->moveObject(ptr, x, y, z, true, true); } dynamic_cast(runtime.getContext()).updatePtr(base,ptr); float ax = ptr.getRefData().getPosition().rot[0]; float ay = ptr.getRefData().getPosition().rot[1]; // 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()) zRot = zRot/60.0f; MWBase::Environment::get().getWorld()->rotateObject(ptr,ax,ay,osg::DegreesToRadians(zRot)); ptr.getClass().adjustPosition(ptr, false); } }; class OpPlaceItemCell : public Interpreter::Opcode0 { public: void execute (Interpreter::Runtime& runtime) override { std::string itemID = runtime.getStringLiteral (runtime[0].mInteger); runtime.pop(); std::string cellID = runtime.getStringLiteral (runtime[0].mInteger); runtime.pop(); Interpreter::Type_Float x = runtime[0].mFloat; runtime.pop(); Interpreter::Type_Float y = runtime[0].mFloat; runtime.pop(); Interpreter::Type_Float z = runtime[0].mFloat; runtime.pop(); Interpreter::Type_Float zRotDegrees = runtime[0].mFloat; runtime.pop(); MWWorld::CellStore* store = nullptr; try { store = MWBase::Environment::get().getWorld()->getInterior(cellID); } catch(std::exception&) { 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) { runtime.getContext().report ("unknown cell (" + cellID + ")"); Log(Debug::Error) << "Error: unknown cell (" << cellID << ")"; } } if(store) { ESM::Position pos; pos.pos[0] = x; pos.pos[1] = y; pos.pos[2] = z; pos.rot[0] = pos.rot[1] = 0; pos.rot[2] = osg::DegreesToRadians(zRotDegrees); MWWorld::ManualRef ref(MWBase::Environment::get().getWorld()->getStore(),itemID); ref.getPtr().getCellRef().setPosition(pos); MWWorld::Ptr placed = MWBase::Environment::get().getWorld()->placeObject(ref.getPtr(),store,pos); placed.getClass().adjustPosition(placed, true); /* Start of tes3mp addition Send an ID_OBJECT_PLACE or ID_OBJECT_SPAWN packet every time an object is placed in the world through a script */ if (mwmp::Main::get().getLocalPlayer()->isLoggedIn()) { mwmp::ObjectList *objectList = mwmp::Main::get().getNetworking()->getObjectList(); objectList->reset(); objectList->packetOrigin = ScriptController::getPacketOriginFromContextType(runtime.getContext().getContextType()); objectList->originClientScript = runtime.getContext().getCurrentScriptName(); if (placed.getClass().isActor()) { objectList->addObjectSpawn(placed); objectList->sendObjectSpawn(); } else { objectList->addObjectPlace(placed); objectList->sendObjectPlace(); } } /* End of tes3mp addition */ /* Start of tes3mp change (major) Instead of actually keeping this object as is, delete it after sending the packet and wait for the server to send it back with a unique mpNum of its own */ MWBase::Environment::get().getWorld()->deleteObject(placed); /* End of tes3mp change (major) */ } } }; class OpPlaceItem : public Interpreter::Opcode0 { public: void execute (Interpreter::Runtime& runtime) override { std::string itemID = runtime.getStringLiteral (runtime[0].mInteger); runtime.pop(); Interpreter::Type_Float x = runtime[0].mFloat; runtime.pop(); Interpreter::Type_Float y = runtime[0].mFloat; runtime.pop(); Interpreter::Type_Float z = runtime[0].mFloat; runtime.pop(); Interpreter::Type_Float zRotDegrees = runtime[0].mFloat; runtime.pop(); MWWorld::Ptr player = MWMechanics::getPlayer(); if (!player.isInCell()) throw std::runtime_error("player not in a cell"); MWWorld::CellStore* store = nullptr; if (player.getCell()->isExterior()) { int cx,cy; MWBase::Environment::get().getWorld()->positionToIndex(x,y,cx,cy); store = MWBase::Environment::get().getWorld()->getExterior(cx,cy); } else store = player.getCell(); ESM::Position pos; pos.pos[0] = x; pos.pos[1] = y; pos.pos[2] = z; pos.rot[0] = pos.rot[1] = 0; pos.rot[2] = osg::DegreesToRadians(zRotDegrees); MWWorld::ManualRef ref(MWBase::Environment::get().getWorld()->getStore(),itemID); ref.getPtr().getCellRef().setPosition(pos); MWWorld::Ptr placed = MWBase::Environment::get().getWorld()->placeObject(ref.getPtr(),store,pos); placed.getClass().adjustPosition(placed, true); /* Start of tes3mp addition Send an ID_OBJECT_PLACE or ID_OBJECT_SPAWN packet every time an object is placed in the world through a script */ if (mwmp::Main::get().getLocalPlayer()->isLoggedIn()) { mwmp::ObjectList *objectList = mwmp::Main::get().getNetworking()->getObjectList(); objectList->reset(); objectList->packetOrigin = ScriptController::getPacketOriginFromContextType(runtime.getContext().getContextType()); objectList->originClientScript = runtime.getContext().getCurrentScriptName(); if (placed.getClass().isActor()) { objectList->addObjectSpawn(placed); objectList->sendObjectSpawn(); } else { objectList->addObjectPlace(placed); objectList->sendObjectPlace(); } } /* End of tes3mp addition */ /* Start of tes3mp change (major) Instead of actually keeping this object as is, delete it after sending the packet and wait for the server to send it back with a unique mpNum of its own */ MWBase::Environment::get().getWorld()->deleteObject(placed); /* End of tes3mp change (major) */ } }; template class OpPlaceAt : public Interpreter::Opcode0 { public: void execute (Interpreter::Runtime& runtime) override { MWWorld::Ptr actor = pc ? MWMechanics::getPlayer() : R()(runtime); std::string itemID = runtime.getStringLiteral (runtime[0].mInteger); runtime.pop(); Interpreter::Type_Integer count = runtime[0].mInteger; runtime.pop(); Interpreter::Type_Float distance = runtime[0].mFloat; runtime.pop(); Interpreter::Type_Integer direction = runtime[0].mInteger; runtime.pop(); if (direction < 0 || direction > 3) throw std::runtime_error ("invalid direction"); if (count<0) throw std::runtime_error ("count must be non-negative"); if (!actor.isInCell()) throw std::runtime_error ("actor is not in a cell"); for (int i=0; igetStore(), itemID, 1); MWWorld::Ptr ptr = MWBase::Environment::get().getWorld()->safePlaceObject(ref.getPtr(), actor, actor.getCell(), direction, distance); MWBase::Environment::get().getWorld()->scaleObject(ptr, actor.getCellRef().getScale()); /* Start of tes3mp addition Send an ID_OBJECT_PLACE or ID_OBJECT_SPAWN packet every time an object is placed in the world through a script */ if (mwmp::Main::get().getLocalPlayer()->isLoggedIn()) { mwmp::ObjectList *objectList = mwmp::Main::get().getNetworking()->getObjectList(); objectList->reset(); objectList->packetOrigin = ScriptController::getPacketOriginFromContextType(runtime.getContext().getContextType()); objectList->originClientScript = runtime.getContext().getCurrentScriptName(); if (ptr.getClass().isActor()) { objectList->addObjectSpawn(ptr); objectList->sendObjectSpawn(); } else { objectList->addObjectPlace(ptr); objectList->sendObjectPlace(); } } /* End of tes3mp addition */ /* Start of tes3mp change (major) Instead of actually keeping this object as is, delete it after sending the packet and wait for the server to send it back with a unique mpNum of its own */ MWBase::Environment::get().getWorld()->deleteObject(ptr); /* End of tes3mp change (major) */ } } }; template class OpRotate : public Interpreter::Opcode0 { public: void execute (Interpreter::Runtime& runtime) override { const MWWorld::Ptr& ptr = R()(runtime); std::string axis = runtime.getStringLiteral (runtime[0].mInteger); runtime.pop(); Interpreter::Type_Float rotation = osg::DegreesToRadians(runtime[0].mFloat*MWBase::Environment::get().getFrameDuration()); runtime.pop(); float ax = ptr.getRefData().getPosition().rot[0]; float ay = ptr.getRefData().getPosition().rot[1]; float az = ptr.getRefData().getPosition().rot[2]; if (axis == "x") MWBase::Environment::get().getWorld()->rotateObject(ptr,ax+rotation,ay,az); else if (axis == "y") MWBase::Environment::get().getWorld()->rotateObject(ptr,ax,ay+rotation,az); else if (axis == "z") MWBase::Environment::get().getWorld()->rotateObject(ptr,ax,ay,az+rotation); } }; template class OpRotateWorld : public Interpreter::Opcode0 { public: void execute (Interpreter::Runtime& runtime) override { MWWorld::Ptr ptr = R()(runtime); std::string axis = runtime.getStringLiteral (runtime[0].mInteger); runtime.pop(); Interpreter::Type_Float rotation = osg::DegreesToRadians(runtime[0].mFloat*MWBase::Environment::get().getFrameDuration()); runtime.pop(); if (!ptr.getRefData().getBaseNode()) return; // We can rotate actors only around Z axis if (ptr.getClass().isActor() && (axis == "x" || axis == "y")) return; osg::Quat rot; if (axis == "x") rot = osg::Quat(rotation, -osg::X_AXIS); else if (axis == "y") rot = osg::Quat(rotation, -osg::Y_AXIS); else if (axis == "z") rot = osg::Quat(rotation, -osg::Z_AXIS); else return; osg::Quat attitude = ptr.getRefData().getBaseNode()->getAttitude(); MWBase::Environment::get().getWorld()->rotateWorldObject(ptr, attitude * rot); } }; template class OpSetAtStart : public Interpreter::Opcode0 { public: void execute (Interpreter::Runtime& runtime) override { MWWorld::Ptr ptr = R()(runtime); if (!ptr.isInCell()) return; float xr = ptr.getCellRef().getPosition().rot[0]; float yr = ptr.getCellRef().getPosition().rot[1]; float zr = ptr.getCellRef().getPosition().rot[2]; MWBase::Environment::get().getWorld()->rotateObject(ptr, xr, yr, zr); dynamic_cast(runtime.getContext()).updatePtr(ptr, MWBase::Environment::get().getWorld()->moveObject(ptr, ptr.getCellRef().getPosition().pos[0], ptr.getCellRef().getPosition().pos[1], ptr.getCellRef().getPosition().pos[2])); } }; template class OpMove : public Interpreter::Opcode0 { public: void execute (Interpreter::Runtime& runtime) override { const MWWorld::Ptr& ptr = R()(runtime); if (!ptr.isInCell()) return; std::string axis = runtime.getStringLiteral (runtime[0].mInteger); runtime.pop(); Interpreter::Type_Float movement = (runtime[0].mFloat*MWBase::Environment::get().getFrameDuration()); runtime.pop(); osg::Vec3f posChange; if (axis == "x") { posChange=osg::Vec3f(movement, 0, 0); } else if (axis == "y") { posChange=osg::Vec3f(0, movement, 0); } else if (axis == "z") { posChange=osg::Vec3f(0, 0, movement); } else return; // is it correct that disabled objects can't be Move-d? if (!ptr.getRefData().getBaseNode()) return; osg::Vec3f diff = ptr.getRefData().getBaseNode()->getAttitude() * posChange; // We should move actors, standing on moving object, too. // This approach can be used to create elevators. moveStandingActors(ptr, diff); dynamic_cast(runtime.getContext()).updatePtr(ptr, MWBase::Environment::get().getWorld()->moveObjectBy(ptr, diff, false, true)); } }; template class OpMoveWorld : public Interpreter::Opcode0 { public: void execute (Interpreter::Runtime& runtime) override { MWWorld::Ptr ptr = R()(runtime); if (!ptr.isInCell()) return; std::string axis = runtime.getStringLiteral (runtime[0].mInteger); runtime.pop(); Interpreter::Type_Float movement = (runtime[0].mFloat*MWBase::Environment::get().getFrameDuration()); runtime.pop(); osg::Vec3f diff; if (axis == "x") diff.x() = movement; else if (axis == "y") diff.y() = movement; else if (axis == "z") diff.z() = movement; else return; // We should move actors, standing on moving object, too. // This approach can be used to create elevators. moveStandingActors(ptr, diff); dynamic_cast(runtime.getContext()).updatePtr(ptr, MWBase::Environment::get().getWorld()->moveObjectBy(ptr, diff, false, true)); } }; class OpResetActors : public Interpreter::Opcode0 { public: void execute (Interpreter::Runtime& runtime) override { MWBase::Environment::get().getWorld()->resetActors(); } }; class OpFixme : public Interpreter::Opcode0 { public: void execute (Interpreter::Runtime& runtime) override { MWBase::Environment::get().getWorld()->fixPosition(); } }; void installOpcodes (Interpreter::Interpreter& interpreter) { interpreter.installSegment5(Compiler::Transformation::opcodeGetDistance, new OpGetDistance); interpreter.installSegment5(Compiler::Transformation::opcodeGetDistanceExplicit, new OpGetDistance); interpreter.installSegment5(Compiler::Transformation::opcodeSetScale,new OpSetScale); interpreter.installSegment5(Compiler::Transformation::opcodeSetScaleExplicit,new OpSetScale); interpreter.installSegment5(Compiler::Transformation::opcodeSetAngle,new OpSetAngle); interpreter.installSegment5(Compiler::Transformation::opcodeSetAngleExplicit,new OpSetAngle); interpreter.installSegment5(Compiler::Transformation::opcodeGetScale,new OpGetScale); interpreter.installSegment5(Compiler::Transformation::opcodeGetScaleExplicit,new OpGetScale); interpreter.installSegment5(Compiler::Transformation::opcodeGetAngle,new OpGetAngle); interpreter.installSegment5(Compiler::Transformation::opcodeGetAngleExplicit,new OpGetAngle); interpreter.installSegment5(Compiler::Transformation::opcodeGetPos,new OpGetPos); interpreter.installSegment5(Compiler::Transformation::opcodeGetPosExplicit,new OpGetPos); interpreter.installSegment5(Compiler::Transformation::opcodeSetPos,new OpSetPos); interpreter.installSegment5(Compiler::Transformation::opcodeSetPosExplicit,new OpSetPos); interpreter.installSegment5(Compiler::Transformation::opcodeGetStartingPos,new OpGetStartingPos); interpreter.installSegment5(Compiler::Transformation::opcodeGetStartingPosExplicit,new OpGetStartingPos); interpreter.installSegment5(Compiler::Transformation::opcodePosition,new OpPosition); interpreter.installSegment5(Compiler::Transformation::opcodePositionExplicit,new OpPosition); interpreter.installSegment5(Compiler::Transformation::opcodePositionCell,new OpPositionCell); interpreter.installSegment5(Compiler::Transformation::opcodePositionCellExplicit,new OpPositionCell); interpreter.installSegment5(Compiler::Transformation::opcodePlaceItemCell,new OpPlaceItemCell); interpreter.installSegment5(Compiler::Transformation::opcodePlaceItem,new OpPlaceItem); interpreter.installSegment5(Compiler::Transformation::opcodePlaceAtPc,new OpPlaceAt); interpreter.installSegment5(Compiler::Transformation::opcodePlaceAtMe,new OpPlaceAt); interpreter.installSegment5(Compiler::Transformation::opcodePlaceAtMeExplicit,new OpPlaceAt); interpreter.installSegment5(Compiler::Transformation::opcodeModScale,new OpModScale); interpreter.installSegment5(Compiler::Transformation::opcodeModScaleExplicit,new OpModScale); interpreter.installSegment5(Compiler::Transformation::opcodeRotate,new OpRotate); interpreter.installSegment5(Compiler::Transformation::opcodeRotateExplicit,new OpRotate); interpreter.installSegment5(Compiler::Transformation::opcodeRotateWorld,new OpRotateWorld); interpreter.installSegment5(Compiler::Transformation::opcodeRotateWorldExplicit,new OpRotateWorld); interpreter.installSegment5(Compiler::Transformation::opcodeSetAtStart,new OpSetAtStart); interpreter.installSegment5(Compiler::Transformation::opcodeSetAtStartExplicit,new OpSetAtStart); interpreter.installSegment5(Compiler::Transformation::opcodeMove,new OpMove); interpreter.installSegment5(Compiler::Transformation::opcodeMoveExplicit,new OpMove); interpreter.installSegment5(Compiler::Transformation::opcodeMoveWorld,new OpMoveWorld); interpreter.installSegment5(Compiler::Transformation::opcodeMoveWorldExplicit,new OpMoveWorld); interpreter.installSegment5(Compiler::Transformation::opcodeGetStartingAngle, new OpGetStartingAngle); interpreter.installSegment5(Compiler::Transformation::opcodeGetStartingAngleExplicit, new OpGetStartingAngle); interpreter.installSegment5(Compiler::Transformation::opcodeResetActors, new OpResetActors); interpreter.installSegment5(Compiler::Transformation::opcodeFixme, new OpFixme); } } } ================================================ FILE: apps/openmw/mwscript/transformationextensions.hpp ================================================ #ifndef GAME_SCRIPT_TRANSFORMATIONEXTENSIONS_H #define GAME_SCRIPT_TRANSFORMATIONEXTENSIONS_H namespace Compiler { class Extensions; } namespace Interpreter { class Interpreter; } namespace MWScript { /// \brief stats-related script functionality (creatures and NPCs) namespace Transformation { void installOpcodes (Interpreter::Interpreter& interpreter); } } #endif ================================================ FILE: apps/openmw/mwscript/userextensions.cpp ================================================ #include "userextensions.hpp" #include #include #include #include #include #include #include "ref.hpp" namespace MWScript { /// Temporary script extensions. /// /// \attention Do not commit changes to this file to a git repository! namespace User { class OpUser1 : public Interpreter::Opcode0 { public: void execute (Interpreter::Runtime& runtime) override { runtime.getContext().report ("user1: not in use"); } }; class OpUser2 : public Interpreter::Opcode0 { public: void execute (Interpreter::Runtime& runtime) override { runtime.getContext().report ("user2: not in use"); } }; template class OpUser3 : public Interpreter::Opcode0 { public: void execute (Interpreter::Runtime& runtime) override { // MWWorld::Ptr ptr = R()(runtime); runtime.getContext().report ("user3: not in use"); } }; template class OpUser4 : public Interpreter::Opcode0 { public: void execute (Interpreter::Runtime& runtime) override { // MWWorld::Ptr ptr = R()(runtime); runtime.getContext().report ("user4: not in use"); } }; void installOpcodes (Interpreter::Interpreter& interpreter) { interpreter.installSegment5 (Compiler::User::opcodeUser1, new OpUser1); interpreter.installSegment5 (Compiler::User::opcodeUser2, new OpUser2); interpreter.installSegment5 (Compiler::User::opcodeUser3, new OpUser3); interpreter.installSegment5 (Compiler::User::opcodeUser3Explicit, new OpUser3); interpreter.installSegment5 (Compiler::User::opcodeUser4, new OpUser4); interpreter.installSegment5 (Compiler::User::opcodeUser4Explicit, new OpUser4); } } } ================================================ FILE: apps/openmw/mwscript/userextensions.hpp ================================================ #ifndef GAME_SCRIPT_USEREXTENSIONS_H #define GAME_SCRIPT_USEREXTENSIONS_H namespace Compiler { class Extensions; } namespace Interpreter { class Interpreter; } namespace MWScript { /// \brief Temporary script functionality limited to the console namespace User { void installOpcodes (Interpreter::Interpreter& interpreter); } } #endif ================================================ FILE: apps/openmw/mwsound/alext.h ================================================ /** * OpenAL cross platform audio library * Copyright (C) 2008 by authors. * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License as published by the Free Software Foundation; either * version 2 of the License, or (at your option) any later version. * * This library 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 * Library General Public License for more details. * * You should have received a copy of the GNU Library General Public * License along with this library; if not, write to the * Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. * Or go to https://www.gnu.org/copyleft/lgpl.html */ #ifndef AL_ALEXT_H #define AL_ALEXT_H #include /* Define int64_t and uint64_t types */ #if defined(__STDC_VERSION__) && __STDC_VERSION__ >= 199901L #include #elif defined(_WIN32) && defined(__GNUC__) #include #elif defined(_WIN32) typedef __int64 int64_t; typedef unsigned __int64 uint64_t; #else /* Fallback if nothing above works */ #include #endif #include "alc.h" #include "al.h" #ifdef __cplusplus extern "C" { #endif #ifndef AL_LOKI_IMA_ADPCM_format #define AL_LOKI_IMA_ADPCM_format 1 #define AL_FORMAT_IMA_ADPCM_MONO16_EXT 0x10000 #define AL_FORMAT_IMA_ADPCM_STEREO16_EXT 0x10001 #endif #ifndef AL_LOKI_WAVE_format #define AL_LOKI_WAVE_format 1 #define AL_FORMAT_WAVE_EXT 0x10002 #endif #ifndef AL_EXT_vorbis #define AL_EXT_vorbis 1 #define AL_FORMAT_VORBIS_EXT 0x10003 #endif #ifndef AL_LOKI_quadriphonic #define AL_LOKI_quadriphonic 1 #define AL_FORMAT_QUAD8_LOKI 0x10004 #define AL_FORMAT_QUAD16_LOKI 0x10005 #endif #ifndef AL_EXT_float32 #define AL_EXT_float32 1 #define AL_FORMAT_MONO_FLOAT32 0x10010 #define AL_FORMAT_STEREO_FLOAT32 0x10011 #endif #ifndef AL_EXT_double #define AL_EXT_double 1 #define AL_FORMAT_MONO_DOUBLE_EXT 0x10012 #define AL_FORMAT_STEREO_DOUBLE_EXT 0x10013 #endif #ifndef AL_EXT_MULAW #define AL_EXT_MULAW 1 #define AL_FORMAT_MONO_MULAW_EXT 0x10014 #define AL_FORMAT_STEREO_MULAW_EXT 0x10015 #endif #ifndef AL_EXT_ALAW #define AL_EXT_ALAW 1 #define AL_FORMAT_MONO_ALAW_EXT 0x10016 #define AL_FORMAT_STEREO_ALAW_EXT 0x10017 #endif #ifndef ALC_LOKI_audio_channel #define ALC_LOKI_audio_channel 1 #define ALC_CHAN_MAIN_LOKI 0x500001 #define ALC_CHAN_PCM_LOKI 0x500002 #define ALC_CHAN_CD_LOKI 0x500003 #endif #ifndef AL_EXT_MCFORMATS #define AL_EXT_MCFORMATS 1 #define AL_FORMAT_QUAD8 0x1204 #define AL_FORMAT_QUAD16 0x1205 #define AL_FORMAT_QUAD32 0x1206 #define AL_FORMAT_REAR8 0x1207 #define AL_FORMAT_REAR16 0x1208 #define AL_FORMAT_REAR32 0x1209 #define AL_FORMAT_51CHN8 0x120A #define AL_FORMAT_51CHN16 0x120B #define AL_FORMAT_51CHN32 0x120C #define AL_FORMAT_61CHN8 0x120D #define AL_FORMAT_61CHN16 0x120E #define AL_FORMAT_61CHN32 0x120F #define AL_FORMAT_71CHN8 0x1210 #define AL_FORMAT_71CHN16 0x1211 #define AL_FORMAT_71CHN32 0x1212 #endif #ifndef AL_EXT_MULAW_MCFORMATS #define AL_EXT_MULAW_MCFORMATS 1 #define AL_FORMAT_MONO_MULAW 0x10014 #define AL_FORMAT_STEREO_MULAW 0x10015 #define AL_FORMAT_QUAD_MULAW 0x10021 #define AL_FORMAT_REAR_MULAW 0x10022 #define AL_FORMAT_51CHN_MULAW 0x10023 #define AL_FORMAT_61CHN_MULAW 0x10024 #define AL_FORMAT_71CHN_MULAW 0x10025 #endif #ifndef AL_EXT_IMA4 #define AL_EXT_IMA4 1 #define AL_FORMAT_MONO_IMA4 0x1300 #define AL_FORMAT_STEREO_IMA4 0x1301 #endif #ifndef AL_EXT_STATIC_BUFFER #define AL_EXT_STATIC_BUFFER 1 typedef ALvoid (AL_APIENTRY*PFNALBUFFERDATASTATICPROC)(const ALint,ALenum,ALvoid*,ALsizei,ALsizei); #ifdef AL_ALEXT_PROTOTYPES AL_API ALvoid AL_APIENTRY alBufferDataStatic(const ALint buffer, ALenum format, ALvoid *data, ALsizei len, ALsizei freq); #endif #endif #ifndef ALC_EXT_EFX #define ALC_EXT_EFX 1 #include "efx.h" #endif #ifndef ALC_EXT_disconnect #define ALC_EXT_disconnect 1 #define ALC_CONNECTED 0x313 #endif #ifndef ALC_EXT_thread_local_context #define ALC_EXT_thread_local_context 1 typedef ALCboolean (ALC_APIENTRY*PFNALCSETTHREADCONTEXTPROC)(ALCcontext *context); typedef ALCcontext* (ALC_APIENTRY*PFNALCGETTHREADCONTEXTPROC)(void); #ifdef AL_ALEXT_PROTOTYPES ALC_API ALCboolean ALC_APIENTRY alcSetThreadContext(ALCcontext *context); ALC_API ALCcontext* ALC_APIENTRY alcGetThreadContext(void); #endif #endif #ifndef AL_EXT_source_distance_model #define AL_EXT_source_distance_model 1 #define AL_SOURCE_DISTANCE_MODEL 0x200 #endif #ifndef AL_SOFT_buffer_sub_data #define AL_SOFT_buffer_sub_data 1 #define AL_BYTE_RW_OFFSETS_SOFT 0x1031 #define AL_SAMPLE_RW_OFFSETS_SOFT 0x1032 typedef ALvoid (AL_APIENTRY*PFNALBUFFERSUBDATASOFTPROC)(ALuint,ALenum,const ALvoid*,ALsizei,ALsizei); #ifdef AL_ALEXT_PROTOTYPES AL_API ALvoid AL_APIENTRY alBufferSubDataSOFT(ALuint buffer,ALenum format,const ALvoid *data,ALsizei offset,ALsizei length); #endif #endif #ifndef AL_SOFT_loop_points #define AL_SOFT_loop_points 1 #define AL_LOOP_POINTS_SOFT 0x2015 #endif #ifndef AL_EXT_FOLDBACK #define AL_EXT_FOLDBACK 1 #define AL_EXT_FOLDBACK_NAME "AL_EXT_FOLDBACK" #define AL_FOLDBACK_EVENT_BLOCK 0x4112 #define AL_FOLDBACK_EVENT_START 0x4111 #define AL_FOLDBACK_EVENT_STOP 0x4113 #define AL_FOLDBACK_MODE_MONO 0x4101 #define AL_FOLDBACK_MODE_STEREO 0x4102 typedef void (AL_APIENTRY*LPALFOLDBACKCALLBACK)(ALenum,ALsizei); typedef void (AL_APIENTRY*LPALREQUESTFOLDBACKSTART)(ALenum,ALsizei,ALsizei,ALfloat*,LPALFOLDBACKCALLBACK); typedef void (AL_APIENTRY*LPALREQUESTFOLDBACKSTOP)(void); #ifdef AL_ALEXT_PROTOTYPES AL_API void AL_APIENTRY alRequestFoldbackStart(ALenum mode,ALsizei count,ALsizei length,ALfloat *mem,LPALFOLDBACKCALLBACK callback); AL_API void AL_APIENTRY alRequestFoldbackStop(void); #endif #endif #ifndef ALC_EXT_DEDICATED #define ALC_EXT_DEDICATED 1 #define AL_DEDICATED_GAIN 0x0001 #define AL_EFFECT_DEDICATED_DIALOGUE 0x9001 #define AL_EFFECT_DEDICATED_LOW_FREQUENCY_EFFECT 0x9000 #endif #ifndef AL_SOFT_buffer_samples #define AL_SOFT_buffer_samples 1 /* Channel configurations */ #define AL_MONO_SOFT 0x1500 #define AL_STEREO_SOFT 0x1501 #define AL_REAR_SOFT 0x1502 #define AL_QUAD_SOFT 0x1503 #define AL_5POINT1_SOFT 0x1504 #define AL_6POINT1_SOFT 0x1505 #define AL_7POINT1_SOFT 0x1506 /* Sample types */ #define AL_BYTE_SOFT 0x1400 #define AL_UNSIGNED_BYTE_SOFT 0x1401 #define AL_SHORT_SOFT 0x1402 #define AL_UNSIGNED_SHORT_SOFT 0x1403 #define AL_INT_SOFT 0x1404 #define AL_UNSIGNED_INT_SOFT 0x1405 #define AL_FLOAT_SOFT 0x1406 #define AL_DOUBLE_SOFT 0x1407 #define AL_BYTE3_SOFT 0x1408 #define AL_UNSIGNED_BYTE3_SOFT 0x1409 /* Storage formats */ #define AL_MONO8_SOFT 0x1100 #define AL_MONO16_SOFT 0x1101 #define AL_MONO32F_SOFT 0x10010 #define AL_STEREO8_SOFT 0x1102 #define AL_STEREO16_SOFT 0x1103 #define AL_STEREO32F_SOFT 0x10011 #define AL_QUAD8_SOFT 0x1204 #define AL_QUAD16_SOFT 0x1205 #define AL_QUAD32F_SOFT 0x1206 #define AL_REAR8_SOFT 0x1207 #define AL_REAR16_SOFT 0x1208 #define AL_REAR32F_SOFT 0x1209 #define AL_5POINT1_8_SOFT 0x120A #define AL_5POINT1_16_SOFT 0x120B #define AL_5POINT1_32F_SOFT 0x120C #define AL_6POINT1_8_SOFT 0x120D #define AL_6POINT1_16_SOFT 0x120E #define AL_6POINT1_32F_SOFT 0x120F #define AL_7POINT1_8_SOFT 0x1210 #define AL_7POINT1_16_SOFT 0x1211 #define AL_7POINT1_32F_SOFT 0x1212 /* Buffer attributes */ #define AL_INTERNAL_FORMAT_SOFT 0x2008 #define AL_BYTE_LENGTH_SOFT 0x2009 #define AL_SAMPLE_LENGTH_SOFT 0x200A #define AL_SEC_LENGTH_SOFT 0x200B typedef void (AL_APIENTRY*LPALBUFFERSAMPLESSOFT)(ALuint,ALuint,ALenum,ALsizei,ALenum,ALenum,const ALvoid*); typedef void (AL_APIENTRY*LPALBUFFERSUBSAMPLESSOFT)(ALuint,ALsizei,ALsizei,ALenum,ALenum,const ALvoid*); typedef void (AL_APIENTRY*LPALGETBUFFERSAMPLESSOFT)(ALuint,ALsizei,ALsizei,ALenum,ALenum,ALvoid*); typedef ALboolean (AL_APIENTRY*LPALISBUFFERFORMATSUPPORTEDSOFT)(ALenum); #ifdef AL_ALEXT_PROTOTYPES AL_API void AL_APIENTRY alBufferSamplesSOFT(ALuint buffer, ALuint samplerate, ALenum internalformat, ALsizei samples, ALenum channels, ALenum type, const ALvoid *data); AL_API void AL_APIENTRY alBufferSubSamplesSOFT(ALuint buffer, ALsizei offset, ALsizei samples, ALenum channels, ALenum type, const ALvoid *data); AL_API void AL_APIENTRY alGetBufferSamplesSOFT(ALuint buffer, ALsizei offset, ALsizei samples, ALenum channels, ALenum type, ALvoid *data); AL_API ALboolean AL_APIENTRY alIsBufferFormatSupportedSOFT(ALenum format); #endif #endif #ifndef AL_SOFT_direct_channels #define AL_SOFT_direct_channels 1 #define AL_DIRECT_CHANNELS_SOFT 0x1033 #endif #ifndef ALC_SOFT_loopback #define ALC_SOFT_loopback 1 #define ALC_FORMAT_CHANNELS_SOFT 0x1990 #define ALC_FORMAT_TYPE_SOFT 0x1991 /* Sample types */ #define ALC_BYTE_SOFT 0x1400 #define ALC_UNSIGNED_BYTE_SOFT 0x1401 #define ALC_SHORT_SOFT 0x1402 #define ALC_UNSIGNED_SHORT_SOFT 0x1403 #define ALC_INT_SOFT 0x1404 #define ALC_UNSIGNED_INT_SOFT 0x1405 #define ALC_FLOAT_SOFT 0x1406 /* Channel configurations */ #define ALC_MONO_SOFT 0x1500 #define ALC_STEREO_SOFT 0x1501 #define ALC_QUAD_SOFT 0x1503 #define ALC_5POINT1_SOFT 0x1504 #define ALC_6POINT1_SOFT 0x1505 #define ALC_7POINT1_SOFT 0x1506 typedef ALCdevice* (ALC_APIENTRY*LPALCLOOPBACKOPENDEVICESOFT)(const ALCchar*); typedef ALCboolean (ALC_APIENTRY*LPALCISRENDERFORMATSUPPORTEDSOFT)(ALCdevice*,ALCsizei,ALCenum,ALCenum); typedef void (ALC_APIENTRY*LPALCRENDERSAMPLESSOFT)(ALCdevice*,ALCvoid*,ALCsizei); #ifdef AL_ALEXT_PROTOTYPES ALC_API ALCdevice* ALC_APIENTRY alcLoopbackOpenDeviceSOFT(const ALCchar *deviceName); ALC_API ALCboolean ALC_APIENTRY alcIsRenderFormatSupportedSOFT(ALCdevice *device, ALCsizei freq, ALCenum channels, ALCenum type); ALC_API void ALC_APIENTRY alcRenderSamplesSOFT(ALCdevice *device, ALCvoid *buffer, ALCsizei samples); #endif #endif #ifndef AL_EXT_STEREO_ANGLES #define AL_EXT_STEREO_ANGLES 1 #define AL_STEREO_ANGLES 0x1030 #endif #ifndef AL_EXT_SOURCE_RADIUS #define AL_EXT_SOURCE_RADIUS 1 #define AL_SOURCE_RADIUS 0x1031 #endif #ifndef AL_SOFT_source_latency #define AL_SOFT_source_latency 1 #define AL_SAMPLE_OFFSET_LATENCY_SOFT 0x1200 #define AL_SEC_OFFSET_LATENCY_SOFT 0x1201 typedef int64_t ALint64SOFT; typedef uint64_t ALuint64SOFT; typedef void (AL_APIENTRY*LPALSOURCEDSOFT)(ALuint,ALenum,ALdouble); typedef void (AL_APIENTRY*LPALSOURCE3DSOFT)(ALuint,ALenum,ALdouble,ALdouble,ALdouble); typedef void (AL_APIENTRY*LPALSOURCEDVSOFT)(ALuint,ALenum,const ALdouble*); typedef void (AL_APIENTRY*LPALGETSOURCEDSOFT)(ALuint,ALenum,ALdouble*); typedef void (AL_APIENTRY*LPALGETSOURCE3DSOFT)(ALuint,ALenum,ALdouble*,ALdouble*,ALdouble*); typedef void (AL_APIENTRY*LPALGETSOURCEDVSOFT)(ALuint,ALenum,ALdouble*); typedef void (AL_APIENTRY*LPALSOURCEI64SOFT)(ALuint,ALenum,ALint64SOFT); typedef void (AL_APIENTRY*LPALSOURCE3I64SOFT)(ALuint,ALenum,ALint64SOFT,ALint64SOFT,ALint64SOFT); typedef void (AL_APIENTRY*LPALSOURCEI64VSOFT)(ALuint,ALenum,const ALint64SOFT*); typedef void (AL_APIENTRY*LPALGETSOURCEI64SOFT)(ALuint,ALenum,ALint64SOFT*); typedef void (AL_APIENTRY*LPALGETSOURCE3I64SOFT)(ALuint,ALenum,ALint64SOFT*,ALint64SOFT*,ALint64SOFT*); typedef void (AL_APIENTRY*LPALGETSOURCEI64VSOFT)(ALuint,ALenum,ALint64SOFT*); #ifdef AL_ALEXT_PROTOTYPES AL_API void AL_APIENTRY alSourcedSOFT(ALuint source, ALenum param, ALdouble value); AL_API void AL_APIENTRY alSource3dSOFT(ALuint source, ALenum param, ALdouble value1, ALdouble value2, ALdouble value3); AL_API void AL_APIENTRY alSourcedvSOFT(ALuint source, ALenum param, const ALdouble *values); AL_API void AL_APIENTRY alGetSourcedSOFT(ALuint source, ALenum param, ALdouble *value); AL_API void AL_APIENTRY alGetSource3dSOFT(ALuint source, ALenum param, ALdouble *value1, ALdouble *value2, ALdouble *value3); AL_API void AL_APIENTRY alGetSourcedvSOFT(ALuint source, ALenum param, ALdouble *values); AL_API void AL_APIENTRY alSourcei64SOFT(ALuint source, ALenum param, ALint64SOFT value); AL_API void AL_APIENTRY alSource3i64SOFT(ALuint source, ALenum param, ALint64SOFT value1, ALint64SOFT value2, ALint64SOFT value3); AL_API void AL_APIENTRY alSourcei64vSOFT(ALuint source, ALenum param, const ALint64SOFT *values); AL_API void AL_APIENTRY alGetSourcei64SOFT(ALuint source, ALenum param, ALint64SOFT *value); AL_API void AL_APIENTRY alGetSource3i64SOFT(ALuint source, ALenum param, ALint64SOFT *value1, ALint64SOFT *value2, ALint64SOFT *value3); AL_API void AL_APIENTRY alGetSourcei64vSOFT(ALuint source, ALenum param, ALint64SOFT *values); #endif #endif #ifndef ALC_EXT_DEFAULT_FILTER_ORDER #define ALC_EXT_DEFAULT_FILTER_ORDER 1 #define ALC_DEFAULT_FILTER_ORDER 0x1100 #endif #ifndef AL_SOFT_deferred_updates #define AL_SOFT_deferred_updates 1 #define AL_DEFERRED_UPDATES_SOFT 0xC002 typedef ALvoid (AL_APIENTRY*LPALDEFERUPDATESSOFT)(void); typedef ALvoid (AL_APIENTRY*LPALPROCESSUPDATESSOFT)(void); #ifdef AL_ALEXT_PROTOTYPES AL_API ALvoid AL_APIENTRY alDeferUpdatesSOFT(void); AL_API ALvoid AL_APIENTRY alProcessUpdatesSOFT(void); #endif #endif #ifndef AL_SOFT_block_alignment #define AL_SOFT_block_alignment 1 #define AL_UNPACK_BLOCK_ALIGNMENT_SOFT 0x200C #define AL_PACK_BLOCK_ALIGNMENT_SOFT 0x200D #endif #ifndef AL_SOFT_MSADPCM #define AL_SOFT_MSADPCM 1 #define AL_FORMAT_MONO_MSADPCM_SOFT 0x1302 #define AL_FORMAT_STEREO_MSADPCM_SOFT 0x1303 #endif #ifndef AL_SOFT_source_length #define AL_SOFT_source_length 1 /*#define AL_BYTE_LENGTH_SOFT 0x2009*/ /*#define AL_SAMPLE_LENGTH_SOFT 0x200A*/ /*#define AL_SEC_LENGTH_SOFT 0x200B*/ #endif #ifndef ALC_SOFT_pause_device #define ALC_SOFT_pause_device 1 typedef void (ALC_APIENTRY*LPALCDEVICEPAUSESOFT)(ALCdevice *device); typedef void (ALC_APIENTRY*LPALCDEVICERESUMESOFT)(ALCdevice *device); #ifdef AL_ALEXT_PROTOTYPES ALC_API void ALC_APIENTRY alcDevicePauseSOFT(ALCdevice *device); ALC_API void ALC_APIENTRY alcDeviceResumeSOFT(ALCdevice *device); #endif #endif #ifndef AL_EXT_BFORMAT #define AL_EXT_BFORMAT 1 #define AL_FORMAT_BFORMAT2D_8 0x20021 #define AL_FORMAT_BFORMAT2D_16 0x20022 #define AL_FORMAT_BFORMAT2D_FLOAT32 0x20023 #define AL_FORMAT_BFORMAT3D_8 0x20031 #define AL_FORMAT_BFORMAT3D_16 0x20032 #define AL_FORMAT_BFORMAT3D_FLOAT32 0x20033 #endif #ifndef AL_EXT_MULAW_BFORMAT #define AL_EXT_MULAW_BFORMAT 1 #define AL_FORMAT_BFORMAT2D_MULAW 0x10031 #define AL_FORMAT_BFORMAT3D_MULAW 0x10032 #endif #ifndef ALC_SOFT_HRTF #define ALC_SOFT_HRTF 1 #define ALC_HRTF_SOFT 0x1992 #define ALC_DONT_CARE_SOFT 0x0002 #define ALC_HRTF_STATUS_SOFT 0x1993 #define ALC_HRTF_DISABLED_SOFT 0x0000 #define ALC_HRTF_ENABLED_SOFT 0x0001 #define ALC_HRTF_DENIED_SOFT 0x0002 #define ALC_HRTF_REQUIRED_SOFT 0x0003 #define ALC_HRTF_HEADPHONES_DETECTED_SOFT 0x0004 #define ALC_HRTF_UNSUPPORTED_FORMAT_SOFT 0x0005 #define ALC_NUM_HRTF_SPECIFIERS_SOFT 0x1994 #define ALC_HRTF_SPECIFIER_SOFT 0x1995 #define ALC_HRTF_ID_SOFT 0x1996 typedef const ALCchar* (ALC_APIENTRY*LPALCGETSTRINGISOFT)(ALCdevice *device, ALCenum paramName, ALCsizei index); typedef ALCboolean (ALC_APIENTRY*LPALCRESETDEVICESOFT)(ALCdevice *device, const ALCint *attribs); #ifdef AL_ALEXT_PROTOTYPES ALC_API const ALCchar* ALC_APIENTRY alcGetStringiSOFT(ALCdevice *device, ALCenum paramName, ALCsizei index); ALC_API ALCboolean ALC_APIENTRY alcResetDeviceSOFT(ALCdevice *device, const ALCint *attribs); #endif #endif #ifndef AL_SOFT_gain_clamp_ex #define AL_SOFT_gain_clamp_ex 1 #define AL_GAIN_LIMIT_SOFT 0x200E #endif #ifndef AL_SOFT_source_resampler #define AL_SOFT_source_resampler #define AL_NUM_RESAMPLERS_SOFT 0x1210 #define AL_DEFAULT_RESAMPLER_SOFT 0x1211 #define AL_SOURCE_RESAMPLER_SOFT 0x1212 #define AL_RESAMPLER_NAME_SOFT 0x1213 typedef const ALchar* (AL_APIENTRY*LPALGETSTRINGISOFT)(ALenum pname, ALsizei index); #ifdef AL_ALEXT_PROTOTYPES AL_API const ALchar* AL_APIENTRY alGetStringiSOFT(ALenum pname, ALsizei index); #endif #endif #ifndef AL_SOFT_source_spatialize #define AL_SOFT_source_spatialize #define AL_SOURCE_SPATIALIZE_SOFT 0x1214 #define AL_AUTO_SOFT 0x0002 #endif #ifndef ALC_SOFT_output_limiter #define ALC_SOFT_output_limiter #define ALC_OUTPUT_LIMITER_SOFT 0x199A #endif #ifdef __cplusplus } #endif #endif ================================================ FILE: apps/openmw/mwsound/efx-presets.h ================================================ /* Reverb presets for EFX */ #ifndef EFX_PRESETS_H #define EFX_PRESETS_H #ifndef EFXEAXREVERBPROPERTIES_DEFINED #define EFXEAXREVERBPROPERTIES_DEFINED typedef struct { float flDensity; float flDiffusion; float flGain; float flGainHF; float flGainLF; float flDecayTime; float flDecayHFRatio; float flDecayLFRatio; float flReflectionsGain; float flReflectionsDelay; float flReflectionsPan[3]; float flLateReverbGain; float flLateReverbDelay; float flLateReverbPan[3]; float flEchoTime; float flEchoDepth; float flModulationTime; float flModulationDepth; float flAirAbsorptionGainHF; float flHFReference; float flLFReference; float flRoomRolloffFactor; int iDecayHFLimit; } EFXEAXREVERBPROPERTIES, *LPEFXEAXREVERBPROPERTIES; #endif /* Default Presets */ #define EFX_REVERB_PRESET_GENERIC \ { 1.0000f, 1.0000f, 0.3162f, 0.8913f, 1.0000f, 1.4900f, 0.8300f, 1.0000f, 0.0500f, 0.0070f, { 0.0000f, 0.0000f, 0.0000f }, 1.2589f, 0.0110f, { 0.0000f, 0.0000f, 0.0000f }, 0.2500f, 0.0000f, 0.2500f, 0.0000f, 0.9943f, 5000.0000f, 250.0000f, 0.0000f, 0x1 } #define EFX_REVERB_PRESET_PADDEDCELL \ { 0.1715f, 1.0000f, 0.3162f, 0.0010f, 1.0000f, 0.1700f, 0.1000f, 1.0000f, 0.2500f, 0.0010f, { 0.0000f, 0.0000f, 0.0000f }, 1.2691f, 0.0020f, { 0.0000f, 0.0000f, 0.0000f }, 0.2500f, 0.0000f, 0.2500f, 0.0000f, 0.9943f, 5000.0000f, 250.0000f, 0.0000f, 0x1 } #define EFX_REVERB_PRESET_ROOM \ { 0.4287f, 1.0000f, 0.3162f, 0.5929f, 1.0000f, 0.4000f, 0.8300f, 1.0000f, 0.1503f, 0.0020f, { 0.0000f, 0.0000f, 0.0000f }, 1.0629f, 0.0030f, { 0.0000f, 0.0000f, 0.0000f }, 0.2500f, 0.0000f, 0.2500f, 0.0000f, 0.9943f, 5000.0000f, 250.0000f, 0.0000f, 0x1 } #define EFX_REVERB_PRESET_BATHROOM \ { 0.1715f, 1.0000f, 0.3162f, 0.2512f, 1.0000f, 1.4900f, 0.5400f, 1.0000f, 0.6531f, 0.0070f, { 0.0000f, 0.0000f, 0.0000f }, 3.2734f, 0.0110f, { 0.0000f, 0.0000f, 0.0000f }, 0.2500f, 0.0000f, 0.2500f, 0.0000f, 0.9943f, 5000.0000f, 250.0000f, 0.0000f, 0x1 } #define EFX_REVERB_PRESET_LIVINGROOM \ { 0.9766f, 1.0000f, 0.3162f, 0.0010f, 1.0000f, 0.5000f, 0.1000f, 1.0000f, 0.2051f, 0.0030f, { 0.0000f, 0.0000f, 0.0000f }, 0.2805f, 0.0040f, { 0.0000f, 0.0000f, 0.0000f }, 0.2500f, 0.0000f, 0.2500f, 0.0000f, 0.9943f, 5000.0000f, 250.0000f, 0.0000f, 0x1 } #define EFX_REVERB_PRESET_STONEROOM \ { 1.0000f, 1.0000f, 0.3162f, 0.7079f, 1.0000f, 2.3100f, 0.6400f, 1.0000f, 0.4411f, 0.0120f, { 0.0000f, 0.0000f, 0.0000f }, 1.1003f, 0.0170f, { 0.0000f, 0.0000f, 0.0000f }, 0.2500f, 0.0000f, 0.2500f, 0.0000f, 0.9943f, 5000.0000f, 250.0000f, 0.0000f, 0x1 } #define EFX_REVERB_PRESET_AUDITORIUM \ { 1.0000f, 1.0000f, 0.3162f, 0.5781f, 1.0000f, 4.3200f, 0.5900f, 1.0000f, 0.4032f, 0.0200f, { 0.0000f, 0.0000f, 0.0000f }, 0.7170f, 0.0300f, { 0.0000f, 0.0000f, 0.0000f }, 0.2500f, 0.0000f, 0.2500f, 0.0000f, 0.9943f, 5000.0000f, 250.0000f, 0.0000f, 0x1 } #define EFX_REVERB_PRESET_CONCERTHALL \ { 1.0000f, 1.0000f, 0.3162f, 0.5623f, 1.0000f, 3.9200f, 0.7000f, 1.0000f, 0.2427f, 0.0200f, { 0.0000f, 0.0000f, 0.0000f }, 0.9977f, 0.0290f, { 0.0000f, 0.0000f, 0.0000f }, 0.2500f, 0.0000f, 0.2500f, 0.0000f, 0.9943f, 5000.0000f, 250.0000f, 0.0000f, 0x1 } #define EFX_REVERB_PRESET_CAVE \ { 1.0000f, 1.0000f, 0.3162f, 1.0000f, 1.0000f, 2.9100f, 1.3000f, 1.0000f, 0.5000f, 0.0150f, { 0.0000f, 0.0000f, 0.0000f }, 0.7063f, 0.0220f, { 0.0000f, 0.0000f, 0.0000f }, 0.2500f, 0.0000f, 0.2500f, 0.0000f, 0.9943f, 5000.0000f, 250.0000f, 0.0000f, 0x0 } #define EFX_REVERB_PRESET_ARENA \ { 1.0000f, 1.0000f, 0.3162f, 0.4477f, 1.0000f, 7.2400f, 0.3300f, 1.0000f, 0.2612f, 0.0200f, { 0.0000f, 0.0000f, 0.0000f }, 1.0186f, 0.0300f, { 0.0000f, 0.0000f, 0.0000f }, 0.2500f, 0.0000f, 0.2500f, 0.0000f, 0.9943f, 5000.0000f, 250.0000f, 0.0000f, 0x1 } #define EFX_REVERB_PRESET_HANGAR \ { 1.0000f, 1.0000f, 0.3162f, 0.3162f, 1.0000f, 10.0500f, 0.2300f, 1.0000f, 0.5000f, 0.0200f, { 0.0000f, 0.0000f, 0.0000f }, 1.2560f, 0.0300f, { 0.0000f, 0.0000f, 0.0000f }, 0.2500f, 0.0000f, 0.2500f, 0.0000f, 0.9943f, 5000.0000f, 250.0000f, 0.0000f, 0x1 } #define EFX_REVERB_PRESET_CARPETEDHALLWAY \ { 0.4287f, 1.0000f, 0.3162f, 0.0100f, 1.0000f, 0.3000f, 0.1000f, 1.0000f, 0.1215f, 0.0020f, { 0.0000f, 0.0000f, 0.0000f }, 0.1531f, 0.0300f, { 0.0000f, 0.0000f, 0.0000f }, 0.2500f, 0.0000f, 0.2500f, 0.0000f, 0.9943f, 5000.0000f, 250.0000f, 0.0000f, 0x1 } #define EFX_REVERB_PRESET_HALLWAY \ { 0.3645f, 1.0000f, 0.3162f, 0.7079f, 1.0000f, 1.4900f, 0.5900f, 1.0000f, 0.2458f, 0.0070f, { 0.0000f, 0.0000f, 0.0000f }, 1.6615f, 0.0110f, { 0.0000f, 0.0000f, 0.0000f }, 0.2500f, 0.0000f, 0.2500f, 0.0000f, 0.9943f, 5000.0000f, 250.0000f, 0.0000f, 0x1 } #define EFX_REVERB_PRESET_STONECORRIDOR \ { 1.0000f, 1.0000f, 0.3162f, 0.7612f, 1.0000f, 2.7000f, 0.7900f, 1.0000f, 0.2472f, 0.0130f, { 0.0000f, 0.0000f, 0.0000f }, 1.5758f, 0.0200f, { 0.0000f, 0.0000f, 0.0000f }, 0.2500f, 0.0000f, 0.2500f, 0.0000f, 0.9943f, 5000.0000f, 250.0000f, 0.0000f, 0x1 } #define EFX_REVERB_PRESET_ALLEY \ { 1.0000f, 0.3000f, 0.3162f, 0.7328f, 1.0000f, 1.4900f, 0.8600f, 1.0000f, 0.2500f, 0.0070f, { 0.0000f, 0.0000f, 0.0000f }, 0.9954f, 0.0110f, { 0.0000f, 0.0000f, 0.0000f }, 0.1250f, 0.9500f, 0.2500f, 0.0000f, 0.9943f, 5000.0000f, 250.0000f, 0.0000f, 0x1 } #define EFX_REVERB_PRESET_FOREST \ { 1.0000f, 0.3000f, 0.3162f, 0.0224f, 1.0000f, 1.4900f, 0.5400f, 1.0000f, 0.0525f, 0.1620f, { 0.0000f, 0.0000f, 0.0000f }, 0.7682f, 0.0880f, { 0.0000f, 0.0000f, 0.0000f }, 0.1250f, 1.0000f, 0.2500f, 0.0000f, 0.9943f, 5000.0000f, 250.0000f, 0.0000f, 0x1 } #define EFX_REVERB_PRESET_CITY \ { 1.0000f, 0.5000f, 0.3162f, 0.3981f, 1.0000f, 1.4900f, 0.6700f, 1.0000f, 0.0730f, 0.0070f, { 0.0000f, 0.0000f, 0.0000f }, 0.1427f, 0.0110f, { 0.0000f, 0.0000f, 0.0000f }, 0.2500f, 0.0000f, 0.2500f, 0.0000f, 0.9943f, 5000.0000f, 250.0000f, 0.0000f, 0x1 } #define EFX_REVERB_PRESET_MOUNTAINS \ { 1.0000f, 0.2700f, 0.3162f, 0.0562f, 1.0000f, 1.4900f, 0.2100f, 1.0000f, 0.0407f, 0.3000f, { 0.0000f, 0.0000f, 0.0000f }, 0.1919f, 0.1000f, { 0.0000f, 0.0000f, 0.0000f }, 0.2500f, 1.0000f, 0.2500f, 0.0000f, 0.9943f, 5000.0000f, 250.0000f, 0.0000f, 0x0 } #define EFX_REVERB_PRESET_QUARRY \ { 1.0000f, 1.0000f, 0.3162f, 0.3162f, 1.0000f, 1.4900f, 0.8300f, 1.0000f, 0.0000f, 0.0610f, { 0.0000f, 0.0000f, 0.0000f }, 1.7783f, 0.0250f, { 0.0000f, 0.0000f, 0.0000f }, 0.1250f, 0.7000f, 0.2500f, 0.0000f, 0.9943f, 5000.0000f, 250.0000f, 0.0000f, 0x1 } #define EFX_REVERB_PRESET_PLAIN \ { 1.0000f, 0.2100f, 0.3162f, 0.1000f, 1.0000f, 1.4900f, 0.5000f, 1.0000f, 0.0585f, 0.1790f, { 0.0000f, 0.0000f, 0.0000f }, 0.1089f, 0.1000f, { 0.0000f, 0.0000f, 0.0000f }, 0.2500f, 1.0000f, 0.2500f, 0.0000f, 0.9943f, 5000.0000f, 250.0000f, 0.0000f, 0x1 } #define EFX_REVERB_PRESET_PARKINGLOT \ { 1.0000f, 1.0000f, 0.3162f, 1.0000f, 1.0000f, 1.6500f, 1.5000f, 1.0000f, 0.2082f, 0.0080f, { 0.0000f, 0.0000f, 0.0000f }, 0.2652f, 0.0120f, { 0.0000f, 0.0000f, 0.0000f }, 0.2500f, 0.0000f, 0.2500f, 0.0000f, 0.9943f, 5000.0000f, 250.0000f, 0.0000f, 0x0 } #define EFX_REVERB_PRESET_SEWERPIPE \ { 0.3071f, 0.8000f, 0.3162f, 0.3162f, 1.0000f, 2.8100f, 0.1400f, 1.0000f, 1.6387f, 0.0140f, { 0.0000f, 0.0000f, 0.0000f }, 3.2471f, 0.0210f, { 0.0000f, 0.0000f, 0.0000f }, 0.2500f, 0.0000f, 0.2500f, 0.0000f, 0.9943f, 5000.0000f, 250.0000f, 0.0000f, 0x1 } #define EFX_REVERB_PRESET_UNDERWATER \ { 0.3645f, 1.0000f, 0.3162f, 0.0100f, 1.0000f, 1.4900f, 0.1000f, 1.0000f, 0.5963f, 0.0070f, { 0.0000f, 0.0000f, 0.0000f }, 7.0795f, 0.0110f, { 0.0000f, 0.0000f, 0.0000f }, 0.2500f, 0.0000f, 1.1800f, 0.3480f, 0.9943f, 5000.0000f, 250.0000f, 0.0000f, 0x1 } #define EFX_REVERB_PRESET_DRUGGED \ { 0.4287f, 0.5000f, 0.3162f, 1.0000f, 1.0000f, 8.3900f, 1.3900f, 1.0000f, 0.8760f, 0.0020f, { 0.0000f, 0.0000f, 0.0000f }, 3.1081f, 0.0300f, { 0.0000f, 0.0000f, 0.0000f }, 0.2500f, 0.0000f, 0.2500f, 1.0000f, 0.9943f, 5000.0000f, 250.0000f, 0.0000f, 0x0 } #define EFX_REVERB_PRESET_DIZZY \ { 0.3645f, 0.6000f, 0.3162f, 0.6310f, 1.0000f, 17.2300f, 0.5600f, 1.0000f, 0.1392f, 0.0200f, { 0.0000f, 0.0000f, 0.0000f }, 0.4937f, 0.0300f, { 0.0000f, 0.0000f, 0.0000f }, 0.2500f, 1.0000f, 0.8100f, 0.3100f, 0.9943f, 5000.0000f, 250.0000f, 0.0000f, 0x0 } #define EFX_REVERB_PRESET_PSYCHOTIC \ { 0.0625f, 0.5000f, 0.3162f, 0.8404f, 1.0000f, 7.5600f, 0.9100f, 1.0000f, 0.4864f, 0.0200f, { 0.0000f, 0.0000f, 0.0000f }, 2.4378f, 0.0300f, { 0.0000f, 0.0000f, 0.0000f }, 0.2500f, 0.0000f, 4.0000f, 1.0000f, 0.9943f, 5000.0000f, 250.0000f, 0.0000f, 0x0 } /* Castle Presets */ #define EFX_REVERB_PRESET_CASTLE_SMALLROOM \ { 1.0000f, 0.8900f, 0.3162f, 0.3981f, 0.1000f, 1.2200f, 0.8300f, 0.3100f, 0.8913f, 0.0220f, { 0.0000f, 0.0000f, 0.0000f }, 1.9953f, 0.0110f, { 0.0000f, 0.0000f, 0.0000f }, 0.1380f, 0.0800f, 0.2500f, 0.0000f, 0.9943f, 5168.6001f, 139.5000f, 0.0000f, 0x1 } #define EFX_REVERB_PRESET_CASTLE_SHORTPASSAGE \ { 1.0000f, 0.8900f, 0.3162f, 0.3162f, 0.1000f, 2.3200f, 0.8300f, 0.3100f, 0.8913f, 0.0070f, { 0.0000f, 0.0000f, 0.0000f }, 1.2589f, 0.0230f, { 0.0000f, 0.0000f, 0.0000f }, 0.1380f, 0.0800f, 0.2500f, 0.0000f, 0.9943f, 5168.6001f, 139.5000f, 0.0000f, 0x1 } #define EFX_REVERB_PRESET_CASTLE_MEDIUMROOM \ { 1.0000f, 0.9300f, 0.3162f, 0.2818f, 0.1000f, 2.0400f, 0.8300f, 0.4600f, 0.6310f, 0.0220f, { 0.0000f, 0.0000f, 0.0000f }, 1.5849f, 0.0110f, { 0.0000f, 0.0000f, 0.0000f }, 0.1550f, 0.0300f, 0.2500f, 0.0000f, 0.9943f, 5168.6001f, 139.5000f, 0.0000f, 0x1 } #define EFX_REVERB_PRESET_CASTLE_LARGEROOM \ { 1.0000f, 0.8200f, 0.3162f, 0.2818f, 0.1259f, 2.5300f, 0.8300f, 0.5000f, 0.4467f, 0.0340f, { 0.0000f, 0.0000f, 0.0000f }, 1.2589f, 0.0160f, { 0.0000f, 0.0000f, 0.0000f }, 0.1850f, 0.0700f, 0.2500f, 0.0000f, 0.9943f, 5168.6001f, 139.5000f, 0.0000f, 0x1 } #define EFX_REVERB_PRESET_CASTLE_LONGPASSAGE \ { 1.0000f, 0.8900f, 0.3162f, 0.3981f, 0.1000f, 3.4200f, 0.8300f, 0.3100f, 0.8913f, 0.0070f, { 0.0000f, 0.0000f, 0.0000f }, 1.4125f, 0.0230f, { 0.0000f, 0.0000f, 0.0000f }, 0.1380f, 0.0800f, 0.2500f, 0.0000f, 0.9943f, 5168.6001f, 139.5000f, 0.0000f, 0x1 } #define EFX_REVERB_PRESET_CASTLE_HALL \ { 1.0000f, 0.8100f, 0.3162f, 0.2818f, 0.1778f, 3.1400f, 0.7900f, 0.6200f, 0.1778f, 0.0560f, { 0.0000f, 0.0000f, 0.0000f }, 1.1220f, 0.0240f, { 0.0000f, 0.0000f, 0.0000f }, 0.2500f, 0.0000f, 0.2500f, 0.0000f, 0.9943f, 5168.6001f, 139.5000f, 0.0000f, 0x1 } #define EFX_REVERB_PRESET_CASTLE_CUPBOARD \ { 1.0000f, 0.8900f, 0.3162f, 0.2818f, 0.1000f, 0.6700f, 0.8700f, 0.3100f, 1.4125f, 0.0100f, { 0.0000f, 0.0000f, 0.0000f }, 3.5481f, 0.0070f, { 0.0000f, 0.0000f, 0.0000f }, 0.1380f, 0.0800f, 0.2500f, 0.0000f, 0.9943f, 5168.6001f, 139.5000f, 0.0000f, 0x1 } #define EFX_REVERB_PRESET_CASTLE_COURTYARD \ { 1.0000f, 0.4200f, 0.3162f, 0.4467f, 0.1995f, 2.1300f, 0.6100f, 0.2300f, 0.2239f, 0.1600f, { 0.0000f, 0.0000f, 0.0000f }, 0.7079f, 0.0360f, { 0.0000f, 0.0000f, 0.0000f }, 0.2500f, 0.3700f, 0.2500f, 0.0000f, 0.9943f, 5000.0000f, 250.0000f, 0.0000f, 0x0 } #define EFX_REVERB_PRESET_CASTLE_ALCOVE \ { 1.0000f, 0.8900f, 0.3162f, 0.5012f, 0.1000f, 1.6400f, 0.8700f, 0.3100f, 1.0000f, 0.0070f, { 0.0000f, 0.0000f, 0.0000f }, 1.4125f, 0.0340f, { 0.0000f, 0.0000f, 0.0000f }, 0.1380f, 0.0800f, 0.2500f, 0.0000f, 0.9943f, 5168.6001f, 139.5000f, 0.0000f, 0x1 } /* Factory Presets */ #define EFX_REVERB_PRESET_FACTORY_SMALLROOM \ { 0.3645f, 0.8200f, 0.3162f, 0.7943f, 0.5012f, 1.7200f, 0.6500f, 1.3100f, 0.7079f, 0.0100f, { 0.0000f, 0.0000f, 0.0000f }, 1.7783f, 0.0240f, { 0.0000f, 0.0000f, 0.0000f }, 0.1190f, 0.0700f, 0.2500f, 0.0000f, 0.9943f, 3762.6001f, 362.5000f, 0.0000f, 0x1 } #define EFX_REVERB_PRESET_FACTORY_SHORTPASSAGE \ { 0.3645f, 0.6400f, 0.2512f, 0.7943f, 0.5012f, 2.5300f, 0.6500f, 1.3100f, 1.0000f, 0.0100f, { 0.0000f, 0.0000f, 0.0000f }, 1.2589f, 0.0380f, { 0.0000f, 0.0000f, 0.0000f }, 0.1350f, 0.2300f, 0.2500f, 0.0000f, 0.9943f, 3762.6001f, 362.5000f, 0.0000f, 0x1 } #define EFX_REVERB_PRESET_FACTORY_MEDIUMROOM \ { 0.4287f, 0.8200f, 0.2512f, 0.7943f, 0.5012f, 2.7600f, 0.6500f, 1.3100f, 0.2818f, 0.0220f, { 0.0000f, 0.0000f, 0.0000f }, 1.4125f, 0.0230f, { 0.0000f, 0.0000f, 0.0000f }, 0.1740f, 0.0700f, 0.2500f, 0.0000f, 0.9943f, 3762.6001f, 362.5000f, 0.0000f, 0x1 } #define EFX_REVERB_PRESET_FACTORY_LARGEROOM \ { 0.4287f, 0.7500f, 0.2512f, 0.7079f, 0.6310f, 4.2400f, 0.5100f, 1.3100f, 0.1778f, 0.0390f, { 0.0000f, 0.0000f, 0.0000f }, 1.1220f, 0.0230f, { 0.0000f, 0.0000f, 0.0000f }, 0.2310f, 0.0700f, 0.2500f, 0.0000f, 0.9943f, 3762.6001f, 362.5000f, 0.0000f, 0x1 } #define EFX_REVERB_PRESET_FACTORY_LONGPASSAGE \ { 0.3645f, 0.6400f, 0.2512f, 0.7943f, 0.5012f, 4.0600f, 0.6500f, 1.3100f, 1.0000f, 0.0200f, { 0.0000f, 0.0000f, 0.0000f }, 1.2589f, 0.0370f, { 0.0000f, 0.0000f, 0.0000f }, 0.1350f, 0.2300f, 0.2500f, 0.0000f, 0.9943f, 3762.6001f, 362.5000f, 0.0000f, 0x1 } #define EFX_REVERB_PRESET_FACTORY_HALL \ { 0.4287f, 0.7500f, 0.3162f, 0.7079f, 0.6310f, 7.4300f, 0.5100f, 1.3100f, 0.0631f, 0.0730f, { 0.0000f, 0.0000f, 0.0000f }, 0.8913f, 0.0270f, { 0.0000f, 0.0000f, 0.0000f }, 0.2500f, 0.0700f, 0.2500f, 0.0000f, 0.9943f, 3762.6001f, 362.5000f, 0.0000f, 0x1 } #define EFX_REVERB_PRESET_FACTORY_CUPBOARD \ { 0.3071f, 0.6300f, 0.2512f, 0.7943f, 0.5012f, 0.4900f, 0.6500f, 1.3100f, 1.2589f, 0.0100f, { 0.0000f, 0.0000f, 0.0000f }, 1.9953f, 0.0320f, { 0.0000f, 0.0000f, 0.0000f }, 0.1070f, 0.0700f, 0.2500f, 0.0000f, 0.9943f, 3762.6001f, 362.5000f, 0.0000f, 0x1 } #define EFX_REVERB_PRESET_FACTORY_COURTYARD \ { 0.3071f, 0.5700f, 0.3162f, 0.3162f, 0.6310f, 2.3200f, 0.2900f, 0.5600f, 0.2239f, 0.1400f, { 0.0000f, 0.0000f, 0.0000f }, 0.3981f, 0.0390f, { 0.0000f, 0.0000f, 0.0000f }, 0.2500f, 0.2900f, 0.2500f, 0.0000f, 0.9943f, 3762.6001f, 362.5000f, 0.0000f, 0x1 } #define EFX_REVERB_PRESET_FACTORY_ALCOVE \ { 0.3645f, 0.5900f, 0.2512f, 0.7943f, 0.5012f, 3.1400f, 0.6500f, 1.3100f, 1.4125f, 0.0100f, { 0.0000f, 0.0000f, 0.0000f }, 1.0000f, 0.0380f, { 0.0000f, 0.0000f, 0.0000f }, 0.1140f, 0.1000f, 0.2500f, 0.0000f, 0.9943f, 3762.6001f, 362.5000f, 0.0000f, 0x1 } /* Ice Palace Presets */ #define EFX_REVERB_PRESET_ICEPALACE_SMALLROOM \ { 1.0000f, 0.8400f, 0.3162f, 0.5623f, 0.2818f, 1.5100f, 1.5300f, 0.2700f, 0.8913f, 0.0100f, { 0.0000f, 0.0000f, 0.0000f }, 1.4125f, 0.0110f, { 0.0000f, 0.0000f, 0.0000f }, 0.1640f, 0.1400f, 0.2500f, 0.0000f, 0.9943f, 12428.5000f, 99.6000f, 0.0000f, 0x1 } #define EFX_REVERB_PRESET_ICEPALACE_SHORTPASSAGE \ { 1.0000f, 0.7500f, 0.3162f, 0.5623f, 0.2818f, 1.7900f, 1.4600f, 0.2800f, 0.5012f, 0.0100f, { 0.0000f, 0.0000f, 0.0000f }, 1.1220f, 0.0190f, { 0.0000f, 0.0000f, 0.0000f }, 0.1770f, 0.0900f, 0.2500f, 0.0000f, 0.9943f, 12428.5000f, 99.6000f, 0.0000f, 0x1 } #define EFX_REVERB_PRESET_ICEPALACE_MEDIUMROOM \ { 1.0000f, 0.8700f, 0.3162f, 0.5623f, 0.4467f, 2.2200f, 1.5300f, 0.3200f, 0.3981f, 0.0390f, { 0.0000f, 0.0000f, 0.0000f }, 1.1220f, 0.0270f, { 0.0000f, 0.0000f, 0.0000f }, 0.1860f, 0.1200f, 0.2500f, 0.0000f, 0.9943f, 12428.5000f, 99.6000f, 0.0000f, 0x1 } #define EFX_REVERB_PRESET_ICEPALACE_LARGEROOM \ { 1.0000f, 0.8100f, 0.3162f, 0.5623f, 0.4467f, 3.1400f, 1.5300f, 0.3200f, 0.2512f, 0.0390f, { 0.0000f, 0.0000f, 0.0000f }, 1.0000f, 0.0270f, { 0.0000f, 0.0000f, 0.0000f }, 0.2140f, 0.1100f, 0.2500f, 0.0000f, 0.9943f, 12428.5000f, 99.6000f, 0.0000f, 0x1 } #define EFX_REVERB_PRESET_ICEPALACE_LONGPASSAGE \ { 1.0000f, 0.7700f, 0.3162f, 0.5623f, 0.3981f, 3.0100f, 1.4600f, 0.2800f, 0.7943f, 0.0120f, { 0.0000f, 0.0000f, 0.0000f }, 1.2589f, 0.0250f, { 0.0000f, 0.0000f, 0.0000f }, 0.1860f, 0.0400f, 0.2500f, 0.0000f, 0.9943f, 12428.5000f, 99.6000f, 0.0000f, 0x1 } #define EFX_REVERB_PRESET_ICEPALACE_HALL \ { 1.0000f, 0.7600f, 0.3162f, 0.4467f, 0.5623f, 5.4900f, 1.5300f, 0.3800f, 0.1122f, 0.0540f, { 0.0000f, 0.0000f, 0.0000f }, 0.6310f, 0.0520f, { 0.0000f, 0.0000f, 0.0000f }, 0.2260f, 0.1100f, 0.2500f, 0.0000f, 0.9943f, 12428.5000f, 99.6000f, 0.0000f, 0x1 } #define EFX_REVERB_PRESET_ICEPALACE_CUPBOARD \ { 1.0000f, 0.8300f, 0.3162f, 0.5012f, 0.2239f, 0.7600f, 1.5300f, 0.2600f, 1.1220f, 0.0120f, { 0.0000f, 0.0000f, 0.0000f }, 1.9953f, 0.0160f, { 0.0000f, 0.0000f, 0.0000f }, 0.1430f, 0.0800f, 0.2500f, 0.0000f, 0.9943f, 12428.5000f, 99.6000f, 0.0000f, 0x1 } #define EFX_REVERB_PRESET_ICEPALACE_COURTYARD \ { 1.0000f, 0.5900f, 0.3162f, 0.2818f, 0.3162f, 2.0400f, 1.2000f, 0.3800f, 0.3162f, 0.1730f, { 0.0000f, 0.0000f, 0.0000f }, 0.3162f, 0.0430f, { 0.0000f, 0.0000f, 0.0000f }, 0.2350f, 0.4800f, 0.2500f, 0.0000f, 0.9943f, 12428.5000f, 99.6000f, 0.0000f, 0x1 } #define EFX_REVERB_PRESET_ICEPALACE_ALCOVE \ { 1.0000f, 0.8400f, 0.3162f, 0.5623f, 0.2818f, 2.7600f, 1.4600f, 0.2800f, 1.1220f, 0.0100f, { 0.0000f, 0.0000f, 0.0000f }, 0.8913f, 0.0300f, { 0.0000f, 0.0000f, 0.0000f }, 0.1610f, 0.0900f, 0.2500f, 0.0000f, 0.9943f, 12428.5000f, 99.6000f, 0.0000f, 0x1 } /* Space Station Presets */ #define EFX_REVERB_PRESET_SPACESTATION_SMALLROOM \ { 0.2109f, 0.7000f, 0.3162f, 0.7079f, 0.8913f, 1.7200f, 0.8200f, 0.5500f, 0.7943f, 0.0070f, { 0.0000f, 0.0000f, 0.0000f }, 1.4125f, 0.0130f, { 0.0000f, 0.0000f, 0.0000f }, 0.1880f, 0.2600f, 0.2500f, 0.0000f, 0.9943f, 3316.1001f, 458.2000f, 0.0000f, 0x1 } #define EFX_REVERB_PRESET_SPACESTATION_SHORTPASSAGE \ { 0.2109f, 0.8700f, 0.3162f, 0.6310f, 0.8913f, 3.5700f, 0.5000f, 0.5500f, 1.0000f, 0.0120f, { 0.0000f, 0.0000f, 0.0000f }, 1.1220f, 0.0160f, { 0.0000f, 0.0000f, 0.0000f }, 0.1720f, 0.2000f, 0.2500f, 0.0000f, 0.9943f, 3316.1001f, 458.2000f, 0.0000f, 0x1 } #define EFX_REVERB_PRESET_SPACESTATION_MEDIUMROOM \ { 0.2109f, 0.7500f, 0.3162f, 0.6310f, 0.8913f, 3.0100f, 0.5000f, 0.5500f, 0.3981f, 0.0340f, { 0.0000f, 0.0000f, 0.0000f }, 1.1220f, 0.0350f, { 0.0000f, 0.0000f, 0.0000f }, 0.2090f, 0.3100f, 0.2500f, 0.0000f, 0.9943f, 3316.1001f, 458.2000f, 0.0000f, 0x1 } #define EFX_REVERB_PRESET_SPACESTATION_LARGEROOM \ { 0.3645f, 0.8100f, 0.3162f, 0.6310f, 0.8913f, 3.8900f, 0.3800f, 0.6100f, 0.3162f, 0.0560f, { 0.0000f, 0.0000f, 0.0000f }, 0.8913f, 0.0350f, { 0.0000f, 0.0000f, 0.0000f }, 0.2330f, 0.2800f, 0.2500f, 0.0000f, 0.9943f, 3316.1001f, 458.2000f, 0.0000f, 0x1 } #define EFX_REVERB_PRESET_SPACESTATION_LONGPASSAGE \ { 0.4287f, 0.8200f, 0.3162f, 0.6310f, 0.8913f, 4.6200f, 0.6200f, 0.5500f, 1.0000f, 0.0120f, { 0.0000f, 0.0000f, 0.0000f }, 1.2589f, 0.0310f, { 0.0000f, 0.0000f, 0.0000f }, 0.2500f, 0.2300f, 0.2500f, 0.0000f, 0.9943f, 3316.1001f, 458.2000f, 0.0000f, 0x1 } #define EFX_REVERB_PRESET_SPACESTATION_HALL \ { 0.4287f, 0.8700f, 0.3162f, 0.6310f, 0.8913f, 7.1100f, 0.3800f, 0.6100f, 0.1778f, 0.1000f, { 0.0000f, 0.0000f, 0.0000f }, 0.6310f, 0.0470f, { 0.0000f, 0.0000f, 0.0000f }, 0.2500f, 0.2500f, 0.2500f, 0.0000f, 0.9943f, 3316.1001f, 458.2000f, 0.0000f, 0x1 } #define EFX_REVERB_PRESET_SPACESTATION_CUPBOARD \ { 0.1715f, 0.5600f, 0.3162f, 0.7079f, 0.8913f, 0.7900f, 0.8100f, 0.5500f, 1.4125f, 0.0070f, { 0.0000f, 0.0000f, 0.0000f }, 1.7783f, 0.0180f, { 0.0000f, 0.0000f, 0.0000f }, 0.1810f, 0.3100f, 0.2500f, 0.0000f, 0.9943f, 3316.1001f, 458.2000f, 0.0000f, 0x1 } #define EFX_REVERB_PRESET_SPACESTATION_ALCOVE \ { 0.2109f, 0.7800f, 0.3162f, 0.7079f, 0.8913f, 1.1600f, 0.8100f, 0.5500f, 1.4125f, 0.0070f, { 0.0000f, 0.0000f, 0.0000f }, 1.0000f, 0.0180f, { 0.0000f, 0.0000f, 0.0000f }, 0.1920f, 0.2100f, 0.2500f, 0.0000f, 0.9943f, 3316.1001f, 458.2000f, 0.0000f, 0x1 } /* Wooden Galleon Presets */ #define EFX_REVERB_PRESET_WOODEN_SMALLROOM \ { 1.0000f, 1.0000f, 0.3162f, 0.1122f, 0.3162f, 0.7900f, 0.3200f, 0.8700f, 1.0000f, 0.0320f, { 0.0000f, 0.0000f, 0.0000f }, 0.8913f, 0.0290f, { 0.0000f, 0.0000f, 0.0000f }, 0.2500f, 0.0000f, 0.2500f, 0.0000f, 0.9943f, 4705.0000f, 99.6000f, 0.0000f, 0x1 } #define EFX_REVERB_PRESET_WOODEN_SHORTPASSAGE \ { 1.0000f, 1.0000f, 0.3162f, 0.1259f, 0.3162f, 1.7500f, 0.5000f, 0.8700f, 0.8913f, 0.0120f, { 0.0000f, 0.0000f, 0.0000f }, 0.6310f, 0.0240f, { 0.0000f, 0.0000f, 0.0000f }, 0.2500f, 0.0000f, 0.2500f, 0.0000f, 0.9943f, 4705.0000f, 99.6000f, 0.0000f, 0x1 } #define EFX_REVERB_PRESET_WOODEN_MEDIUMROOM \ { 1.0000f, 1.0000f, 0.3162f, 0.1000f, 0.2818f, 1.4700f, 0.4200f, 0.8200f, 0.8913f, 0.0490f, { 0.0000f, 0.0000f, 0.0000f }, 0.8913f, 0.0290f, { 0.0000f, 0.0000f, 0.0000f }, 0.2500f, 0.0000f, 0.2500f, 0.0000f, 0.9943f, 4705.0000f, 99.6000f, 0.0000f, 0x1 } #define EFX_REVERB_PRESET_WOODEN_LARGEROOM \ { 1.0000f, 1.0000f, 0.3162f, 0.0891f, 0.2818f, 2.6500f, 0.3300f, 0.8200f, 0.8913f, 0.0660f, { 0.0000f, 0.0000f, 0.0000f }, 0.7943f, 0.0490f, { 0.0000f, 0.0000f, 0.0000f }, 0.2500f, 0.0000f, 0.2500f, 0.0000f, 0.9943f, 4705.0000f, 99.6000f, 0.0000f, 0x1 } #define EFX_REVERB_PRESET_WOODEN_LONGPASSAGE \ { 1.0000f, 1.0000f, 0.3162f, 0.1000f, 0.3162f, 1.9900f, 0.4000f, 0.7900f, 1.0000f, 0.0200f, { 0.0000f, 0.0000f, 0.0000f }, 0.4467f, 0.0360f, { 0.0000f, 0.0000f, 0.0000f }, 0.2500f, 0.0000f, 0.2500f, 0.0000f, 0.9943f, 4705.0000f, 99.6000f, 0.0000f, 0x1 } #define EFX_REVERB_PRESET_WOODEN_HALL \ { 1.0000f, 1.0000f, 0.3162f, 0.0794f, 0.2818f, 3.4500f, 0.3000f, 0.8200f, 0.8913f, 0.0880f, { 0.0000f, 0.0000f, 0.0000f }, 0.7943f, 0.0630f, { 0.0000f, 0.0000f, 0.0000f }, 0.2500f, 0.0000f, 0.2500f, 0.0000f, 0.9943f, 4705.0000f, 99.6000f, 0.0000f, 0x1 } #define EFX_REVERB_PRESET_WOODEN_CUPBOARD \ { 1.0000f, 1.0000f, 0.3162f, 0.1413f, 0.3162f, 0.5600f, 0.4600f, 0.9100f, 1.1220f, 0.0120f, { 0.0000f, 0.0000f, 0.0000f }, 1.1220f, 0.0280f, { 0.0000f, 0.0000f, 0.0000f }, 0.2500f, 0.0000f, 0.2500f, 0.0000f, 0.9943f, 4705.0000f, 99.6000f, 0.0000f, 0x1 } #define EFX_REVERB_PRESET_WOODEN_COURTYARD \ { 1.0000f, 0.6500f, 0.3162f, 0.0794f, 0.3162f, 1.7900f, 0.3500f, 0.7900f, 0.5623f, 0.1230f, { 0.0000f, 0.0000f, 0.0000f }, 0.1000f, 0.0320f, { 0.0000f, 0.0000f, 0.0000f }, 0.2500f, 0.0000f, 0.2500f, 0.0000f, 0.9943f, 4705.0000f, 99.6000f, 0.0000f, 0x1 } #define EFX_REVERB_PRESET_WOODEN_ALCOVE \ { 1.0000f, 1.0000f, 0.3162f, 0.1259f, 0.3162f, 1.2200f, 0.6200f, 0.9100f, 1.1220f, 0.0120f, { 0.0000f, 0.0000f, 0.0000f }, 0.7079f, 0.0240f, { 0.0000f, 0.0000f, 0.0000f }, 0.2500f, 0.0000f, 0.2500f, 0.0000f, 0.9943f, 4705.0000f, 99.6000f, 0.0000f, 0x1 } /* Sports Presets */ #define EFX_REVERB_PRESET_SPORT_EMPTYSTADIUM \ { 1.0000f, 1.0000f, 0.3162f, 0.4467f, 0.7943f, 6.2600f, 0.5100f, 1.1000f, 0.0631f, 0.1830f, { 0.0000f, 0.0000f, 0.0000f }, 0.3981f, 0.0380f, { 0.0000f, 0.0000f, 0.0000f }, 0.2500f, 0.0000f, 0.2500f, 0.0000f, 0.9943f, 5000.0000f, 250.0000f, 0.0000f, 0x1 } #define EFX_REVERB_PRESET_SPORT_SQUASHCOURT \ { 1.0000f, 0.7500f, 0.3162f, 0.3162f, 0.7943f, 2.2200f, 0.9100f, 1.1600f, 0.4467f, 0.0070f, { 0.0000f, 0.0000f, 0.0000f }, 0.7943f, 0.0110f, { 0.0000f, 0.0000f, 0.0000f }, 0.1260f, 0.1900f, 0.2500f, 0.0000f, 0.9943f, 7176.8999f, 211.2000f, 0.0000f, 0x1 } #define EFX_REVERB_PRESET_SPORT_SMALLSWIMMINGPOOL \ { 1.0000f, 0.7000f, 0.3162f, 0.7943f, 0.8913f, 2.7600f, 1.2500f, 1.1400f, 0.6310f, 0.0200f, { 0.0000f, 0.0000f, 0.0000f }, 0.7943f, 0.0300f, { 0.0000f, 0.0000f, 0.0000f }, 0.1790f, 0.1500f, 0.8950f, 0.1900f, 0.9943f, 5000.0000f, 250.0000f, 0.0000f, 0x0 } #define EFX_REVERB_PRESET_SPORT_LARGESWIMMINGPOOL \ { 1.0000f, 0.8200f, 0.3162f, 0.7943f, 1.0000f, 5.4900f, 1.3100f, 1.1400f, 0.4467f, 0.0390f, { 0.0000f, 0.0000f, 0.0000f }, 0.5012f, 0.0490f, { 0.0000f, 0.0000f, 0.0000f }, 0.2220f, 0.5500f, 1.1590f, 0.2100f, 0.9943f, 5000.0000f, 250.0000f, 0.0000f, 0x0 } #define EFX_REVERB_PRESET_SPORT_GYMNASIUM \ { 1.0000f, 0.8100f, 0.3162f, 0.4467f, 0.8913f, 3.1400f, 1.0600f, 1.3500f, 0.3981f, 0.0290f, { 0.0000f, 0.0000f, 0.0000f }, 0.5623f, 0.0450f, { 0.0000f, 0.0000f, 0.0000f }, 0.1460f, 0.1400f, 0.2500f, 0.0000f, 0.9943f, 7176.8999f, 211.2000f, 0.0000f, 0x1 } #define EFX_REVERB_PRESET_SPORT_FULLSTADIUM \ { 1.0000f, 1.0000f, 0.3162f, 0.0708f, 0.7943f, 5.2500f, 0.1700f, 0.8000f, 0.1000f, 0.1880f, { 0.0000f, 0.0000f, 0.0000f }, 0.2818f, 0.0380f, { 0.0000f, 0.0000f, 0.0000f }, 0.2500f, 0.0000f, 0.2500f, 0.0000f, 0.9943f, 5000.0000f, 250.0000f, 0.0000f, 0x1 } #define EFX_REVERB_PRESET_SPORT_STADIUMTANNOY \ { 1.0000f, 0.7800f, 0.3162f, 0.5623f, 0.5012f, 2.5300f, 0.8800f, 0.6800f, 0.2818f, 0.2300f, { 0.0000f, 0.0000f, 0.0000f }, 0.5012f, 0.0630f, { 0.0000f, 0.0000f, 0.0000f }, 0.2500f, 0.2000f, 0.2500f, 0.0000f, 0.9943f, 5000.0000f, 250.0000f, 0.0000f, 0x1 } /* Prefab Presets */ #define EFX_REVERB_PRESET_PREFAB_WORKSHOP \ { 0.4287f, 1.0000f, 0.3162f, 0.1413f, 0.3981f, 0.7600f, 1.0000f, 1.0000f, 1.0000f, 0.0120f, { 0.0000f, 0.0000f, 0.0000f }, 1.1220f, 0.0120f, { 0.0000f, 0.0000f, 0.0000f }, 0.2500f, 0.0000f, 0.2500f, 0.0000f, 0.9943f, 5000.0000f, 250.0000f, 0.0000f, 0x0 } #define EFX_REVERB_PRESET_PREFAB_SCHOOLROOM \ { 0.4022f, 0.6900f, 0.3162f, 0.6310f, 0.5012f, 0.9800f, 0.4500f, 0.1800f, 1.4125f, 0.0170f, { 0.0000f, 0.0000f, 0.0000f }, 1.4125f, 0.0150f, { 0.0000f, 0.0000f, 0.0000f }, 0.0950f, 0.1400f, 0.2500f, 0.0000f, 0.9943f, 7176.8999f, 211.2000f, 0.0000f, 0x1 } #define EFX_REVERB_PRESET_PREFAB_PRACTISEROOM \ { 0.4022f, 0.8700f, 0.3162f, 0.3981f, 0.5012f, 1.1200f, 0.5600f, 0.1800f, 1.2589f, 0.0100f, { 0.0000f, 0.0000f, 0.0000f }, 1.4125f, 0.0110f, { 0.0000f, 0.0000f, 0.0000f }, 0.0950f, 0.1400f, 0.2500f, 0.0000f, 0.9943f, 7176.8999f, 211.2000f, 0.0000f, 0x1 } #define EFX_REVERB_PRESET_PREFAB_OUTHOUSE \ { 1.0000f, 0.8200f, 0.3162f, 0.1122f, 0.1585f, 1.3800f, 0.3800f, 0.3500f, 0.8913f, 0.0240f, { 0.0000f, 0.0000f, -0.0000f }, 0.6310f, 0.0440f, { 0.0000f, 0.0000f, 0.0000f }, 0.1210f, 0.1700f, 0.2500f, 0.0000f, 0.9943f, 2854.3999f, 107.5000f, 0.0000f, 0x0 } #define EFX_REVERB_PRESET_PREFAB_CARAVAN \ { 1.0000f, 1.0000f, 0.3162f, 0.0891f, 0.1259f, 0.4300f, 1.5000f, 1.0000f, 1.0000f, 0.0120f, { 0.0000f, 0.0000f, 0.0000f }, 1.9953f, 0.0120f, { 0.0000f, 0.0000f, 0.0000f }, 0.2500f, 0.0000f, 0.2500f, 0.0000f, 0.9943f, 5000.0000f, 250.0000f, 0.0000f, 0x0 } /* Dome and Pipe Presets */ #define EFX_REVERB_PRESET_DOME_TOMB \ { 1.0000f, 0.7900f, 0.3162f, 0.3548f, 0.2239f, 4.1800f, 0.2100f, 0.1000f, 0.3868f, 0.0300f, { 0.0000f, 0.0000f, 0.0000f }, 1.6788f, 0.0220f, { 0.0000f, 0.0000f, 0.0000f }, 0.1770f, 0.1900f, 0.2500f, 0.0000f, 0.9943f, 2854.3999f, 20.0000f, 0.0000f, 0x0 } #define EFX_REVERB_PRESET_PIPE_SMALL \ { 1.0000f, 1.0000f, 0.3162f, 0.3548f, 0.2239f, 5.0400f, 0.1000f, 0.1000f, 0.5012f, 0.0320f, { 0.0000f, 0.0000f, 0.0000f }, 2.5119f, 0.0150f, { 0.0000f, 0.0000f, 0.0000f }, 0.2500f, 0.0000f, 0.2500f, 0.0000f, 0.9943f, 2854.3999f, 20.0000f, 0.0000f, 0x1 } #define EFX_REVERB_PRESET_DOME_SAINTPAULS \ { 1.0000f, 0.8700f, 0.3162f, 0.3548f, 0.2239f, 10.4800f, 0.1900f, 0.1000f, 0.1778f, 0.0900f, { 0.0000f, 0.0000f, 0.0000f }, 1.2589f, 0.0420f, { 0.0000f, 0.0000f, 0.0000f }, 0.2500f, 0.1200f, 0.2500f, 0.0000f, 0.9943f, 2854.3999f, 20.0000f, 0.0000f, 0x1 } #define EFX_REVERB_PRESET_PIPE_LONGTHIN \ { 0.2560f, 0.9100f, 0.3162f, 0.4467f, 0.2818f, 9.2100f, 0.1800f, 0.1000f, 0.7079f, 0.0100f, { 0.0000f, 0.0000f, 0.0000f }, 0.7079f, 0.0220f, { 0.0000f, 0.0000f, 0.0000f }, 0.2500f, 0.0000f, 0.2500f, 0.0000f, 0.9943f, 2854.3999f, 20.0000f, 0.0000f, 0x0 } #define EFX_REVERB_PRESET_PIPE_LARGE \ { 1.0000f, 1.0000f, 0.3162f, 0.3548f, 0.2239f, 8.4500f, 0.1000f, 0.1000f, 0.3981f, 0.0460f, { 0.0000f, 0.0000f, 0.0000f }, 1.5849f, 0.0320f, { 0.0000f, 0.0000f, 0.0000f }, 0.2500f, 0.0000f, 0.2500f, 0.0000f, 0.9943f, 2854.3999f, 20.0000f, 0.0000f, 0x1 } #define EFX_REVERB_PRESET_PIPE_RESONANT \ { 0.1373f, 0.9100f, 0.3162f, 0.4467f, 0.2818f, 6.8100f, 0.1800f, 0.1000f, 0.7079f, 0.0100f, { 0.0000f, 0.0000f, 0.0000f }, 1.0000f, 0.0220f, { 0.0000f, 0.0000f, 0.0000f }, 0.2500f, 0.0000f, 0.2500f, 0.0000f, 0.9943f, 2854.3999f, 20.0000f, 0.0000f, 0x0 } /* Outdoors Presets */ #define EFX_REVERB_PRESET_OUTDOORS_BACKYARD \ { 1.0000f, 0.4500f, 0.3162f, 0.2512f, 0.5012f, 1.1200f, 0.3400f, 0.4600f, 0.4467f, 0.0690f, { 0.0000f, 0.0000f, -0.0000f }, 0.7079f, 0.0230f, { 0.0000f, 0.0000f, 0.0000f }, 0.2180f, 0.3400f, 0.2500f, 0.0000f, 0.9943f, 4399.1001f, 242.9000f, 0.0000f, 0x0 } #define EFX_REVERB_PRESET_OUTDOORS_ROLLINGPLAINS \ { 1.0000f, 0.0000f, 0.3162f, 0.0112f, 0.6310f, 2.1300f, 0.2100f, 0.4600f, 0.1778f, 0.3000f, { 0.0000f, 0.0000f, -0.0000f }, 0.4467f, 0.0190f, { 0.0000f, 0.0000f, 0.0000f }, 0.2500f, 1.0000f, 0.2500f, 0.0000f, 0.9943f, 4399.1001f, 242.9000f, 0.0000f, 0x0 } #define EFX_REVERB_PRESET_OUTDOORS_DEEPCANYON \ { 1.0000f, 0.7400f, 0.3162f, 0.1778f, 0.6310f, 3.8900f, 0.2100f, 0.4600f, 0.3162f, 0.2230f, { 0.0000f, 0.0000f, -0.0000f }, 0.3548f, 0.0190f, { 0.0000f, 0.0000f, 0.0000f }, 0.2500f, 1.0000f, 0.2500f, 0.0000f, 0.9943f, 4399.1001f, 242.9000f, 0.0000f, 0x0 } #define EFX_REVERB_PRESET_OUTDOORS_CREEK \ { 1.0000f, 0.3500f, 0.3162f, 0.1778f, 0.5012f, 2.1300f, 0.2100f, 0.4600f, 0.3981f, 0.1150f, { 0.0000f, 0.0000f, -0.0000f }, 0.1995f, 0.0310f, { 0.0000f, 0.0000f, 0.0000f }, 0.2180f, 0.3400f, 0.2500f, 0.0000f, 0.9943f, 4399.1001f, 242.9000f, 0.0000f, 0x0 } #define EFX_REVERB_PRESET_OUTDOORS_VALLEY \ { 1.0000f, 0.2800f, 0.3162f, 0.0282f, 0.1585f, 2.8800f, 0.2600f, 0.3500f, 0.1413f, 0.2630f, { 0.0000f, 0.0000f, -0.0000f }, 0.3981f, 0.1000f, { 0.0000f, 0.0000f, 0.0000f }, 0.2500f, 0.3400f, 0.2500f, 0.0000f, 0.9943f, 2854.3999f, 107.5000f, 0.0000f, 0x0 } /* Mood Presets */ #define EFX_REVERB_PRESET_MOOD_HEAVEN \ { 1.0000f, 0.9400f, 0.3162f, 0.7943f, 0.4467f, 5.0400f, 1.1200f, 0.5600f, 0.2427f, 0.0200f, { 0.0000f, 0.0000f, 0.0000f }, 1.2589f, 0.0290f, { 0.0000f, 0.0000f, 0.0000f }, 0.2500f, 0.0800f, 2.7420f, 0.0500f, 0.9977f, 5000.0000f, 250.0000f, 0.0000f, 0x1 } #define EFX_REVERB_PRESET_MOOD_HELL \ { 1.0000f, 0.5700f, 0.3162f, 0.3548f, 0.4467f, 3.5700f, 0.4900f, 2.0000f, 0.0000f, 0.0200f, { 0.0000f, 0.0000f, 0.0000f }, 1.4125f, 0.0300f, { 0.0000f, 0.0000f, 0.0000f }, 0.1100f, 0.0400f, 2.1090f, 0.5200f, 0.9943f, 5000.0000f, 139.5000f, 0.0000f, 0x0 } #define EFX_REVERB_PRESET_MOOD_MEMORY \ { 1.0000f, 0.8500f, 0.3162f, 0.6310f, 0.3548f, 4.0600f, 0.8200f, 0.5600f, 0.0398f, 0.0000f, { 0.0000f, 0.0000f, 0.0000f }, 1.1220f, 0.0000f, { 0.0000f, 0.0000f, 0.0000f }, 0.2500f, 0.0000f, 0.4740f, 0.4500f, 0.9886f, 5000.0000f, 250.0000f, 0.0000f, 0x0 } /* Driving Presets */ #define EFX_REVERB_PRESET_DRIVING_COMMENTATOR \ { 1.0000f, 0.0000f, 0.3162f, 0.5623f, 0.5012f, 2.4200f, 0.8800f, 0.6800f, 0.1995f, 0.0930f, { 0.0000f, 0.0000f, 0.0000f }, 0.2512f, 0.0170f, { 0.0000f, 0.0000f, 0.0000f }, 0.2500f, 1.0000f, 0.2500f, 0.0000f, 0.9886f, 5000.0000f, 250.0000f, 0.0000f, 0x1 } #define EFX_REVERB_PRESET_DRIVING_PITGARAGE \ { 0.4287f, 0.5900f, 0.3162f, 0.7079f, 0.5623f, 1.7200f, 0.9300f, 0.8700f, 0.5623f, 0.0000f, { 0.0000f, 0.0000f, 0.0000f }, 1.2589f, 0.0160f, { 0.0000f, 0.0000f, 0.0000f }, 0.2500f, 0.1100f, 0.2500f, 0.0000f, 0.9943f, 5000.0000f, 250.0000f, 0.0000f, 0x0 } #define EFX_REVERB_PRESET_DRIVING_INCAR_RACER \ { 0.0832f, 0.8000f, 0.3162f, 1.0000f, 0.7943f, 0.1700f, 2.0000f, 0.4100f, 1.7783f, 0.0070f, { 0.0000f, 0.0000f, 0.0000f }, 0.7079f, 0.0150f, { 0.0000f, 0.0000f, 0.0000f }, 0.2500f, 0.0000f, 0.2500f, 0.0000f, 0.9943f, 10268.2002f, 251.0000f, 0.0000f, 0x1 } #define EFX_REVERB_PRESET_DRIVING_INCAR_SPORTS \ { 0.0832f, 0.8000f, 0.3162f, 0.6310f, 1.0000f, 0.1700f, 0.7500f, 0.4100f, 1.0000f, 0.0100f, { 0.0000f, 0.0000f, 0.0000f }, 0.5623f, 0.0000f, { 0.0000f, 0.0000f, 0.0000f }, 0.2500f, 0.0000f, 0.2500f, 0.0000f, 0.9943f, 10268.2002f, 251.0000f, 0.0000f, 0x1 } #define EFX_REVERB_PRESET_DRIVING_INCAR_LUXURY \ { 0.2560f, 1.0000f, 0.3162f, 0.1000f, 0.5012f, 0.1300f, 0.4100f, 0.4600f, 0.7943f, 0.0100f, { 0.0000f, 0.0000f, 0.0000f }, 1.5849f, 0.0100f, { 0.0000f, 0.0000f, 0.0000f }, 0.2500f, 0.0000f, 0.2500f, 0.0000f, 0.9943f, 10268.2002f, 251.0000f, 0.0000f, 0x1 } #define EFX_REVERB_PRESET_DRIVING_FULLGRANDSTAND \ { 1.0000f, 1.0000f, 0.3162f, 0.2818f, 0.6310f, 3.0100f, 1.3700f, 1.2800f, 0.3548f, 0.0900f, { 0.0000f, 0.0000f, 0.0000f }, 0.1778f, 0.0490f, { 0.0000f, 0.0000f, 0.0000f }, 0.2500f, 0.0000f, 0.2500f, 0.0000f, 0.9943f, 10420.2002f, 250.0000f, 0.0000f, 0x0 } #define EFX_REVERB_PRESET_DRIVING_EMPTYGRANDSTAND \ { 1.0000f, 1.0000f, 0.3162f, 1.0000f, 0.7943f, 4.6200f, 1.7500f, 1.4000f, 0.2082f, 0.0900f, { 0.0000f, 0.0000f, 0.0000f }, 0.2512f, 0.0490f, { 0.0000f, 0.0000f, 0.0000f }, 0.2500f, 0.0000f, 0.2500f, 0.0000f, 0.9943f, 10420.2002f, 250.0000f, 0.0000f, 0x0 } #define EFX_REVERB_PRESET_DRIVING_TUNNEL \ { 1.0000f, 0.8100f, 0.3162f, 0.3981f, 0.8913f, 3.4200f, 0.9400f, 1.3100f, 0.7079f, 0.0510f, { 0.0000f, 0.0000f, 0.0000f }, 0.7079f, 0.0470f, { 0.0000f, 0.0000f, 0.0000f }, 0.2140f, 0.0500f, 0.2500f, 0.0000f, 0.9943f, 5000.0000f, 155.3000f, 0.0000f, 0x1 } /* City Presets */ #define EFX_REVERB_PRESET_CITY_STREETS \ { 1.0000f, 0.7800f, 0.3162f, 0.7079f, 0.8913f, 1.7900f, 1.1200f, 0.9100f, 0.2818f, 0.0460f, { 0.0000f, 0.0000f, 0.0000f }, 0.1995f, 0.0280f, { 0.0000f, 0.0000f, 0.0000f }, 0.2500f, 0.2000f, 0.2500f, 0.0000f, 0.9943f, 5000.0000f, 250.0000f, 0.0000f, 0x1 } #define EFX_REVERB_PRESET_CITY_SUBWAY \ { 1.0000f, 0.7400f, 0.3162f, 0.7079f, 0.8913f, 3.0100f, 1.2300f, 0.9100f, 0.7079f, 0.0460f, { 0.0000f, 0.0000f, 0.0000f }, 1.2589f, 0.0280f, { 0.0000f, 0.0000f, 0.0000f }, 0.1250f, 0.2100f, 0.2500f, 0.0000f, 0.9943f, 5000.0000f, 250.0000f, 0.0000f, 0x1 } #define EFX_REVERB_PRESET_CITY_MUSEUM \ { 1.0000f, 0.8200f, 0.3162f, 0.1778f, 0.1778f, 3.2800f, 1.4000f, 0.5700f, 0.2512f, 0.0390f, { 0.0000f, 0.0000f, -0.0000f }, 0.8913f, 0.0340f, { 0.0000f, 0.0000f, 0.0000f }, 0.1300f, 0.1700f, 0.2500f, 0.0000f, 0.9943f, 2854.3999f, 107.5000f, 0.0000f, 0x0 } #define EFX_REVERB_PRESET_CITY_LIBRARY \ { 1.0000f, 0.8200f, 0.3162f, 0.2818f, 0.0891f, 2.7600f, 0.8900f, 0.4100f, 0.3548f, 0.0290f, { 0.0000f, 0.0000f, -0.0000f }, 0.8913f, 0.0200f, { 0.0000f, 0.0000f, 0.0000f }, 0.1300f, 0.1700f, 0.2500f, 0.0000f, 0.9943f, 2854.3999f, 107.5000f, 0.0000f, 0x0 } #define EFX_REVERB_PRESET_CITY_UNDERPASS \ { 1.0000f, 0.8200f, 0.3162f, 0.4467f, 0.8913f, 3.5700f, 1.1200f, 0.9100f, 0.3981f, 0.0590f, { 0.0000f, 0.0000f, 0.0000f }, 0.8913f, 0.0370f, { 0.0000f, 0.0000f, 0.0000f }, 0.2500f, 0.1400f, 0.2500f, 0.0000f, 0.9920f, 5000.0000f, 250.0000f, 0.0000f, 0x1 } #define EFX_REVERB_PRESET_CITY_ABANDONED \ { 1.0000f, 0.6900f, 0.3162f, 0.7943f, 0.8913f, 3.2800f, 1.1700f, 0.9100f, 0.4467f, 0.0440f, { 0.0000f, 0.0000f, 0.0000f }, 0.2818f, 0.0240f, { 0.0000f, 0.0000f, 0.0000f }, 0.2500f, 0.2000f, 0.2500f, 0.0000f, 0.9966f, 5000.0000f, 250.0000f, 0.0000f, 0x1 } /* Misc. Presets */ #define EFX_REVERB_PRESET_DUSTYROOM \ { 0.3645f, 0.5600f, 0.3162f, 0.7943f, 0.7079f, 1.7900f, 0.3800f, 0.2100f, 0.5012f, 0.0020f, { 0.0000f, 0.0000f, 0.0000f }, 1.2589f, 0.0060f, { 0.0000f, 0.0000f, 0.0000f }, 0.2020f, 0.0500f, 0.2500f, 0.0000f, 0.9886f, 13046.0000f, 163.3000f, 0.0000f, 0x1 } #define EFX_REVERB_PRESET_CHAPEL \ { 1.0000f, 0.8400f, 0.3162f, 0.5623f, 1.0000f, 4.6200f, 0.6400f, 1.2300f, 0.4467f, 0.0320f, { 0.0000f, 0.0000f, 0.0000f }, 0.7943f, 0.0490f, { 0.0000f, 0.0000f, 0.0000f }, 0.2500f, 0.0000f, 0.2500f, 0.1100f, 0.9943f, 5000.0000f, 250.0000f, 0.0000f, 0x1 } #define EFX_REVERB_PRESET_SMALLWATERROOM \ { 1.0000f, 0.7000f, 0.3162f, 0.4477f, 1.0000f, 1.5100f, 1.2500f, 1.1400f, 0.8913f, 0.0200f, { 0.0000f, 0.0000f, 0.0000f }, 1.4125f, 0.0300f, { 0.0000f, 0.0000f, 0.0000f }, 0.1790f, 0.1500f, 0.8950f, 0.1900f, 0.9920f, 5000.0000f, 250.0000f, 0.0000f, 0x0 } #endif /* EFX_PRESETS_H */ ================================================ FILE: apps/openmw/mwsound/efx.h ================================================ #ifndef AL_EFX_H #define AL_EFX_H #include "alc.h" #include "al.h" #ifdef __cplusplus extern "C" { #endif #define ALC_EXT_EFX_NAME "ALC_EXT_EFX" #define ALC_EFX_MAJOR_VERSION 0x20001 #define ALC_EFX_MINOR_VERSION 0x20002 #define ALC_MAX_AUXILIARY_SENDS 0x20003 /* Listener properties. */ #define AL_METERS_PER_UNIT 0x20004 /* Source properties. */ #define AL_DIRECT_FILTER 0x20005 #define AL_AUXILIARY_SEND_FILTER 0x20006 #define AL_AIR_ABSORPTION_FACTOR 0x20007 #define AL_ROOM_ROLLOFF_FACTOR 0x20008 #define AL_CONE_OUTER_GAINHF 0x20009 #define AL_DIRECT_FILTER_GAINHF_AUTO 0x2000A #define AL_AUXILIARY_SEND_FILTER_GAIN_AUTO 0x2000B #define AL_AUXILIARY_SEND_FILTER_GAINHF_AUTO 0x2000C /* Effect properties. */ /* Reverb effect parameters */ #define AL_REVERB_DENSITY 0x0001 #define AL_REVERB_DIFFUSION 0x0002 #define AL_REVERB_GAIN 0x0003 #define AL_REVERB_GAINHF 0x0004 #define AL_REVERB_DECAY_TIME 0x0005 #define AL_REVERB_DECAY_HFRATIO 0x0006 #define AL_REVERB_REFLECTIONS_GAIN 0x0007 #define AL_REVERB_REFLECTIONS_DELAY 0x0008 #define AL_REVERB_LATE_REVERB_GAIN 0x0009 #define AL_REVERB_LATE_REVERB_DELAY 0x000A #define AL_REVERB_AIR_ABSORPTION_GAINHF 0x000B #define AL_REVERB_ROOM_ROLLOFF_FACTOR 0x000C #define AL_REVERB_DECAY_HFLIMIT 0x000D /* EAX Reverb effect parameters */ #define AL_EAXREVERB_DENSITY 0x0001 #define AL_EAXREVERB_DIFFUSION 0x0002 #define AL_EAXREVERB_GAIN 0x0003 #define AL_EAXREVERB_GAINHF 0x0004 #define AL_EAXREVERB_GAINLF 0x0005 #define AL_EAXREVERB_DECAY_TIME 0x0006 #define AL_EAXREVERB_DECAY_HFRATIO 0x0007 #define AL_EAXREVERB_DECAY_LFRATIO 0x0008 #define AL_EAXREVERB_REFLECTIONS_GAIN 0x0009 #define AL_EAXREVERB_REFLECTIONS_DELAY 0x000A #define AL_EAXREVERB_REFLECTIONS_PAN 0x000B #define AL_EAXREVERB_LATE_REVERB_GAIN 0x000C #define AL_EAXREVERB_LATE_REVERB_DELAY 0x000D #define AL_EAXREVERB_LATE_REVERB_PAN 0x000E #define AL_EAXREVERB_ECHO_TIME 0x000F #define AL_EAXREVERB_ECHO_DEPTH 0x0010 #define AL_EAXREVERB_MODULATION_TIME 0x0011 #define AL_EAXREVERB_MODULATION_DEPTH 0x0012 #define AL_EAXREVERB_AIR_ABSORPTION_GAINHF 0x0013 #define AL_EAXREVERB_HFREFERENCE 0x0014 #define AL_EAXREVERB_LFREFERENCE 0x0015 #define AL_EAXREVERB_ROOM_ROLLOFF_FACTOR 0x0016 #define AL_EAXREVERB_DECAY_HFLIMIT 0x0017 /* Chorus effect parameters */ #define AL_CHORUS_WAVEFORM 0x0001 #define AL_CHORUS_PHASE 0x0002 #define AL_CHORUS_RATE 0x0003 #define AL_CHORUS_DEPTH 0x0004 #define AL_CHORUS_FEEDBACK 0x0005 #define AL_CHORUS_DELAY 0x0006 /* Distortion effect parameters */ #define AL_DISTORTION_EDGE 0x0001 #define AL_DISTORTION_GAIN 0x0002 #define AL_DISTORTION_LOWPASS_CUTOFF 0x0003 #define AL_DISTORTION_EQCENTER 0x0004 #define AL_DISTORTION_EQBANDWIDTH 0x0005 /* Echo effect parameters */ #define AL_ECHO_DELAY 0x0001 #define AL_ECHO_LRDELAY 0x0002 #define AL_ECHO_DAMPING 0x0003 #define AL_ECHO_FEEDBACK 0x0004 #define AL_ECHO_SPREAD 0x0005 /* Flanger effect parameters */ #define AL_FLANGER_WAVEFORM 0x0001 #define AL_FLANGER_PHASE 0x0002 #define AL_FLANGER_RATE 0x0003 #define AL_FLANGER_DEPTH 0x0004 #define AL_FLANGER_FEEDBACK 0x0005 #define AL_FLANGER_DELAY 0x0006 /* Frequency shifter effect parameters */ #define AL_FREQUENCY_SHIFTER_FREQUENCY 0x0001 #define AL_FREQUENCY_SHIFTER_LEFT_DIRECTION 0x0002 #define AL_FREQUENCY_SHIFTER_RIGHT_DIRECTION 0x0003 /* Vocal morpher effect parameters */ #define AL_VOCAL_MORPHER_PHONEMEA 0x0001 #define AL_VOCAL_MORPHER_PHONEMEA_COARSE_TUNING 0x0002 #define AL_VOCAL_MORPHER_PHONEMEB 0x0003 #define AL_VOCAL_MORPHER_PHONEMEB_COARSE_TUNING 0x0004 #define AL_VOCAL_MORPHER_WAVEFORM 0x0005 #define AL_VOCAL_MORPHER_RATE 0x0006 /* Pitchshifter effect parameters */ #define AL_PITCH_SHIFTER_COARSE_TUNE 0x0001 #define AL_PITCH_SHIFTER_FINE_TUNE 0x0002 /* Ringmodulator effect parameters */ #define AL_RING_MODULATOR_FREQUENCY 0x0001 #define AL_RING_MODULATOR_HIGHPASS_CUTOFF 0x0002 #define AL_RING_MODULATOR_WAVEFORM 0x0003 /* Autowah effect parameters */ #define AL_AUTOWAH_ATTACK_TIME 0x0001 #define AL_AUTOWAH_RELEASE_TIME 0x0002 #define AL_AUTOWAH_RESONANCE 0x0003 #define AL_AUTOWAH_PEAK_GAIN 0x0004 /* Compressor effect parameters */ #define AL_COMPRESSOR_ONOFF 0x0001 /* Equalizer effect parameters */ #define AL_EQUALIZER_LOW_GAIN 0x0001 #define AL_EQUALIZER_LOW_CUTOFF 0x0002 #define AL_EQUALIZER_MID1_GAIN 0x0003 #define AL_EQUALIZER_MID1_CENTER 0x0004 #define AL_EQUALIZER_MID1_WIDTH 0x0005 #define AL_EQUALIZER_MID2_GAIN 0x0006 #define AL_EQUALIZER_MID2_CENTER 0x0007 #define AL_EQUALIZER_MID2_WIDTH 0x0008 #define AL_EQUALIZER_HIGH_GAIN 0x0009 #define AL_EQUALIZER_HIGH_CUTOFF 0x000A /* Effect type */ #define AL_EFFECT_FIRST_PARAMETER 0x0000 #define AL_EFFECT_LAST_PARAMETER 0x8000 #define AL_EFFECT_TYPE 0x8001 /* Effect types, used with the AL_EFFECT_TYPE property */ #define AL_EFFECT_NULL 0x0000 #define AL_EFFECT_REVERB 0x0001 #define AL_EFFECT_CHORUS 0x0002 #define AL_EFFECT_DISTORTION 0x0003 #define AL_EFFECT_ECHO 0x0004 #define AL_EFFECT_FLANGER 0x0005 #define AL_EFFECT_FREQUENCY_SHIFTER 0x0006 #define AL_EFFECT_VOCAL_MORPHER 0x0007 #define AL_EFFECT_PITCH_SHIFTER 0x0008 #define AL_EFFECT_RING_MODULATOR 0x0009 #define AL_EFFECT_AUTOWAH 0x000A #define AL_EFFECT_COMPRESSOR 0x000B #define AL_EFFECT_EQUALIZER 0x000C #define AL_EFFECT_EAXREVERB 0x8000 /* Auxiliary Effect Slot properties. */ #define AL_EFFECTSLOT_EFFECT 0x0001 #define AL_EFFECTSLOT_GAIN 0x0002 #define AL_EFFECTSLOT_AUXILIARY_SEND_AUTO 0x0003 /* NULL Auxiliary Slot ID to disable a source send. */ #define AL_EFFECTSLOT_NULL 0x0000 /* Filter properties. */ /* Lowpass filter parameters */ #define AL_LOWPASS_GAIN 0x0001 #define AL_LOWPASS_GAINHF 0x0002 /* Highpass filter parameters */ #define AL_HIGHPASS_GAIN 0x0001 #define AL_HIGHPASS_GAINLF 0x0002 /* Bandpass filter parameters */ #define AL_BANDPASS_GAIN 0x0001 #define AL_BANDPASS_GAINLF 0x0002 #define AL_BANDPASS_GAINHF 0x0003 /* Filter type */ #define AL_FILTER_FIRST_PARAMETER 0x0000 #define AL_FILTER_LAST_PARAMETER 0x8000 #define AL_FILTER_TYPE 0x8001 /* Filter types, used with the AL_FILTER_TYPE property */ #define AL_FILTER_NULL 0x0000 #define AL_FILTER_LOWPASS 0x0001 #define AL_FILTER_HIGHPASS 0x0002 #define AL_FILTER_BANDPASS 0x0003 /* Effect object function types. */ typedef void (AL_APIENTRY *LPALGENEFFECTS)(ALsizei, ALuint*); typedef void (AL_APIENTRY *LPALDELETEEFFECTS)(ALsizei, const ALuint*); typedef ALboolean (AL_APIENTRY *LPALISEFFECT)(ALuint); typedef void (AL_APIENTRY *LPALEFFECTI)(ALuint, ALenum, ALint); typedef void (AL_APIENTRY *LPALEFFECTIV)(ALuint, ALenum, const ALint*); typedef void (AL_APIENTRY *LPALEFFECTF)(ALuint, ALenum, ALfloat); typedef void (AL_APIENTRY *LPALEFFECTFV)(ALuint, ALenum, const ALfloat*); typedef void (AL_APIENTRY *LPALGETEFFECTI)(ALuint, ALenum, ALint*); typedef void (AL_APIENTRY *LPALGETEFFECTIV)(ALuint, ALenum, ALint*); typedef void (AL_APIENTRY *LPALGETEFFECTF)(ALuint, ALenum, ALfloat*); typedef void (AL_APIENTRY *LPALGETEFFECTFV)(ALuint, ALenum, ALfloat*); /* Filter object function types. */ typedef void (AL_APIENTRY *LPALGENFILTERS)(ALsizei, ALuint*); typedef void (AL_APIENTRY *LPALDELETEFILTERS)(ALsizei, const ALuint*); typedef ALboolean (AL_APIENTRY *LPALISFILTER)(ALuint); typedef void (AL_APIENTRY *LPALFILTERI)(ALuint, ALenum, ALint); typedef void (AL_APIENTRY *LPALFILTERIV)(ALuint, ALenum, const ALint*); typedef void (AL_APIENTRY *LPALFILTERF)(ALuint, ALenum, ALfloat); typedef void (AL_APIENTRY *LPALFILTERFV)(ALuint, ALenum, const ALfloat*); typedef void (AL_APIENTRY *LPALGETFILTERI)(ALuint, ALenum, ALint*); typedef void (AL_APIENTRY *LPALGETFILTERIV)(ALuint, ALenum, ALint*); typedef void (AL_APIENTRY *LPALGETFILTERF)(ALuint, ALenum, ALfloat*); typedef void (AL_APIENTRY *LPALGETFILTERFV)(ALuint, ALenum, ALfloat*); /* Auxiliary Effect Slot object function types. */ typedef void (AL_APIENTRY *LPALGENAUXILIARYEFFECTSLOTS)(ALsizei, ALuint*); typedef void (AL_APIENTRY *LPALDELETEAUXILIARYEFFECTSLOTS)(ALsizei, const ALuint*); typedef ALboolean (AL_APIENTRY *LPALISAUXILIARYEFFECTSLOT)(ALuint); typedef void (AL_APIENTRY *LPALAUXILIARYEFFECTSLOTI)(ALuint, ALenum, ALint); typedef void (AL_APIENTRY *LPALAUXILIARYEFFECTSLOTIV)(ALuint, ALenum, const ALint*); typedef void (AL_APIENTRY *LPALAUXILIARYEFFECTSLOTF)(ALuint, ALenum, ALfloat); typedef void (AL_APIENTRY *LPALAUXILIARYEFFECTSLOTFV)(ALuint, ALenum, const ALfloat*); typedef void (AL_APIENTRY *LPALGETAUXILIARYEFFECTSLOTI)(ALuint, ALenum, ALint*); typedef void (AL_APIENTRY *LPALGETAUXILIARYEFFECTSLOTIV)(ALuint, ALenum, ALint*); typedef void (AL_APIENTRY *LPALGETAUXILIARYEFFECTSLOTF)(ALuint, ALenum, ALfloat*); typedef void (AL_APIENTRY *LPALGETAUXILIARYEFFECTSLOTFV)(ALuint, ALenum, ALfloat*); #ifdef AL_ALEXT_PROTOTYPES AL_API ALvoid AL_APIENTRY alGenEffects(ALsizei n, ALuint *effects); AL_API ALvoid AL_APIENTRY alDeleteEffects(ALsizei n, const ALuint *effects); AL_API ALboolean AL_APIENTRY alIsEffect(ALuint effect); AL_API ALvoid AL_APIENTRY alEffecti(ALuint effect, ALenum param, ALint iValue); AL_API ALvoid AL_APIENTRY alEffectiv(ALuint effect, ALenum param, const ALint *piValues); AL_API ALvoid AL_APIENTRY alEffectf(ALuint effect, ALenum param, ALfloat flValue); AL_API ALvoid AL_APIENTRY alEffectfv(ALuint effect, ALenum param, const ALfloat *pflValues); AL_API ALvoid AL_APIENTRY alGetEffecti(ALuint effect, ALenum param, ALint *piValue); AL_API ALvoid AL_APIENTRY alGetEffectiv(ALuint effect, ALenum param, ALint *piValues); AL_API ALvoid AL_APIENTRY alGetEffectf(ALuint effect, ALenum param, ALfloat *pflValue); AL_API ALvoid AL_APIENTRY alGetEffectfv(ALuint effect, ALenum param, ALfloat *pflValues); AL_API ALvoid AL_APIENTRY alGenFilters(ALsizei n, ALuint *filters); AL_API ALvoid AL_APIENTRY alDeleteFilters(ALsizei n, const ALuint *filters); AL_API ALboolean AL_APIENTRY alIsFilter(ALuint filter); AL_API ALvoid AL_APIENTRY alFilteri(ALuint filter, ALenum param, ALint iValue); AL_API ALvoid AL_APIENTRY alFilteriv(ALuint filter, ALenum param, const ALint *piValues); AL_API ALvoid AL_APIENTRY alFilterf(ALuint filter, ALenum param, ALfloat flValue); AL_API ALvoid AL_APIENTRY alFilterfv(ALuint filter, ALenum param, const ALfloat *pflValues); AL_API ALvoid AL_APIENTRY alGetFilteri(ALuint filter, ALenum param, ALint *piValue); AL_API ALvoid AL_APIENTRY alGetFilteriv(ALuint filter, ALenum param, ALint *piValues); AL_API ALvoid AL_APIENTRY alGetFilterf(ALuint filter, ALenum param, ALfloat *pflValue); AL_API ALvoid AL_APIENTRY alGetFilterfv(ALuint filter, ALenum param, ALfloat *pflValues); AL_API ALvoid AL_APIENTRY alGenAuxiliaryEffectSlots(ALsizei n, ALuint *effectslots); AL_API ALvoid AL_APIENTRY alDeleteAuxiliaryEffectSlots(ALsizei n, const ALuint *effectslots); AL_API ALboolean AL_APIENTRY alIsAuxiliaryEffectSlot(ALuint effectslot); AL_API ALvoid AL_APIENTRY alAuxiliaryEffectSloti(ALuint effectslot, ALenum param, ALint iValue); AL_API ALvoid AL_APIENTRY alAuxiliaryEffectSlotiv(ALuint effectslot, ALenum param, const ALint *piValues); AL_API ALvoid AL_APIENTRY alAuxiliaryEffectSlotf(ALuint effectslot, ALenum param, ALfloat flValue); AL_API ALvoid AL_APIENTRY alAuxiliaryEffectSlotfv(ALuint effectslot, ALenum param, const ALfloat *pflValues); AL_API ALvoid AL_APIENTRY alGetAuxiliaryEffectSloti(ALuint effectslot, ALenum param, ALint *piValue); AL_API ALvoid AL_APIENTRY alGetAuxiliaryEffectSlotiv(ALuint effectslot, ALenum param, ALint *piValues); AL_API ALvoid AL_APIENTRY alGetAuxiliaryEffectSlotf(ALuint effectslot, ALenum param, ALfloat *pflValue); AL_API ALvoid AL_APIENTRY alGetAuxiliaryEffectSlotfv(ALuint effectslot, ALenum param, ALfloat *pflValues); #endif /* Filter ranges and defaults. */ /* Lowpass filter */ #define AL_LOWPASS_MIN_GAIN (0.0f) #define AL_LOWPASS_MAX_GAIN (1.0f) #define AL_LOWPASS_DEFAULT_GAIN (1.0f) #define AL_LOWPASS_MIN_GAINHF (0.0f) #define AL_LOWPASS_MAX_GAINHF (1.0f) #define AL_LOWPASS_DEFAULT_GAINHF (1.0f) /* Highpass filter */ #define AL_HIGHPASS_MIN_GAIN (0.0f) #define AL_HIGHPASS_MAX_GAIN (1.0f) #define AL_HIGHPASS_DEFAULT_GAIN (1.0f) #define AL_HIGHPASS_MIN_GAINLF (0.0f) #define AL_HIGHPASS_MAX_GAINLF (1.0f) #define AL_HIGHPASS_DEFAULT_GAINLF (1.0f) /* Bandpass filter */ #define AL_BANDPASS_MIN_GAIN (0.0f) #define AL_BANDPASS_MAX_GAIN (1.0f) #define AL_BANDPASS_DEFAULT_GAIN (1.0f) #define AL_BANDPASS_MIN_GAINHF (0.0f) #define AL_BANDPASS_MAX_GAINHF (1.0f) #define AL_BANDPASS_DEFAULT_GAINHF (1.0f) #define AL_BANDPASS_MIN_GAINLF (0.0f) #define AL_BANDPASS_MAX_GAINLF (1.0f) #define AL_BANDPASS_DEFAULT_GAINLF (1.0f) /* Effect parameter ranges and defaults. */ /* Standard reverb effect */ #define AL_REVERB_MIN_DENSITY (0.0f) #define AL_REVERB_MAX_DENSITY (1.0f) #define AL_REVERB_DEFAULT_DENSITY (1.0f) #define AL_REVERB_MIN_DIFFUSION (0.0f) #define AL_REVERB_MAX_DIFFUSION (1.0f) #define AL_REVERB_DEFAULT_DIFFUSION (1.0f) #define AL_REVERB_MIN_GAIN (0.0f) #define AL_REVERB_MAX_GAIN (1.0f) #define AL_REVERB_DEFAULT_GAIN (0.32f) #define AL_REVERB_MIN_GAINHF (0.0f) #define AL_REVERB_MAX_GAINHF (1.0f) #define AL_REVERB_DEFAULT_GAINHF (0.89f) #define AL_REVERB_MIN_DECAY_TIME (0.1f) #define AL_REVERB_MAX_DECAY_TIME (20.0f) #define AL_REVERB_DEFAULT_DECAY_TIME (1.49f) #define AL_REVERB_MIN_DECAY_HFRATIO (0.1f) #define AL_REVERB_MAX_DECAY_HFRATIO (2.0f) #define AL_REVERB_DEFAULT_DECAY_HFRATIO (0.83f) #define AL_REVERB_MIN_REFLECTIONS_GAIN (0.0f) #define AL_REVERB_MAX_REFLECTIONS_GAIN (3.16f) #define AL_REVERB_DEFAULT_REFLECTIONS_GAIN (0.05f) #define AL_REVERB_MIN_REFLECTIONS_DELAY (0.0f) #define AL_REVERB_MAX_REFLECTIONS_DELAY (0.3f) #define AL_REVERB_DEFAULT_REFLECTIONS_DELAY (0.007f) #define AL_REVERB_MIN_LATE_REVERB_GAIN (0.0f) #define AL_REVERB_MAX_LATE_REVERB_GAIN (10.0f) #define AL_REVERB_DEFAULT_LATE_REVERB_GAIN (1.26f) #define AL_REVERB_MIN_LATE_REVERB_DELAY (0.0f) #define AL_REVERB_MAX_LATE_REVERB_DELAY (0.1f) #define AL_REVERB_DEFAULT_LATE_REVERB_DELAY (0.011f) #define AL_REVERB_MIN_AIR_ABSORPTION_GAINHF (0.892f) #define AL_REVERB_MAX_AIR_ABSORPTION_GAINHF (1.0f) #define AL_REVERB_DEFAULT_AIR_ABSORPTION_GAINHF (0.994f) #define AL_REVERB_MIN_ROOM_ROLLOFF_FACTOR (0.0f) #define AL_REVERB_MAX_ROOM_ROLLOFF_FACTOR (10.0f) #define AL_REVERB_DEFAULT_ROOM_ROLLOFF_FACTOR (0.0f) #define AL_REVERB_MIN_DECAY_HFLIMIT AL_FALSE #define AL_REVERB_MAX_DECAY_HFLIMIT AL_TRUE #define AL_REVERB_DEFAULT_DECAY_HFLIMIT AL_TRUE /* EAX reverb effect */ #define AL_EAXREVERB_MIN_DENSITY (0.0f) #define AL_EAXREVERB_MAX_DENSITY (1.0f) #define AL_EAXREVERB_DEFAULT_DENSITY (1.0f) #define AL_EAXREVERB_MIN_DIFFUSION (0.0f) #define AL_EAXREVERB_MAX_DIFFUSION (1.0f) #define AL_EAXREVERB_DEFAULT_DIFFUSION (1.0f) #define AL_EAXREVERB_MIN_GAIN (0.0f) #define AL_EAXREVERB_MAX_GAIN (1.0f) #define AL_EAXREVERB_DEFAULT_GAIN (0.32f) #define AL_EAXREVERB_MIN_GAINHF (0.0f) #define AL_EAXREVERB_MAX_GAINHF (1.0f) #define AL_EAXREVERB_DEFAULT_GAINHF (0.89f) #define AL_EAXREVERB_MIN_GAINLF (0.0f) #define AL_EAXREVERB_MAX_GAINLF (1.0f) #define AL_EAXREVERB_DEFAULT_GAINLF (1.0f) #define AL_EAXREVERB_MIN_DECAY_TIME (0.1f) #define AL_EAXREVERB_MAX_DECAY_TIME (20.0f) #define AL_EAXREVERB_DEFAULT_DECAY_TIME (1.49f) #define AL_EAXREVERB_MIN_DECAY_HFRATIO (0.1f) #define AL_EAXREVERB_MAX_DECAY_HFRATIO (2.0f) #define AL_EAXREVERB_DEFAULT_DECAY_HFRATIO (0.83f) #define AL_EAXREVERB_MIN_DECAY_LFRATIO (0.1f) #define AL_EAXREVERB_MAX_DECAY_LFRATIO (2.0f) #define AL_EAXREVERB_DEFAULT_DECAY_LFRATIO (1.0f) #define AL_EAXREVERB_MIN_REFLECTIONS_GAIN (0.0f) #define AL_EAXREVERB_MAX_REFLECTIONS_GAIN (3.16f) #define AL_EAXREVERB_DEFAULT_REFLECTIONS_GAIN (0.05f) #define AL_EAXREVERB_MIN_REFLECTIONS_DELAY (0.0f) #define AL_EAXREVERB_MAX_REFLECTIONS_DELAY (0.3f) #define AL_EAXREVERB_DEFAULT_REFLECTIONS_DELAY (0.007f) #define AL_EAXREVERB_DEFAULT_REFLECTIONS_PAN_XYZ (0.0f) #define AL_EAXREVERB_MIN_LATE_REVERB_GAIN (0.0f) #define AL_EAXREVERB_MAX_LATE_REVERB_GAIN (10.0f) #define AL_EAXREVERB_DEFAULT_LATE_REVERB_GAIN (1.26f) #define AL_EAXREVERB_MIN_LATE_REVERB_DELAY (0.0f) #define AL_EAXREVERB_MAX_LATE_REVERB_DELAY (0.1f) #define AL_EAXREVERB_DEFAULT_LATE_REVERB_DELAY (0.011f) #define AL_EAXREVERB_DEFAULT_LATE_REVERB_PAN_XYZ (0.0f) #define AL_EAXREVERB_MIN_ECHO_TIME (0.075f) #define AL_EAXREVERB_MAX_ECHO_TIME (0.25f) #define AL_EAXREVERB_DEFAULT_ECHO_TIME (0.25f) #define AL_EAXREVERB_MIN_ECHO_DEPTH (0.0f) #define AL_EAXREVERB_MAX_ECHO_DEPTH (1.0f) #define AL_EAXREVERB_DEFAULT_ECHO_DEPTH (0.0f) #define AL_EAXREVERB_MIN_MODULATION_TIME (0.04f) #define AL_EAXREVERB_MAX_MODULATION_TIME (4.0f) #define AL_EAXREVERB_DEFAULT_MODULATION_TIME (0.25f) #define AL_EAXREVERB_MIN_MODULATION_DEPTH (0.0f) #define AL_EAXREVERB_MAX_MODULATION_DEPTH (1.0f) #define AL_EAXREVERB_DEFAULT_MODULATION_DEPTH (0.0f) #define AL_EAXREVERB_MIN_AIR_ABSORPTION_GAINHF (0.892f) #define AL_EAXREVERB_MAX_AIR_ABSORPTION_GAINHF (1.0f) #define AL_EAXREVERB_DEFAULT_AIR_ABSORPTION_GAINHF (0.994f) #define AL_EAXREVERB_MIN_HFREFERENCE (1000.0f) #define AL_EAXREVERB_MAX_HFREFERENCE (20000.0f) #define AL_EAXREVERB_DEFAULT_HFREFERENCE (5000.0f) #define AL_EAXREVERB_MIN_LFREFERENCE (20.0f) #define AL_EAXREVERB_MAX_LFREFERENCE (1000.0f) #define AL_EAXREVERB_DEFAULT_LFREFERENCE (250.0f) #define AL_EAXREVERB_MIN_ROOM_ROLLOFF_FACTOR (0.0f) #define AL_EAXREVERB_MAX_ROOM_ROLLOFF_FACTOR (10.0f) #define AL_EAXREVERB_DEFAULT_ROOM_ROLLOFF_FACTOR (0.0f) #define AL_EAXREVERB_MIN_DECAY_HFLIMIT AL_FALSE #define AL_EAXREVERB_MAX_DECAY_HFLIMIT AL_TRUE #define AL_EAXREVERB_DEFAULT_DECAY_HFLIMIT AL_TRUE /* Chorus effect */ #define AL_CHORUS_WAVEFORM_SINUSOID (0) #define AL_CHORUS_WAVEFORM_TRIANGLE (1) #define AL_CHORUS_MIN_WAVEFORM (0) #define AL_CHORUS_MAX_WAVEFORM (1) #define AL_CHORUS_DEFAULT_WAVEFORM (1) #define AL_CHORUS_MIN_PHASE (-180) #define AL_CHORUS_MAX_PHASE (180) #define AL_CHORUS_DEFAULT_PHASE (90) #define AL_CHORUS_MIN_RATE (0.0f) #define AL_CHORUS_MAX_RATE (10.0f) #define AL_CHORUS_DEFAULT_RATE (1.1f) #define AL_CHORUS_MIN_DEPTH (0.0f) #define AL_CHORUS_MAX_DEPTH (1.0f) #define AL_CHORUS_DEFAULT_DEPTH (0.1f) #define AL_CHORUS_MIN_FEEDBACK (-1.0f) #define AL_CHORUS_MAX_FEEDBACK (1.0f) #define AL_CHORUS_DEFAULT_FEEDBACK (0.25f) #define AL_CHORUS_MIN_DELAY (0.0f) #define AL_CHORUS_MAX_DELAY (0.016f) #define AL_CHORUS_DEFAULT_DELAY (0.016f) /* Distortion effect */ #define AL_DISTORTION_MIN_EDGE (0.0f) #define AL_DISTORTION_MAX_EDGE (1.0f) #define AL_DISTORTION_DEFAULT_EDGE (0.2f) #define AL_DISTORTION_MIN_GAIN (0.01f) #define AL_DISTORTION_MAX_GAIN (1.0f) #define AL_DISTORTION_DEFAULT_GAIN (0.05f) #define AL_DISTORTION_MIN_LOWPASS_CUTOFF (80.0f) #define AL_DISTORTION_MAX_LOWPASS_CUTOFF (24000.0f) #define AL_DISTORTION_DEFAULT_LOWPASS_CUTOFF (8000.0f) #define AL_DISTORTION_MIN_EQCENTER (80.0f) #define AL_DISTORTION_MAX_EQCENTER (24000.0f) #define AL_DISTORTION_DEFAULT_EQCENTER (3600.0f) #define AL_DISTORTION_MIN_EQBANDWIDTH (80.0f) #define AL_DISTORTION_MAX_EQBANDWIDTH (24000.0f) #define AL_DISTORTION_DEFAULT_EQBANDWIDTH (3600.0f) /* Echo effect */ #define AL_ECHO_MIN_DELAY (0.0f) #define AL_ECHO_MAX_DELAY (0.207f) #define AL_ECHO_DEFAULT_DELAY (0.1f) #define AL_ECHO_MIN_LRDELAY (0.0f) #define AL_ECHO_MAX_LRDELAY (0.404f) #define AL_ECHO_DEFAULT_LRDELAY (0.1f) #define AL_ECHO_MIN_DAMPING (0.0f) #define AL_ECHO_MAX_DAMPING (0.99f) #define AL_ECHO_DEFAULT_DAMPING (0.5f) #define AL_ECHO_MIN_FEEDBACK (0.0f) #define AL_ECHO_MAX_FEEDBACK (1.0f) #define AL_ECHO_DEFAULT_FEEDBACK (0.5f) #define AL_ECHO_MIN_SPREAD (-1.0f) #define AL_ECHO_MAX_SPREAD (1.0f) #define AL_ECHO_DEFAULT_SPREAD (-1.0f) /* Flanger effect */ #define AL_FLANGER_WAVEFORM_SINUSOID (0) #define AL_FLANGER_WAVEFORM_TRIANGLE (1) #define AL_FLANGER_MIN_WAVEFORM (0) #define AL_FLANGER_MAX_WAVEFORM (1) #define AL_FLANGER_DEFAULT_WAVEFORM (1) #define AL_FLANGER_MIN_PHASE (-180) #define AL_FLANGER_MAX_PHASE (180) #define AL_FLANGER_DEFAULT_PHASE (0) #define AL_FLANGER_MIN_RATE (0.0f) #define AL_FLANGER_MAX_RATE (10.0f) #define AL_FLANGER_DEFAULT_RATE (0.27f) #define AL_FLANGER_MIN_DEPTH (0.0f) #define AL_FLANGER_MAX_DEPTH (1.0f) #define AL_FLANGER_DEFAULT_DEPTH (1.0f) #define AL_FLANGER_MIN_FEEDBACK (-1.0f) #define AL_FLANGER_MAX_FEEDBACK (1.0f) #define AL_FLANGER_DEFAULT_FEEDBACK (-0.5f) #define AL_FLANGER_MIN_DELAY (0.0f) #define AL_FLANGER_MAX_DELAY (0.004f) #define AL_FLANGER_DEFAULT_DELAY (0.002f) /* Frequency shifter effect */ #define AL_FREQUENCY_SHIFTER_MIN_FREQUENCY (0.0f) #define AL_FREQUENCY_SHIFTER_MAX_FREQUENCY (24000.0f) #define AL_FREQUENCY_SHIFTER_DEFAULT_FREQUENCY (0.0f) #define AL_FREQUENCY_SHIFTER_MIN_LEFT_DIRECTION (0) #define AL_FREQUENCY_SHIFTER_MAX_LEFT_DIRECTION (2) #define AL_FREQUENCY_SHIFTER_DEFAULT_LEFT_DIRECTION (0) #define AL_FREQUENCY_SHIFTER_DIRECTION_DOWN (0) #define AL_FREQUENCY_SHIFTER_DIRECTION_UP (1) #define AL_FREQUENCY_SHIFTER_DIRECTION_OFF (2) #define AL_FREQUENCY_SHIFTER_MIN_RIGHT_DIRECTION (0) #define AL_FREQUENCY_SHIFTER_MAX_RIGHT_DIRECTION (2) #define AL_FREQUENCY_SHIFTER_DEFAULT_RIGHT_DIRECTION (0) /* Vocal morpher effect */ #define AL_VOCAL_MORPHER_MIN_PHONEMEA (0) #define AL_VOCAL_MORPHER_MAX_PHONEMEA (29) #define AL_VOCAL_MORPHER_DEFAULT_PHONEMEA (0) #define AL_VOCAL_MORPHER_MIN_PHONEMEA_COARSE_TUNING (-24) #define AL_VOCAL_MORPHER_MAX_PHONEMEA_COARSE_TUNING (24) #define AL_VOCAL_MORPHER_DEFAULT_PHONEMEA_COARSE_TUNING (0) #define AL_VOCAL_MORPHER_MIN_PHONEMEB (0) #define AL_VOCAL_MORPHER_MAX_PHONEMEB (29) #define AL_VOCAL_MORPHER_DEFAULT_PHONEMEB (10) #define AL_VOCAL_MORPHER_MIN_PHONEMEB_COARSE_TUNING (-24) #define AL_VOCAL_MORPHER_MAX_PHONEMEB_COARSE_TUNING (24) #define AL_VOCAL_MORPHER_DEFAULT_PHONEMEB_COARSE_TUNING (0) #define AL_VOCAL_MORPHER_PHONEME_A (0) #define AL_VOCAL_MORPHER_PHONEME_E (1) #define AL_VOCAL_MORPHER_PHONEME_I (2) #define AL_VOCAL_MORPHER_PHONEME_O (3) #define AL_VOCAL_MORPHER_PHONEME_U (4) #define AL_VOCAL_MORPHER_PHONEME_AA (5) #define AL_VOCAL_MORPHER_PHONEME_AE (6) #define AL_VOCAL_MORPHER_PHONEME_AH (7) #define AL_VOCAL_MORPHER_PHONEME_AO (8) #define AL_VOCAL_MORPHER_PHONEME_EH (9) #define AL_VOCAL_MORPHER_PHONEME_ER (10) #define AL_VOCAL_MORPHER_PHONEME_IH (11) #define AL_VOCAL_MORPHER_PHONEME_IY (12) #define AL_VOCAL_MORPHER_PHONEME_UH (13) #define AL_VOCAL_MORPHER_PHONEME_UW (14) #define AL_VOCAL_MORPHER_PHONEME_B (15) #define AL_VOCAL_MORPHER_PHONEME_D (16) #define AL_VOCAL_MORPHER_PHONEME_F (17) #define AL_VOCAL_MORPHER_PHONEME_G (18) #define AL_VOCAL_MORPHER_PHONEME_J (19) #define AL_VOCAL_MORPHER_PHONEME_K (20) #define AL_VOCAL_MORPHER_PHONEME_L (21) #define AL_VOCAL_MORPHER_PHONEME_M (22) #define AL_VOCAL_MORPHER_PHONEME_N (23) #define AL_VOCAL_MORPHER_PHONEME_P (24) #define AL_VOCAL_MORPHER_PHONEME_R (25) #define AL_VOCAL_MORPHER_PHONEME_S (26) #define AL_VOCAL_MORPHER_PHONEME_T (27) #define AL_VOCAL_MORPHER_PHONEME_V (28) #define AL_VOCAL_MORPHER_PHONEME_Z (29) #define AL_VOCAL_MORPHER_WAVEFORM_SINUSOID (0) #define AL_VOCAL_MORPHER_WAVEFORM_TRIANGLE (1) #define AL_VOCAL_MORPHER_WAVEFORM_SAWTOOTH (2) #define AL_VOCAL_MORPHER_MIN_WAVEFORM (0) #define AL_VOCAL_MORPHER_MAX_WAVEFORM (2) #define AL_VOCAL_MORPHER_DEFAULT_WAVEFORM (0) #define AL_VOCAL_MORPHER_MIN_RATE (0.0f) #define AL_VOCAL_MORPHER_MAX_RATE (10.0f) #define AL_VOCAL_MORPHER_DEFAULT_RATE (1.41f) /* Pitch shifter effect */ #define AL_PITCH_SHIFTER_MIN_COARSE_TUNE (-12) #define AL_PITCH_SHIFTER_MAX_COARSE_TUNE (12) #define AL_PITCH_SHIFTER_DEFAULT_COARSE_TUNE (12) #define AL_PITCH_SHIFTER_MIN_FINE_TUNE (-50) #define AL_PITCH_SHIFTER_MAX_FINE_TUNE (50) #define AL_PITCH_SHIFTER_DEFAULT_FINE_TUNE (0) /* Ring modulator effect */ #define AL_RING_MODULATOR_MIN_FREQUENCY (0.0f) #define AL_RING_MODULATOR_MAX_FREQUENCY (8000.0f) #define AL_RING_MODULATOR_DEFAULT_FREQUENCY (440.0f) #define AL_RING_MODULATOR_MIN_HIGHPASS_CUTOFF (0.0f) #define AL_RING_MODULATOR_MAX_HIGHPASS_CUTOFF (24000.0f) #define AL_RING_MODULATOR_DEFAULT_HIGHPASS_CUTOFF (800.0f) #define AL_RING_MODULATOR_SINUSOID (0) #define AL_RING_MODULATOR_SAWTOOTH (1) #define AL_RING_MODULATOR_SQUARE (2) #define AL_RING_MODULATOR_MIN_WAVEFORM (0) #define AL_RING_MODULATOR_MAX_WAVEFORM (2) #define AL_RING_MODULATOR_DEFAULT_WAVEFORM (0) /* Autowah effect */ #define AL_AUTOWAH_MIN_ATTACK_TIME (0.0001f) #define AL_AUTOWAH_MAX_ATTACK_TIME (1.0f) #define AL_AUTOWAH_DEFAULT_ATTACK_TIME (0.06f) #define AL_AUTOWAH_MIN_RELEASE_TIME (0.0001f) #define AL_AUTOWAH_MAX_RELEASE_TIME (1.0f) #define AL_AUTOWAH_DEFAULT_RELEASE_TIME (0.06f) #define AL_AUTOWAH_MIN_RESONANCE (2.0f) #define AL_AUTOWAH_MAX_RESONANCE (1000.0f) #define AL_AUTOWAH_DEFAULT_RESONANCE (1000.0f) #define AL_AUTOWAH_MIN_PEAK_GAIN (0.00003f) #define AL_AUTOWAH_MAX_PEAK_GAIN (31621.0f) #define AL_AUTOWAH_DEFAULT_PEAK_GAIN (11.22f) /* Compressor effect */ #define AL_COMPRESSOR_MIN_ONOFF (0) #define AL_COMPRESSOR_MAX_ONOFF (1) #define AL_COMPRESSOR_DEFAULT_ONOFF (1) /* Equalizer effect */ #define AL_EQUALIZER_MIN_LOW_GAIN (0.126f) #define AL_EQUALIZER_MAX_LOW_GAIN (7.943f) #define AL_EQUALIZER_DEFAULT_LOW_GAIN (1.0f) #define AL_EQUALIZER_MIN_LOW_CUTOFF (50.0f) #define AL_EQUALIZER_MAX_LOW_CUTOFF (800.0f) #define AL_EQUALIZER_DEFAULT_LOW_CUTOFF (200.0f) #define AL_EQUALIZER_MIN_MID1_GAIN (0.126f) #define AL_EQUALIZER_MAX_MID1_GAIN (7.943f) #define AL_EQUALIZER_DEFAULT_MID1_GAIN (1.0f) #define AL_EQUALIZER_MIN_MID1_CENTER (200.0f) #define AL_EQUALIZER_MAX_MID1_CENTER (3000.0f) #define AL_EQUALIZER_DEFAULT_MID1_CENTER (500.0f) #define AL_EQUALIZER_MIN_MID1_WIDTH (0.01f) #define AL_EQUALIZER_MAX_MID1_WIDTH (1.0f) #define AL_EQUALIZER_DEFAULT_MID1_WIDTH (1.0f) #define AL_EQUALIZER_MIN_MID2_GAIN (0.126f) #define AL_EQUALIZER_MAX_MID2_GAIN (7.943f) #define AL_EQUALIZER_DEFAULT_MID2_GAIN (1.0f) #define AL_EQUALIZER_MIN_MID2_CENTER (1000.0f) #define AL_EQUALIZER_MAX_MID2_CENTER (8000.0f) #define AL_EQUALIZER_DEFAULT_MID2_CENTER (3000.0f) #define AL_EQUALIZER_MIN_MID2_WIDTH (0.01f) #define AL_EQUALIZER_MAX_MID2_WIDTH (1.0f) #define AL_EQUALIZER_DEFAULT_MID2_WIDTH (1.0f) #define AL_EQUALIZER_MIN_HIGH_GAIN (0.126f) #define AL_EQUALIZER_MAX_HIGH_GAIN (7.943f) #define AL_EQUALIZER_DEFAULT_HIGH_GAIN (1.0f) #define AL_EQUALIZER_MIN_HIGH_CUTOFF (4000.0f) #define AL_EQUALIZER_MAX_HIGH_CUTOFF (16000.0f) #define AL_EQUALIZER_DEFAULT_HIGH_CUTOFF (6000.0f) /* Source parameter value ranges and defaults. */ #define AL_MIN_AIR_ABSORPTION_FACTOR (0.0f) #define AL_MAX_AIR_ABSORPTION_FACTOR (10.0f) #define AL_DEFAULT_AIR_ABSORPTION_FACTOR (0.0f) #define AL_MIN_ROOM_ROLLOFF_FACTOR (0.0f) #define AL_MAX_ROOM_ROLLOFF_FACTOR (10.0f) #define AL_DEFAULT_ROOM_ROLLOFF_FACTOR (0.0f) #define AL_MIN_CONE_OUTER_GAINHF (0.0f) #define AL_MAX_CONE_OUTER_GAINHF (1.0f) #define AL_DEFAULT_CONE_OUTER_GAINHF (1.0f) #define AL_MIN_DIRECT_FILTER_GAINHF_AUTO AL_FALSE #define AL_MAX_DIRECT_FILTER_GAINHF_AUTO AL_TRUE #define AL_DEFAULT_DIRECT_FILTER_GAINHF_AUTO AL_TRUE #define AL_MIN_AUXILIARY_SEND_FILTER_GAIN_AUTO AL_FALSE #define AL_MAX_AUXILIARY_SEND_FILTER_GAIN_AUTO AL_TRUE #define AL_DEFAULT_AUXILIARY_SEND_FILTER_GAIN_AUTO AL_TRUE #define AL_MIN_AUXILIARY_SEND_FILTER_GAINHF_AUTO AL_FALSE #define AL_MAX_AUXILIARY_SEND_FILTER_GAINHF_AUTO AL_TRUE #define AL_DEFAULT_AUXILIARY_SEND_FILTER_GAINHF_AUTO AL_TRUE /* Listener parameter value ranges and defaults. */ #define AL_MIN_METERS_PER_UNIT FLT_MIN #define AL_MAX_METERS_PER_UNIT FLT_MAX #define AL_DEFAULT_METERS_PER_UNIT (1.0f) #ifdef __cplusplus } /* extern "C" */ #endif #endif /* AL_EFX_H */ ================================================ FILE: apps/openmw/mwsound/ffmpeg_decoder.cpp ================================================ #include "ffmpeg_decoder.hpp" #include #include #include #include #include namespace MWSound { int FFmpeg_Decoder::readPacket(void *user_data, uint8_t *buf, int buf_size) { try { std::istream& stream = *static_cast(user_data)->mDataStream; stream.clear(); stream.read((char*)buf, buf_size); return stream.gcount(); } catch (std::exception& ) { return 0; } } int FFmpeg_Decoder::writePacket(void *, uint8_t *, int) { Log(Debug::Error) << "can't write to read-only stream"; return -1; } int64_t FFmpeg_Decoder::seek(void *user_data, int64_t offset, int whence) { std::istream& stream = *static_cast(user_data)->mDataStream; whence &= ~AVSEEK_FORCE; stream.clear(); if(whence == AVSEEK_SIZE) { size_t prev = stream.tellg(); stream.seekg(0, std::ios_base::end); size_t size = stream.tellg(); stream.seekg(prev, std::ios_base::beg); return size; } if(whence == SEEK_SET) stream.seekg(offset, std::ios_base::beg); else if(whence == SEEK_CUR) stream.seekg(offset, std::ios_base::cur); else if(whence == SEEK_END) stream.seekg(offset, std::ios_base::end); else return -1; return stream.tellg(); } /* Used by getAV*Data to search for more compressed data, and buffer it in the * correct stream. It won't buffer data for streams that the app doesn't have a * handle for. */ bool FFmpeg_Decoder::getNextPacket() { if(!mStream) return false; int stream_idx = mStream - mFormatCtx->streams; while(av_read_frame(mFormatCtx, &mPacket) >= 0) { /* Check if the packet belongs to this stream */ if(stream_idx == mPacket.stream_index) { if(mPacket.pts != (int64_t)AV_NOPTS_VALUE) mNextPts = av_q2d((*mStream)->time_base)*mPacket.pts; return true; } /* Free the packet and look for another */ av_packet_unref(&mPacket); } return false; } bool FFmpeg_Decoder::getAVAudioData() { bool got_frame = false; if(mCodecCtx->codec_type != AVMEDIA_TYPE_AUDIO) return false; do { /* Decode some data, and check for errors */ int ret = avcodec_receive_frame(mCodecCtx, mFrame); if (ret == AVERROR(EAGAIN)) { if (mPacket.size == 0 && !getNextPacket()) return false; ret = avcodec_send_packet(mCodecCtx, &mPacket); av_packet_unref(&mPacket); if (ret == 0) continue; } if (ret != 0) return false; av_packet_unref(&mPacket); if (mFrame->nb_samples == 0) continue; got_frame = true; if(mSwr) { if(!mDataBuf || mDataBufLen < mFrame->nb_samples) { av_freep(&mDataBuf); if(av_samples_alloc(&mDataBuf, nullptr, av_get_channel_layout_nb_channels(mOutputChannelLayout), mFrame->nb_samples, mOutputSampleFormat, 0) < 0) return false; else mDataBufLen = mFrame->nb_samples; } if(swr_convert(mSwr, (uint8_t**)&mDataBuf, mFrame->nb_samples, (const uint8_t**)mFrame->extended_data, mFrame->nb_samples) < 0) { return false; } mFrameData = &mDataBuf; } else mFrameData = &mFrame->data[0]; } while(!got_frame); mNextPts += (double)mFrame->nb_samples / mCodecCtx->sample_rate; return true; } size_t FFmpeg_Decoder::readAVAudioData(void *data, size_t length) { size_t dec = 0; while(dec < length) { /* If there's no decoded data, find some */ if(mFramePos >= mFrameSize) { if(!getAVAudioData()) break; mFramePos = 0; mFrameSize = mFrame->nb_samples * av_get_channel_layout_nb_channels(mOutputChannelLayout) * av_get_bytes_per_sample(mOutputSampleFormat); } /* Get the amount of bytes remaining to be written, and clamp to * the amount of decoded data we have */ size_t rem = std::min(length-dec, mFrameSize-mFramePos); /* Copy the data to the app's buffer and increment */ memcpy(data, mFrameData[0]+mFramePos, rem); data = (char*)data + rem; dec += rem; mFramePos += rem; } /* Return the number of bytes we were able to get */ return dec; } void FFmpeg_Decoder::open(const std::string &fname) { close(); mDataStream = mResourceMgr->get(fname); if((mFormatCtx=avformat_alloc_context()) == nullptr) throw std::runtime_error("Failed to allocate context"); try { mFormatCtx->pb = avio_alloc_context(nullptr, 0, 0, this, readPacket, writePacket, seek); if(!mFormatCtx->pb || avformat_open_input(&mFormatCtx, fname.c_str(), nullptr, nullptr) != 0) { // "Note that a user-supplied AVFormatContext will be freed on failure". if (mFormatCtx) { if (mFormatCtx->pb != nullptr) { if (mFormatCtx->pb->buffer != nullptr) { av_free(mFormatCtx->pb->buffer); mFormatCtx->pb->buffer = nullptr; } av_free(mFormatCtx->pb); mFormatCtx->pb = nullptr; } avformat_free_context(mFormatCtx); } mFormatCtx = nullptr; throw std::runtime_error("Failed to allocate input stream"); } if(avformat_find_stream_info(mFormatCtx, nullptr) < 0) throw std::runtime_error("Failed to find stream info in "+fname); for(size_t j = 0;j < mFormatCtx->nb_streams;j++) { if(mFormatCtx->streams[j]->codecpar->codec_type == AVMEDIA_TYPE_AUDIO) { mStream = &mFormatCtx->streams[j]; break; } } if(!mStream) throw std::runtime_error("No audio streams in "+fname); const AVCodec *codec = avcodec_find_decoder((*mStream)->codecpar->codec_id); if(!codec) { std::string ss = "No codec found for id " + std::to_string((*mStream)->codecpar->codec_id); throw std::runtime_error(ss); } AVCodecContext *avctx = avcodec_alloc_context3(codec); avcodec_parameters_to_context(avctx, (*mStream)->codecpar); // This is not needed anymore above FFMpeg version 4.0 #if LIBAVCODEC_VERSION_INT < 3805796 av_codec_set_pkt_timebase(avctx, (*mStream)->time_base); #endif mCodecCtx = avctx; if(avcodec_open2(mCodecCtx, codec, nullptr) < 0) throw std::runtime_error(std::string("Failed to open audio codec ") + codec->long_name); mFrame = av_frame_alloc(); if(mCodecCtx->sample_fmt == AV_SAMPLE_FMT_FLT || mCodecCtx->sample_fmt == AV_SAMPLE_FMT_FLTP) mOutputSampleFormat = AV_SAMPLE_FMT_S16; // FIXME: Check for AL_EXT_FLOAT32 support else if(mCodecCtx->sample_fmt == AV_SAMPLE_FMT_U8P) mOutputSampleFormat = AV_SAMPLE_FMT_U8; else if(mCodecCtx->sample_fmt == AV_SAMPLE_FMT_S16P) mOutputSampleFormat = AV_SAMPLE_FMT_S16; else mOutputSampleFormat = AV_SAMPLE_FMT_S16; mOutputChannelLayout = (*mStream)->codecpar->channel_layout; if(mOutputChannelLayout == 0) mOutputChannelLayout = av_get_default_channel_layout(mCodecCtx->channels); mCodecCtx->channel_layout = mOutputChannelLayout; } catch(...) { if(mStream) avcodec_free_context(&mCodecCtx); mStream = nullptr; if (mFormatCtx != nullptr) { if (mFormatCtx->pb->buffer != nullptr) { av_free(mFormatCtx->pb->buffer); mFormatCtx->pb->buffer = nullptr; } av_free(mFormatCtx->pb); mFormatCtx->pb = nullptr; avformat_close_input(&mFormatCtx); } } } void FFmpeg_Decoder::close() { if(mStream) avcodec_free_context(&mCodecCtx); mStream = nullptr; av_packet_unref(&mPacket); av_freep(&mDataBuf); av_frame_free(&mFrame); swr_free(&mSwr); if(mFormatCtx) { if (mFormatCtx->pb != nullptr) { // mFormatCtx->pb->buffer must be freed by hand, // if not, valgrind will show memleak, see: // // https://trac.ffmpeg.org/ticket/1357 // if (mFormatCtx->pb->buffer != nullptr) { av_freep(&mFormatCtx->pb->buffer); } #if LIBAVCODEC_VERSION_INT >= AV_VERSION_INT(57, 80, 100) avio_context_free(&mFormatCtx->pb); #else av_freep(&mFormatCtx->pb); #endif } avformat_close_input(&mFormatCtx); } mDataStream.reset(); } std::string FFmpeg_Decoder::getName() { // In the FFMpeg 4.0 a "filename" field was replaced by "url" #if LIBAVCODEC_VERSION_INT < 3805796 return mFormatCtx->filename; #else return mFormatCtx->url; #endif } void FFmpeg_Decoder::getInfo(int *samplerate, ChannelConfig *chans, SampleType *type) { if(!mStream) throw std::runtime_error("No audio stream info"); if(mOutputSampleFormat == AV_SAMPLE_FMT_U8) *type = SampleType_UInt8; else if(mOutputSampleFormat == AV_SAMPLE_FMT_S16) *type = SampleType_Int16; else if(mOutputSampleFormat == AV_SAMPLE_FMT_FLT) *type = SampleType_Float32; else { mOutputSampleFormat = AV_SAMPLE_FMT_S16; *type = SampleType_Int16; } if(mOutputChannelLayout == AV_CH_LAYOUT_MONO) *chans = ChannelConfig_Mono; else if(mOutputChannelLayout == AV_CH_LAYOUT_STEREO) *chans = ChannelConfig_Stereo; else if(mOutputChannelLayout == AV_CH_LAYOUT_QUAD) *chans = ChannelConfig_Quad; else if(mOutputChannelLayout == AV_CH_LAYOUT_5POINT1) *chans = ChannelConfig_5point1; else if(mOutputChannelLayout == AV_CH_LAYOUT_7POINT1) *chans = ChannelConfig_7point1; else { char str[1024]; av_get_channel_layout_string(str, sizeof(str), mCodecCtx->channels, mCodecCtx->channel_layout); Log(Debug::Error) << "Unsupported channel layout: "<< str; if(mCodecCtx->channels == 1) { mOutputChannelLayout = AV_CH_LAYOUT_MONO; *chans = ChannelConfig_Mono; } else { mOutputChannelLayout = AV_CH_LAYOUT_STEREO; *chans = ChannelConfig_Stereo; } } *samplerate = mCodecCtx->sample_rate; int64_t ch_layout = mCodecCtx->channel_layout; if(ch_layout == 0) ch_layout = av_get_default_channel_layout(mCodecCtx->channels); if(mOutputSampleFormat != mCodecCtx->sample_fmt || mOutputChannelLayout != ch_layout) { mSwr = swr_alloc_set_opts(mSwr, // SwrContext mOutputChannelLayout, // output ch layout mOutputSampleFormat, // output sample format mCodecCtx->sample_rate, // output sample rate ch_layout, // input ch layout mCodecCtx->sample_fmt, // input sample format mCodecCtx->sample_rate, // input sample rate 0, // logging level offset nullptr); // log context if(!mSwr) throw std::runtime_error("Couldn't allocate SwrContext"); int init=swr_init(mSwr); if(init < 0) throw std::runtime_error("Couldn't initialize SwrContext: "+std::to_string(init)); } } size_t FFmpeg_Decoder::read(char *buffer, size_t bytes) { if(!mStream) { Log(Debug::Error) << "No audio stream"; return 0; } return readAVAudioData(buffer, bytes); } void FFmpeg_Decoder::readAll(std::vector &output) { if(!mStream) { Log(Debug::Error) << "No audio stream"; return; } while(getAVAudioData()) { size_t got = mFrame->nb_samples * av_get_channel_layout_nb_channels(mOutputChannelLayout) * av_get_bytes_per_sample(mOutputSampleFormat); const char *inbuf = reinterpret_cast(mFrameData[0]); output.insert(output.end(), inbuf, inbuf+got); } } size_t FFmpeg_Decoder::getSampleOffset() { int delay = (mFrameSize-mFramePos) / av_get_channel_layout_nb_channels(mOutputChannelLayout) / av_get_bytes_per_sample(mOutputSampleFormat); return (int)(mNextPts*mCodecCtx->sample_rate) - delay; } FFmpeg_Decoder::FFmpeg_Decoder(const VFS::Manager* vfs) : Sound_Decoder(vfs) , mFormatCtx(nullptr) , mCodecCtx(nullptr) , mStream(nullptr) , mFrame(nullptr) , mFrameSize(0) , mFramePos(0) , mNextPts(0.0) , mSwr(nullptr) , mOutputSampleFormat(AV_SAMPLE_FMT_NONE) , mOutputChannelLayout(0) , mDataBuf(nullptr) , mFrameData(nullptr) , mDataBufLen(0) { memset(&mPacket, 0, sizeof(mPacket)); /* We need to make sure ffmpeg is initialized. Optionally silence warning * output from the lib */ static bool done_init = false; if(!done_init) { // This is not needed anymore above FFMpeg version 4.0 #if LIBAVCODEC_VERSION_INT < 3805796 av_register_all(); #endif av_log_set_level(AV_LOG_ERROR); done_init = true; } } FFmpeg_Decoder::~FFmpeg_Decoder() { close(); } } ================================================ FILE: apps/openmw/mwsound/ffmpeg_decoder.hpp ================================================ #ifndef GAME_SOUND_FFMPEG_DECODER_H #define GAME_SOUND_FFMPEG_DECODER_H #include #if defined(_MSC_VER) #pragma warning (push) #pragma warning (disable : 4244) #endif extern "C" { #include #include #include // From version 54.56 binkaudio encoding format changed from S16 to FLTP. See: // https://gitorious.org/ffmpeg/ffmpeg/commit/7bfd1766d1c18f07b0a2dd042418a874d49ea60d // https://ffmpeg.zeranoe.com/forum/viewtopic.php?f=15&t=872 #include } #if defined(_MSC_VER) #pragma warning (pop) #endif #include #include #include #include "sound_decoder.hpp" namespace MWSound { class FFmpeg_Decoder final : public Sound_Decoder { AVFormatContext *mFormatCtx; AVCodecContext *mCodecCtx; AVStream **mStream; AVPacket mPacket; AVFrame *mFrame; int mFrameSize; int mFramePos; double mNextPts; SwrContext *mSwr; enum AVSampleFormat mOutputSampleFormat; int64_t mOutputChannelLayout; uint8_t *mDataBuf; uint8_t **mFrameData; int mDataBufLen; bool getNextPacket(); Files::IStreamPtr mDataStream; static int readPacket(void *user_data, uint8_t *buf, int buf_size); static int writePacket(void *user_data, uint8_t *buf, int buf_size); static int64_t seek(void *user_data, int64_t offset, int whence); bool getAVAudioData(); size_t readAVAudioData(void *data, size_t length); void open(const std::string &fname) override; void close() override; std::string getName() override; void getInfo(int *samplerate, ChannelConfig *chans, SampleType *type) override; size_t read(char *buffer, size_t bytes) override; void readAll(std::vector &output) override; size_t getSampleOffset() override; FFmpeg_Decoder& operator=(const FFmpeg_Decoder &rhs); FFmpeg_Decoder(const FFmpeg_Decoder &rhs); public: explicit FFmpeg_Decoder(const VFS::Manager* vfs); virtual ~FFmpeg_Decoder(); friend class SoundManager; }; } #endif ================================================ FILE: apps/openmw/mwsound/loudness.cpp ================================================ #include "loudness.hpp" #include #include #include #include "soundmanagerimp.hpp" namespace MWSound { void Sound_Loudness::analyzeLoudness(const std::vector< char >& data) { mQueue.insert( mQueue.end(), data.begin(), data.end() ); if (!mQueue.size()) return; int samplesPerSegment = static_cast(mSampleRate / mSamplesPerSec); int numSamples = bytesToFrames(mQueue.size(), mChannelConfig, mSampleType); int advance = framesToBytes(1, mChannelConfig, mSampleType); int segment=0; int sample=0; while (segment < numSamples/samplesPerSegment) { float sum=0; int samplesAdded = 0; while (sample < numSamples && sample < (segment+1)*samplesPerSegment) { // get sample on a scale from -1 to 1 float value = 0; if (mSampleType == SampleType_UInt8) value = ((char)(mQueue[sample*advance]^0x80))/128.f; else if (mSampleType == SampleType_Int16) { value = *reinterpret_cast(&mQueue[sample*advance]); value /= float(std::numeric_limits::max()); } else if (mSampleType == SampleType_Float32) { value = *reinterpret_cast(&mQueue[sample*advance]); value = std::max(-1.f, std::min(1.f, value)); // Float samples *should* be scaled to [-1,1] already. } sum += value*value; ++samplesAdded; ++sample; } float rms = 0; // root mean square if (samplesAdded > 0) rms = std::sqrt(sum / samplesAdded); mSamples.push_back(rms); ++segment; } mQueue.erase(mQueue.begin(), mQueue.begin() + sample*advance); } float Sound_Loudness::getLoudnessAtTime(float sec) const { if(mSamplesPerSec <= 0.0f || mSamples.empty() || sec < 0.0f) return 0.0f; size_t index = static_cast(sec * mSamplesPerSec); index = std::max(0, std::min(index, mSamples.size()-1)); return mSamples[index]; } } ================================================ FILE: apps/openmw/mwsound/loudness.hpp ================================================ #ifndef GAME_SOUND_LOUDNESS_H #define GAME_SOUND_LOUDNESS_H #include #include #include "sound_decoder.hpp" namespace MWSound { class Sound_Loudness { float mSamplesPerSec; int mSampleRate; ChannelConfig mChannelConfig; SampleType mSampleType; // Loudness sample info std::vector mSamples; std::deque mQueue; public: /** * @param samplesPerSecond How many loudness values per second of audio to compute. * @param sampleRate the sample rate of the sound buffer * @param chans channel layout of the buffer * @param type sample type of the buffer */ Sound_Loudness(float samplesPerSecond, int sampleRate, ChannelConfig chans, SampleType type) : mSamplesPerSec(samplesPerSecond) , mSampleRate(sampleRate) , mChannelConfig(chans) , mSampleType(type) { } /** * Analyzes the energy (closely related to loudness) of a sound buffer. * The buffer will be divided into segments according to \a valuesPerSecond, * and for each segment a loudness value in the range of [0,1] will be computed. * The computed values are then added to the mSamples vector. This method should be called continuously * with chunks of audio until the whole audio file is processed. * If the size of \a data does not exactly fit a number of loudness samples, the remainder * will be kept in the mQueue and used in the next call to analyzeLoudness. * @param data the sound buffer to analyze, containing raw samples */ void analyzeLoudness(const std::vector& data); /** * Get loudness at a particular time. Before calling this, the stream has to be analyzed up to that point in time (see analyzeLoudness()). */ float getLoudnessAtTime(float sec) const; }; } #endif /* GAME_SOUND_LOUDNESS_H */ ================================================ FILE: apps/openmw/mwsound/movieaudiofactory.cpp ================================================ #include "movieaudiofactory.hpp" #include #include #include "../mwbase/environment.hpp" #include "../mwbase/soundmanager.hpp" #include "sound_decoder.hpp" #include "sound.hpp" namespace MWSound { class MovieAudioDecoder; class MWSoundDecoderBridge final : public Sound_Decoder { public: MWSoundDecoderBridge(MWSound::MovieAudioDecoder* decoder) : Sound_Decoder(nullptr) , mDecoder(decoder) { } private: MWSound::MovieAudioDecoder* mDecoder; void open(const std::string &fname) override; void close() override; std::string getName() override; void getInfo(int *samplerate, ChannelConfig *chans, SampleType *type) override; size_t read(char *buffer, size_t bytes) override; size_t getSampleOffset() override; }; class MovieAudioDecoder : public Video::MovieAudioDecoder { public: MovieAudioDecoder(Video::VideoState *videoState) : Video::MovieAudioDecoder(videoState), mAudioTrack(nullptr) { mDecoderBridge.reset(new MWSoundDecoderBridge(this)); } size_t getSampleOffset() { ssize_t clock_delay = (mFrameSize-mFramePos) / av_get_channel_layout_nb_channels(mOutputChannelLayout) / av_get_bytes_per_sample(mOutputSampleFormat); return (size_t)(mAudioClock*mAudioContext->sample_rate) - clock_delay; } std::string getStreamName() { return std::string(); } private: // MovieAudioDecoder overrides double getAudioClock() override { return (double)getSampleOffset()/(double)mAudioContext->sample_rate - MWBase::Environment::get().getSoundManager()->getTrackTimeDelay(mAudioTrack); } void adjustAudioSettings(AVSampleFormat& sampleFormat, uint64_t& channelLayout, int& sampleRate) override { if (sampleFormat == AV_SAMPLE_FMT_U8P || sampleFormat == AV_SAMPLE_FMT_U8) sampleFormat = AV_SAMPLE_FMT_U8; else if (sampleFormat == AV_SAMPLE_FMT_S16P || sampleFormat == AV_SAMPLE_FMT_S16) sampleFormat = AV_SAMPLE_FMT_S16; else if (sampleFormat == AV_SAMPLE_FMT_FLTP || sampleFormat == AV_SAMPLE_FMT_FLT) sampleFormat = AV_SAMPLE_FMT_S16; // FIXME: check for AL_EXT_FLOAT32 support else sampleFormat = AV_SAMPLE_FMT_S16; if (channelLayout == AV_CH_LAYOUT_5POINT1 || channelLayout == AV_CH_LAYOUT_7POINT1 || channelLayout == AV_CH_LAYOUT_QUAD) // FIXME: check for AL_EXT_MCFORMATS support channelLayout = AV_CH_LAYOUT_STEREO; else if (channelLayout != AV_CH_LAYOUT_MONO && channelLayout != AV_CH_LAYOUT_STEREO) channelLayout = AV_CH_LAYOUT_STEREO; } public: ~MovieAudioDecoder() { if(mAudioTrack) MWBase::Environment::get().getSoundManager()->stopTrack(mAudioTrack); mAudioTrack = nullptr; mDecoderBridge.reset(); } MWBase::SoundStream *mAudioTrack; std::shared_ptr mDecoderBridge; }; void MWSoundDecoderBridge::open(const std::string &fname) { throw std::runtime_error("Method not implemented"); } void MWSoundDecoderBridge::close() {} std::string MWSoundDecoderBridge::getName() { return mDecoder->getStreamName(); } void MWSoundDecoderBridge::getInfo(int *samplerate, ChannelConfig *chans, SampleType *type) { *samplerate = mDecoder->getOutputSampleRate(); uint64_t outputChannelLayout = mDecoder->getOutputChannelLayout(); if (outputChannelLayout == AV_CH_LAYOUT_MONO) *chans = ChannelConfig_Mono; else if (outputChannelLayout == AV_CH_LAYOUT_5POINT1) *chans = ChannelConfig_5point1; else if (outputChannelLayout == AV_CH_LAYOUT_7POINT1) *chans = ChannelConfig_7point1; else if (outputChannelLayout == AV_CH_LAYOUT_STEREO) *chans = ChannelConfig_Stereo; else if (outputChannelLayout == AV_CH_LAYOUT_QUAD) *chans = ChannelConfig_Quad; else throw std::runtime_error("Unsupported channel layout: "+ std::to_string(outputChannelLayout)); AVSampleFormat outputSampleFormat = mDecoder->getOutputSampleFormat(); if (outputSampleFormat == AV_SAMPLE_FMT_U8) *type = SampleType_UInt8; else if (outputSampleFormat == AV_SAMPLE_FMT_FLT) *type = SampleType_Float32; else if (outputSampleFormat == AV_SAMPLE_FMT_S16) *type = SampleType_Int16; else { char str[1024]; av_get_sample_fmt_string(str, sizeof(str), outputSampleFormat); throw std::runtime_error(std::string("Unsupported sample format: ")+str); } } size_t MWSoundDecoderBridge::read(char *buffer, size_t bytes) { return mDecoder->read(buffer, bytes); } size_t MWSoundDecoderBridge::getSampleOffset() { return mDecoder->getSampleOffset(); } std::shared_ptr MovieAudioFactory::createDecoder(Video::VideoState* videoState) { std::shared_ptr decoder(new MWSound::MovieAudioDecoder(videoState)); decoder->setupFormat(); MWBase::SoundManager *sndMgr = MWBase::Environment::get().getSoundManager(); MWBase::SoundStream *sound = sndMgr->playTrack(decoder->mDecoderBridge, MWSound::Type::Movie); if (!sound) { decoder.reset(); return decoder; } decoder->mAudioTrack = sound; return decoder; } } ================================================ FILE: apps/openmw/mwsound/movieaudiofactory.hpp ================================================ #ifndef OPENMW_MWSOUND_MOVIEAUDIOFACTORY_H #define OPENMW_MWSOUND_MOVIEAUDIOFACTORY_H #include namespace MWSound { class MovieAudioFactory : public Video::MovieAudioFactory { std::shared_ptr createDecoder(Video::VideoState* videoState) override; }; } #endif ================================================ FILE: apps/openmw/mwsound/openal_output.cpp ================================================ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "openal_output.hpp" #include "sound_decoder.hpp" #include "sound.hpp" #include "soundmanagerimp.hpp" #include "loudness.hpp" #include "efx-presets.h" #ifndef ALC_ALL_DEVICES_SPECIFIER #define ALC_ALL_DEVICES_SPECIFIER 0x1013 #endif #define MAKE_PTRID(id) ((void*)(uintptr_t)id) #define GET_PTRID(ptr) ((ALuint)(uintptr_t)ptr) namespace { const int sLoudnessFPS = 20; // loudness values per second of audio ALCenum checkALCError(ALCdevice *device, const char *func, int line) { ALCenum err = alcGetError(device); if(err != ALC_NO_ERROR) Log(Debug::Error) << "ALC error "<< alcGetString(device, err) << " (" << err << ") @ " << func << ":" << line; return err; } #define getALCError(d) checkALCError((d), __FUNCTION__, __LINE__) ALenum checkALError(const char *func, int line) { ALenum err = alGetError(); if(err != AL_NO_ERROR) Log(Debug::Error) << "AL error " << alGetString(err) << " (" << err << ") @ " << func << ":" << line; return err; } #define getALError() checkALError(__FUNCTION__, __LINE__) // Helper to get an OpenAL extension function template void convertPointer(T& dest, R src) { memcpy(&dest, &src, sizeof(src)); } template void getALCFunc(T& func, ALCdevice *device, const char *name) { void* funcPtr = alcGetProcAddress(device, name); convertPointer(func, funcPtr); } template void getALFunc(T& func, const char *name) { void* funcPtr = alGetProcAddress(name); convertPointer(func, funcPtr); } // Effect objects LPALGENEFFECTS alGenEffects; LPALDELETEEFFECTS alDeleteEffects; LPALISEFFECT alIsEffect; LPALEFFECTI alEffecti; LPALEFFECTIV alEffectiv; LPALEFFECTF alEffectf; LPALEFFECTFV alEffectfv; LPALGETEFFECTI alGetEffecti; LPALGETEFFECTIV alGetEffectiv; LPALGETEFFECTF alGetEffectf; LPALGETEFFECTFV alGetEffectfv; // Filter objects LPALGENFILTERS alGenFilters; LPALDELETEFILTERS alDeleteFilters; LPALISFILTER alIsFilter; LPALFILTERI alFilteri; LPALFILTERIV alFilteriv; LPALFILTERF alFilterf; LPALFILTERFV alFilterfv; LPALGETFILTERI alGetFilteri; LPALGETFILTERIV alGetFilteriv; LPALGETFILTERF alGetFilterf; LPALGETFILTERFV alGetFilterfv; // Auxiliary slot objects LPALGENAUXILIARYEFFECTSLOTS alGenAuxiliaryEffectSlots; LPALDELETEAUXILIARYEFFECTSLOTS alDeleteAuxiliaryEffectSlots; LPALISAUXILIARYEFFECTSLOT alIsAuxiliaryEffectSlot; LPALAUXILIARYEFFECTSLOTI alAuxiliaryEffectSloti; LPALAUXILIARYEFFECTSLOTIV alAuxiliaryEffectSlotiv; LPALAUXILIARYEFFECTSLOTF alAuxiliaryEffectSlotf; LPALAUXILIARYEFFECTSLOTFV alAuxiliaryEffectSlotfv; LPALGETAUXILIARYEFFECTSLOTI alGetAuxiliaryEffectSloti; LPALGETAUXILIARYEFFECTSLOTIV alGetAuxiliaryEffectSlotiv; LPALGETAUXILIARYEFFECTSLOTF alGetAuxiliaryEffectSlotf; LPALGETAUXILIARYEFFECTSLOTFV alGetAuxiliaryEffectSlotfv; void LoadEffect(ALuint effect, const EFXEAXREVERBPROPERTIES &props) { ALint type = AL_NONE; alGetEffecti(effect, AL_EFFECT_TYPE, &type); if(type == AL_EFFECT_EAXREVERB) { alEffectf(effect, AL_EAXREVERB_DIFFUSION, props.flDiffusion); alEffectf(effect, AL_EAXREVERB_DENSITY, props.flDensity); alEffectf(effect, AL_EAXREVERB_GAIN, props.flGain); alEffectf(effect, AL_EAXREVERB_GAINHF, props.flGainHF); alEffectf(effect, AL_EAXREVERB_GAINLF, props.flGainLF); alEffectf(effect, AL_EAXREVERB_DECAY_TIME, props.flDecayTime); alEffectf(effect, AL_EAXREVERB_DECAY_HFRATIO, props.flDecayHFRatio); alEffectf(effect, AL_EAXREVERB_DECAY_LFRATIO, props.flDecayLFRatio); alEffectf(effect, AL_EAXREVERB_REFLECTIONS_GAIN, props.flReflectionsGain); alEffectf(effect, AL_EAXREVERB_REFLECTIONS_DELAY, props.flReflectionsDelay); alEffectfv(effect, AL_EAXREVERB_REFLECTIONS_PAN, props.flReflectionsPan); alEffectf(effect, AL_EAXREVERB_LATE_REVERB_GAIN, props.flLateReverbGain); alEffectf(effect, AL_EAXREVERB_LATE_REVERB_DELAY, props.flLateReverbDelay); alEffectfv(effect, AL_EAXREVERB_LATE_REVERB_PAN, props.flLateReverbPan); alEffectf(effect, AL_EAXREVERB_ECHO_TIME, props.flEchoTime); alEffectf(effect, AL_EAXREVERB_ECHO_DEPTH, props.flEchoDepth); alEffectf(effect, AL_EAXREVERB_MODULATION_TIME, props.flModulationTime); alEffectf(effect, AL_EAXREVERB_MODULATION_DEPTH, props.flModulationDepth); alEffectf(effect, AL_EAXREVERB_AIR_ABSORPTION_GAINHF, props.flAirAbsorptionGainHF); alEffectf(effect, AL_EAXREVERB_HFREFERENCE, props.flHFReference); alEffectf(effect, AL_EAXREVERB_LFREFERENCE, props.flLFReference); alEffectf(effect, AL_EAXREVERB_ROOM_ROLLOFF_FACTOR, props.flRoomRolloffFactor); alEffecti(effect, AL_EAXREVERB_DECAY_HFLIMIT, props.iDecayHFLimit ? AL_TRUE : AL_FALSE); } else if(type == AL_EFFECT_REVERB) { alEffectf(effect, AL_REVERB_DIFFUSION, props.flDiffusion); alEffectf(effect, AL_REVERB_DENSITY, props.flDensity); alEffectf(effect, AL_REVERB_GAIN, props.flGain); alEffectf(effect, AL_REVERB_GAINHF, props.flGainHF); alEffectf(effect, AL_REVERB_DECAY_TIME, props.flDecayTime); alEffectf(effect, AL_REVERB_DECAY_HFRATIO, props.flDecayHFRatio); alEffectf(effect, AL_REVERB_REFLECTIONS_GAIN, props.flReflectionsGain); alEffectf(effect, AL_REVERB_REFLECTIONS_DELAY, props.flReflectionsDelay); alEffectf(effect, AL_REVERB_LATE_REVERB_GAIN, props.flLateReverbGain); alEffectf(effect, AL_REVERB_LATE_REVERB_DELAY, props.flLateReverbDelay); alEffectf(effect, AL_REVERB_AIR_ABSORPTION_GAINHF, props.flAirAbsorptionGainHF); alEffectf(effect, AL_REVERB_ROOM_ROLLOFF_FACTOR, props.flRoomRolloffFactor); alEffecti(effect, AL_REVERB_DECAY_HFLIMIT, props.iDecayHFLimit ? AL_TRUE : AL_FALSE); } getALError(); } } namespace MWSound { static ALenum getALFormat(ChannelConfig chans, SampleType type) { struct FormatEntry { ALenum format; ChannelConfig chans; SampleType type; }; struct FormatEntryExt { const char name[32]; ChannelConfig chans; SampleType type; }; static const std::array fmtlist{{ { AL_FORMAT_MONO16, ChannelConfig_Mono, SampleType_Int16 }, { AL_FORMAT_MONO8, ChannelConfig_Mono, SampleType_UInt8 }, { AL_FORMAT_STEREO16, ChannelConfig_Stereo, SampleType_Int16 }, { AL_FORMAT_STEREO8, ChannelConfig_Stereo, SampleType_UInt8 }, }}; for(auto &fmt : fmtlist) { if(fmt.chans == chans && fmt.type == type) return fmt.format; } if(alIsExtensionPresent("AL_EXT_MCFORMATS")) { static const std::array mcfmtlist{{ { "AL_FORMAT_QUAD16", ChannelConfig_Quad, SampleType_Int16 }, { "AL_FORMAT_QUAD8", ChannelConfig_Quad, SampleType_UInt8 }, { "AL_FORMAT_51CHN16", ChannelConfig_5point1, SampleType_Int16 }, { "AL_FORMAT_51CHN8", ChannelConfig_5point1, SampleType_UInt8 }, { "AL_FORMAT_71CHN16", ChannelConfig_7point1, SampleType_Int16 }, { "AL_FORMAT_71CHN8", ChannelConfig_7point1, SampleType_UInt8 }, }}; for(auto &fmt : mcfmtlist) { if(fmt.chans == chans && fmt.type == type) { ALenum format = alGetEnumValue(fmt.name); if(format != 0 && format != -1) return format; } } } if(alIsExtensionPresent("AL_EXT_FLOAT32")) { static const std::array fltfmtlist{{ { "AL_FORMAT_MONO_FLOAT32", ChannelConfig_Mono, SampleType_Float32 }, { "AL_FORMAT_STEREO_FLOAT32", ChannelConfig_Stereo, SampleType_Float32 }, }}; for(auto &fmt : fltfmtlist) { if(fmt.chans == chans && fmt.type == type) { ALenum format = alGetEnumValue(fmt.name); if(format != 0 && format != -1) return format; } } if(alIsExtensionPresent("AL_EXT_MCFORMATS")) { static const std::array fltmcfmtlist{{ { "AL_FORMAT_QUAD32", ChannelConfig_Quad, SampleType_Float32 }, { "AL_FORMAT_51CHN32", ChannelConfig_5point1, SampleType_Float32 }, { "AL_FORMAT_71CHN32", ChannelConfig_7point1, SampleType_Float32 }, }}; for(auto &fmt : fltmcfmtlist) { if(fmt.chans == chans && fmt.type == type) { ALenum format = alGetEnumValue(fmt.name); if(format != 0 && format != -1) return format; } } } } Log(Debug::Warning) << "Unsupported sound format (" << getChannelConfigName(chans) << ", " << getSampleTypeName(type) << ")"; return AL_NONE; } // // A streaming OpenAL sound. // class OpenAL_SoundStream { static const ALfloat sBufferLength; private: ALuint mSource; std::array mBuffers; ALint mCurrentBufIdx; ALenum mFormat; ALsizei mSampleRate; ALuint mBufferSize; ALuint mFrameSize; ALint mSilence; DecoderPtr mDecoder; std::unique_ptr mLoudnessAnalyzer; std::atomic mIsFinished; void updateAll(bool local); OpenAL_SoundStream(const OpenAL_SoundStream &rhs); OpenAL_SoundStream& operator=(const OpenAL_SoundStream &rhs); friend class OpenAL_Output; public: OpenAL_SoundStream(ALuint src, DecoderPtr decoder); ~OpenAL_SoundStream(); bool init(bool getLoudnessData=false); bool isPlaying(); double getStreamDelay() const; double getStreamOffset() const; float getCurrentLoudness() const; bool process(); ALint refillQueue(); }; const ALfloat OpenAL_SoundStream::sBufferLength = 0.125f; // // A background streaming thread (keeps active streams processed) // struct OpenAL_Output::StreamThread { typedef std::vector StreamVec; StreamVec mStreams; std::atomic mQuitNow; std::mutex mMutex; std::condition_variable mCondVar; std::thread mThread; StreamThread() : mQuitNow(false) , mThread([this] { run(); }) { } ~StreamThread() { mQuitNow = true; mMutex.lock(); mMutex.unlock(); mCondVar.notify_all(); mThread.join(); } // thread entry point void run() { std::unique_lock lock(mMutex); while(!mQuitNow) { StreamVec::iterator iter = mStreams.begin(); while(iter != mStreams.end()) { if((*iter)->process() == false) iter = mStreams.erase(iter); else ++iter; } mCondVar.wait_for(lock, std::chrono::milliseconds(50)); } } void add(OpenAL_SoundStream *stream) { std::lock_guard lock(mMutex); if(std::find(mStreams.begin(), mStreams.end(), stream) == mStreams.end()) { mStreams.push_back(stream); mCondVar.notify_all(); } } void remove(OpenAL_SoundStream *stream) { std::lock_guard lock(mMutex); StreamVec::iterator iter = std::find(mStreams.begin(), mStreams.end(), stream); if(iter != mStreams.end()) mStreams.erase(iter); } void removeAll() { std::lock_guard lock(mMutex); mStreams.clear(); } private: StreamThread(const StreamThread &rhs); StreamThread& operator=(const StreamThread &rhs); }; OpenAL_SoundStream::OpenAL_SoundStream(ALuint src, DecoderPtr decoder) : mSource(src), mCurrentBufIdx(0), mFormat(AL_NONE), mSampleRate(0) , mBufferSize(0), mFrameSize(0), mSilence(0), mDecoder(std::move(decoder)) , mLoudnessAnalyzer(nullptr), mIsFinished(true) { mBuffers.fill(0); } OpenAL_SoundStream::~OpenAL_SoundStream() { if(mBuffers[0] && alIsBuffer(mBuffers[0])) alDeleteBuffers(mBuffers.size(), mBuffers.data()); alGetError(); mDecoder->close(); } bool OpenAL_SoundStream::init(bool getLoudnessData) { alGenBuffers(mBuffers.size(), mBuffers.data()); ALenum err = getALError(); if(err != AL_NO_ERROR) return false; ChannelConfig chans; SampleType type; try { mDecoder->getInfo(&mSampleRate, &chans, &type); mFormat = getALFormat(chans, type); } catch(std::exception &e) { Log(Debug::Error) << "Failed to get stream info: " << e.what(); return false; } switch(type) { case SampleType_UInt8: mSilence = 0x80; break; case SampleType_Int16: mSilence = 0x00; break; case SampleType_Float32: mSilence = 0x00; break; } mFrameSize = framesToBytes(1, chans, type); mBufferSize = static_cast(sBufferLength*mSampleRate); mBufferSize *= mFrameSize; if (getLoudnessData) mLoudnessAnalyzer.reset(new Sound_Loudness(sLoudnessFPS, mSampleRate, chans, type)); mIsFinished = false; return true; } bool OpenAL_SoundStream::isPlaying() { ALint state; alGetSourcei(mSource, AL_SOURCE_STATE, &state); getALError(); if(state == AL_PLAYING || state == AL_PAUSED) return true; return !mIsFinished; } double OpenAL_SoundStream::getStreamDelay() const { ALint state = AL_STOPPED; double d = 0.0; ALint offset; alGetSourcei(mSource, AL_SAMPLE_OFFSET, &offset); alGetSourcei(mSource, AL_SOURCE_STATE, &state); if(state == AL_PLAYING || state == AL_PAUSED) { ALint queued; alGetSourcei(mSource, AL_BUFFERS_QUEUED, &queued); ALint inqueue = mBufferSize/mFrameSize*queued - offset; d = (double)inqueue / (double)mSampleRate; } getALError(); return d; } double OpenAL_SoundStream::getStreamOffset() const { ALint state = AL_STOPPED; ALint offset; double t; alGetSourcei(mSource, AL_SAMPLE_OFFSET, &offset); alGetSourcei(mSource, AL_SOURCE_STATE, &state); if(state == AL_PLAYING || state == AL_PAUSED) { ALint queued; alGetSourcei(mSource, AL_BUFFERS_QUEUED, &queued); ALint inqueue = mBufferSize/mFrameSize*queued - offset; t = (double)(mDecoder->getSampleOffset() - inqueue) / (double)mSampleRate; } else { /* Underrun, or not started yet. The decoder offset is where we'll play * next. */ t = (double)mDecoder->getSampleOffset() / (double)mSampleRate; } getALError(); return t; } float OpenAL_SoundStream::getCurrentLoudness() const { if (!mLoudnessAnalyzer.get()) return 0.f; float time = getStreamOffset(); return mLoudnessAnalyzer->getLoudnessAtTime(time); } bool OpenAL_SoundStream::process() { try { if(refillQueue() > 0) { ALint state; alGetSourcei(mSource, AL_SOURCE_STATE, &state); if(state != AL_PLAYING && state != AL_PAUSED) { // Ensure all processed buffers are removed so we don't replay them. refillQueue(); alSourcePlay(mSource); } } } catch(std::exception&) { Log(Debug::Error) << "Error updating stream \"" << mDecoder->getName() << "\""; mIsFinished = true; } return !mIsFinished; } ALint OpenAL_SoundStream::refillQueue() { ALint processed; alGetSourcei(mSource, AL_BUFFERS_PROCESSED, &processed); while(processed > 0) { ALuint buf; alSourceUnqueueBuffers(mSource, 1, &buf); --processed; } ALint queued; alGetSourcei(mSource, AL_BUFFERS_QUEUED, &queued); if(!mIsFinished && (ALuint)queued < mBuffers.size()) { std::vector data(mBufferSize); for(;!mIsFinished && (ALuint)queued < mBuffers.size();++queued) { size_t got = mDecoder->read(data.data(), data.size()); if(got < data.size()) { mIsFinished = true; std::fill(data.begin()+got, data.end(), mSilence); } if(got > 0) { if (mLoudnessAnalyzer.get()) mLoudnessAnalyzer->analyzeLoudness(data); ALuint bufid = mBuffers[mCurrentBufIdx]; alBufferData(bufid, mFormat, data.data(), data.size(), mSampleRate); alSourceQueueBuffers(mSource, 1, &bufid); mCurrentBufIdx = (mCurrentBufIdx+1) % mBuffers.size(); } } } return queued; } // // An OpenAL output device // std::vector OpenAL_Output::enumerate() { std::vector devlist; const ALCchar *devnames; if(alcIsExtensionPresent(nullptr, "ALC_ENUMERATE_ALL_EXT")) devnames = alcGetString(nullptr, ALC_ALL_DEVICES_SPECIFIER); else devnames = alcGetString(nullptr, ALC_DEVICE_SPECIFIER); while(devnames && *devnames) { devlist.emplace_back(devnames); devnames += strlen(devnames)+1; } return devlist; } bool OpenAL_Output::init(const std::string &devname, const std::string &hrtfname, HrtfMode hrtfmode) { deinit(); Log(Debug::Info) << "Initializing OpenAL..."; mDevice = alcOpenDevice(devname.c_str()); if(!mDevice && !devname.empty()) { Log(Debug::Warning) << "Failed to open \"" << devname << "\", trying default"; mDevice = alcOpenDevice(nullptr); } if(!mDevice) { Log(Debug::Error) << "Failed to open default audio device"; return false; } const ALCchar *name = nullptr; if(alcIsExtensionPresent(mDevice, "ALC_ENUMERATE_ALL_EXT")) name = alcGetString(mDevice, ALC_ALL_DEVICES_SPECIFIER); if(alcGetError(mDevice) != AL_NO_ERROR || !name) name = alcGetString(mDevice, ALC_DEVICE_SPECIFIER); Log(Debug::Info) << "Opened \"" << name << "\""; ALCint major=0, minor=0; alcGetIntegerv(mDevice, ALC_MAJOR_VERSION, 1, &major); alcGetIntegerv(mDevice, ALC_MINOR_VERSION, 1, &minor); Log(Debug::Info) << " ALC Version: " << major << "." << minor <<"\n" << " ALC Extensions: " << alcGetString(mDevice, ALC_EXTENSIONS); ALC.EXT_EFX = alcIsExtensionPresent(mDevice, "ALC_EXT_EFX"); ALC.SOFT_HRTF = alcIsExtensionPresent(mDevice, "ALC_SOFT_HRTF"); std::vector attrs; attrs.reserve(15); if(ALC.SOFT_HRTF) { LPALCGETSTRINGISOFT alcGetStringiSOFT = nullptr; getALCFunc(alcGetStringiSOFT, mDevice, "alcGetStringiSOFT"); attrs.push_back(ALC_HRTF_SOFT); attrs.push_back(hrtfmode == HrtfMode::Disable ? ALC_FALSE : hrtfmode == HrtfMode::Enable ? ALC_TRUE : /*hrtfmode == HrtfMode::Auto ?*/ ALC_DONT_CARE_SOFT); if(!hrtfname.empty()) { ALCint index = -1; ALCint num_hrtf; alcGetIntegerv(mDevice, ALC_NUM_HRTF_SPECIFIERS_SOFT, 1, &num_hrtf); for(ALCint i = 0;i < num_hrtf;++i) { const ALCchar *entry = alcGetStringiSOFT(mDevice, ALC_HRTF_SPECIFIER_SOFT, i); if(hrtfname == entry) { index = i; break; } } if(index < 0) Log(Debug::Warning) << "Failed to find HRTF \"" << hrtfname << "\", using default"; else { attrs.push_back(ALC_HRTF_ID_SOFT); attrs.push_back(index); } } } attrs.push_back(0); mContext = alcCreateContext(mDevice, attrs.data()); if(!mContext || alcMakeContextCurrent(mContext) == ALC_FALSE) { Log(Debug::Error) << "Failed to setup audio context: "<(maxmono+maxstereo, 256); if (maxtotal == 0) // workaround for broken implementations maxtotal = 256; } for(size_t i = 0;i < maxtotal;i++) { ALuint src = 0; alGenSources(1, &src); if(alGetError() != AL_NO_ERROR) break; mFreeSources.push_back(src); } if(mFreeSources.empty()) { Log(Debug::Warning) << "Could not allocate any sound sourcess"; alcMakeContextCurrent(nullptr); alcDestroyContext(mContext); mContext = nullptr; alcCloseDevice(mDevice); mDevice = nullptr; return false; } Log(Debug::Info) << "Allocated " << mFreeSources.size() << " sound sources"; if(ALC.EXT_EFX) { #define LOAD_FUNC(x) getALFunc(x, #x) LOAD_FUNC(alGenEffects); LOAD_FUNC(alDeleteEffects); LOAD_FUNC(alIsEffect); LOAD_FUNC(alEffecti); LOAD_FUNC(alEffectiv); LOAD_FUNC(alEffectf); LOAD_FUNC(alEffectfv); LOAD_FUNC(alGetEffecti); LOAD_FUNC(alGetEffectiv); LOAD_FUNC(alGetEffectf); LOAD_FUNC(alGetEffectfv); LOAD_FUNC(alGenFilters); LOAD_FUNC(alDeleteFilters); LOAD_FUNC(alIsFilter); LOAD_FUNC(alFilteri); LOAD_FUNC(alFilteriv); LOAD_FUNC(alFilterf); LOAD_FUNC(alFilterfv); LOAD_FUNC(alGetFilteri); LOAD_FUNC(alGetFilteriv); LOAD_FUNC(alGetFilterf); LOAD_FUNC(alGetFilterfv); LOAD_FUNC(alGenAuxiliaryEffectSlots); LOAD_FUNC(alDeleteAuxiliaryEffectSlots); LOAD_FUNC(alIsAuxiliaryEffectSlot); LOAD_FUNC(alAuxiliaryEffectSloti); LOAD_FUNC(alAuxiliaryEffectSlotiv); LOAD_FUNC(alAuxiliaryEffectSlotf); LOAD_FUNC(alAuxiliaryEffectSlotfv); LOAD_FUNC(alGetAuxiliaryEffectSloti); LOAD_FUNC(alGetAuxiliaryEffectSlotiv); LOAD_FUNC(alGetAuxiliaryEffectSlotf); LOAD_FUNC(alGetAuxiliaryEffectSlotfv); #undef LOAD_FUNC if(getALError() != AL_NO_ERROR) { ALC.EXT_EFX = false; goto skip_efx; } alGenFilters(1, &mWaterFilter); if(alGetError() == AL_NO_ERROR) { alFilteri(mWaterFilter, AL_FILTER_TYPE, AL_FILTER_LOWPASS); if(alGetError() == AL_NO_ERROR) { Log(Debug::Info) << "Low-pass filter supported"; alFilterf(mWaterFilter, AL_LOWPASS_GAIN, 0.9f); alFilterf(mWaterFilter, AL_LOWPASS_GAINHF, 0.125f); } else { alDeleteFilters(1, &mWaterFilter); mWaterFilter = 0; } alGetError(); } alGenAuxiliaryEffectSlots(1, &mEffectSlot); alGetError(); alGenEffects(1, &mDefaultEffect); if(alGetError() == AL_NO_ERROR) { alEffecti(mDefaultEffect, AL_EFFECT_TYPE, AL_EFFECT_EAXREVERB); if(alGetError() == AL_NO_ERROR) Log(Debug::Info) << "EAX Reverb supported"; else { alEffecti(mDefaultEffect, AL_EFFECT_TYPE, AL_EFFECT_REVERB); if(alGetError() == AL_NO_ERROR) Log(Debug::Info) << "Standard Reverb supported"; } EFXEAXREVERBPROPERTIES props = EFX_REVERB_PRESET_LIVINGROOM; props.flGain = 0.0f; LoadEffect(mDefaultEffect, props); } alGenEffects(1, &mWaterEffect); if(alGetError() == AL_NO_ERROR) { alEffecti(mWaterEffect, AL_EFFECT_TYPE, AL_EFFECT_EAXREVERB); if(alGetError() != AL_NO_ERROR) { alEffecti(mWaterEffect, AL_EFFECT_TYPE, AL_EFFECT_REVERB); alGetError(); } LoadEffect(mWaterEffect, EFX_REVERB_PRESET_UNDERWATER); } alListenerf(AL_METERS_PER_UNIT, 1.0f / Constants::UnitsPerMeter); } skip_efx: alDistanceModel(AL_INVERSE_DISTANCE_CLAMPED); // Speed of sound is in units per second. Take the sound speed in air (assumed // meters per second), multiply by the units per meter to get the speed in u/s. alSpeedOfSound(Constants::SoundSpeedInAir * Constants::UnitsPerMeter); alGetError(); mInitialized = true; return true; } void OpenAL_Output::deinit() { mStreamThread->removeAll(); for(ALuint source : mFreeSources) alDeleteSources(1, &source); mFreeSources.clear(); if(mEffectSlot) alDeleteAuxiliaryEffectSlots(1, &mEffectSlot); mEffectSlot = 0; if(mDefaultEffect) alDeleteEffects(1, &mDefaultEffect); mDefaultEffect = 0; if(mWaterEffect) alDeleteEffects(1, &mWaterEffect); mWaterEffect = 0; if(mWaterFilter) alDeleteFilters(1, &mWaterFilter); mWaterFilter = 0; alcMakeContextCurrent(nullptr); if(mContext) alcDestroyContext(mContext); mContext = nullptr; if(mDevice) alcCloseDevice(mDevice); mDevice = nullptr; mInitialized = false; } std::vector OpenAL_Output::enumerateHrtf() { std::vector ret; if(!mDevice || !ALC.SOFT_HRTF) return ret; LPALCGETSTRINGISOFT alcGetStringiSOFT = nullptr; getALCFunc(alcGetStringiSOFT, mDevice, "alcGetStringiSOFT"); ALCint num_hrtf; alcGetIntegerv(mDevice, ALC_NUM_HRTF_SPECIFIERS_SOFT, 1, &num_hrtf); ret.reserve(num_hrtf); for(ALCint i = 0;i < num_hrtf;++i) { const ALCchar *entry = alcGetStringiSOFT(mDevice, ALC_HRTF_SPECIFIER_SOFT, i); ret.emplace_back(entry); } return ret; } void OpenAL_Output::setHrtf(const std::string &hrtfname, HrtfMode hrtfmode) { if(!mDevice || !ALC.SOFT_HRTF) { Log(Debug::Info) << "HRTF extension not present"; return; } LPALCGETSTRINGISOFT alcGetStringiSOFT = nullptr; getALCFunc(alcGetStringiSOFT, mDevice, "alcGetStringiSOFT"); LPALCRESETDEVICESOFT alcResetDeviceSOFT = nullptr; getALCFunc(alcResetDeviceSOFT, mDevice, "alcResetDeviceSOFT"); std::vector attrs; attrs.reserve(15); attrs.push_back(ALC_HRTF_SOFT); attrs.push_back(hrtfmode == HrtfMode::Disable ? ALC_FALSE : hrtfmode == HrtfMode::Enable ? ALC_TRUE : /*hrtfmode == HrtfMode::Auto ?*/ ALC_DONT_CARE_SOFT); if(!hrtfname.empty()) { ALCint index = -1; ALCint num_hrtf; alcGetIntegerv(mDevice, ALC_NUM_HRTF_SPECIFIERS_SOFT, 1, &num_hrtf); for(ALCint i = 0;i < num_hrtf;++i) { const ALCchar *entry = alcGetStringiSOFT(mDevice, ALC_HRTF_SPECIFIER_SOFT, i); if(hrtfname == entry) { index = i; break; } } if(index < 0) Log(Debug::Warning) << "Failed to find HRTF name \"" << hrtfname << "\", using default"; else { attrs.push_back(ALC_HRTF_ID_SOFT); attrs.push_back(index); } } attrs.push_back(0); alcResetDeviceSOFT(mDevice, attrs.data()); ALCint hrtf_state; alcGetIntegerv(mDevice, ALC_HRTF_SOFT, 1, &hrtf_state); if(!hrtf_state) Log(Debug::Info) << "HRTF disabled"; else { const ALCchar *hrtf = alcGetString(mDevice, ALC_HRTF_SPECIFIER_SOFT); Log(Debug::Info) << "Enabled HRTF " << hrtf; } } std::pair OpenAL_Output::loadSound(const std::string &fname) { getALError(); std::vector data; ALenum format = AL_NONE; int srate = 0; 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); } ChannelConfig chans; SampleType type; decoder->getInfo(&srate, &chans, &type); format = getALFormat(chans, type); if(format) decoder->readAll(data); } catch(std::exception &e) { Log(Debug::Error) << "Failed to load audio from " << fname << ": " << e.what(); } if(data.empty()) { // If we failed to get any usable audio, substitute with silence. format = AL_FORMAT_MONO8; srate = 8000; data.assign(8000, -128); } ALint size; ALuint buf = 0; alGenBuffers(1, &buf); alBufferData(buf, format, data.data(), data.size(), srate); alGetBufferi(buf, AL_SIZE, &size); if(getALError() != AL_NO_ERROR) { if(buf && alIsBuffer(buf)) alDeleteBuffers(1, &buf); getALError(); return std::make_pair(nullptr, 0); } return std::make_pair(MAKE_PTRID(buf), size); } size_t OpenAL_Output::unloadSound(Sound_Handle data) { ALuint buffer = GET_PTRID(data); if(!buffer) return 0; // Make sure no sources are playing this buffer before unloading it. SoundVec::const_iterator iter = mActiveSounds.begin(); for(;iter != mActiveSounds.end();++iter) { if(!(*iter)->mHandle) continue; ALuint source = GET_PTRID((*iter)->mHandle); ALint srcbuf; alGetSourcei(source, AL_BUFFER, &srcbuf); if((ALuint)srcbuf == buffer) { alSourceStop(source); alSourcei(source, AL_BUFFER, 0); } } ALint size = 0; alGetBufferi(buffer, AL_SIZE, &size); alDeleteBuffers(1, &buffer); getALError(); return size; } void OpenAL_Output::initCommon2D(ALuint source, const osg::Vec3f &pos, ALfloat gain, ALfloat pitch, bool loop, bool useenv) { alSourcef(source, AL_REFERENCE_DISTANCE, 1.0f); alSourcef(source, AL_MAX_DISTANCE, 1000.0f); alSourcef(source, AL_ROLLOFF_FACTOR, 0.0f); alSourcei(source, AL_SOURCE_RELATIVE, AL_TRUE); alSourcei(source, AL_LOOPING, loop ? AL_TRUE : AL_FALSE); if(AL.SOFT_source_spatialize) alSourcei(source, AL_SOURCE_SPATIALIZE_SOFT, AL_FALSE); if(useenv) { if(mWaterFilter) alSourcei(source, AL_DIRECT_FILTER, (mListenerEnv == Env_Underwater) ? mWaterFilter : AL_FILTER_NULL ); else if(mListenerEnv == Env_Underwater) { gain *= 0.9f; pitch *= 0.7f; } if(mEffectSlot) alSource3i(source, AL_AUXILIARY_SEND_FILTER, mEffectSlot, 0, AL_FILTER_NULL); } else { if(mWaterFilter) alSourcei(source, AL_DIRECT_FILTER, AL_FILTER_NULL); if(mEffectSlot) alSource3i(source, AL_AUXILIARY_SEND_FILTER, AL_EFFECTSLOT_NULL, 0, AL_FILTER_NULL); } alSourcef(source, AL_GAIN, gain); alSourcef(source, AL_PITCH, pitch); alSourcefv(source, AL_POSITION, pos.ptr()); alSource3f(source, AL_DIRECTION, 0.0f, 0.0f, 0.0f); alSource3f(source, AL_VELOCITY, 0.0f, 0.0f, 0.0f); } void OpenAL_Output::initCommon3D(ALuint source, const osg::Vec3f &pos, ALfloat mindist, ALfloat maxdist, ALfloat gain, ALfloat pitch, bool loop, bool useenv) { alSourcef(source, AL_REFERENCE_DISTANCE, mindist); alSourcef(source, AL_MAX_DISTANCE, maxdist); alSourcef(source, AL_ROLLOFF_FACTOR, 1.0f); alSourcei(source, AL_SOURCE_RELATIVE, AL_FALSE); alSourcei(source, AL_LOOPING, loop ? AL_TRUE : AL_FALSE); if(AL.SOFT_source_spatialize) alSourcei(source, AL_SOURCE_SPATIALIZE_SOFT, AL_TRUE); if((pos - mListenerPos).length2() > maxdist*maxdist) gain = 0.0f; if(useenv) { if(mWaterFilter) alSourcei(source, AL_DIRECT_FILTER, (mListenerEnv == Env_Underwater) ? mWaterFilter : AL_FILTER_NULL ); else if(mListenerEnv == Env_Underwater) { gain *= 0.9f; pitch *= 0.7f; } if(mEffectSlot) alSource3i(source, AL_AUXILIARY_SEND_FILTER, mEffectSlot, 0, AL_FILTER_NULL); } else { if(mWaterFilter) alSourcei(source, AL_DIRECT_FILTER, AL_FILTER_NULL); if(mEffectSlot) alSource3i(source, AL_AUXILIARY_SEND_FILTER, AL_EFFECTSLOT_NULL, 0, AL_FILTER_NULL); } alSourcef(source, AL_GAIN, gain); alSourcef(source, AL_PITCH, pitch); alSourcefv(source, AL_POSITION, pos.ptr()); alSource3f(source, AL_DIRECTION, 0.0f, 0.0f, 0.0f); 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) { if(is3d) { if((pos - mListenerPos).length2() > maxdist*maxdist) gain = 0.0f; } if(useenv && mListenerEnv == Env_Underwater && !mWaterFilter) { gain *= 0.9f; pitch *= 0.7f; } alSourcef(source, AL_GAIN, gain); alSourcef(source, AL_PITCH, pitch); alSourcefv(source, AL_POSITION, pos.ptr()); alSource3f(source, AL_DIRECTION, 0.0f, 0.0f, 0.0f); alSource3f(source, AL_VELOCITY, 0.0f, 0.0f, 0.0f); } bool OpenAL_Output::playSound(Sound *sound, Sound_Handle data, float offset) { ALuint source; if(mFreeSources.empty()) { Log(Debug::Warning) << "No free sources!"; return false; } source = mFreeSources.front(); initCommon2D(source, sound->getPosition(), sound->getRealVolume(), sound->getPitch(), sound->getIsLooping(), sound->getUseEnv()); alSourcei(source, AL_BUFFER, GET_PTRID(data)); alSourcef(source, AL_SEC_OFFSET, offset); if(getALError() != AL_NO_ERROR) { alSourceRewind(source); alSourcei(source, AL_BUFFER, 0); alGetError(); return false; } alSourcePlay(source); if(getALError() != AL_NO_ERROR) { alSourceRewind(source); alSourcei(source, AL_BUFFER, 0); alGetError(); return false; } mFreeSources.pop_front(); sound->mHandle = MAKE_PTRID(source); mActiveSounds.push_back(sound); return true; } bool OpenAL_Output::playSound3D(Sound *sound, Sound_Handle data, float offset) { ALuint source; if(mFreeSources.empty()) { Log(Debug::Warning) << "No free sources!"; return false; } source = mFreeSources.front(); initCommon3D(source, sound->getPosition(), sound->getMinDistance(), sound->getMaxDistance(), sound->getRealVolume(), sound->getPitch(), sound->getIsLooping(), sound->getUseEnv()); alSourcei(source, AL_BUFFER, GET_PTRID(data)); alSourcef(source, AL_SEC_OFFSET, offset); if(getALError() != AL_NO_ERROR) { alSourceRewind(source); alSourcei(source, AL_BUFFER, 0); alGetError(); return false; } alSourcePlay(source); if(getALError() != AL_NO_ERROR) { alSourceRewind(source); alSourcei(source, AL_BUFFER, 0); alGetError(); return false; } mFreeSources.pop_front(); sound->mHandle = MAKE_PTRID(source); mActiveSounds.push_back(sound); return true; } void OpenAL_Output::finishSound(Sound *sound) { if(!sound->mHandle) return; ALuint source = GET_PTRID(sound->mHandle); sound->mHandle = nullptr; // Rewind the stream to put the source back into an AL_INITIAL state, for // the next time it's used. alSourceRewind(source); alSourcei(source, AL_BUFFER, 0); getALError(); mFreeSources.push_back(source); mActiveSounds.erase(std::find(mActiveSounds.begin(), mActiveSounds.end(), sound)); } bool OpenAL_Output::isSoundPlaying(Sound *sound) { if(!sound->mHandle) return false; ALuint source = GET_PTRID(sound->mHandle); ALint state = AL_STOPPED; alGetSourcei(source, AL_SOURCE_STATE, &state); getALError(); return state == AL_PLAYING || state == AL_PAUSED; } void OpenAL_Output::updateSound(Sound *sound) { if(!sound->mHandle) return; ALuint source = GET_PTRID(sound->mHandle); updateCommon(source, sound->getPosition(), sound->getMaxDistance(), sound->getRealVolume(), sound->getPitch(), sound->getUseEnv(), sound->getIs3D()); getALError(); } bool OpenAL_Output::streamSound(DecoderPtr decoder, Stream *sound, bool getLoudnessData) { if(mFreeSources.empty()) { Log(Debug::Warning) << "No free sources!"; return false; } ALuint source = mFreeSources.front(); if(sound->getIsLooping()) Log(Debug::Warning) << "Warning: cannot loop stream \"" << decoder->getName() << "\""; initCommon2D(source, sound->getPosition(), sound->getRealVolume(), sound->getPitch(), false, sound->getUseEnv()); if(getALError() != AL_NO_ERROR) return false; OpenAL_SoundStream *stream = new OpenAL_SoundStream(source, std::move(decoder)); if(!stream->init(getLoudnessData)) { delete stream; return false; } mStreamThread->add(stream); mFreeSources.pop_front(); sound->mHandle = stream; mActiveStreams.push_back(sound); return true; } bool OpenAL_Output::streamSound3D(DecoderPtr decoder, Stream *sound, bool getLoudnessData) { if(mFreeSources.empty()) { Log(Debug::Warning) << "No free sources!"; return false; } ALuint source = mFreeSources.front(); if(sound->getIsLooping()) Log(Debug::Warning) << "Warning: cannot loop stream \"" << decoder->getName() << "\""; initCommon3D(source, sound->getPosition(), sound->getMinDistance(), sound->getMaxDistance(), sound->getRealVolume(), sound->getPitch(), false, sound->getUseEnv()); if(getALError() != AL_NO_ERROR) return false; OpenAL_SoundStream *stream = new OpenAL_SoundStream(source, std::move(decoder)); if(!stream->init(getLoudnessData)) { delete stream; return false; } mStreamThread->add(stream); mFreeSources.pop_front(); sound->mHandle = stream; mActiveStreams.push_back(sound); return true; } void OpenAL_Output::finishStream(Stream *sound) { if(!sound->mHandle) return; OpenAL_SoundStream *stream = reinterpret_cast(sound->mHandle); ALuint source = stream->mSource; sound->mHandle = nullptr; mStreamThread->remove(stream); // Rewind the stream to put the source back into an AL_INITIAL state, for // the next time it's used. alSourceRewind(source); alSourcei(source, AL_BUFFER, 0); getALError(); mFreeSources.push_back(source); mActiveStreams.erase(std::find(mActiveStreams.begin(), mActiveStreams.end(), sound)); delete stream; } double OpenAL_Output::getStreamDelay(Stream *sound) { if(!sound->mHandle) return 0.0; OpenAL_SoundStream *stream = reinterpret_cast(sound->mHandle); return stream->getStreamDelay(); } double OpenAL_Output::getStreamOffset(Stream *sound) { if(!sound->mHandle) return 0.0; OpenAL_SoundStream *stream = reinterpret_cast(sound->mHandle); std::lock_guard lock(mStreamThread->mMutex); return stream->getStreamOffset(); } float OpenAL_Output::getStreamLoudness(Stream *sound) { if(!sound->mHandle) return 0.0; OpenAL_SoundStream *stream = reinterpret_cast(sound->mHandle); std::lock_guard lock(mStreamThread->mMutex); return stream->getCurrentLoudness(); } bool OpenAL_Output::isStreamPlaying(Stream *sound) { if(!sound->mHandle) return false; OpenAL_SoundStream *stream = reinterpret_cast(sound->mHandle); std::lock_guard lock(mStreamThread->mMutex); return stream->isPlaying(); } void OpenAL_Output::updateStream(Stream *sound) { if(!sound->mHandle) return; OpenAL_SoundStream *stream = reinterpret_cast(sound->mHandle); ALuint source = stream->mSource; updateCommon(source, sound->getPosition(), sound->getMaxDistance(), sound->getRealVolume(), sound->getPitch(), sound->getUseEnv(), sound->getIs3D()); getALError(); } void OpenAL_Output::startUpdate() { alcSuspendContext(alcGetCurrentContext()); } void OpenAL_Output::finishUpdate() { alcProcessContext(alcGetCurrentContext()); } void OpenAL_Output::updateListener(const osg::Vec3f &pos, const osg::Vec3f &atdir, const osg::Vec3f &updir, Environment env) { if(mContext) { ALfloat orient[6] = { atdir.x(), atdir.y(), atdir.z(), updir.x(), updir.y(), updir.z() }; alListenerfv(AL_POSITION, pos.ptr()); alListenerfv(AL_ORIENTATION, orient); if(env != mListenerEnv) { alSpeedOfSound(((env == Env_Underwater) ? Constants::SoundSpeedUnderwater : Constants::SoundSpeedInAir) * Constants::UnitsPerMeter); // Update active sources with the environment's direct filter if(mWaterFilter) { ALuint filter = (env == Env_Underwater) ? mWaterFilter : AL_FILTER_NULL; for(Sound *sound : mActiveSounds) { if(sound->getUseEnv()) alSourcei(GET_PTRID(sound->mHandle), AL_DIRECT_FILTER, filter); } for(Stream *sound : mActiveStreams) { if(sound->getUseEnv()) alSourcei( reinterpret_cast(sound->mHandle)->mSource, AL_DIRECT_FILTER, filter ); } } // Update the environment effect if(mEffectSlot) alAuxiliaryEffectSloti(mEffectSlot, AL_EFFECTSLOT_EFFECT, (env == Env_Underwater) ? mWaterEffect : mDefaultEffect ); } getALError(); } mListenerPos = pos; mListenerEnv = env; } void OpenAL_Output::pauseSounds(int types) { std::vector sources; for(Sound *sound : mActiveSounds) { if((types&sound->getPlayType())) sources.push_back(GET_PTRID(sound->mHandle)); } for(Stream *sound : mActiveStreams) { if((types&sound->getPlayType())) { OpenAL_SoundStream *stream = reinterpret_cast(sound->mHandle); sources.push_back(stream->mSource); } } if(!sources.empty()) { alSourcePausev(sources.size(), sources.data()); getALError(); } } void OpenAL_Output::pauseActiveDevice() { if (mDevice == nullptr) return; if(alcIsExtensionPresent(mDevice, "ALC_SOFT_PAUSE_DEVICE")) { LPALCDEVICEPAUSESOFT alcDevicePauseSOFT = nullptr; getALCFunc(alcDevicePauseSOFT, mDevice, "alcDevicePauseSOFT"); alcDevicePauseSOFT(mDevice); getALCError(mDevice); } alListenerf(AL_GAIN, 0.0f); } void OpenAL_Output::resumeActiveDevice() { if (mDevice == nullptr) return; if(alcIsExtensionPresent(mDevice, "ALC_SOFT_PAUSE_DEVICE")) { LPALCDEVICERESUMESOFT alcDeviceResumeSOFT = nullptr; getALCFunc(alcDeviceResumeSOFT, mDevice, "alcDeviceResumeSOFT"); alcDeviceResumeSOFT(mDevice); getALCError(mDevice); } alListenerf(AL_GAIN, 1.0f); } void OpenAL_Output::resumeSounds(int types) { std::vector sources; for(Sound *sound : mActiveSounds) { if((types&sound->getPlayType())) sources.push_back(GET_PTRID(sound->mHandle)); } for(Stream *sound : mActiveStreams) { if((types&sound->getPlayType())) { OpenAL_SoundStream *stream = reinterpret_cast(sound->mHandle); sources.push_back(stream->mSource); } } if(!sources.empty()) { alSourcePlayv(sources.size(), sources.data()); getALError(); } } OpenAL_Output::OpenAL_Output(SoundManager &mgr) : Sound_Output(mgr) , mDevice(nullptr), mContext(nullptr) , mListenerPos(0.0f, 0.0f, 0.0f), mListenerEnv(Env_Normal) , mWaterFilter(0), mWaterEffect(0), mDefaultEffect(0), mEffectSlot(0) , mStreamThread(new StreamThread) { } OpenAL_Output::~OpenAL_Output() { OpenAL_Output::deinit(); } } ================================================ FILE: apps/openmw/mwsound/openal_output.hpp ================================================ #ifndef GAME_SOUND_OPENAL_OUTPUT_H #define GAME_SOUND_OPENAL_OUTPUT_H #include #include #include #include #include "alc.h" #include "al.h" #include "alext.h" #include "sound_output.hpp" namespace MWSound { class SoundManager; class Sound; class Stream; class OpenAL_Output : public Sound_Output { ALCdevice *mDevice; ALCcontext *mContext; struct { bool EXT_EFX : 1; bool SOFT_HRTF : 1; } ALC = {false, false}; struct { bool SOFT_source_spatialize : 1; } AL = {false}; typedef std::deque IDDq; IDDq mFreeSources; typedef std::vector SoundVec; SoundVec mActiveSounds; typedef std::vector StreamVec; StreamVec mActiveStreams; osg::Vec3f mListenerPos; Environment mListenerEnv; ALuint mWaterFilter; ALuint mWaterEffect; ALuint mDefaultEffect; ALuint mEffectSlot; struct StreamThread; std::unique_ptr mStreamThread; 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); OpenAL_Output& operator=(const OpenAL_Output &rhs); OpenAL_Output(const OpenAL_Output &rhs); public: std::vector enumerate() override; bool init(const std::string &devname, const std::string &hrtfname, HrtfMode hrtfmode) override; void deinit() override; std::vector enumerateHrtf() override; void setHrtf(const std::string &hrtfname, HrtfMode hrtfmode) override; std::pair loadSound(const std::string &fname) override; size_t unloadSound(Sound_Handle data) override; bool playSound(Sound *sound, Sound_Handle data, float offset) override; bool playSound3D(Sound *sound, Sound_Handle data, float offset) override; void finishSound(Sound *sound) override; bool isSoundPlaying(Sound *sound) override; void updateSound(Sound *sound) override; bool streamSound(DecoderPtr decoder, Stream *sound, bool getLoudnessData=false) override; bool streamSound3D(DecoderPtr decoder, Stream *sound, bool getLoudnessData) override; void finishStream(Stream *sound) override; double getStreamDelay(Stream *sound) override; double getStreamOffset(Stream *sound) override; float getStreamLoudness(Stream *sound) override; bool isStreamPlaying(Stream *sound) override; void updateStream(Stream *sound) override; void startUpdate() override; void finishUpdate() override; void updateListener(const osg::Vec3f &pos, const osg::Vec3f &atdir, const osg::Vec3f &updir, Environment env) override; void pauseSounds(int types) override; void resumeSounds(int types) override; void pauseActiveDevice() override; void resumeActiveDevice() override; OpenAL_Output(SoundManager &mgr); virtual ~OpenAL_Output(); }; } #endif ================================================ FILE: apps/openmw/mwsound/regionsoundselector.cpp ================================================ #include "regionsoundselector.hpp" #include #include #include #include #include "../mwbase/world.hpp" #include "../mwworld/esmstore.hpp" namespace MWSound { namespace { int addChance(int result, const ESM::Region::SoundRef &v) { return result + v.mChance; } } RegionSoundSelector::RegionSoundSelector() : mMinTimeBetweenSounds(Fallback::Map::getFloat("Weather_Minimum_Time_Between_Environmental_Sounds")) , mMaxTimeBetweenSounds(Fallback::Map::getFloat("Weather_Maximum_Time_Between_Environmental_Sounds")) {} std::optional RegionSoundSelector::getNextRandom(float duration, const std::string& regionName, const MWBase::World& world) { mTimePassed += duration; if (mTimePassed < mTimeToNextEnvSound) return {}; const float a = Misc::Rng::rollClosedProbability(); mTimeToNextEnvSound = mMinTimeBetweenSounds + (mMaxTimeBetweenSounds - mMinTimeBetweenSounds) * a; mTimePassed = 0; if (mLastRegionName != regionName) { mLastRegionName = regionName; mSumChance = 0; } const ESM::Region* const region = world.getStore().get().search(mLastRegionName); if (region == nullptr) return {}; if (mSumChance == 0) { mSumChance = std::accumulate(region->mSoundList.begin(), region->mSoundList.end(), 0, addChance); if (mSumChance == 0) return {}; } const int r = Misc::Rng::rollDice(std::max(mSumChance, 100)); int pos = 0; const auto isSelected = [&] (const ESM::Region::SoundRef& sound) { if (r - pos < sound.mChance) return true; pos += sound.mChance; return false; }; const auto it = std::find_if(region->mSoundList.begin(), region->mSoundList.end(), isSelected); if (it == region->mSoundList.end()) return {}; return it->mSound; } } ================================================ FILE: apps/openmw/mwsound/regionsoundselector.hpp ================================================ #ifndef GAME_SOUND_REGIONSOUNDSELECTOR_H #define GAME_SOUND_REGIONSOUNDSELECTOR_H #include #include namespace MWBase { class World; } namespace MWSound { class RegionSoundSelector { public: std::optional getNextRandom(float duration, const std::string& regionName, const MWBase::World& world); RegionSoundSelector(); private: float mTimeToNextEnvSound = 0.0f; int mSumChance = 0; std::string mLastRegionName; float mTimePassed = 0.0; float mMinTimeBetweenSounds; float mMaxTimeBetweenSounds; }; } #endif ================================================ FILE: apps/openmw/mwsound/sound.hpp ================================================ #ifndef GAME_SOUND_SOUND_H #define GAME_SOUND_SOUND_H #include #include "sound_output.hpp" namespace MWSound { // Extra play flags, not intended for caller use enum PlayModeEx { Play_2D = 0, Play_3D = 1 << 31, }; // For testing individual PlayMode flags inline int operator&(int a, PlayMode b) { return a & static_cast(b); } inline int operator&(PlayMode a, PlayMode b) { return static_cast(a) & static_cast(b); } struct SoundParams { osg::Vec3f mPos; float mVolume = 1; float mBaseVolume = 1; float mPitch = 1; float mMinDistance = 1; float mMaxDistance = 1000; int mFlags = 0; float mFadeOutTime = 0; }; class SoundBase { SoundBase& operator=(const SoundBase&) = delete; SoundBase(const SoundBase&) = delete; SoundBase(SoundBase&&) = delete; SoundParams mParams; protected: Sound_Instance mHandle = nullptr; friend class OpenAL_Output; public: 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) { if (mParams.mFadeOutTime > 0.0f) { float soundDuration = std::min(duration, mParams.mFadeOutTime); mParams.mVolume *= (mParams.mFadeOutTime - soundDuration) / mParams.mFadeOutTime; mParams.mFadeOutTime -= soundDuration; } } const osg::Vec3f &getPosition() const { return mParams.mPos; } float getRealVolume() const { return mParams.mVolume * mParams.mBaseVolume; } float getPitch() const { return mParams.mPitch; } float getMinDistance() const { return mParams.mMinDistance; } float getMaxDistance() const { return mParams.mMaxDistance; } MWSound::Type getPlayType() const { return static_cast(mParams.mFlags & MWSound::Type::Mask); } bool getUseEnv() const { return !(mParams.mFlags & MWSound::PlayMode::NoEnv); } 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; } void init(const SoundParams& params) { mParams = params; mHandle = nullptr; } SoundBase() = default; }; class Sound : public SoundBase { Sound& operator=(const Sound&) = delete; Sound(const Sound&) = delete; Sound(Sound&&) = delete; public: Sound() { } }; class Stream : public SoundBase { Stream& operator=(const Stream&) = delete; Stream(const Stream&) = delete; Stream(Stream&&) = delete; public: Stream() { } }; } #endif ================================================ FILE: apps/openmw/mwsound/sound_buffer.cpp ================================================ #include "sound_buffer.hpp" #include "../mwbase/environment.hpp" #include "../mwbase/world.hpp" #include "../mwworld/esmstore.hpp" #include #include #include #include #include namespace MWSound { namespace { struct AudioParams { float mAudioDefaultMinDistance; float mAudioDefaultMaxDistance; float mAudioMinDistanceMult; float mAudioMaxDistanceMult; }; AudioParams makeAudioParams(const MWBase::World& world) { const auto& settings = world.getStore().get(); AudioParams params; params.mAudioDefaultMinDistance = settings.find("fAudioDefaultMinDistance")->mValue.getFloat(); params.mAudioDefaultMaxDistance = settings.find("fAudioDefaultMaxDistance")->mValue.getFloat(); params.mAudioMinDistanceMult = settings.find("fAudioMinDistanceMult")->mValue.getFloat(); params.mAudioMaxDistanceMult = settings.find("fAudioMaxDistanceMult")->mValue.getFloat(); return params; } } SoundBufferPool::SoundBufferPool(const VFS::Manager& vfs, Sound_Output& output) : mVfs(&vfs), mOutput(&output), mBufferCacheMax(std::max(Settings::Manager::getInt("buffer cache max", "Sound"), 1) * 1024 * 1024), mBufferCacheMin(std::min(static_cast(std::max(Settings::Manager::getInt("buffer cache min", "Sound"), 1)) * 1024 * 1024, mBufferCacheMax)) { } SoundBufferPool::~SoundBufferPool() { clear(); } Sound_Buffer* SoundBufferPool::lookup(const std::string& soundId) const { const auto it = mBufferNameMap.find(soundId); if (it != mBufferNameMap.end()) { Sound_Buffer* sfx = it->second; if (sfx->getHandle() != nullptr) return sfx; } return nullptr; } Sound_Buffer* SoundBufferPool::load(const std::string& soundId) { if (mBufferNameMap.empty()) { for (const ESM::Sound& sound : MWBase::Environment::get().getWorld()->getStore().get()) insertSound(Misc::StringUtils::lowerCase(sound.mId), sound); } Sound_Buffer* sfx; const auto it = mBufferNameMap.find(soundId); if (it != mBufferNameMap.end()) sfx = it->second; else { const ESM::Sound *sound = MWBase::Environment::get().getWorld()->getStore().get().search(soundId); if (sound == nullptr) return {}; sfx = insertSound(soundId, *sound); } if (sfx->getHandle() == nullptr) { auto [handle, size] = mOutput->loadSound(sfx->getResourceName()); if (handle == nullptr) return {}; sfx->mHandle = handle; mBufferCacheSize += size; if (mBufferCacheSize > mBufferCacheMax) { unloadUnused(); if (!mUnusedBuffers.empty() && mBufferCacheSize > mBufferCacheMax) Log(Debug::Warning) << "No unused sound buffers to free, using " << mBufferCacheSize << " bytes!"; } mUnusedBuffers.push_front(sfx); } return sfx; } void SoundBufferPool::clear() { for (auto &sfx : mSoundBuffers) { if(sfx.mHandle) mOutput->unloadSound(sfx.mHandle); sfx.mHandle = nullptr; } mUnusedBuffers.clear(); } Sound_Buffer* SoundBufferPool::insertSound(const std::string& soundId, const ESM::Sound& sound) { static const AudioParams audioParams = makeAudioParams(*MWBase::Environment::get().getWorld()); float volume = static_cast(std::pow(10.0, (sound.mData.mVolume / 255.0 * 3348.0 - 3348.0) / 2000.0)); float min = sound.mData.mMinRange; float max = sound.mData.mMaxRange; if (min == 0 && max == 0) { min = audioParams.mAudioDefaultMinDistance; max = audioParams.mAudioDefaultMaxDistance; } min *= audioParams.mAudioMinDistanceMult; max *= audioParams.mAudioMaxDistanceMult; min = std::max(min, 1.0f); max = std::max(min, max); Sound_Buffer& sfx = mSoundBuffers.emplace_back("Sound/" + sound.mSound, volume, min, max); mVfs->normalizeFilename(sfx.mResourceName); mBufferNameMap.emplace(soundId, &sfx); return &sfx; } void SoundBufferPool::unloadUnused() { while (!mUnusedBuffers.empty() && mBufferCacheSize > mBufferCacheMin) { Sound_Buffer* const unused = mUnusedBuffers.back(); mBufferCacheSize -= mOutput->unloadSound(unused->getHandle()); unused->mHandle = nullptr; mUnusedBuffers.pop_back(); } } } ================================================ FILE: apps/openmw/mwsound/sound_buffer.hpp ================================================ #ifndef GAME_SOUND_SOUND_BUFFER_H #define GAME_SOUND_SOUND_BUFFER_H #include #include #include #include #include "sound_output.hpp" namespace ESM { struct Sound; } namespace VFS { class Manager; } namespace MWSound { class SoundBufferPool; class Sound_Buffer { public: template Sound_Buffer(T&& resname, float volume, float mindist, float maxdist) : mResourceName(std::forward(resname)), mVolume(volume), mMinDist(mindist), mMaxDist(maxdist) {} const std::string& getResourceName() const noexcept { return mResourceName; } Sound_Handle getHandle() const noexcept { return mHandle; } float getVolume() const noexcept { return mVolume; } float getMinDist() const noexcept { return mMinDist; } float getMaxDist() const noexcept { return mMaxDist; } private: std::string mResourceName; float mVolume; float mMinDist; float mMaxDist; Sound_Handle mHandle = nullptr; std::size_t mUses = 0; friend class SoundBufferPool; }; class SoundBufferPool { public: SoundBufferPool(const VFS::Manager& vfs, Sound_Output& output); SoundBufferPool(const SoundBufferPool&) = delete; ~SoundBufferPool(); /// Lookup a soundId for its sound data (resource name, local volume, /// minRange, and maxRange) Sound_Buffer* lookup(const std::string& soundId) const; /// Lookup a soundId for its sound data (resource name, local volume, /// minRange, and maxRange), and ensure it's ready for use. Sound_Buffer* load(const std::string& soundId); void use(Sound_Buffer& sfx) { if (sfx.mUses++ == 0) { const auto it = std::find(mUnusedBuffers.begin(), mUnusedBuffers.end(), &sfx); if (it != mUnusedBuffers.end()) mUnusedBuffers.erase(it); } } void release(Sound_Buffer& sfx) { if (--sfx.mUses == 0) mUnusedBuffers.push_front(&sfx); } void clear(); private: const VFS::Manager* const mVfs; Sound_Output* mOutput; std::deque mSoundBuffers; std::unordered_map mBufferNameMap; std::size_t mBufferCacheMax; std::size_t mBufferCacheMin; std::size_t mBufferCacheSize = 0; // NOTE: unused buffers are stored in front-newest order. std::deque mUnusedBuffers; inline Sound_Buffer* insertSound(const std::string& soundId, const ESM::Sound& sound); inline void unloadUnused(); }; } #endif /* GAME_SOUND_SOUND_BUFFER_H */ ================================================ FILE: apps/openmw/mwsound/sound_decoder.hpp ================================================ #ifndef GAME_SOUND_SOUND_DECODER_H #define GAME_SOUND_SOUND_DECODER_H #include #include namespace VFS { class Manager; } namespace MWSound { enum SampleType { SampleType_UInt8, SampleType_Int16, SampleType_Float32 }; const char *getSampleTypeName(SampleType type); enum ChannelConfig { ChannelConfig_Mono, ChannelConfig_Stereo, ChannelConfig_Quad, ChannelConfig_5point1, ChannelConfig_7point1 }; const char *getChannelConfigName(ChannelConfig config); size_t framesToBytes(size_t frames, ChannelConfig config, SampleType type); size_t bytesToFrames(size_t bytes, ChannelConfig config, SampleType type); struct Sound_Decoder { const VFS::Manager* mResourceMgr; virtual void open(const std::string &fname) = 0; virtual void close() = 0; virtual std::string getName() = 0; virtual void getInfo(int *samplerate, ChannelConfig *chans, SampleType *type) = 0; virtual size_t read(char *buffer, size_t bytes) = 0; virtual void readAll(std::vector &output); virtual size_t getSampleOffset() = 0; Sound_Decoder(const VFS::Manager* resourceMgr) : mResourceMgr(resourceMgr) { } virtual ~Sound_Decoder() { } private: Sound_Decoder(const Sound_Decoder &rhs); Sound_Decoder& operator=(const Sound_Decoder &rhs); }; } #endif ================================================ FILE: apps/openmw/mwsound/sound_output.hpp ================================================ #ifndef GAME_SOUND_SOUND_OUTPUT_H #define GAME_SOUND_SOUND_OUTPUT_H #include #include #include #include "../mwbase/soundmanager.hpp" namespace MWSound { class SoundManager; struct Sound_Decoder; class Sound; class Stream; // An opaque handle for the implementation's sound buffers. typedef void *Sound_Handle; // An opaque handle for the implementation's sound instances. typedef void *Sound_Instance; enum class HrtfMode { Disable, Enable, Auto }; enum Environment { Env_Normal, Env_Underwater }; class Sound_Output { SoundManager &mManager; virtual std::vector enumerate() = 0; virtual bool init(const std::string &devname, const std::string &hrtfname, HrtfMode hrtfmode) = 0; virtual void deinit() = 0; virtual std::vector enumerateHrtf() = 0; virtual void setHrtf(const std::string &hrtfname, HrtfMode hrtfmode) = 0; virtual std::pair loadSound(const std::string &fname) = 0; virtual size_t unloadSound(Sound_Handle data) = 0; virtual bool playSound(Sound *sound, Sound_Handle data, float offset) = 0; virtual bool playSound3D(Sound *sound, Sound_Handle data, float offset) = 0; virtual void finishSound(Sound *sound) = 0; virtual bool isSoundPlaying(Sound *sound) = 0; virtual void updateSound(Sound *sound) = 0; virtual bool streamSound(DecoderPtr decoder, Stream *sound, bool getLoudnessData=false) = 0; virtual bool streamSound3D(DecoderPtr decoder, Stream *sound, bool getLoudnessData) = 0; virtual void finishStream(Stream *sound) = 0; virtual double getStreamDelay(Stream *sound) = 0; virtual double getStreamOffset(Stream *sound) = 0; virtual float getStreamLoudness(Stream *sound) = 0; virtual bool isStreamPlaying(Stream *sound) = 0; virtual void updateStream(Stream *sound) = 0; virtual void startUpdate() = 0; virtual void finishUpdate() = 0; virtual void updateListener(const osg::Vec3f &pos, const osg::Vec3f &atdir, const osg::Vec3f &updir, Environment env) = 0; virtual void pauseSounds(int types) = 0; virtual void resumeSounds(int types) = 0; virtual void pauseActiveDevice() = 0; virtual void resumeActiveDevice() = 0; Sound_Output& operator=(const Sound_Output &rhs); Sound_Output(const Sound_Output &rhs); protected: bool mInitialized; Sound_Output(SoundManager &mgr) : mManager(mgr), mInitialized(false) { } public: virtual ~Sound_Output() { } bool isInitialized() const { return mInitialized; } friend class OpenAL_Output; friend class SoundManager; friend class SoundBufferPool; }; } #endif ================================================ FILE: apps/openmw/mwsound/soundmanagerimp.cpp ================================================ #include "soundmanagerimp.hpp" #include #include #include #include #include #include #include #include "../mwbase/environment.hpp" #include "../mwbase/world.hpp" #include "../mwbase/statemanager.hpp" #include "../mwworld/esmstore.hpp" #include "../mwworld/cellstore.hpp" #include "../mwmechanics/actorutil.hpp" #include "sound_buffer.hpp" #include "sound_decoder.hpp" #include "sound_output.hpp" #include "sound.hpp" #include "openal_output.hpp" #include "ffmpeg_decoder.hpp" namespace MWSound { namespace { constexpr float sMinUpdateInterval = 1.0f / 30.0f; WaterSoundUpdaterSettings makeWaterSoundUpdaterSettings() { WaterSoundUpdaterSettings settings; settings.mNearWaterRadius = Fallback::Map::getInt("Water_NearWaterRadius"); settings.mNearWaterPoints = Fallback::Map::getInt("Water_NearWaterPoints"); settings.mNearWaterIndoorTolerance = Fallback::Map::getFloat("Water_NearWaterIndoorTolerance"); settings.mNearWaterOutdoorTolerance = Fallback::Map::getFloat("Water_NearWaterOutdoorTolerance"); settings.mNearWaterIndoorID = Misc::StringUtils::lowerCase(Fallback::Map::getString("Water_NearWaterIndoorID")); settings.mNearWaterOutdoorID = Misc::StringUtils::lowerCase(Fallback::Map::getString("Water_NearWaterOutdoorID")); return settings; } } // For combining PlayMode and Type flags inline int operator|(PlayMode a, Type b) { return static_cast(a) | static_cast(b); } SoundManager::SoundManager(const VFS::Manager* vfs, bool useSound) : mVFS(vfs) , mOutput(new OpenAL_Output(*this)) , mWaterSoundUpdater(makeWaterSoundUpdaterSettings()) , mSoundBuffers(*vfs, *mOutput) , mListenerUnderwater(false) , mListenerPos(0,0,0) , mListenerDir(1,0,0) , mListenerUp(0,0,1) , mUnderwaterSound(nullptr) , mNearWaterSound(nullptr) , mPlaybackPaused(false) , mTimePassed(0.f) , mLastCell(nullptr) , mCurrentRegionSound(nullptr) { if(!useSound) { Log(Debug::Info) << "Sound disabled."; return; } std::string hrtfname = Settings::Manager::getString("hrtf", "Sound"); int hrtfstate = Settings::Manager::getInt("hrtf enable", "Sound"); HrtfMode hrtfmode = hrtfstate < 0 ? HrtfMode::Auto : hrtfstate > 0 ? HrtfMode::Enable : HrtfMode::Disable; std::string devname = Settings::Manager::getString("device", "Sound"); if(!mOutput->init(devname, hrtfname, hrtfmode)) { Log(Debug::Error) << "Failed to initialize audio output, sound disabled"; return; } std::vector names = mOutput->enumerate(); std::stringstream stream; stream << "Enumerated output devices:\n"; for(const std::string &name : names) stream << " " << name; Log(Debug::Info) << stream.str(); stream.str(""); names = mOutput->enumerateHrtf(); if(!names.empty()) { stream << "Enumerated HRTF names:\n"; for(const std::string &name : names) stream << " " << name; Log(Debug::Info) << stream.str(); } } SoundManager::~SoundManager() { SoundManager::clear(); mSoundBuffers.clear(); mOutput.reset(); } // Return a new decoder instance, used as needed by the output implementations DecoderPtr SoundManager::getDecoder() { return std::make_shared(mVFS); } DecoderPtr SoundManager::loadVoice(const std::string &voicefile) { 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); } return decoder; } catch(std::exception &e) { Log(Debug::Error) << "Failed to load audio from " << voicefile << ": " << e.what(); } return nullptr; } SoundPtr SoundManager::getSoundRef() { return mSounds.get(); } StreamPtr SoundManager::getStreamRef() { return mStreams.get(); } StreamPtr SoundManager::playVoice(DecoderPtr decoder, const osg::Vec3f &pos, bool playlocal) { MWBase::World* world = MWBase::Environment::get().getWorld(); static const float fAudioMinDistanceMult = world->getStore().get().find("fAudioMinDistanceMult")->mValue.getFloat(); static const float fAudioMaxDistanceMult = world->getStore().get().find("fAudioMaxDistanceMult")->mValue.getFloat(); static const float fAudioVoiceDefaultMinDistance = world->getStore().get().find("fAudioVoiceDefaultMinDistance")->mValue.getFloat(); static const float fAudioVoiceDefaultMaxDistance = world->getStore().get().find("fAudioVoiceDefaultMaxDistance")->mValue.getFloat(); static float minDistance = std::max(fAudioVoiceDefaultMinDistance * fAudioMinDistanceMult, 1.0f); static float maxDistance = std::max(fAudioVoiceDefaultMaxDistance * fAudioMaxDistanceMult, minDistance); bool played; float basevol = volumeFromType(Type::Voice); StreamPtr sound = getStreamRef(); if(playlocal) { sound->init([&] { SoundParams params; params.mBaseVolume = basevol; params.mFlags = PlayMode::NoEnv | Type::Voice | Play_2D; return params; } ()); played = mOutput->streamSound(decoder, sound.get(), true); } else { sound->init([&] { SoundParams params; params.mPos = pos; params.mBaseVolume = basevol; params.mMinDistance = minDistance; params.mMaxDistance = maxDistance; params.mFlags = PlayMode::Normal | Type::Voice | Play_3D; return params; } ()); played = mOutput->streamSound3D(decoder, sound.get(), true); } if(!played) return nullptr; return sound; } // Gets the combined volume settings for the given sound type float SoundManager::volumeFromType(Type type) const { return mVolumeSettings.getVolumeFromType(type); } void SoundManager::stopMusic() { if(mMusic) { mOutput->finishStream(mMusic.get()); mMusic = nullptr; } } void SoundManager::streamMusicFull(const std::string& filename) { if(!mOutput->isInitialized()) return; Log(Debug::Info) << "Playing " << filename; mLastPlayedMusic = filename; stopMusic(); DecoderPtr decoder = getDecoder(); decoder->open(filename); mMusic = getStreamRef(); mMusic->init([&] { SoundParams params; params.mBaseVolume = volumeFromType(Type::Music); params.mFlags = PlayMode::NoEnv | Type::Music | Play_2D; return params; } ()); mOutput->streamSound(decoder, mMusic.get()); } void SoundManager::advanceMusic(const std::string& filename) { if (!isMusicPlaying()) { streamMusicFull(filename); return; } mNextMusic = filename; mMusic->setFadeout(1.f); } void SoundManager::startRandomTitle() { const std::vector &filelist = mMusicFiles[mCurrentPlaylist]; auto &tracklist = mMusicToPlay[mCurrentPlaylist]; // Do a Fisher-Yates shuffle // Repopulate if playlist is empty if(tracklist.empty()) { tracklist.resize(filelist.size()); std::iota(tracklist.begin(), tracklist.end(), 0); } int i = Misc::Rng::rollDice(tracklist.size()); // Reshuffle if last played music is the same after a repopulation if(filelist[tracklist[i]] == mLastPlayedMusic) i = (i+1) % tracklist.size(); // Remove music from list after advancing music advanceMusic(filelist[tracklist[i]]); tracklist[i] = tracklist.back(); tracklist.pop_back(); } void SoundManager::streamMusic(const std::string& filename) { advanceMusic("Music/"+filename); } bool SoundManager::isMusicPlaying() { return mMusic && mOutput->isStreamPlaying(mMusic.get()); } void SoundManager::playPlaylist(const std::string &playlist) { if (mCurrentPlaylist == playlist) return; if (mMusicFiles.find(playlist) == mMusicFiles.end()) { std::vector filelist; const std::map& index = mVFS->getIndex(); std::string pattern = "Music/" + playlist; mVFS->normalizeFilename(pattern); std::map::const_iterator found = index.lower_bound(pattern); while (found != index.end()) { if (found->first.size() >= pattern.size() && found->first.substr(0, pattern.size()) == pattern) filelist.push_back(found->first); else break; ++found; } mMusicFiles[playlist] = filelist; } if (mMusicFiles[playlist].empty()) return; mCurrentPlaylist = playlist; startRandomTitle(); } void SoundManager::playTitleMusic() { if (mCurrentPlaylist == "Title") return; if (mMusicFiles.find("Title") == mMusicFiles.end()) { std::vector filelist; const std::map& index = mVFS->getIndex(); // Is there an ini setting for this filename or something? std::string filename = "music/special/morrowind title.mp3"; auto found = index.find(filename); if (found != index.end()) { filelist.emplace_back(found->first); mMusicFiles["Title"] = filelist; } else { Log(Debug::Warning) << "Title music not found"; return; } } if (mMusicFiles["Title"].empty()) return; mCurrentPlaylist = "Title"; startRandomTitle(); } void SoundManager::say(const MWWorld::ConstPtr &ptr, const std::string &filename) { if(!mOutput->isInitialized()) return; std::string voicefile = "Sound/"+filename; mVFS->normalizeFilename(voicefile); DecoderPtr decoder = loadVoice(voicefile); if (!decoder) return; MWBase::World *world = MWBase::Environment::get().getWorld(); const osg::Vec3f pos = world->getActorHeadTransform(ptr).getTrans(); stopSay(ptr); StreamPtr sound = playVoice(decoder, pos, (ptr == MWMechanics::getPlayer())); if(!sound) return; mSaySoundsQueue.emplace(ptr, std::move(sound)); } float SoundManager::getSaySoundLoudness(const MWWorld::ConstPtr &ptr) const { SaySoundMap::const_iterator snditer = mActiveSaySounds.find(ptr); if(snditer != mActiveSaySounds.end()) { Stream *sound = snditer->second.get(); return mOutput->getStreamLoudness(sound); } return 0.0f; } void SoundManager::say(const std::string& filename) { if(!mOutput->isInitialized()) return; std::string voicefile = "Sound/"+filename; mVFS->normalizeFilename(voicefile); DecoderPtr decoder = loadVoice(voicefile); if (!decoder) return; stopSay(MWWorld::ConstPtr()); StreamPtr sound = playVoice(decoder, osg::Vec3f(), true); if(!sound) return; mActiveSaySounds.emplace(MWWorld::ConstPtr(), std::move(sound)); } bool SoundManager::sayDone(const MWWorld::ConstPtr &ptr) const { SaySoundMap::const_iterator snditer = mActiveSaySounds.find(ptr); if(snditer != mActiveSaySounds.end()) { if(mOutput->isStreamPlaying(snditer->second.get())) return false; return true; } return true; } bool SoundManager::sayActive(const MWWorld::ConstPtr &ptr) const { SaySoundMap::const_iterator snditer = mSaySoundsQueue.find(ptr); if(snditer != mSaySoundsQueue.end()) { if(mOutput->isStreamPlaying(snditer->second.get())) return true; return false; } snditer = mActiveSaySounds.find(ptr); if(snditer != mActiveSaySounds.end()) { if(mOutput->isStreamPlaying(snditer->second.get())) return true; return false; } return false; } void SoundManager::stopSay(const MWWorld::ConstPtr &ptr) { SaySoundMap::iterator snditer = mSaySoundsQueue.find(ptr); if(snditer != mSaySoundsQueue.end()) { mOutput->finishStream(snditer->second.get()); mSaySoundsQueue.erase(snditer); } snditer = mActiveSaySounds.find(ptr); if(snditer != mActiveSaySounds.end()) { mOutput->finishStream(snditer->second.get()); mActiveSaySounds.erase(snditer); } } Stream *SoundManager::playTrack(const DecoderPtr& decoder, Type type) { if(!mOutput->isInitialized()) return nullptr; StreamPtr track = getStreamRef(); track->init([&] { SoundParams params; params.mBaseVolume = volumeFromType(type); params.mFlags = PlayMode::NoEnv | type | Play_2D; return params; } ()); if(!mOutput->streamSound(decoder, track.get())) return nullptr; Stream* result = track.get(); const auto it = std::lower_bound(mActiveTracks.begin(), mActiveTracks.end(), track); mActiveTracks.insert(it, std::move(track)); return result; } void SoundManager::stopTrack(Stream *stream) { mOutput->finishStream(stream); TrackList::iterator iter = std::lower_bound(mActiveTracks.begin(), mActiveTracks.end(), stream, [] (const StreamPtr& lhs, Stream* rhs) { return lhs.get() < rhs; }); if(iter != mActiveTracks.end() && iter->get() == stream) mActiveTracks.erase(iter); } double SoundManager::getTrackTimeDelay(Stream *stream) { return mOutput->getStreamDelay(stream); } Sound* SoundManager::playSound(const std::string& soundId, float volume, float pitch, Type type, PlayMode mode, float offset) { if(!mOutput->isInitialized()) return nullptr; Sound_Buffer *sfx = mSoundBuffers.load(Misc::StringUtils::lowerCase(soundId)); if(!sfx) return nullptr; // Only one copy of given sound can be played at time, so stop previous copy stopSound(sfx, MWWorld::ConstPtr()); SoundPtr sound = getSoundRef(); sound->init([&] { SoundParams params; params.mVolume = volume * sfx->getVolume(); params.mBaseVolume = volumeFromType(type); params.mPitch = pitch; params.mFlags = mode | type | Play_2D; return params; } ()); if(!mOutput->playSound(sound.get(), sfx->getHandle(), offset)) return nullptr; Sound* result = sound.get(); mActiveSounds[MWWorld::ConstPtr()].emplace_back(std::move(sound), sfx); mSoundBuffers.use(*sfx); return result; } Sound *SoundManager::playSound3D(const MWWorld::ConstPtr &ptr, const std::string& soundId, float volume, float pitch, Type type, PlayMode mode, float offset) { if(!mOutput->isInitialized()) return nullptr; const osg::Vec3f objpos(ptr.getRefData().getPosition().asVec3()); if ((mode & PlayMode::RemoveAtDistance) && (mListenerPos - objpos).length2() > 2000 * 2000) return nullptr; // Look up the sound in the ESM data Sound_Buffer *sfx = mSoundBuffers.load(Misc::StringUtils::lowerCase(soundId)); if(!sfx) return nullptr; // Only one copy of given sound can be played at time on ptr, so stop previous copy stopSound(sfx, ptr); bool played; SoundPtr sound = getSoundRef(); if(!(mode&PlayMode::NoPlayerLocal) && ptr == MWMechanics::getPlayer()) { sound->init([&] { SoundParams params; params.mVolume = volume * sfx->getVolume(); params.mBaseVolume = volumeFromType(type); params.mPitch = pitch; params.mFlags = mode | type | Play_2D; return params; } ()); played = mOutput->playSound(sound.get(), sfx->getHandle(), offset); } else { sound->init([&] { SoundParams params; params.mPos = objpos; params.mVolume = volume * sfx->getVolume(); params.mBaseVolume = volumeFromType(type); params.mPitch = pitch; params.mMinDistance = sfx->getMinDist(); params.mMaxDistance = sfx->getMaxDist(); params.mFlags = mode | type | Play_3D; return params; } ()); played = mOutput->playSound3D(sound.get(), sfx->getHandle(), offset); } if(!played) return nullptr; Sound* result = sound.get(); mActiveSounds[ptr].emplace_back(std::move(sound), sfx); mSoundBuffers.use(*sfx); return result; } Sound *SoundManager::playSound3D(const osg::Vec3f& initialPos, const std::string& soundId, float volume, float pitch, Type type, PlayMode mode, float offset) { if(!mOutput->isInitialized()) return nullptr; // Look up the sound in the ESM data Sound_Buffer *sfx = mSoundBuffers.load(Misc::StringUtils::lowerCase(soundId)); if(!sfx) return nullptr; SoundPtr sound = getSoundRef(); sound->init([&] { SoundParams params; params.mPos = initialPos; params.mVolume = volume * sfx->getVolume(); params.mBaseVolume = volumeFromType(type); params.mPitch = pitch; params.mMinDistance = sfx->getMinDist(); params.mMaxDistance = sfx->getMaxDist(); params.mFlags = mode | type | Play_3D; return params; } ()); if(!mOutput->playSound3D(sound.get(), sfx->getHandle(), offset)) return nullptr; Sound* result = sound.get(); mActiveSounds[MWWorld::ConstPtr()].emplace_back(std::move(sound), sfx); mSoundBuffers.use(*sfx); return result; } void SoundManager::stopSound(Sound *sound) { if(sound) mOutput->finishSound(sound); } void SoundManager::stopSound(Sound_Buffer *sfx, const MWWorld::ConstPtr &ptr) { SoundMap::iterator snditer = mActiveSounds.find(ptr); if(snditer != mActiveSounds.end()) { for(SoundBufferRefPair &snd : snditer->second) { if(snd.second == sfx) mOutput->finishSound(snd.first.get()); } } } void SoundManager::stopSound3D(const MWWorld::ConstPtr &ptr, const std::string& soundId) { if(!mOutput->isInitialized()) return; Sound_Buffer *sfx = mSoundBuffers.lookup(Misc::StringUtils::lowerCase(soundId)); if (!sfx) return; stopSound(sfx, ptr); } void SoundManager::stopSound3D(const MWWorld::ConstPtr &ptr) { SoundMap::iterator snditer = mActiveSounds.find(ptr); if(snditer != mActiveSounds.end()) { for(SoundBufferRefPair &snd : snditer->second) mOutput->finishSound(snd.first.get()); } SaySoundMap::iterator sayiter = mSaySoundsQueue.find(ptr); if(sayiter != mSaySoundsQueue.end()) mOutput->finishStream(sayiter->second.get()); sayiter = mActiveSaySounds.find(ptr); if(sayiter != mActiveSaySounds.end()) mOutput->finishStream(sayiter->second.get()); } void SoundManager::stopSound(const MWWorld::CellStore *cell) { for(SoundMap::value_type &snd : mActiveSounds) { if(!snd.first.isEmpty() && snd.first != MWMechanics::getPlayer() && snd.first.getCell() == cell) { for(SoundBufferRefPair &sndbuf : snd.second) mOutput->finishSound(sndbuf.first.get()); } } for(SaySoundMap::value_type &snd : mSaySoundsQueue) { if(!snd.first.isEmpty() && snd.first != MWMechanics::getPlayer() && snd.first.getCell() == cell) mOutput->finishStream(snd.second.get()); } for(SaySoundMap::value_type &snd : mActiveSaySounds) { if(!snd.first.isEmpty() && snd.first != MWMechanics::getPlayer() && snd.first.getCell() == cell) mOutput->finishStream(snd.second.get()); } } void SoundManager::fadeOutSound3D(const MWWorld::ConstPtr &ptr, const std::string& soundId, float duration) { SoundMap::iterator snditer = mActiveSounds.find(ptr); if(snditer != mActiveSounds.end()) { Sound_Buffer *sfx = mSoundBuffers.lookup(Misc::StringUtils::lowerCase(soundId)); if (sfx == nullptr) return; for(SoundBufferRefPair &sndbuf : snditer->second) { if(sndbuf.second == sfx) sndbuf.first->setFadeout(duration); } } } bool SoundManager::getSoundPlaying(const MWWorld::ConstPtr &ptr, const std::string& soundId) const { SoundMap::const_iterator snditer = mActiveSounds.find(ptr); if(snditer != mActiveSounds.end()) { Sound_Buffer *sfx = mSoundBuffers.lookup(Misc::StringUtils::lowerCase(soundId)); return std::find_if(snditer->second.cbegin(), snditer->second.cend(), [this,sfx](const SoundBufferRefPair &snd) -> bool { return snd.second == sfx && mOutput->isSoundPlaying(snd.first.get()); } ) != snditer->second.cend(); } return false; } void SoundManager::pauseSounds(BlockerType blocker, int types) { if(mOutput->isInitialized()) { if (mPausedSoundTypes[blocker] != 0) resumeSounds(blocker); types = types & Type::Mask; mOutput->pauseSounds(types); mPausedSoundTypes[blocker] = types; } } void SoundManager::resumeSounds(BlockerType blocker) { if(mOutput->isInitialized()) { mPausedSoundTypes[blocker] = 0; int types = int(Type::Mask); for (int currentBlocker = 0; currentBlocker < BlockerType::MaxCount; currentBlocker++) { if (currentBlocker != blocker) types &= ~mPausedSoundTypes[currentBlocker]; } mOutput->resumeSounds(types); } } void SoundManager::pausePlayback() { if (mPlaybackPaused) return; mPlaybackPaused = true; mOutput->pauseActiveDevice(); } void SoundManager::resumePlayback() { if (!mPlaybackPaused) return; mPlaybackPaused = false; mOutput->resumeActiveDevice(); } void SoundManager::updateRegionSound(float duration) { MWBase::World *world = MWBase::Environment::get().getWorld(); const MWWorld::ConstPtr player = world->getPlayerPtr(); const ESM::Cell *cell = player.getCell()->getCell(); if (!cell->isExterior()) return; if (mCurrentRegionSound && mOutput->isSoundPlaying(mCurrentRegionSound)) return; if (const auto next = mRegionSoundSelector.getNextRandom(duration, cell->mRegion, *world)) mCurrentRegionSound = playSound(*next, 1.0f, 1.0f); } void SoundManager::updateWaterSound() { MWBase::World* world = MWBase::Environment::get().getWorld(); const MWWorld::ConstPtr player = world->getPlayerPtr(); const ESM::Cell *curcell = player.getCell()->getCell(); const auto update = mWaterSoundUpdater.update(player, *world); WaterSoundAction action; Sound_Buffer* sfx; std::tie(action, sfx) = getWaterSoundAction(update, curcell); switch (action) { case WaterSoundAction::DoNothing: break; case WaterSoundAction::SetVolume: mNearWaterSound->setVolume(update.mVolume * sfx->getVolume()); break; case WaterSoundAction::FinishSound: mOutput->finishSound(mNearWaterSound); mNearWaterSound = nullptr; break; case WaterSoundAction::PlaySound: if (mNearWaterSound) mOutput->finishSound(mNearWaterSound); mNearWaterSound = playSound(update.mId, update.mVolume, 1.0f, Type::Sfx, PlayMode::Loop); break; } mLastCell = curcell; } std::pair SoundManager::getWaterSoundAction( const WaterSoundUpdate& update, const ESM::Cell* cell) const { if (mNearWaterSound) { if (update.mVolume == 0.0f) return {WaterSoundAction::FinishSound, nullptr}; bool soundIdChanged = false; Sound_Buffer* sfx = mSoundBuffers.lookup(update.mId); if (mLastCell != cell) { const auto snditer = mActiveSounds.find(MWWorld::ConstPtr()); if (snditer != mActiveSounds.end()) { const auto pairiter = std::find_if( snditer->second.begin(), snditer->second.end(), [this](const SoundBufferRefPairList::value_type &item) -> bool { return mNearWaterSound == item.first.get(); } ); if (pairiter != snditer->second.end() && pairiter->second != sfx) soundIdChanged = true; } } if (soundIdChanged) return {WaterSoundAction::PlaySound, nullptr}; if (sfx) return {WaterSoundAction::SetVolume, sfx}; } else if (update.mVolume > 0.0f) return {WaterSoundAction::PlaySound, nullptr}; return {WaterSoundAction::DoNothing, nullptr}; } void SoundManager::updateSounds(float duration) { // We update active say sounds map for specific actors here // because for vanilla compatibility we can't do it immediately. SaySoundMap::iterator queuesayiter = mSaySoundsQueue.begin(); while (queuesayiter != mSaySoundsQueue.end()) { const auto dst = mActiveSaySounds.find(queuesayiter->first); if (dst == mActiveSaySounds.end()) mActiveSaySounds.emplace(queuesayiter->first, std::move(queuesayiter->second)); else dst->second = std::move(queuesayiter->second); mSaySoundsQueue.erase(queuesayiter++); } mTimePassed += duration; if (mTimePassed < sMinUpdateInterval) return; duration = mTimePassed; mTimePassed = 0.0f; // Make sure music is still playing if(!isMusicPlaying() && !mCurrentPlaylist.empty()) startRandomTitle(); Environment env = Env_Normal; if (mListenerUnderwater) env = Env_Underwater; else if(mUnderwaterSound) { mOutput->finishSound(mUnderwaterSound); mUnderwaterSound = nullptr; } mOutput->startUpdate(); mOutput->updateListener( mListenerPos, mListenerDir, mListenerUp, env ); updateMusic(duration); // Check if any sounds are finished playing, and trash them SoundMap::iterator snditer = mActiveSounds.begin(); while(snditer != mActiveSounds.end()) { MWWorld::ConstPtr ptr = snditer->first; SoundBufferRefPairList::iterator sndidx = snditer->second.begin(); while(sndidx != snditer->second.end()) { Sound *sound = sndidx->first.get(); if(!ptr.isEmpty() && 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(!mOutput->isSoundPlaying(sound)) { mOutput->finishSound(sound); if (sound == mUnderwaterSound) mUnderwaterSound = nullptr; if (sound == mNearWaterSound) mNearWaterSound = nullptr; mSoundBuffers.release(*sndidx->second); sndidx = snditer->second.erase(sndidx); } else { sound->updateFade(duration); mOutput->updateSound(sound); ++sndidx; } } if(snditer->second.empty()) snditer = mActiveSounds.erase(snditer); else ++snditer; } SaySoundMap::iterator sayiter = mActiveSaySounds.begin(); while(sayiter != mActiveSaySounds.end()) { MWWorld::ConstPtr ptr = sayiter->first; Stream *sound = sayiter->second.get(); if(!ptr.isEmpty() && sound->getIs3D()) { MWBase::World *world = MWBase::Environment::get().getWorld(); const osg::Vec3f pos = world->getActorHeadTransform(ptr).getTrans(); sound->setPosition(pos); if(sound->getDistanceCull()) { if((mListenerPos - pos).length2() > 2000*2000) mOutput->finishStream(sound); } } if(!mOutput->isStreamPlaying(sound)) { mOutput->finishStream(sound); mActiveSaySounds.erase(sayiter++); } else { sound->updateFade(duration); mOutput->updateStream(sound); ++sayiter; } } TrackList::iterator trkiter = mActiveTracks.begin(); for(;trkiter != mActiveTracks.end();++trkiter) { Stream *sound = trkiter->get(); if(!mOutput->isStreamPlaying(sound)) { mOutput->finishStream(sound); trkiter = mActiveTracks.erase(trkiter); } else { sound->updateFade(duration); mOutput->updateStream(sound); ++trkiter; } } if(mListenerUnderwater) { // Play underwater sound (after updating sounds) if(!mUnderwaterSound) mUnderwaterSound = playSound("Underwater", 1.0f, 1.0f, Type::Sfx, PlayMode::LoopNoEnv); } mOutput->finishUpdate(); } void SoundManager::updateMusic(float duration) { if (!mNextMusic.empty()) { mMusic->updateFade(duration); mOutput->updateStream(mMusic.get()); if (mMusic->getRealVolume() <= 0.f) { streamMusicFull(mNextMusic); mNextMusic.clear(); } } } void SoundManager::update(float duration) { if(!mOutput->isInitialized() || mPlaybackPaused) return; updateSounds(duration); if (MWBase::Environment::get().getStateManager()->getState()!= MWBase::StateManager::State_NoGame) { updateRegionSound(duration); updateWaterSound(); } } void SoundManager::processChangedSettings(const Settings::CategorySettingVector& settings) { mVolumeSettings.update(); if(!mOutput->isInitialized()) return; mOutput->startUpdate(); for(SoundMap::value_type &snd : mActiveSounds) { for(SoundBufferRefPair &sndbuf : snd.second) { Sound *sound = sndbuf.first.get(); sound->setBaseVolume(volumeFromType(sound->getPlayType())); mOutput->updateSound(sound); } } for(SaySoundMap::value_type &snd : mActiveSaySounds) { Stream *sound = snd.second.get(); sound->setBaseVolume(volumeFromType(sound->getPlayType())); mOutput->updateStream(sound); } for(SaySoundMap::value_type &snd : mSaySoundsQueue) { Stream *sound = snd.second.get(); sound->setBaseVolume(volumeFromType(sound->getPlayType())); mOutput->updateStream(sound); } for (const StreamPtr& sound : mActiveTracks) { sound->setBaseVolume(volumeFromType(sound->getPlayType())); mOutput->updateStream(sound.get()); } if(mMusic) { mMusic->setBaseVolume(volumeFromType(mMusic->getPlayType())); mOutput->updateStream(mMusic.get()); } mOutput->finishUpdate(); } void SoundManager::setListenerPosDir(const osg::Vec3f &pos, const osg::Vec3f &dir, const osg::Vec3f &up, bool underwater) { mListenerPos = pos; mListenerDir = dir; mListenerUp = up; mListenerUnderwater = underwater; mWaterSoundUpdater.setUnderwater(underwater); } void SoundManager::updatePtr(const MWWorld::ConstPtr &old, const MWWorld::ConstPtr &updated) { SoundMap::iterator snditer = mActiveSounds.find(old); if(snditer != mActiveSounds.end()) { SoundBufferRefPairList sndlist = std::move(snditer->second); mActiveSounds.erase(snditer); mActiveSounds.emplace(updated, std::move(sndlist)); } SaySoundMap::iterator sayiter = mSaySoundsQueue.find(old); if(sayiter != mSaySoundsQueue.end()) { StreamPtr stream = std::move(sayiter->second); mSaySoundsQueue.erase(sayiter); mSaySoundsQueue.emplace(updated, std::move(stream)); } sayiter = mActiveSaySounds.find(old); if(sayiter != mActiveSaySounds.end()) { StreamPtr stream = std::move(sayiter->second); mActiveSaySounds.erase(sayiter); mActiveSaySounds.emplace(updated, std::move(stream)); } } // Default readAll implementation, for decoders that can't do anything // better void Sound_Decoder::readAll(std::vector &output) { size_t total = output.size(); size_t got; output.resize(total+32768); while((got=read(&output[total], output.size()-total)) > 0) { total += got; output.resize(total*2); } output.resize(total); } const char *getSampleTypeName(SampleType type) { switch(type) { case SampleType_UInt8: return "U8"; case SampleType_Int16: return "S16"; case SampleType_Float32: return "Float32"; } return "(unknown sample type)"; } const char *getChannelConfigName(ChannelConfig config) { switch(config) { case ChannelConfig_Mono: return "Mono"; case ChannelConfig_Stereo: return "Stereo"; case ChannelConfig_Quad: return "Quad"; case ChannelConfig_5point1: return "5.1 Surround"; case ChannelConfig_7point1: return "7.1 Surround"; } return "(unknown channel config)"; } size_t framesToBytes(size_t frames, ChannelConfig config, SampleType type) { switch(config) { case ChannelConfig_Mono: frames *= 1; break; case ChannelConfig_Stereo: frames *= 2; break; case ChannelConfig_Quad: frames *= 4; break; case ChannelConfig_5point1: frames *= 6; break; case ChannelConfig_7point1: frames *= 8; break; } switch(type) { case SampleType_UInt8: frames *= 1; break; case SampleType_Int16: frames *= 2; break; case SampleType_Float32: frames *= 4; break; } return frames; } size_t bytesToFrames(size_t bytes, ChannelConfig config, SampleType type) { return bytes / framesToBytes(1, config, type); } void SoundManager::clear() { SoundManager::stopMusic(); for(SoundMap::value_type &snd : mActiveSounds) { for(SoundBufferRefPair &sndbuf : snd.second) { mOutput->finishSound(sndbuf.first.get()); mSoundBuffers.release(*sndbuf.second); } } mActiveSounds.clear(); mUnderwaterSound = nullptr; mNearWaterSound = nullptr; for(SaySoundMap::value_type &snd : mSaySoundsQueue) mOutput->finishStream(snd.second.get()); mSaySoundsQueue.clear(); for(SaySoundMap::value_type &snd : mActiveSaySounds) mOutput->finishStream(snd.second.get()); mActiveSaySounds.clear(); for(StreamPtr& sound : mActiveTracks) mOutput->finishStream(sound.get()); mActiveTracks.clear(); mPlaybackPaused = false; std::fill(std::begin(mPausedSoundTypes), std::end(mPausedSoundTypes), 0); } } ================================================ FILE: apps/openmw/mwsound/soundmanagerimp.hpp ================================================ #ifndef GAME_SOUND_SOUNDMANAGER_H #define GAME_SOUND_SOUNDMANAGER_H #include #include #include #include #include #include #include #include #include "../mwbase/soundmanager.hpp" #include "regionsoundselector.hpp" #include "watersoundupdater.hpp" #include "type.hpp" #include "volumesettings.hpp" #include "sound_buffer.hpp" namespace VFS { class Manager; } namespace ESM { struct Sound; struct Cell; } namespace MWSound { class Sound_Output; struct Sound_Decoder; class Sound; class Stream; using SoundPtr = Misc::ObjectPtr; using StreamPtr = Misc::ObjectPtr; class SoundManager : public MWBase::SoundManager { const VFS::Manager* mVFS; std::unique_ptr mOutput; // Caches available music tracks by std::unordered_map> mMusicFiles; std::unordered_map> mMusicToPlay; // A list with music files not yet played std::string mLastPlayedMusic; // The music file that was last played VolumeSettings mVolumeSettings; WaterSoundUpdater mWaterSoundUpdater; SoundBufferPool mSoundBuffers; Misc::ObjectPool mSounds; Misc::ObjectPool mStreams; typedef std::pair SoundBufferRefPair; typedef std::vector SoundBufferRefPairList; typedef std::map SoundMap; SoundMap mActiveSounds; typedef std::map SaySoundMap; SaySoundMap mSaySoundsQueue; SaySoundMap mActiveSaySounds; typedef std::vector TrackList; TrackList mActiveTracks; StreamPtr mMusic; std::string mCurrentPlaylist; bool mListenerUnderwater; osg::Vec3f mListenerPos; osg::Vec3f mListenerDir; osg::Vec3f mListenerUp; int mPausedSoundTypes[BlockerType::MaxCount] = {}; Sound *mUnderwaterSound; Sound *mNearWaterSound; std::string mNextMusic; bool mPlaybackPaused; RegionSoundSelector mRegionSoundSelector; float mTimePassed; const ESM::Cell *mLastCell; Sound* mCurrentRegionSound; Sound_Buffer *insertSound(const std::string &soundId, const ESM::Sound *sound); // returns a decoder to start streaming, or nullptr if the sound was not found DecoderPtr loadVoice(const std::string &voicefile); SoundPtr getSoundRef(); StreamPtr getStreamRef(); StreamPtr playVoice(DecoderPtr decoder, const osg::Vec3f &pos, bool playlocal); void streamMusicFull(const std::string& filename); void advanceMusic(const std::string& filename); void startRandomTitle(); void updateSounds(float duration); void updateRegionSound(float duration); void updateWaterSound(); void updateMusic(float duration); float volumeFromType(Type type) const; enum class WaterSoundAction { DoNothing, SetVolume, FinishSound, PlaySound, }; std::pair getWaterSoundAction(const WaterSoundUpdate& update, const ESM::Cell* cell) const; SoundManager(const SoundManager &rhs); SoundManager& operator=(const SoundManager &rhs); protected: DecoderPtr getDecoder(); friend class OpenAL_Output; void stopSound(Sound_Buffer *sfx, const MWWorld::ConstPtr &ptr); ///< Stop the given object from playing given sound buffer. public: SoundManager(const VFS::Manager* vfs, bool useSound); ~SoundManager() override; void processChangedSettings(const Settings::CategorySettingVector& settings) override; void stopMusic() override; ///< Stops music if it's playing void streamMusic(const std::string& filename) override; ///< Play a soundifle /// \param filename name of a sound file in "Music/" in the data directory. bool isMusicPlaying() override; ///< Returns true if music is playing void playPlaylist(const std::string &playlist) override; ///< Start playing music from the selected folder /// \param name of the folder that contains the playlist void playTitleMusic() override; ///< Start playing title music void say(const MWWorld::ConstPtr &reference, const std::string& filename) override; ///< Make an actor say some text. /// \param filename name of a sound file in "Sound/" in the data directory. void say(const std::string& filename) override; ///< Say some text, without an actor ref /// \param filename name of a sound file in "Sound/" in the data directory. bool sayActive(const MWWorld::ConstPtr &reference=MWWorld::ConstPtr()) const override; ///< Is actor not speaking? bool sayDone(const MWWorld::ConstPtr &reference=MWWorld::ConstPtr()) const override; ///< For scripting backward compatibility void stopSay(const MWWorld::ConstPtr &reference=MWWorld::ConstPtr()) override; ///< Stop an actor speaking float getSaySoundLoudness(const MWWorld::ConstPtr& reference) const override; ///< Check the currently playing say sound for this actor /// and get an average loudness value (scale [0,1]) at the current time position. /// If the actor is not saying anything, returns 0. Stream *playTrack(const DecoderPtr& decoder, Type type) override; ///< Play a 2D audio track, using a custom decoder void stopTrack(Stream *stream) override; ///< Stop the given audio track from playing double getTrackTimeDelay(Stream *stream) override; ///< Retives the time delay, in seconds, of the audio track (must be a sound /// returned by \ref playTrack). Only intended to be called by the track /// decoder's read method. Sound *playSound(const std::string& soundId, float volume, float pitch, Type type=Type::Sfx, PlayMode mode=PlayMode::Normal, float offset=0) override; ///< Play a sound, independently of 3D-position ///< @param offset Number of seconds into the sound to start playback. Sound *playSound3D(const MWWorld::ConstPtr &reference, const std::string& soundId, float volume, float pitch, Type type=Type::Sfx, PlayMode mode=PlayMode::Normal, float offset=0) override; ///< Play a 3D sound attached to an MWWorld::Ptr. Will be updated automatically with the Ptr's position, unless Play_NoTrack is specified. ///< @param offset Number of seconds into the sound to start playback. Sound *playSound3D(const osg::Vec3f& initialPos, const std::string& soundId, float volume, float pitch, Type type, PlayMode mode, float offset=0) override; ///< Play a 3D sound at \a initialPos. If the sound should be moving, it must be updated using Sound::setPosition. ///< @param offset Number of seconds into the sound to start playback. void stopSound(Sound *sound) override; ///< Stop the given sound from playing /// @note no-op if \a sound is null void stopSound3D(const MWWorld::ConstPtr &reference, const std::string& soundId) override; ///< Stop the given object from playing the given sound, void stopSound3D(const MWWorld::ConstPtr &reference) override; ///< Stop the given object from playing all sounds. void stopSound(const MWWorld::CellStore *cell) override; ///< Stop all sounds for the given cell. void fadeOutSound3D(const MWWorld::ConstPtr &reference, const std::string& soundId, float duration) override; ///< Fade out given sound (that is already playing) of given object ///< @param reference Reference to object, whose sound is faded out ///< @param soundId ID of the sound to fade out. ///< @param duration Time until volume reaches 0. bool getSoundPlaying(const MWWorld::ConstPtr &reference, const std::string& soundId) const override; ///< Is the given sound currently playing on the given object? void pauseSounds(MWSound::BlockerType blocker, int types=int(Type::Mask)) override; ///< Pauses all currently playing sounds, including music. void resumeSounds(MWSound::BlockerType blocker) override; ///< Resumes all previously paused sounds. void pausePlayback() override; void resumePlayback() override; void update(float duration) override; void setListenerPosDir(const osg::Vec3f &pos, const osg::Vec3f &dir, const osg::Vec3f &up, bool underwater) override; void updatePtr (const MWWorld::ConstPtr& old, const MWWorld::ConstPtr& updated) override; void clear() override; }; } #endif ================================================ FILE: apps/openmw/mwsound/type.hpp ================================================ #ifndef GAME_SOUND_TYPE_H #define GAME_SOUND_TYPE_H namespace MWSound { enum class Type { Sfx = 1 << 4, /* Normal SFX sound */ Voice = 1 << 5, /* Voice sound */ Foot = 1 << 6, /* Footstep sound */ Music = 1 << 7, /* Music track */ Movie = 1 << 8, /* Movie audio track */ Mask = Sfx | Voice | Foot | Music | Movie }; } #endif ================================================ FILE: apps/openmw/mwsound/volumesettings.cpp ================================================ #include "volumesettings.hpp" #include #include namespace MWSound { namespace { float clamp(float value) { return std::max(0.0f, std::min(1.0f, value)); } } VolumeSettings::VolumeSettings() : mMasterVolume(clamp(Settings::Manager::getFloat("master volume", "Sound"))), mSFXVolume(clamp(Settings::Manager::getFloat("sfx volume", "Sound"))), mMusicVolume(clamp(Settings::Manager::getFloat("music volume", "Sound"))), mVoiceVolume(clamp(Settings::Manager::getFloat("voice volume", "Sound"))), mFootstepsVolume(clamp(Settings::Manager::getFloat("footsteps volume", "Sound"))) { } float VolumeSettings::getVolumeFromType(Type type) const { float volume = mMasterVolume; switch(type) { case Type::Sfx: volume *= mSFXVolume; break; case Type::Voice: volume *= mVoiceVolume; break; case Type::Foot: volume *= mFootstepsVolume; break; case Type::Music: volume *= mMusicVolume; break; case Type::Movie: case Type::Mask: break; } return volume; } void VolumeSettings::update() { *this = VolumeSettings(); } } ================================================ FILE: apps/openmw/mwsound/volumesettings.hpp ================================================ #ifndef GAME_SOUND_VOLUMESETTINGS_H #define GAME_SOUND_VOLUMESETTINGS_H #include "type.hpp" namespace MWSound { class VolumeSettings { public: VolumeSettings(); float getVolumeFromType(Type type) const; void update(); private: float mMasterVolume; float mSFXVolume; float mMusicVolume; float mVoiceVolume; float mFootstepsVolume; }; } #endif ================================================ FILE: apps/openmw/mwsound/watersoundupdater.cpp ================================================ #include "watersoundupdater.hpp" #include "../mwbase/world.hpp" #include "../mwworld/cellstore.hpp" #include "../mwworld/ptr.hpp" #include #include namespace MWSound { WaterSoundUpdater::WaterSoundUpdater(const WaterSoundUpdaterSettings& settings) : mSettings(settings) { } WaterSoundUpdate WaterSoundUpdater::update(const MWWorld::ConstPtr& player, const MWBase::World& world) const { WaterSoundUpdate result; result.mId = player.getCell()->isExterior() ? mSettings.mNearWaterOutdoorID : mSettings.mNearWaterIndoorID; result.mVolume = std::min(1.0f, getVolume(player, world)); return result; } float WaterSoundUpdater::getVolume(const MWWorld::ConstPtr& player, const MWBase::World& world) const { if (mListenerUnderwater) return 1.0f; const MWWorld::CellStore& cell = *player.getCell(); if (!cell.getCell()->hasWater()) return 0.0f; const osg::Vec3f pos = player.getRefData().getPosition().asVec3(); const float dist = std::abs(cell.getWaterLevel() - pos.z()); if (cell.isExterior() && dist < mSettings.mNearWaterOutdoorTolerance) { if (mSettings.mNearWaterPoints <= 1) return (mSettings.mNearWaterOutdoorTolerance - dist) / mSettings.mNearWaterOutdoorTolerance; const float step = mSettings.mNearWaterRadius * 2.0f / (mSettings.mNearWaterPoints - 1); int underwaterPoints = 0; for (int x = 0; x < mSettings.mNearWaterPoints; x++) { for (int y = 0; y < mSettings.mNearWaterPoints; y++) { const float terrainX = pos.x() - mSettings.mNearWaterRadius + x * step; const float terrainY = pos.y() - mSettings.mNearWaterRadius + y * step; const float height = world.getTerrainHeightAt(osg::Vec3f(terrainX, terrainY, 0.0f)); if (height < 0) underwaterPoints++; } } return underwaterPoints * 2.0f / (mSettings.mNearWaterPoints * mSettings.mNearWaterPoints); } if (!cell.isExterior() && dist < mSettings.mNearWaterIndoorTolerance) return (mSettings.mNearWaterIndoorTolerance - dist) / mSettings.mNearWaterIndoorTolerance; return 0.0f; } } ================================================ FILE: apps/openmw/mwsound/watersoundupdater.hpp ================================================ #ifndef GAME_SOUND_WATERSOUNDUPDATER_H #define GAME_SOUND_WATERSOUNDUPDATER_H #include namespace MWBase { class World; } namespace MWWorld { class ConstPtr; } namespace MWSound { struct WaterSoundUpdaterSettings { int mNearWaterRadius; int mNearWaterPoints; float mNearWaterIndoorTolerance; float mNearWaterOutdoorTolerance; std::string mNearWaterIndoorID; std::string mNearWaterOutdoorID; }; struct WaterSoundUpdate { std::string mId; float mVolume; }; class WaterSoundUpdater { public: explicit WaterSoundUpdater(const WaterSoundUpdaterSettings& settings); WaterSoundUpdate update(const MWWorld::ConstPtr& player, const MWBase::World& world) const; void setUnderwater(bool value) { mListenerUnderwater = value; } private: const WaterSoundUpdaterSettings mSettings; bool mListenerUnderwater = false; float getVolume(const MWWorld::ConstPtr& player, const MWBase::World& world) const; }; } #endif ================================================ FILE: apps/openmw/mwstate/character.cpp ================================================ #include "character.hpp" #include #include #include #include #include bool MWState::operator< (const Slot& left, const Slot& right) { return left.mTimeStamp ignore reader.getRecHeader(); slot.mProfile.load (reader); if (Misc::StringUtils::lowerCase (slot.mProfile.mContentFiles.at (0))!= Misc::StringUtils::lowerCase (game)) return; // this file is for a different game -> ignore mSlots.push_back (slot); } void MWState::Character::addSlot (const ESM::SavedGame& profile) { Slot slot; 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) { if (std::isalnum(*it)) // Ignores multibyte characters and non alphanumeric characters stream << *it; else stream << "_"; } const std::string ext = ".omwsave"; slot.mPath = mPath / (stream.str() + ext); // Append an index if necessary to ensure a unique file int i=0; while (boost::filesystem::exists(slot.mPath)) { const std::string test = stream.str() + " - " + std::to_string(++i); slot.mPath = mPath / (test + ext); } slot.mProfile = profile; slot.mTimeStamp = std::time (nullptr); mSlots.push_back (slot); } MWState::Character::Character (const boost::filesystem::path& saves, const std::string& game) : mPath (saves) { if (!boost::filesystem::is_directory (mPath)) { boost::filesystem::create_directories (mPath); } else { for (boost::filesystem::directory_iterator iter (mPath); iter!=boost::filesystem::directory_iterator(); ++iter) { boost::filesystem::path slotPath = *iter; try { addSlot (slotPath, game); } catch (...) {} // ignoring bad saved game files for now } std::sort (mSlots.begin(), mSlots.end()); } } void MWState::Character::cleanup() { if (mSlots.size() == 0) { // All slots are gone, no need to keep the empty directory if (boost::filesystem::is_directory (mPath)) { // Extra safety check to make sure the directory is empty (e.g. slots failed to parse header) boost::filesystem::directory_iterator it(mPath); if (it == boost::filesystem::directory_iterator()) boost::filesystem::remove_all(mPath); } } } const MWState::Slot *MWState::Character::createSlot (const ESM::SavedGame& profile) { addSlot (profile); return &mSlots.back(); } void MWState::Character::deleteSlot (const Slot *slot) { int index = slot - &mSlots[0]; if (index<0 || index>=static_cast (mSlots.size())) { // sanity check; not entirely reliable throw std::logic_error ("slot not found"); } boost::filesystem::remove(slot->mPath); mSlots.erase (mSlots.begin()+index); } const MWState::Slot *MWState::Character::updateSlot (const Slot *slot, const ESM::SavedGame& profile) { int index = slot - &mSlots[0]; if (index<0 || index>=static_cast (mSlots.size())) { // sanity check; not entirely reliable throw std::logic_error ("slot not found"); } Slot newSlot = *slot; newSlot.mProfile = profile; newSlot.mTimeStamp = std::time (nullptr); mSlots.erase (mSlots.begin()+index); mSlots.push_back (newSlot); return &mSlots.back(); } MWState::Character::SlotIterator MWState::Character::begin() const { return mSlots.rbegin(); } MWState::Character::SlotIterator MWState::Character::end() const { return mSlots.rend(); } ESM::SavedGame MWState::Character::getSignature() const { if (mSlots.empty()) throw std::logic_error ("character signature not available"); std::vector::const_iterator iter (mSlots.begin()); Slot slot = *iter; for (++iter; iter!=mSlots.end(); ++iter) if (iter->mProfile.mPlayerLevel>slot.mProfile.mPlayerLevel) slot = *iter; else if (iter->mProfile.mPlayerLevel==slot.mProfile.mPlayerLevel && iter->mTimeStamp>slot.mTimeStamp) slot = *iter; return slot.mProfile; } const boost::filesystem::path& MWState::Character::getPath() const { return mPath; } ================================================ FILE: apps/openmw/mwstate/character.hpp ================================================ #ifndef GAME_STATE_CHARACTER_H #define GAME_STATE_CHARACTER_H #include #include namespace MWState { struct Slot { boost::filesystem::path mPath; ESM::SavedGame mProfile; std::time_t mTimeStamp; }; bool operator< (const Slot& left, const Slot& right); class Character { public: typedef std::vector::const_reverse_iterator SlotIterator; private: boost::filesystem::path mPath; std::vector mSlots; void addSlot (const boost::filesystem::path& path, const std::string& game); void addSlot (const ESM::SavedGame& profile); public: Character (const boost::filesystem::path& saves, const std::string& game); void cleanup(); ///< Delete the directory we used, if it is empty const Slot *createSlot (const ESM::SavedGame& profile); ///< Create new slot. /// /// \attention The ownership of the slot is not transferred. /// \note Slot must belong to this character. /// /// \attention The \a slot pointer will be invalidated by this call. void deleteSlot (const Slot *slot); const Slot *updateSlot (const Slot *slot, const ESM::SavedGame& profile); /// \note Slot must belong to this character. /// /// \attention The \a slot pointer will be invalidated by this call. SlotIterator begin() const; ///< Any call to createSlot and updateSlot can invalidate the returned iterator. SlotIterator end() const; const boost::filesystem::path& getPath() const; ESM::SavedGame getSignature() const; ///< Return signature information for this character. /// /// \attention This function must not be called if there are no slots. }; } #endif ================================================ FILE: apps/openmw/mwstate/charactermanager.cpp ================================================ #include "charactermanager.hpp" #include #include #include MWState::CharacterManager::CharacterManager (const boost::filesystem::path& saves, const std::string& game) : mPath (saves), mCurrent (nullptr), mGame (game) { if (!boost::filesystem::is_directory (mPath)) { boost::filesystem::create_directories (mPath); } else { for (boost::filesystem::directory_iterator iter (mPath); iter!=boost::filesystem::directory_iterator(); ++iter) { boost::filesystem::path characterDir = *iter; if (boost::filesystem::is_directory (characterDir)) { Character character (characterDir, mGame); if (character.begin()!=character.end()) mCharacters.push_back (character); } } } } MWState::Character *MWState::CharacterManager::getCurrentCharacter () { return mCurrent; } void MWState::CharacterManager::deleteSlot(const MWState::Character *character, const MWState::Slot *slot) { std::list::iterator it = findCharacter(character); it->deleteSlot(slot); if (character->begin() == character->end()) { // All slots deleted, cleanup and remove this character it->cleanup(); if (character == mCurrent) mCurrent = nullptr; mCharacters.erase(it); } } MWState::Character* MWState::CharacterManager::createCharacter(const std::string& name) { 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) { if (std::isalnum(*it)) // Ignores multibyte characters and non alphanumeric characters stream << *it; else stream << "_"; } boost::filesystem::path path = mPath / stream.str(); // Append an index if necessary to ensure a unique directory int i=0; while (boost::filesystem::exists(path)) { std::ostringstream test; test << stream.str(); test << " - " << ++i; path = mPath / test.str(); } mCharacters.emplace_back(path, mGame); return &mCharacters.back(); } std::list::iterator MWState::CharacterManager::findCharacter(const MWState::Character* character) { std::list::iterator it = mCharacters.begin(); for (; it != mCharacters.end(); ++it) { if (&*it == character) break; } if (it == mCharacters.end()) throw std::logic_error ("invalid character"); return it; } void MWState::CharacterManager::setCurrentCharacter (const Character *character) { if (!character) mCurrent = nullptr; else { std::list::iterator it = findCharacter(character); mCurrent = &*it; } } std::list::const_iterator MWState::CharacterManager::begin() const { return mCharacters.begin(); } std::list::const_iterator MWState::CharacterManager::end() const { return mCharacters.end(); } ================================================ FILE: apps/openmw/mwstate/charactermanager.hpp ================================================ #ifndef GAME_STATE_CHARACTERMANAGER_H #define GAME_STATE_CHARACTERMANAGER_H #include #include "character.hpp" namespace MWState { class CharacterManager { boost::filesystem::path mPath; // Uses std::list, so that mCurrent stays valid when characters are deleted std::list mCharacters; Character *mCurrent; std::string mGame; private: CharacterManager (const CharacterManager&); ///< Not implemented CharacterManager& operator= (const CharacterManager&); ///< Not implemented std::list::iterator findCharacter(const MWState::Character* character); public: CharacterManager (const boost::filesystem::path& saves, const std::string& game); Character *getCurrentCharacter (); ///< @note May return null void deleteSlot(const MWState::Character *character, const MWState::Slot *slot); Character* createCharacter(const std::string& name); ///< Create new character within saved game management /// \param name Name for the character (does not need to be unique) void setCurrentCharacter (const Character *character); std::list::const_iterator begin() const; std::list::const_iterator end() const; }; } #endif ================================================ FILE: apps/openmw/mwstate/quicksavemanager.cpp ================================================ #include "quicksavemanager.hpp" MWState::QuickSaveManager::QuickSaveManager(std::string &saveName, unsigned int maxSaves) : mSaveName(saveName) , mMaxSaves(maxSaves) , mSlotsVisited(0) , mOldestSlotVisited(nullptr) { } void MWState::QuickSaveManager::visitSave(const Slot *saveSlot) { if(mSaveName == saveSlot->mProfile.mDescription) { ++mSlotsVisited; if(isOldestSave(saveSlot)) mOldestSlotVisited = saveSlot; } } bool MWState::QuickSaveManager::isOldestSave(const Slot *compare) const { if(mOldestSlotVisited == nullptr) return true; return (compare->mTimeStamp <= mOldestSlotVisited->mTimeStamp); } bool MWState::QuickSaveManager::shouldCreateNewSlot() const { return (mSlotsVisited < mMaxSaves); } const MWState::Slot *MWState::QuickSaveManager::getNextQuickSaveSlot() { if(shouldCreateNewSlot()) return nullptr; return mOldestSlotVisited; } ================================================ FILE: apps/openmw/mwstate/quicksavemanager.hpp ================================================ #ifndef GAME_STATE_QUICKSAVEMANAGER_H #define GAME_STATE_QUICKSAVEMANAGER_H #include #include "character.hpp" namespace MWState{ class QuickSaveManager{ std::string mSaveName; unsigned int mMaxSaves; unsigned int mSlotsVisited; const Slot *mOldestSlotVisited; private: bool shouldCreateNewSlot() const; bool isOldestSave(const Slot *compare) const; public: QuickSaveManager(std::string &saveName, unsigned int maxSaves); ///< A utility class to manage multiple quicksave slots /// /// \param saveName The name of the save ("QuickSave", "AutoSave", etc) /// \param maxSaves The maximum number of save slots to create before recycling old ones void visitSave(const Slot *saveSlot); ///< Visits the given \a slot \a const Slot *getNextQuickSaveSlot(); ///< Get the slot that the next quicksave should use. /// ///\return Either the oldest quicksave slot visited, or nullptr if a new slot can be made }; } #endif ================================================ FILE: apps/openmw/mwstate/statemanagerimp.cpp ================================================ #include "statemanagerimp.hpp" #include #include #include #include #include #include #include #include #include #include #include #include "../mwbase/environment.hpp" #include "../mwbase/world.hpp" #include "../mwbase/journal.hpp" #include "../mwbase/dialoguemanager.hpp" #include "../mwbase/windowmanager.hpp" #include "../mwbase/mechanicsmanager.hpp" #include "../mwbase/scriptmanager.hpp" #include "../mwbase/soundmanager.hpp" #include "../mwbase/inputmanager.hpp" #include "../mwworld/player.hpp" #include "../mwworld/class.hpp" #include "../mwworld/cellstore.hpp" #include "../mwworld/esmstore.hpp" #include "../mwmechanics/npcstats.hpp" #include "../mwmechanics/actorutil.hpp" #include "../mwscript/globalscripts.hpp" #include "quicksavemanager.hpp" void MWState::StateManager::cleanup (bool force) { if (mState!=State_NoGame || force) { MWBase::Environment::get().getSoundManager()->clear(); MWBase::Environment::get().getDialogueManager()->clear(); MWBase::Environment::get().getJournal()->clear(); MWBase::Environment::get().getScriptManager()->clear(); MWBase::Environment::get().getWindowManager()->clear(); MWBase::Environment::get().getWorld()->clear(); MWBase::Environment::get().getInputManager()->clear(); MWBase::Environment::get().getMechanicsManager()->clear(); mState = State_NoGame; mCharacterManager.setCurrentCharacter(nullptr); mTimePlayed = 0; MWMechanics::CreatureStats::cleanup(); } } std::map MWState::StateManager::buildContentFileIndexMap (const ESM::ESMReader& reader) const { const std::vector& current = MWBase::Environment::get().getWorld()->getContentFiles(); const std::vector& prev = reader.getGameFiles(); std::map map; for (int iPrev = 0; iPrev (prev.size()); ++iPrev) { std::string id = Misc::StringUtils::lowerCase (prev[iPrev].name); for (int iCurrent = 0; iCurrent (current.size()); ++iCurrent) if (id==Misc::StringUtils::lowerCase (current[iCurrent])) { map.insert (std::make_pair (iPrev, iCurrent)); break; } } return map; } MWState::StateManager::StateManager (const boost::filesystem::path& saves, const std::string& game) : mQuitRequest (false), mAskLoadRecent(false), mState (State_NoGame), mCharacterManager (saves, game), mTimePlayed (0) { } void MWState::StateManager::requestQuit() { mQuitRequest = true; } bool MWState::StateManager::hasQuitRequest() const { return mQuitRequest; } void MWState::StateManager::askLoadRecent() { if (MWBase::Environment::get().getWindowManager()->getMode() == MWGui::GM_MainMenu) return; if( !mAskLoadRecent ) { const MWState::Character* character = getCurrentCharacter(); if(!character || character->begin() == character->end())//no saves { MWBase::Environment::get().getWindowManager()->pushGuiMode (MWGui::GM_MainMenu); } else { MWState::Slot lastSave = *character->begin(); std::vector buttons; buttons.emplace_back("#{sYes}"); buttons.emplace_back("#{sNo}"); std::string tag("%s"); std::string message = MWBase::Environment::get().getWindowManager()->getGameSettingString("sLoadLastSaveMsg", tag); size_t pos = message.find(tag); message.replace(pos, tag.length(), lastSave.mProfile.mDescription); MWBase::Environment::get().getWindowManager()->interactiveMessageBox(message, buttons); mAskLoadRecent = true; } } } MWState::StateManager::State MWState::StateManager::getState() const { return mState; } void MWState::StateManager::newGame (bool bypass) { cleanup(); if (!bypass) MWBase::Environment::get().getWindowManager()->setNewGame (true); try { Log(Debug::Info) << "Starting a new game"; MWBase::Environment::get().getScriptManager()->getGlobalScripts().addStartup(); MWBase::Environment::get().getWorld()->startNewGame (bypass); mState = State_Running; MWBase::Environment::get().getWindowManager()->fadeScreenOut(0); MWBase::Environment::get().getWindowManager()->fadeScreenIn(1); } catch (std::exception& e) { std::stringstream error; error << "Failed to start new game: " << e.what(); Log(Debug::Error) << error.str(); cleanup (true); MWBase::Environment::get().getWindowManager()->pushGuiMode (MWGui::GM_MainMenu); std::vector buttons; buttons.emplace_back("#{sOk}"); MWBase::Environment::get().getWindowManager()->interactiveMessageBox(error.str(), buttons); } } void MWState::StateManager::endGame() { mState = State_Ended; } void MWState::StateManager::resumeGame() { mState = State_Running; } void MWState::StateManager::saveGame (const std::string& description, const Slot *slot) { MWState::Character* character = getCurrentCharacter(); try { if (!character) { MWWorld::ConstPtr player = MWMechanics::getPlayer(); std::string name = player.get()->mBase->mName; character = mCharacterManager.createCharacter(name); mCharacterManager.setCurrentCharacter(character); } ESM::SavedGame profile; MWBase::World& world = *MWBase::Environment::get().getWorld(); MWWorld::Ptr player = world.getPlayerPtr(); profile.mContentFiles = world.getContentFiles(); profile.mPlayerName = player.get()->mBase->mName; profile.mPlayerLevel = player.getClass().getNpcStats (player).getLevel(); std::string classId = player.get()->mBase->mClass; if (world.getStore().get().isDynamic(classId)) profile.mPlayerClassName = world.getStore().get().find(classId)->mName; else profile.mPlayerClassId = classId; profile.mPlayerCell = world.getCellName(); profile.mInGameTime = world.getEpochTimeStamp(); profile.mTimePlayed = mTimePlayed; profile.mDescription = description; Log(Debug::Info) << "Making a screenshot for saved game '" << description << "'";; writeScreenshot(profile.mScreenshot); if (!slot) slot = character->createSlot (profile); else slot = character->updateSlot (slot, profile); // Make sure the animation state held by references is up to date before saving the game. MWBase::Environment::get().getMechanicsManager()->persistAnimationStates(); Log(Debug::Info) << "Writing saved game '" << description << "' for character '" << profile.mPlayerName << "'"; // Write to a memory stream first. If there is an exception during the save process, we don't want to trash the // existing save file we are overwriting. std::stringstream stream; ESM::ESMWriter writer; for (const std::string& contentFile : MWBase::Environment::get().getWorld()->getContentFiles()) writer.addMaster(contentFile, 0); // not using the size information anyway -> use value of 0 writer.setFormat (ESM::SavedGame::sCurrentFormat); // all unused writer.setVersion(0); writer.setType(0); writer.setAuthor(""); writer.setDescription(""); int recordCount = 1 // saved game header +MWBase::Environment::get().getJournal()->countSavedGameRecords() +MWBase::Environment::get().getWorld()->countSavedGameRecords() +MWBase::Environment::get().getScriptManager()->getGlobalScripts().countSavedGameRecords() +MWBase::Environment::get().getDialogueManager()->countSavedGameRecords() +MWBase::Environment::get().getWindowManager()->countSavedGameRecords() +MWBase::Environment::get().getMechanicsManager()->countSavedGameRecords() +MWBase::Environment::get().getInputManager()->countSavedGameRecords(); writer.setRecordCount (recordCount); writer.save (stream); Loading::Listener& listener = *MWBase::Environment::get().getWindowManager()->getLoadingScreen(); // Using only Cells for progress information, since they typically have the largest records by far listener.setProgressRange(MWBase::Environment::get().getWorld()->countSavedGameCells()); listener.setLabel("#{sNotifyMessage4}", true); Loading::ScopedLoad load(&listener); writer.startRecord (ESM::REC_SAVE); slot->mProfile.save (writer); writer.endRecord (ESM::REC_SAVE); MWBase::Environment::get().getJournal()->write (writer, listener); MWBase::Environment::get().getDialogueManager()->write (writer, listener); MWBase::Environment::get().getWorld()->write (writer, listener); MWBase::Environment::get().getScriptManager()->getGlobalScripts().write (writer, listener); MWBase::Environment::get().getWindowManager()->write(writer, listener); MWBase::Environment::get().getMechanicsManager()->write(writer, listener); MWBase::Environment::get().getInputManager()->write(writer, listener); // Ensure we have written the number of records that was estimated if (writer.getRecordCount() != recordCount+1) // 1 extra for TES3 record Log(Debug::Warning) << "Warning: number of written savegame records does not match. Estimated: " << recordCount+1 << ", written: " << writer.getRecordCount(); writer.close(); if (stream.fail()) throw std::runtime_error("Write operation failed (memory stream)"); // All good, write to file boost::filesystem::ofstream filestream (slot->mPath, std::ios::binary); filestream << stream.rdbuf(); if (filestream.fail()) throw std::runtime_error("Write operation failed (file stream)"); Settings::Manager::setString ("character", "Saves", slot->mPath.parent_path().filename().string()); } catch (const std::exception& e) { std::stringstream error; error << "Failed to save game: " << e.what(); Log(Debug::Error) << error.str(); std::vector buttons; buttons.emplace_back("#{sOk}"); MWBase::Environment::get().getWindowManager()->interactiveMessageBox(error.str(), buttons); // If no file was written, clean up the slot if (character && slot && !boost::filesystem::exists(slot->mPath)) { character->deleteSlot(slot); character->cleanup(); } } } void MWState::StateManager::quickSave (std::string name) { /* Start of tes3mp change (major) It should not be possible to quicksave the game in multiplayer, so it has been disabled */ return; /* End of tes3mp change (major) */ if (!(mState==State_Running && MWBase::Environment::get().getWorld()->getGlobalInt ("chargenstate")==-1 // char gen && MWBase::Environment::get().getWindowManager()->isSavingAllowed())) { //You can not save your game right now MWBase::Environment::get().getWindowManager()->messageBox("#{sSaveGameDenied}"); return; } int maxSaves = Settings::Manager::getInt("max quicksaves", "Saves"); if(maxSaves < 1) maxSaves = 1; Character* currentCharacter = getCurrentCharacter(); //Get current character QuickSaveManager saveFinder = QuickSaveManager(name, maxSaves); if (currentCharacter) { for (auto& save : *currentCharacter) { //Visiting slots allows the quicksave finder to find the oldest quicksave saveFinder.visitSave(&save); } } //Once all the saves have been visited, the save finder can tell us which //one to replace (or create) saveGame(name, saveFinder.getNextQuickSaveSlot()); } void MWState::StateManager::loadGame(const std::string& filepath) { for (const auto& character : mCharacterManager) { for (const auto& slot : character) { if (slot.mPath == boost::filesystem::path(filepath)) { loadGame(&character, slot.mPath.string()); return; } } } MWState::Character* character = getCurrentCharacter(); loadGame(character, filepath); } void MWState::StateManager::loadGame (const Character *character, const std::string& filepath) { try { cleanup(); Log(Debug::Info) << "Reading save file " << boost::filesystem::path(filepath).filename().string(); ESM::ESMReader reader; reader.open (filepath); if (reader.getFormat() > ESM::SavedGame::sCurrentFormat) throw std::runtime_error("This save file was created using a newer version of OpenMW and is thus not supported. Please upgrade to the newest OpenMW version to load this file."); std::map contentFileMap = buildContentFileIndexMap (reader); Loading::Listener& listener = *MWBase::Environment::get().getWindowManager()->getLoadingScreen(); listener.setProgressRange(100); listener.setLabel("#{sLoadingMessage14}"); Loading::ScopedLoad load(&listener); bool firstPersonCam = false; size_t total = reader.getFileSize(); int currentPercent = 0; while (reader.hasMoreRecs()) { ESM::NAME n = reader.getRecName(); reader.getRecHeader(); switch (n.intval) { case ESM::REC_SAVE: { ESM::SavedGame profile; profile.load(reader); if (!verifyProfile(profile)) { cleanup (true); MWBase::Environment::get().getWindowManager()->pushGuiMode (MWGui::GM_MainMenu); return; } mTimePlayed = profile.mTimePlayed; Log(Debug::Info) << "Loading saved game '" << profile.mDescription << "' for character '" << profile.mPlayerName << "'"; } break; case ESM::REC_JOUR: case ESM::REC_JOUR_LEGACY: case ESM::REC_QUES: MWBase::Environment::get().getJournal()->readRecord (reader, n.intval); break; case ESM::REC_DIAS: MWBase::Environment::get().getDialogueManager()->readRecord (reader, n.intval); break; case ESM::REC_ALCH: case ESM::REC_ARMO: case ESM::REC_BOOK: case ESM::REC_CLAS: case ESM::REC_CLOT: case ESM::REC_ENCH: case ESM::REC_NPC_: case ESM::REC_SPEL: case ESM::REC_WEAP: case ESM::REC_GLOB: case ESM::REC_PLAY: case ESM::REC_CSTA: case ESM::REC_WTHR: case ESM::REC_DYNA: case ESM::REC_ACTC: case ESM::REC_PROJ: case ESM::REC_MPRJ: case ESM::REC_ENAB: case ESM::REC_LEVC: case ESM::REC_LEVI: case ESM::REC_CREA: case ESM::REC_CONT: MWBase::Environment::get().getWorld()->readRecord(reader, n.intval, contentFileMap); break; case ESM::REC_CAM_: reader.getHNT(firstPersonCam, "FIRS"); break; case ESM::REC_GSCR: MWBase::Environment::get().getScriptManager()->getGlobalScripts().readRecord (reader, n.intval, contentFileMap); break; case ESM::REC_GMAP: case ESM::REC_KEYS: case ESM::REC_ASPL: case ESM::REC_MARK: MWBase::Environment::get().getWindowManager()->readRecord(reader, n.intval); break; case ESM::REC_DCOU: case ESM::REC_STLN: MWBase::Environment::get().getMechanicsManager()->readRecord(reader, n.intval); break; case ESM::REC_INPU: MWBase::Environment::get().getInputManager()->readRecord(reader, n.intval); break; default: // ignore invalid records Log(Debug::Warning) << "Warning: Ignoring unknown record: " << n.toString(); reader.skipRecord(); } int progressPercent = static_cast(float(reader.getFileOffset())/total*100); if (progressPercent > currentPercent) { listener.increaseProgress(progressPercent-currentPercent); currentPercent = progressPercent; } } mCharacterManager.setCurrentCharacter(character); mState = State_Running; if (character) Settings::Manager::setString ("character", "Saves", character->getPath().filename().string()); MWBase::Environment::get().getWindowManager()->setNewGame(false); MWBase::Environment::get().getWorld()->saveLoaded(); MWBase::Environment::get().getWorld()->setupPlayer(); MWBase::Environment::get().getWorld()->renderPlayer(); MWBase::Environment::get().getWindowManager()->updatePlayer(); MWBase::Environment::get().getMechanicsManager()->playerLoaded(); if (firstPersonCam != MWBase::Environment::get().getWorld()->isFirstPerson()) MWBase::Environment::get().getWorld()->togglePOV(); MWWorld::ConstPtr ptr = MWMechanics::getPlayer(); if (ptr.isInCell()) { const ESM::CellId& cellId = ptr.getCell()->getCell()->getCellId(); // Use detectWorldSpaceChange=false, otherwise some of the data we just loaded would be cleared again MWBase::Environment::get().getWorld()->changeToCell (cellId, ptr.getRefData().getPosition(), false, false); } else { // Cell no longer exists (i.e. changed game files), choose a default cell Log(Debug::Warning) << "Warning: Player character's cell no longer exists, changing to the default cell"; MWWorld::CellStore* cell = MWBase::Environment::get().getWorld()->getExterior(0,0); float x,y; MWBase::Environment::get().getWorld()->indexToPosition(0,0,x,y,false); ESM::Position pos; pos.pos[0] = x; pos.pos[1] = y; pos.pos[2] = 0; // should be adjusted automatically (adjustPlayerPos=true) pos.rot[0] = 0; pos.rot[1] = 0; pos.rot[2] = 0; MWBase::Environment::get().getWorld()->changeToCell(cell->getCell()->getCellId(), pos, true, false); } MWBase::Environment::get().getWorld()->updateProjectilesCasters(); // Vanilla MW will restart startup scripts when a save game is loaded. This is unintuitive, // but some mods may be using it as a reload detector. MWBase::Environment::get().getScriptManager()->getGlobalScripts().addStartup(); // Since we passed "changeEvent=false" to changeCell, we shouldn't have triggered the cell change flag. // But make sure the flag is cleared anyway in case it was set from an earlier game. MWBase::Environment::get().getWorld()->markCellAsUnchanged(); } catch (const std::exception& e) { std::stringstream error; error << "Failed to load saved game: " << e.what(); Log(Debug::Error) << error.str(); cleanup (true); MWBase::Environment::get().getWindowManager()->pushGuiMode (MWGui::GM_MainMenu); std::vector buttons; buttons.emplace_back("#{sOk}"); MWBase::Environment::get().getWindowManager()->interactiveMessageBox(error.str(), buttons); } } void MWState::StateManager::quickLoad() { /* Start of tes3mp change (major) It should not be possible to quickload the game in multiplayer, so it has been disabled */ return; /* End of tes3mp change (major) */ if (Character* currentCharacter = getCurrentCharacter ()) { if (currentCharacter->begin() == currentCharacter->end()) return; loadGame (currentCharacter, currentCharacter->begin()->mPath.string()); //Get newest save } } void MWState::StateManager::deleteGame(const MWState::Character *character, const MWState::Slot *slot) { mCharacterManager.deleteSlot(character, slot); } MWState::Character *MWState::StateManager::getCurrentCharacter () { return mCharacterManager.getCurrentCharacter(); } MWState::StateManager::CharacterIterator MWState::StateManager::characterBegin() { return mCharacterManager.begin(); } MWState::StateManager::CharacterIterator MWState::StateManager::characterEnd() { return mCharacterManager.end(); } void MWState::StateManager::update (float duration) { mTimePlayed += duration; // Note: It would be nicer to trigger this from InputManager, i.e. the very beginning of the frame update. if (mAskLoadRecent) { int iButton = MWBase::Environment::get().getWindowManager()->readPressedButton(); MWState::Character *curCharacter = getCurrentCharacter(); if(iButton==0 && curCharacter) { mAskLoadRecent = false; //Load last saved game for current character MWState::Slot lastSave = *curCharacter->begin(); loadGame(curCharacter, lastSave.mPath.string()); } else if(iButton==1) { mAskLoadRecent = false; MWBase::Environment::get().getWindowManager()->pushGuiMode (MWGui::GM_MainMenu); } } } bool MWState::StateManager::verifyProfile(const ESM::SavedGame& profile) const { const std::vector& selectedContentFiles = MWBase::Environment::get().getWorld()->getContentFiles(); bool notFound = false; for (const std::string& contentFile : profile.mContentFiles) { if (std::find(selectedContentFiles.begin(), selectedContentFiles.end(), contentFile) == selectedContentFiles.end()) { Log(Debug::Warning) << "Warning: Saved game dependency " << contentFile << " is missing."; notFound = true; } } if (notFound) { std::vector buttons; buttons.emplace_back("#{sYes}"); buttons.emplace_back("#{sNo}"); MWBase::Environment::get().getWindowManager()->interactiveMessageBox("#{sMissingMastersMsg}", buttons, true); int selectedButton = MWBase::Environment::get().getWindowManager()->readPressedButton(); if (selectedButton == 1 || selectedButton == -1) return false; } return true; } void MWState::StateManager::writeScreenshot(std::vector &imageData) const { int screenshotW = 259*2, screenshotH = 133*2; // *2 to get some nice antialiasing osg::ref_ptr screenshot (new osg::Image); MWBase::Environment::get().getWorld()->screenshot(screenshot.get(), screenshotW, screenshotH); osgDB::ReaderWriter* readerwriter = osgDB::Registry::instance()->getReaderWriterForExtension("jpg"); if (!readerwriter) { Log(Debug::Error) << "Error: Unable to write screenshot, can't find a jpg ReaderWriter"; return; } std::ostringstream ostream; osgDB::ReaderWriter::WriteResult result = readerwriter->writeImage(*screenshot, ostream); if (!result.success()) { Log(Debug::Error) << "Error: Unable to write screenshot: " << result.message() << " code " << result.status(); return; } std::string data = ostream.str(); imageData = std::vector(data.begin(), data.end()); } ================================================ FILE: apps/openmw/mwstate/statemanagerimp.hpp ================================================ #ifndef GAME_STATE_STATEMANAGER_H #define GAME_STATE_STATEMANAGER_H #include #include "../mwbase/statemanager.hpp" #include #include "charactermanager.hpp" namespace MWState { class StateManager : public MWBase::StateManager { bool mQuitRequest; bool mAskLoadRecent; State mState; CharacterManager mCharacterManager; double mTimePlayed; private: void cleanup (bool force = false); bool verifyProfile (const ESM::SavedGame& profile) const; void writeScreenshot (std::vector& imageData) const; std::map buildContentFileIndexMap (const ESM::ESMReader& reader) const; public: StateManager (const boost::filesystem::path& saves, const std::string& game); void requestQuit() override; bool hasQuitRequest() const override; void askLoadRecent() override; State getState() const override; void newGame (bool bypass = false) override; ///< Start a new game. /// /// \param bypass Skip new game mechanics. void endGame() override; void resumeGame() override; void deleteGame (const MWState::Character *character, const MWState::Slot *slot) override; ///< Delete a saved game slot from this character. If all save slots are deleted, the character will be deleted too. void saveGame (const std::string& description, const Slot *slot = nullptr) override; ///< Write a saved game to \a slot or create a new slot if \a slot == 0. /// /// \note Slot must belong to the current character. ///Saves a file, using supplied filename, overwritting if needed /** This is mostly used for quicksaving and autosaving, for they use the same name over and over again \param name Name of save, defaults to "Quicksave"**/ void quickSave(std::string name = "Quicksave") override; ///Loads the last saved file /** Used for quickload **/ void quickLoad() override; void loadGame (const std::string& filepath) override; ///< Load a saved game directly from the given file path. This will search the CharacterManager /// for a Character containing this save file, and set this Character current if one was found. /// Otherwise, a new Character will be created. void loadGame (const Character *character, const std::string &filepath) override; ///< Load a saved game file belonging to the given character. Character *getCurrentCharacter () override; ///< @note May return null. CharacterIterator characterBegin() override; ///< Any call to SaveGame and getCurrentCharacter can invalidate the returned /// iterator. CharacterIterator characterEnd() override; void update (float duration) override; }; } #endif ================================================ FILE: apps/openmw/mwworld/action.cpp ================================================ #include "action.hpp" /* Start of tes3mp addition Include additional headers for multiplayer purposes */ #include "../mwmp/Main.hpp" #include "../mwmp/Networking.hpp" #include "../mwmp/CellController.hpp" #include "../mwmp/ObjectList.hpp" /* End of tes3mp addition */ #include "../mwbase/environment.hpp" #include "../mwbase/world.hpp" #include "../mwbase/soundmanager.hpp" #include "../mwbase/windowmanager.hpp" #include "../mwmechanics/actorutil.hpp" const MWWorld::Ptr& MWWorld::Action::getTarget() const { return mTarget; } void MWWorld::Action::setTarget(const MWWorld::Ptr& target) { mTarget = target; } MWWorld::Action::Action (bool keepSound, const Ptr& target) : mKeepSound (keepSound), mSoundOffset(0), mTarget (target) {} MWWorld::Action::~Action() {} void MWWorld::Action::execute (const Ptr& actor, bool noSound) { if(!mSoundId.empty() && !noSound) { MWSound::PlayMode envType = MWSound::PlayMode::Normal; // Action sounds should not have a distortion in GUI mode // example: take an item or drink a potion underwater if (actor == MWMechanics::getPlayer() && MWBase::Environment::get().getWindowManager()->isGuiMode()) { envType = MWSound::PlayMode::NoEnv; } if(mKeepSound && actor == MWMechanics::getPlayer()) { MWBase::Environment::get().getSoundManager()->playSound(mSoundId, 1.0, 1.0, MWSound::Type::Sfx, envType, mSoundOffset ); /* Start of tes3mp addition Send an ID_OBJECT_SOUND packet every time the local player makes a sound here */ mwmp::ObjectList *objectList = mwmp::Main::get().getNetworking()->getObjectList(); objectList->reset(); objectList->packetOrigin = mwmp::CLIENT_GAMEPLAY; objectList->addObjectSound(actor, mSoundId, 1.0, 1.0); objectList->sendObjectSound(); /* End of tes3mp addition */ } else { bool local = mTarget.isEmpty() || !mTarget.isInCell(); // no usable target if(mKeepSound) { MWBase::Environment::get().getSoundManager()->playSound3D( (local ? actor : mTarget).getRefData().getPosition().asVec3(), mSoundId, 1.0, 1.0, MWSound::Type::Sfx, envType, mSoundOffset ); /* Start of tes3mp addition Send an ID_OBJECT_SOUND packet every time a local actor makes a sound here */ if (mwmp::Main::get().getCellController()->isLocalActor(actor)) { mwmp::ObjectList* objectList = mwmp::Main::get().getNetworking()->getObjectList(); objectList->reset(); objectList->packetOrigin = mwmp::CLIENT_GAMEPLAY; objectList->addObjectSound(local ? actor : mTarget, mSoundId, 1.0, 1.0); objectList->sendObjectSound(); } /* End of tes3mp addition */ } else { MWBase::Environment::get().getSoundManager()->playSound3D(local ? actor : mTarget, mSoundId, 1.0, 1.0, MWSound::Type::Sfx, envType, mSoundOffset ); /* Start of tes3mp addition Send an ID_OBJECT_SOUND packet every time a local actor makes a sound here */ if (mwmp::Main::get().getCellController()->isLocalActor(actor)) { mwmp::ObjectList* objectList = mwmp::Main::get().getNetworking()->getObjectList(); objectList->reset(); objectList->packetOrigin = mwmp::CLIENT_GAMEPLAY; objectList->addObjectSound(local ? actor : mTarget, mSoundId, 1.0, 1.0); objectList->sendObjectSound(); } /* End of tes3mp addition */ } } } executeImp (actor); } void MWWorld::Action::setSound (const std::string& id) { mSoundId = id; } void MWWorld::Action::setSoundOffset(float offset) { mSoundOffset=offset; } ================================================ FILE: apps/openmw/mwworld/action.hpp ================================================ #ifndef GAME_MWWORLD_ACTION_H #define GAME_MWWORLD_ACTION_H #include #include "ptr.hpp" namespace MWWorld { /// \brief Abstract base for actions class Action { std::string mSoundId; bool mKeepSound; float mSoundOffset; Ptr mTarget; // not implemented Action (const Action& action); Action& operator= (const Action& action); virtual void executeImp (const Ptr& actor) = 0; protected: void setTarget(const Ptr&); public: const Ptr& getTarget() const; Action (bool keepSound = false, const Ptr& target = Ptr()); ///< \param keepSound Keep playing the sound even if the object the sound is played on is removed. virtual ~Action(); virtual bool isNullAction() { return false; } ///< Is running this action a no-op? (default false) void execute (const Ptr& actor, bool noSound = false); void setSound (const std::string& id); void setSoundOffset(float offset); }; } #endif ================================================ FILE: apps/openmw/mwworld/actionalchemy.cpp ================================================ #include "actionalchemy.hpp" #include "../mwbase/environment.hpp" #include "../mwbase/windowmanager.hpp" #include "../mwmechanics/actorutil.hpp" namespace MWWorld { ActionAlchemy::ActionAlchemy(bool force) : Action (false) , mForce(force) { } void ActionAlchemy::executeImp (const Ptr& actor) { if (actor != MWMechanics::getPlayer()) return; if(!mForce && MWMechanics::isPlayerInCombat()) { //Ensure we're not in combat MWBase::Environment::get().getWindowManager()->messageBox("#{sInventoryMessage3}"); return; } MWBase::Environment::get().getWindowManager()->pushGuiMode(MWGui::GM_Alchemy); } } ================================================ FILE: apps/openmw/mwworld/actionalchemy.hpp ================================================ #ifndef GAME_MWWORLD_ACTIONALCHEMY_H #define GAME_MWWORLD_ACTIONALCHEMY_H #include "action.hpp" namespace MWWorld { class ActionAlchemy : public Action { bool mForce; void executeImp (const Ptr& actor) override; public: ActionAlchemy(bool force=false); }; } #endif ================================================ FILE: apps/openmw/mwworld/actionapply.cpp ================================================ #include "actionapply.hpp" #include "class.hpp" #include "../mwbase/environment.hpp" #include "../mwbase/world.hpp" #include "../mwworld/containerstore.hpp" #include "../mwmechanics/actorutil.hpp" namespace MWWorld { ActionApply::ActionApply (const Ptr& object, const std::string& id) : Action (false, object), mId (id) {} void ActionApply::executeImp (const Ptr& actor) { MWBase::Environment::get().getWorld()->breakInvisibility(actor); actor.getClass().apply (actor, mId, actor); actor.getClass().getContainerStore(actor).remove(getTarget(), 1, actor); } ActionApplyWithSkill::ActionApplyWithSkill (const Ptr& object, const std::string& id, int skillIndex, int usageType) : Action (false, object), mId (id), mSkillIndex (skillIndex), mUsageType (usageType) {} void ActionApplyWithSkill::executeImp (const Ptr& actor) { MWBase::Environment::get().getWorld()->breakInvisibility(actor); if (actor.getClass().apply (actor, mId, actor) && mUsageType!=-1 && actor == MWMechanics::getPlayer()) actor.getClass().skillUsageSucceeded (actor, mSkillIndex, mUsageType); actor.getClass().getContainerStore(actor).remove(getTarget(), 1, actor); } } ================================================ FILE: apps/openmw/mwworld/actionapply.hpp ================================================ #ifndef GAME_MWWORLD_ACTIONAPPLY_H #define GAME_MWWORLD_ACTIONAPPLY_H #include #include "action.hpp" namespace MWWorld { class ActionApply : public Action { std::string mId; void executeImp (const Ptr& actor) override; public: ActionApply (const Ptr& object, const std::string& id); }; class ActionApplyWithSkill : public Action { std::string mId; int mSkillIndex; int mUsageType; void executeImp (const Ptr& actor) override; public: ActionApplyWithSkill (const Ptr& object, const std::string& id, int skillIndex, int usageType); }; } #endif ================================================ FILE: apps/openmw/mwworld/actiondoor.cpp ================================================ #include "actiondoor.hpp" #include "../mwbase/environment.hpp" #include "../mwbase/world.hpp" namespace MWWorld { ActionDoor::ActionDoor (const MWWorld::Ptr& object) : Action (false, object) { } void ActionDoor::executeImp (const MWWorld::Ptr& actor) { MWBase::Environment::get().getWorld()->activateDoor(getTarget()); } } ================================================ FILE: apps/openmw/mwworld/actiondoor.hpp ================================================ #ifndef GAME_MWWORLD_ACTIONDOOR_H #define GAME_MWWORLD_ACTIONDOOR_H #include "action.hpp" #include "ptr.hpp" namespace MWWorld { class ActionDoor : public Action { void executeImp (const MWWorld::Ptr& actor) override; public: ActionDoor (const Ptr& object); }; } #endif ================================================ FILE: apps/openmw/mwworld/actioneat.cpp ================================================ #include "actioneat.hpp" #include #include "../mwworld/containerstore.hpp" #include "../mwmechanics/actorutil.hpp" #include "class.hpp" namespace MWWorld { void ActionEat::executeImp (const Ptr& actor) { // remove used item (assume the item is present in inventory) getTarget().getContainerStore()->remove(getTarget(), 1, actor); // apply to actor std::string id = getTarget().getCellRef().getRefId(); if (actor.getClass().apply (actor, id, actor) && actor == MWMechanics::getPlayer()) actor.getClass().skillUsageSucceeded (actor, ESM::Skill::Alchemy, 1); } ActionEat::ActionEat (const MWWorld::Ptr& object) : Action (false, object) {} } ================================================ FILE: apps/openmw/mwworld/actioneat.hpp ================================================ #ifndef GAME_MWWORLD_ACTIONEAT_H #define GAME_MWWORLD_ACTIONEAT_H #include "action.hpp" namespace MWWorld { class ActionEat : public Action { void executeImp (const Ptr& actor) override; public: ActionEat (const MWWorld::Ptr& object); }; } #endif ================================================ FILE: apps/openmw/mwworld/actionequip.cpp ================================================ #include "actionequip.hpp" #include "../mwbase/environment.hpp" #include "../mwbase/windowmanager.hpp" #include "../mwmechanics/actorutil.hpp" #include #include "inventorystore.hpp" #include "player.hpp" #include "class.hpp" namespace MWWorld { ActionEquip::ActionEquip (const MWWorld::Ptr& object, bool force) : Action (false, object) , mForce(force) { } void ActionEquip::executeImp (const Ptr& actor) { MWWorld::Ptr object = getTarget(); MWWorld::InventoryStore& invStore = actor.getClass().getInventoryStore(actor); if (object.getClass().hasItemHealth(object) && object.getCellRef().getCharge() == 0) { if (actor == MWMechanics::getPlayer()) MWBase::Environment::get().getWindowManager()->messageBox("#{sInventoryMessage1}"); return; } if (!mForce) { std::pair result = object.getClass().canBeEquipped (object, actor); // display error message if the player tried to equip something if (!result.second.empty() && actor == MWMechanics::getPlayer()) MWBase::Environment::get().getWindowManager()->messageBox(result.second); switch(result.first) { case 0: return; default: break; } } // slots that this item can be equipped in std::pair, bool> slots_ = getTarget().getClass().getEquipmentSlots(getTarget()); if (slots_.first.empty()) return; // retrieve ContainerStoreIterator to the item MWWorld::ContainerStoreIterator it = invStore.begin(); for (; it != invStore.end(); ++it) { if (*it == object) { break; } } if (it == invStore.end()) { std::stringstream error; error << "ActionEquip can't find item " << object.getCellRef().getRefId(); throw std::runtime_error(error.str()); } // equip the item in the first free slot std::vector::const_iterator slot=slots_.first.begin(); for (;slot!=slots_.first.end(); ++slot) { // if the item is equipped already, nothing to do if (invStore.getSlot(*slot) == it) return; if (invStore.getSlot(*slot) == invStore.end()) { // slot is not occupied invStore.equip(*slot, it, actor); break; } } // all slots are occupied -> cycle // move all slots one towards begin(), then equip the item in the slot that is now free if (slot == slots_.first.end()) { ContainerStoreIterator enchItem = invStore.getSelectedEnchantItem(); bool reEquip = false; for (slot = slots_.first.begin(); slot != slots_.first.end(); ++slot) { invStore.unequipSlot(*slot, actor, false); if (slot + 1 != slots_.first.end()) { invStore.equip(*slot, invStore.getSlot(*(slot + 1)), actor); } else { invStore.equip(*slot, it, actor); } //Fix for issue of selected enchated item getting remmoved on cycle if (invStore.getSlot(*slot) == enchItem) { reEquip = true; } } if (reEquip) { invStore.setSelectedEnchantItem(enchItem); } } } } ================================================ FILE: apps/openmw/mwworld/actionequip.hpp ================================================ #ifndef GAME_MWWORLD_ACTIONEQUIP_H #define GAME_MWWORLD_ACTIONEQUIP_H #include "action.hpp" namespace MWWorld { class ActionEquip : public Action { bool mForce; void executeImp (const Ptr& actor) override; public: /// @param item to equip ActionEquip (const Ptr& object, bool force=false); }; } #endif ================================================ FILE: apps/openmw/mwworld/actionharvest.cpp ================================================ #include "actionharvest.hpp" #include #include /* Start of tes3mp addition Include additional headers for multiplayer purposes */ #include "../mwmp/Main.hpp" #include "../mwmp/Networking.hpp" #include "../mwmp/LocalPlayer.hpp" #include "../mwmp/ObjectList.hpp" /* End of tes3mp addition */ #include #include "../mwbase/environment.hpp" #include "../mwbase/mechanicsmanager.hpp" #include "../mwbase/windowmanager.hpp" #include "../mwbase/world.hpp" #include "class.hpp" #include "containerstore.hpp" namespace MWWorld { ActionHarvest::ActionHarvest (const MWWorld::Ptr& container) : Action (true, container) { setSound("Item Ingredient Up"); } void ActionHarvest::executeImp (const MWWorld::Ptr& actor) { if (!MWBase::Environment::get().getWindowManager()->isAllowed(MWGui::GW_Inventory)) return; MWWorld::Ptr target = getTarget(); /* Start of tes3mp addition Prepare an ID_CONTAINER packet that will let the server know about the items removed from the harvested objects */ mwmp::ObjectList* objectList = mwmp::Main::get().getNetworking()->getObjectList(); objectList->reset(); objectList->packetOrigin = mwmp::CLIENT_GAMEPLAY; objectList->cell = *target.getCell()->getCell(); objectList->action = mwmp::BaseObjectList::REMOVE; objectList->containerSubAction = mwmp::BaseObjectList::NONE; mwmp::BaseObject baseObject = objectList->getBaseObjectFromPtr(target); /* End of tes3mp addition */ MWWorld::ContainerStore& store = target.getClass().getContainerStore (target); store.resolve(); MWWorld::ContainerStore& actorStore = actor.getClass().getContainerStore(actor); std::map takenMap; for (MWWorld::ContainerStoreIterator it = store.begin(); it != store.end(); ++it) { if (!it->getClass().showsInInventory(*it)) continue; int itemCount = it->getRefData().getCount(); // Note: it is important to check for crime before move an item from container. Otherwise owner check will not work // for a last item in the container - empty harvested containers are considered as "allowed to use". MWBase::Environment::get().getMechanicsManager()->itemTaken(actor, *it, target, itemCount); actorStore.add(*it, itemCount, actor); /* Start of tes3mp addition Track this item removal in the ID_CONTAINER packet being prepared */ objectList->addContainerItem(baseObject, *it, 0, itemCount); /* End of tes3mp addition */ store.remove(*it, itemCount, getTarget()); takenMap[it->getClass().getName(*it)]+=itemCount; } /* Start of tes3mp addition Send an ID_CONTAINER packet if the local player is logged in */ if (mwmp::Main::get().getLocalPlayer()->isLoggedIn()) { objectList->addBaseObject(baseObject); objectList->sendContainer(); } /* End of tes3mp addition */ // Spawn a messagebox (only for items added to player's inventory) if (actor == MWBase::Environment::get().getWorld()->getPlayerPtr()) { std::ostringstream stream; int lineCount = 0; const static int maxLines = 10; for (auto & pair : takenMap) { std::string itemName = pair.first; int itemCount = pair.second; lineCount++; if (lineCount == maxLines) stream << "\n..."; else if (lineCount > maxLines) break; // The two GMST entries below expand to strings informing the player of what, and how many of it has been added to their inventory std::string msgBox; if (itemCount == 1) { msgBox = MyGUI::LanguageManager::getInstance().replaceTags("\n#{sNotifyMessage60}"); msgBox = Misc::StringUtils::format(msgBox, itemName); } else { msgBox = MyGUI::LanguageManager::getInstance().replaceTags("\n#{sNotifyMessage61}"); msgBox = Misc::StringUtils::format(msgBox, itemCount, itemName); } stream << msgBox; } std::string tooltip = stream.str(); // remove the first newline (easier this way) if (tooltip.size() > 0 && tooltip[0] == '\n') tooltip.erase(0, 1); if (tooltip.size() > 0) MWBase::Environment::get().getWindowManager()->messageBox(tooltip); } // Update animation object MWBase::Environment::get().getWorld()->disable(target); MWBase::Environment::get().getWorld()->enable(target); } } ================================================ FILE: apps/openmw/mwworld/actionharvest.hpp ================================================ #ifndef GAME_MWWORLD_ACTIONHARVEST_H #define GAME_MWWORLD_ACTIONHARVEST_H #include "action.hpp" #include "ptr.hpp" namespace MWWorld { class ActionHarvest : public Action { void executeImp (const MWWorld::Ptr& actor) override; public: ActionHarvest (const Ptr& container); ///< \param container The Container the Player has activated. }; } #endif // ACTIONOPEN_H ================================================ FILE: apps/openmw/mwworld/actionopen.cpp ================================================ #include "actionopen.hpp" #include "../mwbase/environment.hpp" #include "../mwbase/mechanicsmanager.hpp" #include "../mwbase/windowmanager.hpp" #include "../mwmechanics/disease.hpp" namespace MWWorld { ActionOpen::ActionOpen (const MWWorld::Ptr& container) : Action (false, container) { } void ActionOpen::executeImp (const MWWorld::Ptr& actor) { if (!MWBase::Environment::get().getWindowManager()->isAllowed(MWGui::GW_Inventory)) return; if (actor != MWMechanics::getPlayer()) return; if (!MWBase::Environment::get().getMechanicsManager()->onOpen(getTarget())) return; MWMechanics::diseaseContact(actor, getTarget()); MWBase::Environment::get().getWindowManager()->pushGuiMode(MWGui::GM_Container, getTarget()); } } ================================================ FILE: apps/openmw/mwworld/actionopen.hpp ================================================ #ifndef GAME_MWWORLD_ACTIONOPEN_H #define GAME_MWWORLD_ACTIONOPEN_H #include "action.hpp" namespace MWWorld { class ActionOpen : public Action { void executeImp (const MWWorld::Ptr& actor) override; public: ActionOpen (const Ptr& container); ///< \param container The Container the Player has activated. }; } #endif // ACTIONOPEN_H ================================================ FILE: apps/openmw/mwworld/actionread.cpp ================================================ #include "actionread.hpp" /* Start of tes3mp addition Include additional headers for multiplayer purposes */ #include "../mwmp/Main.hpp" #include "../mwmp/LocalPlayer.hpp" /* End of tes3mp addition */ #include "../mwbase/environment.hpp" #include "../mwbase/windowmanager.hpp" #include "../mwbase/world.hpp" #include "../mwmechanics/npcstats.hpp" #include "../mwmechanics/actorutil.hpp" #include "player.hpp" #include "class.hpp" #include "esmstore.hpp" namespace MWWorld { ActionRead::ActionRead (const MWWorld::Ptr& object) : Action (false, object) { } void ActionRead::executeImp (const MWWorld::Ptr& actor) { if (actor != MWMechanics::getPlayer()) return; //Ensure we're not in combat if(MWMechanics::isPlayerInCombat() // Reading in combat is still allowed if the scroll/book is not in the player inventory yet // (since otherwise, there would be no way to pick it up) && getTarget().getContainerStore() == &actor.getClass().getContainerStore(actor) ) { MWBase::Environment::get().getWindowManager()->messageBox("#{sInventoryMessage4}"); return; } LiveCellRef *ref = getTarget().get(); if (ref->mBase->mData.mIsScroll) MWBase::Environment::get().getWindowManager()->pushGuiMode(MWGui::GM_Scroll, getTarget()); else MWBase::Environment::get().getWindowManager()->pushGuiMode(MWGui::GM_Book, getTarget()); MWMechanics::NpcStats& npcStats = actor.getClass().getNpcStats (actor); // Skill gain from books if (ref->mBase->mData.mSkillId >= 0 && ref->mBase->mData.mSkillId < ESM::Skill::Length && !npcStats.hasBeenUsed (ref->mBase->mId)) { MWWorld::LiveCellRef *playerRef = actor.get(); const ESM::Class *class_ = MWBase::Environment::get().getWorld()->getStore().get().find ( playerRef->mBase->mClass ); npcStats.increaseSkill (ref->mBase->mData.mSkillId, *class_, true, true); npcStats.flagAsUsed (ref->mBase->mId); /* Start of tes3mp addition Send an ID_PLAYER_BOOK packet every time a player reads a skill book */ mwmp::Main::get().getLocalPlayer()->sendBook(ref->mBase->mId); /* End of tes3mp addition */ } } } ================================================ FILE: apps/openmw/mwworld/actionread.hpp ================================================ #ifndef GAME_MWWORLD_ACTIONREAD_H #define GAME_MWWORLD_ACTIONREAD_H #include "action.hpp" namespace MWWorld { class ActionRead : public Action { void executeImp (const MWWorld::Ptr& actor) override; public: /// @param book or scroll to read ActionRead (const Ptr& object); }; } #endif // ACTIONREAD_H ================================================ FILE: apps/openmw/mwworld/actionrepair.cpp ================================================ #include "actionrepair.hpp" #include "../mwbase/environment.hpp" #include "../mwbase/windowmanager.hpp" #include "../mwbase/world.hpp" #include "../mwmechanics/actorutil.hpp" namespace MWWorld { ActionRepair::ActionRepair(const Ptr& item, bool force) : Action (false, item) , mForce(force) { } void ActionRepair::executeImp (const Ptr& actor) { if (actor != MWMechanics::getPlayer()) return; if(!mForce && MWMechanics::isPlayerInCombat()) { MWBase::Environment::get().getWindowManager()->messageBox("#{sInventoryMessage2}"); return; } MWBase::Environment::get().getWindowManager()->pushGuiMode(MWGui::GM_Repair, getTarget()); } } ================================================ FILE: apps/openmw/mwworld/actionrepair.hpp ================================================ #ifndef GAME_MWWORLD_ACTIONREPAIR_H #define GAME_MWWORLD_ACTIONREPAIR_H #include "action.hpp" namespace MWWorld { class ActionRepair : public Action { bool mForce; void executeImp (const Ptr& actor) override; public: /// @param item repair hammer ActionRepair(const Ptr& item, bool force=false); }; } #endif ================================================ FILE: apps/openmw/mwworld/actionsoulgem.cpp ================================================ #include "actionsoulgem.hpp" #include "../mwbase/windowmanager.hpp" #include "../mwbase/environment.hpp" #include "../mwmechanics/actorutil.hpp" namespace MWWorld { ActionSoulgem::ActionSoulgem(const Ptr &object) : Action(false, object) { } void ActionSoulgem::executeImp(const Ptr &actor) { if (actor != MWMechanics::getPlayer()) return; if(MWMechanics::isPlayerInCombat()) { //Ensure we're not in combat MWBase::Environment::get().getWindowManager()->messageBox("#{sInventoryMessage5}"); return; } MWBase::Environment::get().getWindowManager()->showSoulgemDialog(getTarget()); } } ================================================ FILE: apps/openmw/mwworld/actionsoulgem.hpp ================================================ #ifndef GAME_MWWORLD_ACTIONSOULGEM_H #define GAME_MWWORLD_ACTIONSOULGEM_H #include "action.hpp" namespace MWWorld { class ActionSoulgem : public Action { void executeImp (const MWWorld::Ptr& actor) override; public: /// @param soulgem to use ActionSoulgem (const Ptr& object); }; } #endif ================================================ FILE: apps/openmw/mwworld/actiontake.cpp ================================================ #include "actiontake.hpp" /* Start of tes3mp addition Include additional headers for multiplayer purposes */ #include "../mwmp/Main.hpp" #include "../mwmp/Networking.hpp" #include "../mwmp/ObjectList.hpp" /* End of tes3mp addition */ #include "../mwbase/environment.hpp" #include "../mwbase/world.hpp" #include "../mwbase/windowmanager.hpp" #include "../mwbase/mechanicsmanager.hpp" #include "../mwgui/inventorywindow.hpp" #include "class.hpp" #include "containerstore.hpp" namespace MWWorld { ActionTake::ActionTake (const MWWorld::Ptr& object) : Action (true, object) {} void ActionTake::executeImp (const Ptr& actor) { // When in GUI mode, we should use drag and drop if (actor == MWBase::Environment::get().getWorld()->getPlayerPtr()) { MWGui::GuiMode mode = MWBase::Environment::get().getWindowManager()->getMode(); if (mode == MWGui::GM_Inventory || mode == MWGui::GM_Container) { MWBase::Environment::get().getWindowManager()->getInventoryWindow()->pickUpObject(getTarget()); return; } } MWBase::Environment::get().getMechanicsManager()->itemTaken( actor, getTarget(), MWWorld::Ptr(), getTarget().getRefData().getCount()); MWWorld::Ptr newitem = *actor.getClass().getContainerStore (actor).add (getTarget(), getTarget().getRefData().getCount(), actor); /* Start of tes3mp addition Send an ID_OBJECT_DELETE packet every time an item is taken from the world by the player outside of the inventory screen */ mwmp::ObjectList *objectList = mwmp::Main::get().getNetworking()->getObjectList(); objectList->reset(); objectList->packetOrigin = mwmp::CLIENT_GAMEPLAY; objectList->addObjectGeneric(getTarget()); objectList->sendObjectDelete(); /* End of tes3mp addition */ MWBase::Environment::get().getWorld()->deleteObject (getTarget()); setTarget(newitem); } } ================================================ FILE: apps/openmw/mwworld/actiontake.hpp ================================================ #ifndef GAME_MWWORLD_ACTIONTAKE_H #define GAME_MWWORLD_ACTIONTAKE_H #include "action.hpp" namespace MWWorld { class ActionTake : public Action { void executeImp (const Ptr& actor) override; public: ActionTake (const MWWorld::Ptr& object); }; } #endif ================================================ FILE: apps/openmw/mwworld/actiontalk.cpp ================================================ #include "actiontalk.hpp" #include "../mwbase/environment.hpp" #include "../mwbase/windowmanager.hpp" #include "../mwmechanics/actorutil.hpp" namespace MWWorld { ActionTalk::ActionTalk (const Ptr& actor) : Action (false, actor) {} void ActionTalk::executeImp (const Ptr& actor) { /* Start of tes3mp change (major) We need to be able to make actors start conversations with players, so reverse the check added by 4118b20608b630b8d166d060a34c1234b80e378d here */ MWBase::Environment::get().getWindowManager()->pushGuiMode(MWGui::GM_Dialogue, getTarget()); /* End of tes3mp change (major) */ } } ================================================ FILE: apps/openmw/mwworld/actiontalk.hpp ================================================ #ifndef GAME_MWWORLD_ACTIONTALK_H #define GAME_MWWORLD_ACTIONTALK_H #include "action.hpp" namespace MWWorld { class ActionTalk : public Action { void executeImp (const Ptr& actor) override; public: ActionTalk (const Ptr& actor); ///< \param actor The actor the player is talking to }; } #endif ================================================ FILE: apps/openmw/mwworld/actionteleport.cpp ================================================ #include "actionteleport.hpp" /* Start of tes3mp addition Include additional headers for multiplayer purposes */ #include #include "../mwbase/windowmanager.hpp" #include "../mwmp/Main.hpp" #include "../mwmp/Networking.hpp" #include "../mwmp/ActorList.hpp" #include "../mwmp/CellController.hpp" #include "../mwmp/MechanicsHelper.hpp" /* End of tes3mp addition */ #include "../mwbase/environment.hpp" #include "../mwbase/world.hpp" #include "../mwbase/mechanicsmanager.hpp" #include "../mwmechanics/creaturestats.hpp" #include "../mwworld/class.hpp" #include "player.hpp" namespace MWWorld { ActionTeleport::ActionTeleport (const std::string& cellName, const ESM::Position& position, bool teleportFollowers) : Action (true), mCellName (cellName), mPosition (position), mTeleportFollowers(teleportFollowers) { } void ActionTeleport::executeImp (const Ptr& actor) { if (mTeleportFollowers) { // Find any NPCs that are following the actor and teleport them with him std::set followers; getFollowers(actor, followers, true); for (std::set::iterator it = followers.begin(); it != followers.end(); ++it) teleport(*it); } teleport(actor); } void ActionTeleport::teleport(const Ptr &actor) { MWBase::World* world = MWBase::Environment::get().getWorld(); actor.getClass().getCreatureStats(actor).land(actor == world->getPlayerPtr()); if(actor == world->getPlayerPtr()) { world->getPlayer().setTeleported(true); if (mCellName.empty()) world->changeToExteriorCell (mPosition, true); else world->changeToInteriorCell (mCellName, mPosition, true); } else { /* Start of tes3mp addition Track the original cell of this actor so we can use it when sending a packet */ ESM::Cell originalCell = *actor.getCell()->getCell(); /* End of tes3mp addition */ /* Start of tes3mp change (minor) If this is a DedicatedActor, get their new cell and override their stored cell with it so their cell change is approved in World::moveObject() */ MWWorld::CellStore *newCellStore; mwmp::CellController *cellController = mwmp::Main::get().getCellController(); if (actor.getClass().getCreatureStats(actor).getAiSequence().isInCombat(world->getPlayerPtr())) actor.getClass().getCreatureStats(actor).getAiSequence().stopCombat(); else if (mCellName.empty()) { int cellX; int cellY; world->positionToIndex(mPosition.pos[0],mPosition.pos[1],cellX,cellY); newCellStore = world->getExterior(cellX, cellY); if (cellController->isDedicatedActor(actor)) cellController->getDedicatedActor(actor)->cell = *newCellStore->getCell(); world->moveObject(actor,world->getExterior(cellX,cellY), mPosition.pos[0],mPosition.pos[1],mPosition.pos[2]); } else { newCellStore = world->getInterior(mCellName); if (cellController->isDedicatedActor(actor)) cellController->getDedicatedActor(actor)->cell = *newCellStore->getCell(); world->moveObject(actor,world->getInterior(mCellName),mPosition.pos[0],mPosition.pos[1],mPosition.pos[2]); } /* Start of tes3mp change (minor) */ /* Start of tes3mp addition Send ActorCellChange packets when an actor follows us across cells, regardless of whether we're the cell authority or not; the server can decide if it wants to comply with them Afterwards, send an ActorAI packet about this actor being our follower, to ensure they remain our follower even if the destination cell has another player as its cell authority */ mwmp::BaseActor baseActor; baseActor.refNum = actor.getCellRef().getRefNum().mIndex; baseActor.mpNum = actor.getCellRef().getMpNum(); baseActor.cell = *newCellStore->getCell(); baseActor.position = actor.getRefData().getPosition(); baseActor.isFollowerCellChange = true; mwmp::ActorList *actorList = mwmp::Main::get().getNetworking()->getActorList(); actorList->reset(); actorList->cell = originalCell; LOG_MESSAGE_SIMPLE(TimedLog::LOG_INFO, "Sending ID_ACTOR_CELL_CHANGE about %s %i-%i to server", actor.getCellRef().getRefId().c_str(), baseActor.refNum, baseActor.mpNum); LOG_APPEND(TimedLog::LOG_INFO, "- Moved from %s to %s", actorList->cell.getDescription().c_str(), baseActor.cell.getDescription().c_str()); actorList->addCellChangeActor(baseActor); actorList->sendCellChangeActors(); // Send ActorAI to bring all players in the new cell up to speed with this follower actorList->cell = baseActor.cell; baseActor.aiAction = mwmp::BaseActorList::FOLLOW; baseActor.aiTarget = MechanicsHelper::getTarget(world->getPlayerPtr()); actorList->addAiActor(baseActor); actorList->sendAiActors(); /* End of tes3mp addition */ } } void ActionTeleport::getFollowers(const MWWorld::Ptr& actor, std::set& out, bool includeHostiles) { std::set followers; MWBase::Environment::get().getMechanicsManager()->getActorsFollowing(actor, followers); for(std::set::iterator it = followers.begin();it != followers.end();++it) { MWWorld::Ptr follower = *it; std::string script = follower.getClass().getScript(follower); if (!includeHostiles && follower.getClass().getCreatureStats(follower).getAiSequence().isInCombat(actor)) continue; if (!script.empty() && follower.getRefData().getLocals().getIntVar(script, "stayoutside") == 1) continue; if ((follower.getRefData().getPosition().asVec3() - actor.getRefData().getPosition().asVec3()).length2() > 800 * 800) continue; out.emplace(follower); } } } ================================================ FILE: apps/openmw/mwworld/actionteleport.hpp ================================================ #ifndef GAME_MWWORLD_ACTIONTELEPORT_H #define GAME_MWWORLD_ACTIONTELEPORT_H #include #include #include #include "action.hpp" namespace MWWorld { class ActionTeleport : public Action { std::string mCellName; ESM::Position mPosition; bool mTeleportFollowers; /// Teleports this actor and also teleports anyone following that actor. void executeImp (const Ptr& actor) override; /// Teleports only the given actor (internal use). void teleport(const Ptr &actor); public: /// If cellName is empty, an exterior cell is assumed. /// @param teleportFollowers Whether to teleport any following actors of the target actor as well. ActionTeleport (const std::string& cellName, const ESM::Position& position, bool teleportFollowers); /// @param includeHostiles If true, include hostile followers (which won't actually be teleported) in the output, /// e.g. so that the teleport action can calm them. static void getFollowers(const MWWorld::Ptr& actor, std::set& out, bool includeHostiles = false); }; } #endif ================================================ FILE: apps/openmw/mwworld/actiontrap.cpp ================================================ #include "actiontrap.hpp" /* Start of tes3mp addition Include additional headers for multiplayer purposes */ #include "../mwmp/Main.hpp" #include "../mwmp/Networking.hpp" #include "../mwmp/ObjectList.hpp" /* End of tes3mp addition */ #include "../mwmechanics/spellcasting.hpp" #include "../mwbase/environment.hpp" #include "../mwbase/world.hpp" namespace MWWorld { void ActionTrap::executeImp(const Ptr &actor) { osg::Vec3f actorPosition(actor.getRefData().getPosition().asVec3()); osg::Vec3f trapPosition(mTrapSource.getRefData().getPosition().asVec3()); float trapRange = MWBase::Environment::get().getWorld()->getMaxActivationDistance(); // Note: can't just detonate the trap at the trapped object's location and use the blast // radius, because for most trap spells this is 1 foot, much less than the activation distance. // Using activation distance as the trap range. if (actor == MWBase::Environment::get().getWorld()->getPlayerPtr() && MWBase::Environment::get().getWorld()->getDistanceToFacedObject() > trapRange) // player activated object outside range of trap { MWMechanics::CastSpell cast(mTrapSource, mTrapSource); cast.mHitPosition = trapPosition; cast.cast(mSpellId); } else // player activated object within range of trap, or NPC activated trap { MWMechanics::CastSpell cast(mTrapSource, actor); cast.mHitPosition = actorPosition; cast.cast(mSpellId); } /* Start of tes3mp change (major) Disable unilateral trap disarming on this client and expect the server's reply to our packet to do it instead */ //mTrapSource.getCellRef().setTrap(""); /* End of tes3mp change (major) */ /* Start of tes3mp addition Send an ID_OBJECT_TRAP packet every time a trap is triggered */ mwmp::ObjectList *objectList = mwmp::Main::get().getNetworking()->getObjectList(); objectList->reset(); objectList->packetOrigin = mwmp::CLIENT_GAMEPLAY; ESM::Position pos; if (actor == MWBase::Environment::get().getWorld()->getPlayerPtr() && MWBase::Environment::get().getWorld()->getDistanceToFacedObject() > trapRange) pos = mTrapSource.getRefData().getPosition(); else pos = actor.getRefData().getPosition(); objectList->addObjectTrap(mTrapSource, pos, false); objectList->sendObjectTrap(); /* End of tes3mp addition */ } } ================================================ FILE: apps/openmw/mwworld/actiontrap.hpp ================================================ #ifndef GAME_MWWORLD_ACTIONTRAP_H #define GAME_MWWORLD_ACTIONTRAP_H #include #include "action.hpp" namespace MWWorld { class ActionTrap : public Action { std::string mSpellId; MWWorld::Ptr mTrapSource; void executeImp (const Ptr& actor) override; public: /// @param spellId /// @param trapSource ActionTrap (const std::string& spellId, const Ptr& trapSource) : Action(false, trapSource), mSpellId(spellId), mTrapSource(trapSource) {} }; } #endif ================================================ FILE: apps/openmw/mwworld/cellpreloader.cpp ================================================ #include "cellpreloader.hpp" #include #include #include #include #include #include #include #include #include #include #include #include #include #include "../mwrender/landmanager.hpp" #include "cellstore.hpp" #include "class.hpp" namespace MWWorld { struct ListModelsVisitor { ListModelsVisitor(std::vector& out) : mOut(out) { } virtual bool operator()(const MWWorld::Ptr& ptr) { ptr.getClass().getModelsToPreload(ptr, mOut); return true; } virtual ~ListModelsVisitor() = default; std::vector& mOut; }; /// Worker thread item: preload models in a cell. class PreloadItem : public SceneUtil::WorkItem { public: /// Constructor to be called from the main thread. PreloadItem(MWWorld::CellStore* cell, Resource::SceneManager* sceneManager, Resource::BulletShapeManager* bulletShapeManager, Resource::KeyframeManager* keyframeManager, Terrain::World* terrain, MWRender::LandManager* landManager, bool preloadInstances) : mIsExterior(cell->getCell()->isExterior()) , mX(cell->getCell()->getGridX()) , mY(cell->getCell()->getGridY()) , mSceneManager(sceneManager) , mBulletShapeManager(bulletShapeManager) , mKeyframeManager(keyframeManager) , mTerrain(terrain) , mLandManager(landManager) , mPreloadInstances(preloadInstances) , mAbort(false) { mTerrainView = mTerrain->createView(); ListModelsVisitor visitor (mMeshes); cell->forEach(visitor); } void abort() override { mAbort = true; } /// Preload work to be called from the worker thread. void doWork() override { if (mIsExterior) { try { mTerrain->cacheCell(mTerrainView.get(), mX, mY); mPreloadedObjects.insert(mLandManager->getLand(mX, mY)); } catch(std::exception&) { } } for (std::string& mesh: mMeshes) { if (mAbort) break; try { mesh = Misc::ResourceHelpers::correctActorModelPath(mesh, mSceneManager->getVFS()); bool animated = false; size_t slashpos = mesh.find_last_of("/\\"); if (slashpos != std::string::npos && slashpos != mesh.size()-1) { Misc::StringUtils::lowerCaseInPlace(mesh); if (mesh[slashpos+1] == 'x') { std::string kfname = mesh; if(kfname.size() > 4 && kfname.compare(kfname.size()-4, 4, ".nif") == 0) { kfname.replace(kfname.size()-4, 4, ".kf"); if (mSceneManager->getVFS()->exists(kfname)) { mPreloadedObjects.insert(mKeyframeManager->get(kfname)); animated = true; } } } } if (mPreloadInstances && animated) mPreloadedObjects.insert(mSceneManager->cacheInstance(mesh)); else mPreloadedObjects.insert(mSceneManager->getTemplate(mesh)); if (mPreloadInstances) mPreloadedObjects.insert(mBulletShapeManager->cacheInstance(mesh)); else mPreloadedObjects.insert(mBulletShapeManager->getShape(mesh)); } catch (std::exception&) { // ignore error for now, would spam the log too much // error will be shown when visiting the cell } } } private: typedef std::vector MeshList; bool mIsExterior; int mX; int mY; MeshList mMeshes; Resource::SceneManager* mSceneManager; Resource::BulletShapeManager* mBulletShapeManager; Resource::KeyframeManager* mKeyframeManager; Terrain::World* mTerrain; MWRender::LandManager* mLandManager; bool mPreloadInstances; std::atomic mAbort; osg::ref_ptr mTerrainView; // keep a ref to the loaded objects to make sure it stays loaded as long as this cell is in the preloaded state std::set > mPreloadedObjects; }; class TerrainPreloadItem : public SceneUtil::WorkItem { public: TerrainPreloadItem(const std::vector >& views, Terrain::World* world, const std::vector& preloadPositions) : mAbort(false) , mProgress(views.size()) , mProgressRange(0) , mTerrainViews(views) , mWorld(world) , mPreloadPositions(preloadPositions) { } bool storeViews(double referenceTime) { for (unsigned int i=0; istoreView(mTerrainViews[i], referenceTime)) return false; return true; } void doWork() override { for (unsigned int i=0; ireset(); mWorld->preload(mTerrainViews[i], mPreloadPositions[i].first, mPreloadPositions[i].second, mAbort, mProgress[i], mProgressRange); } } void abort() override { mAbort = true; } int getProgress() const { return !mProgress.empty() ? mProgress[0].load() : 0; } int getProgressRange() const { return !mProgress.empty() && mProgress[0].load() ? mProgressRange : 0; } private: std::atomic mAbort; std::vector> mProgress; int mProgressRange; std::vector > mTerrainViews; Terrain::World* mWorld; std::vector mPreloadPositions; }; /// Worker thread item: update the resource system's cache, effectively deleting unused entries. class UpdateCacheItem : public SceneUtil::WorkItem { public: UpdateCacheItem(Resource::ResourceSystem* resourceSystem, double referenceTime) : mReferenceTime(referenceTime) , mResourceSystem(resourceSystem) { } void doWork() override { mResourceSystem->updateCache(mReferenceTime); } private: double mReferenceTime; Resource::ResourceSystem* mResourceSystem; }; CellPreloader::CellPreloader(Resource::ResourceSystem* resourceSystem, Resource::BulletShapeManager* bulletShapeManager, Terrain::World* terrain, MWRender::LandManager* landManager) : mResourceSystem(resourceSystem) , mBulletShapeManager(bulletShapeManager) , mTerrain(terrain) , mLandManager(landManager) , mExpiryDelay(0.0) , mMinCacheSize(0) , mMaxCacheSize(0) , mPreloadInstances(true) , mLastResourceCacheUpdate(0.0) , mStoreViewsFailCount(0) { } CellPreloader::~CellPreloader() { if (mTerrainPreloadItem) { mTerrainPreloadItem->abort(); mTerrainPreloadItem->waitTillDone(); mTerrainPreloadItem = nullptr; } if (mUpdateCacheItem) { mUpdateCacheItem->waitTillDone(); mUpdateCacheItem = nullptr; } for (PreloadMap::iterator it = mPreloadCells.begin(); it != mPreloadCells.end();++it) it->second.mWorkItem->abort(); for (PreloadMap::iterator it = mPreloadCells.begin(); it != mPreloadCells.end();++it) it->second.mWorkItem->waitTillDone(); mPreloadCells.clear(); } void CellPreloader::preload(CellStore *cell, double timestamp) { if (!mWorkQueue) { Log(Debug::Error) << "Error: can't preload, no work queue set"; return; } if (cell->getState() == CellStore::State_Unloaded) { Log(Debug::Error) << "Error: can't preload objects for unloaded cell"; return; } PreloadMap::iterator found = mPreloadCells.find(cell); if (found != mPreloadCells.end()) { // already preloaded, nothing to do other than updating the timestamp found->second.mTimeStamp = timestamp; return; } while (mPreloadCells.size() >= mMaxCacheSize) { // throw out oldest cell to make room PreloadMap::iterator oldestCell = mPreloadCells.begin(); double oldestTimestamp = std::numeric_limits::max(); double threshold = 1.0; // seconds for (PreloadMap::iterator it = mPreloadCells.begin(); it != mPreloadCells.end(); ++it) { if (it->second.mTimeStamp < oldestTimestamp) { oldestTimestamp = it->second.mTimeStamp; oldestCell = it; } } if (oldestTimestamp + threshold < timestamp) { oldestCell->second.mWorkItem->abort(); mPreloadCells.erase(oldestCell); } else return; } osg::ref_ptr item (new PreloadItem(cell, mResourceSystem->getSceneManager(), mBulletShapeManager, mResourceSystem->getKeyframeManager(), mTerrain, mLandManager, mPreloadInstances)); mWorkQueue->addWorkItem(item); mPreloadCells[cell] = PreloadEntry(timestamp, item); } void CellPreloader::notifyLoaded(CellStore *cell) { PreloadMap::iterator found = mPreloadCells.find(cell); if (found != mPreloadCells.end()) { // do the deletion in the background thread if (found->second.mWorkItem) { found->second.mWorkItem->abort(); mUnrefQueue->push(mPreloadCells[cell].mWorkItem); } mPreloadCells.erase(found); } } void CellPreloader::clear() { for (PreloadMap::iterator it = mPreloadCells.begin(); it != mPreloadCells.end();) { if (it->second.mWorkItem) { it->second.mWorkItem->abort(); mUnrefQueue->push(it->second.mWorkItem); } mPreloadCells.erase(it++); } } void CellPreloader::updateCache(double timestamp) { for (PreloadMap::iterator it = mPreloadCells.begin(); it != mPreloadCells.end();) { if (mPreloadCells.size() >= mMinCacheSize && it->second.mTimeStamp < timestamp - mExpiryDelay) { if (it->second.mWorkItem) { it->second.mWorkItem->abort(); mUnrefQueue->push(it->second.mWorkItem); } mPreloadCells.erase(it++); } else ++it; } if (timestamp - mLastResourceCacheUpdate > 1.0 && (!mUpdateCacheItem || mUpdateCacheItem->isDone())) { // the resource cache is cleared from the worker thread so that we're not holding up the main thread with delete operations mUpdateCacheItem = new UpdateCacheItem(mResourceSystem, timestamp); mWorkQueue->addWorkItem(mUpdateCacheItem, true); mLastResourceCacheUpdate = timestamp; } if (mTerrainPreloadItem && mTerrainPreloadItem->isDone()) { if (!mTerrainPreloadItem->storeViews(timestamp)) { if (++mStoreViewsFailCount > 100) { OSG_ALWAYS << "paging views are rebuilt every frame, please check for faulty enable/disable scripts." << std::endl; mStoreViewsFailCount = 0; } setTerrainPreloadPositions(std::vector()); } else mStoreViewsFailCount = 0; mTerrainPreloadItem = nullptr; } } void CellPreloader::setExpiryDelay(double expiryDelay) { mExpiryDelay = expiryDelay; } void CellPreloader::setMinCacheSize(unsigned int num) { mMinCacheSize = num; } void CellPreloader::setMaxCacheSize(unsigned int num) { mMaxCacheSize = num; } void CellPreloader::setPreloadInstances(bool preload) { mPreloadInstances = preload; } unsigned int CellPreloader::getMaxCacheSize() const { return mMaxCacheSize; } void CellPreloader::setWorkQueue(osg::ref_ptr workQueue) { mWorkQueue = workQueue; } void CellPreloader::setUnrefQueue(SceneUtil::UnrefQueue* unrefQueue) { mUnrefQueue = unrefQueue; } bool CellPreloader::syncTerrainLoad(const std::vector &positions, int& progress, int& progressRange, double timestamp) { if (!mTerrainPreloadItem) return true; else if (mTerrainPreloadItem->isDone()) { if (mTerrainPreloadItem->storeViews(timestamp)) { mTerrainPreloadItem = nullptr; return true; } else { setTerrainPreloadPositions(std::vector()); setTerrainPreloadPositions(positions); return false; } } else { progress = mTerrainPreloadItem->getProgress(); progressRange = mTerrainPreloadItem->getProgressRange(); return false; } } void CellPreloader::abortTerrainPreloadExcept(const CellPreloader::PositionCellGrid *exceptPos) { const float resetThreshold = ESM::Land::REAL_SIZE; for (const auto& pos : mTerrainPreloadPositions) if (exceptPos && (pos.first-exceptPos->first).length2() < resetThreshold*resetThreshold && pos.second == exceptPos->second) return; if (mTerrainPreloadItem && !mTerrainPreloadItem->isDone()) { mTerrainPreloadItem->abort(); mTerrainPreloadItem->waitTillDone(); } setTerrainPreloadPositions(std::vector()); } bool contains(const std::vector& container, const std::vector& contained) { for (const auto& pos : contained) { bool found = false; for (const auto& pos2 : container) { if ((pos.first-pos2.first).length2() < 1 && pos.second == pos2.second) { found = true; break; } } if (!found) return false; } return true; } void CellPreloader::setTerrainPreloadPositions(const std::vector &positions) { if (positions.empty()) mTerrainPreloadPositions.clear(); else if (contains(mTerrainPreloadPositions, positions)) return; if (mTerrainPreloadItem && !mTerrainPreloadItem->isDone()) return; else { if (mTerrainViews.size() > positions.size()) { for (unsigned int i=positions.size(); ipush(mTerrainViews[i]); mTerrainViews.resize(positions.size()); } else if (mTerrainViews.size() < positions.size()) { for (unsigned int i=mTerrainViews.size(); icreateView()); } mTerrainPreloadPositions = positions; if (!positions.empty()) { mTerrainPreloadItem = new TerrainPreloadItem(mTerrainViews, mTerrain, positions); mWorkQueue->addWorkItem(mTerrainPreloadItem); } } } } ================================================ FILE: apps/openmw/mwworld/cellpreloader.hpp ================================================ #ifndef OPENMW_MWWORLD_CELLPRELOADER_H #define OPENMW_MWWORLD_CELLPRELOADER_H #include #include #include #include #include namespace Resource { class ResourceSystem; class BulletShapeManager; } namespace Terrain { class World; class View; } namespace SceneUtil { class UnrefQueue; } namespace MWRender { class LandManager; } namespace MWWorld { class CellStore; class TerrainPreloadItem; class CellPreloader { public: CellPreloader(Resource::ResourceSystem* resourceSystem, Resource::BulletShapeManager* bulletShapeManager, Terrain::World* terrain, MWRender::LandManager* landManager); ~CellPreloader(); /// Ask a background thread to preload rendering meshes and collision shapes for objects in this cell. /// @note The cell itself must be in State_Loaded or State_Preloaded. void preload(MWWorld::CellStore* cell, double timestamp); void notifyLoaded(MWWorld::CellStore* cell); void clear(); /// Removes preloaded cells that have not had a preload request for a while. void updateCache(double timestamp); /// How long to keep a preloaded cell in cache after it's no longer requested. void setExpiryDelay(double expiryDelay); /// The minimum number of preloaded cells before unused cells get thrown out. void setMinCacheSize(unsigned int num); /// The maximum number of preloaded cells. void setMaxCacheSize(unsigned int num); /// Enables the creation of instances in the preloading thread. void setPreloadInstances(bool preload); unsigned int getMaxCacheSize() const; void setWorkQueue(osg::ref_ptr workQueue); void setUnrefQueue(SceneUtil::UnrefQueue* unrefQueue); typedef std::pair PositionCellGrid; void setTerrainPreloadPositions(const std::vector& positions); bool syncTerrainLoad(const std::vector &positions, int& progress, int& progressRange, double timestamp); void abortTerrainPreloadExcept(const PositionCellGrid *exceptPos); private: Resource::ResourceSystem* mResourceSystem; Resource::BulletShapeManager* mBulletShapeManager; Terrain::World* mTerrain; MWRender::LandManager* mLandManager; osg::ref_ptr mWorkQueue; osg::ref_ptr mUnrefQueue; double mExpiryDelay; unsigned int mMinCacheSize; unsigned int mMaxCacheSize; bool mPreloadInstances; double mLastResourceCacheUpdate; int mStoreViewsFailCount; struct PreloadEntry { PreloadEntry(double timestamp, osg::ref_ptr workItem) : mTimeStamp(timestamp) , mWorkItem(workItem) { } PreloadEntry() : mTimeStamp(0.0) { } double mTimeStamp; osg::ref_ptr mWorkItem; }; typedef std::map PreloadMap; // Cells that are currently being preloaded, or have already finished preloading PreloadMap mPreloadCells; std::vector > mTerrainViews; std::vector mTerrainPreloadPositions; osg::ref_ptr mTerrainPreloadItem; osg::ref_ptr mUpdateCacheItem; }; } #endif ================================================ FILE: apps/openmw/mwworld/cellref.cpp ================================================ #include "cellref.hpp" #include namespace MWWorld { const ESM::RefNum& CellRef::getRefNum() const { return mCellRef.mRefNum; } bool CellRef::hasContentFile() const { return mCellRef.mRefNum.hasContentFile(); } void CellRef::unsetRefNum() { mCellRef.mRefNum.unset(); } /* Start of tes3mp addition Set the unique reference number index of a CellRef, needed to make objects retain their uniqueIndex when they are updated after their records are modified on the fly by the server */ void CellRef::setRefNum(unsigned int index) { mCellRef.mRefNum.mIndex = index; } /* End of tes3mp addition */ /* Start of tes3mp addition Get the mMpNum (unique multiplayer number) of a CellRef */ unsigned int CellRef::getMpNum() const { return mCellRef.mMpNum; } /* End of tes3mp addition */ /* Start of tes3mp addition Set the mMpNum (unique multiplayer reference number) of a CellRef */ void CellRef::setMpNum(unsigned int index) { mCellRef.mMpNum = index; } /* End of tes3mp addition */ std::string CellRef::getRefId() const { return mCellRef.mRefID; } const std::string* CellRef::getRefIdPtr() const { return &mCellRef.mRefID; } bool CellRef::getTeleport() const { return mCellRef.mTeleport; } /* Start of tes3mp addition Make it possible to change the teleport state from elsewhere */ void CellRef::setTeleport(bool teleportState) { mCellRef.mTeleport = teleportState; } /* End of tes3mp addition */ ESM::Position CellRef::getDoorDest() const { return mCellRef.mDoorDest; } /* Start of tes3mp addition Make it possible to change the destination position from elsewhere */ void CellRef::setDoorDest(const ESM::Position& position) { mCellRef.mDoorDest = position; } /* End of tes3mp addition */ std::string CellRef::getDestCell() const { return mCellRef.mDestCell; } /* Start of tes3mp addition Make it possible to change the destination cell from elsewhere */ void CellRef::setDestCell(const std::string& cellDescription) { mCellRef.mDestCell = cellDescription; } /* End of tes3mp addition */ float CellRef::getScale() const { return mCellRef.mScale; } void CellRef::setScale(float scale) { if (scale != mCellRef.mScale) { mChanged = true; mCellRef.mScale = scale; } } ESM::Position CellRef::getPosition() const { return mCellRef.mPos; } void CellRef::setPosition(const ESM::Position &position) { mChanged = true; mCellRef.mPos = position; } float CellRef::getEnchantmentCharge() const { return mCellRef.mEnchantmentCharge; } float CellRef::getNormalizedEnchantmentCharge(int maxCharge) const { if (maxCharge == 0) { return 0; } else if (mCellRef.mEnchantmentCharge == -1) { return 1; } else { return mCellRef.mEnchantmentCharge / static_cast(maxCharge); } } void CellRef::setEnchantmentCharge(float charge) { if (charge != mCellRef.mEnchantmentCharge) { mChanged = true; mCellRef.mEnchantmentCharge = charge; } } int CellRef::getCharge() const { return mCellRef.mChargeInt; } void CellRef::setCharge(int charge) { if (charge != mCellRef.mChargeInt) { mChanged = true; mCellRef.mChargeInt = charge; } } void CellRef::applyChargeRemainderToBeSubtracted(float chargeRemainder) { mCellRef.mChargeIntRemainder += std::abs(chargeRemainder); if (mCellRef.mChargeIntRemainder > 1.0f) { float newChargeRemainder = (mCellRef.mChargeIntRemainder - std::floor(mCellRef.mChargeIntRemainder)); if (mCellRef.mChargeInt <= static_cast(mCellRef.mChargeIntRemainder)) { mCellRef.mChargeInt = 0; } else { mCellRef.mChargeInt -= static_cast(mCellRef.mChargeIntRemainder); } mCellRef.mChargeIntRemainder = newChargeRemainder; } } float CellRef::getChargeFloat() const { return mCellRef.mChargeFloat; } void CellRef::setChargeFloat(float charge) { if (charge != mCellRef.mChargeFloat) { mChanged = true; mCellRef.mChargeFloat = charge; } } std::string CellRef::getOwner() const { return mCellRef.mOwner; } std::string CellRef::getGlobalVariable() const { return mCellRef.mGlobalVariable; } void CellRef::resetGlobalVariable() { if (!mCellRef.mGlobalVariable.empty()) { mChanged = true; mCellRef.mGlobalVariable.erase(); } } void CellRef::setFactionRank(int factionRank) { if (factionRank != mCellRef.mFactionRank) { mChanged = true; mCellRef.mFactionRank = factionRank; } } int CellRef::getFactionRank() const { return mCellRef.mFactionRank; } void CellRef::setOwner(const std::string &owner) { if (owner != mCellRef.mOwner) { mChanged = true; mCellRef.mOwner = owner; } } std::string CellRef::getSoul() const { return mCellRef.mSoul; } void CellRef::setSoul(const std::string &soul) { if (soul != mCellRef.mSoul) { mChanged = true; mCellRef.mSoul = soul; } } std::string CellRef::getFaction() const { return mCellRef.mFaction; } void CellRef::setFaction(const std::string &faction) { if (faction != mCellRef.mFaction) { mChanged = true; mCellRef.mFaction = faction; } } int CellRef::getLockLevel() const { return mCellRef.mLockLevel; } void CellRef::setLockLevel(int lockLevel) { if (lockLevel != mCellRef.mLockLevel) { mChanged = true; mCellRef.mLockLevel = lockLevel; } } void CellRef::lock(int lockLevel) { if(lockLevel != 0) setLockLevel(abs(lockLevel)); //Changes lock to locklevel, if positive else setLockLevel(ESM::UnbreakableLock); // If zero, set to max lock level } void CellRef::unlock() { setLockLevel(-abs(mCellRef.mLockLevel)); //Makes lockLevel negative } std::string CellRef::getKey() const { return mCellRef.mKey; } std::string CellRef::getTrap() const { return mCellRef.mTrap; } void CellRef::setTrap(const std::string& trap) { if (trap != mCellRef.mTrap) { mChanged = true; mCellRef.mTrap = trap; } } int CellRef::getGoldValue() const { return mCellRef.mGoldValue; } void CellRef::setGoldValue(int value) { if (value != mCellRef.mGoldValue) { mChanged = true; mCellRef.mGoldValue = value; } } void CellRef::writeState(ESM::ObjectState &state) const { state.mRef = mCellRef; } bool CellRef::hasChanged() const { return mChanged; } } ================================================ FILE: apps/openmw/mwworld/cellref.hpp ================================================ #ifndef OPENMW_MWWORLD_CELLREF_H #define OPENMW_MWWORLD_CELLREF_H #include namespace ESM { struct ObjectState; } namespace MWWorld { /// \brief Encapsulated variant of ESM::CellRef with change tracking class CellRef { public: CellRef (const ESM::CellRef& ref) : mCellRef(ref) { mChanged = false; } // Note: Currently unused for items in containers const ESM::RefNum& getRefNum() const; // Set RefNum to its default state. void unsetRefNum(); /* Start of tes3mp addition Set the unique reference number index of a CellRef, needed to make objects retain their uniqueIndex when they are updated after their records are modified on the fly by the server */ void setRefNum(unsigned int index); /* End of tes3mp addition */ /* Start of tes3mp addition Get the mMpNum (unique multiplayer reference number) of a CellRef */ unsigned int getMpNum() const; /* End of tes3mp addition */ /* Start of tes3mp addition Set the mMpNum (unique multiplayer reference number) of a CellRef */ void setMpNum(unsigned int index); /* End of tes3mp addition */ /// Does the RefNum have a content file? bool hasContentFile() const; // Id of object being referenced std::string getRefId() const; // Pointer to ID of the object being referenced const std::string* getRefIdPtr() const; // For doors - true if this door teleports to somewhere else, false // if it should open through animation. bool getTeleport() const; /* Start of tes3mp addition Make it possible to change the teleport state from elsewhere */ void setTeleport(bool teleportState); /* End of tes3mp addition */ // Teleport location for the door, if this is a teleporting door. ESM::Position getDoorDest() const; /* Start of tes3mp addition Make it possible to change the destination position from elsewhere */ void setDoorDest(const ESM::Position& position); /* End of tes3mp addition */ // Destination cell for doors (optional) std::string getDestCell() const; /* Start of tes3mp addition Make it possible to change the destination cell from elsewhere */ void setDestCell(const std::string& cellDescription); /* End of tes3mp addition */ // Scale applied to mesh float getScale() const; void setScale(float scale); // The *original* position and rotation as it was given in the Construction Set. // Current position and rotation of the object is stored in RefData. ESM::Position getPosition() const; void setPosition (const ESM::Position& position); // Remaining enchantment charge. This could be -1 if the charge was not touched yet (i.e. full). float getEnchantmentCharge() const; // Remaining enchantment charge rescaled to the supplied maximum charge (such as one of the enchantment). float getNormalizedEnchantmentCharge(int maxCharge) const; void setEnchantmentCharge(float charge); // For weapon or armor, this is the remaining item health. // For tools (lockpicks, probes, repair hammer) it is the remaining uses. // If this returns int(-1) it means full health. int getCharge() const; float getChargeFloat() const; // Implemented as union with int charge void setCharge(int charge); void setChargeFloat(float charge); void applyChargeRemainderToBeSubtracted(float chargeRemainder); // Stores remainders and applies if > 1 // The NPC that owns this object (and will get angry if you steal it) std::string getOwner() const; void setOwner(const std::string& owner); // Name of a global variable. If the global variable is set to '1', using the object is temporarily allowed // even if it has an Owner field. // Used by bed rent scripts to allow the player to use the bed for the duration of the rent. std::string getGlobalVariable() const; void resetGlobalVariable(); // ID of creature trapped in this soul gem std::string getSoul() const; void setSoul(const std::string& soul); // The faction that owns this object (and will get angry if // you take it and are not a faction member) std::string getFaction() const; void setFaction (const std::string& faction); // PC faction rank required to use the item. Sometimes is -1, which means "any rank". void setFactionRank(int factionRank); int getFactionRank() const; // Lock level for doors and containers // Positive for a locked door. 0 for a door that was never locked. // For an unlocked door, it is set to -(previous locklevel) int getLockLevel() const; void setLockLevel(int lockLevel); void lock(int lockLevel); void unlock(); // Key and trap ID names, if any std::string getKey() const; std::string getTrap() const; void setTrap(const std::string& trap); // This is 5 for Gold_005 references, 100 for Gold_100 and so on. int getGoldValue() const; void setGoldValue(int value); // Write the content of this CellRef into the given ObjectState void writeState (ESM::ObjectState& state) const; // Has this CellRef changed since it was originally loaded? bool hasChanged() const; private: bool mChanged; ESM::CellRef mCellRef; }; } #endif ================================================ FILE: apps/openmw/mwworld/cellreflist.hpp ================================================ #ifndef GAME_MWWORLD_CELLREFLIST_H #define GAME_MWWORLD_CELLREFLIST_H #include #include "livecellref.hpp" namespace MWWorld { /// \brief Collection of references of one type template struct CellRefList { typedef LiveCellRef LiveRef; typedef std::list List; List mList; /// Search for the given reference in the given reclist from /// ESMStore. Insert the reference into the list if a match is /// found. If not, throw an exception. /// Moved to cpp file, as we require a custom compare operator for it, /// and the build will fail with an ugly three-way cyclic header dependence /// so we need to pass the instantiation of the method to the linker, when /// all methods are known. void load (ESM::CellRef &ref, bool deleted, const MWWorld::ESMStore &esmStore); LiveRef &insert (const LiveRef &item) { mList.push_back(item); return mList.back(); } /// Remove all references with the given refNum from this list. void remove (const ESM::RefNum &refNum) { for (typename List::iterator it = mList.begin(); it != mList.end();) { if (*it == refNum) mList.erase(it++); else ++it; } } }; } #endif ================================================ FILE: apps/openmw/mwworld/cells.cpp ================================================ #include "cells.hpp" #include #include #include #include #include #include #include #include #include "../mwbase/environment.hpp" #include "../mwbase/world.hpp" #include "esmstore.hpp" #include "containerstore.hpp" #include "cellstore.hpp" namespace { template bool forEachInStore(const std::string& id, Visitor&& visitor, std::map& cellStore) { for(auto& cell : cellStore) { if(cell.second.getState() == MWWorld::CellStore::State_Unloaded) cell.second.preload(); if(cell.second.getState() == MWWorld::CellStore::State_Preloaded) { if(cell.second.hasId(id)) { cell.second.load(); } else continue; } bool cont = cell.second.forEach([&] (MWWorld::Ptr ptr) { if(*ptr.getCellRef().getRefIdPtr() == id) { return visitor(ptr); } return true; }); if(!cont) return false; } return true; } struct PtrCollector { std::vector mPtrs; bool operator()(MWWorld::Ptr ptr) { mPtrs.emplace_back(ptr); return true; } }; } MWWorld::CellStore *MWWorld::Cells::getCellStore (const ESM::Cell *cell) { if (cell->mData.mFlags & ESM::Cell::Interior) { std::string lowerName(Misc::StringUtils::lowerCase(cell->mName)); std::map::iterator result = mInteriors.find (lowerName); if (result==mInteriors.end()) { result = mInteriors.insert (std::make_pair (lowerName, CellStore (cell, mStore, mReader))).first; } return &result->second; } else { std::map, CellStore>::iterator result = mExteriors.find (std::make_pair (cell->getGridX(), cell->getGridY())); if (result==mExteriors.end()) { result = mExteriors.insert (std::make_pair ( std::make_pair (cell->getGridX(), cell->getGridY()), CellStore (cell, mStore, mReader))).first; } return &result->second; } } void MWWorld::Cells::clear() { mInteriors.clear(); mExteriors.clear(); std::fill(mIdCache.begin(), mIdCache.end(), std::make_pair("", (MWWorld::CellStore*)nullptr)); mIdCacheIndex = 0; } /* Start of tes3mp addition Make it possible to clear the CellStore for a specific Cell, allowing cells to be replaced from elsewhere in the code */ void MWWorld::Cells::clear(const ESM::Cell& cell) { if (cell.isExterior()) { std::pair cellCoordinates; cellCoordinates = std::make_pair(cell.getGridX(), cell.getGridY()); mExteriors.erase(cellCoordinates); } else if (mInteriors.count(Misc::StringUtils::lowerCase(cell.mName)) > 0) { mInteriors.erase(Misc::StringUtils::lowerCase(cell.mName)); } } /* End of tes3mp addition */ MWWorld::Ptr MWWorld::Cells::getPtrAndCache (const std::string& name, CellStore& cellStore) { Ptr ptr = getPtr (name, cellStore); if (!ptr.isEmpty() && ptr.isInCell()) { mIdCache[mIdCacheIndex].first = name; mIdCache[mIdCacheIndex].second = &cellStore; if (++mIdCacheIndex>=mIdCache.size()) mIdCacheIndex = 0; } return ptr; } void MWWorld::Cells::writeCell (ESM::ESMWriter& writer, CellStore& cell) const { if (cell.getState()!=CellStore::State_Loaded) cell.load (); ESM::CellState cellState; cell.saveState (cellState); writer.startRecord (ESM::REC_CSTA); cellState.mId.save (writer); cellState.save (writer); cell.writeFog(writer); cell.writeReferences (writer); writer.endRecord (ESM::REC_CSTA); } MWWorld::Cells::Cells (const MWWorld::ESMStore& store, std::vector& reader) : mStore (store), mReader (reader), mIdCacheIndex (0) { int cacheSize = std::clamp(Settings::Manager::getInt("pointers cache size", "Cells"), 40, 1000); mIdCache = IdCache(cacheSize, std::pair ("", (CellStore*)nullptr)); } MWWorld::CellStore *MWWorld::Cells::getExterior (int x, int y) { std::map, CellStore>::iterator result = mExteriors.find (std::make_pair (x, y)); if (result==mExteriors.end()) { const ESM::Cell *cell = mStore.get().search(x, y); if (!cell) { // Cell isn't predefined. Make one on the fly. ESM::Cell record; record.mCellId.mWorldspace = ESM::CellId::sDefaultWorldspace; record.mCellId.mPaged = true; record.mCellId.mIndex.mX = x; record.mCellId.mIndex.mY = y; record.mData.mFlags = ESM::Cell::HasWater; record.mData.mX = x; record.mData.mY = y; record.mWater = 0; record.mMapColor = 0; cell = MWBase::Environment::get().getWorld()->createRecord (record); } result = mExteriors.insert (std::make_pair ( std::make_pair (x, y), CellStore (cell, mStore, mReader))).first; } if (result->second.getState()!=CellStore::State_Loaded) { result->second.load (); } return &result->second; } MWWorld::CellStore *MWWorld::Cells::getInterior (const std::string& name) { std::string lowerName = Misc::StringUtils::lowerCase(name); std::map::iterator result = mInteriors.find (lowerName); if (result==mInteriors.end()) { const ESM::Cell *cell = mStore.get().find(lowerName); result = mInteriors.insert (std::make_pair (lowerName, CellStore (cell, mStore, mReader))).first; } if (result->second.getState()!=CellStore::State_Loaded) { result->second.load (); } return &result->second; } void MWWorld::Cells::rest (double hours) { for (auto &interior : mInteriors) { interior.second.rest(hours); } for (auto &exterior : mExteriors) { exterior.second.rest(hours); } } void MWWorld::Cells::recharge (float duration) { for (auto &interior : mInteriors) { interior.second.recharge(duration); } for (auto &exterior : mExteriors) { exterior.second.recharge(duration); } } MWWorld::CellStore *MWWorld::Cells::getCell (const ESM::CellId& id) { if (id.mPaged) return getExterior (id.mIndex.mX, id.mIndex.mY); return getInterior (id.mWorldspace); } MWWorld::Ptr MWWorld::Cells::getPtr (const std::string& name, CellStore& cell, bool searchInContainers) { if (cell.getState()==CellStore::State_Unloaded) cell.preload (); if (cell.getState()==CellStore::State_Preloaded) { if (cell.hasId (name)) { cell.load (); } else return Ptr(); } Ptr ptr = cell.search (name); if (!ptr.isEmpty() && MWWorld::CellStore::isAccessible(ptr.getRefData(), ptr.getCellRef())) return ptr; if (searchInContainers) return cell.searchInContainer (name); return Ptr(); } MWWorld::Ptr MWWorld::Cells::getPtr (const std::string& name) { // First check the cache for (IdCache::iterator iter (mIdCache.begin()); iter!=mIdCache.end(); ++iter) if (iter->first==name && iter->second) { Ptr ptr = getPtr (name, *iter->second); if (!ptr.isEmpty()) return ptr; } // Then check cells that are already listed // Search in reverse, this is a workaround for an ambiguous chargen_plank reference in the vanilla game. // there is one at -22,16 and one at -2,-9, the latter should be used. for (std::map, CellStore>::reverse_iterator iter = mExteriors.rbegin(); iter!=mExteriors.rend(); ++iter) { Ptr ptr = getPtrAndCache (name, iter->second); if (!ptr.isEmpty()) return ptr; } for (std::map::iterator iter = mInteriors.begin(); iter!=mInteriors.end(); ++iter) { Ptr ptr = getPtrAndCache (name, iter->second); if (!ptr.isEmpty()) return ptr; } // Now try the other cells const MWWorld::Store &cells = mStore.get(); MWWorld::Store::iterator iter; for (iter = cells.extBegin(); iter != cells.extEnd(); ++iter) { CellStore *cellStore = getCellStore (&(*iter)); Ptr ptr = getPtrAndCache (name, *cellStore); if (!ptr.isEmpty()) return ptr; } for (iter = cells.intBegin(); iter != cells.intEnd(); ++iter) { CellStore *cellStore = getCellStore (&(*iter)); Ptr ptr = getPtrAndCache (name, *cellStore); if (!ptr.isEmpty()) return ptr; } // giving up return Ptr(); } MWWorld::Ptr MWWorld::Cells::getPtr (const std::string& id, const ESM::RefNum& refNum) { for (auto& pair : mInteriors) { Ptr ptr = getPtr(pair.second, id, refNum); if (!ptr.isEmpty()) return ptr; } for (auto& pair : mExteriors) { Ptr ptr = getPtr(pair.second, id, refNum); if (!ptr.isEmpty()) return ptr; } return Ptr(); } MWWorld::Ptr MWWorld::Cells::getPtr(CellStore& cellStore, const std::string& id, const ESM::RefNum& refNum) { if (cellStore.getState() == CellStore::State_Unloaded) cellStore.preload(); if (cellStore.getState() == CellStore::State_Preloaded) { if (cellStore.hasId(id)) cellStore.load(); else return Ptr(); } return cellStore.searchViaRefNum(refNum); } void MWWorld::Cells::getExteriorPtrs(const std::string &name, std::vector &out) { const MWWorld::Store &cells = mStore.get(); for (MWWorld::Store::iterator iter = cells.extBegin(); iter != cells.extEnd(); ++iter) { CellStore *cellStore = getCellStore (&(*iter)); Ptr ptr = getPtrAndCache (name, *cellStore); if (!ptr.isEmpty()) out.push_back(ptr); } } void MWWorld::Cells::getInteriorPtrs(const std::string &name, std::vector &out) { const MWWorld::Store &cells = mStore.get(); for (MWWorld::Store::iterator iter = cells.intBegin(); iter != cells.intEnd(); ++iter) { CellStore *cellStore = getCellStore (&(*iter)); Ptr ptr = getPtrAndCache (name, *cellStore); if (!ptr.isEmpty()) out.push_back(ptr); } } std::vector MWWorld::Cells::getAll(const std::string& id) { PtrCollector visitor; if(forEachInStore(id, visitor, mInteriors)) forEachInStore(id, visitor, mExteriors); return visitor.mPtrs; } int MWWorld::Cells::countSavedGameRecords() const { int count = 0; for (std::map::const_iterator iter (mInteriors.begin()); iter!=mInteriors.end(); ++iter) if (iter->second.hasState()) ++count; for (std::map, CellStore>::const_iterator iter (mExteriors.begin()); iter!=mExteriors.end(); ++iter) if (iter->second.hasState()) ++count; return count; } void MWWorld::Cells::write (ESM::ESMWriter& writer, Loading::Listener& progress) const { for (std::map, CellStore>::iterator iter (mExteriors.begin()); iter!=mExteriors.end(); ++iter) if (iter->second.hasState()) { writeCell (writer, iter->second); progress.increaseProgress(); } for (std::map::iterator iter (mInteriors.begin()); iter!=mInteriors.end(); ++iter) if (iter->second.hasState()) { writeCell (writer, iter->second); progress.increaseProgress(); } } struct GetCellStoreCallback : public MWWorld::CellStore::GetCellStoreCallback { public: GetCellStoreCallback(MWWorld::Cells& cells) : mCells(cells) { } MWWorld::Cells& mCells; MWWorld::CellStore* getCellStore(const ESM::CellId& cellId) override { try { return mCells.getCell(cellId); } catch (...) { return nullptr; } } }; bool MWWorld::Cells::readRecord (ESM::ESMReader& reader, uint32_t type, const std::map& contentFileMap) { if (type==ESM::REC_CSTA) { ESM::CellState state; state.mId.load (reader); CellStore *cellStore = nullptr; try { cellStore = getCell (state.mId); } catch (...) { // silently drop cells that don't exist anymore Log(Debug::Warning) << "Warning: Dropping state for cell " << state.mId.mWorldspace << " (cell no longer exists)"; reader.skipRecord(); return true; } state.load (reader); cellStore->loadState (state); if (state.mHasFogOfWar) cellStore->readFog(reader); if (cellStore->getState()!=CellStore::State_Loaded) cellStore->load (); GetCellStoreCallback callback(*this); cellStore->readReferences (reader, contentFileMap, &callback); return true; } return false; } ================================================ FILE: apps/openmw/mwworld/cells.hpp ================================================ #ifndef GAME_MWWORLD_CELLS_H #define GAME_MWWORLD_CELLS_H #include #include #include #include "ptr.hpp" namespace ESM { class ESMReader; class ESMWriter; struct CellId; struct Cell; struct RefNum; } namespace Loading { class Listener; } namespace MWWorld { class ESMStore; /// \brief Cell container class Cells { typedef std::vector > IdCache; const MWWorld::ESMStore& mStore; std::vector& mReader; mutable std::map mInteriors; mutable std::map, CellStore> mExteriors; IdCache mIdCache; std::size_t mIdCacheIndex; Cells (const Cells&); Cells& operator= (const Cells&); CellStore *getCellStore (const ESM::Cell *cell); Ptr getPtrAndCache (const std::string& name, CellStore& cellStore); Ptr getPtr(CellStore& cellStore, const std::string& id, const ESM::RefNum& refNum); void writeCell (ESM::ESMWriter& writer, CellStore& cell) const; public: void clear(); /* Start of tes3mp addition Make it possible to clear the CellStore for a specific Cell, allowing cells to be replaced from elsewhere in the code */ void clear(const ESM::Cell& cell); /* End of tes3mp addition */ Cells (const MWWorld::ESMStore& store, std::vector& reader); CellStore *getExterior (int x, int y); CellStore *getInterior (const std::string& name); CellStore *getCell (const ESM::CellId& id); Ptr getPtr (const std::string& name, CellStore& cellStore, bool searchInContainers = false); ///< \param searchInContainers Only affect loaded cells. /// @note name must be lower case /// @note name must be lower case Ptr getPtr (const std::string& name); Ptr getPtr(const std::string& id, const ESM::RefNum& refNum); void rest (double hours); void recharge (float duration); /// Get all Ptrs referencing \a name in exterior cells /// @note Due to the current implementation of getPtr this only supports one Ptr per cell. /// @note name must be lower case void getExteriorPtrs (const std::string& name, std::vector& out); /// Get all Ptrs referencing \a name in interior cells /// @note Due to the current implementation of getPtr this only supports one Ptr per cell. /// @note name must be lower case void getInteriorPtrs (const std::string& name, std::vector& out); std::vector getAll(const std::string& id); int countSavedGameRecords() const; void write (ESM::ESMWriter& writer, Loading::Listener& progress) const; bool readRecord (ESM::ESMReader& reader, uint32_t type, const std::map& contentFileMap); }; } #endif ================================================ FILE: apps/openmw/mwworld/cellstore.cpp ================================================ #include "cellstore.hpp" #include /* Start of tes3mp addition Include additional headers for multiplayer purposes */ #include #include "../mwmp/Main.hpp" #include "../mwmp/Networking.hpp" #include "../mwmp/CellController.hpp" /* End of tes3mp addition */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include "../mwbase/environment.hpp" #include "../mwbase/mechanicsmanager.hpp" #include "../mwbase/world.hpp" #include "../mwmechanics/creaturestats.hpp" #include "../mwmechanics/recharge.hpp" #include "ptr.hpp" #include "esmstore.hpp" #include "class.hpp" #include "containerstore.hpp" namespace { template MWWorld::Ptr searchInContainerList (MWWorld::CellRefList& containerList, const std::string& id) { for (typename MWWorld::CellRefList::List::iterator iter (containerList.mList.begin()); iter!=containerList.mList.end(); ++iter) { MWWorld::Ptr container (&*iter, nullptr); if (container.getRefData().getCustomData() == nullptr) continue; MWWorld::Ptr ptr = container.getClass().getContainerStore (container).search (id); if (!ptr.isEmpty()) return ptr; } return MWWorld::Ptr(); } template MWWorld::Ptr searchViaActorId (MWWorld::CellRefList& actorList, int actorId, MWWorld::CellStore *cell, const std::map& toIgnore) { for (typename MWWorld::CellRefList::List::iterator iter (actorList.mList.begin()); iter!=actorList.mList.end(); ++iter) { MWWorld::Ptr actor (&*iter, cell); if (toIgnore.find(&*iter) != toIgnore.end()) continue; if (actor.getClass().getCreatureStats (actor).matchesActorId (actorId) && actor.getRefData().getCount() > 0) return actor; } return MWWorld::Ptr(); } template void writeReferenceCollection (ESM::ESMWriter& writer, const MWWorld::CellRefList& collection) { if (!collection.mList.empty()) { // references for (typename MWWorld::CellRefList::List::const_iterator iter (collection.mList.begin()); iter!=collection.mList.end(); ++iter) { if (!iter->mData.hasChanged() && !iter->mRef.hasChanged() && iter->mRef.hasContentFile()) { // Reference that came from a content file and has not been changed -> ignore continue; } if (iter->mData.getCount()==0 && !iter->mRef.hasContentFile()) { // Deleted reference that did not come from a content file -> ignore continue; } RecordType state; iter->save (state); // recordId currently unused writer.writeHNT ("OBJE", collection.mList.front().mBase->sRecordId); state.save (writer); } } } template void fixRestockingImpl(const T* base, RecordType& state) { // Workaround for old saves not containing negative quantities for(const auto& baseItem : base->mInventory.mList) { if(baseItem.mCount < 0) { for(auto& item : state.mInventory.mItems) { if(item.mCount > 0 && Misc::StringUtils::ciEqual(baseItem.mItem, item.mRef.mRefID)) item.mCount = -item.mCount; } } } } template void fixRestocking(const T* base, RecordType& state) {} template<> void fixRestocking<>(const ESM::Creature* base, ESM::CreatureState& state) { fixRestockingImpl(base, state); } template<> void fixRestocking<>(const ESM::NPC* base, ESM::NpcState& state) { fixRestockingImpl(base, state); } template<> void fixRestocking<>(const ESM::Container* base, ESM::ContainerState& state) { fixRestockingImpl(base, state); } template void readReferenceCollection (ESM::ESMReader& reader, MWWorld::CellRefList& collection, const ESM::CellRef& cref, const std::map& contentFileMap, MWWorld::CellStore* cellstore) { const MWWorld::ESMStore& esmStore = MWBase::Environment::get().getWorld()->getStore(); RecordType state; state.mRef = cref; state.load(reader); // If the reference came from a content file, make sure this content file is loaded if (state.mRef.mRefNum.hasContentFile()) { std::map::const_iterator iter = contentFileMap.find (state.mRef.mRefNum.mContentFile); if (iter==contentFileMap.end()) return; // content file has been removed -> skip state.mRef.mRefNum.mContentFile = iter->second; } if (!MWWorld::LiveCellRef::checkState (state)) return; // not valid anymore with current content files -> skip const T *record = esmStore.get().search (state.mRef.mRefID); if (!record) return; if (state.mVersion < 15) fixRestocking(record, state); if (state.mRef.mRefNum.hasContentFile()) { for (typename MWWorld::CellRefList::List::iterator iter (collection.mList.begin()); iter!=collection.mList.end(); ++iter) if (iter->mRef.getRefNum()==state.mRef.mRefNum && *iter->mRef.getRefIdPtr() == state.mRef.mRefID) { // overwrite existing reference float oldscale = iter->mRef.getScale(); iter->load (state); const ESM::Position & oldpos = iter->mRef.getPosition(); const ESM::Position & newpos = iter->mData.getPosition(); const MWWorld::Ptr ptr(&*iter, cellstore); if ((oldscale != iter->mRef.getScale() || oldpos.asVec3() != newpos.asVec3() || oldpos.rot[0] != newpos.rot[0] || oldpos.rot[1] != newpos.rot[1] || oldpos.rot[2] != newpos.rot[2]) && !ptr.getClass().isActor()) MWBase::Environment::get().getWorld()->moveObject(ptr, newpos.pos[0], newpos.pos[1], newpos.pos[2]); if (!iter->mData.isEnabled()) { iter->mData.enable(); MWBase::Environment::get().getWorld()->disable(MWWorld::Ptr(&*iter, cellstore)); } return; } Log(Debug::Warning) << "Warning: Dropping reference to " << state.mRef.mRefID << " (invalid content file link)"; return; } // new reference MWWorld::LiveCellRef ref (record); ref.load (state); collection.mList.push_back (ref); } } namespace MWWorld { template void CellRefList::load(ESM::CellRef &ref, bool deleted, const MWWorld::ESMStore &esmStore) { const MWWorld::Store &store = esmStore.get(); if (const X *ptr = store.search (ref.mRefID)) { typename std::list::iterator iter = std::find(mList.begin(), mList.end(), ref.mRefNum); LiveRef liveCellRef (ref, ptr); if (deleted) liveCellRef.mData.setDeletedByContentFile(true); if (iter != mList.end()) *iter = liveCellRef; else mList.push_back (liveCellRef); } else { Log(Debug::Warning) << "Warning: could not resolve cell reference '" << ref.mRefID << "'" << " (dropping reference)"; } } template bool operator==(const LiveCellRef& ref, int pRefnum) { return (ref.mRef.mRefnum == pRefnum); } Ptr CellStore::getCurrentPtr(LiveCellRefBase *ref) { MovedRefTracker::iterator found = mMovedToAnotherCell.find(ref); if (found != mMovedToAnotherCell.end()) return Ptr(ref, found->second); return Ptr(ref, this); } void CellStore::moveFrom(const Ptr &object, CellStore *from) { if (mState != State_Loaded) load(); mHasState = true; MovedRefTracker::iterator found = mMovedToAnotherCell.find(object.getBase()); if (found != mMovedToAnotherCell.end()) { // A cell we had previously moved an object to is returning it to us. /* Start of tes3mp addition Add extra debug for multiplayer purposes */ if (found->second != from) { LOG_MESSAGE_SIMPLE(TimedLog::LOG_ERROR, "Storage: %s owned %s which it gave to %s which isn't %s, which should result in a crash\n", this->getCell()->getDescription().c_str(), object.getBase()->mRef.getRefId().c_str(), found->second->getCell()->getDescription().c_str(), from->getCell()->getDescription().c_str()); } /* End of tes3mp addition */ assert (found->second == from); mMovedToAnotherCell.erase(found); } else { mMovedHere.insert(std::make_pair(object.getBase(), from)); } updateMergedRefs(); } MWWorld::Ptr CellStore::moveTo(const Ptr &object, CellStore *cellToMoveTo) { if (cellToMoveTo == this) throw std::runtime_error("moveTo: object is already in this cell"); // We assume that *this is in State_Loaded since we could hardly have reference to a live object otherwise. if (mState != State_Loaded) throw std::runtime_error("moveTo: can't move object from a non-loaded cell (how did you get this object anyway?)"); // Ensure that the object actually exists in the cell if (searchViaRefNum(object.getCellRef().getRefNum()).isEmpty()) throw std::runtime_error("moveTo: object is not in this cell"); // Objects with no refnum can't be handled correctly in the merging process that happens // on a save/load, so do a simple copy & delete for these objects. /* Start of tes3mp change (major) Disable the following code because it breaks DedicatedPlayers */ /* if (!object.getCellRef().getRefNum().hasContentFile()) { MWWorld::Ptr copied = object.getClass().copyToCell(object, *cellToMoveTo, object.getRefData().getCount()); object.getRefData().setCount(0); object.getRefData().setBaseNode(nullptr); return copied; } */ /* End of tes3mp change (major) */ MovedRefTracker::iterator found = mMovedHere.find(object.getBase()); if (found != mMovedHere.end()) { // Special case - object didn't originate in this cell // Move it back to its original cell first CellStore* originalCell = found->second; assert (originalCell != this); originalCell->moveFrom(object, this); mMovedHere.erase(found); // Now that object is back to its rightful owner, we can move it if (cellToMoveTo != originalCell) { /* Start of tes3mp addition Add extra debug for multiplayer purposes */ LOG_MESSAGE_SIMPLE(TimedLog::LOG_INFO, "Storage: %s's original cell %s gives it from %s to %s\n", object.getBase()->mRef.getRefId().c_str(), originalCell->getCell()->getDescription().c_str(), this->getCell()->getDescription().c_str(), cellToMoveTo->getCell()->getDescription().c_str()); /* End of tes3mp addition */ originalCell->moveTo(object, cellToMoveTo); } updateMergedRefs(); return MWWorld::Ptr(object.getBase(), cellToMoveTo); } cellToMoveTo->moveFrom(object, this); mMovedToAnotherCell.insert(std::make_pair(object.getBase(), cellToMoveTo)); updateMergedRefs(); return MWWorld::Ptr(object.getBase(), cellToMoveTo); } /* Start of tes3mp addition Make it possible to clear the moves to other cells tracked for objects, allowing for on-the-fly cell resets that don't cause crashes */ void CellStore::clearMovesToCells() { MWBase::World* world = MWBase::Environment::get().getWorld(); for (auto &reference : mMovedHere) { MWWorld::CellStore *otherCell = reference.second; otherCell->mMovedToAnotherCell.erase(reference.first); } for (auto &reference : mMovedToAnotherCell) { MWWorld::CellStore *otherCell = reference.second; otherCell->mMovedHere.erase(reference.first); } mMovedHere.empty(); mMovedToAnotherCell.empty(); } /* End of tes3mp addition */ struct MergeVisitor { MergeVisitor(std::vector& mergeTo, const std::map& movedHere, const std::map& movedToAnotherCell) : mMergeTo(mergeTo) , mMovedHere(movedHere) , mMovedToAnotherCell(movedToAnotherCell) { } bool operator() (const MWWorld::Ptr& ptr) { if (mMovedToAnotherCell.find(ptr.getBase()) != mMovedToAnotherCell.end()) return true; mMergeTo.push_back(ptr.getBase()); return true; } void merge() { for (const auto & [base, _] : mMovedHere) mMergeTo.push_back(base); } private: std::vector& mMergeTo; const std::map& mMovedHere; const std::map& mMovedToAnotherCell; }; void CellStore::updateMergedRefs() { mMergedRefs.clear(); mRechargingItemsUpToDate = false; MergeVisitor visitor(mMergedRefs, mMovedHere, mMovedToAnotherCell); forEachInternal(visitor); visitor.merge(); /* Start of tes3mp addition If the mwmp::Cell corresponding to this CellStore is under the authority of the LocalPlayer, prepare a new initialization of LocalActors in it Warning: Don't directly use initializeLocalActors() from here because that will break any current cell transition that started in World::moveObject() */ if (mwmp::Main::get().getCellController()->hasLocalAuthority(*getCell())) { mwmp::Main::get().getCellController()->getCell(*getCell())->shouldInitializeActors = true; } /* End of tes3mp addition */ } bool CellStore::movedHere(const MWWorld::Ptr& ptr) const { if (ptr.isEmpty()) return false; if (mMovedHere.find(ptr.getBase()) != mMovedHere.end()) return true; return false; } CellStore::CellStore (const ESM::Cell *cell, const MWWorld::ESMStore& esmStore, std::vector& readerList) : mStore(esmStore), mReader(readerList), mCell (cell), mState (State_Unloaded), mHasState (false), mLastRespawn(0,0), mRechargingItemsUpToDate(false) { mWaterLevel = cell->mWater; } const ESM::Cell *CellStore::getCell() const { return mCell; } CellStore::State CellStore::getState() const { return mState; } const std::vector &CellStore::getPreloadedIds() const { return mIds; } bool CellStore::hasState() const { return mHasState; } bool CellStore::hasId (const std::string& id) const { if (mState==State_Unloaded) return false; if (mState==State_Preloaded) return std::binary_search (mIds.begin(), mIds.end(), id); return searchConst (id).isEmpty(); } template struct SearchVisitor { PtrType mFound; const std::string *mIdToFind; bool operator()(const PtrType& ptr) { if (*ptr.getCellRef().getRefIdPtr() == *mIdToFind) { mFound = ptr; return false; } return true; } }; Ptr CellStore::search (const std::string& id) { SearchVisitor searchVisitor; searchVisitor.mIdToFind = &id; forEach(searchVisitor); return searchVisitor.mFound; } ConstPtr CellStore::searchConst (const std::string& id) const { SearchVisitor searchVisitor; searchVisitor.mIdToFind = &id; forEachConst(searchVisitor); return searchVisitor.mFound; } Ptr CellStore::searchViaActorId (int id) { if (Ptr ptr = ::searchViaActorId (mNpcs, id, this, mMovedToAnotherCell)) return ptr; if (Ptr ptr = ::searchViaActorId (mCreatures, id, this, mMovedToAnotherCell)) return ptr; for (const auto& [base, _] : mMovedHere) { MWWorld::Ptr actor (base, this); if (!actor.getClass().isActor()) continue; if (actor.getClass().getCreatureStats (actor).matchesActorId (id) && actor.getRefData().getCount() > 0) return actor; } return Ptr(); } class RefNumSearchVisitor { const ESM::RefNum& mRefNum; public: RefNumSearchVisitor(const ESM::RefNum& refNum) : mRefNum(refNum) {} Ptr mFound; bool operator()(const Ptr& ptr) { if (ptr.getCellRef().getRefNum() == mRefNum) { mFound = ptr; return false; } return true; } }; Ptr CellStore::searchViaRefNum(const ESM::RefNum& refNum) { RefNumSearchVisitor searchVisitor(refNum); forEach(searchVisitor); return searchVisitor.mFound; } /* Start of tes3mp addition A custom type of search visitor used to find objects by their reference numbers */ class SearchExactVisitor { const unsigned int mRefNumToFind; const unsigned int mMpNumToFind; const std::string mRefIdToFind; const bool mActorsOnly; public: SearchExactVisitor(const unsigned int refNum, const unsigned int mpNum, const std::string refId, const bool actorsOnly) : mRefNumToFind(refNum), mMpNumToFind(mpNum), mRefIdToFind(refId), mActorsOnly(actorsOnly) {} Ptr mFound; bool operator()(const Ptr& ptr) { if (ptr.getCellRef().getRefNum().mIndex == mRefNumToFind && ptr.getCellRef().getMpNum() == mMpNumToFind) { if (!mActorsOnly || ptr.getClass().isActor()) { if (mRefIdToFind.empty() || Misc::StringUtils::ciEqual(ptr.getCellRef().getRefId(), mRefIdToFind)) { mFound = ptr; return false; } } } return true; } }; /* End of tes3mp addition */ /* Start of tes3mp addition Allow the searching of objects by their reference numbers */ Ptr CellStore::searchExact (const unsigned int refNum, const unsigned int mpNum, const std::string refId, bool actorsOnly) { // Ensure that all objects searched for have a valid reference number if (refNum == 0 && mpNum == 0) return 0; SearchExactVisitor searchVisitor(refNum, mpNum, refId, actorsOnly); forEach(searchVisitor); return searchVisitor.mFound; } /* End of tes3mp addition */ /* Start of tes3mp addition Make it possible to get the mMergedRefs in the CellStore from elsewhere in the code */ std::vector &CellStore::getMergedRefs() { return mMergedRefs; } /* End of tes3mp addition */ /* Start of tes3mp addition Make it possible to get the mNPCs in the CellStore from elsewhere in the code */ CellRefList *CellStore::getNpcs() { return &mNpcs; } /* End of tes3mp addition */ /* Start of tes3mp addition Make it possible to get the mCreatures in the CellStore from elsewhere in the code */ CellRefList *CellStore::getCreatures() { return &mCreatures; } /* End of tes3mp addition */ /* Start of tes3mp addition Make it possible to get the mCreatureLists in the CellStore from elsewhere in the code */ CellRefList *CellStore::getCreatureLists() { return &mCreatureLists; } /* End of tes3mp addition */ /* Start of tes3mp addition Make it possible to get the mContainers in the CellStore from elsewhere in the code */ CellRefList *CellStore::getContainers() { return &mContainers; } /* End of tes3mp addition */ float CellStore::getWaterLevel() const { if (isExterior()) return -1; return mWaterLevel; } void CellStore::setWaterLevel (float level) { mWaterLevel = level; mHasState = true; } std::size_t CellStore::count() const { return mMergedRefs.size(); } void CellStore::load () { if (mState!=State_Loaded) { if (mState==State_Preloaded) mIds.clear(); loadRefs (); mState = State_Loaded; } } void CellStore::preload () { if (mState==State_Unloaded) { listRefs (); mState = State_Preloaded; } } void CellStore::listRefs() { std::vector& esm = mReader; assert (mCell); if (mCell->mContextList.empty()) return; // this is a dynamically generated cell -> skipping. // Load references from all plugins that do something with this cell. for (size_t i = 0; i < mCell->mContextList.size(); i++) { try { // Reopen the ESM reader and seek to the right position. int index = mCell->mContextList[i].index; mCell->restore (esm[index], i); ESM::CellRef ref; // Get each reference in turn bool deleted = false; while (mCell->getNextRef (esm[index], ref, deleted)) { if (deleted) continue; // Don't list reference if it was moved to a different cell. ESM::MovedCellRefTracker::const_iterator iter = std::find(mCell->mMovedRefs.begin(), mCell->mMovedRefs.end(), ref.mRefNum); if (iter != mCell->mMovedRefs.end()) { continue; } Misc::StringUtils::lowerCaseInPlace(ref.mRefID); mIds.push_back(std::move(ref.mRefID)); } } catch (std::exception& e) { Log(Debug::Error) << "An error occurred listing references for cell " << getCell()->getDescription() << ": " << e.what(); } } // List moved references, from separately tracked list. for (const auto& [ref, deleted]: mCell->mLeasedRefs) { if (!deleted) mIds.push_back(Misc::StringUtils::lowerCase(ref.mRefID)); } std::sort (mIds.begin(), mIds.end()); } void CellStore::loadRefs() { std::vector& esm = mReader; assert (mCell); if (mCell->mContextList.empty()) return; // this is a dynamically generated cell -> skipping. std::map refNumToID; // used to detect refID modifications // Load references from all plugins that do something with this cell. for (size_t i = 0; i < mCell->mContextList.size(); i++) { try { // Reopen the ESM reader and seek to the right position. int index = mCell->mContextList[i].index; mCell->restore (esm[index], i); ESM::CellRef ref; ref.mRefNum.mContentFile = ESM::RefNum::RefNum_NoContentFile; // Get each reference in turn bool deleted = false; while(mCell->getNextRef(esm[index], ref, deleted)) { // Don't load reference if it was moved to a different cell. ESM::MovedCellRefTracker::const_iterator iter = std::find(mCell->mMovedRefs.begin(), mCell->mMovedRefs.end(), ref.mRefNum); if (iter != mCell->mMovedRefs.end()) { continue; } loadRef (ref, deleted, refNumToID); } } catch (std::exception& e) { Log(Debug::Error) << "An error occurred loading references for cell " << getCell()->getDescription() << ": " << e.what(); } } // Load moved references, from separately tracked list. for (const auto& leasedRef : mCell->mLeasedRefs) { ESM::CellRef &ref = const_cast(leasedRef.first); bool deleted = leasedRef.second; loadRef (ref, deleted, refNumToID); } updateMergedRefs(); } bool CellStore::isExterior() const { return mCell->isExterior(); } Ptr CellStore::searchInContainer (const std::string& id) { bool oldState = mHasState; mHasState = true; if (Ptr ptr = searchInContainerList (mContainers, id)) return ptr; if (Ptr ptr = searchInContainerList (mCreatures, id)) return ptr; if (Ptr ptr = searchInContainerList (mNpcs, id)) return ptr; mHasState = oldState; return Ptr(); } void CellStore::loadRef (ESM::CellRef& ref, bool deleted, std::map& refNumToID) { Misc::StringUtils::lowerCaseInPlace (ref.mRefID); const MWWorld::ESMStore& store = mStore; std::map::iterator it = refNumToID.find(ref.mRefNum); if (it != refNumToID.end()) { if (it->second != ref.mRefID) { // refID was modified, make sure we don't end up with duplicated refs switch (store.find(it->second)) { case ESM::REC_ACTI: mActivators.remove(ref.mRefNum); break; case ESM::REC_ALCH: mPotions.remove(ref.mRefNum); break; case ESM::REC_APPA: mAppas.remove(ref.mRefNum); break; case ESM::REC_ARMO: mArmors.remove(ref.mRefNum); break; case ESM::REC_BOOK: mBooks.remove(ref.mRefNum); break; case ESM::REC_CLOT: mClothes.remove(ref.mRefNum); break; case ESM::REC_CONT: mContainers.remove(ref.mRefNum); break; case ESM::REC_CREA: mCreatures.remove(ref.mRefNum); break; case ESM::REC_DOOR: mDoors.remove(ref.mRefNum); break; case ESM::REC_INGR: mIngreds.remove(ref.mRefNum); break; case ESM::REC_LEVC: mCreatureLists.remove(ref.mRefNum); break; case ESM::REC_LEVI: mItemLists.remove(ref.mRefNum); break; case ESM::REC_LIGH: mLights.remove(ref.mRefNum); break; case ESM::REC_LOCK: mLockpicks.remove(ref.mRefNum); break; case ESM::REC_MISC: mMiscItems.remove(ref.mRefNum); break; case ESM::REC_NPC_: mNpcs.remove(ref.mRefNum); break; case ESM::REC_PROB: mProbes.remove(ref.mRefNum); break; case ESM::REC_REPA: mRepairs.remove(ref.mRefNum); break; case ESM::REC_STAT: mStatics.remove(ref.mRefNum); break; case ESM::REC_WEAP: mWeapons.remove(ref.mRefNum); break; case ESM::REC_BODY: mBodyParts.remove(ref.mRefNum); break; default: break; } } } switch (store.find (ref.mRefID)) { case ESM::REC_ACTI: mActivators.load(ref, deleted, store); break; case ESM::REC_ALCH: mPotions.load(ref, deleted,store); break; case ESM::REC_APPA: mAppas.load(ref, deleted, store); break; case ESM::REC_ARMO: mArmors.load(ref, deleted, store); break; case ESM::REC_BOOK: mBooks.load(ref, deleted, store); break; case ESM::REC_CLOT: mClothes.load(ref, deleted, store); break; case ESM::REC_CONT: mContainers.load(ref, deleted, store); break; case ESM::REC_CREA: mCreatures.load(ref, deleted, store); break; case ESM::REC_DOOR: mDoors.load(ref, deleted, store); break; case ESM::REC_INGR: mIngreds.load(ref, deleted, store); break; case ESM::REC_LEVC: mCreatureLists.load(ref, deleted, store); break; case ESM::REC_LEVI: mItemLists.load(ref, deleted, store); break; case ESM::REC_LIGH: mLights.load(ref, deleted, store); break; case ESM::REC_LOCK: mLockpicks.load(ref, deleted, store); break; case ESM::REC_MISC: mMiscItems.load(ref, deleted, store); break; 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_WEAP: mWeapons.load(ref, deleted, store); break; case ESM::REC_BODY: mBodyParts.load(ref, deleted, store); break; case 0: Log(Debug::Error) << "Cell reference '" + ref.mRefID + "' not found!"; return; default: Log(Debug::Error) << "Error: Ignoring reference '" << ref.mRefID << "' of unhandled type"; return; } refNumToID[ref.mRefNum] = ref.mRefID; } void CellStore::loadState (const ESM::CellState& state) { mHasState = true; if (mCell->mData.mFlags & ESM::Cell::Interior && mCell->mData.mFlags & ESM::Cell::HasWater) mWaterLevel = state.mWaterLevel; mLastRespawn = MWWorld::TimeStamp(state.mLastRespawn); } void CellStore::saveState (ESM::CellState& state) const { state.mId = mCell->getCellId(); if (mCell->mData.mFlags & ESM::Cell::Interior && mCell->mData.mFlags & ESM::Cell::HasWater) state.mWaterLevel = mWaterLevel; state.mHasFogOfWar = (mFogState.get() ? 1 : 0); state.mLastRespawn = mLastRespawn.toEsm(); } void CellStore::writeFog(ESM::ESMWriter &writer) const { if (mFogState.get()) { mFogState->save(writer, mCell->mData.mFlags & ESM::Cell::Interior); } } void CellStore::readFog(ESM::ESMReader &reader) { mFogState.reset(new ESM::FogState()); mFogState->load(reader); } void CellStore::writeReferences (ESM::ESMWriter& writer) const { writeReferenceCollection (writer, mActivators); writeReferenceCollection (writer, mPotions); writeReferenceCollection (writer, mAppas); writeReferenceCollection (writer, mArmors); writeReferenceCollection (writer, mBooks); writeReferenceCollection (writer, mClothes); writeReferenceCollection (writer, mContainers); writeReferenceCollection (writer, mCreatures); writeReferenceCollection (writer, mDoors); writeReferenceCollection (writer, mIngreds); writeReferenceCollection (writer, mCreatureLists); writeReferenceCollection (writer, mItemLists); writeReferenceCollection (writer, mLights); writeReferenceCollection (writer, mLockpicks); writeReferenceCollection (writer, mMiscItems); writeReferenceCollection (writer, mNpcs); writeReferenceCollection (writer, mProbes); writeReferenceCollection (writer, mRepairs); writeReferenceCollection (writer, mStatics); writeReferenceCollection (writer, mWeapons); writeReferenceCollection (writer, mBodyParts); for (const auto& [base, store] : mMovedToAnotherCell) { ESM::RefNum refNum = base->mRef.getRefNum(); ESM::CellId movedTo = store->getCell()->getCellId(); refNum.save(writer, true, "MVRF"); movedTo.save(writer); } } void CellStore::readReferences (ESM::ESMReader& reader, const std::map& contentFileMap, GetCellStoreCallback* callback) { mHasState = true; while (reader.isNextSub ("OBJE")) { unsigned int unused; reader.getHT (unused); // load the RefID first so we know what type of object it is ESM::CellRef cref; cref.loadId(reader, true); int type = MWBase::Environment::get().getWorld()->getStore().find(cref.mRefID); if (type == 0) { Log(Debug::Warning) << "Dropping reference to '" << cref.mRefID << "' (object no longer exists)"; reader.skipHSubUntil("OBJE"); continue; } switch (type) { case ESM::REC_ACTI: readReferenceCollection (reader, mActivators, cref, contentFileMap, this); break; case ESM::REC_ALCH: readReferenceCollection (reader, mPotions, cref, contentFileMap, this); break; case ESM::REC_APPA: readReferenceCollection (reader, mAppas, cref, contentFileMap, this); break; case ESM::REC_ARMO: readReferenceCollection (reader, mArmors, cref, contentFileMap, this); break; case ESM::REC_BOOK: readReferenceCollection (reader, mBooks, cref, contentFileMap, this); break; case ESM::REC_CLOT: readReferenceCollection (reader, mClothes, cref, contentFileMap, this); break; case ESM::REC_CONT: readReferenceCollection (reader, mContainers, cref, contentFileMap, this); break; case ESM::REC_CREA: readReferenceCollection (reader, mCreatures, cref, contentFileMap, this); break; case ESM::REC_DOOR: readReferenceCollection (reader, mDoors, cref, contentFileMap, this); break; case ESM::REC_INGR: readReferenceCollection (reader, mIngreds, cref, contentFileMap, this); break; case ESM::REC_LEVC: readReferenceCollection (reader, mCreatureLists, cref, contentFileMap, this); break; case ESM::REC_LEVI: readReferenceCollection (reader, mItemLists, cref, contentFileMap, this); break; case ESM::REC_LIGH: readReferenceCollection (reader, mLights, cref, contentFileMap, this); break; case ESM::REC_LOCK: readReferenceCollection (reader, mLockpicks, cref, contentFileMap, this); break; case ESM::REC_MISC: readReferenceCollection (reader, mMiscItems, cref, contentFileMap, this); break; case ESM::REC_NPC_: readReferenceCollection (reader, mNpcs, cref, contentFileMap, this); break; case ESM::REC_PROB: readReferenceCollection (reader, mProbes, cref, contentFileMap, this); break; case ESM::REC_REPA: readReferenceCollection (reader, mRepairs, cref, contentFileMap, this); break; case ESM::REC_STAT: readReferenceCollection (reader, mStatics, cref, contentFileMap, this); break; case ESM::REC_WEAP: readReferenceCollection (reader, mWeapons, cref, contentFileMap, this); break; case ESM::REC_BODY: readReferenceCollection (reader, mBodyParts, cref, contentFileMap, this); break; default: throw std::runtime_error ("unknown type in cell reference section"); } } // Do another update here to make sure objects referred to by MVRF tags can be found // This update is only needed for old saves that used the old copy&delete way of moving objects updateMergedRefs(); while (reader.isNextSub("MVRF")) { reader.cacheSubName(); ESM::RefNum refnum; ESM::CellId movedTo; refnum.load(reader, true, "MVRF"); movedTo.load(reader); if (refnum.hasContentFile()) { auto iter = contentFileMap.find(refnum.mContentFile); if (iter != contentFileMap.end()) refnum.mContentFile = iter->second; } // Search for the reference. It might no longer exist if its content file was removed. Ptr movedRef = searchViaRefNum(refnum); if (movedRef.isEmpty()) { Log(Debug::Warning) << "Warning: Dropping moved ref tag for " << refnum.mIndex << " (moved object no longer exists)"; continue; } CellStore* otherCell = callback->getCellStore(movedTo); if (otherCell == nullptr) { Log(Debug::Warning) << "Warning: Dropping moved ref tag for " << movedRef.getCellRef().getRefId() << " (target cell " << movedTo.mWorldspace << " no longer exists). Reference moved back to its original location."; // Note by dropping tag the object will automatically re-appear in its original cell, though potentially at inapproriate coordinates. // Restore original coordinates: movedRef.getRefData().setPosition(movedRef.getCellRef().getPosition()); continue; } if (otherCell == this) { // Should never happen unless someone's tampering with files. Log(Debug::Warning) << "Found invalid moved ref, ignoring"; continue; } moveTo(movedRef, otherCell); } } bool operator== (const CellStore& left, const CellStore& right) { return left.getCell()->getCellId()==right.getCell()->getCellId(); } bool operator!= (const CellStore& left, const CellStore& right) { return !(left==right); } void CellStore::setFog(ESM::FogState *fog) { mFogState.reset(fog); } ESM::FogState* CellStore::getFog() const { return mFogState.get(); } void clearCorpse(const MWWorld::Ptr& ptr) { const MWMechanics::CreatureStats& creatureStats = ptr.getClass().getCreatureStats(ptr); static const float fCorpseClearDelay = MWBase::Environment::get().getWorld()->getStore().get().find("fCorpseClearDelay")->mValue.getFloat(); if (creatureStats.isDead() && creatureStats.isDeathAnimationFinished() && !ptr.getClass().isPersistent(ptr) && creatureStats.getTimeOfDeath() + fCorpseClearDelay <= MWBase::Environment::get().getWorld()->getTimeStamp()) { MWBase::Environment::get().getWorld()->deleteObject(ptr); } } void CellStore::rest(double hours) { if (mState == State_Loaded) { for (CellRefList::List::iterator it (mCreatures.mList.begin()); it!=mCreatures.mList.end(); ++it) { Ptr ptr = getCurrentPtr(&*it); if (!ptr.isEmpty() && ptr.getRefData().getCount() > 0) { MWBase::Environment::get().getMechanicsManager()->restoreDynamicStats(ptr, hours, true); } } for (CellRefList::List::iterator it (mNpcs.mList.begin()); it!=mNpcs.mList.end(); ++it) { Ptr ptr = getCurrentPtr(&*it); if (!ptr.isEmpty() && ptr.getRefData().getCount() > 0) { MWBase::Environment::get().getMechanicsManager()->restoreDynamicStats(ptr, hours, true); } } } } void CellStore::recharge(float duration) { if (duration <= 0) return; if (mState == State_Loaded) { for (CellRefList::List::iterator it (mCreatures.mList.begin()); it!=mCreatures.mList.end(); ++it) { Ptr ptr = getCurrentPtr(&*it); if (!ptr.isEmpty() && ptr.getRefData().getCount() > 0) { ptr.getClass().getContainerStore(ptr).rechargeItems(duration); } } for (CellRefList::List::iterator it (mNpcs.mList.begin()); it!=mNpcs.mList.end(); ++it) { Ptr ptr = getCurrentPtr(&*it); if (!ptr.isEmpty() && ptr.getRefData().getCount() > 0) { ptr.getClass().getContainerStore(ptr).rechargeItems(duration); } } for (CellRefList::List::iterator it (mContainers.mList.begin()); it!=mContainers.mList.end(); ++it) { Ptr ptr = getCurrentPtr(&*it); if (!ptr.isEmpty() && ptr.getRefData().getCustomData() != nullptr && ptr.getRefData().getCount() > 0 && ptr.getClass().getContainerStore(ptr).isResolved()) { ptr.getClass().getContainerStore(ptr).rechargeItems(duration); } } rechargeItems(duration); } } void CellStore::respawn() { if (mState == State_Loaded) { static const int iMonthsToRespawn = MWBase::Environment::get().getWorld()->getStore().get().find("iMonthsToRespawn")->mValue.getInteger(); if (MWBase::Environment::get().getWorld()->getTimeStamp() - mLastRespawn > 24*30*iMonthsToRespawn) { mLastRespawn = MWBase::Environment::get().getWorld()->getTimeStamp(); for (CellRefList::List::iterator it (mContainers.mList.begin()); it!=mContainers.mList.end(); ++it) { Ptr ptr = getCurrentPtr(&*it); ptr.getClass().respawn(ptr); } } for (CellRefList::List::iterator it (mCreatures.mList.begin()); it!=mCreatures.mList.end(); ++it) { Ptr ptr = getCurrentPtr(&*it); clearCorpse(ptr); ptr.getClass().respawn(ptr); } for (CellRefList::List::iterator it (mNpcs.mList.begin()); it!=mNpcs.mList.end(); ++it) { Ptr ptr = getCurrentPtr(&*it); clearCorpse(ptr); ptr.getClass().respawn(ptr); } for (CellRefList::List::iterator it (mCreatureLists.mList.begin()); it!=mCreatureLists.mList.end(); ++it) { Ptr ptr = getCurrentPtr(&*it); // no need to clearCorpse, handled as part of mCreatures ptr.getClass().respawn(ptr); } } } void MWWorld::CellStore::rechargeItems(float duration) { if (!mRechargingItemsUpToDate) { updateRechargingItems(); mRechargingItemsUpToDate = true; } for (const auto& [item, charge] : mRechargingItems) { MWMechanics::rechargeItem(item, charge, duration); } } void MWWorld::CellStore::updateRechargingItems() { mRechargingItems.clear(); const auto update = [this](auto& list) { for (auto & item : list) { Ptr ptr = getCurrentPtr(&item); if (!ptr.isEmpty() && ptr.getRefData().getCount() > 0) { checkItem(ptr); } } }; update(mWeapons.mList); update(mArmors.mList); update(mClothes.mList); update(mBooks.mList); } void MWWorld::CellStore::checkItem(Ptr ptr) { if (ptr.getClass().getEnchantment(ptr).empty()) return; std::string enchantmentId = ptr.getClass().getEnchantment(ptr); const ESM::Enchantment* enchantment = MWBase::Environment::get().getWorld()->getStore().get().search(enchantmentId); if (!enchantment) { Log(Debug::Warning) << "Warning: Can't find enchantment '" << enchantmentId << "' on item " << ptr.getCellRef().getRefId(); return; } if (enchantment->mData.mType == ESM::Enchantment::WhenUsed || enchantment->mData.mType == ESM::Enchantment::WhenStrikes) mRechargingItems.emplace_back(ptr.getBase(), static_cast(enchantment->mData.mCharge)); } } ================================================ FILE: apps/openmw/mwworld/cellstore.hpp ================================================ #ifndef GAME_MWWORLD_CELLSTORE_H #define GAME_MWWORLD_CELLSTORE_H #include #include #include #include #include #include #include "livecellref.hpp" #include "cellreflist.hpp" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "timestamp.hpp" #include "ptr.hpp" namespace ESM { struct Cell; struct CellState; struct FogState; struct CellId; struct RefNum; } namespace MWWorld { class ESMStore; /// \brief Mutable state of a cell class CellStore { public: enum State { State_Unloaded, State_Preloaded, State_Loaded }; private: const MWWorld::ESMStore& mStore; std::vector& mReader; // Even though fog actually belongs to the player and not cells, // it makes sense to store it here since we need it once for each cell. // Note this is nullptr until the cell is explored to save some memory std::shared_ptr mFogState; const ESM::Cell *mCell; State mState; bool mHasState; std::vector mIds; float mWaterLevel; MWWorld::TimeStamp mLastRespawn; // List of refs owned by this cell CellRefList mActivators; CellRefList mPotions; CellRefList mAppas; CellRefList mArmors; CellRefList mBooks; CellRefList mClothes; CellRefList mContainers; CellRefList mCreatures; CellRefList mDoors; CellRefList mIngreds; CellRefList mCreatureLists; CellRefList mItemLists; CellRefList mLights; CellRefList mLockpicks; CellRefList mMiscItems; CellRefList mNpcs; CellRefList mProbes; CellRefList mRepairs; CellRefList mStatics; CellRefList mWeapons; CellRefList mBodyParts; typedef std::map MovedRefTracker; // References owned by a different cell that have been moved here. // MovedRefTracker mMovedHere; // References owned by this cell that have been moved to another cell. // MovedRefTracker mMovedToAnotherCell; // Merged list of ref's currently in this cell - i.e. with added refs from mMovedHere, removed refs from mMovedToAnotherCell std::vector mMergedRefs; // Get the Ptr for the given ref which originated from this cell (possibly moved to another cell at this point). Ptr getCurrentPtr(MWWorld::LiveCellRefBase* ref); /// Moves object from the given cell to this cell. void moveFrom(const MWWorld::Ptr& object, MWWorld::CellStore* from); /// Repopulate mMergedRefs. void updateMergedRefs(); // (item, max charge) typedef std::vector > TRechargingItems; TRechargingItems mRechargingItems; bool mRechargingItemsUpToDate; void updateRechargingItems(); void rechargeItems(float duration); void checkItem(Ptr ptr); // helper function for forEachInternal template bool forEachImp (Visitor& visitor, List& list) { for (typename List::List::iterator iter (list.mList.begin()); iter!=list.mList.end(); ++iter) { if (!isAccessible(iter->mData, iter->mRef)) continue; if (!visitor (MWWorld::Ptr(&*iter, this))) return false; } return true; } // listing only objects owned by this cell. Internal use only, you probably want to use forEach() so that moved objects are accounted for. template bool forEachInternal (Visitor& visitor) { return forEachImp (visitor, mActivators) && forEachImp (visitor, mPotions) && forEachImp (visitor, mAppas) && forEachImp (visitor, mArmors) && forEachImp (visitor, mBooks) && forEachImp (visitor, mClothes) && forEachImp (visitor, mContainers) && forEachImp (visitor, mDoors) && forEachImp (visitor, mIngreds) && forEachImp (visitor, mItemLists) && forEachImp (visitor, mLights) && forEachImp (visitor, mLockpicks) && forEachImp (visitor, mMiscItems) && forEachImp (visitor, mProbes) && forEachImp (visitor, mRepairs) && forEachImp (visitor, mStatics) && forEachImp (visitor, mWeapons) && forEachImp (visitor, mBodyParts) && forEachImp (visitor, mCreatures) && forEachImp (visitor, mNpcs) && forEachImp (visitor, mCreatureLists); } /// @note If you get a linker error here, this means the given type can not be stored in a cell. The supported types are /// defined at the bottom of this file. template CellRefList& get(); public: /// Should this reference be accessible to the outside world (i.e. to scripts / game logic)? /// Determined based on the deletion flags. By default, objects deleted by content files are never accessible; /// objects deleted by setCount(0) are still accessible *if* they came from a content file (needed for vanilla /// scripting compatibility, and the fact that objects may be "un-deleted" in the original game). static bool isAccessible(const MWWorld::RefData& refdata, const MWWorld::CellRef& cref) { return !refdata.isDeletedByContentFile() && (cref.hasContentFile() || refdata.getCount() > 0); } /// Moves object from this cell to the given cell. /// @note automatically updates given cell by calling cellToMoveTo->moveFrom(...) /// @note throws exception if cellToMoveTo == this /// @return updated MWWorld::Ptr with the new CellStore pointer set. MWWorld::Ptr moveTo(const MWWorld::Ptr& object, MWWorld::CellStore* cellToMoveTo); /* Start of tes3mp addition Make it possible to clear the moves to other cells tracked for objects, allowing for on-the-fly cell resets that don't cause crashes */ void clearMovesToCells(); /* End of tes3mp addition */ void rest(double hours); void recharge(float duration); /// Make a copy of the given object and insert it into this cell. /// @note If you get a linker error here, this means the given type can not be inserted into a cell. /// The supported types are defined at the bottom of this file. template LiveCellRefBase* insert(const LiveCellRef* ref) { mHasState = true; CellRefList& list = get(); LiveCellRefBase* ret = &list.insert(*ref); updateMergedRefs(); return ret; } /// @param readerList The readers to use for loading of the cell on-demand. CellStore (const ESM::Cell *cell_, const MWWorld::ESMStore& store, std::vector& readerList); const ESM::Cell *getCell() const; State getState() const; const std::vector& getPreloadedIds() const; ///< Get Ids of objects in this cell, only valid in State_Preloaded bool hasState() const; ///< Does this cell have state that needs to be stored in a saved game file? bool hasId (const std::string& id) const; ///< May return true for deleted IDs when in preload state. Will return false, if cell is /// unloaded. /// @note Will not account for moved references which may exist in Loaded state. Use search() instead if the cell is loaded. Ptr search (const std::string& id); ///< Will return an empty Ptr if cell is not loaded. Does not check references in /// containers. /// @note Triggers CellStore hasState flag. ConstPtr searchConst (const std::string& id) const; ///< Will return an empty Ptr if cell is not loaded. Does not check references in /// containers. /// @note Does not trigger CellStore hasState flag. Ptr searchViaActorId (int id); ///< Will return an empty Ptr if cell is not loaded. Ptr searchViaRefNum (const ESM::RefNum& refNum); ///< Will return an empty Ptr if cell is not loaded. Does not check references in /// containers. /// @note Triggers CellStore hasState flag. /* Start of tes3mp addition Allow the searching of objects by their reference numbers and, optionally, their refIds */ Ptr searchExact (unsigned int refNum, unsigned int mpNum, std::string refId = "", bool actorsOnly = false); /* End of tes3mp addition */ /* Start of tes3mp addition Make it possible to get the mMergedRefs in the CellStore from elsewhere in the code */ std::vector &getMergedRefs(); /* End of tes3mp addition */ /* Start of tes3mp addition Make it possible to get the mNPCs in the CellStore from elsewhere in the code */ CellRefList *getNpcs(); /* End of tes3mp addition */ /* Start of tes3mp addition Make it possible to get the mCreatures in the CellStore from elsewhere in the code */ CellRefList *getCreatures(); /* End of tes3mp addition */ /* Start of tes3mp addition Make it possible to get the mCreatureLists in the CellStore from elsewhere in the code */ CellRefList *getCreatureLists(); /* End of tes3mp addition */ /* Start of tes3mp addition Make it possible to get the mContainers in the CellStore from elsewhere in the code */ CellRefList *getContainers(); /* End of tes3mp addition */ float getWaterLevel() const; bool movedHere(const MWWorld::Ptr& ptr) const; void setWaterLevel (float level); void setFog (ESM::FogState* fog); ///< \note Takes ownership of the pointer ESM::FogState* getFog () const; std::size_t count() const; ///< Return total number of references, including deleted ones. void load (); ///< Load references from content file. void preload (); ///< Build ID list from content file. /// Call visitor (MWWorld::Ptr) for each reference. visitor must return a bool. Returning /// false will abort the iteration. /// \note Prefer using forEachConst when possible. /// \note Do not modify this cell (i.e. remove/add objects) during the forEach, doing this may result in unintended behaviour. /// \attention This function also lists deleted (count 0) objects! /// \return Iteration completed? template bool forEach (Visitor&& visitor) { if (mState != State_Loaded) return false; if (mMergedRefs.empty()) return true; mHasState = true; for (unsigned int i=0; imData, mMergedRefs[i]->mRef)) continue; if (!visitor(MWWorld::Ptr(mMergedRefs[i], this))) return false; } return true; } /// Call visitor (MWWorld::ConstPtr) for each reference. visitor must return a bool. Returning /// false will abort the iteration. /// \note Do not modify this cell (i.e. remove/add objects) during the forEach, doing this may result in unintended behaviour. /// \attention This function also lists deleted (count 0) objects! /// \return Iteration completed? template bool forEachConst (Visitor&& visitor) const { if (mState != State_Loaded) return false; for (unsigned int i=0; imData, mMergedRefs[i]->mRef)) continue; if (!visitor(MWWorld::ConstPtr(mMergedRefs[i], this))) return false; } return true; } /// Call visitor (ref) for each reference of given type. visitor must return a bool. Returning /// false will abort the iteration. /// \note Do not modify this cell (i.e. remove/add objects) during the forEach, doing this may result in unintended behaviour. /// \attention This function also lists deleted (count 0) objects! /// \return Iteration completed? template bool forEachType(Visitor& visitor) { if (mState != State_Loaded) return false; if (mMergedRefs.empty()) return true; mHasState = true; CellRefList& list = get(); for (typename CellRefList::List::iterator it (list.mList.begin()); it!=list.mList.end(); ++it) { LiveCellRefBase* base = &*it; if (mMovedToAnotherCell.find(base) != mMovedToAnotherCell.end()) continue; if (!isAccessible(base->mData, base->mRef)) continue; if (!visitor(MWWorld::Ptr(base, this))) return false; } for (MovedRefTracker::const_iterator it = mMovedHere.begin(); it != mMovedHere.end(); ++it) { LiveCellRefBase* base = it->first; if (dynamic_cast*>(base)) if (!visitor(MWWorld::Ptr(base, this))) return false; } return true; } // NOTE: does not account for moved references // Should be phased out when we have const version of forEach inline const CellRefList& getReadOnlyDoors() const { return mDoors; } inline const CellRefList& getReadOnlyStatics() const { return mStatics; } bool isExterior() const; Ptr searchInContainer (const std::string& id); void loadState (const ESM::CellState& state); void saveState (ESM::CellState& state) const; void writeFog (ESM::ESMWriter& writer) const; void readFog (ESM::ESMReader& reader); void writeReferences (ESM::ESMWriter& writer) const; struct GetCellStoreCallback { public: ///@note must return nullptr if the cell is not found virtual CellStore* getCellStore(const ESM::CellId& cellId) = 0; virtual ~GetCellStoreCallback() = default; }; /// @param callback to use for retrieving of additional CellStore objects by ID (required for resolving moved references) void readReferences (ESM::ESMReader& reader, const std::map& contentFileMap, GetCellStoreCallback* callback); void respawn (); ///< Check mLastRespawn and respawn references if necessary. This is a no-op if the cell is not loaded. private: /// Run through references and store IDs void listRefs(); void loadRefs(); void loadRef (ESM::CellRef& ref, bool deleted, std::map& refNumToID); ///< Make case-adjustments to \a ref and insert it into the respective container. /// /// Invalid \a ref objects are silently dropped. }; template<> inline CellRefList& CellStore::get() { mHasState = true; return mActivators; } template<> inline CellRefList& CellStore::get() { mHasState = true; return mPotions; } template<> inline CellRefList& CellStore::get() { mHasState = true; return mAppas; } template<> inline CellRefList& CellStore::get() { mHasState = true; return mArmors; } template<> inline CellRefList& CellStore::get() { mHasState = true; return mBooks; } template<> inline CellRefList& CellStore::get() { mHasState = true; return mClothes; } template<> inline CellRefList& CellStore::get() { mHasState = true; return mContainers; } template<> inline CellRefList& CellStore::get() { mHasState = true; return mCreatures; } template<> inline CellRefList& CellStore::get() { mHasState = true; return mDoors; } template<> inline CellRefList& CellStore::get() { mHasState = true; return mIngreds; } template<> inline CellRefList& CellStore::get() { mHasState = true; return mCreatureLists; } template<> inline CellRefList& CellStore::get() { mHasState = true; return mItemLists; } template<> inline CellRefList& CellStore::get() { mHasState = true; return mLights; } template<> inline CellRefList& CellStore::get() { mHasState = true; return mLockpicks; } template<> inline CellRefList& CellStore::get() { mHasState = true; return mMiscItems; } template<> inline CellRefList& CellStore::get() { mHasState = true; return mNpcs; } template<> inline CellRefList& CellStore::get() { mHasState = true; return mProbes; } template<> inline CellRefList& CellStore::get() { mHasState = true; return mRepairs; } template<> inline CellRefList& CellStore::get() { mHasState = true; return mStatics; } template<> inline CellRefList& CellStore::get() { mHasState = true; return mWeapons; } template<> inline CellRefList& CellStore::get() { mHasState = true; return mBodyParts; } bool operator== (const CellStore& left, const CellStore& right); bool operator!= (const CellStore& left, const CellStore& right); } #endif ================================================ FILE: apps/openmw/mwworld/cellvisitors.hpp ================================================ #ifndef GAME_MWWORLD_CELLVISITORS_H #define GAME_MWWORLD_CELLVISITORS_H #include #include #include "ptr.hpp" namespace MWWorld { struct ListAndResetObjectsVisitor { std::vector mObjects; bool operator() (MWWorld::Ptr ptr) { if (ptr.getRefData().getBaseNode()) { ptr.getRefData().setBaseNode(nullptr); mObjects.push_back (ptr); } return true; } }; } #endif ================================================ FILE: apps/openmw/mwworld/class.cpp ================================================ #include "class.hpp" #include /* Start of tes3mp addition Include additional headers for multiplayer purposes */ #include /* End of tes3mp addition */ #include #include "../mwbase/environment.hpp" #include "../mwbase/windowmanager.hpp" #include "../mwbase/world.hpp" #include "../mwworld/esmstore.hpp" #include "ptr.hpp" #include "refdata.hpp" #include "nullaction.hpp" #include "failedaction.hpp" #include "actiontake.hpp" #include "containerstore.hpp" #include "../mwgui/tooltips.hpp" #include "../mwmechanics/creaturestats.hpp" #include "../mwmechanics/npcstats.hpp" namespace MWWorld { std::map > Class::sClasses; Class::Class() {} Class::~Class() {} void Class::insertObjectRendering (const Ptr& ptr, const std::string& mesh, MWRender::RenderingInterface& renderingInterface) const { } void Class::insertObject(const Ptr& ptr, const std::string& mesh, MWPhysics::PhysicsSystem& physics) const { } bool Class::apply (const MWWorld::Ptr& ptr, const std::string& id, const MWWorld::Ptr& actor) const { return false; } void Class::skillUsageSucceeded (const MWWorld::Ptr& ptr, int skill, int usageType, float extraFactor) const { throw std::runtime_error ("class does not represent an actor"); } bool Class::canSell (const MWWorld::ConstPtr& item, int npcServices) const { return false; } int Class::getServices(const ConstPtr &actor) const { throw std::runtime_error ("class does not have services"); } MWMechanics::CreatureStats& Class::getCreatureStats (const Ptr& ptr) const { /* Start of tes3mp addition This is a common error in multiplayer, so additional logging has been added for it */ LOG_MESSAGE_SIMPLE(TimedLog::LOG_ERROR, "Attempt at getting creatureStats for %s %i-%i which is a %s!", ptr.getCellRef().getRefId().c_str(), ptr.getCellRef().getRefNum().mIndex, ptr.getCellRef().getMpNum(), ptr.getClass().getTypeName().c_str()); /* End of tes3mp addition */ throw std::runtime_error ("class does not have creature stats"); } MWMechanics::NpcStats& Class::getNpcStats (const Ptr& ptr) const { throw std::runtime_error ("class does not have NPC stats"); } bool Class::hasItemHealth (const ConstPtr& ptr) const { return false; } int Class::getItemHealth(const ConstPtr &ptr) const { if (ptr.getCellRef().getCharge() == -1) return getItemMaxHealth(ptr); else return ptr.getCellRef().getCharge(); } float Class::getItemNormalizedHealth (const ConstPtr& ptr) const { if (getItemMaxHealth(ptr) == 0) { return 0.f; } else { return getItemHealth(ptr) / static_cast(getItemMaxHealth(ptr)); } } int Class::getItemMaxHealth (const ConstPtr& ptr) const { throw std::runtime_error ("class does not have item health"); } void Class::hit(const Ptr& ptr, float attackStrength, int type) const { throw std::runtime_error("class cannot hit"); } void Class::block(const Ptr &ptr) const { throw std::runtime_error("class cannot block"); } void Class::onHit(const Ptr& ptr, float damage, bool ishealth, const Ptr& object, const Ptr& attacker, const osg::Vec3f& hitPosition, bool successful) const { throw std::runtime_error("class cannot be hit"); } std::shared_ptr Class::activate (const Ptr& ptr, const Ptr& actor) const { return std::shared_ptr (new NullAction); } std::shared_ptr Class::use (const Ptr& ptr, bool force) const { return std::shared_ptr (new NullAction); } ContainerStore& Class::getContainerStore (const Ptr& ptr) const { throw std::runtime_error ("class does not have a container store"); } InventoryStore& Class::getInventoryStore (const Ptr& ptr) const { throw std::runtime_error ("class does not have an inventory store"); } /* Start of tes3mp addition Make it possible to check whether a class has a container store */ bool Class::hasContainerStore(const Ptr &ptr) const { return false; } /* End of tes3mp addition */ bool Class::hasInventoryStore(const Ptr &ptr) const { return false; } /* Start of tes3mp addition Make it possible to check whether a class can be harvested */ bool Class::canBeHarvested(const ConstPtr& ptr) const { return false; } /* End of tes3mp addition */ bool Class::canLock(const ConstPtr &ptr) const { return false; } void Class::setRemainingUsageTime (const Ptr& ptr, float duration) const { throw std::runtime_error ("class does not support time-based uses"); } float Class::getRemainingUsageTime (const ConstPtr& ptr) const { return -1; } std::string Class::getScript (const ConstPtr& ptr) const { return ""; } float Class::getMaxSpeed (const Ptr& ptr) const { return 0; } float Class::getCurrentSpeed (const Ptr& ptr) const { return 0; } float Class::getJump (const Ptr& ptr) const { return 0; } int Class::getEnchantmentPoints (const MWWorld::ConstPtr& ptr) const { throw std::runtime_error ("class does not support enchanting"); } MWMechanics::Movement& Class::getMovementSettings (const Ptr& ptr) const { throw std::runtime_error ("movement settings not supported by class"); } osg::Vec3f Class::getRotationVector (const Ptr& ptr) const { return osg::Vec3f (0, 0, 0); } std::pair, bool> Class::getEquipmentSlots (const ConstPtr& ptr) const { return std::make_pair (std::vector(), false); } int Class::getEquipmentSkill (const ConstPtr& ptr) const { return -1; } int Class::getValue (const ConstPtr& ptr) const { throw std::logic_error ("value not supported by this class"); } float Class::getCapacity (const MWWorld::Ptr& ptr) const { throw std::runtime_error ("capacity not supported by this class"); } float Class::getWeight(const ConstPtr &ptr) const { throw std::runtime_error ("weight not supported by this class"); } float Class::getEncumbrance (const MWWorld::Ptr& ptr) const { throw std::runtime_error ("encumbrance not supported by class"); } bool Class::isEssential (const MWWorld::ConstPtr& ptr) const { return false; } float Class::getArmorRating (const MWWorld::Ptr& ptr) const { throw std::runtime_error("Class does not support armor rating"); } const Class& Class::get (const std::string& key) { if (key.empty()) throw std::logic_error ("Class::get(): attempting to get an empty key"); std::map >::const_iterator iter = sClasses.find (key); if (iter==sClasses.end()) throw std::logic_error ("Class::get(): unknown class key: " + key); return *iter->second; } bool Class::isPersistent(const ConstPtr &ptr) const { throw std::runtime_error ("class does not support persistence"); } void Class::registerClass(const std::string& key, std::shared_ptr instance) { instance->mTypeName = key; sClasses.insert(std::make_pair(key, instance)); } std::string Class::getUpSoundId (const ConstPtr& ptr) const { throw std::runtime_error ("class does not have an up sound"); } std::string Class::getDownSoundId (const ConstPtr& ptr) const { throw std::runtime_error ("class does not have an down sound"); } std::string Class::getSoundIdFromSndGen(const Ptr &ptr, const std::string &type) const { throw std::runtime_error("class does not support soundgen look up"); } std::string Class::getInventoryIcon (const MWWorld::ConstPtr& ptr) const { throw std::runtime_error ("class does not have any inventory icon"); } MWGui::ToolTipInfo Class::getToolTipInfo (const ConstPtr& ptr, int count) const { throw std::runtime_error ("class does not have a tool tip"); } bool Class::showsInInventory (const ConstPtr& ptr) const { // NOTE: Don't show WerewolfRobe objects in the inventory, or allow them to be taken. // Vanilla likely uses a hack like this since there's no other way to prevent it from // being shown or taken. return (ptr.getCellRef().getRefId() != "werewolfrobe"); } bool Class::hasToolTip (const ConstPtr& ptr) const { return true; } std::string Class::getEnchantment (const ConstPtr& ptr) const { return ""; } void Class::adjustScale(const MWWorld::ConstPtr& ptr, osg::Vec3f& scale, bool rendering) const { } std::string Class::getModel(const MWWorld::ConstPtr &ptr) const { return ""; } bool Class::useAnim() const { return false; } void Class::getModelsToPreload(const Ptr &ptr, std::vector &models) const { std::string model = getModel(ptr); if (!model.empty()) models.push_back(model); } std::string Class::applyEnchantment(const MWWorld::ConstPtr &ptr, const std::string& enchId, int enchCharge, const std::string& newName) const { throw std::runtime_error ("class can't be enchanted"); } std::pair Class::canBeEquipped(const MWWorld::ConstPtr &ptr, const MWWorld::Ptr &npc) const { return std::make_pair (1, ""); } void Class::adjustPosition(const MWWorld::Ptr& ptr, bool force) const { } std::shared_ptr Class::defaultItemActivate(const Ptr &ptr, const Ptr &actor) const { if(!MWBase::Environment::get().getWindowManager()->isAllowed(MWGui::GW_Inventory)) return std::shared_ptr(new NullAction()); if(actor.getClass().isNpc() && actor.getClass().getNpcStats(actor).isWerewolf()) { const MWWorld::ESMStore &store = MWBase::Environment::get().getWorld()->getStore(); const ESM::Sound *sound = store.get().searchRandom("WolfItem"); std::shared_ptr action(new MWWorld::FailedAction("#{sWerewolfRefusal}")); if(sound) action->setSound(sound->mId); return action; } std::shared_ptr action(new ActionTake(ptr)); action->setSound(getUpSoundId(ptr)); return action; } MWWorld::Ptr Class::copyToCellImpl(const ConstPtr &ptr, CellStore &cell) const { throw std::runtime_error("unable to copy class to cell"); } MWWorld::Ptr Class::copyToCell(const ConstPtr &ptr, CellStore &cell, int count) const { Ptr newPtr = copyToCellImpl(ptr, cell); newPtr.getCellRef().unsetRefNum(); // This RefNum is only valid within the original cell of the reference newPtr.getRefData().setCount(count); return newPtr; } MWWorld::Ptr Class::copyToCell(const ConstPtr &ptr, CellStore &cell, const ESM::Position &pos, int count) const { Ptr newPtr = copyToCell(ptr, cell, count); newPtr.getRefData().setPosition(pos); return newPtr; } bool Class::isBipedal(const ConstPtr &ptr) const { return false; } bool Class::canFly(const ConstPtr &ptr) const { return false; } bool Class::canSwim(const ConstPtr &ptr) const { return false; } bool Class::canWalk(const ConstPtr &ptr) const { return false; } bool Class::isPureWaterCreature(const ConstPtr& ptr) const { return canSwim(ptr) && !isBipedal(ptr) && !canFly(ptr) && !canWalk(ptr); } bool Class::isPureFlyingCreature(const ConstPtr& ptr) const { return canFly(ptr) && !isBipedal(ptr) && !canSwim(ptr) && !canWalk(ptr); } bool Class::isPureLandCreature(const Ptr& ptr) const { return canWalk(ptr) && !isBipedal(ptr) && !canFly(ptr) && !canSwim(ptr); } bool Class::isMobile(const MWWorld::Ptr& ptr) const { return canSwim(ptr) || canWalk(ptr) || canFly(ptr); } float Class::getSkill(const MWWorld::Ptr& ptr, int skill) const { throw std::runtime_error("class does not support skills"); } int Class::getBloodTexture (const MWWorld::ConstPtr& ptr) const { throw std::runtime_error("class does not support gore"); } void Class::readAdditionalState (const MWWorld::Ptr& ptr, const ESM::ObjectState& state) const {} void Class::writeAdditionalState (const MWWorld::ConstPtr& ptr, ESM::ObjectState& state) const {} int Class::getBaseGold(const MWWorld::ConstPtr& ptr) const { throw std::runtime_error("class does not support base gold"); } bool Class::isClass(const MWWorld::ConstPtr& ptr, const std::string &className) const { return false; } MWWorld::DoorState Class::getDoorState (const MWWorld::ConstPtr &ptr) const { throw std::runtime_error("this is not a door"); } void Class::setDoorState (const MWWorld::Ptr &ptr, MWWorld::DoorState state) const { throw std::runtime_error("this is not a door"); } float Class::getNormalizedEncumbrance(const Ptr &ptr) const { float capacity = getCapacity(ptr); float encumbrance = getEncumbrance(ptr); if (encumbrance == 0) return 0.f; if (capacity == 0) return 1.f; return encumbrance / capacity; } std::string Class::getSound(const MWWorld::ConstPtr&) const { return std::string(); } int Class::getBaseFightRating(const ConstPtr &ptr) const { throw std::runtime_error("class does not support fight rating"); } std::string Class::getPrimaryFaction (const MWWorld::ConstPtr& ptr) const { return std::string(); } int Class::getPrimaryFactionRank (const MWWorld::ConstPtr& ptr) const { return -1; } float Class::getEffectiveArmorRating(const ConstPtr &armor, const Ptr &actor) const { throw std::runtime_error("class does not support armor ratings"); } osg::Vec4f Class::getEnchantmentColor(const MWWorld::ConstPtr& item) const { osg::Vec4f result(1,1,1,1); std::string enchantmentName = item.getClass().getEnchantment(item); if (enchantmentName.empty()) return result; const ESM::Enchantment* enchantment = MWBase::Environment::get().getWorld()->getStore().get().search(enchantmentName); if (!enchantment) return result; assert (enchantment->mEffects.mList.size()); const ESM::MagicEffect* magicEffect = MWBase::Environment::get().getWorld()->getStore().get().search( enchantment->mEffects.mList.front().mEffectID); if (!magicEffect) return result; result.x() = magicEffect->mData.mRed / 255.f; result.y() = magicEffect->mData.mGreen / 255.f; result.z() = magicEffect->mData.mBlue / 255.f; return result; } void Class::setBaseAISetting(const std::string& id, MWMechanics::CreatureStats::AiSetting setting, int value) const { throw std::runtime_error ("class does not have creature stats"); } void Class::modifyBaseInventory(const std::string& actorId, const std::string& itemId, int amount) const { throw std::runtime_error ("class does not have an inventory store"); } float Class::getWalkSpeed(const Ptr& /*ptr*/) const { return 0; } float Class::getRunSpeed(const Ptr& /*ptr*/) const { return 0; } float Class::getSwimSpeed(const Ptr& /*ptr*/) const { return 0; } } ================================================ FILE: apps/openmw/mwworld/class.hpp ================================================ #ifndef GAME_MWWORLD_CLASS_H #define GAME_MWWORLD_CLASS_H #include #include #include #include #include #include "ptr.hpp" #include "doorstate.hpp" #include "../mwmechanics/creaturestats.hpp" namespace ESM { struct ObjectState; } namespace MWRender { class RenderingInterface; } namespace MWPhysics { class PhysicsSystem; } namespace MWMechanics { class NpcStats; struct Movement; } namespace MWGui { struct ToolTipInfo; } namespace ESM { struct Position; } namespace MWWorld { class ContainerStore; class InventoryStore; class CellStore; class Action; /// \brief Base class for referenceable esm records class Class { static std::map > sClasses; std::string mTypeName; // not implemented Class (const Class&); Class& operator= (const Class&); protected: Class(); std::shared_ptr defaultItemActivate(const Ptr &ptr, const Ptr &actor) const; ///< Generate default action for activating inventory items virtual Ptr copyToCellImpl(const ConstPtr &ptr, CellStore &cell) const; public: virtual ~Class(); const std::string& getTypeName() const { return mTypeName; } virtual void insertObjectRendering (const Ptr& ptr, const std::string& mesh, MWRender::RenderingInterface& renderingInterface) const; virtual void insertObject(const Ptr& ptr, const std::string& mesh, MWPhysics::PhysicsSystem& physics) const; ///< Add reference into a cell for rendering (default implementation: don't render anything). virtual std::string getName (const ConstPtr& ptr) const = 0; ///< \return name or ID; can return an empty string. virtual void adjustPosition(const MWWorld::Ptr& ptr, bool force) const; ///< Adjust position to stand on ground. Must be called post model load /// @param force do this even if the ptr is flying virtual MWMechanics::CreatureStats& getCreatureStats (const Ptr& ptr) const; ///< Return creature stats or throw an exception, if class does not have creature stats /// (default implementation: throw an exception) virtual bool hasToolTip (const ConstPtr& ptr) const; ///< @return true if this object has a tooltip when focused (default implementation: true) virtual MWGui::ToolTipInfo getToolTipInfo (const ConstPtr& ptr, int count) const; ///< @return the content of the tool tip to be displayed. raises exception if the object has no tooltip. virtual bool showsInInventory (const ConstPtr& ptr) const; ///< Return whether ptr shows in inventory views. /// Hidden items are not displayed and cannot be (re)moved by the user. /// \return True if shown, false if hidden. virtual MWMechanics::NpcStats& getNpcStats (const Ptr& ptr) const; ///< Return NPC stats or throw an exception, if class does not have NPC stats /// (default implementation: throw an exception) virtual bool hasItemHealth (const ConstPtr& ptr) const; ///< \return Item health data available? (default implementation: false) virtual int getItemHealth (const ConstPtr& ptr) const; ///< Return current item health or throw an exception if class does not have item health virtual float getItemNormalizedHealth (const ConstPtr& ptr) const; ///< Return current item health re-scaled to maximum health virtual int getItemMaxHealth (const ConstPtr& ptr) const; ///< Return item max health or throw an exception, if class does not have item health /// (default implementation: throw an exception) virtual void hit(const Ptr& ptr, float attackStrength, int type=-1) const; ///< Execute a melee hit, using the current weapon. This will check the relevant skills /// of the given attacker, and whoever is hit. /// \param attackStrength how long the attack was charged for, a value in 0-1 range. /// \param type - type of attack, one of the MWMechanics::CreatureStats::AttackType /// enums. ignored for creature attacks. /// (default implementation: throw an exception) virtual void onHit(const MWWorld::Ptr &ptr, float damage, bool ishealth, const MWWorld::Ptr &object, const MWWorld::Ptr &attacker, const osg::Vec3f &hitPosition, bool successful) const; ///< Alerts \a ptr that it's being hit for \a damage points to health if \a ishealth is /// true (else fatigue) by \a object (sword, arrow, etc). \a attacker specifies the /// actor responsible for the attack, and \a successful specifies if the hit is /// successful or not. virtual void block (const Ptr& ptr) const; ///< Play the appropriate sound for a blocked attack, depending on the currently equipped shield /// (default implementation: throw an exception) virtual std::shared_ptr activate (const Ptr& ptr, const Ptr& actor) const; ///< Generate action for activation (default implementation: return a null action). virtual std::shared_ptr use (const Ptr& ptr, bool force=false) const; ///< Generate action for using via inventory menu (default implementation: return a /// null action). virtual ContainerStore& getContainerStore (const Ptr& ptr) const; ///< Return container store or throw an exception, if class does not have a /// container store (default implementation: throw an exception) virtual InventoryStore& getInventoryStore (const Ptr& ptr) const; ///< Return inventory store or throw an exception, if class does not have a /// inventory store (default implementation: throw an exception) /* Start of tes3mp addition Make it possible to check whether a class has a container store */ virtual bool hasContainerStore(const Ptr& ptr) const; ///< Does this object have a container store? (default implementation: false) /* End of tes3mp addition */ virtual bool hasInventoryStore (const Ptr& ptr) const; ///< Does this object have an inventory store, i.e. equipment slots? (default implementation: false) virtual bool canLock (const ConstPtr& ptr) const; /* Start of tes3mp addition Make it possible to check whether a class can be harvested */ virtual bool canBeHarvested(const ConstPtr& ptr) const; ///< Can this object be harvested? (default implementation: false) /* End of tes3mp addition */ virtual void setRemainingUsageTime (const Ptr& ptr, float duration) const; ///< Sets the remaining duration of the object, such as an equippable light /// source. (default implementation: throw an exception) virtual float getRemainingUsageTime (const ConstPtr& ptr) const; ///< Returns the remaining duration of the object, such as an equippable light /// source. (default implementation: -1, i.e. infinite) virtual std::string getScript (const ConstPtr& ptr) const; ///< Return name of the script attached to ptr (default implementation: return an empty /// string). virtual float getWalkSpeed(const Ptr& ptr) const; virtual float getRunSpeed(const Ptr& ptr) const; virtual float getSwimSpeed(const Ptr& ptr) const; /// Return maximal movement speed for the current state. virtual float getMaxSpeed(const Ptr& ptr) const; /// Return current movement speed. virtual float getCurrentSpeed(const Ptr& ptr) const; virtual float getJump(const MWWorld::Ptr &ptr) const; ///< Return jump velocity (not accounting for movement) virtual MWMechanics::Movement& getMovementSettings (const Ptr& ptr) const; ///< Return desired movement. virtual osg::Vec3f getRotationVector (const Ptr& ptr) const; ///< Return desired rotations, as euler angles. Sets getMovementSettings(ptr).mRotation to zero. virtual std::pair, bool> getEquipmentSlots (const ConstPtr& ptr) const; ///< \return first: Return IDs of the slot this object can be equipped in; second: can object /// stay stacked when equipped? /// /// Default implementation: return (empty vector, false). virtual int getEquipmentSkill (const ConstPtr& ptr) const; /// Return the index of the skill this item corresponds to when equipped or -1, if there is /// no such skill. /// (default implementation: return -1) virtual int getValue (const ConstPtr& ptr) const; ///< Return trade value of the object. Throws an exception, if the object can't be traded. /// (default implementation: throws an exception) virtual float getCapacity (const MWWorld::Ptr& ptr) const; ///< Return total weight that fits into the object. Throws an exception, if the object can't /// hold other objects. /// (default implementation: throws an exception) virtual float getEncumbrance (const MWWorld::Ptr& ptr) const; ///< Returns total weight of objects inside this object (including modifications from magic /// effects). Throws an exception, if the object can't hold other objects. /// (default implementation: throws an exception) virtual float getNormalizedEncumbrance (const MWWorld::Ptr& ptr) const; ///< Returns encumbrance re-scaled to capacity virtual bool apply (const MWWorld::Ptr& ptr, const std::string& id, const MWWorld::Ptr& actor) const; ///< Apply \a id on \a ptr. /// \param actor Actor that is resposible for the ID being applied to \a ptr. /// \return Any effect? /// /// (default implementation: ignore and return false) virtual void skillUsageSucceeded (const MWWorld::Ptr& ptr, int skill, int usageType, float extraFactor=1.f) const; ///< Inform actor \a ptr that a skill use has succeeded. /// /// (default implementations: throws an exception) virtual bool isEssential (const MWWorld::ConstPtr& ptr) const; ///< Is \a ptr essential? (i.e. may losing \a ptr make the game unwinnable) /// /// (default implementation: return false) virtual std::string getUpSoundId (const ConstPtr& ptr) const; ///< Return the up sound ID of \a ptr or throw an exception, if class does not support ID retrieval /// (default implementation: throw an exception) virtual std::string getDownSoundId (const ConstPtr& ptr) const; ///< Return the down sound ID of \a ptr or throw an exception, if class does not support ID retrieval /// (default implementation: throw an exception) virtual std::string getSoundIdFromSndGen(const Ptr &ptr, const std::string &type) const; ///< Returns the sound ID for \a ptr of the given soundgen \a type. virtual float getArmorRating (const MWWorld::Ptr& ptr) const; ///< @return combined armor rating of this actor virtual std::string getInventoryIcon (const MWWorld::ConstPtr& ptr) const; ///< Return name of inventory icon. virtual std::string getEnchantment (const MWWorld::ConstPtr& ptr) const; ///< @return the enchantment ID if the object is enchanted, otherwise an empty string /// (default implementation: return empty string) virtual int getEnchantmentPoints (const MWWorld::ConstPtr& ptr) const; ///< @return the number of enchantment points available for possible enchanting virtual void adjustScale(const MWWorld::ConstPtr& ptr, osg::Vec3f& scale, bool rendering) const; /// @param rendering Indicates if the scale to adjust is for the rendering mesh, or for the collision mesh virtual bool canSell (const MWWorld::ConstPtr& item, int npcServices) const; ///< Determine whether or not \a item can be sold to an npc with the given \a npcServices virtual int getServices (const MWWorld::ConstPtr& actor) const; virtual std::string getModel(const MWWorld::ConstPtr &ptr) const; virtual bool useAnim() const; ///< Whether or not to use animated variant of model (default false) virtual void getModelsToPreload(const MWWorld::Ptr& ptr, std::vector& models) const; ///< Get a list of models to preload that this object may use (directly or indirectly). default implementation: list getModel(). virtual std::string applyEnchantment(const MWWorld::ConstPtr &ptr, const std::string& enchId, int enchCharge, const std::string& newName) const; ///< Creates a new record using \a ptr as template, with the given name and the given enchantment applied to it. virtual std::pair canBeEquipped(const MWWorld::ConstPtr &ptr, const MWWorld::Ptr &npc) const; ///< Return 0 if player cannot equip item. 1 if can equip. 2 if it's twohanded weapon. 3 if twohanded weapon conflicts with that. /// Second item in the pair specifies the error message virtual float getWeight (const MWWorld::ConstPtr& ptr) const; virtual bool isPersistent (const MWWorld::ConstPtr& ptr) const; virtual bool isKey (const MWWorld::ConstPtr& ptr) const { return false; } virtual bool isGold(const MWWorld::ConstPtr& ptr) const { return false; } virtual bool allowTelekinesis(const MWWorld::ConstPtr& ptr) const { return true; } ///< Return whether this class of object can be activated with telekinesis /// Get a blood texture suitable for \a ptr (see Blood Texture 0-2 in Morrowind.ini) virtual int getBloodTexture (const MWWorld::ConstPtr& ptr) const; virtual Ptr copyToCell(const ConstPtr &ptr, CellStore &cell, int count) const; virtual Ptr copyToCell(const ConstPtr &ptr, CellStore &cell, const ESM::Position &pos, int count) const; virtual bool isActivator() const { return false; } virtual bool isActor() const { return false; } virtual bool isNpc() const { return false; } virtual bool isDoor() const { return false; } virtual bool isBipedal(const MWWorld::ConstPtr& ptr) const; virtual bool canFly(const MWWorld::ConstPtr& ptr) const; virtual bool canSwim(const MWWorld::ConstPtr& ptr) const; virtual bool canWalk(const MWWorld::ConstPtr& ptr) const; bool isPureWaterCreature(const MWWorld::ConstPtr& ptr) const; bool isPureFlyingCreature(const MWWorld::ConstPtr& ptr) const; bool isPureLandCreature(const MWWorld::Ptr& ptr) const; bool isMobile(const MWWorld::Ptr& ptr) const; virtual float getSkill(const MWWorld::Ptr& ptr, int skill) const; virtual void readAdditionalState (const MWWorld::Ptr& ptr, const ESM::ObjectState& state) const; ///< Read additional state from \a state into \a ptr. virtual void writeAdditionalState (const MWWorld::ConstPtr& ptr, ESM::ObjectState& state) const; ///< Write additional state from \a ptr into \a state. static const Class& get (const std::string& key); ///< If there is no class for this \a key, an exception is thrown. static void registerClass (const std::string& key, std::shared_ptr instance); virtual int getBaseGold(const MWWorld::ConstPtr& ptr) const; virtual bool isClass(const MWWorld::ConstPtr& ptr, const std::string &className) const; virtual DoorState getDoorState (const MWWorld::ConstPtr &ptr) const; /// This does not actually cause the door to move. Use World::activateDoor instead. virtual void setDoorState (const MWWorld::Ptr &ptr, DoorState state) const; virtual void respawn (const MWWorld::Ptr& ptr) const {} /// Returns sound id virtual std::string getSound(const MWWorld::ConstPtr& ptr) const; virtual int getBaseFightRating (const MWWorld::ConstPtr& ptr) const; virtual std::string getPrimaryFaction (const MWWorld::ConstPtr& ptr) const; virtual int getPrimaryFactionRank (const MWWorld::ConstPtr& ptr) const; /// Get the effective armor rating, factoring in the actor's skills, for the given armor. virtual float getEffectiveArmorRating(const MWWorld::ConstPtr& armor, const MWWorld::Ptr& actor) const; virtual osg::Vec4f getEnchantmentColor(const MWWorld::ConstPtr& item) const; virtual void setBaseAISetting(const std::string& id, MWMechanics::CreatureStats::AiSetting setting, int value) const; virtual void modifyBaseInventory(const std::string& actorId, const std::string& itemId, int amount) const; }; } #endif ================================================ FILE: apps/openmw/mwworld/containerstore.cpp ================================================ #include "containerstore.hpp" #include #include #include /* Start of tes3mp addition Include additional headers for multiplayer purposes */ #include "../mwmp/Main.hpp" #include "../mwmp/Networking.hpp" #include "../mwmp/LocalPlayer.hpp" #include /* End of tes3mp addition */ #include #include #include "../mwbase/environment.hpp" #include "../mwbase/world.hpp" #include "../mwmechanics/creaturestats.hpp" #include "../mwmechanics/levelledlist.hpp" #include "../mwmechanics/actorutil.hpp" #include "../mwmechanics/recharge.hpp" #include "manualref.hpp" #include "refdata.hpp" #include "class.hpp" #include "localscripts.hpp" #include "player.hpp" namespace { void addScripts(MWWorld::ContainerStore& store, MWWorld::CellStore* cell) { auto& scripts = MWBase::Environment::get().getWorld()->getLocalScripts(); for(const auto&& ptr : store) { const std::string& script = ptr.getClass().getScript(ptr); if(!script.empty()) { MWWorld::Ptr item = ptr; item.mCell = cell; scripts.add(script, item); } } } template float getTotalWeight (const MWWorld::CellRefList& cellRefList) { float sum = 0; for (const auto& iter : cellRefList.mList) { if (iter.mData.getCount()>0) sum += iter.mData.getCount()*iter.mBase->mData.mWeight; } return sum; } template MWWorld::Ptr searchId (MWWorld::CellRefList& list, const std::string& id, MWWorld::ContainerStore *store) { store->resolve(); std::string id2 = Misc::StringUtils::lowerCase (id); for (auto& iter : list.mList) { if (Misc::StringUtils::ciEqual(iter.mBase->mId, id2) && iter.mData.getCount()) { MWWorld::Ptr ptr (&iter, nullptr); ptr.setContainerStore (store); return ptr; } } return MWWorld::Ptr(); } } MWWorld::ResolutionListener::~ResolutionListener() { try { mStore.unresolve(); } catch(const std::exception& e) { Log(Debug::Error) << "Failed to clear temporary container contents: " << e.what(); } } template MWWorld::ContainerStoreIterator MWWorld::ContainerStore::getState (CellRefList& collection, const ESM::ObjectState& state) { if (!LiveCellRef::checkState (state)) return ContainerStoreIterator (this); // not valid anymore with current content files -> skip const T *record = MWBase::Environment::get().getWorld()->getStore(). get().search (state.mRef.mRefID); if (!record) return ContainerStoreIterator (this); LiveCellRef ref (record); ref.load (state); collection.mList.push_back (ref); return ContainerStoreIterator (this, --collection.mList.end()); } void MWWorld::ContainerStore::storeEquipmentState(const MWWorld::LiveCellRefBase &ref, int index, ESM::InventoryState &inventory) const { } void MWWorld::ContainerStore::readEquipmentState(const MWWorld::ContainerStoreIterator& iter, int index, const ESM::InventoryState &inventory) { } template void MWWorld::ContainerStore::storeState (const LiveCellRef& ref, ESM::ObjectState& state) const { ref.save (state); } template void MWWorld::ContainerStore::storeStates (const CellRefList& collection, ESM::InventoryState& inventory, int& index, bool equipable) const { for (const auto& iter : collection.mList) { if (iter.mData.getCount() == 0) continue; ESM::ObjectState state; storeState (iter, state); if (equipable) storeEquipmentState(iter, index, inventory); inventory.mItems.push_back (state); ++index; } } const std::string MWWorld::ContainerStore::sGoldId = "gold_001"; MWWorld::ContainerStore::ContainerStore() : mListener(nullptr) , mRechargingItemsUpToDate(false) , mCachedWeight (0) , mWeightUpToDate (false) , mModified(false) , mResolved(false) , mSeed() , mPtr() {} MWWorld::ContainerStore::~ContainerStore() {} MWWorld::ConstContainerStoreIterator MWWorld::ContainerStore::cbegin (int mask) const { return ConstContainerStoreIterator (mask, this); } MWWorld::ConstContainerStoreIterator MWWorld::ContainerStore::cend() const { return ConstContainerStoreIterator (this); } MWWorld::ConstContainerStoreIterator MWWorld::ContainerStore::begin (int mask) const { return cbegin(mask); } MWWorld::ConstContainerStoreIterator MWWorld::ContainerStore::end() const { return cend(); } MWWorld::ContainerStoreIterator MWWorld::ContainerStore::begin (int mask) { return ContainerStoreIterator (mask, this); } MWWorld::ContainerStoreIterator MWWorld::ContainerStore::end() { return ContainerStoreIterator (this); } int MWWorld::ContainerStore::count(const std::string &id) const { int total=0; for (const auto&& iter : *this) if (Misc::StringUtils::ciEqual(iter.getCellRef().getRefId(), id)) total += iter.getRefData().getCount(); return total; } MWWorld::ContainerStoreListener* MWWorld::ContainerStore::getContListener() const { return mListener; } void MWWorld::ContainerStore::setContListener(MWWorld::ContainerStoreListener* listener) { mListener = listener; } MWWorld::ContainerStoreIterator MWWorld::ContainerStore::unstack(const Ptr &ptr, const Ptr& container, int count) { resolve(); if (ptr.getRefData().getCount() <= count) return end(); MWWorld::ContainerStoreIterator it = addNewStack(ptr, subtractItems(ptr.getRefData().getCount(false), count)); /* Start of tes3mp addition Send an ID_PLAYER_INVENTORY packet every time an item stack gets added for a player here */ Ptr player = MWBase::Environment::get().getWorld()->getPlayerPtr(); if (container == player && this == &player.getClass().getContainerStore(player)) { mwmp::LocalPlayer *localPlayer = mwmp::Main::get().getLocalPlayer(); if (!localPlayer->avoidSendingInventoryPackets) localPlayer->sendItemChange(ptr, ptr.getRefData().getCount() - count, mwmp::InventoryChanges::ADD); } /* End of tes3mp addition */ const std::string script = it->getClass().getScript(*it); if (!script.empty()) MWBase::Environment::get().getWorld()->getLocalScripts().add(script, *it); remove(ptr, ptr.getRefData().getCount()-count, container); return it; } MWWorld::ContainerStoreIterator MWWorld::ContainerStore::restack(const MWWorld::Ptr& item) { resolve(); MWWorld::ContainerStoreIterator retval = end(); for (MWWorld::ContainerStoreIterator iter (begin()); iter != end(); ++iter) { if (item == *iter) { retval = iter; break; } } if (retval == end()) throw std::runtime_error("item is not from this container"); for (MWWorld::ContainerStoreIterator iter (begin()); iter != end(); ++iter) { if (stacks(*iter, item)) { iter->getRefData().setCount(addItems(iter->getRefData().getCount(false), item.getRefData().getCount(false))); item.getRefData().setCount(0); retval = iter; break; } } return retval; } bool MWWorld::ContainerStore::stacks(const ConstPtr& ptr1, const ConstPtr& ptr2) const { const MWWorld::Class& cls1 = ptr1.getClass(); const MWWorld::Class& cls2 = ptr2.getClass(); if (!Misc::StringUtils::ciEqual(ptr1.getCellRef().getRefId(), ptr2.getCellRef().getRefId())) return false; // If it has an enchantment, don't stack when some of the charge is already used if (!ptr1.getClass().getEnchantment(ptr1).empty()) { const ESM::Enchantment* enchantment = MWBase::Environment::get().getWorld()->getStore().get().find( ptr1.getClass().getEnchantment(ptr1)); float maxCharge = static_cast(enchantment->mData.mCharge); float enchantCharge1 = ptr1.getCellRef().getEnchantmentCharge() == -1 ? maxCharge : ptr1.getCellRef().getEnchantmentCharge(); float enchantCharge2 = ptr2.getCellRef().getEnchantmentCharge() == -1 ? maxCharge : ptr2.getCellRef().getEnchantmentCharge(); if (enchantCharge1 != maxCharge || enchantCharge2 != maxCharge) return false; } return ptr1 != ptr2 // an item never stacks onto itself && ptr1.getCellRef().getSoul() == ptr2.getCellRef().getSoul() && ptr1.getClass().getRemainingUsageTime(ptr1) == ptr2.getClass().getRemainingUsageTime(ptr2) // Items with scripts never stack && cls1.getScript(ptr1).empty() && cls2.getScript(ptr2).empty() // item that is already partly used up never stacks && (!cls1.hasItemHealth(ptr1) || ( cls1.getItemHealth(ptr1) == cls1.getItemMaxHealth(ptr1) && cls2.getItemHealth(ptr2) == cls2.getItemMaxHealth(ptr2))); } MWWorld::ContainerStoreIterator MWWorld::ContainerStore::add(const std::string &id, int count, const Ptr &actorPtr) { MWWorld::ManualRef ref(MWBase::Environment::get().getWorld()->getStore(), id, count); return add(ref.getPtr(), count, actorPtr); } MWWorld::ContainerStoreIterator MWWorld::ContainerStore::add (const Ptr& itemPtr, int count, const Ptr& actorPtr, bool /*allowAutoEquip*/, bool resolve) { Ptr player = MWBase::Environment::get().getWorld ()->getPlayerPtr(); MWWorld::ContainerStoreIterator it = addImp(itemPtr, count, resolve); // The copy of the original item we just made MWWorld::Ptr item = *it; /* Start of tes3mp addition Send an ID_PLAYER_INVENTORY packet every time an item gets added for a player here */ if (this == &player.getClass().getContainerStore(player)) { mwmp::LocalPlayer *localPlayer = mwmp::Main::get().getLocalPlayer(); if (!localPlayer->avoidSendingInventoryPackets) { int realCount = count; if (itemPtr.getClass().isGold(itemPtr)) { realCount = realCount * itemPtr.getClass().getValue(itemPtr); } localPlayer->sendItemChange(item, realCount, mwmp::InventoryChanges::ADD); } } /* End of tes3mp addition */ // we may have copied an item from the world, so reset a few things first item.getRefData().setBaseNode(nullptr); // Especially important, otherwise scripts on the item could think that it's actually in a cell ESM::Position pos; pos.rot[0] = 0; pos.rot[1] = 0; pos.rot[2] = 0; pos.pos[0] = 0; pos.pos[1] = 0; pos.pos[2] = 0; item.getCellRef().setPosition(pos); // We do not need to store owners for items in container stores - we do not use it anyway. item.getCellRef().setOwner(""); item.getCellRef().resetGlobalVariable(); item.getCellRef().setFaction(""); item.getCellRef().setFactionRank(-2); // must reset the RefNum on the copied item, so that the RefNum on the original item stays unique // maybe we should do this in the copy constructor instead? item.getCellRef().unsetRefNum(); // destroy link to content file std::string script = item.getClass().getScript(item); if (!script.empty()) { if (actorPtr == player) { // Items in player's inventory have cell set to 0, so their scripts will never be removed item.mCell = nullptr; } else { // Set mCell to the cell of the container/actor, so that the scripts are removed properly when // the cell of the container/actor goes inactive item.mCell = actorPtr.getCell(); } item.mContainerStore = this; MWBase::Environment::get().getWorld()->getLocalScripts().add(script, item); // Set OnPCAdd special variable, if it is declared // Make sure to do this *after* we have added the script to LocalScripts if (actorPtr == player) item.getRefData().getLocals().setVarByInt(script, "onpcadd", 1); } /* Start of tes3mp change (major) Only fire inventory events for actors in loaded cells to avoid crashes */ if (mListener && !actorPtr.getClass().hasInventoryStore(actorPtr) && MWBase::Environment::get().getWorld()->isCellActive(*actorPtr.getCell()->getCell())) mListener->itemAdded(item, count); /* End of tes3mp change (major) */ return it; } MWWorld::ContainerStoreIterator MWWorld::ContainerStore::addImp (const Ptr& ptr, int count, bool markModified) { if(markModified) resolve(); int type = getType(ptr); const MWWorld::ESMStore &esmStore = MWBase::Environment::get().getWorld()->getStore(); // gold needs special handling: when it is inserted into a container, the base object automatically becomes Gold_001 // this ensures that gold piles of different sizes stack with each other (also, several scripts rely on Gold_001 for detecting player gold) if(ptr.getClass().isGold(ptr)) { int realCount = count * ptr.getClass().getValue(ptr); for (MWWorld::ContainerStoreIterator iter (begin(type)); iter!=end(); ++iter) { if (Misc::StringUtils::ciEqual((*iter).getCellRef().getRefId(), MWWorld::ContainerStore::sGoldId)) { iter->getRefData().setCount(addItems(iter->getRefData().getCount(false), realCount)); flagAsModified(); return iter; } } MWWorld::ManualRef ref(esmStore, MWWorld::ContainerStore::sGoldId, realCount); return addNewStack(ref.getPtr(), realCount); } // determine whether to stack or not for (MWWorld::ContainerStoreIterator iter (begin(type)); iter!=end(); ++iter) { if (stacks(*iter, ptr)) { // stack iter->getRefData().setCount(addItems(iter->getRefData().getCount(false), count)); flagAsModified(); return iter; } } // if we got here, this means no stacking return addNewStack(ptr, count); } MWWorld::ContainerStoreIterator MWWorld::ContainerStore::addNewStack (const ConstPtr& ptr, int count) { ContainerStoreIterator it = begin(); switch (getType(ptr)) { case Type_Potion: potions.mList.push_back (*ptr.get()); it = ContainerStoreIterator(this, --potions.mList.end()); break; case Type_Apparatus: appas.mList.push_back (*ptr.get()); it = ContainerStoreIterator(this, --appas.mList.end()); break; case Type_Armor: armors.mList.push_back (*ptr.get()); it = ContainerStoreIterator(this, --armors.mList.end()); break; case Type_Book: books.mList.push_back (*ptr.get()); it = ContainerStoreIterator(this, --books.mList.end()); break; case Type_Clothing: clothes.mList.push_back (*ptr.get()); it = ContainerStoreIterator(this, --clothes.mList.end()); break; case Type_Ingredient: ingreds.mList.push_back (*ptr.get()); it = ContainerStoreIterator(this, --ingreds.mList.end()); break; case Type_Light: lights.mList.push_back (*ptr.get()); it = ContainerStoreIterator(this, --lights.mList.end()); break; case Type_Lockpick: lockpicks.mList.push_back (*ptr.get()); it = ContainerStoreIterator(this, --lockpicks.mList.end()); break; case Type_Miscellaneous: miscItems.mList.push_back (*ptr.get()); it = ContainerStoreIterator(this, --miscItems.mList.end()); break; case Type_Probe: probes.mList.push_back (*ptr.get()); it = ContainerStoreIterator(this, --probes.mList.end()); break; case Type_Repair: repairs.mList.push_back (*ptr.get()); it = ContainerStoreIterator(this, --repairs.mList.end()); break; case Type_Weapon: weapons.mList.push_back (*ptr.get()); it = ContainerStoreIterator(this, --weapons.mList.end()); break; } it->getRefData().setCount(count); flagAsModified(); return it; } void MWWorld::ContainerStore::rechargeItems(float duration) { if (!mRechargingItemsUpToDate) { updateRechargingItems(); mRechargingItemsUpToDate = true; } for (auto& it : mRechargingItems) { if (!MWMechanics::rechargeItem(*it.first, it.second, duration)) continue; // attempt to restack when fully recharged if (it.first->getCellRef().getEnchantmentCharge() == it.second) it.first = restack(*it.first); } } void MWWorld::ContainerStore::updateRechargingItems() { mRechargingItems.clear(); for (ContainerStoreIterator it = begin(); it != end(); ++it) { const std::string& enchantmentId = it->getClass().getEnchantment(*it); if (!enchantmentId.empty()) { const ESM::Enchantment* enchantment = MWBase::Environment::get().getWorld()->getStore().get().search(enchantmentId); if (!enchantment) { Log(Debug::Warning) << "Warning: Can't find enchantment '" << enchantmentId << "' on item " << it->getCellRef().getRefId(); continue; } if (enchantment->mData.mType == ESM::Enchantment::WhenUsed || enchantment->mData.mType == ESM::Enchantment::WhenStrikes) mRechargingItems.emplace_back(it, static_cast(enchantment->mData.mCharge)); } } } int MWWorld::ContainerStore::remove(const std::string& itemId, int count, const Ptr& actor, bool equipReplacement, bool resolveFirst) { if(resolveFirst) resolve(); int toRemove = count; for (ContainerStoreIterator iter(begin()); iter != end() && toRemove > 0; ++iter) if (Misc::StringUtils::ciEqual(iter->getCellRef().getRefId(), itemId)) toRemove -= remove(*iter, toRemove, actor, equipReplacement, resolveFirst); flagAsModified(); // number of removed items return count - toRemove; } bool MWWorld::ContainerStore::hasVisibleItems() const { for (const auto&& iter : *this) { if (iter.getClass().showsInInventory(iter)) return true; } return false; } int MWWorld::ContainerStore::remove(const Ptr& item, int count, const Ptr& actor, bool equipReplacement, bool resolveFirst) { assert(this == item.getContainerStore()); if(resolveFirst) resolve(); /* Start of tes3mp addition Send an ID_PLAYER_INVENTORY packet every time an item gets removed for a player here */ Ptr player = MWBase::Environment::get().getWorld()->getPlayerPtr(); if (this == &player.getClass().getContainerStore(player)) { mwmp::LocalPlayer *localPlayer = mwmp::Main::get().getLocalPlayer(); if (!localPlayer->avoidSendingInventoryPackets) localPlayer->sendItemChange(item, count, mwmp::InventoryChanges::REMOVE); } /* End of tes3mp addition */ int toRemove = count; RefData& itemRef = item.getRefData(); if (itemRef.getCount() <= toRemove) { toRemove -= itemRef.getCount(); itemRef.setCount(0); } else { itemRef.setCount(subtractItems(itemRef.getCount(false), toRemove)); toRemove = 0; } flagAsModified(); // we should not fire event for InventoryStore yet - it has some custom logic /* Start of tes3mp change (major) Only fire inventory events for actors in loaded cells to avoid crashes */ if (mListener && !actor.getClass().hasInventoryStore(actor) && MWBase::Environment::get().getWorld()->isCellActive(*actor.getCell()->getCell())) mListener->itemRemoved(item, count - toRemove); /* End of tes3mp change (major) */ // number of removed items return count - toRemove; } void MWWorld::ContainerStore::fill (const ESM::InventoryList& items, const std::string& owner, Misc::Rng::Seed& seed) { for (const ESM::ContItem& iter : items.mList) { std::string id = Misc::StringUtils::lowerCase(iter.mItem); addInitialItem(id, owner, iter.mCount, &seed); } flagAsModified(); mResolved = true; } void MWWorld::ContainerStore::fillNonRandom (const ESM::InventoryList& items, const std::string& owner, unsigned int seed) { mSeed = seed; for (const ESM::ContItem& iter : items.mList) { std::string id = Misc::StringUtils::lowerCase(iter.mItem); addInitialItem(id, owner, iter.mCount, nullptr); } flagAsModified(); mResolved = false; } void MWWorld::ContainerStore::addInitialItem (const std::string& id, const std::string& owner, int count, Misc::Rng::Seed* seed, bool topLevel) { if (count == 0) return; //Don't restock with nothing. try { ManualRef ref (MWBase::Environment::get().getWorld()->getStore(), id, count); if (ref.getPtr().getClass().getScript(ref.getPtr()).empty()) { addInitialItemImp(ref.getPtr(), owner, count, seed, topLevel); } else { // Adding just one item per time to make sure there isn't a stack of scripted items for (int i = 0; i < std::abs(count); i++) addInitialItemImp(ref.getPtr(), owner, count < 0 ? -1 : 1, seed, topLevel); } } catch (const std::exception& e) { Log(Debug::Warning) << "Warning: MWWorld::ContainerStore::addInitialItem: " << e.what(); } } void MWWorld::ContainerStore::addInitialItemImp(const MWWorld::Ptr& ptr, const std::string& owner, int count, Misc::Rng::Seed* seed, bool topLevel) { if (ptr.getTypeName()==typeid (ESM::ItemLevList).name()) { if(!seed) return; const ESM::ItemLevList* levItemList = ptr.get()->mBase; if (topLevel && std::abs(count) > 1 && levItemList->mFlags & ESM::ItemLevList::Each) { for (int i=0; i 0 ? 1 : -1, seed, true); return; } else { std::string itemId = MWMechanics::getLevelledItem(ptr.get()->mBase, false, *seed); if (itemId.empty()) return; addInitialItem(itemId, owner, count, seed, false); } } else { ptr.getCellRef().setOwner(owner); addImp (ptr, count, false); } } void MWWorld::ContainerStore::clear() { for (auto&& iter : *this) iter.getRefData().setCount (0); flagAsModified(); mModified = true; } void MWWorld::ContainerStore::flagAsModified() { mWeightUpToDate = false; mRechargingItemsUpToDate = false; } bool MWWorld::ContainerStore::isResolved() const { return mResolved; } /* Start of tes3mp addiition Make it possible to set the container's resolved state from elsewhere, to avoid unnecessary refills before overriding its contents */ void MWWorld::ContainerStore::setResolved(bool state) { mResolved = state; } /* End of tes3mp addition */ void MWWorld::ContainerStore::resolve() { if(!mResolved && !mPtr.isEmpty()) { for(const auto&& ptr : *this) ptr.getRefData().setCount(0); Misc::Rng::Seed seed{mSeed}; fill(mPtr.get()->mBase->mInventory, "", seed); addScripts(*this, mPtr.mCell); } mModified = true; } MWWorld::ResolutionHandle MWWorld::ContainerStore::resolveTemporarily() { if(mModified) return {}; std::shared_ptr listener = mResolutionListener.lock(); if(!listener) { listener = std::make_shared(*this); mResolutionListener = listener; } if(!mResolved && !mPtr.isEmpty()) { for(const auto&& ptr : *this) ptr.getRefData().setCount(0); Misc::Rng::Seed seed{mSeed}; fill(mPtr.get()->mBase->mInventory, "", seed); addScripts(*this, mPtr.mCell); } return {listener}; } void MWWorld::ContainerStore::unresolve() { if (mModified) return; if (mResolved && !mPtr.isEmpty()) { for(const auto&& ptr : *this) ptr.getRefData().setCount(0); fillNonRandom(mPtr.get()->mBase->mInventory, "", mSeed); addScripts(*this, mPtr.mCell); mResolved = false; } } float MWWorld::ContainerStore::getWeight() const { if (!mWeightUpToDate) { mCachedWeight = 0; mCachedWeight += getTotalWeight (potions); mCachedWeight += getTotalWeight (appas); mCachedWeight += getTotalWeight (armors); mCachedWeight += getTotalWeight (books); mCachedWeight += getTotalWeight (clothes); mCachedWeight += getTotalWeight (ingreds); mCachedWeight += getTotalWeight (lights); mCachedWeight += getTotalWeight (lockpicks); mCachedWeight += getTotalWeight (miscItems); mCachedWeight += getTotalWeight (probes); mCachedWeight += getTotalWeight (repairs); mCachedWeight += getTotalWeight (weapons); mWeightUpToDate = true; } return mCachedWeight; } int MWWorld::ContainerStore::getType (const ConstPtr& ptr) { if (ptr.isEmpty()) throw std::runtime_error ("can't put a non-existent object into a container"); if (ptr.getTypeName()==typeid (ESM::Potion).name()) return Type_Potion; if (ptr.getTypeName()==typeid (ESM::Apparatus).name()) return Type_Apparatus; if (ptr.getTypeName()==typeid (ESM::Armor).name()) return Type_Armor; if (ptr.getTypeName()==typeid (ESM::Book).name()) return Type_Book; if (ptr.getTypeName()==typeid (ESM::Clothing).name()) return Type_Clothing; if (ptr.getTypeName()==typeid (ESM::Ingredient).name()) return Type_Ingredient; if (ptr.getTypeName()==typeid (ESM::Light).name()) return Type_Light; if (ptr.getTypeName()==typeid (ESM::Lockpick).name()) return Type_Lockpick; if (ptr.getTypeName()==typeid (ESM::Miscellaneous).name()) return Type_Miscellaneous; if (ptr.getTypeName()==typeid (ESM::Probe).name()) return Type_Probe; if (ptr.getTypeName()==typeid (ESM::Repair).name()) return Type_Repair; if (ptr.getTypeName()==typeid (ESM::Weapon).name()) return Type_Weapon; throw std::runtime_error ( "Object '" + ptr.getCellRef().getRefId() + "' of type " + ptr.getTypeName() + " can not be placed into a container"); } MWWorld::Ptr MWWorld::ContainerStore::findReplacement(const std::string& id) { MWWorld::Ptr item; int itemHealth = 1; for (auto&& iter : *this) { int iterHealth = iter.getClass().hasItemHealth(iter) ? iter.getClass().getItemHealth(iter) : 1; if (Misc::StringUtils::ciEqual(iter.getCellRef().getRefId(), id)) { // Prefer the stack with the lowest remaining uses // Try to get item with zero durability only if there are no other items found if (item.isEmpty() || (iterHealth > 0 && iterHealth < itemHealth) || (itemHealth <= 0 && iterHealth > 0)) { item = iter; itemHealth = iterHealth; } } } return item; } MWWorld::Ptr MWWorld::ContainerStore::search (const std::string& id) { resolve(); { Ptr ptr = searchId (potions, id, this); if (!ptr.isEmpty()) return ptr; } { Ptr ptr = searchId (appas, id, this); if (!ptr.isEmpty()) return ptr; } { Ptr ptr = searchId (armors, id, this); if (!ptr.isEmpty()) return ptr; } { Ptr ptr = searchId (books, id, this); if (!ptr.isEmpty()) return ptr; } { Ptr ptr = searchId (clothes, id, this); if (!ptr.isEmpty()) return ptr; } { Ptr ptr = searchId (ingreds, id, this); if (!ptr.isEmpty()) return ptr; } { Ptr ptr = searchId (lights, id, this); if (!ptr.isEmpty()) return ptr; } { Ptr ptr = searchId (lockpicks, id, this); if (!ptr.isEmpty()) return ptr; } { Ptr ptr = searchId (miscItems, id, this); if (!ptr.isEmpty()) return ptr; } { Ptr ptr = searchId (probes, id, this); if (!ptr.isEmpty()) return ptr; } { Ptr ptr = searchId (repairs, id, this); if (!ptr.isEmpty()) return ptr; } { Ptr ptr = searchId (weapons, id, this); if (!ptr.isEmpty()) return ptr; } return Ptr(); } int MWWorld::ContainerStore::addItems(int count1, int count2) { int sum = std::abs(count1) + std::abs(count2); if(count1 < 0 || count2 < 0) return -sum; return sum; } int MWWorld::ContainerStore::subtractItems(int count1, int count2) { int sum = std::abs(count1) - std::abs(count2); if(count1 < 0 || count2 < 0) return -sum; return sum; } void MWWorld::ContainerStore::writeState (ESM::InventoryState& state) const { state.mItems.clear(); int index = 0; storeStates (potions, state, index); storeStates (appas, state, index); storeStates (armors, state, index, true); storeStates (books, state, index, true); // not equipable as such, but for selectedEnchantItem storeStates (clothes, state, index, true); storeStates (ingreds, state, index); storeStates (lockpicks, state, index, true); storeStates (miscItems, state, index); storeStates (probes, state, index, true); storeStates (repairs, state, index); storeStates (weapons, state, index, true); storeStates (lights, state, index, true); } void MWWorld::ContainerStore::readState (const ESM::InventoryState& inventory) { clear(); mModified = true; mResolved = true; int index = 0; for (const ESM::ObjectState& state : inventory.mItems) { int type = MWBase::Environment::get().getWorld()->getStore().find(state.mRef.mRefID); int thisIndex = index++; switch (type) { case ESM::REC_ALCH: getState (potions, state); break; case ESM::REC_APPA: getState (appas, state); break; case ESM::REC_ARMO: readEquipmentState (getState (armors, state), thisIndex, inventory); break; case ESM::REC_BOOK: readEquipmentState (getState (books, state), thisIndex, inventory); break; // not equipable as such, but for selectedEnchantItem case ESM::REC_CLOT: readEquipmentState (getState (clothes, state), thisIndex, inventory); break; case ESM::REC_INGR: getState (ingreds, state); break; case ESM::REC_LOCK: readEquipmentState (getState (lockpicks, state), thisIndex, inventory); break; case ESM::REC_MISC: getState (miscItems, state); break; case ESM::REC_PROB: readEquipmentState (getState (probes, state), thisIndex, inventory); break; case ESM::REC_REPA: getState (repairs, state); break; case ESM::REC_WEAP: readEquipmentState (getState (weapons, state), thisIndex, inventory); break; case ESM::REC_LIGH: readEquipmentState (getState (lights, state), thisIndex, inventory); break; case 0: Log(Debug::Warning) << "Dropping inventory reference to '" << state.mRef.mRefID << "' (object no longer exists)"; break; default: Log(Debug::Warning) << "Warning: Invalid item type in inventory state, refid " << state.mRef.mRefID; break; } } } template template void MWWorld::ContainerStoreIteratorBase::copy (const ContainerStoreIteratorBase& src) { mType = src.mType; mMask = src.mMask; mContainer = src.mContainer; mPtr = src.mPtr; switch (src.mType) { case MWWorld::ContainerStore::Type_Potion: mPotion = src.mPotion; break; case MWWorld::ContainerStore::Type_Apparatus: mApparatus = src.mApparatus; break; case MWWorld::ContainerStore::Type_Armor: mArmor = src.mArmor; break; case MWWorld::ContainerStore::Type_Book: mBook = src.mBook; break; case MWWorld::ContainerStore::Type_Clothing: mClothing = src.mClothing; break; case MWWorld::ContainerStore::Type_Ingredient: mIngredient = src.mIngredient; break; case MWWorld::ContainerStore::Type_Light: mLight = src.mLight; break; case MWWorld::ContainerStore::Type_Lockpick: mLockpick = src.mLockpick; break; case MWWorld::ContainerStore::Type_Miscellaneous: mMiscellaneous = src.mMiscellaneous; break; case MWWorld::ContainerStore::Type_Probe: mProbe = src.mProbe; break; case MWWorld::ContainerStore::Type_Repair: mRepair = src.mRepair; break; case MWWorld::ContainerStore::Type_Weapon: mWeapon = src.mWeapon; break; case -1: break; default: assert(0); } } template void MWWorld::ContainerStoreIteratorBase::incType() { if (mType==0) mType = 1; else if (mType!=-1) { mType <<= 1; if (mType>ContainerStore::Type_Last) mType = -1; } } template void MWWorld::ContainerStoreIteratorBase::nextType() { while (mType!=-1) { incType(); if ((mType & mMask) && mType>0) if (resetIterator()) break; } } template bool MWWorld::ContainerStoreIteratorBase::resetIterator() { switch (mType) { case ContainerStore::Type_Potion: mPotion = mContainer->potions.mList.begin(); return mPotion!=mContainer->potions.mList.end(); case ContainerStore::Type_Apparatus: mApparatus = mContainer->appas.mList.begin(); return mApparatus!=mContainer->appas.mList.end(); case ContainerStore::Type_Armor: mArmor = mContainer->armors.mList.begin(); return mArmor!=mContainer->armors.mList.end(); case ContainerStore::Type_Book: mBook = mContainer->books.mList.begin(); return mBook!=mContainer->books.mList.end(); case ContainerStore::Type_Clothing: mClothing = mContainer->clothes.mList.begin(); return mClothing!=mContainer->clothes.mList.end(); case ContainerStore::Type_Ingredient: mIngredient = mContainer->ingreds.mList.begin(); return mIngredient!=mContainer->ingreds.mList.end(); case ContainerStore::Type_Light: mLight = mContainer->lights.mList.begin(); return mLight!=mContainer->lights.mList.end(); case ContainerStore::Type_Lockpick: mLockpick = mContainer->lockpicks.mList.begin(); return mLockpick!=mContainer->lockpicks.mList.end(); case ContainerStore::Type_Miscellaneous: mMiscellaneous = mContainer->miscItems.mList.begin(); return mMiscellaneous!=mContainer->miscItems.mList.end(); case ContainerStore::Type_Probe: mProbe = mContainer->probes.mList.begin(); return mProbe!=mContainer->probes.mList.end(); case ContainerStore::Type_Repair: mRepair = mContainer->repairs.mList.begin(); return mRepair!=mContainer->repairs.mList.end(); case ContainerStore::Type_Weapon: mWeapon = mContainer->weapons.mList.begin(); return mWeapon!=mContainer->weapons.mList.end(); } return false; } template bool MWWorld::ContainerStoreIteratorBase::incIterator() { switch (mType) { case ContainerStore::Type_Potion: ++mPotion; return mPotion==mContainer->potions.mList.end(); case ContainerStore::Type_Apparatus: ++mApparatus; return mApparatus==mContainer->appas.mList.end(); case ContainerStore::Type_Armor: ++mArmor; return mArmor==mContainer->armors.mList.end(); case ContainerStore::Type_Book: ++mBook; return mBook==mContainer->books.mList.end(); case ContainerStore::Type_Clothing: ++mClothing; return mClothing==mContainer->clothes.mList.end(); case ContainerStore::Type_Ingredient: ++mIngredient; return mIngredient==mContainer->ingreds.mList.end(); case ContainerStore::Type_Light: ++mLight; return mLight==mContainer->lights.mList.end(); case ContainerStore::Type_Lockpick: ++mLockpick; return mLockpick==mContainer->lockpicks.mList.end(); case ContainerStore::Type_Miscellaneous: ++mMiscellaneous; return mMiscellaneous==mContainer->miscItems.mList.end(); case ContainerStore::Type_Probe: ++mProbe; return mProbe==mContainer->probes.mList.end(); case ContainerStore::Type_Repair: ++mRepair; return mRepair==mContainer->repairs.mList.end(); case ContainerStore::Type_Weapon: ++mWeapon; return mWeapon==mContainer->weapons.mList.end(); } return true; } template template bool MWWorld::ContainerStoreIteratorBase::isEqual (const ContainerStoreIteratorBase& other) const { if (mContainer!=other.mContainer) return false; if (mType!=other.mType) return false; switch (mType) { case ContainerStore::Type_Potion: return mPotion==other.mPotion; case ContainerStore::Type_Apparatus: return mApparatus==other.mApparatus; case ContainerStore::Type_Armor: return mArmor==other.mArmor; case ContainerStore::Type_Book: return mBook==other.mBook; case ContainerStore::Type_Clothing: return mClothing==other.mClothing; case ContainerStore::Type_Ingredient: return mIngredient==other.mIngredient; case ContainerStore::Type_Light: return mLight==other.mLight; case ContainerStore::Type_Lockpick: return mLockpick==other.mLockpick; case ContainerStore::Type_Miscellaneous: return mMiscellaneous==other.mMiscellaneous; case ContainerStore::Type_Probe: return mProbe==other.mProbe; case ContainerStore::Type_Repair: return mRepair==other.mRepair; case ContainerStore::Type_Weapon: return mWeapon==other.mWeapon; case -1: return true; } return false; } template PtrType *MWWorld::ContainerStoreIteratorBase::operator->() const { mPtr = **this; return &mPtr; } template PtrType MWWorld::ContainerStoreIteratorBase::operator*() const { PtrType ptr; switch (mType) { case ContainerStore::Type_Potion: ptr = PtrType (&*mPotion, nullptr); break; case ContainerStore::Type_Apparatus: ptr = PtrType (&*mApparatus, nullptr); break; case ContainerStore::Type_Armor: ptr = PtrType (&*mArmor, nullptr); break; case ContainerStore::Type_Book: ptr = PtrType (&*mBook, nullptr); break; case ContainerStore::Type_Clothing: ptr = PtrType (&*mClothing, nullptr); break; case ContainerStore::Type_Ingredient: ptr = PtrType (&*mIngredient, nullptr); break; case ContainerStore::Type_Light: ptr = PtrType (&*mLight, nullptr); break; case ContainerStore::Type_Lockpick: ptr = PtrType (&*mLockpick, nullptr); break; case ContainerStore::Type_Miscellaneous: ptr = PtrType (&*mMiscellaneous, nullptr); break; case ContainerStore::Type_Probe: ptr = PtrType (&*mProbe, nullptr); break; case ContainerStore::Type_Repair: ptr = PtrType (&*mRepair, nullptr); break; case ContainerStore::Type_Weapon: ptr = PtrType (&*mWeapon, nullptr); break; } if (ptr.isEmpty()) throw std::runtime_error ("invalid iterator"); ptr.setContainerStore (mContainer); return ptr; } template MWWorld::ContainerStoreIteratorBase& MWWorld::ContainerStoreIteratorBase::operator++() { do { if (incIterator()) nextType(); } while (mType!=-1 && !(**this).getRefData().getCount()); return *this; } template MWWorld::ContainerStoreIteratorBase MWWorld::ContainerStoreIteratorBase::operator++ (int) { ContainerStoreIteratorBase iter (*this); ++*this; return iter; } template MWWorld::ContainerStoreIteratorBase& MWWorld::ContainerStoreIteratorBase::operator= (const ContainerStoreIteratorBase& rhs) { if (this!=&rhs) { copy(rhs); } return *this; } template int MWWorld::ContainerStoreIteratorBase::getType() const { return mType; } template const MWWorld::ContainerStore *MWWorld::ContainerStoreIteratorBase::getContainerStore() const { return mContainer; } template MWWorld::ContainerStoreIteratorBase::ContainerStoreIteratorBase (ContainerStoreType container) : mType (-1), mMask (0), mContainer (container) {} template MWWorld::ContainerStoreIteratorBase::ContainerStoreIteratorBase (int mask, ContainerStoreType container) : mType (0), mMask (mask), mContainer (container) { nextType(); if (mType==-1 || (**this).getRefData().getCount()) return; ++*this; } template MWWorld::ContainerStoreIteratorBase::ContainerStoreIteratorBase (ContainerStoreType container, typename Iterator::type iterator) : mType(MWWorld::ContainerStore::Type_Potion), mMask(MWWorld::ContainerStore::Type_All), mContainer(container), mPotion(iterator){} template MWWorld::ContainerStoreIteratorBase::ContainerStoreIteratorBase (ContainerStoreType container, typename Iterator::type iterator) : mType(MWWorld::ContainerStore::Type_Apparatus), mMask(MWWorld::ContainerStore::Type_All), mContainer(container), mApparatus(iterator){} template MWWorld::ContainerStoreIteratorBase::ContainerStoreIteratorBase (ContainerStoreType container, typename Iterator::type iterator) : mType(MWWorld::ContainerStore::Type_Armor), mMask(MWWorld::ContainerStore::Type_All), mContainer(container), mArmor(iterator){} template MWWorld::ContainerStoreIteratorBase::ContainerStoreIteratorBase (ContainerStoreType container, typename Iterator::type iterator) : mType(MWWorld::ContainerStore::Type_Book), mMask(MWWorld::ContainerStore::Type_All), mContainer(container), mBook(iterator){} template MWWorld::ContainerStoreIteratorBase::ContainerStoreIteratorBase (ContainerStoreType container, typename Iterator::type iterator) : mType(MWWorld::ContainerStore::Type_Clothing), mMask(MWWorld::ContainerStore::Type_All), mContainer(container), mClothing(iterator){} template MWWorld::ContainerStoreIteratorBase::ContainerStoreIteratorBase (ContainerStoreType container, typename Iterator::type iterator) : mType(MWWorld::ContainerStore::Type_Ingredient), mMask(MWWorld::ContainerStore::Type_All), mContainer(container), mIngredient(iterator){} template MWWorld::ContainerStoreIteratorBase::ContainerStoreIteratorBase (ContainerStoreType container, typename Iterator::type iterator) : mType(MWWorld::ContainerStore::Type_Light), mMask(MWWorld::ContainerStore::Type_All), mContainer(container), mLight(iterator){} template MWWorld::ContainerStoreIteratorBase::ContainerStoreIteratorBase (ContainerStoreType container, typename Iterator::type iterator) : mType(MWWorld::ContainerStore::Type_Lockpick), mMask(MWWorld::ContainerStore::Type_All), mContainer(container), mLockpick(iterator){} template MWWorld::ContainerStoreIteratorBase::ContainerStoreIteratorBase (ContainerStoreType container, typename Iterator::type iterator) : mType(MWWorld::ContainerStore::Type_Miscellaneous), mMask(MWWorld::ContainerStore::Type_All), mContainer(container), mMiscellaneous(iterator){} template MWWorld::ContainerStoreIteratorBase::ContainerStoreIteratorBase (ContainerStoreType container, typename Iterator::type iterator) : mType(MWWorld::ContainerStore::Type_Probe), mMask(MWWorld::ContainerStore::Type_All), mContainer(container), mProbe(iterator){} template MWWorld::ContainerStoreIteratorBase::ContainerStoreIteratorBase (ContainerStoreType container, typename Iterator::type iterator) : mType(MWWorld::ContainerStore::Type_Repair), mMask(MWWorld::ContainerStore::Type_All), mContainer(container), mRepair(iterator){} template MWWorld::ContainerStoreIteratorBase::ContainerStoreIteratorBase (ContainerStoreType container, typename Iterator::type iterator) : mType(MWWorld::ContainerStore::Type_Weapon), mMask(MWWorld::ContainerStore::Type_All), mContainer(container), mWeapon(iterator){} template bool MWWorld::operator== (const ContainerStoreIteratorBase& left, const ContainerStoreIteratorBase& right) { return left.isEqual (right); } template bool MWWorld::operator!= (const ContainerStoreIteratorBase& left, const ContainerStoreIteratorBase& right) { return !(left==right); } template class MWWorld::ContainerStoreIteratorBase; template class MWWorld::ContainerStoreIteratorBase; template bool MWWorld::operator== (const ContainerStoreIteratorBase& left, const ContainerStoreIteratorBase& right); template bool MWWorld::operator!= (const ContainerStoreIteratorBase& left, const ContainerStoreIteratorBase& right); template bool MWWorld::operator== (const ContainerStoreIteratorBase& left, const ContainerStoreIteratorBase& right); template bool MWWorld::operator!= (const ContainerStoreIteratorBase& left, const ContainerStoreIteratorBase& right); template bool MWWorld::operator== (const ContainerStoreIteratorBase& left, const ContainerStoreIteratorBase& right); template bool MWWorld::operator!= (const ContainerStoreIteratorBase& left, const ContainerStoreIteratorBase& right); template bool MWWorld::operator== (const ContainerStoreIteratorBase& left, const ContainerStoreIteratorBase& right); template bool MWWorld::operator!= (const ContainerStoreIteratorBase& left, const ContainerStoreIteratorBase& right); template void MWWorld::ContainerStoreIteratorBase::copy(const ContainerStoreIteratorBase& src); template void MWWorld::ContainerStoreIteratorBase::copy(const ContainerStoreIteratorBase& src); template void MWWorld::ContainerStoreIteratorBase::copy(const ContainerStoreIteratorBase& src); ================================================ FILE: apps/openmw/mwworld/containerstore.hpp ================================================ #ifndef GAME_MWWORLD_CONTAINERSTORE_H #define GAME_MWWORLD_CONTAINERSTORE_H #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "ptr.hpp" #include "cellreflist.hpp" namespace ESM { struct InventoryList; struct InventoryState; } namespace MWClass { class Container; } namespace MWWorld { class ContainerStore; template class ContainerStoreIteratorBase; typedef ContainerStoreIteratorBase ContainerStoreIterator; typedef ContainerStoreIteratorBase ConstContainerStoreIterator; class ResolutionListener { ContainerStore& mStore; public: ResolutionListener(ContainerStore& store) : mStore(store) {} ~ResolutionListener(); }; class ResolutionHandle { std::shared_ptr mListener; public: ResolutionHandle(std::shared_ptr listener) : mListener(listener) {} ResolutionHandle() {} }; class ContainerStoreListener { public: virtual void itemAdded(const ConstPtr& item, int count) {} virtual void itemRemoved(const ConstPtr& item, int count) {} virtual ~ContainerStoreListener() = default; }; class ContainerStore { public: static constexpr int Type_Potion = 0x0001; static constexpr int Type_Apparatus = 0x0002; static constexpr int Type_Armor = 0x0004; static constexpr int Type_Book = 0x0008; static constexpr int Type_Clothing = 0x0010; static constexpr int Type_Ingredient = 0x0020; static constexpr int Type_Light = 0x0040; static constexpr int Type_Lockpick = 0x0080; static constexpr int Type_Miscellaneous = 0x0100; static constexpr int Type_Probe = 0x0200; static constexpr int Type_Repair = 0x0400; static constexpr int Type_Weapon = 0x0800; static constexpr int Type_Last = Type_Weapon; static constexpr int Type_All = 0xffff; static const std::string sGoldId; protected: ContainerStoreListener* mListener; // (item, max charge) typedef std::vector > TRechargingItems; TRechargingItems mRechargingItems; bool mRechargingItemsUpToDate; private: MWWorld::CellRefList potions; MWWorld::CellRefList appas; MWWorld::CellRefList armors; MWWorld::CellRefList books; MWWorld::CellRefList clothes; MWWorld::CellRefList ingreds; MWWorld::CellRefList lights; MWWorld::CellRefList lockpicks; MWWorld::CellRefList miscItems; MWWorld::CellRefList probes; MWWorld::CellRefList repairs; MWWorld::CellRefList weapons; mutable float mCachedWeight; mutable bool mWeightUpToDate; bool mModified; bool mResolved; unsigned int mSeed; MWWorld::Ptr mPtr; std::weak_ptr mResolutionListener; ContainerStoreIterator addImp (const Ptr& ptr, int count, bool markModified = true); void addInitialItem (const std::string& id, const std::string& owner, int count, Misc::Rng::Seed* seed, bool topLevel=true); void addInitialItemImp (const MWWorld::Ptr& ptr, const std::string& owner, int count, Misc::Rng::Seed* seed, bool topLevel=true); template ContainerStoreIterator getState (CellRefList& collection, const ESM::ObjectState& state); template void storeState (const LiveCellRef& ref, ESM::ObjectState& state) const; template void storeStates (const CellRefList& collection, ESM::InventoryState& inventory, int& index, bool equipable = false) const; void updateRechargingItems(); virtual void storeEquipmentState (const MWWorld::LiveCellRefBase& ref, int index, ESM::InventoryState& inventory) const; virtual void readEquipmentState (const MWWorld::ContainerStoreIterator& iter, int index, const ESM::InventoryState& inventory); public: ContainerStore(); virtual ~ContainerStore(); virtual std::unique_ptr clone() { return std::make_unique(*this); } ConstContainerStoreIterator cbegin (int mask = Type_All) const; ConstContainerStoreIterator cend() const; ConstContainerStoreIterator begin (int mask = Type_All) const; ConstContainerStoreIterator end() const; ContainerStoreIterator begin (int mask = Type_All); ContainerStoreIterator end(); bool hasVisibleItems() const; virtual ContainerStoreIterator add (const Ptr& itemPtr, int count, const Ptr& actorPtr, bool allowAutoEquip = true, bool resolve = true); ///< Add the item pointed to by \a ptr to this container. (Stacks automatically if needed) /// /// \note The item pointed to is not required to exist beyond this function call. /// /// \attention Do not add items to an existing stack by increasing the count instead of /// calling this function! /// /// @return if stacking happened, return iterator to the item that was stacked against, otherwise iterator to the newly inserted item. ContainerStoreIterator add(const std::string& id, int count, const Ptr& actorPtr); ///< Utility to construct a ManualRef and call add(ptr, count, actorPtr, true) int remove(const std::string& itemId, int count, const Ptr& actor, bool equipReplacement = 0, bool resolve = true); ///< Remove \a count item(s) designated by \a itemId from this container. /// /// @return the number of items actually removed virtual int remove(const Ptr& item, int count, const Ptr& actor, bool equipReplacement = 0, bool resolve = true); ///< Remove \a count item(s) designated by \a item from this inventory. /// /// @return the number of items actually removed void rechargeItems (float duration); ///< Restore charge on enchanted items. Note this should only be done for the player. ContainerStoreIterator unstack (const Ptr& ptr, const Ptr& container, int count = 1); ///< Unstack an item in this container. The item's count will be set to count, then a new stack will be added with (origCount-count). /// /// @return an iterator to the new stack, or end() if no new stack was created. MWWorld::ContainerStoreIterator restack (const MWWorld::Ptr& item); ///< Attempt to re-stack an item in this container. /// If a compatible stack is found, the item's count is added to that stack, then the original is deleted. /// @return If the item was stacked, return the stack, otherwise return the old (untouched) item. int count (const std::string& id) const; ///< @return How many items with refID \a id are in this container? ContainerStoreListener* getContListener() const; void setContListener(ContainerStoreListener* listener); protected: ContainerStoreIterator addNewStack (const ConstPtr& ptr, int count); ///< Add the item to this container (do not try to stack it onto existing items) virtual void flagAsModified(); /// + and - operations that can deal with negative stacks /// Note that negativity is infectious static int addItems(int count1, int count2); static int subtractItems(int count1, int count2); public: virtual bool stacks (const ConstPtr& ptr1, const ConstPtr& ptr2) const; ///< @return true if the two specified objects can stack with each other void fill (const ESM::InventoryList& items, const std::string& owner, Misc::Rng::Seed& seed = Misc::Rng::getSeed()); ///< Insert items into *this. void fillNonRandom (const ESM::InventoryList& items, const std::string& owner, unsigned int seed); ///< Insert items into *this, excluding leveled items virtual void clear(); ///< Empty container. float getWeight() const; ///< Return total weight of the items contained in *this. static int getType (const ConstPtr& ptr); ///< This function throws an exception, if ptr does not point to an object, that can be /// put into a container. Ptr findReplacement(const std::string& id); ///< Returns replacement for object with given id. Prefer used items (with low durability left). Ptr search (const std::string& id); virtual void writeState (ESM::InventoryState& state) const; virtual void readState (const ESM::InventoryState& state); bool isResolved() const; /* Start of tes3mp addiition Make it possible to set the container's resolved state from elsewhere, to avoid unnecessary refills before overriding its contents */ void setResolved(bool state); /* End of tes3mp addition */ void resolve(); ResolutionHandle resolveTemporarily(); void unresolve(); friend class ContainerStoreIteratorBase; friend class ContainerStoreIteratorBase; friend class ResolutionListener; friend class MWClass::Container; }; template class ContainerStoreIteratorBase : public std::iterator { template struct IsConvertible { static constexpr bool value = true; }; template struct IsConvertible { static constexpr bool value = false; }; template struct IteratorTrait { typedef typename MWWorld::CellRefList::List::iterator type; }; template struct IteratorTrait { typedef typename MWWorld::CellRefList::List::const_iterator type; }; template struct Iterator : IteratorTrait { }; template struct ContainerStoreTrait { typedef ContainerStore* type; }; template struct ContainerStoreTrait { typedef const ContainerStore* type; }; typedef typename ContainerStoreTrait::type ContainerStoreType; int mType; int mMask; ContainerStoreType mContainer; mutable PtrType mPtr; typename Iterator::type mPotion; typename Iterator::type mApparatus; typename Iterator::type mArmor; typename Iterator::type mBook; typename Iterator::type mClothing; typename Iterator::type mIngredient; typename Iterator::type mLight; typename Iterator::type mLockpick; typename Iterator::type mMiscellaneous; typename Iterator::type mProbe; typename Iterator::type mRepair; typename Iterator::type mWeapon; ContainerStoreIteratorBase (ContainerStoreType container); ///< End-iterator ContainerStoreIteratorBase (int mask, ContainerStoreType container); ///< Begin-iterator // construct iterator using a CellRefList iterator ContainerStoreIteratorBase (ContainerStoreType container, typename Iterator::type); ContainerStoreIteratorBase (ContainerStoreType container, typename Iterator::type); ContainerStoreIteratorBase (ContainerStoreType container, typename Iterator::type); ContainerStoreIteratorBase (ContainerStoreType container, typename Iterator::type); ContainerStoreIteratorBase (ContainerStoreType container, typename Iterator::type); ContainerStoreIteratorBase (ContainerStoreType container, typename Iterator::type); ContainerStoreIteratorBase (ContainerStoreType container, typename Iterator::type); ContainerStoreIteratorBase (ContainerStoreType container, typename Iterator::type); ContainerStoreIteratorBase (ContainerStoreType container, typename Iterator::type); ContainerStoreIteratorBase (ContainerStoreType container, typename Iterator::type); ContainerStoreIteratorBase (ContainerStoreType container, typename Iterator::type); ContainerStoreIteratorBase (ContainerStoreType container, typename Iterator::type); template void copy (const ContainerStoreIteratorBase& src); void incType (); void nextType (); bool resetIterator (); ///< Reset iterator for selected type. /// /// \return Type not empty? bool incIterator (); ///< Increment iterator for selected type. /// /// \return reached the end? public: template ContainerStoreIteratorBase (const ContainerStoreIteratorBase& other) { char CANNOT_CONVERT_CONST_ITERATOR_TO_ITERATOR[IsConvertible::value ? 1 : -1]; ((void)CANNOT_CONVERT_CONST_ITERATOR_TO_ITERATOR); copy (other); } template bool isEqual(const ContainerStoreIteratorBase& other) const; PtrType *operator->() const; PtrType operator*() const; ContainerStoreIteratorBase& operator++ (); ContainerStoreIteratorBase operator++ (int); ContainerStoreIteratorBase& operator= (const ContainerStoreIteratorBase& rhs); ContainerStoreIteratorBase (const ContainerStoreIteratorBase& rhs) = default; int getType() const; const ContainerStore *getContainerStore() const; friend class ContainerStore; friend class ContainerStoreIteratorBase; friend class ContainerStoreIteratorBase; }; template bool operator== (const ContainerStoreIteratorBase& left, const ContainerStoreIteratorBase& right); template bool operator!= (const ContainerStoreIteratorBase& left, const ContainerStoreIteratorBase& right); } #endif ================================================ FILE: apps/openmw/mwworld/contentloader.hpp ================================================ #ifndef CONTENTLOADER_HPP #define CONTENTLOADER_HPP #include #include #include #include "components/loadinglistener/loadinglistener.hpp" 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())); } protected: Loading::Listener& mListener; }; } /* namespace MWWorld */ #endif /* CONTENTLOADER_HPP */ ================================================ FILE: apps/openmw/mwworld/customdata.cpp ================================================ #include "customdata.hpp" #include #include #include namespace MWWorld { MWClass::CreatureCustomData &CustomData::asCreatureCustomData() { std::stringstream error; error << "bad cast " << typeid(this).name() << " to CreatureCustomData"; throw std::logic_error(error.str()); } const MWClass::CreatureCustomData &CustomData::asCreatureCustomData() const { std::stringstream error; error << "bad cast " << typeid(this).name() << " to CreatureCustomData"; throw std::logic_error(error.str()); } MWClass::NpcCustomData &CustomData::asNpcCustomData() { std::stringstream error; error << "bad cast " << typeid(this).name() << " to NpcCustomData"; throw std::logic_error(error.str()); } const MWClass::NpcCustomData &CustomData::asNpcCustomData() const { std::stringstream error; error << "bad cast " << typeid(this).name() << " to NpcCustomData"; throw std::logic_error(error.str()); } MWClass::ContainerCustomData &CustomData::asContainerCustomData() { std::stringstream error; error << "bad cast " << typeid(this).name() << " to ContainerCustomData"; throw std::logic_error(error.str()); } const MWClass::ContainerCustomData &CustomData::asContainerCustomData() const { std::stringstream error; error << "bad cast " << typeid(this).name() << " to ContainerCustomData"; throw std::logic_error(error.str()); } MWClass::DoorCustomData &CustomData::asDoorCustomData() { std::stringstream error; error << "bad cast " << typeid(this).name() << " to DoorCustomData"; throw std::logic_error(error.str()); } const MWClass::DoorCustomData &CustomData::asDoorCustomData() const { std::stringstream error; error << "bad cast " << typeid(this).name() << " to DoorCustomData"; throw std::logic_error(error.str()); } MWClass::CreatureLevListCustomData &CustomData::asCreatureLevListCustomData() { std::stringstream error; error << "bad cast " << typeid(this).name() << " to CreatureLevListCustomData"; throw std::logic_error(error.str()); } const MWClass::CreatureLevListCustomData &CustomData::asCreatureLevListCustomData() const { std::stringstream error; error << "bad cast " << typeid(this).name() << " to CreatureLevListCustomData"; throw std::logic_error(error.str()); } } ================================================ FILE: apps/openmw/mwworld/customdata.hpp ================================================ #ifndef GAME_MWWORLD_CUSTOMDATA_H #define GAME_MWWORLD_CUSTOMDATA_H #include namespace MWClass { class CreatureCustomData; class NpcCustomData; class ContainerCustomData; class DoorCustomData; class CreatureLevListCustomData; } namespace MWWorld { /// \brief Base class for the MW-class-specific part of RefData class CustomData { public: virtual ~CustomData() {} virtual std::unique_ptr clone() const = 0; // Fast version of dynamic_cast. Needs to be overridden in the respective class. virtual MWClass::CreatureCustomData& asCreatureCustomData(); virtual const MWClass::CreatureCustomData& asCreatureCustomData() const; virtual MWClass::NpcCustomData& asNpcCustomData(); virtual const MWClass::NpcCustomData& asNpcCustomData() const; virtual MWClass::ContainerCustomData& asContainerCustomData(); virtual const MWClass::ContainerCustomData& asContainerCustomData() const; virtual MWClass::DoorCustomData& asDoorCustomData(); virtual const MWClass::DoorCustomData& asDoorCustomData() const; virtual MWClass::CreatureLevListCustomData& asCreatureLevListCustomData(); virtual const MWClass::CreatureLevListCustomData& asCreatureLevListCustomData() const; }; template struct TypedCustomData : CustomData { std::unique_ptr clone() const final { return std::make_unique(*static_cast(this)); } }; } #endif ================================================ FILE: apps/openmw/mwworld/datetimemanager.cpp ================================================ #include "datetimemanager.hpp" #include "../mwbase/environment.hpp" #include "../mwbase/world.hpp" #include "esmstore.hpp" #include "globals.hpp" #include "timestamp.hpp" namespace { static int getDaysPerMonth(int month) { switch (month) { case 0: return 31; case 1: return 28; case 2: return 31; case 3: return 30; case 4: return 31; case 5: return 30; case 6: return 31; case 7: return 31; case 8: return 30; case 9: return 31; case 10: return 30; case 11: return 31; } throw std::runtime_error ("month out of range"); } } namespace MWWorld { void DateTimeManager::setup(Globals& globalVariables) { mGameHour = globalVariables["gamehour"].getFloat(); mDaysPassed = globalVariables["dayspassed"].getInteger(); mDay = globalVariables["day"].getInteger(); mMonth = globalVariables["month"].getInteger(); mYear = globalVariables["year"].getInteger(); mTimeScale = globalVariables["timescale"].getFloat(); } void DateTimeManager::setHour(double hour) { if (hour < 0) hour = 0; int days = static_cast(hour / 24); hour = std::fmod(hour, 24); mGameHour = static_cast(hour); if (days > 0) setDay(days + mDay); } void DateTimeManager::setDay(int day) { if (day < 1) day = 1; int month = mMonth; while (true) { int days = getDaysPerMonth(month); if (day <= days) break; if (month < 11) { ++month; } else { month = 0; mYear++; } day -= days; } mDay = day; mMonth = month; } TimeStamp DateTimeManager::getTimeStamp() const { return TimeStamp(mGameHour, mDaysPassed); } float DateTimeManager::getTimeScaleFactor() const { return mTimeScale; } ESM::EpochTimeStamp DateTimeManager::getEpochTimeStamp() const { ESM::EpochTimeStamp timeStamp; timeStamp.mGameHour = mGameHour; timeStamp.mDay = mDay; timeStamp.mMonth = mMonth; timeStamp.mYear = mYear; return timeStamp; } void DateTimeManager::setMonth(int month) { if (month < 0) month = 0; int years = month / 12; month = month % 12; int days = getDaysPerMonth(month); if (mDay > days) mDay = days; mMonth = month; if (years > 0) mYear += years; } void DateTimeManager::advanceTime(double hours, Globals& globalVariables) { hours += mGameHour; setHour(hours); int days = static_cast(hours / 24); if (days > 0) mDaysPassed += days; globalVariables["gamehour"].setFloat(mGameHour); globalVariables["dayspassed"].setInteger(mDaysPassed); globalVariables["day"].setInteger(mDay); globalVariables["month"].setInteger(mMonth); globalVariables["year"].setInteger(mYear); } std::string DateTimeManager::getMonthName(int month) const { if (month == -1) month = mMonth; const int months = 12; if (month < 0 || month >= months) return std::string(); static const char *monthNames[months] = { "sMonthMorningstar", "sMonthSunsdawn", "sMonthFirstseed", "sMonthRainshand", "sMonthSecondseed", "sMonthMidyear", "sMonthSunsheight", "sMonthLastseed", "sMonthHeartfire", "sMonthFrostfall", "sMonthSunsdusk", "sMonthEveningstar" }; const ESM::GameSetting *setting = MWBase::Environment::get().getWorld()->getStore().get().find(monthNames[month]); return setting->mValue.getString(); } bool DateTimeManager::updateGlobalFloat(const std::string& name, float value) { if (name=="gamehour") { setHour(value); return true; } else if (name=="day") { setDay(static_cast(value)); return true; } else if (name=="month") { setMonth(static_cast(value)); return true; } else if (name=="year") { mYear = static_cast(value); } else if (name=="timescale") { mTimeScale = value; } else if (name=="dayspassed") { mDaysPassed = static_cast(value); } return false; } bool DateTimeManager::updateGlobalInt(const std::string& name, int value) { if (name=="gamehour") { setHour(static_cast(value)); return true; } else if (name=="day") { setDay(value); return true; } else if (name=="month") { setMonth(value); return true; } else if (name=="year") { mYear = value; } else if (name=="timescale") { mTimeScale = static_cast(value); } else if (name=="dayspassed") { mDaysPassed = value; } return false; } } ================================================ FILE: apps/openmw/mwworld/datetimemanager.hpp ================================================ #ifndef GAME_MWWORLD_DATETIMEMANAGER_H #define GAME_MWWORLD_DATETIMEMANAGER_H #include namespace ESM { struct EpochTimeStamp; } namespace MWWorld { class Globals; class TimeStamp; class DateTimeManager { int mDaysPassed = 0; int mDay = 0; int mMonth = 0; int mYear = 0; float mGameHour = 0.f; float mTimeScale = 0.f; void setHour(double hour); void setDay(int day); void setMonth(int month); public: std::string getMonthName(int month) const; TimeStamp getTimeStamp() const; ESM::EpochTimeStamp getEpochTimeStamp() const; float getTimeScaleFactor() const; void advanceTime(double hours, Globals& globalVariables); void setup(Globals& globalVariables); bool updateGlobalInt(const std::string& name, int value); bool updateGlobalFloat(const std::string& name, float value); }; } #endif ================================================ FILE: apps/openmw/mwworld/doorstate.hpp ================================================ #ifndef GAME_MWWORLD_DOORSTATE_H #define GAME_MWWORLD_DOORSTATE_H namespace MWWorld { enum class DoorState { Idle = 0, Opening = 1, Closing = 2, }; } #endif ================================================ FILE: apps/openmw/mwworld/esmloader.cpp ================================================ #include "esmloader.hpp" #include "esmstore.hpp" #include namespace MWWorld { EsmLoader::EsmLoader(MWWorld::ESMStore& store, std::vector& readers, ToUTF8::Utf8Encoder* encoder, Loading::Listener& listener) : ContentLoader(listener) , mEsm(readers) , mStore(store) , mEncoder(encoder) { } void EsmLoader::load(const boost::filesystem::path& filepath, int& index) { ContentLoader::load(filepath.filename(), index); ESM::ESMReader lEsm; lEsm.setEncoder(mEncoder); lEsm.setIndex(index); lEsm.setGlobalReaderList(&mEsm); lEsm.open(filepath.string()); mEsm[index] = lEsm; mStore.load(mEsm[index], &mListener); } } /* namespace MWWorld */ ================================================ FILE: apps/openmw/mwworld/esmloader.hpp ================================================ #ifndef ESMLOADER_HPP #define ESMLOADER_HPP #include #include "contentloader.hpp" namespace ToUTF8 { class Utf8Encoder; } namespace ESM { class ESMReader; } namespace MWWorld { class ESMStore; struct EsmLoader : public ContentLoader { EsmLoader(MWWorld::ESMStore& store, std::vector& readers, ToUTF8::Utf8Encoder* encoder, Loading::Listener& listener); void load(const boost::filesystem::path& filepath, int& index) override; private: std::vector& mEsm; MWWorld::ESMStore& mStore; ToUTF8::Utf8Encoder* mEncoder; }; } /* namespace MWWorld */ #endif // ESMLOADER_HPP ================================================ FILE: apps/openmw/mwworld/esmstore.cpp ================================================ #include "esmstore.hpp" #include #include #include #include #include #include #include #include #include "../mwmechanics/spelllist.hpp" namespace { struct Ref { ESM::RefNum mRefNum; std::size_t mRefID; Ref(ESM::RefNum refNum, std::size_t refID) : mRefNum(refNum), mRefID(refID) {} }; constexpr std::size_t deletedRefID = std::numeric_limits::max(); void readRefs(const ESM::Cell& cell, std::vector& refs, std::vector& refIDs, std::vector& readers) { for (size_t i = 0; i < cell.mContextList.size(); i++) { size_t index = cell.mContextList[i].index; if (readers.size() <= index) readers.resize(index + 1); cell.restore(readers[index], i); ESM::CellRef ref; ref.mRefNum.mContentFile = ESM::RefNum::RefNum_NoContentFile; bool deleted = false; while(cell.getNextRef(readers[index], ref, deleted)) { if(deleted) refs.emplace_back(ref.mRefNum, deletedRefID); else if (std::find(cell.mMovedRefs.begin(), cell.mMovedRefs.end(), ref.mRefNum) == cell.mMovedRefs.end()) { refs.emplace_back(ref.mRefNum, refIDs.size()); refIDs.push_back(std::move(ref.mRefID)); } } } for(const auto& [value, deleted] : cell.mLeasedRefs) { if(deleted) refs.emplace_back(value.mRefNum, deletedRefID); else { refs.emplace_back(value.mRefNum, refIDs.size()); refIDs.push_back(value.mRefID); } } } std::vector getNPCsToReplace(const MWWorld::Store& factions, const MWWorld::Store& classes, const std::map& npcs) { // Cache first class from store - we will use it if current class is not found std::string defaultCls; auto it = classes.begin(); if (it != classes.end()) defaultCls = it->mId; else throw std::runtime_error("List of NPC classes is empty!"); // Validate NPCs for non-existing class and faction. // We will replace invalid entries by fixed ones std::vector npcsToReplace; for (const auto& npcIter : npcs) { ESM::NPC npc = npcIter.second; bool changed = false; const std::string npcFaction = npc.mFaction; if (!npcFaction.empty()) { const ESM::Faction *fact = factions.search(npcFaction); if (!fact) { Log(Debug::Verbose) << "NPC '" << npc.mId << "' (" << npc.mName << ") has nonexistent faction '" << npc.mFaction << "', ignoring it."; npc.mFaction.clear(); npc.mNpdt.mRank = 0; changed = true; } } std::string npcClass = npc.mClass; if (!npcClass.empty()) { const ESM::Class *cls = classes.search(npcClass); if (!cls) { Log(Debug::Verbose) << "NPC '" << npc.mId << "' (" << npc.mName << ") has nonexistent class '" << npc.mClass << "', using '" << defaultCls << "' class as replacement."; npc.mClass = defaultCls; changed = true; } } if (changed) npcsToReplace.push_back(npc); } return npcsToReplace; } } namespace MWWorld { static bool isCacheableRecord(int id) { if (id == ESM::REC_ACTI || id == ESM::REC_ALCH || id == ESM::REC_APPA || id == ESM::REC_ARMO || id == ESM::REC_BOOK || id == ESM::REC_CLOT || id == ESM::REC_CONT || id == ESM::REC_CREA || id == ESM::REC_DOOR || id == ESM::REC_INGR || id == ESM::REC_LEVC || id == ESM::REC_LEVI || id == ESM::REC_LIGH || id == ESM::REC_LOCK || id == ESM::REC_MISC || id == ESM::REC_NPC_ || id == ESM::REC_PROB || id == ESM::REC_REPA || id == ESM::REC_STAT || id == ESM::REC_WEAP || id == ESM::REC_BODY) { return true; } return false; } void ESMStore::load(ESM::ESMReader &esm, Loading::Listener* listener) { listener->setProgressRange(1000); ESM::Dialogue *dialogue = nullptr; // Land texture loading needs to use a separate internal store for each plugin. // We set the number of plugins here to avoid continual resizes during loading, // and so we can properly verify if valid plugin indices are being passed to the // LandTexture Store retrieval methods. mLandTextures.resize(esm.getGlobalReaderList()->size()); /// \todo Move this to somewhere else. ESMReader? // Cache parent esX files by tracking their indices in the global list of // all files/readers used by the engine. This will greaty accelerate // refnumber mangling, as required for handling moved references. const std::vector &masters = esm.getGameFiles(); std::vector *allPlugins = esm.getGlobalReaderList(); for (size_t j = 0; j < masters.size(); j++) { const ESM::Header::MasterData &mast = masters[j]; std::string fname = mast.name; int index = ~0; for (int i = 0; i < esm.getIndex(); i++) { const std::string candidate = allPlugins->at(i).getContext().filename; std::string fnamecandidate = boost::filesystem::path(candidate).filename().string(); if (Misc::StringUtils::ciEqual(fname, fnamecandidate)) { index = i; break; } } if (index == (int)~0) { // Tried to load a parent file that has not been loaded yet. This is bad, // the launcher should have taken care of this. std::string fstring = "File " + esm.getName() + " asks for parent file " + masters[j].name + ", but it has not been loaded yet. Please check your load order."; esm.fail(fstring); } esm.addParentFileIndex(index); } // Loop through all records while(esm.hasMoreRecs()) { ESM::NAME n = esm.getRecName(); esm.getRecHeader(); // Look up the record type. std::map::iterator it = mStores.find(n.intval); if (it == mStores.end()) { if (n.intval == ESM::REC_INFO) { if (dialogue) { dialogue->readInfo(esm, esm.getIndex() != 0); } else { Log(Debug::Error) << "Error: info record without dialog"; esm.skipRecord(); } } else if (n.intval == ESM::REC_MGEF) { mMagicEffects.load (esm); } else if (n.intval == ESM::REC_SKIL) { mSkills.load (esm); } else if (n.intval==ESM::REC_FILT || n.intval == ESM::REC_DBGP) { // ignore project file only records esm.skipRecord(); } else { std::stringstream error; error << "Unknown record: " << n.toString(); throw std::runtime_error(error.str()); } } else { RecordId id = it->second->load(esm); if (id.mIsDeleted) { it->second->eraseStatic(id.mId); continue; } if (n.intval==ESM::REC_DIAL) { dialogue = const_cast(mDialogs.find(id.mId)); } else { dialogue = nullptr; } } listener->setProgress(static_cast(esm.getFileOffset() / (float)esm.getFileSize() * 1000)); } } void ESMStore::setUp(bool validateRecords) { mIds.clear(); std::map::iterator storeIt = mStores.begin(); for (; storeIt != mStores.end(); ++storeIt) { storeIt->second->setUp(); if (isCacheableRecord(storeIt->first)) { std::vector identifiers; storeIt->second->listIdentifier(identifiers); for (std::vector::const_iterator record = identifiers.begin(); record != identifiers.end(); ++record) mIds[*record] = storeIt->first; } } if (mStaticIds.empty()) mStaticIds = mIds; mSkills.setUp(); mMagicEffects.setUp(); mAttributes.setUp(); mDialogs.setUp(); if (validateRecords) { validate(); countRecords(); } } void ESMStore::countRecords() { if(!mRefCount.empty()) return; std::vector refs; std::vector refIDs; std::vector readers; for(auto it = mCells.intBegin(); it != mCells.intEnd(); it++) readRefs(*it, refs, refIDs, readers); for(auto it = mCells.extBegin(); it != mCells.extEnd(); it++) readRefs(*it, refs, refIDs, readers); const auto lessByRefNum = [] (const Ref& l, const Ref& r) { return l.mRefNum < r.mRefNum; }; std::stable_sort(refs.begin(), refs.end(), lessByRefNum); const auto equalByRefNum = [] (const Ref& l, const Ref& r) { return l.mRefNum == r.mRefNum; }; const auto incrementRefCount = [&] (const Ref& value) { if (value.mRefID != deletedRefID) { std::string& refId = refIDs[value.mRefID]; Misc::StringUtils::lowerCaseInPlace(refId); ++mRefCount[std::move(refId)]; } }; Misc::forEachUnique(refs.rbegin(), refs.rend(), equalByRefNum, incrementRefCount); } int ESMStore::getRefCount(const std::string& id) const { const std::string lowerId = Misc::StringUtils::lowerCase(id); auto it = mRefCount.find(lowerId); if(it == mRefCount.end()) return 0; return it->second; } void ESMStore::validate() { std::vector npcsToReplace = getNPCsToReplace(mFactions, mClasses, mNpcs.mStatic); for (const ESM::NPC &npc : npcsToReplace) { mNpcs.eraseStatic(npc.mId); mNpcs.insertStatic(npc); } // Validate spell effects for invalid arguments std::vector spellsToReplace; for (ESM::Spell spell : mSpells) { if (spell.mEffects.mList.empty()) continue; bool changed = false; auto iter = spell.mEffects.mList.begin(); while (iter != spell.mEffects.mList.end()) { const ESM::MagicEffect* mgef = mMagicEffects.search(iter->mEffectID); if (!mgef) { Log(Debug::Verbose) << "Spell '" << spell.mId << "' has an invalid effect (index " << iter->mEffectID << ") present. Dropping the effect."; iter = spell.mEffects.mList.erase(iter); changed = true; continue; } if (mgef->mData.mFlags & ESM::MagicEffect::TargetSkill) { if (iter->mAttribute != -1) { iter->mAttribute = -1; Log(Debug::Verbose) << ESM::MagicEffect::effectIdToString(iter->mEffectID) << " effect of spell '" << spell.mId << "' has an attribute argument present. Dropping the argument."; changed = true; } } else if (mgef->mData.mFlags & ESM::MagicEffect::TargetAttribute) { if (iter->mSkill != -1) { iter->mSkill = -1; Log(Debug::Verbose) << ESM::MagicEffect::effectIdToString(iter->mEffectID) << " effect of spell '" << spell.mId << "' has a skill argument present. Dropping the argument."; changed = true; } } else if (iter->mSkill != -1 || iter->mAttribute != -1) { iter->mSkill = -1; iter->mAttribute = -1; Log(Debug::Verbose) << ESM::MagicEffect::effectIdToString(iter->mEffectID) << " effect of spell '" << spell.mId << "' has argument(s) present. Dropping the argument(s)."; changed = true; } ++iter; } if (changed) spellsToReplace.emplace_back(spell); } for (const ESM::Spell &spell : spellsToReplace) { mSpells.eraseStatic(spell.mId); mSpells.insertStatic(spell); } } void ESMStore::validateDynamic() { std::vector npcsToReplace = getNPCsToReplace(mFactions, mClasses, mNpcs.mDynamic); for (const ESM::NPC &npc : npcsToReplace) mNpcs.insert(npc); } int ESMStore::countSavedGameRecords() const { return 1 // DYNA (dynamic name counter) +mPotions.getDynamicSize() +mArmors.getDynamicSize() +mBooks.getDynamicSize() +mClasses.getDynamicSize() +mClothes.getDynamicSize() +mEnchants.getDynamicSize() +mNpcs.getDynamicSize() +mSpells.getDynamicSize() +mWeapons.getDynamicSize() +mCreatureLists.getDynamicSize() +mItemLists.getDynamicSize() +mCreatures.getDynamicSize() +mContainers.getDynamicSize(); } void ESMStore::write (ESM::ESMWriter& writer, Loading::Listener& progress) const { writer.startRecord(ESM::REC_DYNA); writer.startSubRecord("COUN"); writer.writeT(mDynamicCount); writer.endRecord("COUN"); writer.endRecord(ESM::REC_DYNA); mPotions.write (writer, progress); mArmors.write (writer, progress); mBooks.write (writer, progress); mClasses.write (writer, progress); mClothes.write (writer, progress); mEnchants.write (writer, progress); mSpells.write (writer, progress); mWeapons.write (writer, progress); mNpcs.write (writer, progress); mItemLists.write (writer, progress); mCreatureLists.write (writer, progress); mCreatures.write (writer, progress); mContainers.write (writer, progress); } bool ESMStore::readRecord (ESM::ESMReader& reader, uint32_t type) { switch (type) { case ESM::REC_ALCH: case ESM::REC_ARMO: case ESM::REC_BOOK: case ESM::REC_CLAS: case ESM::REC_CLOT: case ESM::REC_ENCH: case ESM::REC_SPEL: case ESM::REC_WEAP: case ESM::REC_LEVI: case ESM::REC_LEVC: mStores[type]->read (reader); return true; case ESM::REC_NPC_: case ESM::REC_CREA: case ESM::REC_CONT: mStores[type]->read (reader, true); return true; case ESM::REC_DYNA: reader.getSubNameIs("COUN"); reader.getHT(mDynamicCount); return true; default: return false; } } void ESMStore::checkPlayer() { setUp(); const ESM::NPC *player = mNpcs.find ("player"); if (!mRaces.find (player->mRace) || !mClasses.find (player->mClass)) throw std::runtime_error ("Invalid player record (race or class unavailable"); } std::pair, bool> ESMStore::getSpellList(const std::string& originalId) const { const std::string id = Misc::StringUtils::lowerCase(originalId); auto result = mSpellListCache.find(id); std::shared_ptr ptr; if (result != mSpellListCache.end()) ptr = result->second.lock(); if (!ptr) { int type = find(id); ptr = std::make_shared(id, type); if (result != mSpellListCache.end()) result->second = ptr; else mSpellListCache.insert({id, ptr}); return {ptr, false}; } return {ptr, true}; } } // end namespace ================================================ FILE: apps/openmw/mwworld/esmstore.hpp ================================================ #ifndef OPENMW_MWWORLD_ESMSTORE_H #define OPENMW_MWWORLD_ESMSTORE_H #include #include #include #include #include #include "store.hpp" namespace Loading { class Listener; } namespace MWMechanics { class SpellList; } namespace MWWorld { class ESMStore { Store mActivators; Store mPotions; Store mAppas; Store mArmors; Store mBodyParts; Store mBooks; Store mBirthSigns; Store mClasses; Store mClothes; Store mContainers; Store mCreatures; Store mDialogs; Store mDoors; Store mEnchants; Store mFactions; Store mGlobals; Store mIngreds; Store mCreatureLists; Store mItemLists; Store mLights; Store mLockpicks; Store mMiscItems; Store mNpcs; Store mProbes; Store mRaces; Store mRegions; Store mRepairs; Store mSoundGens; Store mSounds; Store mSpells; Store mStartScripts; Store mStatics; Store mWeapons; Store mGameSettings; Store mScripts; // Lists that need special rules Store mCells; Store mLands; Store mLandTextures; Store mPathgrids; Store mMagicEffects; Store mSkills; // Special entry which is hardcoded and not loaded from an ESM Store mAttributes; // Lookup of all IDs. Makes looking up references faster. Just // maps the id name to the record type. std::map mIds; std::map mStaticIds; std::unordered_map mRefCount; std::map mStores; unsigned int mDynamicCount; mutable std::map > mSpellListCache; /// Validate entries in store after setup void validate(); void countRecords(); public: /// \todo replace with SharedIterator typedef std::map::const_iterator iterator; iterator begin() const { return mStores.begin(); } iterator end() const { return mStores.end(); } /// Look up the given ID in 'all'. Returns 0 if not found. /// \note id must be in lower case. int find(const std::string &id) const { std::map::const_iterator it = mIds.find(id); if (it == mIds.end()) { return 0; } return it->second; } int findStatic(const std::string &id) const { std::map::const_iterator it = mStaticIds.find(id); if (it == mStaticIds.end()) { return 0; } return it->second; } ESMStore() : mDynamicCount(0) { mStores[ESM::REC_ACTI] = &mActivators; mStores[ESM::REC_ALCH] = &mPotions; mStores[ESM::REC_APPA] = &mAppas; mStores[ESM::REC_ARMO] = &mArmors; mStores[ESM::REC_BODY] = &mBodyParts; mStores[ESM::REC_BOOK] = &mBooks; mStores[ESM::REC_BSGN] = &mBirthSigns; mStores[ESM::REC_CELL] = &mCells; mStores[ESM::REC_CLAS] = &mClasses; mStores[ESM::REC_CLOT] = &mClothes; mStores[ESM::REC_CONT] = &mContainers; mStores[ESM::REC_CREA] = &mCreatures; mStores[ESM::REC_DIAL] = &mDialogs; mStores[ESM::REC_DOOR] = &mDoors; mStores[ESM::REC_ENCH] = &mEnchants; mStores[ESM::REC_FACT] = &mFactions; mStores[ESM::REC_GLOB] = &mGlobals; mStores[ESM::REC_GMST] = &mGameSettings; mStores[ESM::REC_INGR] = &mIngreds; mStores[ESM::REC_LAND] = &mLands; mStores[ESM::REC_LEVC] = &mCreatureLists; mStores[ESM::REC_LEVI] = &mItemLists; mStores[ESM::REC_LIGH] = &mLights; mStores[ESM::REC_LOCK] = &mLockpicks; mStores[ESM::REC_LTEX] = &mLandTextures; mStores[ESM::REC_MISC] = &mMiscItems; mStores[ESM::REC_NPC_] = &mNpcs; mStores[ESM::REC_PGRD] = &mPathgrids; mStores[ESM::REC_PROB] = &mProbes; mStores[ESM::REC_RACE] = &mRaces; mStores[ESM::REC_REGN] = &mRegions; mStores[ESM::REC_REPA] = &mRepairs; mStores[ESM::REC_SCPT] = &mScripts; mStores[ESM::REC_SNDG] = &mSoundGens; mStores[ESM::REC_SOUN] = &mSounds; mStores[ESM::REC_SPEL] = &mSpells; mStores[ESM::REC_SSCR] = &mStartScripts; mStores[ESM::REC_STAT] = &mStatics; mStores[ESM::REC_WEAP] = &mWeapons; mPathgrids.setCells(mCells); } void clearDynamic () { for (std::map::iterator it = mStores.begin(); it != mStores.end(); ++it) it->second->clearDynamic(); movePlayerRecord(); } void movePlayerRecord () { auto player = mNpcs.find("player"); mNpcs.insert(*player); } /// Validate entries in store after loading a save void validateDynamic(); void load(ESM::ESMReader &esm, Loading::Listener* listener); template const Store &get() const { throw std::runtime_error("Storage for this type not exist"); } /// Insert a custom record (i.e. with a generated ID that will not clash will pre-existing records) template const T *insert(const T &x) { const std::string id = "$dynamic" + std::to_string(mDynamicCount++); Store &store = const_cast &>(get()); if (store.search(id) != nullptr) { const std::string msg = "Try to override existing record '" + id + "'"; throw std::runtime_error(msg); } T record = x; record.mId = id; T *ptr = store.insert(record); for (iterator it = mStores.begin(); it != mStores.end(); ++it) { if (it->second == &store) { mIds[ptr->mId] = it->first; } } return ptr; } /// Insert a record with set ID, and allow it to override a pre-existing static record. template const T *overrideRecord(const T &x) { Store &store = const_cast &>(get()); T *ptr = store.insert(x); for (iterator it = mStores.begin(); it != mStores.end(); ++it) { if (it->second == &store) { mIds[ptr->mId] = it->first; } } return ptr; } template const T *insertStatic(const T &x) { const std::string id = "$dynamic" + std::to_string(mDynamicCount++); Store &store = const_cast &>(get()); if (store.search(id) != nullptr) { const std::string msg = "Try to override existing record '" + id + "'"; throw std::runtime_error(msg); } T record = x; T *ptr = store.insertStatic(record); for (iterator it = mStores.begin(); it != mStores.end(); ++it) { if (it->second == &store) { mIds[ptr->mId] = it->first; } } return ptr; } // This method must be called once, after loading all master/plugin files. This can only be done // from the outside, so it must be public. void setUp(bool validateRecords = false); int countSavedGameRecords() const; void write (ESM::ESMWriter& writer, Loading::Listener& progress) const; bool readRecord (ESM::ESMReader& reader, uint32_t type); ///< \return Known type? // To be called when we are done with dynamic record loading void checkPlayer(); /// @return The number of instances defined in the base files. Excludes changes from the save file. int getRefCount(const std::string& id) const; /// Actors with the same ID share spells, abilities, etc. /// @return The shared spell list to use for this actor and whether or not it has already been initialized. std::pair, bool> getSpellList(const std::string& id) const; }; /* Start of tes3mp addition Make it possible to override a Cell record similarly to how other types of records can be overridden */ template <> inline const ESM::Cell *ESMStore::overrideRecord(const ESM::Cell &cell) { return mCells.override(cell); } /* End of tes3mp addition */ /* Start of tes3mp addition Make it possible to override a Pathgrid record similarly to how other types of records can be overridden */ template <> inline const ESM::Pathgrid* ESMStore::overrideRecord(const ESM::Pathgrid& pathgrid) { return mPathgrids.override(pathgrid); } /* End of tes3mp addition */ template <> inline const ESM::Cell *ESMStore::insert(const ESM::Cell &cell) { return mCells.insert(cell); } template <> inline const ESM::NPC *ESMStore::insert(const ESM::NPC &npc) { const std::string id = "$dynamic" + std::to_string(mDynamicCount++); if (Misc::StringUtils::ciEqual(npc.mId, "player")) { return mNpcs.insert(npc); } else if (mNpcs.search(id) != nullptr) { const std::string msg = "Try to override existing record '" + id + "'"; throw std::runtime_error(msg); } ESM::NPC record = npc; record.mId = id; ESM::NPC *ptr = mNpcs.insert(record); mIds[ptr->mId] = ESM::REC_NPC_; return ptr; } template <> inline const Store &ESMStore::get() const { return mActivators; } template <> inline const Store &ESMStore::get() const { return mPotions; } template <> inline const Store &ESMStore::get() const { return mAppas; } template <> inline const Store &ESMStore::get() const { return mArmors; } template <> inline const Store &ESMStore::get() const { return mBodyParts; } template <> inline const Store &ESMStore::get() const { return mBooks; } template <> inline const Store &ESMStore::get() const { return mBirthSigns; } template <> inline const Store &ESMStore::get() const { return mClasses; } template <> inline const Store &ESMStore::get() const { return mClothes; } template <> inline const Store &ESMStore::get() const { return mContainers; } template <> inline const Store &ESMStore::get() const { return mCreatures; } template <> inline const Store &ESMStore::get() const { return mDialogs; } template <> inline const Store &ESMStore::get() const { return mDoors; } template <> inline const Store &ESMStore::get() const { return mEnchants; } template <> inline const Store &ESMStore::get() const { return mFactions; } template <> inline const Store &ESMStore::get() const { return mGlobals; } template <> inline const Store &ESMStore::get() const { return mIngreds; } template <> inline const Store &ESMStore::get() const { return mCreatureLists; } template <> inline const Store &ESMStore::get() const { return mItemLists; } template <> inline const Store &ESMStore::get() const { return mLights; } template <> inline const Store &ESMStore::get() const { return mLockpicks; } template <> inline const Store &ESMStore::get() const { return mMiscItems; } template <> inline const Store &ESMStore::get() const { return mNpcs; } template <> inline const Store &ESMStore::get() const { return mProbes; } template <> inline const Store &ESMStore::get() const { return mRaces; } template <> inline const Store &ESMStore::get() const { return mRegions; } template <> inline const Store &ESMStore::get() const { return mRepairs; } template <> inline const Store &ESMStore::get() const { return mSoundGens; } template <> inline const Store &ESMStore::get() const { return mSounds; } template <> inline const Store &ESMStore::get() const { return mSpells; } template <> inline const Store &ESMStore::get() const { return mStartScripts; } template <> inline const Store &ESMStore::get() const { return mStatics; } template <> inline const Store &ESMStore::get() const { return mWeapons; } template <> inline const Store &ESMStore::get() const { return mGameSettings; } template <> inline const Store &ESMStore::get() const { return mScripts; } template <> inline const Store &ESMStore::get() const { return mCells; } template <> inline const Store &ESMStore::get() const { return mLands; } template <> inline const Store &ESMStore::get() const { return mLandTextures; } template <> inline const Store &ESMStore::get() const { return mPathgrids; } template <> inline const Store &ESMStore::get() const { return mMagicEffects; } template <> inline const Store &ESMStore::get() const { return mSkills; } template <> inline const Store &ESMStore::get() const { return mAttributes; } } #endif ================================================ FILE: apps/openmw/mwworld/failedaction.cpp ================================================ #include "failedaction.hpp" #include "../mwbase/environment.hpp" #include "../mwbase/windowmanager.hpp" #include "../mwmechanics/actorutil.hpp" namespace MWWorld { FailedAction::FailedAction(const std::string &msg, const Ptr& target) : Action(false, target), mMessage(msg) { } void FailedAction::executeImp(const Ptr &actor) { if(actor == MWMechanics::getPlayer() && !mMessage.empty()) MWBase::Environment::get().getWindowManager()->messageBox(mMessage); } } ================================================ FILE: apps/openmw/mwworld/failedaction.hpp ================================================ #ifndef GAME_MWWORLD_FAILEDACTION_H #define GAME_MWWORLD_FAILEDACTION_H #include "action.hpp" #include "ptr.hpp" namespace MWWorld { class FailedAction : public Action { std::string mMessage; void executeImp(const Ptr &actor) override; public: FailedAction(const std::string &message = std::string(), const Ptr& target = Ptr()); }; } #endif ================================================ FILE: apps/openmw/mwworld/globals.cpp ================================================ #include "globals.hpp" #include #include #include #include #include "esmstore.hpp" namespace MWWorld { Globals::Collection::const_iterator Globals::find (const std::string& name) const { Collection::const_iterator iter = mVariables.find (Misc::StringUtils::lowerCase (name)); if (iter==mVariables.end()) throw std::runtime_error ("unknown global variable: " + name); return iter; } Globals::Collection::iterator Globals::find (const std::string& name) { Collection::iterator iter = mVariables.find (Misc::StringUtils::lowerCase (name)); if (iter==mVariables.end()) throw std::runtime_error ("unknown global variable: " + name); return iter; } void Globals::fill (const MWWorld::ESMStore& store) { mVariables.clear(); const MWWorld::Store& globals = store.get(); for (const ESM::Global& esmGlobal : globals) { mVariables.insert (std::make_pair (Misc::StringUtils::lowerCase (esmGlobal.mId), esmGlobal)); } } const ESM::Variant& Globals::operator[] (const std::string& name) const { return find (Misc::StringUtils::lowerCase (name))->second.mValue; } ESM::Variant& Globals::operator[] (const std::string& name) { return find (Misc::StringUtils::lowerCase (name))->second.mValue; } char Globals::getType (const std::string& name) const { Collection::const_iterator iter = mVariables.find (Misc::StringUtils::lowerCase (name)); if (iter==mVariables.end()) return ' '; switch (iter->second.mValue.getType()) { case ESM::VT_Short: return 's'; case ESM::VT_Long: return 'l'; case ESM::VT_Float: return 'f'; default: return ' '; } } int Globals::countSavedGameRecords() const { return mVariables.size(); } void Globals::write (ESM::ESMWriter& writer, Loading::Listener& progress) const { for (Collection::const_iterator iter (mVariables.begin()); iter!=mVariables.end(); ++iter) { writer.startRecord (ESM::REC_GLOB); iter->second.save (writer); writer.endRecord (ESM::REC_GLOB); } } bool Globals::readRecord (ESM::ESMReader& reader, uint32_t type) { if (type==ESM::REC_GLOB) { ESM::Global global; bool isDeleted = false; // This readRecord() method is used when reading a saved game. // Deleted globals can't appear there, so isDeleted will be ignored here. global.load(reader, isDeleted); Misc::StringUtils::lowerCaseInPlace(global.mId); Collection::iterator iter = mVariables.find (global.mId); if (iter!=mVariables.end()) iter->second = global; return true; } return false; } /* Start of tes3mp addition Make it possible to add a global record from elsewhere */ void Globals::addRecord(const ESM::Global global) { mVariables.insert(std::make_pair(Misc::StringUtils::lowerCase(global.mId), global)); } /* End of tes3mp addition */ /* Start of tes3mp addition Make it possible to check whether a global exists */ bool Globals::hasRecord(const std::string& name) { return (mVariables.find(name) != mVariables.end()); } /* End of tes3mp addition */ } ================================================ FILE: apps/openmw/mwworld/globals.hpp ================================================ #ifndef GAME_MWWORLD_GLOBALS_H #define GAME_MWWORLD_GLOBALS_H #include #include #include #include #include namespace ESM { class ESMWriter; class ESMReader; } namespace Loading { class Listener; } namespace MWWorld { class ESMStore; class Globals { private: typedef std::map Collection; Collection mVariables; // type, value Collection::const_iterator find (const std::string& name) const; Collection::iterator find (const std::string& name); public: const ESM::Variant& operator[] (const std::string& name) const; ESM::Variant& operator[] (const std::string& name); char getType (const std::string& name) const; ///< If there is no global variable with this name, ' ' is returned. void fill (const MWWorld::ESMStore& store); ///< Replace variables with variables from \a store with default values. int countSavedGameRecords() const; void write (ESM::ESMWriter& writer, Loading::Listener& progress) const; bool readRecord (ESM::ESMReader& reader, uint32_t type); ///< Records for variables that do not exist are dropped silently. /// /// \return Known type? /* Start of tes3mp addition Make it possible to add a global record from elsewhere */ void addRecord(const ESM::Global global); /* End of tes3mp addition */ /* Start of tes3mp addition Make it possible to check whether a global exists */ bool hasRecord(const std::string& name); /* End of tes3mp addition */ }; } #endif ================================================ FILE: apps/openmw/mwworld/inventorystore.cpp ================================================ #include "inventorystore.hpp" #include #include #include #include #include #include /* Start of tes3mp addition Include additional headers for multiplayer purposes */ #include #include "../mwmp/Main.hpp" #include "../mwmp/CellController.hpp" #include "../mwmp/PlayerList.hpp" /* End of tes3mp addition */ #include "../mwbase/environment.hpp" #include "../mwbase/world.hpp" #include "../mwbase/mechanicsmanager.hpp" #include "../mwmechanics/npcstats.hpp" #include "../mwmechanics/spellresistance.hpp" #include "../mwmechanics/spellutil.hpp" #include "../mwmechanics/actorutil.hpp" #include "../mwmechanics/weapontype.hpp" #include "esmstore.hpp" #include "class.hpp" void MWWorld::InventoryStore::copySlots (const InventoryStore& store) { // some const-trickery, required because of a flaw in the handling of MW-references and the // resulting workarounds for (std::vector::const_iterator iter ( const_cast (store).mSlots.begin()); iter!=const_cast (store).mSlots.end(); ++iter) { std::size_t distance = std::distance (const_cast (store).begin(), *iter); ContainerStoreIterator slot = begin(); std::advance (slot, distance); mSlots.push_back (slot); } // some const-trickery, required because of a flaw in the handling of MW-references and the // resulting workarounds std::size_t distance = std::distance (const_cast (store).begin(), const_cast (store).mSelectedEnchantItem); ContainerStoreIterator slot = begin(); std::advance (slot, distance); mSelectedEnchantItem = slot; } void MWWorld::InventoryStore::initSlots (TSlots& slots_) { for (int i=0; i (mSlots.size()); ++i) if (mSlots[i].getType()!=-1 && mSlots[i]->getBase()==&ref) { inventory.mEquipmentSlots[index] = i; } if (mSelectedEnchantItem.getType()!=-1 && mSelectedEnchantItem->getBase() == &ref) inventory.mSelectedEnchantItem = index; } void MWWorld::InventoryStore::readEquipmentState(const MWWorld::ContainerStoreIterator &iter, int index, const ESM::InventoryState &inventory) { if (index == inventory.mSelectedEnchantItem) mSelectedEnchantItem = iter; std::map::const_iterator found = inventory.mEquipmentSlots.find(index); if (found != inventory.mEquipmentSlots.end()) { if (found->second < 0 || found->second >= MWWorld::InventoryStore::Slots) throw std::runtime_error("Invalid slot index in inventory state"); // make sure the item can actually be equipped in this slot int slot = found->second; std::pair, bool> allowedSlots = iter->getClass().getEquipmentSlots(*iter); if (!allowedSlots.first.size()) return; if (std::find(allowedSlots.first.begin(), allowedSlots.first.end(), slot) == allowedSlots.first.end()) slot = allowedSlots.first.front(); // unstack if required if (!allowedSlots.second && iter->getRefData().getCount() > 1) { int count = iter->getRefData().getCount(false); MWWorld::ContainerStoreIterator newIter = addNewStack(*iter, count > 0 ? 1 : -1); iter->getRefData().setCount(subtractItems(count, 1)); mSlots[slot] = newIter; } else mSlots[slot] = iter; } } MWWorld::InventoryStore::InventoryStore() : ContainerStore() , mInventoryListener(nullptr) , mUpdatesEnabled (true) , mFirstAutoEquip(true) , mSelectedEnchantItem(end()) { initSlots (mSlots); } MWWorld::InventoryStore::InventoryStore (const InventoryStore& store) : ContainerStore (store) , mMagicEffects(store.mMagicEffects) , mInventoryListener(store.mInventoryListener) , mUpdatesEnabled(store.mUpdatesEnabled) , mFirstAutoEquip(store.mFirstAutoEquip) , mPermanentMagicEffectMagnitudes(store.mPermanentMagicEffectMagnitudes) , mSelectedEnchantItem(end()) { copySlots (store); } MWWorld::InventoryStore& MWWorld::InventoryStore::operator= (const InventoryStore& store) { if (this == &store) return *this; mListener = store.mListener; mInventoryListener = store.mInventoryListener; mMagicEffects = store.mMagicEffects; mFirstAutoEquip = store.mFirstAutoEquip; mPermanentMagicEffectMagnitudes = store.mPermanentMagicEffectMagnitudes; mRechargingItemsUpToDate = false; ContainerStore::operator= (store); mSlots.clear(); copySlots (store); return *this; } MWWorld::ContainerStoreIterator MWWorld::InventoryStore::add(const Ptr& itemPtr, int count, const Ptr& actorPtr, bool allowAutoEquip, bool resolve) { const MWWorld::ContainerStoreIterator& retVal = MWWorld::ContainerStore::add(itemPtr, count, actorPtr, allowAutoEquip, resolve); // Auto-equip items if an armor/clothing item is added, but not for the player nor werewolves if (allowAutoEquip && actorPtr != MWMechanics::getPlayer() && actorPtr.getClass().isNpc() && !actorPtr.getClass().getNpcStats(actorPtr).isWerewolf()) { std::string type = itemPtr.getTypeName(); if (type == typeid(ESM::Armor).name() || type == typeid(ESM::Clothing).name()) autoEquip(actorPtr); } /* Start of tes3mp change (major) Only fire inventory events for actors in loaded cells to avoid crashes */ if (mListener && MWBase::Environment::get().getWorld()->isCellActive(*actorPtr.getCell()->getCell())) mListener->itemAdded(*retVal, count); /* End of tes3mp change (major) */ return retVal; } void MWWorld::InventoryStore::equip (int slot, const ContainerStoreIterator& iterator, const Ptr& actor) { if (iterator == end()) throw std::runtime_error ("can't equip end() iterator, use unequip function instead"); if (slot<0 || slot>=static_cast (mSlots.size())) throw std::runtime_error ("slot number out of range"); if (iterator.getContainerStore()!=this) throw std::runtime_error ("attempt to equip an item that is not in the inventory"); std::pair, bool> slots_; slots_ = iterator->getClass().getEquipmentSlots (*iterator); if (std::find (slots_.first.begin(), slots_.first.end(), slot)==slots_.first.end()) throw std::runtime_error ("invalid slot"); if (mSlots[slot] != end()) unequipSlot(slot, actor); // unstack item pointed to by iterator if required if (iterator!=end() && !slots_.second && iterator->getRefData().getCount() > 1) // if slots.second is true, item can stay stacked when equipped { unstack(*iterator, actor); } mSlots[slot] = iterator; flagAsModified(); fireEquipmentChangedEvent(actor); updateMagicEffects(actor); } void MWWorld::InventoryStore::unequipAll(const MWWorld::Ptr& actor) { mUpdatesEnabled = false; for (int slot=0; slot < MWWorld::InventoryStore::Slots; ++slot) unequipSlot(slot, actor); mUpdatesEnabled = true; fireEquipmentChangedEvent(actor); updateMagicEffects(actor); } MWWorld::ContainerStoreIterator MWWorld::InventoryStore::getSlot (int slot) { return findSlot (slot); } MWWorld::ConstContainerStoreIterator MWWorld::InventoryStore::getSlot (int slot) const { return findSlot (slot); } MWWorld::ContainerStoreIterator MWWorld::InventoryStore::findSlot (int slot) const { if (slot<0 || slot>=static_cast (mSlots.size())) throw std::runtime_error ("slot number out of range"); if (mSlots[slot]==end()) return mSlots[slot]; if (mSlots[slot]->getRefData().getCount()<1) { // Object has been deleted // This should no longer happen, since the new remove function will unequip first /* Start of tes3mp change (major) Instead of throwing an error, display an error log message with information about the item */ //throw std::runtime_error("Invalid slot, make sure you are not calling RefData::setCount for a container object"); LOG_MESSAGE_SIMPLE(TimedLog::LOG_ERROR, "Invalid slot, make sure you are not calling RefData::setCount for a container object\n- item was %s", mSlots[slot]->getCellRef().getRefId().c_str()); /* End of tes3mp change (major) */ } return mSlots[slot]; } void MWWorld::InventoryStore::autoEquipWeapon (const MWWorld::Ptr& actor, TSlots& slots_) { if (!actor.getClass().isNpc()) { // In original game creatures do not autoequip weapon, but we need it for weapon sheathing. // The only case when the difference is noticable - when this creature sells weapon. // So just disable weapon autoequipping for creatures which sells weapon. int services = actor.getClass().getServices(actor); bool sellsWeapon = services & (ESM::NPC::Weapon|ESM::NPC::MagicItems); if (sellsWeapon) return; } static const ESM::Skill::SkillEnum weaponSkills[] = { ESM::Skill::LongBlade, ESM::Skill::Axe, ESM::Skill::Spear, ESM::Skill::ShortBlade, ESM::Skill::Marksman, ESM::Skill::BluntWeapon }; const size_t weaponSkillsLength = sizeof(weaponSkills) / sizeof(weaponSkills[0]); bool weaponSkillVisited[weaponSkillsLength] = { false }; // give arrows/bolt with max damage by default int arrowMax = 0; int boltMax = 0; ContainerStoreIterator arrow(end()); ContainerStoreIterator bolt(end()); // rate ammo for (ContainerStoreIterator iter(begin(ContainerStore::Type_Weapon)); iter!=end(); ++iter) { const ESM::Weapon* esmWeapon = iter->get()->mBase; if (esmWeapon->mData.mType == ESM::Weapon::Arrow) { if (esmWeapon->mData.mChop[1] >= arrowMax) { arrowMax = esmWeapon->mData.mChop[1]; arrow = iter; } } else if (esmWeapon->mData.mType == ESM::Weapon::Bolt) { if (esmWeapon->mData.mChop[1] >= boltMax) { boltMax = esmWeapon->mData.mChop[1]; bolt = iter; } } } // rate weapon for (int i = 0; i < static_cast(weaponSkillsLength); ++i) { float max = 0; int maxWeaponSkill = -1; for (int j = 0; j < static_cast(weaponSkillsLength); ++j) { float skillValue = actor.getClass().getSkill(actor, static_cast(weaponSkills[j])); if (skillValue > max && !weaponSkillVisited[j]) { max = skillValue; maxWeaponSkill = j; } } if (maxWeaponSkill == -1) break; max = 0; ContainerStoreIterator weapon(end()); for (ContainerStoreIterator iter(begin(ContainerStore::Type_Weapon)); iter!=end(); ++iter) { const ESM::Weapon* esmWeapon = iter->get()->mBase; if (MWMechanics::getWeaponType(esmWeapon->mData.mType)->mWeaponClass == ESM::WeaponType::Ammo) continue; if (iter->getClass().getEquipmentSkill(*iter) == weaponSkills[maxWeaponSkill]) { if (esmWeapon->mData.mChop[1] >= max) { max = esmWeapon->mData.mChop[1]; weapon = iter; } if (esmWeapon->mData.mSlash[1] >= max) { max = esmWeapon->mData.mSlash[1]; weapon = iter; } if (esmWeapon->mData.mThrust[1] >= max) { max = esmWeapon->mData.mThrust[1]; weapon = iter; } } } if (weapon != end() && weapon->getClass().canBeEquipped(*weapon, actor).first) { // Do not equip ranged weapons, if there is no suitable ammo bool hasAmmo = true; const MWWorld::LiveCellRef *ref = weapon->get(); int type = ref->mBase->mData.mType; int ammotype = MWMechanics::getWeaponType(type)->mAmmoType; if (ammotype == ESM::Weapon::Arrow) { if (arrow == end()) hasAmmo = false; else slots_[Slot_Ammunition] = arrow; } else if (ammotype == ESM::Weapon::Bolt) { if (bolt == end()) hasAmmo = false; else slots_[Slot_Ammunition] = bolt; } if (hasAmmo) { std::pair, bool> itemsSlots = weapon->getClass().getEquipmentSlots (*weapon); if (!itemsSlots.first.empty()) { if (!itemsSlots.second) { if (weapon->getRefData().getCount() > 1) { unstack(*weapon, actor); } } int slot = itemsSlots.first.front(); slots_[slot] = weapon; if (ammotype == ESM::Weapon::None) slots_[Slot_Ammunition] = end(); } break; } } weaponSkillVisited[maxWeaponSkill] = true; } } void MWWorld::InventoryStore::autoEquipArmor (const MWWorld::Ptr& actor, TSlots& slots_) { // Only NPCs can wear armor for now. // For creatures we equip only shields. if (!actor.getClass().isNpc()) { autoEquipShield(actor, slots_); return; } const MWBase::World *world = MWBase::Environment::get().getWorld(); const MWWorld::Store &store = world->getStore().get(); static float fUnarmoredBase1 = store.find("fUnarmoredBase1")->mValue.getFloat(); static float fUnarmoredBase2 = store.find("fUnarmoredBase2")->mValue.getFloat(); float unarmoredSkill = actor.getClass().getSkill(actor, ESM::Skill::Unarmored); float unarmoredRating = (fUnarmoredBase1 * unarmoredSkill) * (fUnarmoredBase2 * unarmoredSkill); for (ContainerStoreIterator iter (begin(ContainerStore::Type_Clothing | ContainerStore::Type_Armor)); iter!=end(); ++iter) { Ptr test = *iter; switch(test.getClass().canBeEquipped (test, actor).first) { case 0: continue; default: break; } if (iter.getType() == ContainerStore::Type_Armor && test.getClass().getEffectiveArmorRating(test, actor) <= std::max(unarmoredRating, 0.f)) { continue; } std::pair, bool> itemsSlots = iter->getClass().getEquipmentSlots (*iter); // checking if current item pointed by iter can be equipped for (int slot : itemsSlots.first) { // if true then it means slot is equipped already // check if slot may require swapping if current item is more valuable if (slots_.at (slot)!=end()) { Ptr old = *slots_.at (slot); if (iter.getType() == ContainerStore::Type_Armor) { if (old.getTypeName() == typeid(ESM::Armor).name()) { if (old.get()->mBase->mData.mType < test.get()->mBase->mData.mType) continue; if (old.get()->mBase->mData.mType == test.get()->mBase->mData.mType) { if (old.getClass().getEffectiveArmorRating(old, actor) >= test.getClass().getEffectiveArmorRating(test, actor)) // old armor had better armor rating continue; } } // suitable armor should replace already equipped clothing } else if (iter.getType() == ContainerStore::Type_Clothing) { // if left ring is equipped if (slot == Slot_LeftRing) { // if there is a place for right ring dont swap it if (slots_.at(Slot_RightRing) == end()) { continue; } else // if right ring is equipped too { Ptr rightRing = *slots_.at(Slot_RightRing); // we want to swap cheaper ring only if both are equipped if (old.getClass().getValue (old) >= rightRing.getClass().getValue (rightRing)) continue; } } if (old.getTypeName() == typeid(ESM::Clothing).name()) { // check value if (old.getClass().getValue (old) >= test.getClass().getValue (test)) // old clothing was more valuable continue; } else // suitable clothing should NOT replace already equipped armor continue; } } if (!itemsSlots.second) // if itemsSlots.second is true, item can stay stacked when equipped { // unstack item pointed to by iterator if required if (iter->getRefData().getCount() > 1) { unstack(*iter, actor); } } // if we are here it means item can be equipped or swapped slots_[slot] = iter; break; } } } void MWWorld::InventoryStore::autoEquipShield(const MWWorld::Ptr& actor, TSlots& slots_) { for (ContainerStoreIterator iter(begin(ContainerStore::Type_Armor)); iter != end(); ++iter) { if (iter->get()->mBase->mData.mType != ESM::Armor::Shield) continue; if (iter->getClass().canBeEquipped(*iter, actor).first != 1) continue; std::pair, bool> shieldSlots = iter->getClass().getEquipmentSlots(*iter); int slot = shieldSlots.first[0]; const ContainerStoreIterator& shield = slots_[slot]; if (shield != end() && shield.getType() == Type_Armor && shield->get()->mBase->mData.mType == ESM::Armor::Shield) { if (shield->getClass().getItemHealth(*shield) >= iter->getClass().getItemHealth(*iter)) continue; } slots_[slot] = iter; } } void MWWorld::InventoryStore::autoEquip (const MWWorld::Ptr& actor) { /* Start of tes3mp addition We need DedicatedPlayers and DedicatedActors to wear exactly what they're wearing on their authority client, so don't auto-equip for them */ if (mwmp::PlayerList::isDedicatedPlayer(actor) || mwmp::Main::get().getCellController()->isDedicatedActor(actor)) return; /* End of tes3mp addition */ TSlots slots_; initSlots (slots_); // Disable model update during auto-equip mUpdatesEnabled = false; // Autoequip clothing, armor and weapons. // Equipping lights is handled in Actors::updateEquippedLight based on environment light. // Note: creatures ignore equipment armor rating and only equip shields // Use custom logic for them - select shield based on its health instead of armor rating autoEquipWeapon(actor, slots_); autoEquipArmor(actor, slots_); bool changed = false; for (std::size_t i=0; igetClass().getEnchantment (**iter); if (!enchantmentId.empty()) { const ESM::Enchantment& enchantment = *MWBase::Environment::get().getWorld()->getStore().get().find (enchantmentId); if (enchantment.mData.mType != ESM::Enchantment::ConstantEffect) continue; std::vector params; bool existed = (mPermanentMagicEffectMagnitudes.find((**iter).getCellRef().getRefId()) != mPermanentMagicEffectMagnitudes.end()); if (!existed) { params.resize(enchantment.mEffects.mList.size()); int i=0; for (const ESM::ENAMstruct& effect : enchantment.mEffects.mList) { int delta = effect.mMagnMax - effect.mMagnMin; // Roll some dice, one for each effect if (delta) params[i].mRandom = Misc::Rng::rollDice(delta + 1) / static_cast(delta); // Try resisting each effect params[i].mMultiplier = MWMechanics::getEffectMultiplier(effect.mEffectID, actor, actor); ++i; } // Note that using the RefID as a key here is not entirely correct. // Consider equipping the same item twice (e.g. a ring) // However, permanent enchantments with a random magnitude are kind of an exploit anyway, // so it doesn't really matter if both items will get the same magnitude. *Extreme* edge case. mPermanentMagicEffectMagnitudes[(**iter).getCellRef().getRefId()] = params; } else params = mPermanentMagicEffectMagnitudes[(**iter).getCellRef().getRefId()]; int i=0; for (const ESM::ENAMstruct& effect : enchantment.mEffects.mList) { const ESM::MagicEffect *magicEffect = MWBase::Environment::get().getWorld()->getStore().get().find ( effect.mEffectID); // Fully resisted or can't be applied to target? if (params[i].mMultiplier == 0 || !MWMechanics::checkEffectTarget(effect.mEffectID, actor, actor, actor == MWMechanics::getPlayer())) { i++; continue; } float magnitude = effect.mMagnMin + (effect.mMagnMax - effect.mMagnMin) * params[i].mRandom; magnitude *= params[i].mMultiplier; if (!existed) { // During first auto equip, we don't play any sounds. // Basically we don't want sounds when the actor is first loaded, // the items should appear as if they'd always been equipped. mInventoryListener->permanentEffectAdded(magicEffect, !mFirstAutoEquip); } if (magnitude) mMagicEffects.add (effect, magnitude); i++; } } } // Now drop expired effects for (TEffectMagnitudes::iterator it = mPermanentMagicEffectMagnitudes.begin(); it != mPermanentMagicEffectMagnitudes.end();) { bool found = false; for (TSlots::const_iterator iter (mSlots.begin()); iter!=mSlots.end(); ++iter) { if (*iter == end()) continue; if ((**iter).getCellRef().getRefId() == it->first) { found = true; } } if (!found) mPermanentMagicEffectMagnitudes.erase(it++); else ++it; } // Magic effects are normally not updated when paused, but we need this to make resistances work immediately after equipping MWBase::Environment::get().getMechanicsManager()->updateMagicEffects(actor); mFirstAutoEquip = false; } bool MWWorld::InventoryStore::stacks(const ConstPtr& ptr1, const ConstPtr& ptr2) const { bool canStack = MWWorld::ContainerStore::stacks(ptr1, ptr2); if (!canStack) return false; // don't stack if either item is currently equipped for (TSlots::const_iterator iter (mSlots.begin()); iter!=mSlots.end(); ++iter) { if (*iter != end() && (ptr1 == **iter || ptr2 == **iter)) { bool stackWhenEquipped = (*iter)->getClass().getEquipmentSlots(**iter).second; if (!stackWhenEquipped) return false; } } return true; } void MWWorld::InventoryStore::setSelectedEnchantItem(const ContainerStoreIterator& iterator) { mSelectedEnchantItem = iterator; } MWWorld::ContainerStoreIterator MWWorld::InventoryStore::getSelectedEnchantItem() { return mSelectedEnchantItem; } int MWWorld::InventoryStore::remove(const Ptr& item, int count, const Ptr& actor, bool equipReplacement, bool resolve) { int retCount = ContainerStore::remove(item, count, actor, equipReplacement, resolve); bool wasEquipped = false; if (!item.getRefData().getCount()) { for (int slot=0; slot < MWWorld::InventoryStore::Slots; ++slot) { if (mSlots[slot] == end()) continue; if (*mSlots[slot] == item) { unequipSlot(slot, actor); wasEquipped = true; break; } } } // If an armor/clothing item is removed, try to find a replacement, // but not for the player nor werewolves, and not if the RemoveItem script command // was used (equipReplacement is false) if (equipReplacement && wasEquipped && (actor != MWMechanics::getPlayer()) && actor.getClass().isNpc() && !actor.getClass().getNpcStats(actor).isWerewolf()) { std::string type = item.getTypeName(); if (type == typeid(ESM::Armor).name() || type == typeid(ESM::Clothing).name()) autoEquip(actor); } if (item.getRefData().getCount() == 0 && mSelectedEnchantItem != end() && *mSelectedEnchantItem == item) { mSelectedEnchantItem = end(); } /* Start of tes3mp change (major) Only fire inventory events for actors in loaded cells to avoid crashes */ if (mListener && MWBase::Environment::get().getWorld()->isCellActive(*actor.getCell()->getCell())) mListener->itemRemoved(item, retCount); /* End of tes3mp change (major) */ return retCount; } MWWorld::ContainerStoreIterator MWWorld::InventoryStore::unequipSlot(int slot, const MWWorld::Ptr& actor, bool applyUpdates) { if (slot<0 || slot>=static_cast (mSlots.size())) throw std::runtime_error ("slot number out of range"); ContainerStoreIterator it = mSlots[slot]; if (it != end()) { ContainerStoreIterator retval = it; // empty this slot mSlots[slot] = end(); if (it->getRefData().getCount()) { retval = restack(*it); if (actor == MWMechanics::getPlayer()) { // Unset OnPCEquip Variable on item's script, if it has a script with that variable declared const std::string& script = it->getClass().getScript(*it); if (script != "") (*it).getRefData().getLocals().setVarByInt(script, "onpcequip", 0); } if ((mSelectedEnchantItem != end()) && (mSelectedEnchantItem == it)) { mSelectedEnchantItem = end(); } } if (applyUpdates) { fireEquipmentChangedEvent(actor); updateMagicEffects(actor); } return retval; } return it; } MWWorld::ContainerStoreIterator MWWorld::InventoryStore::unequipItem(const MWWorld::Ptr& item, const MWWorld::Ptr& actor) { for (int slot=0; slot item.getRefData().getCount()) throw std::runtime_error ("attempt to unequip more items than equipped"); if (count == item.getRefData().getCount()) return unequipItem(item, actor); // Move items to an existing stack if possible, otherwise split count items out into a new stack. // Moving counts manually here, since ContainerStore's restack can't target unequipped stacks. for (MWWorld::ContainerStoreIterator iter (begin()); iter != end(); ++iter) { if (stacks(*iter, item) && !isEquipped(*iter)) { iter->getRefData().setCount(addItems(iter->getRefData().getCount(false), count)); item.getRefData().setCount(subtractItems(item.getRefData().getCount(false), count)); return iter; } } return unstack(item, actor, item.getRefData().getCount() - count); } MWWorld::InventoryStoreListener* MWWorld::InventoryStore::getInvListener() { return mInventoryListener; } void MWWorld::InventoryStore::setInvListener(InventoryStoreListener *listener, const Ptr& actor) { mInventoryListener = listener; updateMagicEffects(actor); } void MWWorld::InventoryStore::fireEquipmentChangedEvent(const Ptr& actor) { if (!mUpdatesEnabled) return; /* Start of tes3mp change (major) Only fire inventory events for local players or for other actors in loaded cells to avoid crashes */ if (mInventoryListener) { if (actor == MWMechanics::getPlayer() || MWBase::Environment::get().getWorld()->isCellActive(*actor.getCell()->getCell())) { mInventoryListener->equipmentChanged(); } } /* End of tes3mp change (major) */ // if player, update inventory window /* if (actor == MWMechanics::getPlayer()) { MWBase::Environment::get().getWindowManager()->getInventoryWindow()->updateItemView(); } */ } void MWWorld::InventoryStore::visitEffectSources(MWMechanics::EffectSourceVisitor &visitor) { for (TSlots::const_iterator iter (mSlots.begin()); iter!=mSlots.end(); ++iter) { if (*iter==end()) continue; std::string enchantmentId = (*iter)->getClass().getEnchantment (**iter); if (enchantmentId.empty()) continue; const ESM::Enchantment& enchantment = *MWBase::Environment::get().getWorld()->getStore().get().find (enchantmentId); if (enchantment.mData.mType != ESM::Enchantment::ConstantEffect) continue; if (mPermanentMagicEffectMagnitudes.find((**iter).getCellRef().getRefId()) == mPermanentMagicEffectMagnitudes.end()) continue; int i=0; for (const ESM::ENAMstruct& effect : enchantment.mEffects.mList) { i++; // Don't get spell icon display information for enchantments that weren't actually applied if (mMagicEffects.get(MWMechanics::EffectKey(effect)).getMagnitude() == 0) continue; const EffectParams& params = mPermanentMagicEffectMagnitudes[(**iter).getCellRef().getRefId()][i-1]; float magnitude = effect.mMagnMin + (effect.mMagnMax - effect.mMagnMin) * params.mRandom; magnitude *= params.mMultiplier; if (magnitude > 0) visitor.visit(MWMechanics::EffectKey(effect), i-1, (**iter).getClass().getName(**iter), (**iter).getCellRef().getRefId(), -1, magnitude); } } } void MWWorld::InventoryStore::purgeEffect(short effectId, bool wholeSpell) { for (TSlots::const_iterator it = mSlots.begin(); it != mSlots.end(); ++it) { if (*it != end()) purgeEffect(effectId, (*it)->getCellRef().getRefId(), wholeSpell); } } void MWWorld::InventoryStore::purgeEffect(short effectId, const std::string &sourceId, bool wholeSpell, int effectIndex) { TEffectMagnitudes::iterator effectMagnitudeIt = mPermanentMagicEffectMagnitudes.find(sourceId); if (effectMagnitudeIt == mPermanentMagicEffectMagnitudes.end()) return; for (TSlots::const_iterator iter (mSlots.begin()); iter!=mSlots.end(); ++iter) { if (*iter==end()) continue; if ((*iter)->getCellRef().getRefId() != sourceId) continue; std::string enchantmentId = (*iter)->getClass().getEnchantment (**iter); if (!enchantmentId.empty()) { const ESM::Enchantment& enchantment = *MWBase::Environment::get().getWorld()->getStore().get().find (enchantmentId); if (enchantment.mData.mType != ESM::Enchantment::ConstantEffect) continue; std::vector& params = effectMagnitudeIt->second; int i=0; for (std::vector::const_iterator effectIt (enchantment.mEffects.mList.begin()); effectIt!=enchantment.mEffects.mList.end(); ++effectIt, ++i) { if (effectIt->mEffectID != effectId) continue; if (effectIndex >= 0 && effectIndex != i) continue; if (wholeSpell) { mPermanentMagicEffectMagnitudes.erase(sourceId); return; } float magnitude = effectIt->mMagnMin + (effectIt->mMagnMax - effectIt->mMagnMin) * params[i].mRandom; magnitude *= params[i].mMultiplier; if (magnitude) mMagicEffects.add (*effectIt, -magnitude); params[i].mMultiplier = 0; } } } } void MWWorld::InventoryStore::clear() { mSlots.clear(); initSlots (mSlots); ContainerStore::clear(); } bool MWWorld::InventoryStore::isEquipped(const MWWorld::ConstPtr &item) { for (int i=0; i < MWWorld::InventoryStore::Slots; ++i) { if (getSlot(i) != end() && *getSlot(i) == item) return true; } return false; } void MWWorld::InventoryStore::writeState(ESM::InventoryState &state) const { MWWorld::ContainerStore::writeState(state); for (TEffectMagnitudes::const_iterator it = mPermanentMagicEffectMagnitudes.begin(); it != mPermanentMagicEffectMagnitudes.end(); ++it) { std::vector > params; for (std::vector::const_iterator pIt = it->second.begin(); pIt != it->second.end(); ++pIt) { params.emplace_back(pIt->mRandom, pIt->mMultiplier); } state.mPermanentMagicEffectMagnitudes[it->first] = params; } } void MWWorld::InventoryStore::readState(const ESM::InventoryState &state) { MWWorld::ContainerStore::readState(state); for (ESM::InventoryState::TEffectMagnitudes::const_iterator it = state.mPermanentMagicEffectMagnitudes.begin(); it != state.mPermanentMagicEffectMagnitudes.end(); ++it) { std::vector params; for (std::vector >::const_iterator pIt = it->second.begin(); pIt != it->second.end(); ++pIt) { EffectParams p; p.mRandom = pIt->first; p.mMultiplier = pIt->second; params.push_back(p); } mPermanentMagicEffectMagnitudes[it->first] = params; } } ================================================ FILE: apps/openmw/mwworld/inventorystore.hpp ================================================ #ifndef GAME_MWWORLD_INVENTORYSTORE_H #define GAME_MWWORLD_INVENTORYSTORE_H #include "containerstore.hpp" #include "../mwmechanics/magiceffects.hpp" namespace ESM { struct MagicEffect; } namespace MWMechanics { class NpcStats; } namespace MWWorld { class InventoryStoreListener { public: /** * Fired when items are equipped or unequipped */ virtual void equipmentChanged () {} /** * @param effect * @param isNew Is this effect new (e.g. the item for it was just now manually equipped) * or was it loaded from a savegame / initial game state? \n * If it isn't new, non-looping VFX should not be played. * @param playSound Play effect sound? */ virtual void permanentEffectAdded (const ESM::MagicEffect *magicEffect, bool isNew) {} virtual ~InventoryStoreListener() = default; }; ///< \brief Variant of the ContainerStore for NPCs class InventoryStore : public ContainerStore { public: static constexpr int Slot_Helmet = 0; static constexpr int Slot_Cuirass = 1; static constexpr int Slot_Greaves = 2; static constexpr int Slot_LeftPauldron = 3; static constexpr int Slot_RightPauldron = 4; static constexpr int Slot_LeftGauntlet = 5; static constexpr int Slot_RightGauntlet = 6; static constexpr int Slot_Boots = 7; static constexpr int Slot_Shirt = 8; static constexpr int Slot_Pants = 9; static constexpr int Slot_Skirt = 10; static constexpr int Slot_Robe = 11; static constexpr int Slot_LeftRing = 12; static constexpr int Slot_RightRing = 13; static constexpr int Slot_Amulet = 14; static constexpr int Slot_Belt = 15; static constexpr int Slot_CarriedRight = 16; static constexpr int Slot_CarriedLeft = 17; static constexpr int Slot_Ammunition = 18; static constexpr int Slots = 19; static constexpr int Slot_NoSlot = -1; private: MWMechanics::MagicEffects mMagicEffects; InventoryStoreListener* mInventoryListener; // Enables updates of magic effects and actor model whenever items are equipped or unequipped. // This is disabled during autoequip to avoid excessive updates bool mUpdatesEnabled; bool mFirstAutoEquip; // Vanilla allows permanent effects with a random magnitude, so it needs to be stored here. // We also need this to only play sounds and particle effects when the item is equipped, rather than on every update. struct EffectParams { // Modifier to scale between min and max magnitude float mRandom; // Multiplier for when an effect was fully or partially resisted float mMultiplier; }; typedef std::map > TEffectMagnitudes; TEffectMagnitudes mPermanentMagicEffectMagnitudes; typedef std::vector TSlots; TSlots mSlots; void autoEquipWeapon(const MWWorld::Ptr& actor, TSlots& slots_); void autoEquipArmor(const MWWorld::Ptr& actor, TSlots& slots_); void autoEquipShield(const MWWorld::Ptr& actor, TSlots& slots_); // selected magic item (for using enchantments of type "Cast once" or "Cast when used") ContainerStoreIterator mSelectedEnchantItem; void copySlots (const InventoryStore& store); void initSlots (TSlots& slots_); void updateMagicEffects(const Ptr& actor); void fireEquipmentChangedEvent(const Ptr& actor); void storeEquipmentState (const MWWorld::LiveCellRefBase& ref, int index, ESM::InventoryState& inventory) const override; void readEquipmentState (const MWWorld::ContainerStoreIterator& iter, int index, const ESM::InventoryState& inventory) override; ContainerStoreIterator findSlot (int slot) const; public: InventoryStore(); InventoryStore (const InventoryStore& store); InventoryStore& operator= (const InventoryStore& store); std::unique_ptr clone() override { return std::make_unique(*this); } ContainerStoreIterator add (const Ptr& itemPtr, int count, const Ptr& actorPtr, bool allowAutoEquip = true, bool resolve = true) override; ///< Add the item pointed to by \a ptr to this container. (Stacks automatically if needed) /// Auto-equip items if specific conditions are fulfilled and allowAutoEquip is true (see the implementation). /// /// \note The item pointed to is not required to exist beyond this function call. /// /// \attention Do not add items to an existing stack by increasing the count instead of /// calling this function! /// /// @return if stacking happened, return iterator to the item that was stacked against, otherwise iterator to the newly inserted item. void equip (int slot, const ContainerStoreIterator& iterator, const Ptr& actor); ///< \warning \a iterator can not be an end()-iterator, use unequip function instead bool isEquipped(const MWWorld::ConstPtr& item); ///< Utility function, returns true if the given item is equipped in any slot void setSelectedEnchantItem(const ContainerStoreIterator& iterator); ///< set the selected magic item (for using enchantments of type "Cast once" or "Cast when used") /// \note to unset the selected item, call this method with end() iterator ContainerStoreIterator getSelectedEnchantItem(); ///< @return selected magic item (for using enchantments of type "Cast once" or "Cast when used") /// \note if no item selected, return end() iterator ContainerStoreIterator getSlot (int slot); ConstContainerStoreIterator getSlot(int slot) const; ContainerStoreIterator getPreferredShield(const MWWorld::Ptr& actor); void unequipAll(const MWWorld::Ptr& actor); ///< Unequip all currently equipped items. void autoEquip (const MWWorld::Ptr& actor); ///< Auto equip items according to stats and item value. const MWMechanics::MagicEffects& getMagicEffects() const; ///< Return magic effects from worn items. bool stacks (const ConstPtr& ptr1, const ConstPtr& ptr2) const override; ///< @return true if the two specified objects can stack with each other using ContainerStore::remove; int remove(const Ptr& item, int count, const Ptr& actor, bool equipReplacement = 0, bool resolve = true) override; ///< Remove \a count item(s) designated by \a item from this inventory. /// /// @return the number of items actually removed ContainerStoreIterator unequipSlot(int slot, const Ptr& actor, bool applyUpdates = true); ///< Unequip \a slot. /// /// @return an iterator to the item that was previously in the slot ContainerStoreIterator unequipItem(const Ptr& item, const Ptr& actor); ///< Unequip an item identified by its Ptr. An exception is thrown /// if the item is not currently equipped. /// /// @return an iterator to the item that was previously in the slot /// (it can be re-stacked so its count may be different than when it /// was equipped). ContainerStoreIterator unequipItemQuantity(const Ptr& item, const Ptr& actor, int count); ///< Unequip a specific quantity of an item identified by its Ptr. /// An exception is thrown if the item is not currently equipped, /// if count <= 0, or if count > the item stack size. /// /// @return an iterator to the unequipped items that were previously /// in the slot (they can be re-stacked so its count may be different /// than the requested count). void setInvListener (InventoryStoreListener* listener, const Ptr& actor); ///< Set a listener for various events, see \a InventoryStoreListener InventoryStoreListener* getInvListener(); void visitEffectSources (MWMechanics::EffectSourceVisitor& visitor); void purgeEffect (short effectId, bool wholeSpell = false); ///< Remove a magic effect void purgeEffect (short effectId, const std::string& sourceId, bool wholeSpell = false, int effectIndex=-1); ///< Remove a magic effect void clear() override; ///< Empty container. void writeState (ESM::InventoryState& state) const override; void readState (const ESM::InventoryState& state) override; }; } #endif ================================================ FILE: apps/openmw/mwworld/livecellref.cpp ================================================ #include "livecellref.hpp" #include #include #include "../mwbase/environment.hpp" #include "../mwbase/world.hpp" #include "ptr.hpp" #include "class.hpp" #include "esmstore.hpp" MWWorld::LiveCellRefBase::LiveCellRefBase(const std::string& type, const ESM::CellRef &cref) : mClass(&Class::get(type)), mRef(cref), mData(cref) { } void MWWorld::LiveCellRefBase::loadImp (const ESM::ObjectState& state) { mRef = state.mRef; mData = RefData (state, mData.isDeletedByContentFile()); Ptr ptr (this); if (state.mHasLocals) { std::string scriptId = mClass->getScript (ptr); // Make sure we still have a script. It could have been coming from a content file that is no longer active. if (!scriptId.empty()) { if (const ESM::Script* script = MWBase::Environment::get().getWorld()->getStore().get().search (scriptId)) { try { mData.setLocals (*script); mData.getLocals().read (state.mLocals, scriptId); } catch (const std::exception& exception) { Log(Debug::Error) << "Error: failed to load state for local script " << scriptId << " because an exception has been thrown: " << exception.what(); } } } } mClass->readAdditionalState (ptr, state); if (!mRef.getSoul().empty() && !MWBase::Environment::get().getWorld()->getStore().get().search(mRef.getSoul())) { Log(Debug::Warning) << "Soul '" << mRef.getSoul() << "' not found, removing the soul from soul gem"; mRef.setSoul(std::string()); } } void MWWorld::LiveCellRefBase::saveImp (ESM::ObjectState& state) const { mRef.writeState(state); ConstPtr ptr (this); mData.write (state, mClass->getScript (ptr)); mClass->writeAdditionalState (ptr, state); } bool MWWorld::LiveCellRefBase::checkStateImp (const ESM::ObjectState& state) { return true; } ================================================ FILE: apps/openmw/mwworld/livecellref.hpp ================================================ #ifndef GAME_MWWORLD_LIVECELLREF_H #define GAME_MWWORLD_LIVECELLREF_H #include #include "cellref.hpp" #include "refdata.hpp" namespace ESM { struct ObjectState; } namespace MWWorld { class Ptr; class ESMStore; class Class; /// Used to create pointers to hold any type of LiveCellRef<> object. struct LiveCellRefBase { const Class *mClass; /** Information about this instance, such as 3D location and rotation * and individual type-dependent data. */ MWWorld::CellRef mRef; /** runtime-data */ RefData mData; LiveCellRefBase(const std::string& type, const ESM::CellRef &cref=ESM::CellRef()); /* Need this for the class to be recognized as polymorphic */ virtual ~LiveCellRefBase() { } virtual void load (const ESM::ObjectState& state) = 0; ///< Load state into a LiveCellRef, that has already been initialised with base and class. /// /// \attention Must not be called with an invalid \a state. virtual void save (ESM::ObjectState& state) const = 0; ///< Save LiveCellRef state into \a state. protected: void loadImp (const ESM::ObjectState& state); ///< Load state into a LiveCellRef, that has already been initialised with base and /// class. /// /// \attention Must not be called with an invalid \a state. void saveImp (ESM::ObjectState& state) const; ///< Save LiveCellRef state into \a state. static bool checkStateImp (const ESM::ObjectState& state); ///< Check if state is valid and report errors. /// /// \return Valid? /// /// \note Does not check if the RefId exists. }; inline bool operator== (const LiveCellRefBase& cellRef, const ESM::RefNum refNum) { return cellRef.mRef.getRefNum()==refNum; } /// A reference to one object (of any type) in a cell. /// /// Constructing this with a CellRef instance in the constructor means that /// in practice (where D is RefData) the possibly mutable data is copied /// across to mData. If later adding data (such as position) to CellRef /// this would have to be manually copied across. template struct LiveCellRef : public LiveCellRefBase { LiveCellRef(const ESM::CellRef& cref, const X* b = nullptr) : LiveCellRefBase(typeid(X).name(), cref), mBase(b) {} LiveCellRef(const X* b = nullptr) : LiveCellRefBase(typeid(X).name()), mBase(b) {} // The object that this instance is based on. const X* mBase; void load (const ESM::ObjectState& state) override; ///< Load state into a LiveCellRef, that has already been initialised with base and class. /// /// \attention Must not be called with an invalid \a state. void save (ESM::ObjectState& state) const override; ///< Save LiveCellRef state into \a state. static bool checkState (const ESM::ObjectState& state); ///< Check if state is valid and report errors. /// /// \return Valid? /// /// \note Does not check if the RefId exists. }; template void LiveCellRef::load (const ESM::ObjectState& state) { loadImp (state); } template void LiveCellRef::save (ESM::ObjectState& state) const { saveImp (state); } template bool LiveCellRef::checkState (const ESM::ObjectState& state) { return checkStateImp (state); } } #endif ================================================ FILE: apps/openmw/mwworld/localscripts.cpp ================================================ #include "localscripts.hpp" #include #include "esmstore.hpp" #include "cellstore.hpp" #include "class.hpp" #include "containerstore.hpp" namespace { struct AddScriptsVisitor { AddScriptsVisitor(MWWorld::LocalScripts& scripts) : mScripts(scripts) { } MWWorld::LocalScripts& mScripts; bool operator()(const MWWorld::Ptr& ptr) { if (ptr.getRefData().isDeleted()) return true; std::string script = ptr.getClass().getScript(ptr); if (!script.empty()) mScripts.add(script, ptr); return true; } }; struct AddContainerItemScriptsVisitor { AddContainerItemScriptsVisitor(MWWorld::LocalScripts& scripts) : mScripts(scripts) { } MWWorld::LocalScripts& mScripts; bool operator()(const MWWorld::Ptr& containerPtr) { // Ignore containers without generated content if (containerPtr.getTypeName() == typeid(ESM::Container).name() && containerPtr.getRefData().getCustomData() == nullptr) return true; MWWorld::ContainerStore& container = containerPtr.getClass().getContainerStore(containerPtr); for(MWWorld::ContainerStoreIterator it = container.begin(); it != container.end(); ++it) { std::string script = it->getClass().getScript(*it); if(script != "") { MWWorld::Ptr item = *it; item.mCell = containerPtr.getCell(); mScripts.add (script, item); } } return true; } }; } MWWorld::LocalScripts::LocalScripts (const MWWorld::ESMStore& store) : mStore (store) { mIter = mScripts.end(); } void MWWorld::LocalScripts::startIteration() { mIter = mScripts.begin(); } bool MWWorld::LocalScripts::getNext(std::pair& script) { if (mIter!=mScripts.end()) { std::list >::iterator iter = mIter++; script = *iter; return true; } return false; } void MWWorld::LocalScripts::add (const std::string& scriptName, const Ptr& ptr) { if (const ESM::Script *script = mStore.get().search (scriptName)) { try { ptr.getRefData().setLocals (*script); for (std::list >::iterator iter = mScripts.begin(); iter!=mScripts.end(); ++iter) if (iter->second==ptr) { Log(Debug::Warning) << "Error: tried to add local script twice for " << ptr.getCellRef().getRefId(); remove(ptr); break; } mScripts.emplace_back (scriptName, ptr); } catch (const std::exception& exception) { Log(Debug::Error) << "failed to add local script " << scriptName << " because an exception has been thrown: " << exception.what(); } } else Log(Debug::Warning) << "failed to add local script " << scriptName << " because the script does not exist."; } void MWWorld::LocalScripts::addCell (CellStore *cell) { AddScriptsVisitor addScriptsVisitor(*this); cell->forEach(addScriptsVisitor); AddContainerItemScriptsVisitor addContainerItemScriptsVisitor(*this); cell->forEachType(addContainerItemScriptsVisitor); cell->forEachType(addContainerItemScriptsVisitor); cell->forEachType(addContainerItemScriptsVisitor); } void MWWorld::LocalScripts::clear() { mScripts.clear(); } void MWWorld::LocalScripts::clearCell (CellStore *cell) { std::list >::iterator iter = mScripts.begin(); while (iter!=mScripts.end()) { if (iter->second.mCell==cell) { if (iter==mIter) ++mIter; mScripts.erase (iter++); } else ++iter; } } void MWWorld::LocalScripts::remove (RefData *ref) { for (std::list >::iterator iter = mScripts.begin(); iter!=mScripts.end(); ++iter) if (&(iter->second.getRefData()) == ref) { if (iter==mIter) ++mIter; mScripts.erase (iter); break; } } void MWWorld::LocalScripts::remove (const Ptr& ptr) { for (std::list >::iterator iter = mScripts.begin(); iter!=mScripts.end(); ++iter) if (iter->second==ptr) { if (iter==mIter) ++mIter; mScripts.erase (iter); break; } } ================================================ FILE: apps/openmw/mwworld/localscripts.hpp ================================================ #ifndef GAME_MWWORLD_LOCALSCRIPTS_H #define GAME_MWWORLD_LOCALSCRIPTS_H #include #include #include "ptr.hpp" namespace MWWorld { class ESMStore; class CellStore; class RefData; /// \brief List of active local scripts class LocalScripts { std::list > mScripts; std::list >::iterator mIter; const MWWorld::ESMStore& mStore; public: LocalScripts (const MWWorld::ESMStore& store); void startIteration(); ///< Set the iterator to the begin of the script list. bool getNext(std::pair& script); ///< Get next local script /// @return Did we get a script? void add (const std::string& scriptName, const Ptr& ptr); ///< Add script to collection of active local scripts. void addCell (CellStore *cell); ///< Add all local scripts in a cell. void clear(); ///< Clear active local scripts collection. void clearCell (CellStore *cell); ///< Remove all scripts belonging to \a cell. void remove (RefData *ref); void remove (const Ptr& ptr); ///< Remove script for given reference (ignored if reference does not have a script listed). }; } #endif ================================================ FILE: apps/openmw/mwworld/manualref.cpp ================================================ #include "manualref.hpp" #include "esmstore.hpp" namespace { template void create(const MWWorld::Store& list, const std::string& name, boost::any& refValue, MWWorld::Ptr& ptrValue) { const T* base = list.find(name); ESM::CellRef cellRef; cellRef.mRefNum.unset(); cellRef.mRefID = name; cellRef.mScale = 1; cellRef.mFactionRank = 0; cellRef.mChargeInt = -1; cellRef.mChargeIntRemainder = 0.0f; cellRef.mGoldValue = 1; cellRef.mEnchantmentCharge = -1; cellRef.mTeleport = false; cellRef.mLockLevel = 0; cellRef.mReferenceBlocked = 0; /* Start of tes3mp addition Set the mMpNum (unique multiplayer reference number) to 0 by default */ cellRef.mMpNum = 0; /* End of tes3mp addition */ MWWorld::LiveCellRef ref(cellRef, base); refValue = ref; ptrValue = MWWorld::Ptr(&boost::any_cast&>(refValue), nullptr); } } MWWorld::ManualRef::ManualRef(const MWWorld::ESMStore& store, const std::string& name, const int count) { std::string lowerName = Misc::StringUtils::lowerCase(name); switch (store.find(lowerName)) { case ESM::REC_ACTI: create(store.get(), lowerName, mRef, mPtr); break; case ESM::REC_ALCH: create(store.get(), lowerName, mRef, mPtr); break; case ESM::REC_APPA: create(store.get(), lowerName, mRef, mPtr); break; case ESM::REC_ARMO: create(store.get(), lowerName, mRef, mPtr); break; case ESM::REC_BOOK: create(store.get(), lowerName, mRef, mPtr); break; case ESM::REC_CLOT: create(store.get(), lowerName, mRef, mPtr); break; case ESM::REC_CONT: create(store.get(), lowerName, mRef, mPtr); break; case ESM::REC_CREA: create(store.get(), lowerName, mRef, mPtr); break; case ESM::REC_DOOR: create(store.get(), lowerName, mRef, mPtr); break; case ESM::REC_INGR: create(store.get(), lowerName, mRef, mPtr); break; case ESM::REC_LEVC: create(store.get(), lowerName, mRef, mPtr); break; case ESM::REC_LEVI: create(store.get(), lowerName, mRef, mPtr); break; case ESM::REC_LIGH: create(store.get(), lowerName, mRef, mPtr); break; case ESM::REC_LOCK: create(store.get(), lowerName, mRef, mPtr); break; case ESM::REC_MISC: create(store.get(), lowerName, mRef, mPtr); break; case ESM::REC_NPC_: create(store.get(), lowerName, mRef, mPtr); break; case ESM::REC_PROB: create(store.get(), lowerName, mRef, mPtr); break; case ESM::REC_REPA: create(store.get(), lowerName, mRef, mPtr); break; case ESM::REC_STAT: create(store.get(), lowerName, mRef, mPtr); break; case ESM::REC_WEAP: create(store.get(), lowerName, mRef, mPtr); break; case ESM::REC_BODY: create(store.get(), lowerName, mRef, mPtr); break; case 0: throw std::logic_error("failed to create manual cell ref for " + lowerName + " (unknown ID)"); default: throw std::logic_error("failed to create manual cell ref for " + lowerName + " (unknown type)"); } mPtr.getRefData().setCount(count); } ================================================ FILE: apps/openmw/mwworld/manualref.hpp ================================================ #ifndef GAME_MWWORLD_MANUALREF_H #define GAME_MWWORLD_MANUALREF_H #include #include "ptr.hpp" namespace MWWorld { /// \brief Manually constructed live cell ref class ManualRef { boost::any mRef; Ptr mPtr; ManualRef (const ManualRef&); ManualRef& operator= (const ManualRef&); public: ManualRef(const MWWorld::ESMStore& store, const std::string& name, const int count = 1); const Ptr& getPtr() const { return mPtr; } }; } #endif ================================================ FILE: apps/openmw/mwworld/nullaction.hpp ================================================ #ifndef GAME_MWWORLD_NULLACTION_H #define GAME_MWWORLD_NULLACTION_H #include "action.hpp" namespace MWWorld { /// \brief Action: do nothing class NullAction : public Action { void executeImp (const Ptr& actor) override {} bool isNullAction() override { return true; } }; } #endif ================================================ FILE: apps/openmw/mwworld/player.cpp ================================================ #include "player.hpp" #include #include /* Start of tes3mp addition Include additional headers for multiplayer purposes */ #include "../mwmp/Main.hpp" #include "../mwmp/Networking.hpp" #include "../mwmp/ObjectList.hpp" /* End of tes3mp addition */ #include #include #include #include #include #include "../mwworld/esmstore.hpp" #include "../mwworld/inventorystore.hpp" #include "../mwbase/environment.hpp" #include "../mwbase/world.hpp" #include "../mwbase/windowmanager.hpp" #include "../mwbase/mechanicsmanager.hpp" #include "../mwmechanics/movement.hpp" #include "../mwmechanics/npcstats.hpp" #include "../mwmechanics/spellutil.hpp" #include "class.hpp" #include "ptr.hpp" #include "cellstore.hpp" namespace MWWorld { Player::Player (const ESM::NPC *player) : mCellStore(nullptr), mLastKnownExteriorPosition(0,0,0), mMarkedPosition(ESM::Position()), mMarkedCell(nullptr), mAutoMove(false), mForwardBackward(0), mTeleported(false), mCurrentCrimeId(-1), mPaidCrimeId(-1), mAttackingOrSpell(false), mJumping(false) { ESM::CellRef cellRef; cellRef.blank(); cellRef.mRefID = "player"; mPlayer = LiveCellRef(cellRef, player); ESM::Position playerPos = mPlayer.mData.getPosition(); playerPos.pos[0] = playerPos.pos[1] = playerPos.pos[2] = 0; mPlayer.mData.setPosition(playerPos); } void Player::saveStats() { MWMechanics::NpcStats& stats = getPlayer().getClass().getNpcStats(getPlayer()); for (int i=0; i& gmst = MWBase::Environment::get().getWorld()->getStore().get(); MWMechanics::CreatureStats& creatureStats = getPlayer().getClass().getCreatureStats(getPlayer()); MWMechanics::NpcStats& npcStats = getPlayer().getClass().getNpcStats(getPlayer()); MWMechanics::DynamicStat health = creatureStats.getDynamic(0); creatureStats.setHealth(int(health.getBase() / gmst.find("fWereWolfHealth")->mValue.getFloat())); for (int i=0; i& gmst = MWBase::Environment::get().getWorld()->getStore().get(); MWMechanics::CreatureStats& creatureStats = getPlayer().getClass().getCreatureStats(getPlayer()); MWMechanics::NpcStats& npcStats = getPlayer().getClass().getNpcStats(getPlayer()); MWMechanics::DynamicStat health = creatureStats.getDynamic(0); creatureStats.setHealth(int(health.getBase() * gmst.find("fWereWolfHealth")->mValue.getFloat())); for(size_t i = 0;i < ESM::Attribute::Length;++i) { // Oh, Bethesda. It's "Intelligence". std::string name = "fWerewolf"+((i==ESM::Attribute::Intelligence) ? std::string("Intellegence") : ESM::Attribute::sAttributeNames[i]); MWMechanics::AttributeValue value = npcStats.getAttribute(i); value.setBase(int(gmst.find(name)->mValue.getFloat())); npcStats.setAttribute(i, value); } for(size_t i = 0;i < ESM::Skill::Length;i++) { // Acrobatics is set separately for some reason. if(i == ESM::Skill::Acrobatics) continue; // "Mercantile"! >_< 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); } } void Player::set(const ESM::NPC *player) { mPlayer.mBase = player; } void Player::setCell (MWWorld::CellStore *cellStore) { mCellStore = cellStore; } MWWorld::Ptr Player::getPlayer() { MWWorld::Ptr ptr (&mPlayer, mCellStore); return ptr; } MWWorld::ConstPtr Player::getConstPlayer() const { MWWorld::ConstPtr ptr (&mPlayer, mCellStore); return ptr; } void Player::setBirthSign (const std::string &sign) { mSign = sign; } const std::string& Player::getBirthSign() const { return mSign; } void Player::setDrawState (MWMechanics::DrawState_ state) { MWWorld::Ptr ptr = getPlayer(); ptr.getClass().getNpcStats(ptr).setDrawState (state); } bool Player::getAutoMove() const { return mAutoMove; } void Player::setAutoMove (bool enable) { MWWorld::Ptr ptr = getPlayer(); mAutoMove = enable; int value = mForwardBackward; if (mAutoMove) value = 1; ptr.getClass().getMovementSettings(ptr).mPosition[1] = value; } void Player::setLeftRight (float value) { MWWorld::Ptr ptr = getPlayer(); ptr.getClass().getMovementSettings(ptr).mPosition[0] = value; } void Player::setForwardBackward (float value) { MWWorld::Ptr ptr = getPlayer(); mForwardBackward = value; if (mAutoMove) value = 1; ptr.getClass().getMovementSettings(ptr).mPosition[1] = value; } void Player::setUpDown(int value) { MWWorld::Ptr ptr = getPlayer(); ptr.getClass().getMovementSettings(ptr).mPosition[2] = static_cast(value); } void Player::setRunState(bool run) { MWWorld::Ptr ptr = getPlayer(); ptr.getClass().getCreatureStats(ptr).setMovementFlag(MWMechanics::CreatureStats::Flag_Run, run); } void Player::setSneak(bool sneak) { MWWorld::Ptr ptr = getPlayer(); ptr.getClass().getCreatureStats(ptr).setMovementFlag(MWMechanics::CreatureStats::Flag_Sneak, sneak); } void Player::yaw(float yaw) { MWWorld::Ptr ptr = getPlayer(); ptr.getClass().getMovementSettings(ptr).mRotation[2] += yaw; } void Player::pitch(float pitch) { MWWorld::Ptr ptr = getPlayer(); ptr.getClass().getMovementSettings(ptr).mRotation[0] += pitch; } void Player::roll(float roll) { MWWorld::Ptr ptr = getPlayer(); ptr.getClass().getMovementSettings(ptr).mRotation[1] += roll; } MWMechanics::DrawState_ Player::getDrawState() { MWWorld::Ptr ptr = getPlayer(); return ptr.getClass().getNpcStats(ptr).getDrawState(); } void Player::activate() { if (MWBase::Environment::get().getWindowManager()->isGuiMode()) return; MWWorld::Ptr player = getPlayer(); const MWMechanics::NpcStats &playerStats = player.getClass().getNpcStats(player); bool godmode = MWBase::Environment::get().getWorld()->getGodModeState(); if ((!godmode && playerStats.isParalyzed()) || playerStats.getKnockedDown() || playerStats.isDead()) return; MWWorld::Ptr toActivate = MWBase::Environment::get().getWorld()->getFacedObject(); if (toActivate.isEmpty()) return; if (!toActivate.getClass().hasToolTip(toActivate)) return; /* Start of tes3mp change (major) Disable unilateral activation on this client and expect the server's reply to our packet to do it instead */ //MWBase::Environment::get().getWorld()->activate(toActivate, player); /* End of tes3mp change (major) */ /* Start of tes3mp addition Send an ID_OBJECT_ACTIVATE packet every time an object is activated here */ mwmp::ObjectList *objectList = mwmp::Main::get().getNetworking()->getObjectList(); objectList->reset(); objectList->packetOrigin = mwmp::CLIENT_GAMEPLAY; objectList->addObjectActivate(toActivate, player); objectList->sendObjectActivate(); /* End of tes3mp addition */ } bool Player::wasTeleported() const { return mTeleported; } void Player::setTeleported(bool teleported) { mTeleported = teleported; } void Player::setAttackingOrSpell(bool attackingOrSpell) { mAttackingOrSpell = attackingOrSpell; } bool Player::getAttackingOrSpell() const { return mAttackingOrSpell; } void Player::setJumping(bool jumping) { mJumping = jumping; } bool Player::getJumping() const { return mJumping; } bool Player::isInCombat() { return MWBase::Environment::get().getMechanicsManager()->getActorsFighting(getPlayer()).size() != 0; } bool Player::enemiesNearby() { return MWBase::Environment::get().getMechanicsManager()->getEnemiesNearby(getPlayer()).size() != 0; } void Player::markPosition(CellStore *markedCell, const ESM::Position& markedPosition) { mMarkedCell = markedCell; mMarkedPosition = markedPosition; } void Player::getMarkedPosition(CellStore*& markedCell, ESM::Position &markedPosition) const { markedCell = mMarkedCell; if (mMarkedCell) markedPosition = mMarkedPosition; } void Player::clear() { mCellStore = nullptr; mSign.clear(); mMarkedCell = nullptr; mAutoMove = false; mForwardBackward = 0; mTeleported = false; mAttackingOrSpell = false; mJumping = false; mCurrentCrimeId = -1; mPaidCrimeId = -1; mPreviousItems.clear(); mLastKnownExteriorPosition = osg::Vec3f(0,0,0); for (int i=0; igetCell()->getCellId(); player.mCurrentCrimeId = mCurrentCrimeId; player.mPaidCrimeId = mPaidCrimeId; player.mBirthsign = mSign; player.mLastKnownExteriorPosition[0] = mLastKnownExteriorPosition.x(); player.mLastKnownExteriorPosition[1] = mLastKnownExteriorPosition.y(); player.mLastKnownExteriorPosition[2] = mLastKnownExteriorPosition.z(); if (mMarkedCell) { player.mHasMark = true; player.mMarkedPosition = mMarkedPosition; player.mMarkedCell = mMarkedCell->getCell()->getCellId(); } else player.mHasMark = false; for (int i=0; i().search (player.mBirthsign); if (!sign) throw std::runtime_error ("invalid player state record (birthsign does not exist)"); } mCurrentCrimeId = player.mCurrentCrimeId; mPaidCrimeId = player.mPaidCrimeId; mSign = player.mBirthsign; mLastKnownExteriorPosition.x() = player.mLastKnownExteriorPosition[0]; mLastKnownExteriorPosition.y() = player.mLastKnownExteriorPosition[1]; mLastKnownExteriorPosition.z() = player.mLastKnownExteriorPosition[2]; if (player.mHasMark && !player.mMarkedCell.mPaged) { // interior cell -> need to check if it exists (exterior cell will be // generated on the fly) if (!world.getStore().get().search (player.mMarkedCell.mWorldspace)) player.mHasMark = false; // drop mark silently } if (player.mHasMark) { mMarkedPosition = player.mMarkedPosition; mMarkedCell = world.getCell (player.mMarkedCell); } else { mMarkedCell = nullptr; } mForwardBackward = 0; mTeleported = false; mPreviousItems = player.mPreviousItems; return true; } return false; } int Player::getNewCrimeId() { return ++mCurrentCrimeId; } void Player::recordCrimeId() { mPaidCrimeId = mCurrentCrimeId; } int Player::getCrimeId() const { return mPaidCrimeId; } void Player::setPreviousItem(const std::string& boundItemId, const std::string& previousItemId) { mPreviousItems[boundItemId] = previousItemId; } std::string Player::getPreviousItem(const std::string& boundItemId) { return mPreviousItems[boundItemId]; } void Player::erasePreviousItem(const std::string& boundItemId) { mPreviousItems.erase(boundItemId); } void Player::setSelectedSpell(const std::string& spellId) { Ptr player = getPlayer(); InventoryStore& store = player.getClass().getInventoryStore(player); store.setSelectedEnchantItem(store.end()); int castChance = int(MWMechanics::getSpellSuccessChance(spellId, player)); MWBase::Environment::get().getWindowManager()->setSelectedSpell(spellId, castChance); MWBase::Environment::get().getWindowManager()->updateSpellWindow(); } } ================================================ FILE: apps/openmw/mwworld/player.hpp ================================================ #ifndef GAME_MWWORLD_PLAYER_H #define GAME_MWWORLD_PLAYER_H #include #include "../mwworld/refdata.hpp" #include "../mwworld/livecellref.hpp" #include "../mwmechanics/drawstate.hpp" #include "../mwmechanics/stat.hpp" #include #include namespace ESM { struct NPC; class ESMWriter; class ESMReader; } namespace Loading { class Listener; } namespace MWWorld { class CellStore; class ConstPtr; /// \brief NPC object representing the player and additional player data class Player { LiveCellRef mPlayer; MWWorld::CellStore *mCellStore; std::string mSign; osg::Vec3f mLastKnownExteriorPosition; ESM::Position mMarkedPosition; // If no position was marked, this is nullptr CellStore* mMarkedCell; bool mAutoMove; float mForwardBackward; bool mTeleported; int mCurrentCrimeId; // the id assigned witnesses int mPaidCrimeId; // the last id paid off (0 bounty) typedef std::map PreviousItems; // previous equipped items, needed for bound spells PreviousItems mPreviousItems; // Saved stats prior to becoming a werewolf MWMechanics::SkillValue mSaveSkills[ESM::Skill::Length]; MWMechanics::AttributeValue mSaveAttributes[ESM::Attribute::Length]; bool mAttackingOrSpell; bool mJumping; public: Player(const ESM::NPC *player); void saveStats(); void restoreStats(); void setWerewolfStats(); // For mark/recall magic effects void markPosition (CellStore* markedCell, const ESM::Position& markedPosition); void getMarkedPosition (CellStore*& markedCell, ESM::Position& markedPosition) const; /// Interiors can not always be mapped to a world position. However /// world position is still required for divine / almsivi magic effects /// and the player arrow on the global map. void setLastKnownExteriorPosition (const osg::Vec3f& position) { mLastKnownExteriorPosition = position; } osg::Vec3f getLastKnownExteriorPosition() const { return mLastKnownExteriorPosition; } void set (const ESM::NPC *player); void setCell (MWWorld::CellStore *cellStore); MWWorld::Ptr getPlayer(); MWWorld::ConstPtr getConstPlayer() const; void setBirthSign(const std::string &sign); const std::string &getBirthSign() const; void setDrawState (MWMechanics::DrawState_ state); MWMechanics::DrawState_ getDrawState(); /// \todo constness /// Activate the object under the crosshair, if any void activate(); bool getAutoMove() const; void setAutoMove (bool enable); void setLeftRight (float value); void setForwardBackward (float value); void setUpDown(int value); void setRunState(bool run); void setSneak(bool sneak); void yaw(float yaw); void pitch(float pitch); void roll(float roll); bool wasTeleported() const; void setTeleported(bool teleported); void setAttackingOrSpell(bool attackingOrSpell); bool getAttackingOrSpell() const; void setJumping(bool jumping); bool getJumping() const; ///Checks all nearby actors to see if anyone has an aipackage against you bool isInCombat(); bool enemiesNearby(); void clear(); void write (ESM::ESMWriter& writer, Loading::Listener& progress) const; bool readRecord (ESM::ESMReader& reader, uint32_t type); int getNewCrimeId(); // get new id for witnesses void recordCrimeId(); // record the paid crime id when bounty is 0 int getCrimeId() const; // get the last paid crime id void setPreviousItem(const std::string& boundItemId, const std::string& previousItemId); std::string getPreviousItem(const std::string& boundItemId); void erasePreviousItem(const std::string& boundItemId); void setSelectedSpell(const std::string& spellId); }; } #endif ================================================ FILE: apps/openmw/mwworld/projectilemanager.cpp ================================================ #include "projectilemanager.hpp" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "../mwworld/manualref.hpp" #include "../mwworld/class.hpp" #include "../mwworld/esmstore.hpp" #include "../mwworld/inventorystore.hpp" #include "../mwbase/soundmanager.hpp" #include "../mwbase/world.hpp" #include "../mwbase/environment.hpp" #include "../mwmechanics/combat.hpp" #include "../mwmechanics/creaturestats.hpp" #include "../mwmechanics/spellcasting.hpp" #include "../mwmechanics/actorutil.hpp" #include "../mwmechanics/aipackage.hpp" #include "../mwmechanics/weapontype.hpp" #include "../mwrender/animation.hpp" #include "../mwrender/vismask.hpp" #include "../mwrender/renderingmanager.hpp" #include "../mwrender/util.hpp" #include "../mwsound/sound.hpp" #include "../mwphysics/physicssystem.hpp" #include "../mwphysics/projectile.hpp" /* Start of tes3mp addition Include additional headers for multiplayer purposes */ #include "../mwmp/MechanicsHelper.hpp" /* End of tes3mp addition */ namespace { ESM::EffectList getMagicBoltData(std::vector& projectileIDs, std::set& sounds, float& speed, std::string& texture, std::string& sourceName, const std::string& id) { const MWWorld::ESMStore& esmStore = MWBase::Environment::get().getWorld()->getStore(); const ESM::EffectList* effects; if (const ESM::Spell* spell = esmStore.get().search(id)) // check if it's a spell { sourceName = spell->mName; effects = &spell->mEffects; } else // check if it's an enchanted item { MWWorld::ManualRef ref(esmStore, id); MWWorld::Ptr ptr = ref.getPtr(); const ESM::Enchantment* ench = esmStore.get().find(ptr.getClass().getEnchantment(ptr)); sourceName = ptr.getClass().getName(ptr); effects = &ench->mEffects; } int count = 0; speed = 0.0f; ESM::EffectList projectileEffects; for (std::vector::const_iterator iter (effects->mList.begin()); iter!=effects->mList.end(); ++iter) { const ESM::MagicEffect *magicEffect = MWBase::Environment::get().getWorld()->getStore().get().find ( iter->mEffectID); // Speed of multi-effect projectiles should be the average of the constituent effects, // based on observation of the original engine. speed += magicEffect->mData.mSpeed; count++; if (iter->mRange != ESM::RT_Target) continue; if (magicEffect->mBolt.empty()) projectileIDs.emplace_back("VFX_DefaultBolt"); else projectileIDs.push_back(magicEffect->mBolt); static const std::string schools[] = { "alteration", "conjuration", "destruction", "illusion", "mysticism", "restoration" }; if (!magicEffect->mBoltSound.empty()) sounds.emplace(magicEffect->mBoltSound); else sounds.emplace(schools[magicEffect->mData.mSchool] + " bolt"); projectileEffects.mList.push_back(*iter); } if (count != 0) speed /= count; // the particle texture is only used if there is only one projectile if (projectileEffects.mList.size() == 1) { const ESM::MagicEffect *magicEffect = MWBase::Environment::get().getWorld()->getStore().get().find ( effects->mList.begin()->mEffectID); texture = magicEffect->mParticle; } if (projectileEffects.mList.size() > 1) // insert a VFX_Multiple projectile if there are multiple projectile effects { const std::string ID = "VFX_Multiple" + std::to_string(effects->mList.size()); std::vector::iterator it; it = projectileIDs.begin(); it = projectileIDs.insert(it, ID); } return projectileEffects; } osg::Vec4 getMagicBoltLightDiffuseColor(const ESM::EffectList& effects) { // Calculate combined light diffuse color from magical effects osg::Vec4 lightDiffuseColor; float lightDiffuseRed = 0.0f; float lightDiffuseGreen = 0.0f; float lightDiffuseBlue = 0.0f; for (std::vector::const_iterator iter(effects.mList.begin()); iter != effects.mList.end(); ++iter) { const ESM::MagicEffect *magicEffect = MWBase::Environment::get().getWorld()->getStore().get().find( iter->mEffectID); lightDiffuseRed += (static_cast(magicEffect->mData.mRed) / 255.f); lightDiffuseGreen += (static_cast(magicEffect->mData.mGreen) / 255.f); lightDiffuseBlue += (static_cast(magicEffect->mData.mBlue) / 255.f); } int numberOfEffects = effects.mList.size(); lightDiffuseColor = osg::Vec4(lightDiffuseRed / numberOfEffects , lightDiffuseGreen / numberOfEffects , lightDiffuseBlue / numberOfEffects , 1.0f); return lightDiffuseColor; } } namespace MWWorld { ProjectileManager::ProjectileManager(osg::Group* parent, Resource::ResourceSystem* resourceSystem, MWRender::RenderingManager* rendering, MWPhysics::PhysicsSystem* physics) : mParent(parent) , mResourceSystem(resourceSystem) , mRendering(rendering) , mPhysics(physics) , mCleanupTimer(0.0f) { } /// Rotates an osg::PositionAttitudeTransform over time. class RotateCallback : public osg::NodeCallback { public: RotateCallback(const osg::Vec3f& axis = osg::Vec3f(0,-1,0), float rotateSpeed = osg::PI*2) : mAxis(axis) , mRotateSpeed(rotateSpeed) { } void operator()(osg::Node* node, osg::NodeVisitor* nv) override { osg::PositionAttitudeTransform* transform = static_cast(node); double time = nv->getFrameStamp()->getSimulationTime(); osg::Quat orient = osg::Quat(time * mRotateSpeed, mAxis); transform->setAttitude(orient); traverse(node, nv); } private: osg::Vec3f mAxis; float mRotateSpeed; }; void ProjectileManager::createModel(State &state, const std::string &model, const osg::Vec3f& pos, const osg::Quat& orient, bool rotate, bool createLight, osg::Vec4 lightDiffuseColor, std::string texture) { state.mNode = new osg::PositionAttitudeTransform; state.mNode->setNodeMask(MWRender::Mask_Effect); state.mNode->setPosition(pos); state.mNode->setAttitude(orient); osg::Group* attachTo = state.mNode; if (rotate) { osg::ref_ptr rotateNode (new osg::PositionAttitudeTransform); rotateNode->addUpdateCallback(new RotateCallback()); state.mNode->addChild(rotateNode); attachTo = rotateNode; } osg::ref_ptr projectile = mResourceSystem->getSceneManager()->getInstance(model, attachTo); if (state.mIdMagic.size() > 1) for (size_t iter = 1; iter != state.mIdMagic.size(); ++iter) { std::ostringstream nodeName; nodeName << "Dummy" << std::setw(2) << std::setfill('0') << iter; const ESM::Weapon* weapon = MWBase::Environment::get().getWorld()->getStore().get().find (state.mIdMagic.at(iter)); SceneUtil::FindByNameVisitor findVisitor(nodeName.str()); attachTo->accept(findVisitor); if (findVisitor.mFoundNode) mResourceSystem->getSceneManager()->getInstance("meshes\\" + weapon->mModel, findVisitor.mFoundNode); } if (createLight) { osg::ref_ptr projectileLight(new osg::Light); projectileLight->setAmbient(osg::Vec4(1.0f, 1.0f, 1.0f, 1.0f)); projectileLight->setDiffuse(lightDiffuseColor); projectileLight->setSpecular(osg::Vec4(0.0f, 0.0f, 0.0f, 0.0f)); projectileLight->setConstantAttenuation(0.f); projectileLight->setLinearAttenuation(0.1f); projectileLight->setQuadraticAttenuation(0.f); projectileLight->setPosition(osg::Vec4(pos, 1.0)); SceneUtil::LightSource* projectileLightSource = new SceneUtil::LightSource; projectileLightSource->setNodeMask(MWRender::Mask_Lighting); projectileLightSource->setRadius(66.f); state.mNode->addChild(projectileLightSource); projectileLightSource->setLight(projectileLight); } SceneUtil::DisableFreezeOnCullVisitor disableFreezeOnCullVisitor; state.mNode->accept(disableFreezeOnCullVisitor); state.mNode->addCullCallback(new SceneUtil::LightListCallback); mParent->addChild(state.mNode); state.mEffectAnimationTime.reset(new MWRender::EffectAnimationTime); SceneUtil::AssignControllerSourcesVisitor assignVisitor (state.mEffectAnimationTime); state.mNode->accept(assignVisitor); MWRender::overrideFirstRootTexture(texture, mResourceSystem, projectile); } void ProjectileManager::update(State& state, float duration) { state.mEffectAnimationTime->addTime(duration); } void ProjectileManager::launchMagicBolt(const std::string &spellId, const Ptr &caster, const osg::Vec3f& fallbackDirection) { osg::Vec3f pos = caster.getRefData().getPosition().asVec3(); if (caster.getClass().isActor()) { // Note: we ignore the collision box offset, this is required to make some flying creatures work as intended. pos.z() += mPhysics->getRenderingHalfExtents(caster).z() * 2 * Constants::TorsoHeight; } if (MWBase::Environment::get().getWorld()->isUnderwater(caster.getCell(), pos)) // Underwater casting not possible return; osg::Quat orient; if (caster.getClass().isActor()) orient = osg::Quat(caster.getRefData().getPosition().rot[0], osg::Vec3f(-1,0,0)) * osg::Quat(caster.getRefData().getPosition().rot[2], osg::Vec3f(0,0,-1)); else orient.makeRotate(osg::Vec3f(0,1,0), osg::Vec3f(fallbackDirection)); /* Start of tes3mp addition If the actor casting this is a LocalPlayer or LocalActor, track their projectile origin so it can be sent in the next PlayerCast or ActorCast packet Otherwise, set the projectileOrigin for a DedicatedPlayer or DedicatedActor */ mwmp::Cast* localCast = MechanicsHelper::getLocalCast(caster); if (localCast) { localCast->hasProjectile = true; localCast->projectileOrigin.origin[0] = pos.x(); localCast->projectileOrigin.origin[1] = pos.y(); localCast->projectileOrigin.origin[2] = pos.z(); localCast->projectileOrigin.orientation[0] = orient.x(); localCast->projectileOrigin.orientation[1] = orient.y(); localCast->projectileOrigin.orientation[2] = orient.z(); localCast->projectileOrigin.orientation[3] = orient.w(); } else { mwmp::Cast* dedicatedCast = MechanicsHelper::getDedicatedCast(caster); if (dedicatedCast) { pos = osg::Vec3f(dedicatedCast->projectileOrigin.origin[0], dedicatedCast->projectileOrigin.origin[1], dedicatedCast->projectileOrigin.origin[2]); orient = osg::Quat(dedicatedCast->projectileOrigin.orientation[0], dedicatedCast->projectileOrigin.orientation[1], dedicatedCast->projectileOrigin.orientation[2], dedicatedCast->projectileOrigin.orientation[3]); } } /* End of tes3mp addition */ MagicBoltState state; state.mSpellId = spellId; state.mCasterHandle = caster; if (caster.getClass().isActor()) state.mActorId = caster.getClass().getCreatureStats(caster).getActorId(); else state.mActorId = -1; std::string texture; state.mEffects = getMagicBoltData(state.mIdMagic, state.mSoundIds, state.mSpeed, texture, state.mSourceName, state.mSpellId); // Non-projectile should have been removed by getMagicBoltData if (state.mEffects.mList.empty()) return; if (!caster.getClass().isActor() && fallbackDirection.length2() <= 0) { Log(Debug::Warning) << "Unable to launch magic bolt (direction to target is empty)"; return; } MWWorld::ManualRef ref(MWBase::Environment::get().getWorld()->getStore(), state.mIdMagic.at(0)); MWWorld::Ptr ptr = ref.getPtr(); osg::Vec4 lightDiffuseColor = getMagicBoltLightDiffuseColor(state.mEffects); auto model = ptr.getClass().getModel(ptr); createModel(state, model, pos, orient, true, true, lightDiffuseColor, texture); MWBase::SoundManager *sndMgr = MWBase::Environment::get().getSoundManager(); for (const std::string &soundid : state.mSoundIds) { MWBase::Sound *sound = sndMgr->playSound3D(pos, soundid, 1.0f, 1.0f, MWSound::Type::Sfx, MWSound::PlayMode::Loop); if (sound) state.mSounds.push_back(sound); } // in case there are multiple effects, the model is a dummy without geometry. Use the second effect for physics shape if (state.mIdMagic.size() > 1) model = "meshes\\" + MWBase::Environment::get().getWorld()->getStore().get().find(state.mIdMagic.at(1))->mModel; state.mProjectileId = mPhysics->addProjectile(caster, pos, model, true); state.mToDelete = false; mMagicBolts.push_back(state); } void ProjectileManager::launchProjectile(Ptr actor, ConstPtr projectile, const osg::Vec3f &pos, const osg::Quat &orient, Ptr bow, float speed, float attackStrength) { ProjectileState state; state.mActorId = actor.getClass().getCreatureStats(actor).getActorId(); state.mBowId = bow.getCellRef().getRefId(); state.mVelocity = orient * osg::Vec3f(0,1,0) * speed; state.mIdArrow = projectile.getCellRef().getRefId(); state.mCasterHandle = actor; state.mAttackStrength = attackStrength; int type = projectile.get()->mBase->mData.mType; state.mThrown = MWMechanics::getWeaponType(type)->mWeaponClass == ESM::WeaponType::Thrown; MWWorld::ManualRef ref(MWBase::Environment::get().getWorld()->getStore(), projectile.getCellRef().getRefId()); MWWorld::Ptr ptr = ref.getPtr(); const auto model = ptr.getClass().getModel(ptr); createModel(state, model, pos, orient, false, false, osg::Vec4(0,0,0,0)); if (!ptr.getClass().getEnchantment(ptr).empty()) SceneUtil::addEnchantedGlow(state.mNode, mResourceSystem, ptr.getClass().getEnchantmentColor(ptr)); state.mProjectileId = mPhysics->addProjectile(actor, pos, model, false); state.mToDelete = false; mProjectiles.push_back(state); } void ProjectileManager::updateCasters() { for (auto& state : mProjectiles) mPhysics->setCaster(state.mProjectileId, state.getCaster()); for (auto& state : mMagicBolts) { // casters are identified by actor id in the savegame. objects doesn't have one so they can't be identified back. // TODO: should object-type caster be restored from savegame? if (state.mActorId == -1) continue; auto caster = state.getCaster(); if (caster.isEmpty()) { Log(Debug::Error) << "Couldn't find caster with ID " << state.mActorId; cleanupMagicBolt(state); continue; } mPhysics->setCaster(state.mProjectileId, caster); } } void ProjectileManager::update(float dt) { periodicCleanup(dt); moveProjectiles(dt); moveMagicBolts(dt); } void ProjectileManager::periodicCleanup(float dt) { mCleanupTimer -= dt; if (mCleanupTimer <= 0.0f) { mCleanupTimer = 2.0f; auto isCleanable = [](const ProjectileManager::State& state) -> bool { const float farawayThreshold = 72000.0f; osg::Vec3 playerPos = MWMechanics::getPlayer().getRefData().getPosition().asVec3(); return (state.mNode->getPosition() - playerPos).length2() >= farawayThreshold*farawayThreshold; }; for (auto& projectileState : mProjectiles) { if (isCleanable(projectileState)) cleanupProjectile(projectileState); } for (auto& magicBoltState : mMagicBolts) { if (isCleanable(magicBoltState)) cleanupMagicBolt(magicBoltState); } } } void ProjectileManager::moveMagicBolts(float duration) { for (auto& magicBoltState : mMagicBolts) { if (magicBoltState.mToDelete) continue; auto* projectile = mPhysics->getProjectile(magicBoltState.mProjectileId); if (!projectile->isActive()) continue; // If the actor caster is gone, the magic bolt needs to be removed from the scene during the next frame. MWWorld::Ptr caster = magicBoltState.getCaster(); if (!caster.isEmpty() && caster.getClass().isActor()) { if (caster.getRefData().getCount() <= 0 || caster.getClass().getCreatureStats(caster).isDead()) { cleanupMagicBolt(magicBoltState); continue; } } osg::Quat orient = magicBoltState.mNode->getAttitude(); static float fTargetSpellMaxSpeed = MWBase::Environment::get().getWorld()->getStore().get() .find("fTargetSpellMaxSpeed")->mValue.getFloat(); float speed = fTargetSpellMaxSpeed * magicBoltState.mSpeed; osg::Vec3f direction = orient * osg::Vec3f(0,1,0); direction.normalize(); osg::Vec3f newPos = projectile->getPosition() + direction * duration * speed; update(magicBoltState, duration); // For AI actors, get combat targets to use in the ray cast. Only those targets will return a positive hit result. std::vector targetActors; if (!caster.isEmpty() && caster.getClass().isActor() && caster != MWMechanics::getPlayer()) caster.getClass().getCreatureStats(caster).getAiSequence().getCombatTargets(targetActors); projectile->setValidTargets(targetActors); mPhysics->updateProjectile(magicBoltState.mProjectileId, newPos); } } void ProjectileManager::moveProjectiles(float duration) { for (auto& projectileState : mProjectiles) { if (projectileState.mToDelete) continue; auto* projectile = mPhysics->getProjectile(projectileState.mProjectileId); if (!projectile->isActive()) continue; // gravity constant - must be way lower than the gravity affecting actors, since we're not // simulating aerodynamics at all projectileState.mVelocity -= osg::Vec3f(0, 0, Constants::GravityConst * Constants::UnitsPerMeter * 0.1f) * duration; osg::Vec3f newPos = projectile->getPosition() + projectileState.mVelocity * duration; // rotation does not work well for throwing projectiles - their roll angle will depend on shooting direction. if (!projectileState.mThrown) { osg::Quat orient; orient.makeRotate(osg::Vec3f(0,1,0), projectileState.mVelocity); projectileState.mNode->setAttitude(orient); } update(projectileState, duration); MWWorld::Ptr caster = projectileState.getCaster(); // For AI actors, get combat targets to use in the ray cast. Only those targets will return a positive hit result. std::vector targetActors; if (!caster.isEmpty() && caster.getClass().isActor() && caster != MWMechanics::getPlayer()) caster.getClass().getCreatureStats(caster).getAiSequence().getCombatTargets(targetActors); projectile->setValidTargets(targetActors); mPhysics->updateProjectile(projectileState.mProjectileId, newPos); } } void ProjectileManager::processHits() { for (auto& projectileState : mProjectiles) { if (projectileState.mToDelete) continue; auto* projectile = mPhysics->getProjectile(projectileState.mProjectileId); const auto pos = projectile->getPosition(); projectileState.mNode->setPosition(pos); if (projectile->isActive()) continue; const auto target = projectile->getTarget(); auto caster = projectileState.getCaster(); assert(target != caster); if (caster.isEmpty()) caster = target; // Try to get a Ptr to the bow that was used. It might no longer exist. MWWorld::ManualRef projectileRef(MWBase::Environment::get().getWorld()->getStore(), projectileState.mIdArrow); MWWorld::Ptr bow = projectileRef.getPtr(); if (!caster.isEmpty() && projectileState.mIdArrow != projectileState.mBowId) { MWWorld::InventoryStore& inv = caster.getClass().getInventoryStore(caster); MWWorld::ContainerStoreIterator invIt = inv.getSlot(MWWorld::InventoryStore::Slot_CarriedRight); if (invIt != inv.end() && Misc::StringUtils::ciEqual(invIt->getCellRef().getRefId(), projectileState.mBowId)) bow = *invIt; } if (projectile->getHitWater()) mRendering->emitWaterRipple(pos); MWMechanics::projectileHit(caster, target, bow, projectileRef.getPtr(), pos, projectileState.mAttackStrength); cleanupProjectile(projectileState); } for (auto& magicBoltState : mMagicBolts) { if (magicBoltState.mToDelete) continue; auto* projectile = mPhysics->getProjectile(magicBoltState.mProjectileId); const auto pos = projectile->getPosition(); magicBoltState.mNode->setPosition(pos); for (const auto& sound : magicBoltState.mSounds) sound->setPosition(pos); if (projectile->isActive()) continue; const auto target = projectile->getTarget(); const auto caster = magicBoltState.getCaster(); assert(target != caster); MWMechanics::CastSpell cast(caster, target); cast.mHitPosition = pos; cast.mId = magicBoltState.mSpellId; cast.mSourceName = magicBoltState.mSourceName; cast.mStack = false; cast.inflict(target, caster, magicBoltState.mEffects, ESM::RT_Target, false, true); MWBase::Environment::get().getWorld()->explodeSpell(pos, magicBoltState.mEffects, caster, target, ESM::RT_Target, magicBoltState.mSpellId, magicBoltState.mSourceName); cleanupMagicBolt(magicBoltState); } mProjectiles.erase(std::remove_if(mProjectiles.begin(), mProjectiles.end(), [](const State& state) { return state.mToDelete; }), mProjectiles.end()); mMagicBolts.erase(std::remove_if(mMagicBolts.begin(), mMagicBolts.end(), [](const State& state) { return state.mToDelete; }), mMagicBolts.end()); } void ProjectileManager::cleanupProjectile(ProjectileManager::ProjectileState& state) { mParent->removeChild(state.mNode); mPhysics->removeProjectile(state.mProjectileId); state.mToDelete = true; } void ProjectileManager::cleanupMagicBolt(ProjectileManager::MagicBoltState& state) { mParent->removeChild(state.mNode); mPhysics->removeProjectile(state.mProjectileId); state.mToDelete = true; for (size_t soundIter = 0; soundIter != state.mSounds.size(); soundIter++) { MWBase::Environment::get().getSoundManager()->stopSound(state.mSounds.at(soundIter)); } } void ProjectileManager::clear() { for (auto& mProjectile : mProjectiles) cleanupProjectile(mProjectile); mProjectiles.clear(); for (auto& mMagicBolt : mMagicBolts) cleanupMagicBolt(mMagicBolt); mMagicBolts.clear(); } void ProjectileManager::write(ESM::ESMWriter &writer, Loading::Listener &progress) const { for (std::vector::const_iterator it = mProjectiles.begin(); it != mProjectiles.end(); ++it) { writer.startRecord(ESM::REC_PROJ); ESM::ProjectileState state; state.mId = it->mIdArrow; state.mPosition = ESM::Vector3(osg::Vec3f(it->mNode->getPosition())); state.mOrientation = ESM::Quaternion(osg::Quat(it->mNode->getAttitude())); state.mActorId = it->mActorId; state.mBowId = it->mBowId; state.mVelocity = it->mVelocity; state.mAttackStrength = it->mAttackStrength; state.save(writer); writer.endRecord(ESM::REC_PROJ); } for (std::vector::const_iterator it = mMagicBolts.begin(); it != mMagicBolts.end(); ++it) { writer.startRecord(ESM::REC_MPRJ); ESM::MagicBoltState state; state.mId = it->mIdMagic.at(0); state.mPosition = ESM::Vector3(osg::Vec3f(it->mNode->getPosition())); state.mOrientation = ESM::Quaternion(osg::Quat(it->mNode->getAttitude())); state.mActorId = it->mActorId; state.mSpellId = it->mSpellId; state.mSpeed = it->mSpeed; state.save(writer); writer.endRecord(ESM::REC_MPRJ); } } bool ProjectileManager::readRecord(ESM::ESMReader &reader, uint32_t type) { if (type == ESM::REC_PROJ) { ESM::ProjectileState esm; esm.load(reader); ProjectileState state; state.mActorId = esm.mActorId; state.mBowId = esm.mBowId; state.mVelocity = esm.mVelocity; state.mIdArrow = esm.mId; state.mAttackStrength = esm.mAttackStrength; state.mToDelete = false; std::string model; try { MWWorld::ManualRef ref(MWBase::Environment::get().getWorld()->getStore(), esm.mId); MWWorld::Ptr ptr = ref.getPtr(); model = ptr.getClass().getModel(ptr); int weaponType = ptr.get()->mBase->mData.mType; state.mThrown = MWMechanics::getWeaponType(weaponType)->mWeaponClass == ESM::WeaponType::Thrown; state.mProjectileId = mPhysics->addProjectile(state.getCaster(), osg::Vec3f(esm.mPosition), model, false); } catch(...) { return true; } createModel(state, model, osg::Vec3f(esm.mPosition), osg::Quat(esm.mOrientation), false, false, osg::Vec4(0,0,0,0)); mProjectiles.push_back(state); return true; } if (type == ESM::REC_MPRJ) { ESM::MagicBoltState esm; esm.load(reader); MagicBoltState state; state.mIdMagic.push_back(esm.mId); state.mSpellId = esm.mSpellId; state.mActorId = esm.mActorId; state.mToDelete = false; std::string texture; try { state.mEffects = getMagicBoltData(state.mIdMagic, state.mSoundIds, state.mSpeed, texture, state.mSourceName, state.mSpellId); } catch(...) { Log(Debug::Warning) << "Warning: Failed to recreate magic projectile from saved data (id \"" << state.mSpellId << "\" no longer exists?)"; return true; } state.mSpeed = esm.mSpeed; // speed is derived from non-projectile effects as well as // projectile effects, so we can't calculate it from the save // file's effect list, which is already trimmed of non-projectile // effects. We need to use the stored value. std::string model; try { MWWorld::ManualRef ref(MWBase::Environment::get().getWorld()->getStore(), state.mIdMagic.at(0)); MWWorld::Ptr ptr = ref.getPtr(); model = ptr.getClass().getModel(ptr); } catch(...) { return true; } osg::Vec4 lightDiffuseColor = getMagicBoltLightDiffuseColor(state.mEffects); createModel(state, model, osg::Vec3f(esm.mPosition), osg::Quat(esm.mOrientation), true, true, lightDiffuseColor, texture); state.mProjectileId = mPhysics->addProjectile(state.getCaster(), osg::Vec3f(esm.mPosition), model, true); MWBase::SoundManager *sndMgr = MWBase::Environment::get().getSoundManager(); for (const std::string &soundid : state.mSoundIds) { MWBase::Sound *sound = sndMgr->playSound3D(esm.mPosition, soundid, 1.0f, 1.0f, MWSound::Type::Sfx, MWSound::PlayMode::Loop); if (sound) state.mSounds.push_back(sound); } mMagicBolts.push_back(state); return true; } return false; } int ProjectileManager::countSavedGameRecords() const { return mMagicBolts.size() + mProjectiles.size(); } MWWorld::Ptr ProjectileManager::State::getCaster() { if (!mCasterHandle.isEmpty()) return mCasterHandle; return MWBase::Environment::get().getWorld()->searchPtrViaActorId(mActorId); } } ================================================ FILE: apps/openmw/mwworld/projectilemanager.hpp ================================================ #ifndef OPENMW_MWWORLD_PROJECTILEMANAGER_H #define OPENMW_MWWORLD_PROJECTILEMANAGER_H #include #include #include #include #include "../mwbase/soundmanager.hpp" #include "ptr.hpp" namespace MWPhysics { class PhysicsSystem; } namespace Loading { class Listener; } namespace osg { class Group; class Quat; } namespace Resource { class ResourceSystem; } namespace MWRender { class EffectAnimationTime; class RenderingManager; } namespace MWWorld { class ProjectileManager { public: ProjectileManager (osg::Group* parent, Resource::ResourceSystem* resourceSystem, MWRender::RenderingManager* rendering, MWPhysics::PhysicsSystem* physics); /// If caster is an actor, the actor's facing orientation is used. Otherwise fallbackDirection is used. void launchMagicBolt (const std::string &spellId, const MWWorld::Ptr& caster, const osg::Vec3f& fallbackDirection); void launchProjectile (MWWorld::Ptr actor, MWWorld::ConstPtr projectile, const osg::Vec3f& pos, const osg::Quat& orient, MWWorld::Ptr bow, float speed, float attackStrength); void updateCasters(); void update(float dt); void processHits(); /// Removes all current projectiles. Should be called when switching to a new worldspace. void clear(); void write (ESM::ESMWriter& writer, Loading::Listener& progress) const; bool readRecord (ESM::ESMReader& reader, uint32_t type); int countSavedGameRecords() const; private: osg::ref_ptr mParent; Resource::ResourceSystem* mResourceSystem; MWRender::RenderingManager* mRendering; MWPhysics::PhysicsSystem* mPhysics; float mCleanupTimer; struct State { osg::ref_ptr mNode; std::shared_ptr mEffectAnimationTime; int mActorId; int mProjectileId; // TODO: this will break when the game is saved and reloaded, since there is currently // no way to write identifiers for non-actors to a savegame. MWWorld::Ptr mCasterHandle; MWWorld::Ptr getCaster(); // MW-ids of a magic projectile std::vector mIdMagic; // MW-id of an arrow projectile std::string mIdArrow; bool mToDelete; }; struct MagicBoltState : public State { std::string mSpellId; // Name of item to display as effect source in magic menu (in case we casted an enchantment) std::string mSourceName; ESM::EffectList mEffects; float mSpeed; std::vector mSounds; std::set mSoundIds; }; struct ProjectileState : public State { // RefID of the bow or crossbow the actor was using when this projectile was fired (may be empty) std::string mBowId; osg::Vec3f mVelocity; float mAttackStrength; bool mThrown; }; std::vector mMagicBolts; std::vector mProjectiles; void cleanupProjectile(ProjectileState& state); void cleanupMagicBolt(MagicBoltState& state); void periodicCleanup(float dt); void moveProjectiles(float dt); void moveMagicBolts(float dt); void createModel (State& state, const std::string& model, const osg::Vec3f& pos, const osg::Quat& orient, bool rotate, bool createLight, osg::Vec4 lightDiffuseColor, std::string texture = ""); void update (State& state, float duration); void operator=(const ProjectileManager&); ProjectileManager(const ProjectileManager&); }; } #endif ================================================ FILE: apps/openmw/mwworld/ptr.cpp ================================================ #include "ptr.hpp" #include #include "containerstore.hpp" #include "class.hpp" #include "livecellref.hpp" const std::string& MWWorld::Ptr::getTypeName() const { if(mRef != nullptr) return mRef->mClass->getTypeName(); throw std::runtime_error("Can't get type name from an empty object."); } MWWorld::LiveCellRefBase *MWWorld::Ptr::getBase() const { if (!mRef) throw std::runtime_error ("Can't access cell ref pointed to by null Ptr"); return mRef; } MWWorld::CellRef& MWWorld::Ptr::getCellRef() const { assert(mRef); return mRef->mRef; } MWWorld::RefData& MWWorld::Ptr::getRefData() const { assert(mRef); return mRef->mData; } void MWWorld::Ptr::setContainerStore (ContainerStore *store) { assert (store); assert (!mCell); mContainerStore = store; } MWWorld::ContainerStore *MWWorld::Ptr::getContainerStore() const { return mContainerStore; } MWWorld::Ptr::operator const void *() { return mRef; } // ------------------------------------------------------------------------------- const std::string &MWWorld::ConstPtr::getTypeName() const { if(mRef != nullptr) return mRef->mClass->getTypeName(); throw std::runtime_error("Can't get type name from an empty object."); } const MWWorld::LiveCellRefBase *MWWorld::ConstPtr::getBase() const { if (!mRef) throw std::runtime_error ("Can't access cell ref pointed to by null Ptr"); return mRef; } void MWWorld::ConstPtr::setContainerStore (const ContainerStore *store) { assert (store); assert (!mCell); mContainerStore = store; } const MWWorld::ContainerStore *MWWorld::ConstPtr::getContainerStore() const { return mContainerStore; } MWWorld::ConstPtr::operator const void *() { return mRef; } ================================================ FILE: apps/openmw/mwworld/ptr.hpp ================================================ #ifndef GAME_MWWORLD_PTR_H #define GAME_MWWORLD_PTR_H #include #include #include #include "livecellref.hpp" namespace MWWorld { class ContainerStore; class CellStore; struct LiveCellRefBase; /// \brief Pointer to a LiveCellRef class Ptr { public: MWWorld::LiveCellRefBase *mRef; CellStore *mCell; ContainerStore *mContainerStore; public: Ptr(MWWorld::LiveCellRefBase *liveCellRef=nullptr, CellStore *cell=nullptr) : mRef(liveCellRef), mCell(cell), mContainerStore(nullptr) { } bool isEmpty() const { return mRef == nullptr; } const std::string& getTypeName() const; const Class& getClass() const { if(mRef != nullptr) return *(mRef->mClass); throw std::runtime_error("Cannot get class of an empty object"); } template MWWorld::LiveCellRef *get() const { MWWorld::LiveCellRef *ref = dynamic_cast*>(mRef); if(ref) return ref; std::stringstream str; str<< "Bad LiveCellRef cast to "<mRef.getRefId().c_str() << " " << mRef->mRef.getRefNum().mIndex << "-" << mRef->mRef.getMpNum(); /* End of tes3mp change (major) */ else str<< "an empty object"; throw std::runtime_error(str.str()); } MWWorld::LiveCellRefBase *getBase() const; MWWorld::CellRef& getCellRef() const; RefData& getRefData() const; CellStore *getCell() const { assert(mCell); return mCell; } bool isInCell() const { return (mContainerStore == nullptr) && (mCell != nullptr); } void setContainerStore (ContainerStore *store); ///< Must not be called on references that are in a cell. ContainerStore *getContainerStore() const; ///< May return a 0-pointer, if reference is not in a container. operator const void *(); ///< Return a 0-pointer, if Ptr is empty; return a non-0-pointer, if Ptr is not empty }; /// \brief Pointer to a const LiveCellRef /// @note a Ptr can be implicitely converted to a ConstPtr, but you can not convert a ConstPtr to a Ptr. class ConstPtr { public: const MWWorld::LiveCellRefBase *mRef; const CellStore *mCell; const ContainerStore *mContainerStore; public: ConstPtr(const MWWorld::LiveCellRefBase *liveCellRef=nullptr, const CellStore *cell=nullptr) : mRef(liveCellRef), mCell(cell), mContainerStore(nullptr) { } ConstPtr(const MWWorld::Ptr& ptr) : mRef(ptr.mRef), mCell(ptr.mCell), mContainerStore(ptr.mContainerStore) { } bool isEmpty() const { return mRef == nullptr; } const std::string& getTypeName() const; const Class& getClass() const { if(mRef != nullptr) return *(mRef->mClass); throw std::runtime_error("Cannot get class of an empty object"); } template const MWWorld::LiveCellRef *get() const { const MWWorld::LiveCellRef *ref = dynamic_cast*>(mRef); if(ref) return ref; std::stringstream str; str<< "Bad LiveCellRef cast to "<mRef.getRefId().c_str() << " " << mRef->mRef.getRefNum().mIndex << "-" << mRef->mRef.getMpNum(); /* End of tes3mp change (major) */ else str<< "an empty object"; throw std::runtime_error(str.str()); } const MWWorld::LiveCellRefBase *getBase() const; const MWWorld::CellRef& getCellRef() const { assert(mRef); return mRef->mRef; } const RefData& getRefData() const { assert(mRef); return mRef->mData; } const CellStore *getCell() const { assert(mCell); return mCell; } bool isInCell() const { return (mContainerStore == nullptr) && (mCell != nullptr); } void setContainerStore (const ContainerStore *store); ///< Must not be called on references that are in a cell. const ContainerStore *getContainerStore() const; ///< May return a 0-pointer, if reference is not in a container. operator const void *(); ///< Return a 0-pointer, if Ptr is empty; return a non-0-pointer, if Ptr is not empty }; inline bool operator== (const Ptr& left, const Ptr& right) { return left.mRef==right.mRef; } inline bool operator!= (const Ptr& left, const Ptr& right) { return !(left==right); } inline bool operator< (const Ptr& left, const Ptr& right) { return left.mRef= (const Ptr& left, const Ptr& right) { return !(left (const Ptr& left, const Ptr& right) { return rightright); } inline bool operator== (const ConstPtr& left, const ConstPtr& right) { return left.mRef==right.mRef; } inline bool operator!= (const ConstPtr& left, const ConstPtr& right) { return !(left==right); } inline bool operator< (const ConstPtr& left, const ConstPtr& right) { return left.mRef= (const ConstPtr& left, const ConstPtr& right) { return !(left (const ConstPtr& left, const ConstPtr& right) { return rightright); } } #endif ================================================ FILE: apps/openmw/mwworld/recordcmp.hpp ================================================ #ifndef OPENMW_MWWORLD_RECORDCMP_H #define OPENMW_MWWORLD_RECORDCMP_H #include #include namespace MWWorld { struct RecordCmp { template bool operator()(const T &x, const T& y) const { return x.mId < y.mId; } }; template <> inline bool RecordCmp::operator()(const ESM::Dialogue &x, const ESM::Dialogue &y) const { return Misc::StringUtils::ciLess(x.mId, y.mId); } template <> inline bool RecordCmp::operator()(const ESM::Cell &x, const ESM::Cell &y) const { return Misc::StringUtils::ciLess(x.mName, y.mName); } template <> inline bool RecordCmp::operator()(const ESM::Pathgrid &x, const ESM::Pathgrid &y) const { return Misc::StringUtils::ciLess(x.mCell, y.mCell); } } // end namespace #endif ================================================ FILE: apps/openmw/mwworld/refdata.cpp ================================================ #include "refdata.hpp" #include #include "customdata.hpp" #include "cellstore.hpp" #include "../mwbase/environment.hpp" #include "../mwbase/world.hpp" namespace { enum RefDataFlags { Flag_SuppressActivate = 1, // If set, activation will be suppressed and redirected to the OnActivate flag, which can then be handled by a script. Flag_OnActivate = 2, Flag_ActivationBuffered = 4 }; } namespace MWWorld { void RefData::copy (const RefData& refData) { mBaseNode = refData.mBaseNode; mLocals = refData.mLocals; mEnabled = refData.mEnabled; mCount = refData.mCount; mPosition = refData.mPosition; mChanged = refData.mChanged; mDeletedByContentFile = refData.mDeletedByContentFile; mFlags = refData.mFlags; mAnimationState = refData.mAnimationState; mCustomData = refData.mCustomData ? refData.mCustomData->clone() : nullptr; } void RefData::cleanup() { mBaseNode = nullptr; mCustomData = nullptr; } RefData::RefData() : mBaseNode(nullptr), mDeletedByContentFile(false), mEnabled (true), mCount (1), mCustomData (nullptr), mChanged(false), mFlags(0) { for (int i=0; i<3; ++i) { mPosition.pos[i] = 0; mPosition.rot[i] = 0; } } RefData::RefData (const ESM::CellRef& cellRef) : mBaseNode(nullptr), mDeletedByContentFile(false), mEnabled (true), mCount (1), mPosition (cellRef.mPos), mCustomData (nullptr), mChanged(false), mFlags(0) // Loading from ESM/ESP files -> assume unchanged { } RefData::RefData (const ESM::ObjectState& objectState, bool deletedByContentFile) : mBaseNode(nullptr), mDeletedByContentFile(deletedByContentFile), mEnabled (objectState.mEnabled != 0), mCount (objectState.mCount), mPosition (objectState.mPosition), mAnimationState(objectState.mAnimationState), mCustomData (nullptr), mChanged(true), mFlags(objectState.mFlags) // Loading from a savegame -> assume changed { // "Note that the ActivationFlag_UseEnabled is saved to the reference, // which will result in permanently suppressed activation if the reference script is removed. // This occurred when removing the animated containers mod, and the fix in MCP is to reset UseEnabled to true on loading a game." mFlags &= (~Flag_SuppressActivate); } RefData::RefData (const RefData& refData) : mBaseNode(nullptr), mCustomData (nullptr) { try { copy (refData); mFlags &= ~(Flag_SuppressActivate|Flag_OnActivate|Flag_ActivationBuffered); } catch (...) { cleanup(); throw; } } void RefData::write (ESM::ObjectState& objectState, const std::string& scriptId) const { objectState.mHasLocals = mLocals.write (objectState.mLocals, scriptId); objectState.mEnabled = mEnabled; objectState.mCount = mCount; objectState.mPosition = mPosition; objectState.mFlags = mFlags; objectState.mAnimationState = mAnimationState; } RefData& RefData::operator= (const RefData& refData) { try { cleanup(); copy (refData); } catch (...) { cleanup(); throw; } return *this; } RefData::~RefData() { try { cleanup(); } catch (...) {} } void RefData::setBaseNode(SceneUtil::PositionAttitudeTransform *base) { mBaseNode = base; } SceneUtil::PositionAttitudeTransform* RefData::getBaseNode() { return mBaseNode; } const SceneUtil::PositionAttitudeTransform* RefData::getBaseNode() const { return mBaseNode; } int RefData::getCount(bool absolute) const { if(absolute) return std::abs(mCount); return mCount; } void RefData::setLocals (const ESM::Script& script) { if (mLocals.configure (script) && !mLocals.isEmpty()) mChanged = true; } void RefData::setCount (int count) { if(count == 0) MWBase::Environment::get().getWorld()->removeRefScript(this); mChanged = true; mCount = count; } void RefData::setDeletedByContentFile(bool deleted) { mDeletedByContentFile = deleted; } bool RefData::isDeleted() const { return mDeletedByContentFile || mCount == 0; } bool RefData::isDeletedByContentFile() const { return mDeletedByContentFile; } MWScript::Locals& RefData::getLocals() { return mLocals; } bool RefData::isEnabled() const { return mEnabled; } void RefData::enable() { if (!mEnabled) { mChanged = true; mEnabled = true; } } void RefData::disable() { if (mEnabled) { mChanged = true; mEnabled = false; } } void RefData::setPosition(const ESM::Position& pos) { mChanged = true; mPosition = pos; } const ESM::Position& RefData::getPosition() const { return mPosition; } void RefData::setCustomData(std::unique_ptr&& value) noexcept { mChanged = true; // We do not currently track CustomData, so assume anything with a CustomData is changed mCustomData = std::move(value); } CustomData *RefData::getCustomData() { return mCustomData.get(); } const CustomData *RefData::getCustomData() const { return mCustomData.get(); } bool RefData::hasChanged() const { return mChanged || !mAnimationState.empty(); } bool RefData::activateByScript() { bool ret = (mFlags & Flag_ActivationBuffered); mFlags &= ~(Flag_SuppressActivate|Flag_OnActivate); return ret; } bool RefData::activate() { if (mFlags & Flag_SuppressActivate) { mFlags |= Flag_OnActivate|Flag_ActivationBuffered; return false; } else { return true; } } bool RefData::onActivate() { bool ret = mFlags & Flag_OnActivate; mFlags |= Flag_SuppressActivate; mFlags &= (~Flag_OnActivate); return ret; } const ESM::AnimationState& RefData::getAnimationState() const { return mAnimationState; } ESM::AnimationState& RefData::getAnimationState() { return mAnimationState; } } ================================================ FILE: apps/openmw/mwworld/refdata.hpp ================================================ #ifndef GAME_MWWORLD_REFDATA_H #define GAME_MWWORLD_REFDATA_H #include #include #include "../mwscript/locals.hpp" #include "../mwworld/customdata.hpp" #include #include namespace SceneUtil { class PositionAttitudeTransform; } namespace ESM { class Script; class CellRef; struct ObjectState; } namespace MWWorld { class CustomData; class RefData { SceneUtil::PositionAttitudeTransform* mBaseNode; MWScript::Locals mLocals; /// separate delete flag used for deletion by a content file /// @note not stored in the save game file. bool mDeletedByContentFile; bool mEnabled; /// 0: deleted int mCount; ESM::Position mPosition; ESM::AnimationState mAnimationState; std::unique_ptr mCustomData; void copy (const RefData& refData); void cleanup(); bool mChanged; unsigned int mFlags; public: RefData(); /// @param cellRef Used to copy constant data such as position into this class where it can /// be altered without affecting the original data. This makes it possible /// to reset the position as the original data is still held in the CellRef RefData (const ESM::CellRef& cellRef); RefData (const ESM::ObjectState& objectState, bool deletedByContentFile); ///< Ignores local variables and custom data (not enough context available here to /// perform these operations). RefData (const RefData& refData); RefData (RefData&& other) noexcept = default; ~RefData(); void write (ESM::ObjectState& objectState, const std::string& scriptId = "") const; ///< Ignores custom data (not enough context available here to /// perform this operations). RefData& operator= (const RefData& refData); RefData& operator= (RefData&& other) noexcept = default; /// Return base node (can be a null pointer). SceneUtil::PositionAttitudeTransform* getBaseNode(); /// Return base node (can be a null pointer). const SceneUtil::PositionAttitudeTransform* getBaseNode() const; /// Set base node (can be a null pointer). void setBaseNode (SceneUtil::PositionAttitudeTransform* base); int getCount(bool absolute = true) const; void setLocals (const ESM::Script& script); void setCount (int count); ///< Set object count (an object pile is a simple object with a count >1). /// /// \warning Do not call setCount() to add or remove objects from a /// container or an actor's inventory. Call ContainerStore::add() or /// ContainerStore::remove() instead. /// This flag is only used for content stack loading and will not be stored in the savegame. /// If the object was deleted by gameplay, then use setCount(0) instead. void setDeletedByContentFile(bool deleted); /// Returns true if the object was either deleted by the content file or by gameplay. bool isDeleted() const; /// Returns true if the object was deleted by a content file. bool isDeletedByContentFile() const; MWScript::Locals& getLocals(); bool isEnabled() const; void enable(); void disable(); void setPosition (const ESM::Position& pos); const ESM::Position& getPosition() const; void setCustomData(std::unique_ptr&& value) noexcept; ///< Set custom data (potentially replacing old custom data). The ownership of \a data is /// transferred to this. CustomData *getCustomData(); ///< May return a 0-pointer. The ownership of the return data object is not transferred. const CustomData *getCustomData() const; bool activate(); bool onActivate(); bool activateByScript(); bool hasChanged() const; ///< Has this RefData changed since it was originally loaded? const ESM::AnimationState& getAnimationState() const; ESM::AnimationState& getAnimationState(); /* Start of tes3mp addition Track the last state communicated to the server for this reference, to avoid packet spam when the server denies our state change request or is slow to reply */ enum StateCommunication { None = 0, Enabled = 1, Disabled = 2, Deleted = 3 }; private: short mLastCommunicatedState = StateCommunication::None; public: short getLastCommunicatedState() { return mLastCommunicatedState; }; void setLastCommunicatedState(short communicationState) { mLastCommunicatedState = communicationState; }; /* End of tes3mp addition */ }; } #endif ================================================ FILE: apps/openmw/mwworld/scene.cpp ================================================ #include "scene.hpp" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include /* Start of tes3mp addition Include additional headers for multiplayer purposes */ #include "../mwmp/Main.hpp" #include "../mwmp/LocalPlayer.hpp" /* End of tes3mp addition */ #include "../mwbase/environment.hpp" #include "../mwbase/world.hpp" #include "../mwbase/soundmanager.hpp" #include "../mwbase/mechanicsmanager.hpp" #include "../mwbase/windowmanager.hpp" #include "../mwrender/renderingmanager.hpp" #include "../mwrender/landmanager.hpp" #include "../mwphysics/physicssystem.hpp" #include "../mwphysics/actor.hpp" #include "../mwphysics/object.hpp" #include "../mwphysics/heightfield.hpp" #include "player.hpp" #include "localscripts.hpp" #include "esmstore.hpp" #include "class.hpp" #include "cellvisitors.hpp" #include "cellstore.hpp" #include "cellpreloader.hpp" namespace { using MWWorld::RotationOrder; osg::Quat makeActorOsgQuat(const ESM::Position& position) { return osg::Quat(position.rot[2], osg::Vec3(0, 0, -1)); } osg::Quat makeInversedOrderObjectOsgQuat(const ESM::Position& position) { const float xr = position.rot[0]; const float yr = position.rot[1]; const float zr = position.rot[2]; return osg::Quat(xr, osg::Vec3(-1, 0, 0)) * osg::Quat(yr, osg::Vec3(0, -1, 0)) * osg::Quat(zr, osg::Vec3(0, 0, -1)); } osg::Quat makeObjectOsgQuat(const ESM::Position& position) { const float xr = position.rot[0]; const float yr = position.rot[1]; const float zr = position.rot[2]; return osg::Quat(zr, osg::Vec3(0, 0, -1)) * osg::Quat(yr, osg::Vec3(0, -1, 0)) * osg::Quat(xr, osg::Vec3(-1, 0, 0)); } void setNodeRotation(const MWWorld::Ptr& ptr, MWRender::RenderingManager& rendering, RotationOrder order) { if (!ptr.getRefData().getBaseNode()) return; rendering.rotateObject(ptr, ptr.getClass().isActor() ? makeActorOsgQuat(ptr.getRefData().getPosition()) : (order == RotationOrder::inverse ? makeInversedOrderObjectOsgQuat(ptr.getRefData().getPosition()) : makeObjectOsgQuat(ptr.getRefData().getPosition())) ); } std::string getModel(const MWWorld::Ptr &ptr, const VFS::Manager *vfs) { bool useAnim = ptr.getClass().useAnim(); std::string model = ptr.getClass().getModel(ptr); if (useAnim) model = Misc::ResourceHelpers::correctActorModelPath(model, vfs); const std::string &id = ptr.getCellRef().getRefId(); if (id == "prisonmarker" || id == "divinemarker" || id == "templemarker" || id == "northmarker") model = ""; // marker objects that have a hardcoded function in the game logic, should be hidden from the player return model; } void addObject(const MWWorld::Ptr& ptr, MWPhysics::PhysicsSystem& physics, MWRender::RenderingManager& rendering, std::set& pagedRefs) { if (ptr.getRefData().getBaseNode() || physics.getActor(ptr)) { Log(Debug::Warning) << "Warning: Tried to add " << ptr.getCellRef().getRefId() << " to the scene twice"; return; } bool useAnim = ptr.getClass().useAnim(); std::string model = getModel(ptr, rendering.getResourceSystem()->getVFS()); const ESM::RefNum& refnum = ptr.getCellRef().getRefNum(); if (!refnum.hasContentFile() || pagedRefs.find(refnum) == pagedRefs.end()) ptr.getClass().insertObjectRendering(ptr, model, rendering); else ptr.getRefData().setBaseNode(new SceneUtil::PositionAttitudeTransform); // FIXME remove this when physics code is fixed not to depend on basenode setNodeRotation(ptr, rendering, RotationOrder::direct); ptr.getClass().insertObject (ptr, model, physics); if (useAnim) MWBase::Environment::get().getMechanicsManager()->add(ptr); if (ptr.getClass().isActor()) rendering.addWaterRippleEmitter(ptr); // Restore effect particles MWBase::Environment::get().getWorld()->applyLoopingParticles(ptr); } void addObject(const MWWorld::Ptr& ptr, const MWPhysics::PhysicsSystem& physics, DetourNavigator::Navigator& navigator) { if (const auto object = physics.getObject(ptr)) { if (ptr.getClass().isDoor() && !ptr.getCellRef().getTeleport()) { btVector3 aabbMin; btVector3 aabbMax; object->getShapeInstance()->getCollisionShape()->getAabb(btTransform::getIdentity(), aabbMin, aabbMax); const auto center = (aabbMax + aabbMin) * 0.5f; const auto distanceFromDoor = MWBase::Environment::get().getWorld()->getMaxActivationDistance() * 0.5f; const auto toPoint = aabbMax.x() - aabbMin.x() < aabbMax.y() - aabbMin.y() ? btVector3(distanceFromDoor, 0, 0) : btVector3(0, distanceFromDoor, 0); const auto transform = object->getTransform(); const btTransform closedDoorTransform( Misc::Convert::toBullet(makeObjectOsgQuat(ptr.getCellRef().getPosition())), transform.getOrigin() ); const auto start = Misc::Convert::makeOsgVec3f(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 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), transform ); } else { navigator.addObject( DetourNavigator::ObjectId(object), DetourNavigator::ObjectShapes(object->getShapeInstance()), object->getTransform() ); } } else if (physics.getActor(ptr)) { navigator.addAgent(MWBase::Environment::get().getWorld()->getPathfindingHalfExtents(ptr)); } } struct InsertVisitor { MWWorld::CellStore& mCell; Loading::Listener& mLoadingListener; bool mTest; std::vector mToInsert; InsertVisitor (MWWorld::CellStore& cell, Loading::Listener& loadingListener, bool test); bool operator() (const MWWorld::Ptr& ptr); template void insert(AddObject&& addObject); }; InsertVisitor::InsertVisitor (MWWorld::CellStore& cell, Loading::Listener& loadingListener, bool test) : mCell (cell), mLoadingListener (loadingListener), mTest(test) {} bool InsertVisitor::operator() (const MWWorld::Ptr& ptr) { // do not insert directly as we can't modify the cell from within the visitation // CreatureLevList::insertObjectRendering may spawn a new creature mToInsert.push_back(ptr); return true; } template void InsertVisitor::insert(AddObject&& addObject) { for (MWWorld::Ptr& ptr : mToInsert) { if (!ptr.getRefData().isDeleted() && ptr.getRefData().isEnabled()) { try { addObject(ptr); } catch (const std::exception& e) { std::string error ("failed to render '" + ptr.getCellRef().getRefId() + "': "); Log(Debug::Error) << error + e.what(); } } if (!mTest) mLoadingListener.increaseProgress (1); } } struct PositionVisitor { bool operator() (const MWWorld::Ptr& ptr) { if (!ptr.getRefData().isDeleted() && ptr.getRefData().isEnabled()) ptr.getClass().adjustPosition (ptr, false); return true; } }; int getCellPositionDistanceToOrigin(const std::pair& cellPosition) { return std::abs(cellPosition.first) + std::abs(cellPosition.second); } } namespace MWWorld { void Scene::removeFromPagedRefs(const Ptr &ptr) { const ESM::RefNum& refnum = ptr.getCellRef().getRefNum(); if (refnum.hasContentFile() && mPagedRefs.erase(refnum)) { if (!ptr.getRefData().getBaseNode()) return; ptr.getClass().insertObjectRendering(ptr, getModel(ptr, mRendering.getResourceSystem()->getVFS()), mRendering); setNodeRotation(ptr, mRendering, RotationOrder::direct); reloadTerrain(); } } void Scene::updateObjectPosition(const Ptr &ptr, const osg::Vec3f &pos, bool movePhysics) { mRendering.moveObject(ptr, pos); if (movePhysics) { mPhysics->updatePosition(ptr); } } void Scene::updateObjectRotation(const Ptr &ptr, RotationOrder order) { setNodeRotation(ptr, mRendering, order); mPhysics->updateRotation(ptr); } void Scene::updateObjectScale(const Ptr &ptr) { float scale = ptr.getCellRef().getScale(); osg::Vec3f scaleVec (scale, scale, scale); ptr.getClass().adjustScale(ptr, scaleVec, true); mRendering.scaleObject(ptr, scaleVec); mPhysics->updateScale(ptr); } void Scene::update (float duration, bool paused) { mPreloader->updateCache(mRendering.getReferenceTime()); preloadCells(duration); mRendering.update (duration, paused); } void Scene::unloadCell (CellStoreCollection::iterator iter, bool test) { if (!test) Log(Debug::Info) << "Unloading cell " << (*iter)->getCell()->getDescription(); const auto navigator = MWBase::Environment::get().getWorld()->getNavigator(); ListAndResetObjectsVisitor visitor; /* Start of tes3mp addition Set a const pointer to the iterator's ESM::Cell here, because (*iter)->getCell() can become invalid later down */ const ESM::Cell* cell = (*iter)->getCell(); /* End of tes3mp addition */ (*iter)->forEach(visitor); const auto world = MWBase::Environment::get().getWorld(); for (const auto& ptr : visitor.mObjects) { if (const auto object = mPhysics->getObject(ptr)) navigator->removeObject(DetourNavigator::ObjectId(object)); else if (mPhysics->getActor(ptr)) { navigator->removeAgent(world->getPathfindingHalfExtents(ptr)); mRendering.removeActorPath(ptr); } mPhysics->remove(ptr); } const auto cellX = (*iter)->getCell()->getGridX(); const auto cellY = (*iter)->getCell()->getGridY(); if ((*iter)->getCell()->isExterior()) { if (const auto heightField = mPhysics->getHeightField(cellX, cellY)) navigator->removeObject(DetourNavigator::ObjectId(heightField)); mPhysics->removeHeightField(cellX, cellY); } if ((*iter)->getCell()->hasWater()) navigator->removeWater(osg::Vec2i(cellX, cellY)); if (const auto pathgrid = world->getStore().get().search(*(*iter)->getCell())) navigator->removePathgrid(*pathgrid); const auto player = world->getPlayerPtr(); navigator->update(player.getRefData().getPosition().asVec3()); MWBase::Environment::get().getMechanicsManager()->drop (*iter); mRendering.removeCell(*iter); MWBase::Environment::get().getWindowManager()->removeCell(*iter); MWBase::Environment::get().getWorld()->getLocalScripts().clearCell (*iter); MWBase::Environment::get().getSoundManager()->stopSound (*iter); mActiveCells.erase(*iter); /* Start of tes3mp addition Store a cell unload for the LocalPlayer */ mwmp::Main::get().getLocalPlayer()->storeCellState(*cell, mwmp::CellState::UNLOAD); /* End of tes3mp addition */ } void Scene::loadCell (CellStore *cell, Loading::Listener* loadingListener, bool respawn, bool test) { std::pair result = mActiveCells.insert(cell); if(result.second) { if (test) Log(Debug::Info) << "Testing cell " << cell->getCell()->getDescription(); else Log(Debug::Info) << "Loading cell " << cell->getCell()->getDescription(); float verts = ESM::Land::LAND_SIZE; float worldsize = ESM::Land::REAL_SIZE; const auto world = MWBase::Environment::get().getWorld(); const auto navigator = world->getNavigator(); const int cellX = cell->getCell()->getGridX(); const int cellY = cell->getCell()->getGridY(); // Load terrain physics first... if (!test && cell->getCell()->isExterior()) { osg::ref_ptr land = mRendering.getLandManager()->getLand(cellX, cellY); const ESM::Land::LandData* data = land ? land->getData(ESM::Land::DATA_VHGT) : nullptr; if (data) { mPhysics->addHeightField (data->mHeights, cellX, cellY, worldsize / (verts-1), verts, data->mMinHeight, data->mMaxHeight, land.get()); } else { static std::vector defaultHeight; defaultHeight.resize(verts*verts, ESM::Land::DEFAULT_HEIGHT); mPhysics->addHeightField (&defaultHeight[0], cell->getCell()->getGridX(), cell->getCell()->getGridY(), worldsize / (verts-1), verts, ESM::Land::DEFAULT_HEIGHT, ESM::Land::DEFAULT_HEIGHT, land.get()); } if (const auto heightField = mPhysics->getHeightField(cellX, cellY)) navigator->addObject(DetourNavigator::ObjectId(heightField), heightField, *heightField->getShape(), heightField->getCollisionObject()->getWorldTransform()); } if (const auto pathgrid = world->getStore().get().search(*cell->getCell())) navigator->addPathgrid(*cell->getCell(), *pathgrid); // register local scripts // do this before insertCell, to make sure we don't add scripts from levelled creature spawning twice MWBase::Environment::get().getWorld()->getLocalScripts().addCell (cell); if (respawn) cell->respawn(); // ... then references. This is important for adjustPosition to work correctly. insertCell (*cell, loadingListener, test); mRendering.addCell(cell); if (!test) { MWBase::Environment::get().getWindowManager()->addCell(cell); bool waterEnabled = cell->getCell()->hasWater() || cell->isExterior(); float waterLevel = cell->getWaterLevel(); mRendering.setWaterEnabled(waterEnabled); if (waterEnabled) { mPhysics->enableWater(waterLevel); mRendering.setWaterHeight(waterLevel); if (cell->getCell()->isExterior()) { if (const auto heightField = mPhysics->getHeightField(cellX, cellY)) navigator->addWater(osg::Vec2i(cellX, cellY), ESM::Land::REAL_SIZE, cell->getWaterLevel(), heightField->getCollisionObject()->getWorldTransform()); } else { navigator->addWater(osg::Vec2i(cellX, cellY), std::numeric_limits::max(), cell->getWaterLevel(), btTransform::getIdentity()); } } else mPhysics->disableWater(); const auto player = MWBase::Environment::get().getWorld()->getPlayerPtr(); navigator->update(player.getRefData().getPosition().asVec3()); if (!cell->isExterior() && !(cell->getCell()->mData.mFlags & ESM::Cell::QuasiEx)) { mRendering.configureAmbient(cell->getCell()); } /* Start of tes3mp addition Store a cell load for the LocalPlayer */ mwmp::Main::get().getLocalPlayer()->storeCellState(*cell->getCell(), mwmp::CellState::LOAD); /* End of tes3mp addition */ } } mPreloader->notifyLoaded(cell); } void Scene::clear() { CellStoreCollection::iterator active = mActiveCells.begin(); while (active!=mActiveCells.end()) unloadCell (active++); assert(mActiveCells.empty()); mCurrentCell = nullptr; mPreloader->clear(); } osg::Vec4i Scene::gridCenterToBounds(const osg::Vec2i& centerCell) const { return osg::Vec4i(centerCell.x()-mHalfGridSize,centerCell.y()-mHalfGridSize,centerCell.x()+mHalfGridSize+1,centerCell.y()+mHalfGridSize+1); } osg::Vec2i Scene::getNewGridCenter(const osg::Vec3f &pos, const osg::Vec2i* currentGridCenter) const { if (currentGridCenter) { float centerX, centerY; MWBase::Environment::get().getWorld()->indexToPosition(currentGridCenter->x(), currentGridCenter->y(), centerX, centerY, true); float distance = std::max(std::abs(centerX-pos.x()), std::abs(centerY-pos.y())); const float maxDistance = Constants::CellSizeInUnits / 2 + mCellLoadingThreshold; // 1/2 cell size + threshold if (distance <= maxDistance) return *currentGridCenter; } osg::Vec2i newCenter; MWBase::Environment::get().getWorld()->positionToIndex(pos.x(), pos.y(), newCenter.x(), newCenter.y()); return newCenter; } void Scene::playerMoved(const osg::Vec3f &pos) { const auto navigator = MWBase::Environment::get().getWorld()->getNavigator(); const auto player = MWBase::Environment::get().getWorld()->getPlayerPtr(); navigator->updatePlayerPosition(player.getRefData().getPosition().asVec3()); if (!mCurrentCell || !mCurrentCell->isExterior()) return; osg::Vec2i newCell = getNewGridCenter(pos, &mCurrentGridCenter); if (newCell != mCurrentGridCenter) changeCellGrid(pos, newCell.x(), newCell.y()); } void Scene::changeCellGrid (const osg::Vec3f &pos, int playerCellX, int playerCellY, bool changeEvent) { CellStoreCollection::iterator active = mActiveCells.begin(); while (active!=mActiveCells.end()) { if ((*active)->getCell()->isExterior()) { if (std::abs (playerCellX-(*active)->getCell()->getGridX())<=mHalfGridSize && std::abs (playerCellY-(*active)->getCell()->getGridY())<=mHalfGridSize) { // keep cells within the new grid ++active; continue; } } unloadCell (active++); } mCurrentGridCenter = osg::Vec2i(playerCellX, playerCellY); osg::Vec4i newGrid = gridCenterToBounds(mCurrentGridCenter); mRendering.setActiveGrid(newGrid); preloadTerrain(pos, true); mPagedRefs.clear(); mRendering.getPagedRefnums(newGrid, mPagedRefs); std::size_t refsToLoad = 0; std::vector> cellsPositionsToLoad; // get the number of refs to load for (int x = playerCellX - mHalfGridSize; x <= playerCellX + mHalfGridSize; ++x) { for (int y = playerCellY - mHalfGridSize; y <= playerCellY + mHalfGridSize; ++y) { CellStoreCollection::iterator iter = mActiveCells.begin(); while (iter!=mActiveCells.end()) { assert ((*iter)->getCell()->isExterior()); if (x==(*iter)->getCell()->getGridX() && y==(*iter)->getCell()->getGridY()) break; ++iter; } if (iter==mActiveCells.end()) { refsToLoad += MWBase::Environment::get().getWorld()->getExterior(x, y)->count(); cellsPositionsToLoad.emplace_back(x, y); } } } Loading::Listener* loadingListener = MWBase::Environment::get().getWindowManager()->getLoadingScreen(); Loading::ScopedLoad load(loadingListener); std::string loadingExteriorText = "#{sLoadingMessage3}"; loadingListener->setLabel(loadingExteriorText); loadingListener->setProgressRange(refsToLoad); const auto getDistanceToPlayerCell = [&] (const std::pair& cellPosition) { return std::abs(cellPosition.first - playerCellX) + std::abs(cellPosition.second - playerCellY); }; const auto getCellPositionPriority = [&] (const std::pair& cellPosition) { return std::make_pair(getDistanceToPlayerCell(cellPosition), getCellPositionDistanceToOrigin(cellPosition)); }; std::sort(cellsPositionsToLoad.begin(), cellsPositionsToLoad.end(), [&] (const std::pair& lhs, const std::pair& rhs) { return getCellPositionPriority(lhs) < getCellPositionPriority(rhs); }); // Load cells for (const auto& cellPosition : cellsPositionsToLoad) { const auto x = cellPosition.first; const auto y = cellPosition.second; CellStoreCollection::iterator iter = mActiveCells.begin(); while (iter != mActiveCells.end()) { assert ((*iter)->getCell()->isExterior()); if (x == (*iter)->getCell()->getGridX() && y == (*iter)->getCell()->getGridY()) break; ++iter; } if (iter == mActiveCells.end()) { CellStore *cell = MWBase::Environment::get().getWorld()->getExterior(x, y); loadCell (cell, loadingListener, changeEvent); } } /* Start of tes3mp addition Send an ID_PLAYER_CELL_STATE packet with all cell states stored in LocalPlayer and then clear them, but only if the player is logged in on the server */ if (mwmp::Main::get().getLocalPlayer()->isLoggedIn()) { mwmp::Main::get().getLocalPlayer()->sendCellStates(); mwmp::Main::get().getLocalPlayer()->clearCellStates(); } /* End of tes3mp addition */ CellStore* current = MWBase::Environment::get().getWorld()->getExterior(playerCellX, playerCellY); MWBase::Environment::get().getWindowManager()->changeCell(current); if (changeEvent) mCellChanged = true; mNavigator.wait(*loadingListener, DetourNavigator::WaitConditionType::requiredTilesPresent); } void Scene::testExteriorCells() { // Note: temporary disable ICO to decrease memory usage mRendering.getResourceSystem()->getSceneManager()->setIncrementalCompileOperation(nullptr); mRendering.getResourceSystem()->setExpiryDelay(1.f); const MWWorld::Store &cells = MWBase::Environment::get().getWorld()->getStore().get(); Loading::Listener* loadingListener = MWBase::Environment::get().getWindowManager()->getLoadingScreen(); Loading::ScopedLoad load(loadingListener); loadingListener->setProgressRange(cells.getExtSize()); MWWorld::Store::iterator it = cells.extBegin(); int i = 1; for (; it != cells.extEnd(); ++it) { loadingListener->setLabel("Testing exterior cells ("+std::to_string(i)+"/"+std::to_string(cells.getExtSize())+")..."); CellStoreCollection::iterator iter = mActiveCells.begin(); CellStore *cell = MWBase::Environment::get().getWorld()->getExterior(it->mData.mX, it->mData.mY); loadCell (cell, loadingListener, false, true); iter = mActiveCells.begin(); while (iter != mActiveCells.end()) { if (it->isExterior() && it->mData.mX == (*iter)->getCell()->getGridX() && it->mData.mY == (*iter)->getCell()->getGridY()) { unloadCell(iter, true); break; } ++iter; } mRendering.getResourceSystem()->updateCache(mRendering.getReferenceTime()); mRendering.getUnrefQueue()->flush(mRendering.getWorkQueue()); loadingListener->increaseProgress (1); i++; } mRendering.getResourceSystem()->getSceneManager()->setIncrementalCompileOperation(mRendering.getIncrementalCompileOperation()); mRendering.getResourceSystem()->setExpiryDelay(Settings::Manager::getFloat("cache expiry delay", "Cells")); } void Scene::testInteriorCells() { // Note: temporary disable ICO to decrease memory usage mRendering.getResourceSystem()->getSceneManager()->setIncrementalCompileOperation(nullptr); mRendering.getResourceSystem()->setExpiryDelay(1.f); const MWWorld::Store &cells = MWBase::Environment::get().getWorld()->getStore().get(); Loading::Listener* loadingListener = MWBase::Environment::get().getWindowManager()->getLoadingScreen(); Loading::ScopedLoad load(loadingListener); loadingListener->setProgressRange(cells.getIntSize()); int i = 1; MWWorld::Store::iterator it = cells.intBegin(); for (; it != cells.intEnd(); ++it) { loadingListener->setLabel("Testing interior cells ("+std::to_string(i)+"/"+std::to_string(cells.getIntSize())+")..."); CellStore *cell = MWBase::Environment::get().getWorld()->getInterior(it->mName); loadCell (cell, loadingListener, false, true); CellStoreCollection::iterator iter = mActiveCells.begin(); while (iter != mActiveCells.end()) { assert (!(*iter)->getCell()->isExterior()); if (it->mName == (*iter)->getCell()->mName) { unloadCell(iter, true); break; } ++iter; } mRendering.getResourceSystem()->updateCache(mRendering.getReferenceTime()); mRendering.getUnrefQueue()->flush(mRendering.getWorkQueue()); loadingListener->increaseProgress (1); i++; } mRendering.getResourceSystem()->getSceneManager()->setIncrementalCompileOperation(mRendering.getIncrementalCompileOperation()); mRendering.getResourceSystem()->setExpiryDelay(Settings::Manager::getFloat("cache expiry delay", "Cells")); } void Scene::changePlayerCell(CellStore *cell, const ESM::Position &pos, bool adjustPlayerPos) { mCurrentCell = cell; mRendering.enableTerrain(cell->isExterior()); MWBase::World *world = MWBase::Environment::get().getWorld(); MWWorld::Ptr old = world->getPlayerPtr(); world->getPlayer().setCell(cell); MWWorld::Ptr player = world->getPlayerPtr(); mRendering.updatePlayerPtr(player); if (adjustPlayerPos) { world->moveObject(player, pos.pos[0], pos.pos[1], pos.pos[2]); float x = pos.rot[0]; float y = pos.rot[1]; float z = pos.rot[2]; world->rotateObject(player, x, y, z); player.getClass().adjustPosition(player, true); } MWBase::Environment::get().getMechanicsManager()->updateCell(old, player); MWBase::Environment::get().getWindowManager()->watchActor(player); mPhysics->updatePtr(old, player); world->adjustSky(); mLastPlayerPos = player.getRefData().getPosition().asVec3(); } Scene::Scene (MWRender::RenderingManager& rendering, MWPhysics::PhysicsSystem *physics, DetourNavigator::Navigator& navigator) : mCurrentCell (nullptr), mCellChanged (false), mPhysics(physics), mRendering(rendering), mNavigator(navigator) , mCellLoadingThreshold(1024.f) , mPreloadDistance(Settings::Manager::getInt("preload distance", "Cells")) , mPreloadEnabled(Settings::Manager::getBool("preload enabled", "Cells")) , mPreloadExteriorGrid(Settings::Manager::getBool("preload exterior grid", "Cells")) , mPreloadDoors(Settings::Manager::getBool("preload doors", "Cells")) , mPreloadFastTravel(Settings::Manager::getBool("preload fast travel", "Cells")) , mPredictionTime(Settings::Manager::getFloat("prediction time", "Cells")) { mPreloader.reset(new CellPreloader(rendering.getResourceSystem(), physics->getShapeManager(), rendering.getTerrain(), rendering.getLandManager())); mPreloader->setWorkQueue(mRendering.getWorkQueue()); mPreloader->setUnrefQueue(rendering.getUnrefQueue()); mPhysics->setUnrefQueue(rendering.getUnrefQueue()); rendering.getResourceSystem()->setExpiryDelay(Settings::Manager::getFloat("cache expiry delay", "Cells")); mPreloader->setExpiryDelay(Settings::Manager::getFloat("preload cell expiry delay", "Cells")); mPreloader->setMinCacheSize(Settings::Manager::getInt("preload cell cache min", "Cells")); mPreloader->setMaxCacheSize(Settings::Manager::getInt("preload cell cache max", "Cells")); mPreloader->setPreloadInstances(Settings::Manager::getBool("preload instances", "Cells")); } Scene::~Scene() { } bool Scene::hasCellChanged() const { return mCellChanged; } const Scene::CellStoreCollection& Scene::getActiveCells() const { return mActiveCells; } void Scene::changeToInteriorCell (const std::string& cellName, const ESM::Position& position, bool adjustPlayerPos, bool changeEvent) { CellStore *cell = MWBase::Environment::get().getWorld()->getInterior(cellName); bool useFading = (mCurrentCell != nullptr); if (useFading) MWBase::Environment::get().getWindowManager()->fadeScreenOut(0.5); Loading::Listener* loadingListener = MWBase::Environment::get().getWindowManager()->getLoadingScreen(); std::string loadingInteriorText = "#{sLoadingMessage2}"; loadingListener->setLabel(loadingInteriorText); Loading::ScopedLoad load(loadingListener); if(mCurrentCell != nullptr && *mCurrentCell == *cell) { MWBase::World *world = MWBase::Environment::get().getWorld(); world->moveObject(world->getPlayerPtr(), position.pos[0], position.pos[1], position.pos[2]); float x = position.rot[0]; float y = position.rot[1]; float z = position.rot[2]; world->rotateObject(world->getPlayerPtr(), x, y, z); if (adjustPlayerPos) world->getPlayerPtr().getClass().adjustPosition(world->getPlayerPtr(), true); MWBase::Environment::get().getWindowManager()->fadeScreenIn(0.5); return; } Log(Debug::Info) << "Changing to interior"; // unload CellStoreCollection::iterator active = mActiveCells.begin(); while (active!=mActiveCells.end()) unloadCell (active++); loadingListener->setProgressRange(cell->count()); // Load cell. mPagedRefs.clear(); loadCell (cell, loadingListener, changeEvent); /* Start of tes3mp addition Send an ID_PLAYER_CELL_STATE packet with all cell states stored in LocalPlayer and then clear them, but only if the player is logged in on the server */ if (mwmp::Main::get().getLocalPlayer()->isLoggedIn()) { mwmp::Main::get().getLocalPlayer()->sendCellStates(); mwmp::Main::get().getLocalPlayer()->clearCellStates(); } /* End of tes3mp addition */ changePlayerCell(cell, position, adjustPlayerPos); // adjust fog mRendering.configureFog(mCurrentCell->getCell()); // Sky system MWBase::Environment::get().getWorld()->adjustSky(); if (changeEvent) mCellChanged = true; if (useFading) MWBase::Environment::get().getWindowManager()->fadeScreenIn(0.5); MWBase::Environment::get().getWindowManager()->changeCell(mCurrentCell); mNavigator.wait(*loadingListener, DetourNavigator::WaitConditionType::requiredTilesPresent); } void Scene::changeToExteriorCell (const ESM::Position& position, bool adjustPlayerPos, bool changeEvent) { int x = 0; int y = 0; MWBase::Environment::get().getWorld()->positionToIndex (position.pos[0], position.pos[1], x, y); if (changeEvent) MWBase::Environment::get().getWindowManager()->fadeScreenOut(0.5); changeCellGrid(position.asVec3(), x, y, changeEvent); CellStore* current = MWBase::Environment::get().getWorld()->getExterior(x, y); changePlayerCell(current, position, adjustPlayerPos); if (changeEvent) MWBase::Environment::get().getWindowManager()->fadeScreenIn(0.5); } CellStore* Scene::getCurrentCell () { return mCurrentCell; } void Scene::markCellAsUnchanged() { mCellChanged = false; } void Scene::insertCell (CellStore &cell, Loading::Listener* loadingListener, bool test) { InsertVisitor insertVisitor (cell, *loadingListener, test); cell.forEach (insertVisitor); insertVisitor.insert([&] (const MWWorld::Ptr& ptr) { addObject(ptr, *mPhysics, mRendering, mPagedRefs); }); insertVisitor.insert([&] (const MWWorld::Ptr& ptr) { addObject(ptr, *mPhysics, mNavigator); }); // do adjustPosition (snapping actors to ground) after objects are loaded, so we don't depend on the loading order PositionVisitor posVisitor; cell.forEach (posVisitor); } void Scene::addObjectToScene (const Ptr& ptr) { try { addObject(ptr, *mPhysics, mRendering, mPagedRefs); addObject(ptr, *mPhysics, mNavigator); MWBase::Environment::get().getWorld()->scaleObject(ptr, ptr.getCellRef().getScale()); const auto navigator = MWBase::Environment::get().getWorld()->getNavigator(); const auto player = MWBase::Environment::get().getWorld()->getPlayerPtr(); navigator->update(player.getRefData().getPosition().asVec3()); } catch (std::exception& e) { Log(Debug::Error) << "failed to render '" << ptr.getCellRef().getRefId() << "': " << e.what(); } } void Scene::removeObjectFromScene (const Ptr& ptr) { MWBase::Environment::get().getMechanicsManager()->remove (ptr); MWBase::Environment::get().getSoundManager()->stopSound3D (ptr); const auto navigator = MWBase::Environment::get().getWorld()->getNavigator(); if (const auto object = mPhysics->getObject(ptr)) { navigator->removeObject(DetourNavigator::ObjectId(object)); const auto player = MWBase::Environment::get().getWorld()->getPlayerPtr(); navigator->update(player.getRefData().getPosition().asVec3()); } else if (mPhysics->getActor(ptr)) { navigator->removeAgent(MWBase::Environment::get().getWorld()->getPathfindingHalfExtents(ptr)); } mPhysics->remove(ptr); mRendering.removeObject (ptr); if (ptr.getClass().isActor()) mRendering.removeWaterRippleEmitter(ptr); ptr.getRefData().setBaseNode(nullptr); } bool Scene::isCellActive(const CellStore &cell) { CellStoreCollection::iterator active = mActiveCells.begin(); while (active != mActiveCells.end()) { if (**active == cell) { return true; } ++active; } return false; } Ptr Scene::searchPtrViaActorId (int actorId) { for (CellStoreCollection::const_iterator iter (mActiveCells.begin()); iter!=mActiveCells.end(); ++iter) if (Ptr ptr = (*iter)->searchViaActorId (actorId)) return ptr; return Ptr(); } class PreloadMeshItem : public SceneUtil::WorkItem { public: PreloadMeshItem(const std::string& mesh, Resource::SceneManager* sceneManager) : mMesh(mesh), mSceneManager(sceneManager) { } void doWork() override { try { mSceneManager->getTemplate(mMesh); } catch (std::exception&) { } } private: std::string mMesh; Resource::SceneManager* mSceneManager; }; void Scene::preload(const std::string &mesh, bool useAnim) { std::string mesh_ = mesh; if (useAnim) mesh_ = Misc::ResourceHelpers::correctActorModelPath(mesh_, mRendering.getResourceSystem()->getVFS()); if (!mRendering.getResourceSystem()->getSceneManager()->checkLoaded(mesh_, mRendering.getReferenceTime())) mRendering.getWorkQueue()->addWorkItem(new PreloadMeshItem(mesh_, mRendering.getResourceSystem()->getSceneManager())); } void Scene::preloadCells(float dt) { if (dt<=1e-06) return; std::vector exteriorPositions; const MWWorld::ConstPtr player = MWBase::Environment::get().getWorld()->getPlayerPtr(); osg::Vec3f playerPos = player.getRefData().getPosition().asVec3(); osg::Vec3f moved = playerPos - mLastPlayerPos; osg::Vec3f predictedPos = playerPos + moved / dt * mPredictionTime; if (mCurrentCell->isExterior()) exteriorPositions.emplace_back(predictedPos, gridCenterToBounds(getNewGridCenter(predictedPos, &mCurrentGridCenter))); mLastPlayerPos = playerPos; if (mPreloadEnabled) { if (mPreloadDoors) preloadTeleportDoorDestinations(playerPos, predictedPos, exteriorPositions); if (mPreloadExteriorGrid) preloadExteriorGrid(playerPos, predictedPos); if (mPreloadFastTravel) preloadFastTravelDestinations(playerPos, predictedPos, exteriorPositions); } mPreloader->setTerrainPreloadPositions(exteriorPositions); } void Scene::preloadTeleportDoorDestinations(const osg::Vec3f& playerPos, const osg::Vec3f& predictedPos, std::vector& exteriorPositions) { std::vector teleportDoors; for (const MWWorld::CellStore* cellStore : mActiveCells) { typedef MWWorld::CellRefList::List DoorList; const DoorList &doors = cellStore->getReadOnlyDoors().mList; for (auto& door : doors) { if (!door.mRef.getTeleport()) { continue; } teleportDoors.emplace_back(&door, cellStore); } } for (const MWWorld::ConstPtr& door : teleportDoors) { float sqrDistToPlayer = (playerPos - door.getRefData().getPosition().asVec3()).length2(); sqrDistToPlayer = std::min(sqrDistToPlayer, (predictedPos - door.getRefData().getPosition().asVec3()).length2()); if (sqrDistToPlayer < mPreloadDistance*mPreloadDistance) { try { if (!door.getCellRef().getDestCell().empty()) preloadCell(MWBase::Environment::get().getWorld()->getInterior(door.getCellRef().getDestCell())); else { osg::Vec3f pos = door.getCellRef().getDoorDest().asVec3(); int x,y; MWBase::Environment::get().getWorld()->positionToIndex (pos.x(), pos.y(), x, y); preloadCell(MWBase::Environment::get().getWorld()->getExterior(x,y), true); exteriorPositions.emplace_back(pos, gridCenterToBounds(getNewGridCenter(pos))); } } catch (std::exception&) { // ignore error for now, would spam the log too much } } } } void Scene::preloadExteriorGrid(const osg::Vec3f& playerPos, const osg::Vec3f& predictedPos) { if (!MWBase::Environment::get().getWorld()->isCellExterior()) return; int halfGridSizePlusOne = mHalfGridSize + 1; int cellX,cellY; cellX = mCurrentGridCenter.x(); cellY = mCurrentGridCenter.y(); float centerX, centerY; MWBase::Environment::get().getWorld()->indexToPosition(cellX, cellY, centerX, centerY, true); for (int dx = -halfGridSizePlusOne; dx <= halfGridSizePlusOne; ++dx) { for (int dy = -halfGridSizePlusOne; dy <= halfGridSizePlusOne; ++dy) { if (dy != halfGridSizePlusOne && dy != -halfGridSizePlusOne && dx != halfGridSizePlusOne && dx != -halfGridSizePlusOne) continue; // only care about the outer (not yet loaded) part of the grid float thisCellCenterX, thisCellCenterY; MWBase::Environment::get().getWorld()->indexToPosition(cellX+dx, cellY+dy, thisCellCenterX, thisCellCenterY, true); float dist = std::max(std::abs(thisCellCenterX - playerPos.x()), std::abs(thisCellCenterY - playerPos.y())); dist = std::min(dist,std::max(std::abs(thisCellCenterX - predictedPos.x()), std::abs(thisCellCenterY - predictedPos.y()))); float loadDist = Constants::CellSizeInUnits / 2 + Constants::CellSizeInUnits - mCellLoadingThreshold + mPreloadDistance; if (dist < loadDist) preloadCell(MWBase::Environment::get().getWorld()->getExterior(cellX+dx, cellY+dy)); } } } void Scene::preloadCell(CellStore *cell, bool preloadSurrounding) { if (preloadSurrounding && cell->isExterior()) { int x = cell->getCell()->getGridX(); int y = cell->getCell()->getGridY(); unsigned int numpreloaded = 0; for (int dx = -mHalfGridSize; dx <= mHalfGridSize; ++dx) { for (int dy = -mHalfGridSize; dy <= mHalfGridSize; ++dy) { mPreloader->preload(MWBase::Environment::get().getWorld()->getExterior(x+dx, y+dy), mRendering.getReferenceTime()); if (++numpreloaded >= mPreloader->getMaxCacheSize()) break; } } } else mPreloader->preload(cell, mRendering.getReferenceTime()); } void Scene::preloadTerrain(const osg::Vec3f &pos, bool sync) { std::vector vec; vec.emplace_back(pos, gridCenterToBounds(getNewGridCenter(pos))); if (sync && mRendering.pagingUnlockCache()) mPreloader->abortTerrainPreloadExcept(nullptr); else mPreloader->abortTerrainPreloadExcept(&vec[0]); mPreloader->setTerrainPreloadPositions(vec); if (!sync) return; Loading::Listener* loadingListener = MWBase::Environment::get().getWindowManager()->getLoadingScreen(); Loading::ScopedLoad load(loadingListener); int progress = 0, initialProgress = -1, progressRange = 0; while (!mPreloader->syncTerrainLoad(vec, progress, progressRange, mRendering.getReferenceTime())) { if (initialProgress == -1) { loadingListener->setLabel("#{sLoadingMessage4}"); initialProgress = progress; } if (progress) { loadingListener->setProgressRange(std::max(0, progressRange-initialProgress)); loadingListener->setProgress(progress-initialProgress); } else loadingListener->setProgress(0); std::this_thread::sleep_for(std::chrono::milliseconds(5)); } } void Scene::reloadTerrain() { mPreloader->setTerrainPreloadPositions(std::vector()); } struct ListFastTravelDestinationsVisitor { ListFastTravelDestinationsVisitor(float preloadDist, const osg::Vec3f& playerPos) : mPreloadDist(preloadDist) , mPlayerPos(playerPos) { } bool operator()(const MWWorld::Ptr& ptr) { if ((ptr.getRefData().getPosition().asVec3() - mPlayerPos).length2() > mPreloadDist * mPreloadDist) return true; if (ptr.getClass().isNpc()) { const std::vector& transport = ptr.get()->mBase->mTransport.mList; mList.insert(mList.begin(), transport.begin(), transport.end()); } else { const std::vector& transport = ptr.get()->mBase->mTransport.mList; mList.insert(mList.begin(), transport.begin(), transport.end()); } return true; } float mPreloadDist; osg::Vec3f mPlayerPos; std::vector mList; }; void Scene::preloadFastTravelDestinations(const osg::Vec3f& playerPos, const osg::Vec3f& /*predictedPos*/, std::vector& exteriorPositions) // ignore predictedPos here since opening dialogue with travel service takes extra time { const MWWorld::ConstPtr player = MWBase::Environment::get().getWorld()->getPlayerPtr(); ListFastTravelDestinationsVisitor listVisitor(mPreloadDistance, player.getRefData().getPosition().asVec3()); for (MWWorld::CellStore* cellStore : mActiveCells) { cellStore->forEachType(listVisitor); cellStore->forEachType(listVisitor); } for (ESM::Transport::Dest& dest : listVisitor.mList) { if (!dest.mCellName.empty()) preloadCell(MWBase::Environment::get().getWorld()->getInterior(dest.mCellName)); else { osg::Vec3f pos = dest.mPos.asVec3(); int x,y; MWBase::Environment::get().getWorld()->positionToIndex( pos.x(), pos.y(), x, y); preloadCell(MWBase::Environment::get().getWorld()->getExterior(x,y), true); exteriorPositions.emplace_back(pos, gridCenterToBounds(getNewGridCenter(pos))); } } } } ================================================ FILE: apps/openmw/mwworld/scene.hpp ================================================ #ifndef GAME_MWWORLD_SCENE_H #define GAME_MWWORLD_SCENE_H #include #include #include "ptr.hpp" #include "globals.hpp" #include #include #include #include namespace osg { class Vec3f; } namespace ESM { struct Position; } namespace Files { class Collections; } namespace Loading { class Listener; } namespace DetourNavigator { struct Navigator; } namespace MWRender { class SkyManager; class RenderingManager; } namespace MWPhysics { class PhysicsSystem; } namespace MWWorld { class Player; class CellStore; class CellPreloader; enum class RotationOrder { direct, inverse }; class Scene { public: typedef std::set CellStoreCollection; private: CellStore* mCurrentCell; // the cell the player is in CellStoreCollection mActiveCells; bool mCellChanged; MWPhysics::PhysicsSystem *mPhysics; MWRender::RenderingManager& mRendering; DetourNavigator::Navigator& mNavigator; std::unique_ptr mPreloader; float mCellLoadingThreshold; float mPreloadDistance; bool mPreloadEnabled; bool mPreloadExteriorGrid; bool mPreloadDoors; bool mPreloadFastTravel; float mPredictionTime; static const int mHalfGridSize = Constants::CellGridRadius; osg::Vec3f mLastPlayerPos; std::set mPagedRefs; void insertCell (CellStore &cell, Loading::Listener* loadingListener, bool test = false); osg::Vec2i mCurrentGridCenter; // Load and unload cells as necessary to create a cell grid with "X" and "Y" in the center void changeCellGrid (const osg::Vec3f &pos, int playerCellX, int playerCellY, bool changeEvent = true); typedef std::pair PositionCellGrid; void preloadCells(float dt); void preloadTeleportDoorDestinations(const osg::Vec3f& playerPos, const osg::Vec3f& predictedPos, std::vector& exteriorPositions); void preloadExteriorGrid(const osg::Vec3f& playerPos, const osg::Vec3f& predictedPos); void preloadFastTravelDestinations(const osg::Vec3f& playerPos, const osg::Vec3f& predictedPos, std::vector& exteriorPositions); osg::Vec4i gridCenterToBounds(const osg::Vec2i ¢erCell) const; osg::Vec2i getNewGridCenter(const osg::Vec3f &pos, const osg::Vec2i *currentGridCenter = nullptr) const; public: Scene (MWRender::RenderingManager& rendering, MWPhysics::PhysicsSystem *physics, DetourNavigator::Navigator& navigator); ~Scene(); void preloadCell(MWWorld::CellStore* cell, bool preloadSurrounding=false); void preloadTerrain(const osg::Vec3f& pos, bool sync=false); void reloadTerrain(); void unloadCell (CellStoreCollection::iterator iter, bool test = false); void loadCell (CellStore *cell, Loading::Listener* loadingListener, bool respawn, bool test = false); void playerMoved (const osg::Vec3f& pos); void changePlayerCell (CellStore* newCell, const ESM::Position& position, bool adjustPlayerPos); CellStore *getCurrentCell(); const CellStoreCollection& getActiveCells () const; bool hasCellChanged() const; ///< Has the set of active cells changed, since the last frame? void changeToInteriorCell (const std::string& cellName, const ESM::Position& position, bool adjustPlayerPos, bool changeEvent=true); ///< Move to interior cell. /// @param changeEvent Set cellChanged flag? void changeToExteriorCell (const ESM::Position& position, bool adjustPlayerPos, bool changeEvent=true); ///< Move to exterior cell. /// @param changeEvent Set cellChanged flag? void clear(); ///< Change into a void void markCellAsUnchanged(); void update (float duration, bool paused); void addObjectToScene (const Ptr& ptr); ///< Add an object that already exists in the world model to the scene. void removeObjectFromScene (const Ptr& ptr); ///< Remove an object from the scene, but not from the world model. void removeFromPagedRefs(const Ptr &ptr); void updateObjectRotation(const Ptr& ptr, RotationOrder order); void updateObjectScale(const Ptr& ptr); void updateObjectPosition(const Ptr &ptr, const osg::Vec3f &pos, bool movePhysics); bool isCellActive(const CellStore &cell); Ptr searchPtrViaActorId (int actorId); void preload(const std::string& mesh, bool useAnim=false); void testExteriorCells(); void testInteriorCells(); }; } #endif ================================================ FILE: apps/openmw/mwworld/store.cpp ================================================ #include "store.hpp" #include #include #include #include #include #include #include namespace { struct Compare { bool operator()(const ESM::Land *x, const ESM::Land *y) { if (x->mX == y->mX) { return x->mY < y->mY; } return x->mX < y->mX; } bool operator()(const ESM::Land *x, const std::pair& y) { if (x->mX == y.first) { return x->mY < y.second; } return x->mX < y.first; } }; } namespace MWWorld { RecordId::RecordId(const std::string &id, bool isDeleted) : mId(id), mIsDeleted(isDeleted) {} template IndexedStore::IndexedStore() { } template typename IndexedStore::iterator IndexedStore::begin() const { return mStatic.begin(); } template typename IndexedStore::iterator IndexedStore::end() const { return mStatic.end(); } template void IndexedStore::load(ESM::ESMReader &esm) { T record; bool isDeleted = false; record.load(esm, isDeleted); mStatic.insert_or_assign(record.mIndex, record); } template int IndexedStore::getSize() const { return mStatic.size(); } template void IndexedStore::setUp() { } template const T *IndexedStore::search(int index) const { typename Static::const_iterator it = mStatic.find(index); if (it != mStatic.end()) return &(it->second); return nullptr; } template const T *IndexedStore::find(int index) const { const T *ptr = search(index); if (ptr == nullptr) { const std::string msg = T::getRecordType() + " with index " + std::to_string(index) + " not found"; throw std::runtime_error(msg); } return ptr; } // Need to instantiate these before they're used template class IndexedStore; template class IndexedStore; template Store::Store() { } template Store::Store(const Store& orig) : mStatic(orig.mStatic) { } template void Store::clearDynamic() { // remove the dynamic part of mShared assert(mShared.size() >= mStatic.size()); mShared.erase(mShared.begin() + mStatic.size(), mShared.end()); mDynamic.clear(); } template const T *Store::search(const std::string &id) const { std::string idLower = Misc::StringUtils::lowerCase(id); typename Dynamic::const_iterator dit = mDynamic.find(idLower); if (dit != mDynamic.end()) return &dit->second; typename std::map::const_iterator it = mStatic.find(idLower); if (it != mStatic.end()) return &(it->second); return nullptr; } template const T *Store::searchStatic(const std::string &id) const { std::string idLower = Misc::StringUtils::lowerCase(id); typename std::map::const_iterator it = mStatic.find(idLower); if (it != mStatic.end()) return &(it->second); return nullptr; } template bool Store::isDynamic(const std::string &id) const { typename Dynamic::const_iterator dit = mDynamic.find(id); return (dit != mDynamic.end()); } template const T *Store::searchRandom(const std::string &id) const { std::vector results; std::copy_if(mShared.begin(), mShared.end(), std::back_inserter(results), [&id](const T* item) { return Misc::StringUtils::ciCompareLen(id, item->mId, id.size()) == 0; }); if(!results.empty()) return results[Misc::Rng::rollDice(results.size())]; return nullptr; } template const T *Store::find(const std::string &id) const { const T *ptr = search(id); if (ptr == nullptr) { const std::string msg = T::getRecordType() + " '" + id + "' not found"; throw std::runtime_error(msg); } return ptr; } template RecordId Store::load(ESM::ESMReader &esm) { T record; bool isDeleted = false; record.load(esm, isDeleted); Misc::StringUtils::lowerCaseInPlace(record.mId); std::pair inserted = mStatic.insert_or_assign(record.mId, record); if (inserted.second) mShared.push_back(&inserted.first->second); return RecordId(record.mId, isDeleted); } template void Store::setUp() { } template typename Store::iterator Store::begin() const { return mShared.begin(); } template typename Store::iterator Store::end() const { return mShared.end(); } template size_t Store::getSize() const { return mShared.size(); } template int Store::getDynamicSize() const { return mDynamic.size(); } template void Store::listIdentifier(std::vector &list) const { list.reserve(list.size() + getSize()); typename std::vector::const_iterator it = mShared.begin(); for (; it != mShared.end(); ++it) { list.push_back((*it)->mId); } } template T *Store::insert(const T &item, bool overrideOnly) { std::string id = Misc::StringUtils::lowerCase(item.mId); if(overrideOnly) { auto it = mStatic.find(id); if(it == mStatic.end()) return nullptr; } std::pair result = mDynamic.insert_or_assign(id, item); T *ptr = &result.first->second; if (result.second) mShared.push_back(ptr); return ptr; } template T *Store::insertStatic(const T &item) { std::string id = Misc::StringUtils::lowerCase(item.mId); std::pair result = mStatic.insert_or_assign(id, item); T *ptr = &result.first->second; if (result.second) mShared.push_back(ptr); return ptr; } template bool Store::eraseStatic(const std::string &id) { std::string idLower = Misc::StringUtils::lowerCase(id); typename std::map::iterator it = mStatic.find(idLower); if (it != mStatic.end()) { // delete from the static part of mShared typename std::vector::iterator sharedIter = mShared.begin(); typename std::vector::iterator end = sharedIter + mStatic.size(); while (sharedIter != mShared.end() && sharedIter != end) { if((*sharedIter)->mId == idLower) { mShared.erase(sharedIter); break; } ++sharedIter; } mStatic.erase(it); } return true; } template bool Store::erase(const std::string &id) { std::string key = Misc::StringUtils::lowerCase(id); typename Dynamic::iterator it = mDynamic.find(key); if (it == mDynamic.end()) { return false; } mDynamic.erase(it); // have to reinit the whole shared part assert(mShared.size() >= mStatic.size()); mShared.erase(mShared.begin() + mStatic.size(), mShared.end()); for (it = mDynamic.begin(); it != mDynamic.end(); ++it) { mShared.push_back(&it->second); } return true; } template bool Store::erase(const T &item) { return erase(item.mId); } template void Store::write (ESM::ESMWriter& writer, Loading::Listener& progress) const { for (typename Dynamic::const_iterator iter (mDynamic.begin()); iter!=mDynamic.end(); ++iter) { writer.startRecord (T::sRecordId); iter->second.save (writer); writer.endRecord (T::sRecordId); } } template RecordId Store::read(ESM::ESMReader& reader, bool overrideOnly) { T record; bool isDeleted = false; record.load (reader, isDeleted); insert (record, overrideOnly); return RecordId(record.mId, isDeleted); } // LandTexture //========================================================================= Store::Store() { mStatic.emplace_back(); LandTextureList <exl = mStatic[0]; // More than enough to hold Morrowind.esm. Extra lists for plugins will we // added on-the-fly in a different method. ltexl.reserve(128); } const ESM::LandTexture *Store::search(size_t index, size_t plugin) const { assert(plugin < mStatic.size()); const LandTextureList <exl = mStatic[plugin]; if (index >= ltexl.size()) return nullptr; return <exl[index]; } const ESM::LandTexture *Store::find(size_t index, size_t plugin) const { const ESM::LandTexture *ptr = search(index, plugin); if (ptr == nullptr) { const std::string msg = "Land texture with index " + std::to_string(index) + " not found"; throw std::runtime_error(msg); } return ptr; } size_t Store::getSize() const { return mStatic.size(); } size_t Store::getSize(size_t plugin) const { assert(plugin < mStatic.size()); return mStatic[plugin].size(); } RecordId Store::load(ESM::ESMReader &esm, size_t plugin) { ESM::LandTexture lt; bool isDeleted = false; lt.load(esm, isDeleted); assert(plugin < mStatic.size()); // Replace texture for records with given ID and index from all plugins. for (unsigned int i=0; i(search(lt.mIndex, i)); if (tex) { const std::string texId = Misc::StringUtils::lowerCase(tex->mId); const std::string ltId = Misc::StringUtils::lowerCase(lt.mId); if (texId == ltId) { tex->mTexture = lt.mTexture; } } } LandTextureList <exl = mStatic[plugin]; if(lt.mIndex + 1 > (int)ltexl.size()) ltexl.resize(lt.mIndex+1); // Store it ltexl[lt.mIndex] = lt; return RecordId(lt.mId, isDeleted); } RecordId Store::load(ESM::ESMReader &esm) { return load(esm, esm.getIndex()); } Store::iterator Store::begin(size_t plugin) const { assert(plugin < mStatic.size()); return mStatic[plugin].begin(); } Store::iterator Store::end(size_t plugin) const { assert(plugin < mStatic.size()); return mStatic[plugin].end(); } void Store::resize(size_t num) { if (mStatic.size() < num) mStatic.resize(num); } // Land //========================================================================= Store::~Store() { for (const ESM::Land* staticLand : mStatic) { delete staticLand; } } size_t Store::getSize() const { return mStatic.size(); } Store::iterator Store::begin() const { return iterator(mStatic.begin()); } Store::iterator Store::end() const { return iterator(mStatic.end()); } const ESM::Land *Store::search(int x, int y) const { std::pair comp(x,y); std::vector::const_iterator it = std::lower_bound(mStatic.begin(), mStatic.end(), comp, Compare()); if (it != mStatic.end() && (*it)->mX == x && (*it)->mY == y) { return *it; } return nullptr; } const ESM::Land *Store::find(int x, int y) const { const ESM::Land *ptr = search(x, y); if (ptr == nullptr) { const std::string msg = "Land at (" + std::to_string(x) + ", " + std::to_string(y) + ") not found"; throw std::runtime_error(msg); } return ptr; } RecordId Store::load(ESM::ESMReader &esm) { ESM::Land *ptr = new ESM::Land(); bool isDeleted = false; ptr->load(esm, isDeleted); // Same area defined in multiple plugins? -> last plugin wins // Can't use search() because we aren't sorted yet - is there any other way to speed this up? for (std::vector::iterator it = mStatic.begin(); it != mStatic.end(); ++it) { if ((*it)->mX == ptr->mX && (*it)->mY == ptr->mY) { delete *it; mStatic.erase(it); break; } } mStatic.push_back(ptr); return RecordId("", isDeleted); } void Store::setUp() { // The land is static for given game session, there is no need to refresh it every load. if (mBuilt) return; std::sort(mStatic.begin(), mStatic.end(), Compare()); mBuilt = true; } // Cell //========================================================================= const ESM::Cell *Store::search(const ESM::Cell &cell) const { if (cell.isExterior()) { return search(cell.getGridX(), cell.getGridY()); } return search(cell.mName); } void Store::handleMovedCellRefs(ESM::ESMReader& esm, ESM::Cell* cell) { //Handling MovedCellRefs, there is no way to do it inside loadcell while (esm.isNextSub("MVRF")) { ESM::CellRef ref; ESM::MovedCellRef cMRef; cell->getNextMVRF(esm, cMRef); ESM::Cell *cellAlt = const_cast(searchOrCreate(cMRef.mTarget[0], cMRef.mTarget[1])); // Get regular moved reference data. Adapted from CellStore::loadRefs. Maybe we can optimize the following // implementation when the oher implementation works as well. bool deleted = false; cell->getNextRef(esm, ref, deleted); // Add data required to make reference appear in the correct cell. // We should not need to test for duplicates, as this part of the code is pre-cell merge. cell->mMovedRefs.push_back(cMRef); // But there may be duplicates here! ESM::CellRefTracker::iterator iter = std::find_if(cellAlt->mLeasedRefs.begin(), cellAlt->mLeasedRefs.end(), ESM::CellRefTrackerPredicate(ref.mRefNum)); if (iter == cellAlt->mLeasedRefs.end()) cellAlt->mLeasedRefs.emplace_back(std::move(ref), deleted); else *iter = std::make_pair(std::move(ref), deleted); } } const ESM::Cell *Store::search(const std::string &id) const { ESM::Cell cell; cell.mName = Misc::StringUtils::lowerCase(id); std::map::const_iterator it = mInt.find(cell.mName); if (it != mInt.end()) { return &(it->second); } DynamicInt::const_iterator dit = mDynamicInt.find(cell.mName); if (dit != mDynamicInt.end()) { return &dit->second; } return nullptr; } const ESM::Cell *Store::search(int x, int y) const { ESM::Cell cell; cell.mData.mX = x, cell.mData.mY = y; std::pair key(x, y); DynamicExt::const_iterator it = mExt.find(key); if (it != mExt.end()) { return &(it->second); } DynamicExt::const_iterator dit = mDynamicExt.find(key); if (dit != mDynamicExt.end()) { return &dit->second; } return nullptr; } const ESM::Cell *Store::searchStatic(int x, int y) const { ESM::Cell cell; cell.mData.mX = x, cell.mData.mY = y; std::pair key(x, y); DynamicExt::const_iterator it = mExt.find(key); if (it != mExt.end()) { return &(it->second); } return nullptr; } const ESM::Cell *Store::searchOrCreate(int x, int y) { std::pair key(x, y); DynamicExt::const_iterator it = mExt.find(key); if (it != mExt.end()) { return &(it->second); } DynamicExt::const_iterator dit = mDynamicExt.find(key); if (dit != mDynamicExt.end()) { return &dit->second; } ESM::Cell newCell; newCell.mData.mX = x; newCell.mData.mY = y; newCell.mData.mFlags = ESM::Cell::HasWater; newCell.mAmbi.mAmbient = 0; newCell.mAmbi.mSunlight = 0; newCell.mAmbi.mFog = 0; newCell.mAmbi.mFogDensity = 0; return &mExt.insert(std::make_pair(key, newCell)).first->second; } const ESM::Cell *Store::find(const std::string &id) const { const ESM::Cell *ptr = search(id); if (ptr == nullptr) { const std::string msg = "Cell '" + id + "' not found"; throw std::runtime_error(msg); } return ptr; } const ESM::Cell *Store::find(int x, int y) const { const ESM::Cell *ptr = search(x, y); if (ptr == nullptr) { const std::string msg = "Exterior at (" + std::to_string(x) + ", " + std::to_string(y) + ") not found"; throw std::runtime_error(msg); } return ptr; } void Store::clearDynamic() { setUp(); } void Store::setUp() { mSharedInt.clear(); mSharedInt.reserve(mInt.size()); for (auto & [_, cell] : mInt) mSharedInt.push_back(&cell); mSharedExt.clear(); mSharedExt.reserve(mExt.size()); for (auto & [_, cell] : mExt) mSharedExt.push_back(&cell); } RecordId Store::load(ESM::ESMReader &esm) { // Don't automatically assume that a new cell must be spawned. Multiple plugins write to the same cell, // and we merge all this data into one Cell object. However, we can't simply search for the cell id, // as many exterior cells do not have a name. Instead, we need to search by (x,y) coordinates - and they // are not available until both cells have been loaded at least partially! // All cells have a name record, even nameless exterior cells. ESM::Cell cell; bool isDeleted = false; // Load the (x,y) coordinates of the cell, if it is an exterior cell, // so we can find the cell we need to merge with cell.loadNameAndData(esm, isDeleted); std::string idLower = Misc::StringUtils::lowerCase(cell.mName); if(cell.mData.mFlags & ESM::Cell::Interior) { // Store interior cell by name, try to merge with existing parent data. ESM::Cell *oldcell = const_cast(search(idLower)); if (oldcell) { // merge new cell into old cell // push the new references on the list of references to manage (saveContext = true) oldcell->mData = cell.mData; oldcell->mName = cell.mName; // merge name just to be sure (ID will be the same, but case could have been changed) oldcell->loadCell(esm, true); } else { // spawn a new cell cell.loadCell(esm, true); mInt[idLower] = cell; } } else { // Store exterior cells by grid position, try to merge with existing parent data. ESM::Cell *oldcell = const_cast(search(cell.getGridX(), cell.getGridY())); if (oldcell) { // merge new cell into old cell oldcell->mData = cell.mData; oldcell->mName = cell.mName; oldcell->loadCell(esm, false); // handle moved ref (MVRF) subrecords handleMovedCellRefs (esm, &cell); // push the new references on the list of references to manage oldcell->postLoad(esm); // merge lists of leased references, use newer data in case of conflict for (ESM::MovedCellRefTracker::const_iterator it = cell.mMovedRefs.begin(); it != cell.mMovedRefs.end(); ++it) { // remove reference from current leased ref tracker and add it to new cell ESM::MovedCellRefTracker::iterator itold = std::find(oldcell->mMovedRefs.begin(), oldcell->mMovedRefs.end(), it->mRefNum); if (itold != oldcell->mMovedRefs.end()) { if (it->mTarget[0] != itold->mTarget[0] || it->mTarget[1] != itold->mTarget[1]) { ESM::Cell *wipecell = const_cast(search(itold->mTarget[0], itold->mTarget[1])); ESM::CellRefTracker::iterator it_lease = std::find_if(wipecell->mLeasedRefs.begin(), wipecell->mLeasedRefs.end(), ESM::CellRefTrackerPredicate(it->mRefNum)); if (it_lease != wipecell->mLeasedRefs.end()) wipecell->mLeasedRefs.erase(it_lease); else Log(Debug::Error) << "Error: can't find " << it->mRefNum.mIndex << " " << it->mRefNum.mContentFile << " in leasedRefs"; } *itold = *it; } else oldcell->mMovedRefs.push_back(*it); } // We don't need to merge mLeasedRefs of cell / oldcell. This list is filled when another cell moves a // reference to this cell, so the list for the new cell should be empty. The list for oldcell, // however, could have leased refs in it and so should be kept. } else { // spawn a new cell cell.loadCell(esm, false); // handle moved ref (MVRF) subrecords handleMovedCellRefs (esm, &cell); // push the new references on the list of references to manage cell.postLoad(esm); mExt[std::make_pair(cell.mData.mX, cell.mData.mY)] = cell; } } return RecordId(cell.mName, isDeleted); } Store::iterator Store::intBegin() const { return iterator(mSharedInt.begin()); } Store::iterator Store::intEnd() const { return iterator(mSharedInt.end()); } Store::iterator Store::extBegin() const { return iterator(mSharedExt.begin()); } Store::iterator Store::extEnd() const { return iterator(mSharedExt.end()); } const ESM::Cell *Store::searchExtByName(const std::string &id) const { const ESM::Cell *cell = nullptr; for (const ESM::Cell *sharedCell : mSharedExt) { if (Misc::StringUtils::ciEqual(sharedCell->mName, id)) { if (cell == nullptr || (sharedCell->mData.mX > cell->mData.mX) || (sharedCell->mData.mX == cell->mData.mX && sharedCell->mData.mY > cell->mData.mY)) { cell = sharedCell; } } } return cell; } const ESM::Cell *Store::searchExtByRegion(const std::string &id) const { const ESM::Cell *cell = nullptr; for (const ESM::Cell *sharedCell : mSharedExt) { if (Misc::StringUtils::ciEqual(sharedCell->mRegion, id)) { if (cell == nullptr || (sharedCell->mData.mX > cell->mData.mX) || (sharedCell->mData.mX == cell->mData.mX && sharedCell->mData.mY > cell->mData.mY)) { cell = sharedCell; } } } return cell; } size_t Store::getSize() const { return mSharedInt.size() + mSharedExt.size(); } size_t Store::getExtSize() const { return mSharedExt.size(); } size_t Store::getIntSize() const { return mSharedInt.size(); } void Store::listIdentifier(std::vector &list) const { list.reserve(list.size() + mSharedInt.size()); for (const ESM::Cell *sharedCell : mSharedInt) { list.push_back(sharedCell->mName); } } /* Start of tes3mp addition Make it possible to override a Cell record similarly to how other types of records can be overridden */ ESM::Cell *Store::override(const ESM::Cell &cell) { if (search(cell) != 0) { for (auto it = mSharedInt.begin(); it != mSharedInt.end(); ++it) { if (Misc::StringUtils::ciEqual((*it)->mName, cell.mName)) { (*it) = &const_cast(cell); break; } } for (auto it = mInt.begin(); it != mInt.end(); ++it) { if (Misc::StringUtils::ciEqual((*it).second.mName, cell.mName)) { (*it).second = cell; return &(*it).second; } } } else { return insert(cell); } } /* End of tes3mp addition */ ESM::Cell *Store::insert(const ESM::Cell &cell) { if (search(cell) != nullptr) { const std::string cellType = (cell.isExterior()) ? "exterior" : "interior"; throw std::runtime_error("Failed to create " + cellType + " cell"); } ESM::Cell *ptr; if (cell.isExterior()) { std::pair key(cell.getGridX(), cell.getGridY()); // duplicate insertions are avoided by search(ESM::Cell &) std::pair result = mDynamicExt.insert(std::make_pair(key, cell)); ptr = &result.first->second; mSharedExt.push_back(ptr); } else { std::string key = Misc::StringUtils::lowerCase(cell.mName); // duplicate insertions are avoided by search(ESM::Cell &) std::pair result = mDynamicInt.insert(std::make_pair(key, cell)); ptr = &result.first->second; mSharedInt.push_back(ptr); } return ptr; } bool Store::erase(const ESM::Cell &cell) { if (cell.isExterior()) { return erase(cell.getGridX(), cell.getGridY()); } return erase(cell.mName); } bool Store::erase(const std::string &id) { std::string key = Misc::StringUtils::lowerCase(id); DynamicInt::iterator it = mDynamicInt.find(key); if (it == mDynamicInt.end()) { return false; } mDynamicInt.erase(it); mSharedInt.erase( mSharedInt.begin() + mSharedInt.size(), mSharedInt.end() ); for (it = mDynamicInt.begin(); it != mDynamicInt.end(); ++it) { mSharedInt.push_back(&it->second); } return true; } bool Store::erase(int x, int y) { std::pair key(x, y); DynamicExt::iterator it = mDynamicExt.find(key); if (it == mDynamicExt.end()) { return false; } mDynamicExt.erase(it); mSharedExt.erase( mSharedExt.begin() + mSharedExt.size(), mSharedExt.end() ); for (it = mDynamicExt.begin(); it != mDynamicExt.end(); ++it) { mSharedExt.push_back(&it->second); } return true; } // Pathgrid //========================================================================= Store::Store() : mCells(nullptr) { } void Store::setCells(Store& cells) { mCells = &cells; } RecordId Store::load(ESM::ESMReader &esm) { ESM::Pathgrid pathgrid; bool isDeleted = false; pathgrid.load(esm, isDeleted); // Unfortunately the Pathgrid record model does not specify whether the pathgrid belongs to an interior or exterior cell. // For interior cells, mCell is the cell name, but for exterior cells it is either the cell name or if that doesn't exist, the cell's region name. // mX and mY will be (0,0) for interior cells, but there is also an exterior cell with the coordinates of (0,0), so that doesn't help. // Check whether mCell is an interior cell. This isn't perfect, will break if a Region with the same name as an interior cell is created. // A proper fix should be made for future versions of the file format. bool interior = mCells->search(pathgrid.mCell) != nullptr; // Try to overwrite existing record if (interior) { std::pair ret = mInt.insert(std::make_pair(pathgrid.mCell, pathgrid)); if (!ret.second) ret.first->second = pathgrid; } else { std::pair ret = mExt.insert(std::make_pair(std::make_pair(pathgrid.mData.mX, pathgrid.mData.mY), pathgrid)); if (!ret.second) ret.first->second = pathgrid; } return RecordId("", isDeleted); } /* Start of tes3mp addition Make it possible to override a Pathgrid record similarly to how other types of records can be overridden */ ESM::Pathgrid* Store::override(const ESM::Pathgrid& pathgrid) { bool interior = mCells->search(pathgrid.mCell) != nullptr; // Try to overwrite existing record if (interior) { std::pair ret = mInt.insert(std::make_pair(pathgrid.mCell, pathgrid)); if (!ret.second) ret.first->second = pathgrid; return &ret.first->second; } else { std::pair ret = mExt.insert(std::make_pair(std::make_pair(pathgrid.mData.mX, pathgrid.mData.mY), pathgrid)); if (!ret.second) ret.first->second = pathgrid; return &ret.first->second; } } /* End of tes3mp addition */ size_t Store::getSize() const { return mInt.size() + mExt.size(); } void Store::setUp() { } const ESM::Pathgrid *Store::search(int x, int y) const { Exterior::const_iterator it = mExt.find(std::make_pair(x,y)); if (it != mExt.end()) return &(it->second); return nullptr; } const ESM::Pathgrid *Store::search(const std::string& name) const { Interior::const_iterator it = mInt.find(name); if (it != mInt.end()) return &(it->second); return nullptr; } const ESM::Pathgrid *Store::find(int x, int y) const { const ESM::Pathgrid* pathgrid = search(x,y); if (!pathgrid) { const std::string msg = "Pathgrid in cell '" + std::to_string(x) + " " + std::to_string(y) + "' not found"; throw std::runtime_error(msg); } return pathgrid; } const ESM::Pathgrid* Store::find(const std::string& name) const { const ESM::Pathgrid* pathgrid = search(name); if (!pathgrid) { const std::string msg = "Pathgrid in cell '" + name + "' not found"; throw std::runtime_error(msg); } return pathgrid; } const ESM::Pathgrid *Store::search(const ESM::Cell &cell) const { if (!(cell.mData.mFlags & ESM::Cell::Interior)) return search(cell.mData.mX, cell.mData.mY); else return search(cell.mName); } const ESM::Pathgrid *Store::find(const ESM::Cell &cell) const { if (!(cell.mData.mFlags & ESM::Cell::Interior)) return find(cell.mData.mX, cell.mData.mY); else return find(cell.mName); } // Skill //========================================================================= Store::Store() { } // Magic effect //========================================================================= Store::Store() { } // Attribute //========================================================================= Store::Store() { mStatic.reserve(ESM::Attribute::Length); } const ESM::Attribute *Store::search(size_t index) const { if (index >= mStatic.size()) { return nullptr; } return &mStatic[index]; } const ESM::Attribute *Store::find(size_t index) const { const ESM::Attribute *ptr = search(index); if (ptr == nullptr) { const std::string msg = "Attribute with index " + std::to_string(index) + " not found"; throw std::runtime_error(msg); } return ptr; } void Store::setUp() { for (int i = 0; i < ESM::Attribute::Length; ++i) { ESM::Attribute newAttribute; newAttribute.mId = ESM::Attribute::sAttributeIds[i]; newAttribute.mName = ESM::Attribute::sGmstAttributeIds[i]; newAttribute.mDescription = ESM::Attribute::sGmstAttributeDescIds[i]; mStatic.push_back(newAttribute); } } size_t Store::getSize() const { return mStatic.size(); } Store::iterator Store::begin() const { return mStatic.begin(); } Store::iterator Store::end() const { return mStatic.end(); } // Dialogue //========================================================================= template<> void Store::setUp() { // DialInfos marked as deleted are kept during the loading phase, so that the linked list // structure is kept intact for inserting further INFOs. Delete them now that loading is done. for (auto & [_, dial] : mStatic) dial.clearDeletedInfos(); mShared.clear(); mShared.reserve(mStatic.size()); for (auto & [_, dial] : mStatic) mShared.push_back(&dial); } template <> inline RecordId Store::load(ESM::ESMReader &esm) { // The original letter case of a dialogue ID is saved, because it's printed ESM::Dialogue dialogue; bool isDeleted = false; dialogue.loadId(esm); std::string idLower = Misc::StringUtils::lowerCase(dialogue.mId); std::map::iterator found = mStatic.find(idLower); if (found == mStatic.end()) { dialogue.loadData(esm, isDeleted); mStatic.insert(std::make_pair(idLower, dialogue)); } else { found->second.loadData(esm, isDeleted); dialogue = found->second; } return RecordId(dialogue.mId, isDeleted); } template<> bool Store::eraseStatic(const std::string &id) { auto it = mStatic.find(Misc::StringUtils::lowerCase(id)); if (it != mStatic.end()) mStatic.erase(it); return true; } } template class MWWorld::Store; template class MWWorld::Store; template class MWWorld::Store; //template class MWWorld::Store; template class MWWorld::Store; template class MWWorld::Store; template class MWWorld::Store; //template class MWWorld::Store; template class MWWorld::Store; template class MWWorld::Store; template class MWWorld::Store; template class MWWorld::Store; template class MWWorld::Store; template class MWWorld::Store; template class MWWorld::Store; template class MWWorld::Store; template class MWWorld::Store; template class MWWorld::Store; template class MWWorld::Store; template class MWWorld::Store; template class MWWorld::Store; //template class MWWorld::Store; //template class MWWorld::Store; template class MWWorld::Store; template class MWWorld::Store; //template class MWWorld::Store; template class MWWorld::Store; template class MWWorld::Store; //template class MWWorld::Store; template class MWWorld::Store; template class MWWorld::Store; template class MWWorld::Store; template class MWWorld::Store; template class MWWorld::Store; template class MWWorld::Store; //template class MWWorld::Store; template class MWWorld::Store; template class MWWorld::Store; template class MWWorld::Store; template class MWWorld::Store; template class MWWorld::Store; template class MWWorld::Store; ================================================ FILE: apps/openmw/mwworld/store.hpp ================================================ #ifndef OPENMW_MWWORLD_STORE_H #define OPENMW_MWWORLD_STORE_H #include #include #include #include "recordcmp.hpp" namespace ESM { struct Land; } namespace Loading { class Listener; } namespace MWWorld { struct RecordId { std::string mId; bool mIsDeleted; RecordId(const std::string &id = "", bool isDeleted = false); }; class StoreBase { public: virtual ~StoreBase() {} virtual void setUp() {} /// List identifiers of records contained in this Store (case-smashed). No-op for Stores that don't use string IDs. virtual void listIdentifier(std::vector &list) const {} virtual size_t getSize() const = 0; virtual int getDynamicSize() const { return 0; } virtual RecordId load(ESM::ESMReader &esm) = 0; virtual bool eraseStatic(const std::string &id) {return false;} virtual void clearDynamic() {} virtual void write (ESM::ESMWriter& writer, Loading::Listener& progress) const {} virtual RecordId read (ESM::ESMReader& reader, bool overrideOnly = false) { return RecordId(); } ///< Read into dynamic storage }; template class IndexedStore { protected: typedef typename std::map Static; Static mStatic; public: typedef typename std::map::const_iterator iterator; IndexedStore(); iterator begin() const; iterator end() const; void load(ESM::ESMReader &esm); int getSize() const; void setUp(); const T *search(int index) const; const T *find(int index) const; }; template class SharedIterator { typedef typename std::vector::const_iterator Iter; Iter mIter; public: SharedIterator() {} SharedIterator(const SharedIterator &orig) : mIter(orig.mIter) {} SharedIterator(const Iter &iter) : mIter(iter) {} SharedIterator& operator=(const SharedIterator&) = default; SharedIterator &operator++() { ++mIter; return *this; } SharedIterator operator++(int) { SharedIterator iter = *this; ++mIter; return iter; } SharedIterator &operator+=(int advance) { mIter += advance; return *this; } SharedIterator &operator--() { --mIter; return *this; } SharedIterator operator--(int) { SharedIterator iter = *this; --mIter; return iter; } bool operator==(const SharedIterator &x) const { return mIter == x.mIter; } bool operator!=(const SharedIterator &x) const { return !(*this == x); } const T &operator*() const { return **mIter; } const T *operator->() const { return &(**mIter); } }; class ESMStore; template class Store : public StoreBase { std::map mStatic; std::vector mShared; // 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) std::map mDynamic; typedef std::map Dynamic; typedef std::map Static; friend class ESMStore; public: Store(); Store(const Store &orig); typedef SharedIterator iterator; // setUp needs to be called again after void clearDynamic() override; void setUp() override; const T *search(const std::string &id) const; const T *searchStatic(const std::string &id) const; /** * Does the record with this ID come from the dynamic store? */ bool isDynamic(const std::string &id) const; /** Returns a random record that starts with the named ID, or nullptr if not found. */ const T *searchRandom(const std::string &id) const; const T *find(const std::string &id) const; iterator begin() const; iterator end() const; size_t getSize() const override; int getDynamicSize() const override; /// @note The record identifiers are listed in the order that the records were defined by the content files. void listIdentifier(std::vector &list) const override; T *insert(const T &item, bool overrideOnly = false); T *insertStatic(const T &item); bool eraseStatic(const std::string &id) override; bool erase(const std::string &id); bool erase(const T &item); RecordId load(ESM::ESMReader &esm) override; void write(ESM::ESMWriter& writer, Loading::Listener& progress) const override; RecordId read(ESM::ESMReader& reader, bool overrideOnly = false) override; }; template <> class Store : public StoreBase { // For multiple ESM/ESP files we need one list per file. typedef std::vector LandTextureList; std::vector mStatic; public: Store(); typedef std::vector::const_iterator iterator; // Must be threadsafe! Called from terrain background loading threads. // Not a big deal here, since ESM::LandTexture can never be modified or inserted/erased 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 at least \a num plugins. void resize(size_t num); size_t getSize() const override; size_t getSize(size_t plugin) const; RecordId load(ESM::ESMReader &esm, size_t plugin); RecordId load(ESM::ESMReader &esm) override; iterator begin(size_t plugin) const; iterator end(size_t plugin) const; }; template <> class Store : public StoreBase { std::vector mStatic; public: typedef SharedIterator iterator; virtual ~Store(); size_t getSize() const override; iterator begin() const; iterator end() const; // Must be threadsafe! Called from terrain background loading threads. // Not a big deal here, since ESM::Land can never be modified or inserted/erased const ESM::Land *search(int x, int y) const; const ESM::Land *find(int x, int y) const; RecordId load(ESM::ESMReader &esm) override; void setUp() override; private: bool mBuilt = false; }; template <> class Store : public StoreBase { struct DynamicExtCmp { bool operator()(const std::pair &left, const std::pair &right) const { if (left.first == right.first && left.second == right.second) return false; if (left.first == right.first) return left.second > right.second; // Exterior cells are listed in descending, row-major order, // this is a workaround for an ambiguous chargen_plank reference in the vanilla game. // there is one at -22,16 and one at -2,-9, the latter should be used. return left.first > right.first; } }; typedef std::map DynamicInt; typedef std::map, ESM::Cell, DynamicExtCmp> DynamicExt; DynamicInt mInt; DynamicExt mExt; std::vector mSharedInt; std::vector mSharedExt; DynamicInt mDynamicInt; DynamicExt mDynamicExt; const ESM::Cell *search(const ESM::Cell &cell) const; void handleMovedCellRefs(ESM::ESMReader& esm, ESM::Cell* cell); public: typedef SharedIterator iterator; const ESM::Cell *search(const std::string &id) const; const ESM::Cell *search(int x, int y) const; const ESM::Cell *searchStatic(int x, int y) const; const ESM::Cell *searchOrCreate(int x, int y); const ESM::Cell *find(const std::string &id) const; const ESM::Cell *find(int x, int y) const; void clearDynamic() override; void setUp() override; RecordId load(ESM::ESMReader &esm) override; iterator intBegin() const; iterator intEnd() const; iterator extBegin() const; iterator extEnd() const; // Return the northernmost cell in the easternmost column. const ESM::Cell *searchExtByName(const std::string &id) const; // Return the northernmost cell in the easternmost column. const ESM::Cell *searchExtByRegion(const std::string &id) const; size_t getSize() const override; size_t getExtSize() const; size_t getIntSize() const; void listIdentifier(std::vector &list) const override; /* Start of tes3mp addition Make it possible to override a Cell record similarly to how other types of records can be overridden */ ESM::Cell *override(const ESM::Cell &cell); /* End of tes3mp addition */ ESM::Cell *insert(const ESM::Cell &cell); bool erase(const ESM::Cell &cell); bool erase(const std::string &id); bool erase(int x, int y); }; template <> class Store : public StoreBase { private: typedef std::map Interior; typedef std::map, ESM::Pathgrid> Exterior; Interior mInt; Exterior mExt; Store* mCells; public: Store(); void setCells(Store& cells); RecordId load(ESM::ESMReader &esm) override; size_t getSize() const override; void setUp() override; /* Start of tes3mp addition Make it possible to override a Pathgrid record similarly to how other types of records can be overridden */ ESM::Pathgrid* override(const ESM::Pathgrid& pathgrid); /* End of tes3mp addition */ const ESM::Pathgrid *search(int x, int y) const; const ESM::Pathgrid *search(const std::string& name) const; const ESM::Pathgrid *find(int x, int y) const; const ESM::Pathgrid* find(const std::string& name) const; const ESM::Pathgrid *search(const ESM::Cell &cell) const; const ESM::Pathgrid *find(const ESM::Cell &cell) const; }; template <> class Store : public IndexedStore { public: Store(); }; template <> class Store : public IndexedStore { public: Store(); }; template <> class Store : public IndexedStore { std::vector mStatic; public: typedef std::vector::const_iterator iterator; Store(); const ESM::Attribute *search(size_t index) const; const ESM::Attribute *find(size_t index) const; void setUp(); size_t getSize() const; iterator begin() const; iterator end() const; }; template <> class Store : public StoreBase { std::map mStatic; public: typedef std::map::const_iterator iterator; Store(); const ESM::WeaponType *search(const int id) const; const ESM::WeaponType *find(const int id) const; RecordId load(ESM::ESMReader &esm) override { return RecordId(nullptr, false); } ESM::WeaponType* insert(const ESM::WeaponType &weaponType); void setUp() override; size_t getSize() const override; iterator begin() const; iterator end() const; }; } //end namespace #endif ================================================ FILE: apps/openmw/mwworld/timestamp.cpp ================================================ #include "timestamp.hpp" #include #include #include namespace MWWorld { TimeStamp::TimeStamp (float hour, int day) : mHour (hour), mDay (day) { if (hour<0 || hour>=24 || day<0) throw std::runtime_error ("invalid time stamp"); } float TimeStamp::getHour() const { return mHour; } int TimeStamp::getDay() const { return mDay; } TimeStamp& TimeStamp::operator+= (double hours) { if (hours<0) throw std::runtime_error ("can't move time stamp backwards in time"); hours += mHour; mHour = static_cast (std::fmod (hours, 24)); mDay += static_cast(hours / 24); return *this; } bool operator== (const TimeStamp& left, const TimeStamp& right) { return left.getHour()==right.getHour() && left.getDay()==right.getDay(); } bool operator!= (const TimeStamp& left, const TimeStamp& right) { return !(left==right); } bool operator< (const TimeStamp& left, const TimeStamp& right) { if (left.getDay()right.getDay()) return false; return left.getHour() (const TimeStamp& left, const TimeStamp& right) { return !(left<=right); } bool operator>= (const TimeStamp& left, const TimeStamp& right) { return !(left=0 explicit TimeStamp (const ESM::TimeStamp& esm); ESM::TimeStamp toEsm () const; float getHour() const; int getDay() const; TimeStamp& operator+= (double hours); ///< \param hours >=0 }; bool operator== (const TimeStamp& left, const TimeStamp& right); bool operator!= (const TimeStamp& left, const TimeStamp& right); bool operator< (const TimeStamp& left, const TimeStamp& right); bool operator<= (const TimeStamp& left, const TimeStamp& right); bool operator> (const TimeStamp& left, const TimeStamp& right); bool operator>= (const TimeStamp& left, const TimeStamp& right); TimeStamp operator+ (const TimeStamp& stamp, double hours); TimeStamp operator+ (double hours, const TimeStamp& stamp); double operator- (const TimeStamp& left, const TimeStamp& right); ///< Returns the difference between \a left and \a right in in-game hours. } #endif ================================================ FILE: apps/openmw/mwworld/weather.cpp ================================================ #include "weather.hpp" #include #include #include #include /* Start of tes3mp addition Include additional headers for multiplayer purposes */ #include #include "../mwmp/Main.hpp" #include "../mwmp/Networking.hpp" #include "../mwmp/Worldstate.hpp" /* End of tes3mp addition */ #include "../mwbase/environment.hpp" #include "../mwbase/soundmanager.hpp" #include "../mwmechanics/actorutil.hpp" #include "../mwsound/sound.hpp" #include "../mwrender/renderingmanager.hpp" #include "../mwrender/sky.hpp" #include "player.hpp" #include "esmstore.hpp" #include "cellstore.hpp" #include using namespace MWWorld; namespace { static const int invalidWeatherID = -1; // linear interpolate between x and y based on factor. float lerp (float x, float y, float factor) { return x * (1-factor) + y * factor; } // linear interpolate between x and y based on factor. osg::Vec4f lerp (const osg::Vec4f& x, const osg::Vec4f& y, float factor) { return x * (1-factor) + y * factor; } } template T TimeOfDayInterpolator::getValue(const float gameHour, const TimeOfDaySettings& timeSettings, const std::string& prefix) const { WeatherSetting setting = timeSettings.getSetting(prefix); float preSunriseTime = setting.mPreSunriseTime; float postSunriseTime = setting.mPostSunriseTime; float preSunsetTime = setting.mPreSunsetTime; float postSunsetTime = setting.mPostSunsetTime; // night if (gameHour < timeSettings.mNightEnd - preSunriseTime || gameHour > timeSettings.mNightStart + postSunsetTime) return mNightValue; // sunrise else if (gameHour >= timeSettings.mNightEnd - preSunriseTime && gameHour <= timeSettings.mDayStart + postSunriseTime) { float duration = timeSettings.mDayStart + postSunriseTime - timeSettings.mNightEnd + preSunriseTime; float middle = timeSettings.mNightEnd - preSunriseTime + duration / 2.f; if (gameHour <= middle) { // fade in float advance = middle - gameHour; float factor = 0.f; if (duration > 0) factor = advance / duration * 2; return lerp(mSunriseValue, mNightValue, factor); } else { // fade out float advance = gameHour - middle; float factor = 1.f; if (duration > 0) factor = advance / duration * 2; return lerp(mSunriseValue, mDayValue, factor); } } // day else if (gameHour > timeSettings.mDayStart + postSunriseTime && gameHour < timeSettings.mDayEnd - preSunsetTime) return mDayValue; // sunset else if (gameHour >= timeSettings.mDayEnd - preSunsetTime && gameHour <= timeSettings.mNightStart + postSunsetTime) { float duration = timeSettings.mNightStart + postSunsetTime - timeSettings.mDayEnd + preSunsetTime; float middle = timeSettings.mDayEnd - preSunsetTime + duration / 2.f; if (gameHour <= middle) { // fade in float advance = middle - gameHour; float factor = 0.f; if (duration > 0) factor = advance / duration * 2; return lerp(mSunsetValue, mDayValue, factor); } else { // fade out float advance = gameHour - middle; float factor = 1.f; if (duration > 0) factor = advance / duration * 2; return lerp(mSunsetValue, mNightValue, factor); } } // shut up compiler return T(); } template class MWWorld::TimeOfDayInterpolator; template class MWWorld::TimeOfDayInterpolator; Weather::Weather(const std::string& name, float stormWindSpeed, float rainSpeed, float dlFactor, float dlOffset, const std::string& particleEffect) : mCloudTexture(Fallback::Map::getString("Weather_" + name + "_Cloud_Texture")) , mSkyColor(Fallback::Map::getColour("Weather_" + name +"_Sky_Sunrise_Color"), Fallback::Map::getColour("Weather_" + name + "_Sky_Day_Color"), Fallback::Map::getColour("Weather_" + name + "_Sky_Sunset_Color"), Fallback::Map::getColour("Weather_" + name + "_Sky_Night_Color")) , mFogColor(Fallback::Map::getColour("Weather_" + name + "_Fog_Sunrise_Color"), Fallback::Map::getColour("Weather_" + name + "_Fog_Day_Color"), Fallback::Map::getColour("Weather_" + name + "_Fog_Sunset_Color"), Fallback::Map::getColour("Weather_" + name + "_Fog_Night_Color")) , mAmbientColor(Fallback::Map::getColour("Weather_" + name + "_Ambient_Sunrise_Color"), Fallback::Map::getColour("Weather_" + name + "_Ambient_Day_Color"), Fallback::Map::getColour("Weather_" + name + "_Ambient_Sunset_Color"), Fallback::Map::getColour("Weather_" + name + "_Ambient_Night_Color")) , mSunColor(Fallback::Map::getColour("Weather_" + name + "_Sun_Sunrise_Color"), Fallback::Map::getColour("Weather_" + name + "_Sun_Day_Color"), Fallback::Map::getColour("Weather_" + name + "_Sun_Sunset_Color"), Fallback::Map::getColour("Weather_" + name + "_Sun_Night_Color")) , mLandFogDepth(Fallback::Map::getFloat("Weather_" + name + "_Land_Fog_Day_Depth"), Fallback::Map::getFloat("Weather_" + name + "_Land_Fog_Day_Depth"), Fallback::Map::getFloat("Weather_" + name + "_Land_Fog_Day_Depth"), Fallback::Map::getFloat("Weather_" + name + "_Land_Fog_Night_Depth")) , mSunDiscSunsetColor(Fallback::Map::getColour("Weather_" + name + "_Sun_Disc_Sunset_Color")) , mWindSpeed(Fallback::Map::getFloat("Weather_" + name + "_Wind_Speed")) , mCloudSpeed(Fallback::Map::getFloat("Weather_" + name + "_Cloud_Speed")) , mGlareView(Fallback::Map::getFloat("Weather_" + name + "_Glare_View")) , mIsStorm(mWindSpeed > stormWindSpeed) , mRainSpeed(rainSpeed) , mRainEntranceSpeed(Fallback::Map::getFloat("Weather_" + name + "_Rain_Entrance_Speed")) , mRainMaxRaindrops(Fallback::Map::getFloat("Weather_" + name + "_Max_Raindrops")) , mRainDiameter(Fallback::Map::getFloat("Weather_" + name + "_Rain_Diameter")) , mRainThreshold(Fallback::Map::getFloat("Weather_" + name + "_Rain_Threshold")) , mRainMinHeight(Fallback::Map::getFloat("Weather_" + name + "_Rain_Height_Min")) , mRainMaxHeight(Fallback::Map::getFloat("Weather_" + name + "_Rain_Height_Max")) , mParticleEffect(particleEffect) , mRainEffect(Fallback::Map::getBool("Weather_" + name + "_Using_Precip") ? "meshes\\raindrop.nif" : "") , mTransitionDelta(Fallback::Map::getFloat("Weather_" + name + "_Transition_Delta")) , mCloudsMaximumPercent(Fallback::Map::getFloat("Weather_" + name + "_Clouds_Maximum_Percent")) , mThunderFrequency(Fallback::Map::getFloat("Weather_" + name + "_Thunder_Frequency")) , mThunderThreshold(Fallback::Map::getFloat("Weather_" + name + "_Thunder_Threshold")) , mThunderSoundID() , mFlashDecrement(Fallback::Map::getFloat("Weather_" + name + "_Flash_Decrement")) , mFlashBrightness(0.0f) { mDL.FogFactor = dlFactor; mDL.FogOffset = dlOffset; mThunderSoundID[0] = Fallback::Map::getString("Weather_" + name + "_Thunder_Sound_ID_0"); mThunderSoundID[1] = Fallback::Map::getString("Weather_" + name + "_Thunder_Sound_ID_1"); mThunderSoundID[2] = Fallback::Map::getString("Weather_" + name + "_Thunder_Sound_ID_2"); mThunderSoundID[3] = Fallback::Map::getString("Weather_" + name + "_Thunder_Sound_ID_3"); // TODO: support weathers that have both "Ambient Loop Sound ID" and "Rain Loop Sound ID", need to play both sounds at the same time. if (!mRainEffect.empty()) // NOTE: in vanilla, the weathers with rain seem to be hardcoded; changing Using_Precip has no effect { mAmbientLoopSoundID = Fallback::Map::getString("Weather_" + name + "_Rain_Loop_Sound_ID"); if (mAmbientLoopSoundID.empty()) // default to "rain" if not set mAmbientLoopSoundID = "rain"; } else mAmbientLoopSoundID = Fallback::Map::getString("Weather_" + name + "_Ambient_Loop_Sound_ID"); if (Misc::StringUtils::ciEqual(mAmbientLoopSoundID, "None")) mAmbientLoopSoundID.clear(); } float Weather::transitionDelta() const { // Transition Delta describes how quickly transitioning to the weather in question will take, in Hz. Note that the // measurement is in real time, not in-game time. return mTransitionDelta; } float Weather::cloudBlendFactor(const float transitionRatio) const { // Clouds Maximum Percent affects how quickly the sky transitions from one sky texture to the next. return transitionRatio / mCloudsMaximumPercent; } float Weather::calculateThunder(const float transitionRatio, const float elapsedSeconds, const bool isPaused) { // When paused, the flash brightness remains the same and no new strikes can occur. if(!isPaused) { // Morrowind doesn't appear to do any calculations unless the transition ratio is higher than the Thunder Threshold. if(transitionRatio >= mThunderThreshold && mThunderFrequency > 0.0f) { flashDecrement(elapsedSeconds); if(Misc::Rng::rollProbability() <= thunderChance(transitionRatio, elapsedSeconds)) { lightningAndThunder(); } } else { mFlashBrightness = 0.0f; } } return mFlashBrightness; } inline void Weather::flashDecrement(const float elapsedSeconds) { // The Flash Decrement is measured in whole units per second. This means that if the flash brightness was // currently 1.0, then it should take approximately 0.25 seconds to decay to 0.0 (the minimum). float decrement = mFlashDecrement * elapsedSeconds; mFlashBrightness = decrement > mFlashBrightness ? 0.0f : mFlashBrightness - decrement; } inline float Weather::thunderChance(const float transitionRatio, const float elapsedSeconds) const { // This formula is reversed from the observation that with Thunder Frequency set to 1, there are roughly 10 strikes // per minute. It doesn't appear to be tied to in game time as Timescale doesn't affect it. Various values of // Thunder Frequency seem to change the average number of strikes in a linear fashion.. During a transition, it appears to // scaled based on how far past it is past the Thunder Threshold. float scaleFactor = (transitionRatio - mThunderThreshold) / (1.0f - mThunderThreshold); return ((mThunderFrequency * 10.0f) / 60.0f) * elapsedSeconds * scaleFactor; } inline void Weather::lightningAndThunder(void) { // Morrowind seems to vary the intensity of the brightness based on which of the four sound IDs it selects. // They appear to go from 0 (brightest, closest) to 3 (faintest, farthest). The value of 0.25 per distance // was derived by setting the Flash Decrement to 0.1 and measuring how long each value took to decay to 0. // TODO: Determine the distribution of each distance to see if it's evenly weighted. unsigned int distance = Misc::Rng::rollDice(4); // Flash brightness appears additive, since if multiple strikes occur, it takes longer for it to decay to 0. mFlashBrightness += 1 - (distance * 0.25f); MWBase::Environment::get().getSoundManager()->playSound(mThunderSoundID[distance], 1.0, 1.0); } RegionWeather::RegionWeather(const ESM::Region& region) : mWeather(invalidWeatherID) , mChances() { mChances.reserve(10); mChances.push_back(region.mData.mClear); mChances.push_back(region.mData.mCloudy); mChances.push_back(region.mData.mFoggy); mChances.push_back(region.mData.mOvercast); mChances.push_back(region.mData.mRain); mChances.push_back(region.mData.mThunder); mChances.push_back(region.mData.mAsh); mChances.push_back(region.mData.mBlight); mChances.push_back(region.mData.mA); mChances.push_back(region.mData.mB); } RegionWeather::RegionWeather(const ESM::RegionWeatherState& state) : mWeather(state.mWeather) , mChances(state.mChances) { } RegionWeather::operator ESM::RegionWeatherState() const { ESM::RegionWeatherState state = { mWeather, mChances }; return state; } void RegionWeather::setChances(const std::vector& chances) { if(mChances.size() < chances.size()) { mChances.reserve(chances.size()); } int i = 0; for(char chance : chances) { mChances[i] = chance; i++; } // Regional weather no longer supports the current type, select a new weather pattern. if((static_cast(mWeather) >= mChances.size()) || (mChances[mWeather] == 0)) { chooseNewWeather(); } } void RegionWeather::setWeather(int weatherID) { mWeather = weatherID; } int RegionWeather::getWeather() { // If the region weather was already set (by ChangeWeather, or by a previous call) then just return that value. // Note that the region weather will be expired periodically when the weather update timer expires. if(mWeather == invalidWeatherID) { chooseNewWeather(); } return mWeather; } void RegionWeather::chooseNewWeather() { // All probabilities must add to 100 (responsibility of the user). // If chances A and B has values 30 and 70 then by generating 100 numbers 1..100, 30% will be lesser or equal 30 // and 70% will be greater than 30 (in theory). int chance = Misc::Rng::rollDice(100) + 1; // 1..100 int sum = 0; int i = 0; for(; static_cast(i) < mChances.size(); ++i) { sum += mChances[i]; if(chance <= sum) { mWeather = i; return; } } // if we hit this path then the chances don't add to 100, choose a default weather instead mWeather = 0; } MoonModel::MoonModel(const std::string& name) : mFadeInStart(Fallback::Map::getFloat("Moons_" + name + "_Fade_In_Start")) , mFadeInFinish(Fallback::Map::getFloat("Moons_" + name + "_Fade_In_Finish")) , mFadeOutStart(Fallback::Map::getFloat("Moons_" + name + "_Fade_Out_Start")) , mFadeOutFinish(Fallback::Map::getFloat("Moons_" + name + "_Fade_Out_Finish")) , mAxisOffset(Fallback::Map::getFloat("Moons_" + name + "_Axis_Offset")) , mSpeed(Fallback::Map::getFloat("Moons_" + name + "_Speed")) , mDailyIncrement(Fallback::Map::getFloat("Moons_" + name + "_Daily_Increment")) , mFadeStartAngle(Fallback::Map::getFloat("Moons_" + name + "_Fade_Start_Angle")) , mFadeEndAngle(Fallback::Map::getFloat("Moons_" + name + "_Fade_End_Angle")) , mMoonShadowEarlyFadeAngle(Fallback::Map::getFloat("Moons_" + name + "_Moon_Shadow_Early_Fade_Angle")) { // Morrowind appears to have a minimum speed in order to avoid situations where the moon couldn't conceivably // complete a rotation in a single 24 hour period. The value of 180/23 was deduced from reverse engineering. mSpeed = std::min(mSpeed, 180.0f / 23.0f); } MWRender::MoonState MoonModel::calculateState(const TimeStamp& gameTime) const { float rotationFromHorizon = angle(gameTime); MWRender::MoonState state = { rotationFromHorizon, mAxisOffset, // Reverse engineered from Morrowind's scene graph rotation matrices. phase(gameTime), shadowBlend(rotationFromHorizon), earlyMoonShadowAlpha(rotationFromHorizon) * hourlyAlpha(gameTime.getHour()) }; return state; } inline float MoonModel::angle(const TimeStamp& gameTime) const { // Morrowind's moons start travel on one side of the horizon (let's call it H-rise) and travel 180 degrees to the // opposite horizon (let's call it H-set). Upon reaching H-set, they reset to H-rise until the next moon rise. // When calculating the angle of the moon, several cases have to be taken into account: // 1. Moon rises and then sets in one day. // 2. Moon sets and doesn't rise in one day (occurs when the moon rise hour is >= 24). // 3. Moon sets and then rises in one day. float moonRiseHourToday = moonRiseHour(gameTime.getDay()); float moonRiseAngleToday = 0; if(gameTime.getHour() < moonRiseHourToday) { float moonRiseHourYesterday = moonRiseHour(gameTime.getDay() - 1); if(moonRiseHourYesterday < 24) { float moonRiseAngleYesterday = rotation(24 - moonRiseHourYesterday); if(moonRiseAngleYesterday < 180) { // The moon rose but did not set yesterday, so accumulate yesterday's angle with how much we've travelled today. moonRiseAngleToday = rotation(gameTime.getHour()) + moonRiseAngleYesterday; } } } else { moonRiseAngleToday = rotation(gameTime.getHour() - moonRiseHourToday); } if(moonRiseAngleToday >= 180) { // The moon set today, reset the angle to the horizon. moonRiseAngleToday = 0; } return moonRiseAngleToday; } inline float MoonModel::moonRiseHour(unsigned int daysPassed) const { // This arises from the start date of 16 Last Seed, 427 // TODO: Find an alternate formula that doesn't rely on this day being fixed. static const unsigned int startDay = 16; // This odd formula arises from the fact that on 16 Last Seed, 17 increments have occurred, meaning // that upon starting a new game, it must only calculate the moon phase as far back as 1 Last Seed. // Note that we don't modulo after adding the latest daily increment because other calculations need to // know if doing so would cause the moon rise to be postponed until the next day (which happens when // the moon rise hour is >= 24 in Morrowind). return mDailyIncrement + std::fmod((daysPassed - 1 + startDay) * mDailyIncrement, 24.0f); } inline float MoonModel::rotation(float hours) const { // 15 degrees per hour was reverse engineered from the rotation matrices of the Morrowind scene graph. // Note that this correlates to 360 / 24, which is a full rotation every 24 hours, so speed is a measure // of whole rotations that could be completed in a day. return 15.0f * mSpeed * hours; } MWRender::MoonState::Phase MoonModel::phase(const TimeStamp& gameTime) const { // Morrowind starts with a full moon on 16 Last Seed and then begins to wane 17 Last Seed, working on 3 day phase cycle. // If the moon didn't rise yet today, use yesterday's moon phase. if(gameTime.getHour() < moonRiseHour(gameTime.getDay())) return static_cast((gameTime.getDay() / 3) % 8); else return static_cast(((gameTime.getDay() + 1) / 3) % 8); } inline float MoonModel::shadowBlend(float angle) const { // The Fade End Angle and Fade Start Angle describe a region where the moon transitions from a solid disk // that is roughly the color of the sky, to a textured surface. // Depending on the current angle, the following values describe the ratio between the textured moon // and the solid disk: // 1. From Fade End Angle 1 to Fade Start Angle 1 (during moon rise): 0..1 // 2. From Fade Start Angle 1 to Fade Start Angle 2 (between moon rise and moon set): 1 (textured) // 3. From Fade Start Angle 2 to Fade End Angle 2 (during moon set): 1..0 // 4. From Fade End Angle 2 to Fade End Angle 1 (between moon set and moon rise): 0 (solid disk) float fadeAngle = mFadeStartAngle - mFadeEndAngle; float fadeEndAngle2 = 180.0f - mFadeEndAngle; float fadeStartAngle2 = 180.0f - mFadeStartAngle; if((angle >= mFadeEndAngle) && (angle < mFadeStartAngle)) return (angle - mFadeEndAngle) / fadeAngle; else if((angle >= mFadeStartAngle) && (angle < fadeStartAngle2)) return 1.0f; else if((angle >= fadeStartAngle2) && (angle < fadeEndAngle2)) return (fadeEndAngle2 - angle) / fadeAngle; else return 0.0f; } inline float MoonModel::hourlyAlpha(float gameHour) const { // The Fade Out Start / Finish and Fade In Start / Finish describe the hours at which the moon // appears and disappears. // Depending on the current hour, the following values describe how transparent the moon is. // 1. From Fade Out Start to Fade Out Finish: 1..0 // 2. From Fade Out Finish to Fade In Start: 0 (transparent) // 3. From Fade In Start to Fade In Finish: 0..1 // 4. From Fade In Finish to Fade Out Start: 1 (solid) if((gameHour >= mFadeOutStart) && (gameHour < mFadeOutFinish)) return (mFadeOutFinish - gameHour) / (mFadeOutFinish - mFadeOutStart); else if((gameHour >= mFadeOutFinish) && (gameHour < mFadeInStart)) return 0.0f; else if((gameHour >= mFadeInStart) && (gameHour < mFadeInFinish)) return (gameHour - mFadeInStart) / (mFadeInFinish - mFadeInStart); else return 1.0f; } inline float MoonModel::earlyMoonShadowAlpha(float angle) const { // The Moon Shadow Early Fade Angle describes an arc relative to Fade End Angle. // Depending on the current angle, the following values describe how transparent the moon is. // 1. From Moon Shadow Early Fade Angle 1 to Fade End Angle 1 (during moon rise): 0..1 // 2. From Fade End Angle 1 to Fade End Angle 2 (between moon rise and moon set): 1 (solid) // 3. From Fade End Angle 2 to Moon Shadow Early Fade Angle 2 (during moon set): 1..0 // 4. From Moon Shadow Early Fade Angle 2 to Moon Shadow Early Fade Angle 1: 0 (transparent) float moonShadowEarlyFadeAngle1 = mFadeEndAngle - mMoonShadowEarlyFadeAngle; float fadeEndAngle2 = 180.0f - mFadeEndAngle; float moonShadowEarlyFadeAngle2 = fadeEndAngle2 + mMoonShadowEarlyFadeAngle; if((angle >= moonShadowEarlyFadeAngle1) && (angle < mFadeEndAngle)) return (angle - moonShadowEarlyFadeAngle1) / mMoonShadowEarlyFadeAngle; else if((angle >= mFadeEndAngle) && (angle < fadeEndAngle2)) return 1.0f; else if((angle >= fadeEndAngle2) && (angle < moonShadowEarlyFadeAngle2)) return (moonShadowEarlyFadeAngle2 - angle) / mMoonShadowEarlyFadeAngle; else return 0.0f; } WeatherManager::WeatherManager(MWRender::RenderingManager& rendering, MWWorld::ESMStore& store) : mStore(store) , mRendering(rendering) , mSunriseTime(Fallback::Map::getFloat("Weather_Sunrise_Time")) , mSunsetTime(Fallback::Map::getFloat("Weather_Sunset_Time")) , mSunriseDuration(Fallback::Map::getFloat("Weather_Sunrise_Duration")) , mSunsetDuration(Fallback::Map::getFloat("Weather_Sunset_Duration")) , mSunPreSunsetTime(Fallback::Map::getFloat("Weather_Sun_Pre-Sunset_Time")) , mNightFade(0, 0, 0, 1) , mHoursBetweenWeatherChanges(Fallback::Map::getFloat("Weather_Hours_Between_Weather_Changes")) , mRainSpeed(Fallback::Map::getFloat("Weather_Precip_Gravity")) , mUnderwaterFog(Fallback::Map::getFloat("Water_UnderwaterSunriseFog"), Fallback::Map::getFloat("Water_UnderwaterDayFog"), Fallback::Map::getFloat("Water_UnderwaterSunsetFog"), Fallback::Map::getFloat("Water_UnderwaterNightFog")) , mWeatherSettings() , mMasser("Masser") , mSecunda("Secunda") , mWindSpeed(0.f) , mCurrentWindSpeed(0.f) , mNextWindSpeed(0.f) , mIsStorm(false) , mPrecipitation(false) , mStormDirection(0,1,0) , mCurrentRegion() , mTimePassed(0) , mFastForward(false) , mWeatherUpdateTime(mHoursBetweenWeatherChanges) , mTransitionFactor(0) , mNightDayMode(Default) , mCurrentWeather(0) , mNextWeather(0) , mQueuedWeather(0) , mRegions() , mResult() , mAmbientSound(nullptr) , mPlayingSoundID() { mTimeSettings.mNightStart = mSunsetTime + mSunsetDuration; mTimeSettings.mNightEnd = mSunriseTime; mTimeSettings.mDayStart = mSunriseTime + mSunriseDuration; mTimeSettings.mDayEnd = mSunsetTime; mTimeSettings.addSetting("Sky"); mTimeSettings.addSetting("Ambient"); mTimeSettings.addSetting("Fog"); mTimeSettings.addSetting("Sun"); // Morrowind handles stars settings differently for other ones mTimeSettings.mStarsPostSunsetStart = Fallback::Map::getFloat("Weather_Stars_Post-Sunset_Start"); mTimeSettings.mStarsPreSunriseFinish = Fallback::Map::getFloat("Weather_Stars_Pre-Sunrise_Finish"); mTimeSettings.mStarsFadingDuration = Fallback::Map::getFloat("Weather_Stars_Fading_Duration"); WeatherSetting starSetting = { mTimeSettings.mStarsPreSunriseFinish, mTimeSettings.mStarsFadingDuration - mTimeSettings.mStarsPreSunriseFinish, mTimeSettings.mStarsPostSunsetStart, mTimeSettings.mStarsFadingDuration - mTimeSettings.mStarsPostSunsetStart }; mTimeSettings.mSunriseTransitions["Stars"] = starSetting; mWeatherSettings.reserve(10); // These distant land fog factor and offset values are the defaults MGE XE provides. Should be // provided by settings somewhere? addWeather("Clear", 1.0f, 0.0f); // 0 addWeather("Cloudy", 0.9f, 0.0f); // 1 addWeather("Foggy", 0.2f, 30.0f); // 2 addWeather("Overcast", 0.7f, 0.0f); // 3 addWeather("Rain", 0.5f, 10.0f); // 4 addWeather("Thunderstorm", 0.5f, 20.0f); // 5 addWeather("Ashstorm", 0.2f, 50.0f, "meshes\\ashcloud.nif"); // 6 addWeather("Blight", 0.2f, 60.0f, "meshes\\blightcloud.nif"); // 7 addWeather("Snow", 0.5f, 40.0f, "meshes\\snow.nif"); // 8 addWeather("Blizzard", 0.16f, 70.0f, "meshes\\blizzard.nif"); // 9 Store::iterator it = store.get().begin(); for(; it != store.get().end(); ++it) { std::string regionID = Misc::StringUtils::lowerCase(it->mId); mRegions.insert(std::make_pair(regionID, RegionWeather(*it))); } forceWeather(0); } WeatherManager::~WeatherManager() { stopSounds(); } void WeatherManager::changeWeather(const std::string& regionID, const unsigned int weatherID) { // In Morrowind, this seems to have the following behavior, when applied to the current region: // - When there is no transition in progress, start transitioning to the new weather. // - If there is a transition in progress, queue up the transition and process it when the current one completes. // - If there is a transition in progress, and a queued transition, overwrite the queued transition. // - If multiple calls to ChangeWeather are made while paused (console up), only the last call will be used, // meaning that if there was no transition in progress, only the last ChangeWeather will be processed. // If the region isn't current, Morrowind will store the new weather for the region in question. if(weatherID < mWeatherSettings.size()) { std::string lowerCaseRegionID = Misc::StringUtils::lowerCase(regionID); std::map::iterator it = mRegions.find(lowerCaseRegionID); if(it != mRegions.end()) { it->second.setWeather(weatherID); regionalWeatherChanged(it->first, it->second); } } } void WeatherManager::modRegion(const std::string& regionID, const std::vector& chances) { // Sets the region's probability for various weather patterns. Note that this appears to be saved permanently. // In Morrowind, this seems to have the following behavior when applied to the current region: // - If the region supports the current weather, no change in current weather occurs. // - If the region no longer supports the current weather, and there is no transition in progress, begin to // transition to a new supported weather type. // - If the region no longer supports the current weather, and there is a transition in progress, queue a // transition to a new supported weather type. std::string lowerCaseRegionID = Misc::StringUtils::lowerCase(regionID); std::map::iterator it = mRegions.find(lowerCaseRegionID); if(it != mRegions.end()) { it->second.setChances(chances); regionalWeatherChanged(it->first, it->second); } } void WeatherManager::playerTeleported(const std::string& playerRegion, bool isExterior) { // If the player teleports to an outdoors cell in a new region (for instance, by travelling), the weather needs to // be changed immediately, and any transitions for the previous region discarded. { std::map::iterator it = mRegions.find(playerRegion); if(it != mRegions.end() && playerRegion != mCurrentRegion) { /* Start of tes3mp addition If we've moved to another region, set our weather creation ability to false; the server will set it to true if it wants us creating weather here */ setWeatherCreationState(false); /* End of tes3mp addition */ mCurrentRegion = playerRegion; forceWeather(it->second.getWeather()); } /* Start of tes3mp addition There's no scenario where we want our weather creation ability to be true in an interior, so set it to false */ else if (!isExterior) setWeatherCreationState(false); /* End of tes3mp addition */ } } /* Start of tes3mp addition Make it possible to set a specific weather state for a region from elsewhere in the code */ void WeatherManager::setRegionWeather(const std::string& region, const int currentWeather, const int nextWeather, const int queuedWeather, const float transitionFactor, bool force) { bool isSameRegion = Misc::StringUtils::ciEqual(region, mCurrentRegion); // Only ever force weather if we are in the correct region for it if (isSameRegion) { if (force) { mCurrentWeather = currentWeather; mNextWeather = nextWeather; mQueuedWeather = queuedWeather; mTransitionFactor = transitionFactor; } else { // Keep the queued weather in sync if everything else already is if (mCurrentWeather == currentWeather && mNextWeather == nextWeather) { mQueuedWeather = queuedWeather; } // Start moving towards the next weather immediately if it's different from the one we have else if (mNextWeather != nextWeather && nextWeather != -1) { changeWeather(region, nextWeather); } // Otherwise, if the current weather is different from the one we should have, move towards it else if (mCurrentWeather != currentWeather) { changeWeather(region, currentWeather); } } } else { changeWeather(region, currentWeather); } } /* End of tes3mp addition */ float WeatherManager::calculateWindSpeed(int weatherId, float currentSpeed) { float targetSpeed = std::min(8.0f * mWeatherSettings[weatherId].mWindSpeed, 70.f); if (currentSpeed == 0.f) currentSpeed = targetSpeed; float multiplier = mWeatherSettings[weatherId].mRainEffect.empty() ? 1.f : 0.5f; float updatedSpeed = (Misc::Rng::rollClosedProbability() - 0.5f) * multiplier * targetSpeed + currentSpeed; if (updatedSpeed > 0.5f * targetSpeed && updatedSpeed < 2.f * targetSpeed) currentSpeed = updatedSpeed; return currentSpeed; } void WeatherManager::update(float duration, bool paused, const TimeStamp& time, bool isExterior) { MWWorld::ConstPtr player = MWMechanics::getPlayer(); if(!paused || mFastForward) { // Add new transitions when either the player's current external region changes. std::string playerRegion = Misc::StringUtils::lowerCase(player.getCell()->getCell()->mRegion); if(updateWeatherTime() || updateWeatherRegion(playerRegion)) { std::map::iterator it = mRegions.find(mCurrentRegion); if(it != mRegions.end()) { addWeatherTransition(it->second.getWeather()); } } updateWeatherTransitions(duration); } bool isDay = time.getHour() >= mSunriseTime && time.getHour() <= mTimeSettings.mNightStart; if (isExterior && !isDay) mNightDayMode = ExteriorNight; else if (!isExterior && isDay && mWeatherSettings[mCurrentWeather].mGlareView >= 0.5f) mNightDayMode = InteriorDay; else mNightDayMode = Default; if(!isExterior) { mRendering.setSkyEnabled(false); stopSounds(); mWindSpeed = 0.f; mCurrentWindSpeed = 0.f; mNextWindSpeed = 0.f; return; } calculateWeatherResult(time.getHour(), duration, paused); if (!paused) { mWindSpeed = mResult.mWindSpeed; mCurrentWindSpeed = mResult.mCurrentWindSpeed; mNextWindSpeed = mResult.mNextWindSpeed; } mIsStorm = mResult.mIsStorm; // For some reason Ash Storm is not considered as a precipitation weather in game mPrecipitation = !(mResult.mParticleEffect.empty() && mResult.mRainEffect.empty()) && mResult.mParticleEffect != "meshes\\ashcloud.nif"; if (mIsStorm) { osg::Vec3f stormDirection(0, 1, 0); if (mResult.mParticleEffect == "meshes\\ashcloud.nif" || mResult.mParticleEffect == "meshes\\blightcloud.nif") { osg::Vec3f playerPos (MWMechanics::getPlayer().getRefData().getPosition().asVec3()); playerPos.z() = 0; osg::Vec3f redMountainPos (25000, 70000, 0); stormDirection = (playerPos - redMountainPos); stormDirection.normalize(); } mStormDirection = stormDirection; mRendering.getSkyManager()->setStormDirection(mStormDirection); } // disable sun during night if (time.getHour() >= mTimeSettings.mNightStart || time.getHour() <= mSunriseTime) mRendering.getSkyManager()->sunDisable(); else mRendering.getSkyManager()->sunEnable(); // Update the sun direction. Run it east to west at a fixed angle from overhead. // The sun's speed at day and night may differ, since mSunriseTime and mNightStart // mark when the sun is level with the horizon. { // Shift times into a 24-hour window beginning at mSunriseTime... float adjustedHour = time.getHour(); float adjustedNightStart = mTimeSettings.mNightStart; if ( time.getHour() < mSunriseTime ) adjustedHour += 24.f; if ( mTimeSettings.mNightStart < mSunriseTime ) adjustedNightStart += 24.f; const bool is_night = adjustedHour >= adjustedNightStart; const float dayDuration = adjustedNightStart - mSunriseTime; const float nightDuration = 24.f - dayDuration; double theta; if ( !is_night ) { theta = static_cast(osg::PI) * (adjustedHour - mSunriseTime) / dayDuration; } else { theta = static_cast(osg::PI) - static_cast(osg::PI) * (adjustedHour - adjustedNightStart) / nightDuration; } osg::Vec3f final( static_cast(cos(theta)), -0.268f, // approx tan( -15 degrees ) static_cast(sin(theta))); mRendering.setSunDirection( final * -1 ); } float underwaterFog = mUnderwaterFog.getValue(time.getHour(), mTimeSettings, "Fog"); float peakHour = mSunriseTime + (mTimeSettings.mNightStart - mSunriseTime) / 2; float glareFade = 1.f; if (time.getHour() < mSunriseTime || time.getHour() > mTimeSettings.mNightStart) glareFade = 0.f; else if (time.getHour() < peakHour) glareFade = 1.f - (peakHour - time.getHour()) / (peakHour - mSunriseTime); else glareFade = 1.f - (time.getHour() - peakHour) / (mTimeSettings.mNightStart - peakHour); mRendering.getSkyManager()->setGlareTimeOfDayFade(glareFade); mRendering.getSkyManager()->setMasserState(mMasser.calculateState(time)); mRendering.getSkyManager()->setSecundaState(mSecunda.calculateState(time)); mRendering.configureFog(mResult.mFogDepth, underwaterFog, mResult.mDLFogFactor, mResult.mDLFogOffset/100.0f, mResult.mFogColor); mRendering.setAmbientColour(mResult.mAmbientColor); mRendering.setSunColour(mResult.mSunColor, mResult.mSunColor * mResult.mGlareView * glareFade); mRendering.getSkyManager()->setWeather(mResult); // Play sounds if (mPlayingSoundID != mResult.mAmbientLoopSoundID) { stopSounds(); if (!mResult.mAmbientLoopSoundID.empty()) mAmbientSound = MWBase::Environment::get().getSoundManager()->playSound( mResult.mAmbientLoopSoundID, mResult.mAmbientSoundVolume, 1.0, MWSound::Type::Sfx, MWSound::PlayMode::Loop ); mPlayingSoundID = mResult.mAmbientLoopSoundID; } else if (mAmbientSound) mAmbientSound->setVolume(mResult.mAmbientSoundVolume); } void WeatherManager::stopSounds() { if (mAmbientSound) MWBase::Environment::get().getSoundManager()->stopSound(mAmbientSound); mAmbientSound = nullptr; mPlayingSoundID.clear(); } float WeatherManager::getWindSpeed() const { return mWindSpeed; } bool WeatherManager::isInStorm() const { return mIsStorm; } osg::Vec3f WeatherManager::getStormDirection() const { return mStormDirection; } void WeatherManager::advanceTime(double hours, bool incremental) { // In Morrowind, when the player sleeps/waits, serves jail time, travels, or trains, all weather transitions are // immediately applied, regardless of whatever transition time might have been remaining. mTimePassed += hours; mFastForward = !incremental ? true : mFastForward; } unsigned int WeatherManager::getWeatherID() const { return mCurrentWeather; } NightDayMode WeatherManager::getNightDayMode() const { return mNightDayMode; } bool WeatherManager::useTorches(float hour) const { bool isDark = hour < mSunriseTime || hour > mTimeSettings.mNightStart; return isDark && !mPrecipitation; } void WeatherManager::write(ESM::ESMWriter& writer, Loading::Listener& progress) { ESM::WeatherState state; state.mCurrentRegion = mCurrentRegion; state.mTimePassed = mTimePassed; state.mFastForward = mFastForward; state.mWeatherUpdateTime = mWeatherUpdateTime; state.mTransitionFactor = mTransitionFactor; state.mCurrentWeather = mCurrentWeather; state.mNextWeather = mNextWeather; state.mQueuedWeather = mQueuedWeather; std::map::iterator it = mRegions.begin(); for(; it != mRegions.end(); ++it) { state.mRegions.insert(std::make_pair(it->first, it->second)); } writer.startRecord(ESM::REC_WTHR); state.save(writer); writer.endRecord(ESM::REC_WTHR); } bool WeatherManager::readRecord(ESM::ESMReader& reader, uint32_t type) { if(ESM::REC_WTHR == type) { static const int oldestCompatibleSaveFormat = 2; if(reader.getFormat() < oldestCompatibleSaveFormat) { // Weather state isn't really all that important, so to preserve older save games, we'll just discard the // older weather records, rather than fail to handle the record. reader.skipRecord(); } else { ESM::WeatherState state; state.load(reader); mCurrentRegion.swap(state.mCurrentRegion); mTimePassed = state.mTimePassed; mFastForward = state.mFastForward; mWeatherUpdateTime = state.mWeatherUpdateTime; mTransitionFactor = state.mTransitionFactor; mCurrentWeather = state.mCurrentWeather; mNextWeather = state.mNextWeather; mQueuedWeather = state.mQueuedWeather; mRegions.clear(); importRegions(); for(std::map::iterator it = state.mRegions.begin(); it != state.mRegions.end(); ++it) { std::map::iterator found = mRegions.find(it->first); if (found != mRegions.end()) { found->second = RegionWeather(it->second); } } } return true; } return false; } void WeatherManager::clear() { stopSounds(); mCurrentRegion = ""; mTimePassed = 0.0f; mWeatherUpdateTime = 0.0f; forceWeather(0); mRegions.clear(); importRegions(); } /* Start of tes3mp addition Make it possible to check whether the local WeatherManager has the ability to create weather changes */ bool WeatherManager::getWeatherCreationState() { return mWeatherCreationState; } /* End of tes3mp addition */ /* Start of tes3mp addition Make it possible to enable and disable the local WeatherManager's ability to create weather changes */ void WeatherManager::setWeatherCreationState(bool state) { mWeatherCreationState = state; } /* End of tes3mp addition */ /* Start of tes3mp addition Make it possible to send the current weather in a WorldWeather packet when requested from elsewhere in the code */ void WeatherManager::sendWeather() { mwmp::Worldstate *worldstate = mwmp::Main::get().getNetworking()->getWorldstate(); worldstate->sendWeather(mCurrentRegion, mCurrentWeather, mNextWeather, mQueuedWeather, mTransitionFactor); } /* End of tes3mp addition */ inline void WeatherManager::addWeather(const std::string& name, float dlFactor, float dlOffset, const std::string& particleEffect) { static const float fStromWindSpeed = mStore.get().find("fStromWindSpeed")->mValue.getFloat(); Weather weather(name, fStromWindSpeed, mRainSpeed, dlFactor, dlOffset, particleEffect); mWeatherSettings.push_back(weather); } inline void WeatherManager::importRegions() { for(const ESM::Region& region : mStore.get()) { std::string regionID = Misc::StringUtils::lowerCase(region.mId); mRegions.insert(std::make_pair(regionID, RegionWeather(region))); } } inline void WeatherManager::regionalWeatherChanged(const std::string& regionID, RegionWeather& region) { // If the region is current, then add a weather transition for it. MWWorld::ConstPtr player = MWMechanics::getPlayer(); if(player.isInCell()) { if(Misc::StringUtils::ciEqual(regionID, mCurrentRegion)) { addWeatherTransition(region.getWeather()); } } } inline bool WeatherManager::updateWeatherTime() { /* Start of tes3mp change (major) Avoid creating any weather changes on this client unless approved by the server */ if (!mWeatherCreationState) return false; /* End of tes3mp change (major) */ mWeatherUpdateTime -= mTimePassed; mTimePassed = 0.0f; if(mWeatherUpdateTime <= 0.0f) { // Expire all regional weather, so that any call to getWeather() will return a new weather ID. std::map::iterator it = mRegions.begin(); for(; it != mRegions.end(); ++it) { it->second.setWeather(invalidWeatherID); } mWeatherUpdateTime += mHoursBetweenWeatherChanges; return true; } return false; } inline bool WeatherManager::updateWeatherRegion(const std::string& playerRegion) { if(!playerRegion.empty() && playerRegion != mCurrentRegion) { mCurrentRegion = playerRegion; /* Start of tes3mp addition If we've moved to another region, set our weather creation ability to false; the server will set it to true if it wants us creating weather here */ setWeatherCreationState(false); /* End of tes3mp addition */ return true; } return false; } inline void WeatherManager::updateWeatherTransitions(const float elapsedRealSeconds) { /* Start of tes3mp addition Track whether an ID_WORLD_WEATHER packet should be sent or not */ bool shouldSendPacket = false; /* End of tes3mp addition */ // When a player chooses to train, wait, or serves jail time, any transitions will be fast forwarded to the last // weather type set, regardless of the remaining transition time. if(!mFastForward && inTransition()) { const float delta = mWeatherSettings[mNextWeather].transitionDelta(); mTransitionFactor -= elapsedRealSeconds * delta; if(mTransitionFactor <= 0.0f) { mCurrentWeather = mNextWeather; mNextWeather = mQueuedWeather; mQueuedWeather = invalidWeatherID; // We may have begun processing the queued transition, so we need to apply the remaining time towards it. if(inTransition()) { const float newDelta = mWeatherSettings[mNextWeather].transitionDelta(); const float remainingSeconds = -(mTransitionFactor / delta); mTransitionFactor = 1.0f - (remainingSeconds * newDelta); } else { mTransitionFactor = 0.0f; } /* Start of tes3mp addition The weather is changing, so decide to send an ID_WORLD_WEATHER packet */ shouldSendPacket = true; /* End of tes3mp addition */ } } else { /* Start of tes3mp addition If the weather is changing, decide to send an ID_WORLD_WEATHER packet */ if (mQueuedWeather != invalidWeatherID || mNextWeather != invalidWeatherID) shouldSendPacket = true; /* End of tes3mp addition */ if(mQueuedWeather != invalidWeatherID) { mCurrentWeather = mQueuedWeather; } else if(mNextWeather != invalidWeatherID) { mCurrentWeather = mNextWeather; } mNextWeather = invalidWeatherID; mQueuedWeather = invalidWeatherID; mFastForward = false; } /* Start of tes3mp addition Send an ID_WORLD_WEATHER packet every time the weather changes here, but only if we are allowed to create weather changes on this client */ if (shouldSendPacket && mWeatherCreationState && !mCurrentRegion.empty()) { sendWeather(); } /* End of tes3mp addition */ } inline void WeatherManager::forceWeather(const int weatherID) { mTransitionFactor = 0.0f; mCurrentWeather = weatherID; mNextWeather = invalidWeatherID; mQueuedWeather = invalidWeatherID; /* Start of tes3mp addition Send an ID_WORLD_WEATHER packet every time the weather changes here, but only if we are allowed to create weather changes on this client */ if (!mCurrentRegion.empty() && mWeatherCreationState) { sendWeather(); } /* End of tes3mp addition */ } inline bool WeatherManager::inTransition() { return mNextWeather != invalidWeatherID; } inline void WeatherManager::addWeatherTransition(const int weatherID) { // In order to work like ChangeWeather expects, this method begins transitioning to the new weather immediately if // no transition is in progress, otherwise it queues it to be transitioned. assert(weatherID >= 0 && static_cast(weatherID) < mWeatherSettings.size()); if(!inTransition() && (weatherID != mCurrentWeather)) { mNextWeather = weatherID; mTransitionFactor = 1.0f; } else if(inTransition() && (weatherID != mNextWeather)) { mQueuedWeather = weatherID; } /* Start of tes3mp addition Send an ID_WORLD_WEATHER packet every time the weather changes here, but only if we are allowed to create weather changes on this client */ if (mWeatherCreationState) { sendWeather(); } /* End of tes3mp addition */ } inline void WeatherManager::calculateWeatherResult(const float gameHour, const float elapsedSeconds, const bool isPaused) { float flash = 0.0f; if(!inTransition()) { calculateResult(mCurrentWeather, gameHour); flash = mWeatherSettings[mCurrentWeather].calculateThunder(1.0f, elapsedSeconds, isPaused); } else { calculateTransitionResult(1 - mTransitionFactor, gameHour); float currentFlash = mWeatherSettings[mCurrentWeather].calculateThunder(mTransitionFactor, elapsedSeconds, isPaused); float nextFlash = mWeatherSettings[mNextWeather].calculateThunder(1 - mTransitionFactor, elapsedSeconds, isPaused); flash = currentFlash + nextFlash; } osg::Vec4f flashColor(flash, flash, flash, 0.0f); mResult.mFogColor += flashColor; mResult.mAmbientColor += flashColor; mResult.mSunColor += flashColor; } inline void WeatherManager::calculateResult(const int weatherID, const float gameHour) { const Weather& current = mWeatherSettings[weatherID]; mResult.mCloudTexture = current.mCloudTexture; mResult.mCloudBlendFactor = 0; mResult.mNextWindSpeed = 0; mResult.mWindSpeed = mResult.mCurrentWindSpeed = calculateWindSpeed(weatherID, mWindSpeed); mResult.mBaseWindSpeed = mWeatherSettings[weatherID].mWindSpeed; mResult.mCloudSpeed = current.mCloudSpeed; mResult.mGlareView = current.mGlareView; mResult.mAmbientLoopSoundID = current.mAmbientLoopSoundID; mResult.mAmbientSoundVolume = 1.f; mResult.mPrecipitationAlpha = 1.f; mResult.mIsStorm = current.mIsStorm; mResult.mRainSpeed = current.mRainSpeed; mResult.mRainEntranceSpeed = current.mRainEntranceSpeed; mResult.mRainDiameter = current.mRainDiameter; mResult.mRainMinHeight = current.mRainMinHeight; mResult.mRainMaxHeight = current.mRainMaxHeight; mResult.mRainMaxRaindrops = current.mRainMaxRaindrops; mResult.mParticleEffect = current.mParticleEffect; mResult.mRainEffect = current.mRainEffect; mResult.mNight = (gameHour < mSunriseTime || gameHour > mTimeSettings.mNightStart + mTimeSettings.mStarsPostSunsetStart - mTimeSettings.mStarsFadingDuration); mResult.mFogDepth = current.mLandFogDepth.getValue(gameHour, mTimeSettings, "Fog"); mResult.mFogColor = current.mFogColor.getValue(gameHour, mTimeSettings, "Fog"); mResult.mAmbientColor = current.mAmbientColor.getValue(gameHour, mTimeSettings, "Ambient"); mResult.mSunColor = current.mSunColor.getValue(gameHour, mTimeSettings, "Sun"); mResult.mSkyColor = current.mSkyColor.getValue(gameHour, mTimeSettings, "Sky"); mResult.mNightFade = mNightFade.getValue(gameHour, mTimeSettings, "Stars"); mResult.mDLFogFactor = current.mDL.FogFactor; mResult.mDLFogOffset = current.mDL.FogOffset; WeatherSetting setting = mTimeSettings.getSetting("Sun"); float preSunsetTime = setting.mPreSunsetTime; if (gameHour >= mTimeSettings.mDayEnd - preSunsetTime) { float factor = 1.f; if (preSunsetTime > 0) factor = (gameHour - (mTimeSettings.mDayEnd - preSunsetTime)) / preSunsetTime; factor = std::min(1.f, factor); mResult.mSunDiscColor = lerp(osg::Vec4f(1,1,1,1), current.mSunDiscSunsetColor, factor); // The SunDiscSunsetColor in the INI isn't exactly the resulting color on screen, most likely because // MW applied the color to the ambient term as well. After the ambient and emissive terms are added together, the fixed pipeline // would then clamp the total lighting to (1,1,1). A noticeable change in color tone can be observed when only one of the color components gets clamped. // Unfortunately that means we can't use the INI color as is, have to replicate the above nonsense. mResult.mSunDiscColor = mResult.mSunDiscColor + osg::componentMultiply(mResult.mSunDiscColor, mResult.mAmbientColor); for (int i=0; i<3; ++i) mResult.mSunDiscColor[i] = std::min(1.f, mResult.mSunDiscColor[i]); } else mResult.mSunDiscColor = osg::Vec4f(1,1,1,1); if (gameHour >= mTimeSettings.mDayEnd) { // sunset float fade = std::min(1.f, (gameHour - mTimeSettings.mDayEnd) / (mTimeSettings.mNightStart - mTimeSettings.mDayEnd)); fade = fade*fade; mResult.mSunDiscColor.a() = 1.f - fade; } else if (gameHour >= mTimeSettings.mNightEnd && gameHour <= mTimeSettings.mNightEnd + mSunriseDuration / 2.f) { // sunrise mResult.mSunDiscColor.a() = gameHour - mTimeSettings.mNightEnd; } else mResult.mSunDiscColor.a() = 1; } inline void WeatherManager::calculateTransitionResult(const float factor, const float gameHour) { calculateResult(mCurrentWeather, gameHour); const MWRender::WeatherResult current = mResult; calculateResult(mNextWeather, gameHour); const MWRender::WeatherResult other = mResult; mResult.mCloudTexture = current.mCloudTexture; mResult.mNextCloudTexture = other.mCloudTexture; mResult.mCloudBlendFactor = mWeatherSettings[mNextWeather].cloudBlendFactor(factor); mResult.mFogColor = lerp(current.mFogColor, other.mFogColor, factor); mResult.mSunColor = lerp(current.mSunColor, other.mSunColor, factor); mResult.mSkyColor = lerp(current.mSkyColor, other.mSkyColor, factor); mResult.mAmbientColor = lerp(current.mAmbientColor, other.mAmbientColor, factor); mResult.mSunDiscColor = lerp(current.mSunDiscColor, other.mSunDiscColor, factor); mResult.mFogDepth = lerp(current.mFogDepth, other.mFogDepth, factor); mResult.mDLFogFactor = lerp(current.mDLFogFactor, other.mDLFogFactor, factor); mResult.mDLFogOffset = lerp(current.mDLFogOffset, other.mDLFogOffset, factor); mResult.mCurrentWindSpeed = calculateWindSpeed(mCurrentWeather, mCurrentWindSpeed); mResult.mNextWindSpeed = calculateWindSpeed(mNextWeather, mNextWindSpeed); mResult.mBaseWindSpeed = lerp(current.mBaseWindSpeed, other.mBaseWindSpeed, factor); mResult.mWindSpeed = lerp(mResult.mCurrentWindSpeed, mResult.mNextWindSpeed, factor); mResult.mCloudSpeed = lerp(current.mCloudSpeed, other.mCloudSpeed, factor); mResult.mGlareView = lerp(current.mGlareView, other.mGlareView, factor); mResult.mNightFade = lerp(current.mNightFade, other.mNightFade, factor); mResult.mNight = current.mNight; float threshold = mWeatherSettings[mNextWeather].mRainThreshold; if (threshold <= 0) threshold = 0.5f; if(factor < threshold) { mResult.mIsStorm = current.mIsStorm; mResult.mParticleEffect = current.mParticleEffect; mResult.mRainEffect = current.mRainEffect; mResult.mRainSpeed = current.mRainSpeed; mResult.mRainEntranceSpeed = current.mRainEntranceSpeed; mResult.mAmbientSoundVolume = 1 - factor / threshold; mResult.mPrecipitationAlpha = mResult.mAmbientSoundVolume; mResult.mAmbientLoopSoundID = current.mAmbientLoopSoundID; mResult.mRainDiameter = current.mRainDiameter; mResult.mRainMinHeight = current.mRainMinHeight; mResult.mRainMaxHeight = current.mRainMaxHeight; mResult.mRainMaxRaindrops = current.mRainMaxRaindrops; } else { mResult.mIsStorm = other.mIsStorm; mResult.mParticleEffect = other.mParticleEffect; mResult.mRainEffect = other.mRainEffect; mResult.mRainSpeed = other.mRainSpeed; mResult.mRainEntranceSpeed = other.mRainEntranceSpeed; mResult.mAmbientSoundVolume = (factor - threshold) / (1 - threshold); mResult.mPrecipitationAlpha = mResult.mAmbientSoundVolume; mResult.mAmbientLoopSoundID = other.mAmbientLoopSoundID; mResult.mRainDiameter = other.mRainDiameter; mResult.mRainMinHeight = other.mRainMinHeight; mResult.mRainMaxHeight = other.mRainMaxHeight; mResult.mRainMaxRaindrops = other.mRainMaxRaindrops; } } ================================================ FILE: apps/openmw/mwworld/weather.hpp ================================================ #ifndef GAME_MWWORLD_WEATHER_H #define GAME_MWWORLD_WEATHER_H #include #include #include #include #include #include "../mwbase/soundmanager.hpp" #include "../mwrender/sky.hpp" namespace ESM { struct Region; struct RegionWeatherState; class ESMWriter; class ESMReader; } namespace MWRender { class RenderingManager; } namespace Loading { class Listener; } namespace Fallback { class Map; } namespace MWWorld { class TimeStamp; enum NightDayMode { Default = 0, ExteriorNight = 1, InteriorDay = 2 }; struct WeatherSetting { float mPreSunriseTime; float mPostSunriseTime; float mPreSunsetTime; float mPostSunsetTime; }; struct TimeOfDaySettings { float mNightStart; float mNightEnd; float mDayStart; float mDayEnd; std::map mSunriseTransitions; float mStarsPostSunsetStart; float mStarsPreSunriseFinish; float mStarsFadingDuration; WeatherSetting getSetting(const std::string& type) const { std::map::const_iterator it = mSunriseTransitions.find(type); if (it != mSunriseTransitions.end()) { return it->second; } else { return { 1.f, 1.f, 1.f, 1.f }; } } void addSetting(const std::string& type) { WeatherSetting setting = { Fallback::Map::getFloat("Weather_" + type + "_Pre-Sunrise_Time"), Fallback::Map::getFloat("Weather_" + type + "_Post-Sunrise_Time"), Fallback::Map::getFloat("Weather_" + type + "_Pre-Sunset_Time"), Fallback::Map::getFloat("Weather_" + type + "_Post-Sunset_Time") }; mSunriseTransitions[type] = setting; } }; /// Interpolates between 4 data points (sunrise, day, sunset, night) based on the time of day. /// The template value could be a floating point number, or a color. template class TimeOfDayInterpolator { public: TimeOfDayInterpolator(const T& sunrise, const T& day, const T& sunset, const T& night) : mSunriseValue(sunrise), mDayValue(day), mSunsetValue(sunset), mNightValue(night) { } T getValue (const float gameHour, const TimeOfDaySettings& timeSettings, const std::string& prefix) const; private: T mSunriseValue, mDayValue, mSunsetValue, mNightValue; }; /// Defines a single weather setting (according to INI) class Weather { public: Weather(const std::string& name, float stormWindSpeed, float rainSpeed, float dlFactor, float dlOffset, const std::string& particleEffect); std::string mCloudTexture; // Sky (atmosphere) color TimeOfDayInterpolator mSkyColor; // Fog color TimeOfDayInterpolator mFogColor; // Ambient lighting color TimeOfDayInterpolator mAmbientColor; // Sun (directional) lighting color TimeOfDayInterpolator mSunColor; // Fog depth/density TimeOfDayInterpolator mLandFogDepth; // Color modulation for the sun itself during sunset osg::Vec4f mSunDiscSunsetColor; // Used by scripts to animate signs, etc based on the wind (GetWindSpeed) float mWindSpeed; // Cloud animation speed multiplier float mCloudSpeed; // Value between 0 and 1, defines the strength of the sun glare effect. // Also appears to modify how visible the sun, moons, and stars are for various weather effects. float mGlareView; // Fog factor and offset used with distant land rendering. struct { float FogFactor; float FogOffset; } mDL; // Sound effect // This is used for Blight, Ashstorm and Blizzard (Bloodmoon) std::string mAmbientLoopSoundID; // Is this an ash storm / blight storm? If so, the following will happen: // - The particles and clouds will be oriented so they appear to come from the Red Mountain. // - Characters will animate their hand to protect eyes from the storm when looking in its direction (idlestorm animation) // - Slower movement when walking against the storm (fStromWalkMult) bool mIsStorm; // How fast does rain travel down? // In Morrowind.ini this is set globally, but we may want to change it per weather later. float mRainSpeed; // How often does a new rain mesh spawn? float mRainEntranceSpeed; // Maximum count of rain particles int mRainMaxRaindrops; // Radius of rain effect float mRainDiameter; // Transition threshold to spawn rain float mRainThreshold; // Height of rain particles spawn float mRainMinHeight; float mRainMaxHeight; std::string mParticleEffect; std::string mRainEffect; // Note: For Weather Blight, there is a "Disease Chance" (=0.1) setting. But according to MWSFD this feature // is broken in the vanilla game and was disabled. float transitionDelta() const; float cloudBlendFactor(const float transitionRatio) const; float calculateThunder(const float transitionRatio, const float elapsedSeconds, const bool isPaused); private: float mTransitionDelta; float mCloudsMaximumPercent; // Note: In MW, only thunderstorms support these attributes, but in the interest of making weather more // flexible, these settings are imported for all weather types. Only thunderstorms will normally have any // non-zero values. float mThunderFrequency; float mThunderThreshold; std::string mThunderSoundID[4]; float mFlashDecrement; float mFlashBrightness; void flashDecrement(const float elapsedSeconds); float thunderChance(const float transitionRatio, const float elapsedSeconds) const; void lightningAndThunder(void); }; /// A class for storing a region's weather. class RegionWeather { public: explicit RegionWeather(const ESM::Region& region); explicit RegionWeather(const ESM::RegionWeatherState& state); operator ESM::RegionWeatherState() const; void setChances(const std::vector& chances); void setWeather(int weatherID); int getWeather(); private: int mWeather; std::vector mChances; void chooseNewWeather(); }; /// A class that acts as a model for the moons. class MoonModel { public: MoonModel(const std::string& name); MWRender::MoonState calculateState(const TimeStamp& gameTime) const; private: float mFadeInStart; float mFadeInFinish; float mFadeOutStart; float mFadeOutFinish; float mAxisOffset; float mSpeed; float mDailyIncrement; float mFadeStartAngle; float mFadeEndAngle; float mMoonShadowEarlyFadeAngle; float angle(const TimeStamp& gameTime) const; float moonRiseHour(unsigned int daysPassed) const; float rotation(float hours) const; MWRender::MoonState::Phase phase(const TimeStamp& gameTime) const; float shadowBlend(float angle) const; float hourlyAlpha(float gameHour) const; float earlyMoonShadowAlpha(float angle) const; }; /// Interface for weather settings class WeatherManager { public: // Have to pass fallback and Store, can't use singleton since World isn't fully constructed yet at the time WeatherManager(MWRender::RenderingManager& rendering, MWWorld::ESMStore& store); ~WeatherManager(); /** * Change the weather in the specified region * @param region that should be changed * @param ID of the weather setting to shift to */ void changeWeather(const std::string& regionID, const unsigned int weatherID); void modRegion(const std::string& regionID, const std::vector& chances); void playerTeleported(const std::string& playerRegion, bool isExterior); /* Start of tes3mp addition Make it possible to set a specific weather state for a region from elsewhere in the code */ void setRegionWeather(const std::string& region, const int currentWeather, const int nextWeather, const int queuedWeather, const float transitionFactor, bool force); /* End of tes3mp addition */ /** * Per-frame update * @param duration * @param paused */ void update(float duration, bool paused, const TimeStamp& time, bool isExterior); void stopSounds(); float getWindSpeed() const; NightDayMode getNightDayMode() const; /// Are we in an ash or blight storm? bool isInStorm() const; osg::Vec3f getStormDirection() const; void advanceTime(double hours, bool incremental); unsigned int getWeatherID() const; bool useTorches(float hour) const; void write(ESM::ESMWriter& writer, Loading::Listener& progress); bool readRecord(ESM::ESMReader& reader, uint32_t type); void clear(); /* Start of tes3mp addition Make it possible to check whether the local WeatherManager has the ability to create weather changes */ bool getWeatherCreationState(); /* End of tes3mp addition */ /* Start of tes3mp addition Make it possible to enable and disable the local WeatherManager's ability to create weather changes */ void setWeatherCreationState(bool state); /* End of tes3mp addition */ /* Start of tes3mp addition Make it possible to send the current weather in a WorldWeather packet when requested from elsewhere in the code */ void sendWeather(); /* End of tes3mp addition */ private: MWWorld::ESMStore& mStore; MWRender::RenderingManager& mRendering; float mSunriseTime; float mSunsetTime; float mSunriseDuration; float mSunsetDuration; float mSunPreSunsetTime; TimeOfDaySettings mTimeSettings; // fading of night skydome TimeOfDayInterpolator mNightFade; float mHoursBetweenWeatherChanges; float mRainSpeed; // underwater fog not really related to weather, but we handle it here because it's convenient TimeOfDayInterpolator mUnderwaterFog; std::vector mWeatherSettings; MoonModel mMasser; MoonModel mSecunda; float mWindSpeed; float mCurrentWindSpeed; float mNextWindSpeed; bool mIsStorm; bool mPrecipitation; osg::Vec3f mStormDirection; std::string mCurrentRegion; float mTimePassed; bool mFastForward; float mWeatherUpdateTime; float mTransitionFactor; NightDayMode mNightDayMode; int mCurrentWeather; int mNextWeather; int mQueuedWeather; std::map mRegions; MWRender::WeatherResult mResult; MWBase::Sound *mAmbientSound; std::string mPlayingSoundID; /* Start of tes3mp addition Track whether the local WeatherManager should be creating any weather changes by itself; when set to false, only weather changes sent by the server are used */ bool mWeatherCreationState = false; /* End of tes3mp addition */ void addWeather(const std::string& name, float dlFactor, float dlOffset, const std::string& particleEffect = ""); void importRegions(); void regionalWeatherChanged(const std::string& regionID, RegionWeather& region); bool updateWeatherTime(); bool updateWeatherRegion(const std::string& playerRegion); void updateWeatherTransitions(const float elapsedRealSeconds); void forceWeather(const int weatherID); bool inTransition(); void addWeatherTransition(const int weatherID); void calculateWeatherResult(const float gameHour, const float elapsedSeconds, const bool isPaused); void calculateResult(const int weatherID, const float gameHour); void calculateTransitionResult(const float factor, const float gameHour); float calculateWindSpeed(int weatherId, float currentSpeed); }; } #endif // GAME_MWWORLD_WEATHER_H ================================================ FILE: apps/openmw/mwworld/worldimp.cpp ================================================ #include "worldimp.hpp" #include #include #include #include #include #include /* Start of tes3mp addition Include additional headers for multiplayer purposes */ #include #include "../mwmp/Main.hpp" #include "../mwmp/Networking.hpp" #include "../mwmp/LocalPlayer.hpp" #include "../mwmp/PlayerList.hpp" #include "../mwmp/DedicatedPlayer.hpp" #include "../mwmp/LocalActor.hpp" #include "../mwmp/DedicatedActor.hpp" #include "../mwmp/ObjectList.hpp" #include "../mwmp/RecordHelper.hpp" #include "../mwmp/CellController.hpp" #include "../mwmp/MechanicsHelper.hpp" /* End of tes3mp addition */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "../mwbase/environment.hpp" #include "../mwbase/soundmanager.hpp" #include "../mwbase/mechanicsmanager.hpp" #include "../mwbase/windowmanager.hpp" #include "../mwbase/scriptmanager.hpp" #include "../mwmechanics/creaturestats.hpp" #include "../mwmechanics/npcstats.hpp" #include "../mwmechanics/spellcasting.hpp" #include "../mwmechanics/levelledlist.hpp" #include "../mwmechanics/combat.hpp" #include "../mwmechanics/aiavoiddoor.hpp" //Used to tell actors to avoid doors #include "../mwmechanics/summoning.hpp" #include "../mwrender/animation.hpp" #include "../mwrender/npcanimation.hpp" #include "../mwrender/renderingmanager.hpp" #include "../mwrender/camera.hpp" #include "../mwrender/vismask.hpp" #include "../mwscript/globalscripts.hpp" #include "../mwclass/door.hpp" #include "../mwphysics/physicssystem.hpp" #include "../mwphysics/actor.hpp" #include "../mwphysics/collisiontype.hpp" #include "../mwphysics/object.hpp" #include "../mwphysics/constants.hpp" #include "datetimemanager.hpp" #include "player.hpp" #include "manualref.hpp" #include "cellstore.hpp" #include "containerstore.hpp" #include "inventorystore.hpp" #include "actionteleport.hpp" #include "projectilemanager.hpp" #include "weather.hpp" #include "contentloader.hpp" #include "esmloader.hpp" namespace { // Wraps a value to (-PI, PI] void wrap(float& rad) { const float pi = static_cast(osg::PI); if (rad>0) rad = std::fmod(rad+pi, 2.0f*pi)-pi; else rad = std::fmod(rad-pi, 2.0f*pi)+pi; } } namespace MWWorld { struct GameContentLoader : public ContentLoader { GameContentLoader(Loading::Listener& listener) : ContentLoader(listener) { } bool addLoader(const std::string& extension, ContentLoader* loader) { 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()))); if (it != mLoaders.end()) { it->second->load(filepath, index); } else { std::string msg("Cannot load file: "); msg += filepath.string(); throw std::runtime_error(msg.c_str()); } } private: typedef std::map LoadersContainer; LoadersContainer mLoaders; }; void World::adjustSky() { if (mSky && (isCellExterior() || isCellQuasiExterior())) { updateSkyDate(); mRendering->setSkyEnabled(true); } else mRendering->setSkyEnabled(false); } World::World ( osgViewer::Viewer* viewer, osg::ref_ptr rootNode, Resource::ResourceSystem* resourceSystem, SceneUtil::WorkQueue* workQueue, const Files::Collections& fileCollections, const std::vector& contentFiles, const std::vector& groundcoverFiles, ToUTF8::Utf8Encoder* encoder, int activationDistanceOverride, const std::string& startCell, const std::string& startupScript, const std::string& resourcePath, const std::string& userDataPath) : mResourceSystem(resourceSystem), mLocalScripts (mStore), mCells (mStore, mEsm), mSky (true), mGodMode(false), mScriptsEnabled(true), mDiscardMovements(true), mContentFiles (contentFiles), mUserDataPath(userDataPath), mShouldUpdateNavigator(false), mActivationDistanceOverride (activationDistanceOverride), mStartCell(startCell), mDistanceToFacedObject(-1.f), mTeleportEnabled(true), mLevitationEnabled(true), mGoToJail(false), mDaysInPrison(0), mPlayerTraveling(false), mPlayerInJail(false), mSpellPreloadTimer(0.f) { mEsm.resize(contentFiles.size() + groundcoverFiles.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); loadContentFiles(fileCollections, contentFiles, groundcoverFiles, gameContentLoader); listener->loadingOff(); // insert records that may not be present in all versions of MW if (mEsm[0].getFormat() == 0) ensureNeededRecords(); mCurrentDate.reset(new DateTimeManager()); fillGlobalVariables(); mStore.setUp(true); mStore.movePlayerRecord(); mSwimHeightScale = mStore.get().find("fSwimHeightScale")->mValue.getFloat(); mPhysics.reset(new MWPhysics::PhysicsSystem(resourceSystem, rootNode)); if (auto navigatorSettings = DetourNavigator::makeSettingsFromSettingsManager()) { navigatorSettings->mMaxClimb = MWPhysics::sStepSizeUp; navigatorSettings->mMaxSlope = MWPhysics::sMaxSlope; navigatorSettings->mSwimHeightScale = mSwimHeightScale; DetourNavigator::RecastGlobalAllocator::init(); mNavigator.reset(new DetourNavigator::NavigatorImpl(*navigatorSettings)); } else { mNavigator.reset(new DetourNavigator::NavigatorStub()); } mRendering.reset(new MWRender::RenderingManager(viewer, rootNode, resourceSystem, workQueue, resourcePath, *mNavigator)); mProjectileManager.reset(new ProjectileManager(mRendering->getLightRoot(), resourceSystem, mRendering.get(), mPhysics.get())); mRendering->preloadCommonAssets(); mWeatherManager.reset(new MWWorld::WeatherManager(*mRendering, mStore)); mWorldScene.reset(new Scene(*mRendering.get(), mPhysics.get(), *mNavigator)); } void World::fillGlobalVariables() { mGlobalVariables.fill (mStore); mCurrentDate->setup(mGlobalVariables); } void World::startNewGame (bool bypass) { mGoToJail = false; mLevitationEnabled = true; mTeleportEnabled = true; mGodMode = false; mScriptsEnabled = true; mSky = true; // Rebuild player setupPlayer(); renderPlayer(); mRendering->getCamera()->reset(); // we don't want old weather to persist on a new game // Note that if reset later, the initial ChangeWeather that the chargen script calls will be lost. mWeatherManager.reset(); mWeatherManager.reset(new MWWorld::WeatherManager(*mRendering.get(), mStore)); if (!bypass) { // set new game mark mGlobalVariables["chargenstate"].setInteger (1); } else mGlobalVariables["chargenstate"].setInteger (-1); if (bypass && !mStartCell.empty()) { ESM::Position pos; if (findExteriorPosition (mStartCell, pos)) { changeToExteriorCell (pos, true); adjustPosition(getPlayerPtr(), false); } else { findInteriorPosition (mStartCell, pos); changeToInteriorCell (mStartCell, pos, true); } } else { for (int i=0; i<5; ++i) MWBase::Environment::get().getScriptManager()->getGlobalScripts().run(); if (!getPlayerPtr().isInCell()) { ESM::Position pos; /* Start of tes3mp change (major) Spawn at 0, -7 by default */ const int cellSize = Constants::CellSizeInUnits; pos.pos[0] = cellSize / 2; pos.pos[1] = cellSize * -7 + cellSize / 2; pos.pos[2] = 0; pos.rot[0] = 0; pos.rot[1] = 0; pos.rot[2] = 0; mWorldScene->changeToExteriorCell(pos, true); /* End of tes3mp change (major) */ } } if (!bypass) { const std::string& video = Fallback::Map::getString("Movies_New_Game"); if (!video.empty()) MWBase::Environment::get().getWindowManager()->playVideo(video, true); } // enable collision if (!mPhysics->toggleCollisionMode()) mPhysics->toggleCollisionMode(); MWBase::Environment::get().getWindowManager()->updatePlayer(); mCurrentDate->setup(mGlobalVariables); } void World::clear() { mWeatherManager->clear(); mRendering->clear(); mProjectileManager->clear(); mLocalScripts.clear(); mWorldScene->clear(); mStore.clearDynamic(); if (mPlayer) { mPlayer->clear(); mPlayer->setCell(nullptr); mPlayer->getPlayer().getRefData() = RefData(); mPlayer->set(mStore.get().find ("player")); } mCells.clear(); mDoorStates.clear(); mGoToJail = false; mTeleportEnabled = true; mLevitationEnabled = true; mPlayerTraveling = false; mPlayerInJail = false; fillGlobalVariables(); } int World::countSavedGameRecords() const { return mCells.countSavedGameRecords() +mStore.countSavedGameRecords() +mGlobalVariables.countSavedGameRecords() +mProjectileManager->countSavedGameRecords() +1 // player record +1 // weather record +1 // actorId counter +1 // levitation/teleport enabled state +1; // camera } int World::countSavedGameCells() const { return mCells.countSavedGameRecords(); } void World::write (ESM::ESMWriter& writer, Loading::Listener& progress) const { // Active cells could have a dirty fog of war, sync it to the CellStore first for (CellStore* cellstore : mWorldScene->getActiveCells()) { MWBase::Environment::get().getWindowManager()->writeFog(cellstore); } MWMechanics::CreatureStats::writeActorIdCounter(writer); mStore.write (writer, progress); // dynamic Store must be written (and read) before Cells, so that // references to custom made records will be recognized mPlayer->write (writer, progress); mCells.write (writer, progress); mGlobalVariables.write (writer, progress); mWeatherManager->write (writer, progress); mProjectileManager->write (writer, progress); writer.startRecord(ESM::REC_ENAB); writer.writeHNT("TELE", mTeleportEnabled); writer.writeHNT("LEVT", mLevitationEnabled); writer.endRecord(ESM::REC_ENAB); writer.startRecord(ESM::REC_CAM_); writer.writeHNT("FIRS", isFirstPerson()); writer.endRecord(ESM::REC_CAM_); } void World::readRecord (ESM::ESMReader& reader, uint32_t type, const std::map& contentFileMap) { switch (type) { case ESM::REC_ACTC: MWMechanics::CreatureStats::readActorIdCounter(reader); return; case ESM::REC_ENAB: reader.getHNT(mTeleportEnabled, "TELE"); reader.getHNT(mLevitationEnabled, "LEVT"); return; case ESM::REC_PLAY: mStore.checkPlayer(); mPlayer->readRecord(reader, type); if (getPlayerPtr().isInCell()) { if (getPlayerPtr().getCell()->isExterior()) mWorldScene->preloadTerrain(getPlayerPtr().getRefData().getPosition().asVec3()); mWorldScene->preloadCell(getPlayerPtr().getCell(), true); } break; default: if (!mStore.readRecord (reader, type) && !mGlobalVariables.readRecord (reader, type) && !mWeatherManager->readRecord (reader, type) && !mCells.readRecord (reader, type, contentFileMap) && !mProjectileManager->readRecord (reader, type) ) { throw std::runtime_error ("unknown record in saved game"); } break; } } void World::ensureNeededRecords() { std::map gmst; // Companion (tribunal) gmst["sCompanionShare"] = ESM::Variant("Companion Share"); gmst["sCompanionWarningMessage"] = ESM::Variant("Warning message"); gmst["sCompanionWarningButtonOne"] = ESM::Variant("Button 1"); gmst["sCompanionWarningButtonTwo"] = ESM::Variant("Button 2"); gmst["sProfitValue"] = ESM::Variant("Profit Value"); gmst["sTeleportDisabled"] = ESM::Variant("Teleport disabled"); gmst["sLevitateDisabled"] = ESM::Variant("Levitate disabled"); // Missing in unpatched MW 1.0 gmst["sDifficulty"] = ESM::Variant("Difficulty"); gmst["fDifficultyMult"] = ESM::Variant(5.f); gmst["sAuto_Run"] = ESM::Variant("Auto Run"); gmst["sServiceRefusal"] = ESM::Variant("Service Refusal"); gmst["sNeedOneSkill"] = ESM::Variant("Need one skill"); gmst["sNeedTwoSkills"] = ESM::Variant("Need two skills"); gmst["sEasy"] = ESM::Variant("Easy"); gmst["sHard"] = ESM::Variant("Hard"); gmst["sDeleteNote"] = ESM::Variant("Delete Note"); gmst["sEditNote"] = ESM::Variant("Edit Note"); gmst["sAdmireSuccess"] = ESM::Variant("Admire Success"); gmst["sAdmireFail"] = ESM::Variant("Admire Fail"); gmst["sIntimidateSuccess"] = ESM::Variant("Intimidate Success"); gmst["sIntimidateFail"] = ESM::Variant("Intimidate Fail"); gmst["sTauntSuccess"] = ESM::Variant("Taunt Success"); gmst["sTauntFail"] = ESM::Variant("Taunt Fail"); gmst["sBribeSuccess"] = ESM::Variant("Bribe Success"); gmst["sBribeFail"] = ESM::Variant("Bribe Fail"); gmst["fNPCHealthBarTime"] = ESM::Variant(5.f); gmst["fNPCHealthBarFade"] = ESM::Variant(1.f); gmst["fFleeDistance"] = ESM::Variant(3000.f); gmst["sMaxSale"] = ESM::Variant("Max Sale"); gmst["sAnd"] = ESM::Variant("and"); // Werewolf (BM) gmst["fWereWolfRunMult"] = ESM::Variant(1.3f); gmst["fWereWolfSilverWeaponDamageMult"] = ESM::Variant(2.f); gmst["iWerewolfFightMod"] = ESM::Variant(100); gmst["iWereWolfFleeMod"] = ESM::Variant(100); gmst["iWereWolfLevelToAttack"] = ESM::Variant(20); gmst["iWereWolfBounty"] = ESM::Variant(1000); gmst["fCombatDistanceWerewolfMod"] = ESM::Variant(0.3f); for (const auto ¶ms : gmst) { if (!mStore.get().search(params.first)) { ESM::GameSetting record; record.mId = params.first; record.mValue = params.second; mStore.insertStatic(record); } } std::map globals; // vanilla Morrowind does not define dayspassed. globals["dayspassed"] = ESM::Variant(1); // but the addons start counting at 1 :( globals["werewolfclawmult"] = ESM::Variant(25.f); globals["pcknownwerewolf"] = ESM::Variant(0); // following should exist in all versions of MW, but not necessarily in TCs globals["gamehour"] = ESM::Variant(0.f); globals["timescale"] = ESM::Variant(30.f); globals["day"] = ESM::Variant(1); globals["month"] = ESM::Variant(1); globals["year"] = ESM::Variant(1); globals["pcrace"] = ESM::Variant(0); globals["pchascrimegold"] = ESM::Variant(0); globals["pchasgolddiscount"] = ESM::Variant(0); globals["crimegolddiscount"] = ESM::Variant(0); globals["crimegoldturnin"] = ESM::Variant(0); globals["pchasturnin"] = ESM::Variant(0); for (const auto ¶ms : globals) { if (!mStore.get().search(params.first)) { ESM::Global record; record.mId = params.first; record.mValue = params.second; mStore.insertStatic(record); } } std::map statics; // Total conversions from SureAI lack marker records statics["divinemarker"] = "marker_divine.nif"; statics["doormarker"] = "marker_arrow.nif"; statics["northmarker"] = "marker_north.nif"; statics["templemarker"] = "marker_temple.nif"; statics["travelmarker"] = "marker_travel.nif"; for (const auto ¶ms : statics) { if (!mStore.get().search(params.first)) { ESM::Static record; record.mId = params.first; record.mModel = params.second; mStore.insertStatic(record); } } std::map doors; doors["prisonmarker"] = "marker_prison.nif"; for (const auto ¶ms : doors) { if (!mStore.get().search(params.first)) { ESM::Door record; record.mId = params.first; record.mModel = params.second; mStore.insertStatic(record); } } } World::~World() { // Must be cleared before mRendering is destroyed mProjectileManager->clear(); } const ESM::Cell *World::getExterior (const std::string& cellName) const { // first try named cells const ESM::Cell *cell = mStore.get().searchExtByName (cellName); if (cell) return cell; // didn't work -> now check for regions for (const ESM::Region ®ion : mStore.get()) { if (Misc::StringUtils::ciEqual(cellName, region.mName)) { return mStore.get().searchExtByRegion(region.mId); } } return nullptr; } CellStore *World::getExterior (int x, int y) { return mCells.getExterior (x, y); } CellStore *World::getInterior (const std::string& name) { return mCells.getInterior (name); } CellStore *World::getCell (const ESM::CellId& id) { if (id.mPaged) return getExterior (id.mIndex.mX, id.mIndex.mY); else return getInterior (id.mWorldspace); } void World::testExteriorCells() { mWorldScene->testExteriorCells(); } void World::testInteriorCells() { mWorldScene->testInteriorCells(); } void World::useDeathCamera() { if(mRendering->getCamera()->isVanityOrPreviewModeEnabled() ) { mRendering->getCamera()->togglePreviewMode(false); mRendering->getCamera()->toggleVanityMode(false); } if(mRendering->getCamera()->isFirstPerson()) mRendering->getCamera()->toggleViewMode(true); } MWWorld::Player& World::getPlayer() { return *mPlayer; } const MWWorld::ESMStore& World::getStore() const { return mStore; } /* Start of tes3mp addition Make it possible to get the World's ESMStore as a non-const */ MWWorld::ESMStore& World::getModifiableStore() { return mStore; } /* End of tes3mp addition */ std::vector& World::getEsmReader() { return mEsm; } LocalScripts& World::getLocalScripts() { return mLocalScripts; } bool World::hasCellChanged() const { return mWorldScene->hasCellChanged(); } /* Start of tes3mp addition Make it possible to check whether global variables exist and to create new ones */ bool World::hasGlobal(const std::string& name) { return mGlobalVariables.hasRecord(name); } void World::createGlobal(const std::string& name, ESM::VarType varType) { ESM::Global global; global.mId = name; global.mValue.setType(varType); mGlobalVariables.addRecord(global); } /* End of tes3mp addition */ void World::setGlobalInt (const std::string& name, int value) { bool dateUpdated = mCurrentDate->updateGlobalInt(name, value); if (dateUpdated) updateSkyDate(); mGlobalVariables[name].setInteger (value); } void World::setGlobalFloat (const std::string& name, float value) { bool dateUpdated = mCurrentDate->updateGlobalFloat(name, value); if (dateUpdated) updateSkyDate(); mGlobalVariables[name].setFloat(value); } int World::getGlobalInt (const std::string& name) const { return mGlobalVariables[name].getInteger(); } float World::getGlobalFloat (const std::string& name) const { return mGlobalVariables[name].getFloat(); } char World::getGlobalVariableType (const std::string& name) const { return mGlobalVariables.getType (name); } std::string World::getMonthName (int month) const { return mCurrentDate->getMonthName(month); } std::string World::getCellName (const MWWorld::CellStore *cell) const { if (!cell) cell = mWorldScene->getCurrentCell(); return getCellName(cell->getCell()); } std::string World::getCellName(const ESM::Cell* cell) const { if (cell) { if (!cell->isExterior() || !cell->mName.empty()) return cell->mName; if (const ESM::Region* region = mStore.get().search (cell->mRegion)) return region->mName; } return mStore.get().find ("sDefaultCellname")->mValue.getString(); } void World::removeRefScript (MWWorld::RefData *ref) { mLocalScripts.remove (ref); } Ptr World::searchPtr (const std::string& name, bool activeOnly, bool searchInContainers) { Ptr ret; // the player is always in an active cell. if (name=="player") { return mPlayer->getPlayer(); } std::string lowerCaseName = Misc::StringUtils::lowerCase(name); for (CellStore* cellstore : mWorldScene->getActiveCells()) { // TODO: caching still doesn't work efficiently here (only works for the one CellStore that the reference is in) Ptr ptr = mCells.getPtr (lowerCaseName, *cellstore, false); if (!ptr.isEmpty()) return ptr; } if (!activeOnly) { ret = mCells.getPtr (lowerCaseName); if (!ret.isEmpty()) return ret; } if (searchInContainers) { for (CellStore* cellstore : mWorldScene->getActiveCells()) { Ptr ptr = cellstore->searchInContainer(lowerCaseName); if (!ptr.isEmpty()) return ptr; } } Ptr ptr = mPlayer->getPlayer().getClass() .getContainerStore(mPlayer->getPlayer()).search(lowerCaseName); return ptr; } Ptr World::getPtr (const std::string& name, bool activeOnly) { Ptr ret = searchPtr(name, activeOnly); if (!ret.isEmpty()) return ret; std::string error = "failed to find an instance of object '" + name + "'"; if (activeOnly) error += " in active cells"; throw std::runtime_error(error); } Ptr World::searchPtrViaActorId (int actorId) { // The player is not registered in any CellStore so must be checked manually if (actorId == getPlayerPtr().getClass().getCreatureStats(getPlayerPtr()).getActorId()) return getPlayerPtr(); /* Start of tes3mp addition Make it possible to find dedicated players here as well */ else { mwmp::DedicatedPlayer* dedicatedPlayer = mwmp::PlayerList::getPlayer(actorId); if (dedicatedPlayer != nullptr) { return dedicatedPlayer->getPtr(); } } /* End of tes3mp addition */ // Now search cells return mWorldScene->searchPtrViaActorId (actorId); } Ptr World::searchPtrViaRefNum (const std::string& id, const ESM::RefNum& refNum) { return mCells.getPtr (id, refNum); } /* Start of tes3mp addition Make it possible to find a Ptr in any active cell based on its refNum and mpNum */ Ptr World::searchPtrViaUniqueIndex(int refNum, int mpNum) { for (Scene::CellStoreCollection::const_iterator iter(mWorldScene->getActiveCells().begin()); iter != mWorldScene->getActiveCells().end(); ++iter) { CellStore* cellStore = *iter; MWWorld::Ptr ptrFound = cellStore->searchExact(refNum, mpNum); if (ptrFound) return ptrFound; } return nullptr; } /* End of tes3mp addition */ /* Start of tes3mp addition Make it possible to update all Ptrs in active cells that have a certain refId */ void World::updatePtrsWithRefId(std::string refId) { for (Scene::CellStoreCollection::const_iterator iter(mWorldScene->getActiveCells().begin()); iter != mWorldScene->getActiveCells().end(); ++iter) { CellStore* cellStore = *iter; for (auto &mergedRef : cellStore->getMergedRefs()) { if (Misc::StringUtils::ciEqual(refId, mergedRef->mRef.getRefId())) { MWWorld::Ptr ptr(mergedRef, cellStore); const ESM::Position* position = &ptr.getRefData().getPosition(); const unsigned int refNum = ptr.getCellRef().getRefNum().mIndex; const unsigned int mpNum = ptr.getCellRef().getMpNum(); deleteObject(ptr); ptr.getCellRef().unsetRefNum(); ptr.getCellRef().setMpNum(0); MWWorld::ManualRef* reference = new MWWorld::ManualRef(getStore(), refId, 1); MWWorld::Ptr newPtr = placeObject(reference->getPtr(), cellStore, *position); newPtr.getCellRef().setRefNum(refNum); newPtr.getCellRef().setMpNum(mpNum); // Update Ptrs for LocalActors and DedicatedActors if (newPtr.getClass().isActor()) { if (mwmp::Main::get().getCellController()->isLocalActor(refNum, mpNum)) mwmp::Main::get().getCellController()->getLocalActor(refNum, mpNum)->setPtr(newPtr); else if (mwmp::Main::get().getCellController()->isDedicatedActor(refNum, mpNum)) mwmp::Main::get().getCellController()->getDedicatedActor(refNum, mpNum)->setPtr(newPtr); } } } } } /* End of tes3mp addition */ struct FindContainerVisitor { ConstPtr mContainedPtr; Ptr mResult; FindContainerVisitor(const ConstPtr& containedPtr) : mContainedPtr(containedPtr) {} bool operator() (Ptr ptr) { if (mContainedPtr.getContainerStore() == &ptr.getClass().getContainerStore(ptr)) { mResult = ptr; return false; } return true; } }; Ptr World::findContainer(const ConstPtr& ptr) { if (ptr.isInCell()) return Ptr(); Ptr player = getPlayerPtr(); if (ptr.getContainerStore() == &player.getClass().getContainerStore(player)) return player; for (CellStore* cellstore : mWorldScene->getActiveCells()) { FindContainerVisitor visitor(ptr); cellstore->forEachType(visitor); if (visitor.mResult.isEmpty()) cellstore->forEachType(visitor); if (visitor.mResult.isEmpty()) cellstore->forEachType(visitor); if (!visitor.mResult.isEmpty()) return visitor.mResult; } return Ptr(); } void World::addContainerScripts(const Ptr& reference, CellStore * cell) { if( reference.getTypeName()==typeid (ESM::Container).name() || reference.getTypeName()==typeid (ESM::NPC).name() || reference.getTypeName()==typeid (ESM::Creature).name()) { MWWorld::ContainerStore& container = reference.getClass().getContainerStore(reference); for(MWWorld::ContainerStoreIterator it = container.begin(); it != container.end(); ++it) { std::string script = it->getClass().getScript(*it); if(script != "") { MWWorld::Ptr item = *it; item.mCell = cell; mLocalScripts.add (script, item); } } } } void World::enable (const Ptr& reference) { // enable is a no-op for items in containers if (!reference.isInCell()) return; if (!reference.getRefData().isEnabled()) { reference.getRefData().enable(); if(mWorldScene->getActiveCells().find (reference.getCell()) != mWorldScene->getActiveCells().end() && reference.getRefData().getCount()) mWorldScene->addObjectToScene (reference); if (reference.getCellRef().getRefNum().hasContentFile()) { int type = mStore.find(Misc::StringUtils::lowerCase(reference.getCellRef().getRefId())); if (mRendering->pagingEnableObject(type, reference, true)) mWorldScene->reloadTerrain(); } } } void World::removeContainerScripts(const Ptr& reference) { if( reference.getTypeName()==typeid (ESM::Container).name() || reference.getTypeName()==typeid (ESM::NPC).name() || reference.getTypeName()==typeid (ESM::Creature).name()) { MWWorld::ContainerStore& container = reference.getClass().getContainerStore(reference); for(MWWorld::ContainerStoreIterator it = container.begin(); it != container.end(); ++it) { std::string script = it->getClass().getScript(*it); if(script != "") { MWWorld::Ptr item = *it; mLocalScripts.remove (item); } } } } void World::disable (const Ptr& reference) { if (!reference.getRefData().isEnabled()) return; // disable is a no-op for items in containers if (!reference.isInCell()) return; if (reference == getPlayerPtr()) throw std::runtime_error("can not disable player object"); reference.getRefData().disable(); if (reference.getCellRef().getRefNum().hasContentFile()) { int type = mStore.find(Misc::StringUtils::lowerCase(reference.getCellRef().getRefId())); if (mRendering->pagingEnableObject(type, reference, false)) mWorldScene->reloadTerrain(); } if(mWorldScene->getActiveCells().find (reference.getCell())!=mWorldScene->getActiveCells().end() && reference.getRefData().getCount()) mWorldScene->removeObjectFromScene (reference); } void World::advanceTime (double hours, bool incremental) { if (!incremental) { // When we fast-forward time, we should recharge magic items // in all loaded cells, using game world time float duration = hours * 3600; const float timeScaleFactor = getTimeScaleFactor(); if (timeScaleFactor != 0.0f) duration /= timeScaleFactor; rechargeItems(duration, false); } mWeatherManager->advanceTime (hours, incremental); mCurrentDate->advanceTime(hours, mGlobalVariables); updateSkyDate(); if (!incremental) { mRendering->notifyWorldSpaceChanged(); mProjectileManager->clear(); mDiscardMovements = true; } } float World::getTimeScaleFactor() const { return mCurrentDate->getTimeScaleFactor(); } TimeStamp World::getTimeStamp() const { return mCurrentDate->getTimeStamp(); } ESM::EpochTimeStamp World::getEpochTimeStamp() const { return mCurrentDate->getEpochTimeStamp(); } bool World::toggleSky() { mSky = !mSky; mRendering->setSkyEnabled(mSky); return mSky; } int World::getMasserPhase() const { return mRendering->skyGetMasserPhase(); } int World::getSecundaPhase() const { return mRendering->skyGetSecundaPhase(); } void World::setMoonColour (bool red) { mRendering->skySetMoonColour (red); } void World::changeToInteriorCell (const std::string& cellName, const ESM::Position& position, bool adjustPlayerPos, bool changeEvent) { mPhysics->clearQueuedMovement(); mDiscardMovements = true; if (changeEvent && mCurrentWorldSpace != cellName) { // changed worldspace mProjectileManager->clear(); mRendering->notifyWorldSpaceChanged(); mCurrentWorldSpace = cellName; } removeContainerScripts(getPlayerPtr()); mWorldScene->changeToInteriorCell(cellName, position, adjustPlayerPos, changeEvent); addContainerScripts(getPlayerPtr(), getPlayerPtr().getCell()); mRendering->getCamera()->instantTransition(); } void World::changeToExteriorCell (const ESM::Position& position, bool adjustPlayerPos, bool changeEvent) { mPhysics->clearQueuedMovement(); mDiscardMovements = true; if (changeEvent && mCurrentWorldSpace != ESM::CellId::sDefaultWorldspace) { // changed worldspace mProjectileManager->clear(); mRendering->notifyWorldSpaceChanged(); } removeContainerScripts(getPlayerPtr()); mWorldScene->changeToExteriorCell(position, adjustPlayerPos, changeEvent); addContainerScripts(getPlayerPtr(), getPlayerPtr().getCell()); mRendering->getCamera()->instantTransition(); } void World::changeToCell (const ESM::CellId& cellId, const ESM::Position& position, bool adjustPlayerPos, bool changeEvent) { if (!changeEvent) mCurrentWorldSpace = cellId.mWorldspace; if (cellId.mPaged) changeToExteriorCell (position, adjustPlayerPos, changeEvent); else changeToInteriorCell (cellId.mWorldspace, position, adjustPlayerPos, changeEvent); mCurrentDate->setup(mGlobalVariables); } void World::markCellAsUnchanged() { return mWorldScene->markCellAsUnchanged(); } float World::getMaxActivationDistance () { if (mActivationDistanceOverride >= 0) return static_cast(mActivationDistanceOverride); static const int iMaxActivateDist = mStore.get().find("iMaxActivateDist")->mValue.getInteger(); return static_cast(iMaxActivateDist); } MWWorld::Ptr World::getFacedObject() { MWWorld::Ptr facedObject; if (MWBase::Environment::get().getWindowManager()->isGuiMode() && MWBase::Environment::get().getWindowManager()->isConsoleMode()) facedObject = getFacedObject(getMaxActivationDistance() * 50, false); else { float activationDistance = getActivationDistancePlusTelekinesis(); facedObject = getFacedObject(activationDistance, true); if (!facedObject.isEmpty() && !facedObject.getClass().allowTelekinesis(facedObject) && mDistanceToFacedObject > getMaxActivationDistance() && !MWBase::Environment::get().getWindowManager()->isGuiMode()) return nullptr; } return facedObject; } float World::getDistanceToFacedObject() { return mDistanceToFacedObject; } osg::Matrixf World::getActorHeadTransform(const MWWorld::ConstPtr& actor) const { const MWRender::Animation *anim = mRendering->getAnimation(actor); if(anim) { const osg::Node *node = anim->getNode("Head"); if(!node) node = anim->getNode("Bip01 Head"); if(node) { osg::NodePathList nodepaths = node->getParentalNodePaths(); if(!nodepaths.empty()) return osg::computeLocalToWorld(nodepaths[0]); } } return osg::Matrixf::translate(actor.getRefData().getPosition().asVec3()); } std::pair World::getHitContact(const MWWorld::ConstPtr &ptr, float distance, std::vector &targets) { const ESM::Position &posdata = ptr.getRefData().getPosition(); osg::Quat rot = osg::Quat(posdata.rot[0], osg::Vec3f(-1,0,0)) * osg::Quat(posdata.rot[2], osg::Vec3f(0,0,-1)); osg::Vec3f halfExtents = mPhysics->getHalfExtents(ptr); // the origin of hitbox is an actor's front, not center distance += halfExtents.y(); // special cased for better aiming with the camera // if we do not hit anything, will use the default approach as fallback if (ptr == getPlayerPtr()) { osg::Vec3f pos = getActorHeadTransform(ptr).getTrans(); std::pair result = mPhysics->getHitContact(ptr, pos, rot, distance, targets); if(!result.first.isEmpty()) return std::make_pair(result.first, result.second); } osg::Vec3f pos = ptr.getRefData().getPosition().asVec3(); // general case, compatible with all types of different creatures // note: we intentionally do *not* use the collision box offset here, this is required to make // some flying creatures work that have their collision box offset in the air pos.z() += halfExtents.z(); std::pair result = mPhysics->getHitContact(ptr, pos, rot, distance, targets); if(result.first.isEmpty()) return std::make_pair(MWWorld::Ptr(), osg::Vec3f()); return std::make_pair(result.first, result.second); } void World::deleteObject (const Ptr& ptr) { if (!ptr.getRefData().isDeleted() && ptr.getContainerStore() == nullptr) { if (ptr == getPlayerPtr()) throw std::runtime_error("can not delete player object"); ptr.getRefData().setCount(0); if (ptr.isInCell() && mWorldScene->getActiveCells().find(ptr.getCell()) != mWorldScene->getActiveCells().end() && ptr.getRefData().isEnabled()) { mWorldScene->removeObjectFromScene (ptr); mLocalScripts.remove (ptr); removeContainerScripts (ptr); } } } void World::undeleteObject(const Ptr& ptr) { if (!ptr.getCellRef().hasContentFile()) return; if (ptr.getRefData().isDeleted()) { ptr.getRefData().setCount(1); if (mWorldScene->getActiveCells().find(ptr.getCell()) != mWorldScene->getActiveCells().end() && ptr.getRefData().isEnabled()) { mWorldScene->addObjectToScene(ptr); std::string script = ptr.getClass().getScript(ptr); if (!script.empty()) mLocalScripts.add(script, ptr); addContainerScripts(ptr, ptr.getCell()); } } } MWWorld::Ptr World::moveObject(const Ptr &ptr, CellStore* newCell, float x, float y, float z, bool movePhysics) { /* Start of tes3mp addition If we choose to deny this move because it's part of an unapproved cell change, we should also revert the Ptr back to its original coordinates, so keep track of them */ ESM::Position originalPos = ptr.getRefData().getPosition(); /* End of tes3mp addition */ ESM::Position pos = ptr.getRefData().getPosition(); pos.pos[0] = x; pos.pos[1] = y; pos.pos[2] = z; ptr.getRefData().setPosition(pos); osg::Vec3f vec(x, y, z); CellStore *currCell = ptr.isInCell() ? ptr.getCell() : nullptr; // currCell == nullptr should only happen for player, during initial startup bool isPlayer = ptr == mPlayer->getPlayer(); bool haveToMove = isPlayer || (currCell && mWorldScene->isCellActive(*currCell)); MWWorld::Ptr newPtr = ptr; if (!isPlayer && !currCell) throw std::runtime_error("Can not move actor \"" + ptr.getCellRef().getRefId() + "\" to another cell: current cell is nullptr"); if (!newCell) throw std::runtime_error("Can not move actor \"" + ptr.getCellRef().getRefId() + "\" to another cell: new cell is nullptr"); if (currCell != newCell) { /* Start of tes3mp addition Check if a DedicatedPlayer or DedicatedActor's new Ptr cell is the same as their packet cell, and deny the Ptr's movement and cell change if it is not */ if (mwmp::PlayerList::isDedicatedPlayer(ptr) && !mwmp::Main::get().getCellController()->isSameCell(mwmp::PlayerList::getPlayer(ptr)->cell, *newCell->getCell())) { ptr.getRefData().setPosition(originalPos); return ptr; } else if (mwmp::Main::get().getCellController()->isDedicatedActor(ptr) && !mwmp::Main::get().getCellController()->isSameCell(mwmp::Main::get().getCellController()->getDedicatedActor(ptr)->cell, *newCell->getCell())) { ptr.getRefData().setPosition(originalPos); return ptr; } /* End of tes3mp addition */ removeContainerScripts(ptr); if (isPlayer) { if (!newCell->isExterior()) { changeToInteriorCell(Misc::StringUtils::lowerCase(newCell->getCell()->mName), pos, false); removeContainerScripts(getPlayerPtr()); } else { if (mWorldScene->isCellActive(*newCell)) mWorldScene->changePlayerCell(newCell, pos, false); else mWorldScene->changeToExteriorCell(pos, false); } addContainerScripts (getPlayerPtr(), newCell); newPtr = getPlayerPtr(); } else { bool currCellActive = mWorldScene->isCellActive(*currCell); bool newCellActive = mWorldScene->isCellActive(*newCell); if (!currCellActive && newCellActive) { newPtr = currCell->moveTo(ptr, newCell); mWorldScene->addObjectToScene(newPtr); std::string script = newPtr.getClass().getScript(newPtr); if (!script.empty()) { mLocalScripts.add(script, newPtr); } addContainerScripts(newPtr, newCell); } else if (!newCellActive && currCellActive) { mWorldScene->removeObjectFromScene(ptr); mLocalScripts.remove(ptr); removeContainerScripts (ptr); haveToMove = false; newPtr = currCell->moveTo(ptr, newCell); newPtr.getRefData().setBaseNode(nullptr); } else if (!currCellActive && !newCellActive) newPtr = currCell->moveTo(ptr, newCell); else // both cells active { newPtr = currCell->moveTo(ptr, newCell); mRendering->updatePtr(ptr, newPtr); MWBase::Environment::get().getSoundManager()->updatePtr (ptr, newPtr); mPhysics->updatePtr(ptr, newPtr); MWBase::MechanicsManager *mechMgr = MWBase::Environment::get().getMechanicsManager(); mechMgr->updateCell(ptr, newPtr); std::string script = ptr.getClass().getScript(ptr); if (!script.empty()) { mLocalScripts.remove(ptr); removeContainerScripts (ptr); mLocalScripts.add(script, newPtr); addContainerScripts (newPtr, newCell); } } /* Start of tes3mp addition Update the Ptrs of LocalActors, DedicatedPlayers and DedicatedActors */ if (mwmp::Main::get().getCellController()->isLocalActor(ptr)) mwmp::Main::get().getCellController()->getLocalActor(ptr)->setPtr(newPtr); else if (mwmp::Main::get().getCellController()->isDedicatedActor(ptr)) mwmp::Main::get().getCellController()->getDedicatedActor(ptr)->setPtr(newPtr); else if (mwmp::PlayerList::isDedicatedPlayer(ptr)) mwmp::PlayerList::getPlayer(ptr)->setPtr(newPtr); /* End of tes3mp addition */ } MWBase::Environment::get().getWindowManager()->updateConsoleObjectPtr(ptr, newPtr); MWBase::Environment::get().getScriptManager()->getGlobalScripts().updatePtrs(ptr, newPtr); } if (haveToMove && newPtr.getRefData().getBaseNode()) { mWorldScene->updateObjectPosition(newPtr, vec, movePhysics); if (movePhysics) { if (const auto object = mPhysics->getObject(ptr)) updateNavigatorObject(*object); } } if (isPlayer) mWorldScene->playerMoved(vec); else { mRendering->pagingBlacklistObject(mStore.find(ptr.getCellRef().getRefId()), ptr); mWorldScene->removeFromPagedRefs(newPtr); } return newPtr; } MWWorld::Ptr World::moveObject (const Ptr& ptr, float x, float y, float z, bool movePhysics, bool moveToActive) { int cellX, cellY; positionToIndex(x, y, cellX, cellY); CellStore* cell = ptr.getCell(); CellStore* newCell = getExterior(cellX, cellY); bool isCellActive = getPlayerPtr().isInCell() && getPlayerPtr().getCell()->isExterior() && mWorldScene->isCellActive(*newCell); if (cell->isExterior() || (moveToActive && isCellActive && ptr.getClass().isActor())) cell = newCell; return moveObject(ptr, cell, x, y, z, movePhysics); } MWWorld::Ptr World::moveObjectBy(const Ptr& ptr, osg::Vec3f vec, bool moveToActive, bool ignoreCollisions) { auto* actor = mPhysics->getActor(ptr); osg::Vec3f newpos = ptr.getRefData().getPosition().asVec3() + vec; if (actor) actor->adjustPosition(vec, ignoreCollisions); if (ptr.getClass().isActor()) return moveObject(ptr, newpos.x(), newpos.y(), newpos.z(), false, moveToActive && ptr != getPlayerPtr()); return moveObject(ptr, newpos.x(), newpos.y(), newpos.z()); } void World::scaleObject (const Ptr& ptr, float scale) { 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); } if(ptr.getRefData().getBaseNode() != nullptr) mWorldScene->updateObjectScale(ptr); if (mPhysics->getActor(ptr)) mNavigator->addAgent(getPathfindingHalfExtents(ptr)); else if (const auto object = mPhysics->getObject(ptr)) updateNavigatorObject(*object); } void World::rotateObjectImp(const Ptr& ptr, const osg::Vec3f& rot, MWBase::RotationFlags flags) { const float pi = static_cast(osg::PI); ESM::Position pos = ptr.getRefData().getPosition(); float *objRot = pos.rot; if (flags & MWBase::RotationFlag_adjust) { objRot[0] += rot.x(); objRot[1] += rot.y(); objRot[2] += rot.z(); } else { objRot[0] = rot.x(); objRot[1] = rot.y(); objRot[2] = rot.z(); } if(ptr.getClass().isActor()) { /* HACK? Actors shouldn't really be rotating around X (or Y), but * currently it's done so for rotating the camera, which needs * clamping. */ const float half_pi = pi/2.f; if(objRot[0] < -half_pi) objRot[0] = -half_pi; else if(objRot[0] > half_pi) objRot[0] = half_pi; wrap(objRot[1]); wrap(objRot[2]); } ptr.getRefData().setPosition(pos); mRendering->pagingBlacklistObject(mStore.find(ptr.getCellRef().getRefId()), ptr); mWorldScene->removeFromPagedRefs(ptr); if(ptr.getRefData().getBaseNode() != nullptr) { const auto order = flags & MWBase::RotationFlag_inverseOrder ? RotationOrder::inverse : RotationOrder::direct; mWorldScene->updateObjectRotation(ptr, order); if (const auto object = mPhysics->getObject(ptr)) updateNavigatorObject(*object); } } void World::adjustPosition(const Ptr &ptr, bool force) { if (ptr.isEmpty()) { Log(Debug::Warning) << "Unable to adjust position for empty object"; return; } osg::Vec3f pos (ptr.getRefData().getPosition().asVec3()); if(!ptr.getRefData().getBaseNode()) { // will be adjusted when Ptr's cell becomes active return; } if (!ptr.isInCell()) { Log(Debug::Warning) << "Unable to adjust position for object '" << ptr.getCellRef().getRefId() << "' - it has no cell"; return; } const float terrainHeight = ptr.getCell()->isExterior() ? getTerrainHeightAt(pos) : -std::numeric_limits::max(); pos.z() = std::max(pos.z(), terrainHeight) + 20; // place slightly above terrain. will snap down to ground with code below // We still should trace down dead persistent actors - they do not use the "swimdeath" animation. bool swims = ptr.getClass().isActor() && isSwimming(ptr) && !(ptr.getClass().isPersistent(ptr) && ptr.getClass().getCreatureStats(ptr).isDeathAnimationFinished()); if (force || !ptr.getClass().isActor() || (!isFlying(ptr) && !swims && isActorCollisionEnabled(ptr))) { osg::Vec3f traced = mPhysics->traceDown(ptr, pos, Constants::CellSizeInUnits); pos.z() = std::min(pos.z(), traced.z()); } moveObject(ptr, ptr.getCell(), pos.x(), pos.y(), pos.z()); } void World::fixPosition() { const MWWorld::Ptr actor = getPlayerPtr(); const float distance = 128.f; ESM::Position esmPos = actor.getRefData().getPosition(); osg::Quat orientation(esmPos.rot[2], osg::Vec3f(0,0,-1)); osg::Vec3f pos (esmPos.asVec3()); int direction = 0; int fallbackDirections[4] = {direction, (direction+3)%4, (direction+2)%4, (direction+1)%4}; osg::Vec3f targetPos = pos; for (int i=0; i<4; ++i) { direction = fallbackDirections[i]; if (direction == 0) targetPos = pos + (orientation * osg::Vec3f(0,1,0)) * distance; else if(direction == 1) targetPos = pos - (orientation * osg::Vec3f(0,1,0)) * distance; else if(direction == 2) targetPos = pos - (orientation * osg::Vec3f(1,0,0)) * distance; else if(direction == 3) targetPos = pos + (orientation * osg::Vec3f(1,0,0)) * distance; // destination is free if (!castRay(pos.x(), pos.y(), pos.z(), targetPos.x(), targetPos.y(), targetPos.z())) break; } targetPos.z() += distance / 2.f; // move up a bit to get out from geometry, will snap down later osg::Vec3f traced = mPhysics->traceDown(actor, targetPos, Constants::CellSizeInUnits); if (traced != pos) { esmPos.pos[0] = traced.x(); esmPos.pos[1] = traced.y(); esmPos.pos[2] = traced.z(); MWWorld::ActionTeleport(actor.getCell()->isExterior() ? "" : actor.getCell()->getCell()->mName, esmPos, false).execute(actor); } } void World::rotateObject (const Ptr& ptr, float x, float y, float z, MWBase::RotationFlags flags) { rotateObjectImp(ptr, osg::Vec3f(x, y, z), flags); } void World::rotateWorldObject (const Ptr& ptr, osg::Quat rotate) { if(ptr.getRefData().getBaseNode() != nullptr) { mRendering->pagingBlacklistObject(mStore.find(ptr.getCellRef().getRefId()), ptr); mWorldScene->removeFromPagedRefs(ptr); mRendering->rotateObject(ptr, rotate); mPhysics->updateRotation(ptr); if (const auto object = mPhysics->getObject(ptr)) updateNavigatorObject(*object); } } MWWorld::Ptr World::placeObject(const MWWorld::ConstPtr& ptr, MWWorld::CellStore* cell, ESM::Position pos) { return copyObjectToCell(ptr,cell,pos,ptr.getRefData().getCount(),false); } MWWorld::Ptr World::safePlaceObject(const ConstPtr &ptr, const ConstPtr &referenceObject, MWWorld::CellStore* referenceCell, int direction, float distance) { ESM::Position ipos = referenceObject.getRefData().getPosition(); osg::Vec3f pos(ipos.asVec3()); osg::Quat orientation(ipos.rot[2], osg::Vec3f(0,0,-1)); int fallbackDirections[4] = {direction, (direction+3)%4, (direction+2)%4, (direction+1)%4}; osg::Vec3f spawnPoint = pos; for (int i=0; i<4; ++i) { direction = fallbackDirections[i]; if (direction == 0) spawnPoint = pos + (orientation * osg::Vec3f(0,1,0)) * distance; else if(direction == 1) spawnPoint = pos - (orientation * osg::Vec3f(0,1,0)) * distance; else if(direction == 2) spawnPoint = pos - (orientation * osg::Vec3f(1,0,0)) * distance; else if(direction == 3) spawnPoint = pos + (orientation * osg::Vec3f(1,0,0)) * distance; if (!ptr.getClass().isActor()) break; // check if spawn point is safe, fall back to another direction if not spawnPoint.z() += 30; // move up a little to account for slopes, will snap down later if (!castRay(spawnPoint.x(), spawnPoint.y(), spawnPoint.z(), pos.x(), pos.y(), pos.z() + 20)) { // safe break; } } ipos.pos[0] = spawnPoint.x(); ipos.pos[1] = spawnPoint.y(); ipos.pos[2] = spawnPoint.z(); if (referenceObject.getClass().isActor()) { ipos.rot[0] = 0; ipos.rot[1] = 0; } MWWorld::Ptr placed = copyObjectToCell(ptr, referenceCell, ipos, ptr.getRefData().getCount(), false); adjustPosition(placed, true); // snap to ground return placed; } void World::indexToPosition (int cellX, int cellY, float &x, float &y, bool centre) const { const int cellSize = Constants::CellSizeInUnits; x = static_cast(cellSize * cellX); y = static_cast(cellSize * cellY); if (centre) { x += cellSize/2; y += cellSize/2; } } void World::positionToIndex (float x, float y, int &cellX, int &cellY) const { cellX = static_cast(std::floor(x / Constants::CellSizeInUnits)); cellY = static_cast(std::floor(y / Constants::CellSizeInUnits)); } void World::queueMovement(const Ptr &ptr, const osg::Vec3f &velocity) { mPhysics->queueObjectMovement(ptr, velocity); } /* Start of tes3mp addition Make it possible to set the inertial force of a Ptr directly */ void World::setInertialForce(const Ptr& ptr, const osg::Vec3f &force) { MWPhysics::Actor *actor = mPhysics->getActor(ptr); if (actor != nullptr) { actor->setOnGround(false); actor->setInertialForce(force); } } /* End of tes3mp addition */ /* Start of tes3mp addition Make it possible to set whether a Ptr is on the ground or not, needed for proper synchronization in multiplayer */ void World::setOnGround(const Ptr& ptr, bool onGround) { MWPhysics::Actor* actor = mPhysics->getActor(ptr); if (actor != nullptr) { actor->setOnGround(onGround); } } /* End of tes3mp addition */ /* Start of tes3mp addition Make it possible to set the physics framerate from elsewhere */ void World::setPhysicsFramerate(float physFramerate) { mPhysics->setPhysicsFramerate(physFramerate); } /* End of tes3mp addition */ void World::updateAnimatedCollisionShape(const Ptr &ptr) { mPhysics->updateAnimatedCollisionShape(ptr); } void World::doPhysics(float duration, osg::Timer_t frameStart, unsigned int frameNumber, osg::Stats& stats) { mPhysics->stepSimulation(); processDoors(duration); mProjectileManager->update(duration); const auto& results = mPhysics->applyQueuedMovement(duration, mDiscardMovements, frameStart, frameNumber, stats); mProjectileManager->processHits(); mDiscardMovements = false; for(const auto& actor : results) { // Handle player last, in case a cell transition occurs if(actor != getPlayerPtr()) { auto* physactor = mPhysics->getActor(actor); assert(physactor); const auto position = physactor->getSimulationPosition(); moveObject(actor, position.x(), position.y(), position.z(), false, false); } } const auto player = std::find(results.begin(), results.end(), getPlayerPtr()); if (player != results.end()) { auto* physactor = mPhysics->getActor(*player); assert(physactor); const auto position = physactor->getSimulationPosition(); moveObject(*player, position.x(), position.y(), position.z(), false, false); } } void World::updateNavigator() { mPhysics->forEachAnimatedObject([&] (const MWPhysics::Object* object) { updateNavigatorObject(*object); }); for (const auto& door : mDoorStates) if (const auto object = mPhysics->getObject(door.first)) updateNavigatorObject(*object); if (mShouldUpdateNavigator) { mNavigator->update(getPlayerPtr().getRefData().getPosition().asVec3()); mShouldUpdateNavigator = false; } } void World::updateNavigatorObject(const MWPhysics::Object& object) { const DetourNavigator::ObjectShapes shapes(object.getShapeInstance()); mShouldUpdateNavigator = mNavigator->updateObject(DetourNavigator::ObjectId(&object), shapes, object.getTransform()) || mShouldUpdateNavigator; } const MWPhysics::RayCastingInterface* World::getRayCasting() const { return mPhysics.get(); } bool World::castRay (float x1, float y1, float z1, float x2, float y2, float z2) { int mask = MWPhysics::CollisionType_World | MWPhysics::CollisionType_Door; bool result = castRay(x1, y1, z1, x2, y2, z2, mask); return result; } bool World::castRay (float x1, float y1, float z1, float x2, float y2, float z2, int mask) { osg::Vec3f a(x1,y1,z1); osg::Vec3f b(x2,y2,z2); MWPhysics::RayCastingResult result = mPhysics->castRay(a, b, MWWorld::Ptr(), std::vector(), mask); return result.mHit; } bool World::castRay(const osg::Vec3f& from, const osg::Vec3f& to, int mask, const MWWorld::ConstPtr& ignore) { return mPhysics->castRay(from, to, ignore, std::vector(), mask).mHit; } bool World::rotateDoor(const Ptr door, MWWorld::DoorState state, float duration) { const ESM::Position& objPos = door.getRefData().getPosition(); float oldRot = objPos.rot[2]; float minRot = door.getCellRef().getPosition().rot[2]; float maxRot = minRot + osg::DegreesToRadians(90.f); float diff = duration * osg::DegreesToRadians(90.f) * (state == MWWorld::DoorState::Opening ? 1 : -1); float targetRot = std::min(std::max(minRot, oldRot + diff), maxRot); rotateObject(door, objPos.rot[0], objPos.rot[1], targetRot, MWBase::RotationFlag_none); bool reached = (targetRot == maxRot && state != MWWorld::DoorState::Idle) || targetRot == minRot; /// \todo should use convexSweepTest here bool collisionWithActor = false; for (auto& [ptr, point, normal] : mPhysics->getCollisionsPoints(door, MWPhysics::CollisionType_Door, MWPhysics::CollisionType_Actor)) { if (ptr.getClass().isActor()) { auto localPoint = objPos.asVec3() - point; osg::Vec3f direction = osg::Quat(diff, osg::Vec3f(0, 0, 1)) * localPoint - localPoint; direction.normalize(); mPhysics->reportCollision(Misc::Convert::toBullet(point), Misc::Convert::toBullet(normal)); if (direction * normal < 0) // door is turning away from actor continue; collisionWithActor = true; // Collided with actor, ask actor to try to avoid door if(ptr != getPlayerPtr() ) { MWMechanics::AiSequence& seq = ptr.getClass().getCreatureStats(ptr).getAiSequence(); if(seq.getTypeId() != MWMechanics::AiPackageTypeId::AvoidDoor) //Only add it once seq.stack(MWMechanics::AiAvoidDoor(door),ptr); } // we need to undo the rotation reached = false; } } // Cancel door closing sound if collision with actor is detected if (collisionWithActor) { const ESM::Door* ref = door.get()->mBase; if (state == MWWorld::DoorState::Opening) { const std::string& openSound = ref->mOpenSound; if (!openSound.empty() && MWBase::Environment::get().getSoundManager()->getSoundPlaying(door, openSound)) MWBase::Environment::get().getSoundManager()->stopSound3D(door, openSound); } else if (state == MWWorld::DoorState::Closing) { const std::string& closeSound = ref->mCloseSound; if (!closeSound.empty() && MWBase::Environment::get().getSoundManager()->getSoundPlaying(door, closeSound)) MWBase::Environment::get().getSoundManager()->stopSound3D(door, closeSound); } rotateObject(door, objPos.rot[0], objPos.rot[1], oldRot, MWBase::RotationFlag_none); } return reached; } void World::processDoors(float duration) { auto it = mDoorStates.begin(); while (it != mDoorStates.end()) { if (!mWorldScene->isCellActive(*it->first.getCell()) || !it->first.getRefData().getBaseNode()) { // The door is no longer in an active cell, or it was disabled. // Erase from mDoorStates, since we no longer need to move it. // Once we load the door's cell again (or re-enable the door), Door::insertObject will reinsert to mDoorStates. mDoorStates.erase(it++); } else { bool reached = rotateDoor(it->first, it->second, duration); if (reached) { // Mark as non-moving it->first.getClass().setDoorState(it->first, MWWorld::DoorState::Idle); mDoorStates.erase(it++); } else ++it; } } } void World::setActorCollisionMode(const MWWorld::Ptr& ptr, bool internal, bool external) { MWPhysics::Actor *physicActor = mPhysics->getActor(ptr); if (physicActor && physicActor->getCollisionMode() != internal) { physicActor->enableCollisionMode(internal); physicActor->enableCollisionBody(external); } } bool World::isActorCollisionEnabled(const MWWorld::Ptr& ptr) { MWPhysics::Actor *physicActor = mPhysics->getActor(ptr); return physicActor && physicActor->getCollisionMode(); } bool World::toggleCollisionMode() { if (mPhysics->toggleCollisionMode()) { adjustPosition(getPlayerPtr(), true); return true; } return false; } bool World::toggleRenderMode (MWRender::RenderMode mode) { switch (mode) { case MWRender::Render_CollisionDebug: return mPhysics->toggleDebugRendering(); default: return mRendering->toggleRenderMode(mode); } } const ESM::Potion *World::createRecord (const ESM::Potion& record) { return mStore.insert(record); } const ESM::Class *World::createRecord (const ESM::Class& record) { return mStore.insert(record); } const ESM::Spell *World::createRecord (const ESM::Spell& record) { return mStore.insert(record); } const ESM::Cell *World::createRecord (const ESM::Cell& record) { return mStore.insert(record); } const ESM::CreatureLevList *World::createOverrideRecord(const ESM::CreatureLevList &record) { return mStore.overrideRecord(record); } const ESM::ItemLevList *World::createOverrideRecord(const ESM::ItemLevList &record) { return mStore.overrideRecord(record); } const ESM::Creature *World::createOverrideRecord(const ESM::Creature &record) { return mStore.overrideRecord(record); } const ESM::NPC *World::createOverrideRecord(const ESM::NPC &record) { return mStore.overrideRecord(record); } const ESM::Container *World::createOverrideRecord(const ESM::Container &record) { return mStore.overrideRecord(record); } const ESM::NPC *World::createRecord(const ESM::NPC &record) { bool update = false; if (Misc::StringUtils::ciEqual(record.mId, "player")) { const ESM::NPC *player = mPlayer->getPlayer().get()->mBase; update = record.isMale() != player->isMale() || !Misc::StringUtils::ciEqual(record.mRace, player->mRace) || !Misc::StringUtils::ciEqual(record.mHead, player->mHead) || !Misc::StringUtils::ciEqual(record.mHair, player->mHair); } const ESM::NPC *ret = mStore.insert(record); if (update) { renderPlayer(); } return ret; } const ESM::Creature *World::createRecord(const ESM::Creature &record) { return mStore.insert(record); } const ESM::Armor *World::createRecord (const ESM::Armor& record) { return mStore.insert(record); } const ESM::Weapon *World::createRecord (const ESM::Weapon& record) { return mStore.insert(record); } const ESM::Clothing *World::createRecord (const ESM::Clothing& record) { return mStore.insert(record); } const ESM::Enchantment *World::createRecord (const ESM::Enchantment& record) { return mStore.insert(record); } const ESM::Book *World::createRecord (const ESM::Book& record) { return mStore.insert(record); } void World::update (float duration, bool paused) { if (mGoToJail && !paused) goToJail(); // Reset "traveling" flag - there was a frame to detect traveling. mPlayerTraveling = false; // The same thing for "in jail" flag: reset it if: // 1. Player was in jail // 2. Jailing window was closed if (mPlayerInJail && !mGoToJail && !MWBase::Environment::get().getWindowManager()->containsMode(MWGui::GM_Jail)) mPlayerInJail = false; updateWeather(duration, paused); if (!paused) { updateNavigator(); } updatePlayer(); mPhysics->debugDraw(); mWorldScene->update (duration, paused); updateSoundListener(); mSpellPreloadTimer -= duration; if (mSpellPreloadTimer <= 0.f) { mSpellPreloadTimer = 0.1f; preloadSpells(); } } void World::updatePhysics (float duration, bool paused, osg::Timer_t frameStart, unsigned int frameNumber, osg::Stats& stats) { if (!paused) { doPhysics (duration, frameStart, frameNumber, stats); } else { // zero the async stats if we are paused stats.setAttribute(frameNumber, "physicsworker_time_begin", 0); stats.setAttribute(frameNumber, "physicsworker_time_taken", 0); stats.setAttribute(frameNumber, "physicsworker_time_end", 0); } } void World::updatePlayer() { MWWorld::Ptr player = getPlayerPtr(); // TODO: move to MWWorld::Player if (player.getCell()->isExterior()) { ESM::Position pos = player.getRefData().getPosition(); mPlayer->setLastKnownExteriorPosition(pos.asVec3()); } bool isWerewolf = player.getClass().getNpcStats(player).isWerewolf(); bool isFirstPerson = mRendering->getCamera()->isFirstPerson(); if (isWerewolf && isFirstPerson) { float werewolfFov = Fallback::Map::getFloat("General_Werewolf_FOV"); if (werewolfFov != 0) mRendering->overrideFieldOfView(werewolfFov); MWBase::Environment::get().getWindowManager()->setWerewolfOverlay(true); } else { mRendering->resetFieldOfView(); MWBase::Environment::get().getWindowManager()->setWerewolfOverlay(false); } // Sink the camera while sneaking bool sneaking = player.getClass().getCreatureStats(getPlayerPtr()).getStance(MWMechanics::CreatureStats::Stance_Sneak); bool swimming = isSwimming(player); bool flying = isFlying(player); static const float i1stPersonSneakDelta = mStore.get().find("i1stPersonSneakDelta")->mValue.getFloat(); if (sneaking && !swimming && !flying) mRendering->getCamera()->setSneakOffset(i1stPersonSneakDelta); else mRendering->getCamera()->setSneakOffset(0.f); int blind = 0; auto& magicEffects = player.getClass().getCreatureStats(player).getMagicEffects(); if (!mGodMode) blind = static_cast(magicEffects.get(ESM::MagicEffect::Blind).getMagnitude()); MWBase::Environment::get().getWindowManager()->setBlindness(std::max(0, std::min(100, blind))); int nightEye = static_cast(magicEffects.get(ESM::MagicEffect::NightEye).getMagnitude()); mRendering->setNightEyeFactor(std::min(1.f, (nightEye/100.f))); } void World::preloadSpells() { std::string selectedSpell = MWBase::Environment::get().getWindowManager()->getSelectedSpell(); if (!selectedSpell.empty()) { const ESM::Spell* spell = mStore.get().search(selectedSpell); if (spell) preloadEffects(&spell->mEffects); } const MWWorld::Ptr& selectedEnchantItem = MWBase::Environment::get().getWindowManager()->getSelectedEnchantItem(); if (!selectedEnchantItem.isEmpty()) { std::string enchantId = selectedEnchantItem.getClass().getEnchantment(selectedEnchantItem); if (!enchantId.empty()) { const ESM::Enchantment* ench = mStore.get().search(enchantId); if (ench) preloadEffects(&ench->mEffects); } } const MWWorld::Ptr& selectedWeapon = MWBase::Environment::get().getWindowManager()->getSelectedWeapon(); if (!selectedWeapon.isEmpty()) { std::string enchantId = selectedWeapon.getClass().getEnchantment(selectedWeapon); if (!enchantId.empty()) { const ESM::Enchantment* ench = mStore.get().search(enchantId); if (ench && ench->mData.mType == ESM::Enchantment::WhenStrikes) preloadEffects(&ench->mEffects); } } } void World::updateSoundListener() { const ESM::Position& refpos = getPlayerPtr().getRefData().getPosition(); osg::Vec3f listenerPos; if (isFirstPerson()) listenerPos = mRendering->getCameraPosition(); else listenerPos = refpos.asVec3() + osg::Vec3f(0, 0, 1.85f * mPhysics->getHalfExtents(getPlayerPtr()).z()); osg::Quat listenerOrient = osg::Quat(refpos.rot[1], osg::Vec3f(0,-1,0)) * osg::Quat(refpos.rot[0], osg::Vec3f(-1,0,0)) * osg::Quat(refpos.rot[2], osg::Vec3f(0,0,-1)); 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()); MWBase::Environment::get().getSoundManager()->setListenerPosDir(listenerPos, forward, up, underwater); } void World::updateWindowManager () { try { // inform the GUI about focused object MWWorld::Ptr object = getFacedObject (); // retrieve object dimensions so we know where to place the floating label if (!object.isEmpty ()) { osg::BoundingBox bb = mPhysics->getBoundingBox(object); if (!bb.valid() && object.getRefData().getBaseNode()) { osg::ComputeBoundsVisitor computeBoundsVisitor; computeBoundsVisitor.setTraversalMask(~(MWRender::Mask_ParticleSystem|MWRender::Mask_Effect)); object.getRefData().getBaseNode()->accept(computeBoundsVisitor); bb = computeBoundsVisitor.getBoundingBox(); } osg::Vec4f screenBounds = mRendering->getScreenBounds(bb); MWBase::Environment::get().getWindowManager()->setFocusObjectScreenCoords( screenBounds.x(), screenBounds.y(), screenBounds.z(), screenBounds.w()); } MWBase::Environment::get().getWindowManager()->setFocusObject(object); } catch (std::exception& e) { Log(Debug::Error) << "Error updating window manager: " << e.what(); } } MWWorld::Ptr World::getFacedObject(float maxDistance, bool ignorePlayer) { const float camDist = mRendering->getCamera()->getCameraDistance(); maxDistance += camDist; MWWorld::Ptr facedObject; MWRender::RenderingManager::RayResult rayToObject; if (MWBase::Environment::get().getWindowManager()->isGuiMode()) { float x, y; MWBase::Environment::get().getWindowManager()->getMousePosition(x, y); rayToObject = mRendering->castCameraToViewportRay(x, y, maxDistance, ignorePlayer); } else rayToObject = mRendering->castCameraToViewportRay(0.5f, 0.5f, maxDistance, ignorePlayer); facedObject = rayToObject.mHitObject; if (facedObject.isEmpty() && rayToObject.mHitRefnum.hasContentFile()) { for (CellStore* cellstore : mWorldScene->getActiveCells()) { facedObject = cellstore->searchViaRefNum(rayToObject.mHitRefnum); if (!facedObject.isEmpty()) break; } } if (rayToObject.mHit) mDistanceToFacedObject = (rayToObject.mRatio * maxDistance) - camDist; else mDistanceToFacedObject = -1; return facedObject; } bool World::isCellExterior() const { const CellStore *currentCell = mWorldScene->getCurrentCell(); if (currentCell) { return currentCell->getCell()->isExterior(); } return false; } bool World::isCellQuasiExterior() const { const CellStore *currentCell = mWorldScene->getCurrentCell(); if (currentCell) { if (!(currentCell->getCell()->mData.mFlags & ESM::Cell::QuasiEx)) return false; else return true; } return false; } int World::getCurrentWeather() const { return mWeatherManager->getWeatherID(); } unsigned int World::getNightDayMode() const { return mWeatherManager->getNightDayMode(); } void World::changeWeather(const std::string& region, const unsigned int id) { mWeatherManager->changeWeather(region, id); } /* Start of tes3mp addition Make it possible to set a specific weather state for a region from elsewhere in the code */ void World::setRegionWeather(const std::string& region, const unsigned int currentWeather, const unsigned int nextWeather, const unsigned int queuedWeather, const float transitionFactor, bool force) { mWeatherManager->setRegionWeather(region, currentWeather, nextWeather, queuedWeather, transitionFactor, force); } /* End of tes3mp addition */ /* Start of tes3mp addition Make it possible to check whether the local WeatherManager has the ability to create weather changes */ bool World::getWeatherCreationState() { return mWeatherManager->getWeatherCreationState(); } /* End of tes3mp addition */ /* Start of tes3mp addition Make it possible to enable and disable the local WeatherManager's ability to create weather changes */ void World::setWeatherCreationState(bool state) { mWeatherManager->setWeatherCreationState(state); } /* End of tes3mp addition */ /* Start of tes3mp addition Make it possible to send the current weather in a WorldWeather packet when requested from elsewhere in the code */ void World::sendWeather() { mWeatherManager->sendWeather(); } /* End of tes3mp addition */ void World::modRegion(const std::string ®ionid, const std::vector &chances) { mWeatherManager->modRegion(regionid, chances); } osg::Vec2f World::getNorthVector (const CellStore* cell) { MWWorld::ConstPtr northmarker = cell->searchConst("northmarker"); if (northmarker.isEmpty()) return osg::Vec2f(0, 1); osg::Quat orient (-northmarker.getRefData().getPosition().rot[2], osg::Vec3f(0,0,1)); osg::Vec3f dir = orient * osg::Vec3f(0,1,0); osg::Vec2f d (dir.x(), dir.y()); return d; } struct GetDoorMarkerVisitor { GetDoorMarkerVisitor(std::vector& out) : mOut(out) { } std::vector& mOut; bool operator()(const MWWorld::Ptr& ptr) { MWWorld::LiveCellRef& ref = *static_cast* >(ptr.getBase()); if (!ref.mData.isEnabled() || ref.mData.isDeleted()) return true; if (ref.mRef.getTeleport()) { World::DoorMarker newMarker; newMarker.name = MWClass::Door::getDestination(ref); ESM::CellId cellid; if (!ref.mRef.getDestCell().empty()) { cellid.mWorldspace = ref.mRef.getDestCell(); cellid.mPaged = false; cellid.mIndex.mX = 0; cellid.mIndex.mY = 0; } else { cellid.mPaged = true; MWBase::Environment::get().getWorld()->positionToIndex( ref.mRef.getDoorDest().pos[0], ref.mRef.getDoorDest().pos[1], cellid.mIndex.mX, cellid.mIndex.mY); } newMarker.dest = cellid; ESM::Position pos = ref.mData.getPosition (); newMarker.x = pos.pos[0]; newMarker.y = pos.pos[1]; mOut.push_back(newMarker); } return true; } }; void World::getDoorMarkers (CellStore* cell, std::vector& out) { GetDoorMarkerVisitor visitor(out); cell->forEachType(visitor); } void World::setWaterHeight(const float height) { mPhysics->setWaterHeight(height); mRendering->setWaterHeight(height); } bool World::toggleWater() { return mRendering->toggleRenderMode(MWRender::Render_Water); } bool World::toggleWorld() { return mRendering->toggleRenderMode(MWRender::Render_Scene); } bool World::toggleBorders() { return mRendering->toggleBorders(); } void World::PCDropped (const Ptr& item) { std::string script = item.getClass().getScript(item); // Set OnPCDrop Variable on item's script, if it has a script with that variable declared if(script != "") item.getRefData().getLocals().setVarByInt(script, "onpcdrop", 1); } MWWorld::Ptr World::placeObject (const MWWorld::ConstPtr& object, float cursorX, float cursorY, int amount) { const float maxDist = 200.f; MWRender::RenderingManager::RayResult result = mRendering->castCameraToViewportRay(cursorX, cursorY, maxDist, true, true); CellStore* cell = getPlayerPtr().getCell(); ESM::Position pos = getPlayerPtr().getRefData().getPosition(); if (result.mHit) { pos.pos[0] = result.mHitPointWorld.x(); pos.pos[1] = result.mHitPointWorld.y(); pos.pos[2] = result.mHitPointWorld.z(); } // We want only the Z part of the player's rotation pos.rot[0] = 0; pos.rot[1] = 0; // copy the object and set its count Ptr dropped = copyObjectToCell(object, cell, pos, amount, true); // only the player place items in the world, so no need to check actor PCDropped(dropped); return dropped; } bool World::canPlaceObject(float cursorX, float cursorY) { const float maxDist = 200.f; MWRender::RenderingManager::RayResult result = mRendering->castCameraToViewportRay(cursorX, cursorY, maxDist, true, true); if (result.mHit) { // check if the wanted position is on a flat surface, and not e.g. against a vertical wall if (std::acos((result.mHitNormalWorld/result.mHitNormalWorld.length()) * osg::Vec3f(0,0,1)) >= osg::DegreesToRadians(30.f)) return false; return true; } else return false; } Ptr World::copyObjectToCell(const ConstPtr &object, CellStore* cell, ESM::Position pos, int count, bool adjustPos) { if (!cell) throw std::runtime_error("copyObjectToCell(): cannot copy object to null cell"); if (cell->isExterior()) { int cellX, cellY; positionToIndex(pos.pos[0], pos.pos[1], cellX, cellY); cell = mCells.getExterior(cellX, cellY); } MWWorld::Ptr dropped = object.getClass().copyToCell(object, *cell, pos, count); // Reset some position values that could be uninitialized if this item came from a container dropped.getCellRef().setPosition(pos); dropped.getCellRef().unsetRefNum(); if (mWorldScene->isCellActive(*cell)) { if (dropped.getRefData().isEnabled()) { mWorldScene->addObjectToScene(dropped); } std::string script = dropped.getClass().getScript(dropped); if (!script.empty()) { mLocalScripts.add(script, dropped); } addContainerScripts(dropped, cell); } if (!object.getClass().isActor() && adjustPos && dropped.getRefData().getBaseNode()) { // Adjust position so the location we wanted ends up in the middle of the object bounding box osg::ComputeBoundsVisitor computeBounds; computeBounds.setTraversalMask(~MWRender::Mask_ParticleSystem); dropped.getRefData().getBaseNode()->accept(computeBounds); osg::BoundingBox bounds = computeBounds.getBoundingBox(); if (bounds.valid()) { bounds.set(bounds._min - pos.asVec3(), bounds._max - pos.asVec3()); osg::Vec3f adjust ( (bounds.xMin() + bounds.xMax()) / 2, (bounds.yMin() + bounds.yMax()) / 2, bounds.zMin() ); pos.pos[0] -= adjust.x(); pos.pos[1] -= adjust.y(); pos.pos[2] -= adjust.z(); moveObject(dropped, pos.pos[0], pos.pos[1], pos.pos[2]); } } return dropped; } MWWorld::Ptr World::dropObjectOnGround (const Ptr& actor, const ConstPtr& object, int amount) { MWWorld::CellStore* cell = actor.getCell(); ESM::Position pos = actor.getRefData().getPosition(); // We want only the Z part of the actor's rotation pos.rot[0] = 0; pos.rot[1] = 0; osg::Vec3f orig = pos.asVec3(); orig.z() += 20; osg::Vec3f dir (0, 0, -1); float len = 1000000.0; MWRender::RenderingManager::RayResult result = mRendering->castRay(orig, orig+dir*len, true, true); if (result.mHit) pos.pos[2] = result.mHitPointWorld.z(); // copy the object and set its count Ptr dropped = copyObjectToCell(object, cell, pos, amount, true); if(actor == mPlayer->getPlayer()) // Only call if dropped by player PCDropped(dropped); return dropped; } void World::processChangedSettings(const Settings::CategorySettingVector& settings) { mRendering->processChangedSettings(settings); } bool World::isFlying(const MWWorld::Ptr &ptr) const { if(!ptr.getClass().isActor()) return false; const MWMechanics::CreatureStats &stats = ptr.getClass().getCreatureStats(ptr); if (stats.isDead()) return false; const bool isPlayer = ptr == getPlayerConstPtr(); if (!(isPlayer && mGodMode) && stats.getMagicEffects().get(ESM::MagicEffect::Paralyze).getModifier() > 0) return false; if (ptr.getClass().canFly(ptr)) return true; if(stats.getMagicEffects().get(ESM::MagicEffect::Levitate).getMagnitude() > 0 && isLevitationEnabled()) return true; const MWPhysics::Actor* actor = mPhysics->getActor(ptr); if(!actor) return true; return false; } bool World::isSlowFalling(const MWWorld::Ptr &ptr) const { if(!ptr.getClass().isActor()) return false; const MWMechanics::CreatureStats &stats = ptr.getClass().getCreatureStats(ptr); if(stats.getMagicEffects().get(ESM::MagicEffect::SlowFall).getMagnitude() > 0) return true; return false; } bool World::isSubmerged(const MWWorld::ConstPtr &object) const { return isUnderwater(object, 1.0f/mSwimHeightScale); } bool World::isSwimming(const MWWorld::ConstPtr &object) const { return isUnderwater(object, mSwimHeightScale); } bool World::isWading(const MWWorld::ConstPtr &object) const { const float kneeDeep = 0.25f; return isUnderwater(object, kneeDeep); } bool World::isUnderwater(const MWWorld::ConstPtr &object, const float heightRatio) const { osg::Vec3f pos (object.getRefData().getPosition().asVec3()); pos.z() += heightRatio*2*mPhysics->getRenderingHalfExtents(object).z(); const CellStore *currCell = object.isInCell() ? object.getCell() : nullptr; // currCell == nullptr should only happen for player, during initial startup return isUnderwater(currCell, pos); } bool World::isUnderwater(const MWWorld::CellStore* cell, const osg::Vec3f &pos) const { if (!cell) return false; if (!(cell->getCell()->hasWater())) { return false; } return pos.z() < cell->getWaterLevel(); } bool World::isWaterWalkingCastableOnTarget(const MWWorld::ConstPtr &target) const { const MWWorld::CellStore* cell = target.getCell(); if (!cell->getCell()->hasWater()) return true; float waterlevel = cell->getWaterLevel(); // SwimHeightScale affects the upper z position an actor can swim to // while in water. Based on observation from the original engine, // the upper z position you get with a +1 SwimHeightScale is the depth // limit for being able to cast water walking on an underwater target. if (isUnderwater(target, mSwimHeightScale + 1) || (isUnderwater(cell, target.getRefData().getPosition().asVec3()) && !mPhysics->canMoveToWaterSurface(target, waterlevel))) return false; // not castable if too deep or if not enough room to move actor to surface else return true; } bool World::isOnGround(const MWWorld::Ptr &ptr) const { return mPhysics->isOnGround(ptr); } void World::togglePOV(bool force) { mRendering->getCamera()->toggleViewMode(force); } bool World::isFirstPerson() const { return mRendering->getCamera()->isFirstPerson(); } bool World::isPreviewModeEnabled() const { 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); } void World::disableDeferredPreviewRotation() { mRendering->getCamera()->disableDeferredPreviewRotation(); } void World::applyDeferredPreviewRotationToPlayer(float dt) { mRendering->getCamera()->applyDeferredPreviewRotationToPlayer(dt); } void World::allowVanityMode(bool allow) { mRendering->getCamera()->allowVanityMode(allow); } bool World::vanityRotateCamera(float * rot) { if(!mRendering->getCamera()->isVanityOrPreviewModeEnabled()) return false; mRendering->getCamera()->rotateCamera(rot[0], rot[2], true); return true; } void World::adjustCameraDistance(float dist) { mRendering->getCamera()->adjustCameraDistance(dist); } void World::saveLoaded() { mStore.validateDynamic(); } void World::setupPlayer() { const ESM::NPC *player = mStore.get().find("player"); if (!mPlayer) mPlayer.reset(new MWWorld::Player(player)); else { // Remove the old CharacterController MWBase::Environment::get().getMechanicsManager()->remove(getPlayerPtr()); mNavigator->removeAgent(getPathfindingHalfExtents(getPlayerConstPtr())); mPhysics->remove(getPlayerPtr()); mRendering->removePlayer(getPlayerPtr()); mPlayer->set(player); } Ptr ptr = mPlayer->getPlayer(); mRendering->setupPlayer(ptr); } void World::renderPlayer() { MWBase::Environment::get().getMechanicsManager()->remove(getPlayerPtr()); MWWorld::Ptr player = getPlayerPtr(); mRendering->renderPlayer(player); MWRender::NpcAnimation* anim = static_cast(mRendering->getAnimation(player)); player.getClass().getInventoryStore(player).setInvListener(anim, player); player.getClass().getInventoryStore(player).setContListener(anim); scaleObject(player, player.getCellRef().getScale()); // apply race height rotateObject(player, 0.f, 0.f, 0.f, MWBase::RotationFlag_inverseOrder | MWBase::RotationFlag_adjust); MWBase::Environment::get().getMechanicsManager()->add(getPlayerPtr()); MWBase::Environment::get().getWindowManager()->watchActor(getPlayerPtr()); std::string model = getPlayerPtr().getClass().getModel(getPlayerPtr()); model = Misc::ResourceHelpers::correctActorModelPath(model, mResourceSystem->getVFS()); mPhysics->remove(getPlayerPtr()); mPhysics->addActor(getPlayerPtr(), model); applyLoopingParticles(player); mDefaultHalfExtents = mPhysics->getOriginalHalfExtents(getPlayerPtr()); mNavigator->addAgent(getPathfindingHalfExtents(getPlayerConstPtr())); } World::RestPermitted World::canRest () const { CellStore *currentCell = mWorldScene->getCurrentCell(); Ptr player = mPlayer->getPlayer(); RefData &refdata = player.getRefData(); osg::Vec3f playerPos(refdata.getPosition().asVec3()); const MWPhysics::Actor* actor = mPhysics->getActor(player); if (!actor) throw std::runtime_error("can't find player"); if(mPlayer->enemiesNearby()) return Rest_EnemiesAreNearby; if (isUnderwater(currentCell, playerPos) || isWalkingOnWater(player)) return Rest_PlayerIsUnderwater; float fallHeight = player.getClass().getCreatureStats(player).getFallHeight(); float epsilon = 1e-4; if ((actor->getCollisionMode() && (!mPhysics->isOnSolidGround(player) || fallHeight >= epsilon)) || isFlying(player)) return Rest_PlayerIsInAir; if((currentCell->getCell()->mData.mFlags&ESM::Cell::NoSleep) || player.getClass().getNpcStats(player).isWerewolf()) return Rest_OnlyWaiting; return Rest_Allowed; } MWRender::Animation* World::getAnimation(const MWWorld::Ptr &ptr) { auto* animation = mRendering->getAnimation(ptr); if(!animation) { mWorldScene->removeFromPagedRefs(ptr); animation = mRendering->getAnimation(ptr); if(animation) mRendering->pagingBlacklistObject(mStore.find(ptr.getCellRef().getRefId()), ptr); } return animation; } const MWRender::Animation* World::getAnimation(const MWWorld::ConstPtr &ptr) const { return mRendering->getAnimation(ptr); } void World::screenshot(osg::Image* image, int w, int h) { mRendering->screenshot(image, w, h); } bool World::screenshot360(osg::Image* image) { return mRendering->screenshot360(image); } void World::activateDoor(const MWWorld::Ptr& door) { auto state = door.getClass().getDoorState(door); switch (state) { case MWWorld::DoorState::Idle: if (door.getRefData().getPosition().rot[2] == door.getCellRef().getPosition().rot[2]) state = MWWorld::DoorState::Opening; // if closed, then open else state = MWWorld::DoorState::Closing; // if open, then close break; case MWWorld::DoorState::Closing: state = MWWorld::DoorState::Opening; // if closing, then open break; case MWWorld::DoorState::Opening: default: state = MWWorld::DoorState::Closing; // if opening, then close break; } /* Start of tes3mp addition Send an ID_DOOR_STATE packet every time a door is activated */ if (mwmp::Main::get().getLocalPlayer()->isLoggedIn()) { mwmp::ObjectList *objectList = mwmp::Main::get().getNetworking()->getObjectList(); objectList->reset(); objectList->packetOrigin = mwmp::CLIENT_GAMEPLAY; objectList->addDoorState(door, state); objectList->sendDoorState(); } /* End of tes3mp addition */ door.getClass().setDoorState(door, state); mDoorStates[door] = state; } void World::activateDoor(const Ptr &door, MWWorld::DoorState state) { /* Start of tes3mp addition Send an ID_DOOR_STATE packet every time a door is activated */ if (mwmp::Main::get().getLocalPlayer()->isLoggedIn()) { mwmp::ObjectList *objectList = mwmp::Main::get().getNetworking()->getObjectList(); objectList->reset(); objectList->packetOrigin = mwmp::CLIENT_GAMEPLAY; objectList->addDoorState(door, state); objectList->sendDoorState(); } /* End of tes3mp addition */ door.getClass().setDoorState(door, state); mDoorStates[door] = state; if (state == MWWorld::DoorState::Idle) { mDoorStates.erase(door); rotateDoor(door, state, 1); } } /* Start of tes3mp addition Allow the saving of door states without going through World::activateDoor() */ void World::saveDoorState(const Ptr &door, MWWorld::DoorState state) { mDoorStates[door] = state; if (state == MWWorld::DoorState::Idle) mDoorStates.erase(door); } /* End of tes3mp addition */ /* Start of tes3mp addition Make it possible to check whether a cell is active */ bool World::isCellActive(const ESM::Cell& cell) { const Scene::CellStoreCollection& activeCells = mWorldScene->getActiveCells(); mwmp::CellController *cellController = mwmp::Main::get().getCellController(); for (auto it = activeCells.begin(); it != activeCells.end(); ++it) { if (cellController->isSameCell(cell, *(*it)->getCell())) { return true; } } return false; } /* End of tes3mp addition */ /* Start of tes3mp addition Make it possible to unload a cell from elsewhere */ void World::unloadCell(const ESM::Cell& cell) { if (isCellActive(cell)) { const Scene::CellStoreCollection& activeCells = mWorldScene->getActiveCells(); mwmp::CellController *cellController = mwmp::Main::get().getCellController(); mWorldScene->unloadCell(activeCells.find(cellController->getCellStore(cell))); } } /* End of tes3mp addition */ /* Start of tes3mp addition Make it possible to unload all active cells from elsewhere */ void World::unloadActiveCells() { const Scene::CellStoreCollection& activeCells = mWorldScene->getActiveCells(); for (auto it = activeCells.begin(); it != activeCells.end(); ++it) { // Ignore a placeholder interior that a player may currently be in if ((*it)->getCell()->isExterior() || !Misc::StringUtils::ciEqual((*it)->getCell()->getDescription(), RecordHelper::getPlaceholderInteriorCellName())) { mWorldScene->unloadCell(it); } } } /* End of tes3mp addition */ /* Start of tes3mp addition Clear the CellStore for a specific Cell from elsewhere */ void World::clearCellStore(const ESM::Cell& cell) { mwmp::CellController* cellController = mwmp::Main::get().getCellController(); MWWorld::CellStore *cellStore = cellController->getCellStore(cell); if (cellStore != nullptr) cellStore->clearMovesToCells(); mCells.clear(cell); } /* End of tes3mp addition */ bool World::getPlayerStandingOn (const MWWorld::ConstPtr& object) { MWWorld::Ptr player = getPlayerPtr(); return mPhysics->isActorStandingOn(player, object); } bool World::getActorStandingOn (const MWWorld::ConstPtr& object) { std::vector actors; mPhysics->getActorsStandingOn(object, actors); return !actors.empty(); } void World::getActorsStandingOn (const MWWorld::ConstPtr& object, std::vector &actors) { mPhysics->getActorsStandingOn(object, actors); } bool World::getPlayerCollidingWith (const MWWorld::ConstPtr& object) { MWWorld::Ptr player = getPlayerPtr(); return mPhysics->isActorCollidingWith(player, object); } bool World::getActorCollidingWith (const MWWorld::ConstPtr& object) { std::vector actors; mPhysics->getActorsCollidingWith(object, actors); return !actors.empty(); } void World::hurtStandingActors(const ConstPtr &object, float healthPerSecond) { /* Start of tes3mp change (major) Being in a menu should not prevent actors from being hurt in multiplayer, so that check has been commented out */ //if (MWBase::Environment::get().getWindowManager()->isGuiMode()) // return; /* End of tes3mp change (major) */ std::vector actors; mPhysics->getActorsStandingOn(object, actors); for (const Ptr &actor : actors) { MWMechanics::CreatureStats& stats = actor.getClass().getCreatureStats(actor); if (stats.isDead()) continue; mPhysics->markAsNonSolid (object); if (actor == getPlayerPtr() && mGodMode) continue; MWMechanics::DynamicStat health = stats.getHealth(); health.setCurrent(health.getCurrent()-healthPerSecond*MWBase::Environment::get().getFrameDuration()); stats.setHealth(health); if (healthPerSecond > 0.0f) { if (actor == getPlayerPtr()) MWBase::Environment::get().getWindowManager()->activateHitOverlay(false); if (!MWBase::Environment::get().getSoundManager()->getSoundPlaying(actor, "Health Damage")) MWBase::Environment::get().getSoundManager()->playSound3D(actor, "Health Damage", 1.0f, 1.0f); } } } void World::hurtCollidingActors(const ConstPtr &object, float healthPerSecond) { /* Start of tes3mp change (major) Being in a menu should not prevent actors from being hurt in multiplayer, so that check has been commented out */ //if (MWBase::Environment::get().getWindowManager()->isGuiMode()) // return; /* End of tes3mp change (major) */ std::vector actors; mPhysics->getActorsCollidingWith(object, actors); for (const Ptr &actor : actors) { MWMechanics::CreatureStats& stats = actor.getClass().getCreatureStats(actor); if (stats.isDead()) continue; mPhysics->markAsNonSolid (object); if (actor == getPlayerPtr() && mGodMode) continue; MWMechanics::DynamicStat health = stats.getHealth(); health.setCurrent(health.getCurrent()-healthPerSecond*MWBase::Environment::get().getFrameDuration()); stats.setHealth(health); if (healthPerSecond > 0.0f) { if (actor == getPlayerPtr()) MWBase::Environment::get().getWindowManager()->activateHitOverlay(false); if (!MWBase::Environment::get().getSoundManager()->getSoundPlaying(actor, "Health Damage")) MWBase::Environment::get().getSoundManager()->playSound3D(actor, "Health Damage", 1.0f, 1.0f); } } } float World::getWindSpeed() { if (isCellExterior() || isCellQuasiExterior()) return mWeatherManager->getWindSpeed(); else return 0.f; } bool World::isInStorm() const { if (isCellExterior() || isCellQuasiExterior()) return mWeatherManager->isInStorm(); else return false; } osg::Vec3f World::getStormDirection() const { if (isCellExterior() || isCellQuasiExterior()) return mWeatherManager->getStormDirection(); else return osg::Vec3f(0,1,0); } struct GetContainersOwnedByVisitor { GetContainersOwnedByVisitor(const MWWorld::ConstPtr& owner, std::vector& out) : mOwner(owner) , mOut(out) { } MWWorld::ConstPtr mOwner; std::vector& mOut; bool operator()(const MWWorld::Ptr& ptr) { if (ptr.getRefData().isDeleted()) return true; // vanilla Morrowind does not allow to sell items from containers with zero capacity if (ptr.getClass().getCapacity(ptr) <= 0.f) return true; if (Misc::StringUtils::ciEqual(ptr.getCellRef().getOwner(), mOwner.getCellRef().getRefId())) mOut.push_back(ptr); return true; } }; void World::getContainersOwnedBy (const MWWorld::ConstPtr& owner, std::vector& out) { for (CellStore* cellstore : mWorldScene->getActiveCells()) { GetContainersOwnedByVisitor visitor (owner, out); cellstore->forEachType(visitor); } } void World::getItemsOwnedBy (const MWWorld::ConstPtr& npc, std::vector& out) { for (CellStore* cellstore : mWorldScene->getActiveCells()) { cellstore->forEach([&] (const auto& ptr) { if (ptr.getRefData().getBaseNode() && Misc::StringUtils::ciEqual(ptr.getCellRef().getOwner(), npc.getCellRef().getRefId())) out.push_back(ptr); return true; }); } } bool World::getLOS(const MWWorld::ConstPtr& actor, const MWWorld::ConstPtr& targetActor) { if (!targetActor.getRefData().isEnabled() || !actor.getRefData().isEnabled()) return false; // cannot get LOS unless both NPC's are enabled if (!targetActor.getRefData().getBaseNode() || !actor.getRefData().getBaseNode()) return false; // not in active cell return mPhysics->getLineOfSight(actor, targetActor); } float World::getDistToNearestRayHit(const osg::Vec3f& from, const osg::Vec3f& dir, float maxDist, bool includeWater) { osg::Vec3f to (dir); to.normalize(); to = from + (to * maxDist); int collisionTypes = MWPhysics::CollisionType_World | MWPhysics::CollisionType_HeightMap | MWPhysics::CollisionType_Door; if (includeWater) { collisionTypes |= MWPhysics::CollisionType_Water; } MWPhysics::RayCastingResult result = mPhysics->castRay(from, to, MWWorld::Ptr(), std::vector(), collisionTypes); if (!result.mHit) return maxDist; else return (result.mHitPos - from).length(); } void World::enableActorCollision(const MWWorld::Ptr& actor, bool enable) { MWPhysics::Actor *physicActor = mPhysics->getActor(actor); if (physicActor) physicActor->enableCollisionBody(enable); } bool World::findInteriorPosition(const std::string &name, ESM::Position &pos) { pos.rot[0] = pos.rot[1] = pos.rot[2] = 0; pos.pos[0] = pos.pos[1] = pos.pos[2] = 0; MWWorld::CellStore *cellStore = getInterior(name); if (!cellStore) return false; std::vector sortedDoors; for (const MWWorld::LiveCellRef& door : cellStore->getReadOnlyDoors().mList) { if (!door.mRef.getTeleport()) continue; sortedDoors.push_back(&door.mRef); } // Sort teleporting doors alphabetically, first by ID, then by destination cell to make search consistent std::sort(sortedDoors.begin(), sortedDoors.end(), [] (const MWWorld::CellRef *lhs, const MWWorld::CellRef *rhs) { if (lhs->getRefId() != rhs->getRefId()) return lhs->getRefId() < rhs->getRefId(); return lhs->getDestCell() < rhs->getDestCell(); }); for (const MWWorld::CellRef* door : sortedDoors) { MWWorld::CellStore *source = nullptr; // door to exterior if (door->getDestCell().empty()) { int x, y; ESM::Position doorDest = door->getDoorDest(); positionToIndex(doorDest.pos[0], doorDest.pos[1], x, y); source = getExterior(x, y); } // door to interior else { source = getInterior(door->getDestCell()); } if (source) { // Find door leading to our current teleport door // and use its destination to position inside cell. for (const MWWorld::LiveCellRef& destDoor : source->getReadOnlyDoors().mList) { if (Misc::StringUtils::ciEqual(name, destDoor.mRef.getDestCell())) { /// \note Using _any_ door pointed to the interior, /// not the one pointed to current door. pos = destDoor.mRef.getDoorDest(); pos.rot[0] = pos.rot[1] = pos.rot[2] = 0; return true; } } } } // Fall back to the first static location. const MWWorld::CellRefList::List &statics = cellStore->getReadOnlyStatics().mList; if (!statics.empty()) { pos = statics.begin()->mRef.getPosition(); pos.rot[0] = pos.rot[1] = pos.rot[2] = 0; return true; } return false; } bool World::findExteriorPosition(const std::string &name, ESM::Position &pos) { pos.rot[0] = pos.rot[1] = pos.rot[2] = 0; const ESM::Cell *ext = getExterior(name); if (!ext && name.find(',') != std::string::npos) { try { int x = std::stoi(name.substr(0, name.find(','))); int y = std::stoi(name.substr(name.find(',')+1)); ext = getExterior(x, y)->getCell(); } catch (const std::invalid_argument&) { // This exception can be ignored, as this means that name probably refers to a interior cell instead of comma separated coordinates } catch (const std::out_of_range&) { throw std::runtime_error("Cell coordinates out of range."); } } if (ext) { int x = ext->getGridX(); int y = ext->getGridY(); indexToPosition(x, y, pos.pos[0], pos.pos[1], true); // Note: Z pos will be adjusted by adjustPosition later pos.pos[2] = 0; return true; } return false; } void World::enableTeleporting(bool enable) { mTeleportEnabled = enable; } bool World::isTeleportingEnabled() const { return mTeleportEnabled; } void World::enableLevitation(bool enable) { mLevitationEnabled = enable; } bool World::isLevitationEnabled() const { return mLevitationEnabled; } void World::reattachPlayerCamera() { mRendering->rebuildPtr(getPlayerPtr()); } bool World::getGodModeState() const { return mGodMode; } bool World::toggleGodMode() { mGodMode = !mGodMode; return mGodMode; } bool World::toggleScripts() { mScriptsEnabled = !mScriptsEnabled; return mScriptsEnabled; } bool World::getScriptsEnabled() const { return mScriptsEnabled; } void World::loadContentFiles(const Files::Collections& fileCollections, const std::vector& content, const std::vector& groundcover, ContentLoader& contentLoader) { int idx = 0; for (const std::string &file : content) { 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 content file does not exist"; throw std::runtime_error(message); } idx++; } ESM::GroundcoverIndex = idx; 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++; } } bool World::startSpellCast(const Ptr &actor) { MWMechanics::CreatureStats& stats = actor.getClass().getCreatureStats(actor); std::string message; bool fail = false; bool isPlayer = (actor == getPlayerPtr()); std::string selectedSpell = stats.getSpells().getSelectedSpell(); if (!selectedSpell.empty()) { /* Start of tes3mp addition If the spell being cast does not exist on our client, ignore it to avoid framelistener errors */ if (getStore().get().search(selectedSpell) == 0) return false; /* End of tes3mp addition */ /* Start of tes3mp addition Always start spells cast by DedicatedPlayers and DedicatedActors, without unilaterally deducting any magicka for them on this client */ if (mwmp::PlayerList::isDedicatedPlayer(actor) || mwmp::Main::get().getCellController()->isDedicatedActor(actor)) return true; /* End of tes3mp addition */ const ESM::Spell* spell = mStore.get().find(selectedSpell); // Check mana bool godmode = (isPlayer && mGodMode); MWMechanics::DynamicStat magicka = stats.getMagicka(); if (spell->mData.mCost > 0 && magicka.getCurrent() < spell->mData.mCost && !godmode) { message = "#{sMagicInsufficientSP}"; fail = true; } // If this is a power, check if it was already used in the last 24h if (!fail && spell->mData.mType == ESM::Spell::ST_Power && !stats.getSpells().canUsePower(spell)) { message = "#{sPowerAlreadyUsed}"; fail = true; } // Reduce mana if (!fail && !godmode) { magicka.setCurrent(magicka.getCurrent() - spell->mData.mCost); stats.setMagicka(magicka); } } if (isPlayer && fail) MWBase::Environment::get().getWindowManager()->messageBox(message); return !fail; } void World::castSpell(const Ptr &actor, bool manualSpell) { MWMechanics::CreatureStats& stats = actor.getClass().getCreatureStats(actor); // For AI actors, get combat targets to use in the ray cast. Only those targets will return a positive hit result. std::vector targetActors; if (!actor.isEmpty() && actor != MWMechanics::getPlayer() && !manualSpell) stats.getAiSequence().getCombatTargets(targetActors); const float fCombatDistance = mStore.get().find("fCombatDistance")->mValue.getFloat(); osg::Vec3f hitPosition = actor.getRefData().getPosition().asVec3(); // for player we can take faced object first MWWorld::Ptr target; if (actor == MWMechanics::getPlayer()) target = getFacedObject(); // if the faced object can not be activated, do not use it if (!target.isEmpty() && !target.getClass().hasToolTip(target)) target = nullptr; if (target.isEmpty()) { // For scripted spells we should not use hit contact if (manualSpell) { if (actor != MWMechanics::getPlayer()) { for (const auto& package : stats.getAiSequence()) { if (package->getTypeId() == MWMechanics::AiPackageTypeId::Cast) { target = package->getTarget(); break; } } } } else { // For actor targets, we want to use hit contact with bounding boxes. // This is to give a slight tolerance for errors, especially with creatures like the Skeleton that would be very hard to aim at otherwise. // For object targets, we want the detailed shapes (rendering raycast). // If we used the bounding boxes for static objects, then we would not be able to target e.g. objects lying on a shelf. std::pair result1 = getHitContact(actor, fCombatDistance, targetActors); // Get the target to use for "on touch" effects, using the facing direction from Head node osg::Vec3f origin = getActorHeadTransform(actor).getTrans(); osg::Quat orient = osg::Quat(actor.getRefData().getPosition().rot[0], osg::Vec3f(-1,0,0)) * osg::Quat(actor.getRefData().getPosition().rot[2], osg::Vec3f(0,0,-1)); osg::Vec3f direction = orient * osg::Vec3f(0,1,0); float distance = getMaxActivationDistance(); osg::Vec3f dest = origin + direction * distance; MWRender::RenderingManager::RayResult result2 = mRendering->castRay(origin, dest, true, true); float dist1 = std::numeric_limits::max(); float dist2 = std::numeric_limits::max(); if (!result1.first.isEmpty() && result1.first.getClass().isActor()) dist1 = (origin - result1.second).length(); if (result2.mHit) dist2 = (origin - result2.mHitPointWorld).length(); if (!result1.first.isEmpty() && result1.first.getClass().isActor()) { target = result1.first; hitPosition = result1.second; if (dist1 > getMaxActivationDistance()) target = nullptr; } else if (result2.mHit) { target = result2.mHitObject; hitPosition = result2.mHitPointWorld; if (dist2 > getMaxActivationDistance() && !target.isEmpty() && !target.getClass().hasToolTip(target)) target = nullptr; } } } std::string selectedSpell = stats.getSpells().getSelectedSpell(); MWMechanics::CastSpell cast(actor, target, false, manualSpell); cast.mHitPosition = hitPosition; if (!selectedSpell.empty()) { const ESM::Spell* spell = mStore.get().find(selectedSpell); cast.cast(spell); } else if (actor.getClass().hasInventoryStore(actor)) { MWWorld::InventoryStore& inv = actor.getClass().getInventoryStore(actor); if (inv.getSelectedEnchantItem() != inv.end()) /* Start of tes3mp change (minor) If this actor is a LocalPlayer or LocalActor, get their Cast and prepare it for sending Set the cast details before going forward, in case it's a one use item that will get removed (like a scroll) */ { mwmp::Cast *localCast = MechanicsHelper::getLocalCast(actor); if (localCast) { MechanicsHelper::resetCast(localCast); localCast->type = mwmp::Cast::ITEM; localCast->itemId = inv.getSelectedEnchantItem()->getCellRef().getRefId(); localCast->shouldSend = true; } cast.cast(*inv.getSelectedEnchantItem()); } /* End of tes3mp addition */ } } void World::launchProjectile (MWWorld::Ptr& actor, MWWorld::Ptr& projectile, const osg::Vec3f& worldPos, const osg::Quat& orient, MWWorld::Ptr& bow, float speed, float attackStrength) { // An initial position of projectile can be outside shooter's collision box, so any object between shooter and launch position will be ignored. // To avoid this issue, we should check for impact immediately before launch the projectile. // So we cast a 1-yard-length ray from shooter to launch position and check if there are collisions in this area. // TODO: as a better solutuon we should handle projectiles during physics update, not during world update. const osg::Vec3f sourcePos = worldPos + orient * osg::Vec3f(0,-1,0) * 64.f; // Early out if the launch position is underwater bool underwater = MWBase::Environment::get().getWorld()->isUnderwater(MWMechanics::getPlayer().getCell(), worldPos); if (underwater) { MWMechanics::projectileHit(actor, Ptr(), bow, projectile, worldPos, attackStrength); mRendering->emitWaterRipple(worldPos); return; } // For AI actors, get combat targets to use in the ray cast. Only those targets will return a positive hit result. std::vector targetActors; if (!actor.isEmpty() && actor.getClass().isActor() && actor != MWMechanics::getPlayer()) actor.getClass().getCreatureStats(actor).getAiSequence().getCombatTargets(targetActors); // Check for impact, if yes, handle hit, if not, launch projectile MWPhysics::RayCastingResult result = mPhysics->castRay(sourcePos, worldPos, actor, targetActors, 0xff, MWPhysics::CollisionType_Projectile); if (result.mHit) MWMechanics::projectileHit(actor, result.mHitObject, bow, projectile, result.mHitPos, attackStrength); else mProjectileManager->launchProjectile(actor, projectile, worldPos, orient, bow, speed, attackStrength); } void World::launchMagicBolt (const std::string &spellId, const MWWorld::Ptr& caster, const osg::Vec3f& fallbackDirection) { mProjectileManager->launchMagicBolt(spellId, caster, fallbackDirection); } void World::updateProjectilesCasters() { mProjectileManager->updateCasters(); } class ApplyLoopingParticlesVisitor : public MWMechanics::EffectSourceVisitor { private: MWWorld::Ptr mActor; public: ApplyLoopingParticlesVisitor(const MWWorld::Ptr& actor) : mActor(actor) { } void visit (MWMechanics::EffectKey key, int /*effectIndex*/, const std::string& /*sourceName*/, const std::string& /*sourceId*/, int /*casterActorId*/, float /*magnitude*/, float /*remainingTime*/ = -1, float /*totalTime*/ = -1) override { const ESMStore& store = MWBase::Environment::get().getWorld()->getStore(); const auto magicEffect = store.get().find(key.mId); if ((magicEffect->mData.mFlags & ESM::MagicEffect::ContinuousVfx) == 0) return; const ESM::Static* castStatic; if (!magicEffect->mHit.empty()) castStatic = store.get().find (magicEffect->mHit); else castStatic = store.get().find ("VFX_DefaultHit"); MWRender::Animation* anim = MWBase::Environment::get().getWorld()->getAnimation(mActor); if (anim && !castStatic->mModel.empty()) anim->addEffect("meshes\\" + castStatic->mModel, magicEffect->mIndex, /*loop*/true, "", magicEffect->mParticle); } }; void World::applyLoopingParticles(const MWWorld::Ptr& ptr) { const MWWorld::Class &cls = ptr.getClass(); if (cls.isActor()) { ApplyLoopingParticlesVisitor visitor(ptr); cls.getCreatureStats(ptr).getActiveSpells().visitEffectSources(visitor); cls.getCreatureStats(ptr).getSpells().visitEffectSources(visitor); if (cls.hasInventoryStore(ptr)) cls.getInventoryStore(ptr).visitEffectSources(visitor); } } const std::vector& World::getContentFiles() const { return mContentFiles; } void World::breakInvisibility(const Ptr &actor) { actor.getClass().getCreatureStats(actor).getSpells().purgeEffect(ESM::MagicEffect::Invisibility); actor.getClass().getCreatureStats(actor).getActiveSpells().purgeEffect(ESM::MagicEffect::Invisibility); if (actor.getClass().hasInventoryStore(actor)) actor.getClass().getInventoryStore(actor).purgeEffect(ESM::MagicEffect::Invisibility); // Normally updated once per frame, but here it is kinda important to do it right away. MWBase::Environment::get().getMechanicsManager()->updateMagicEffects(actor); } bool World::useTorches() const { // If we are in exterior, check the weather manager. // In interiors there are no precipitations and sun, so check the ambient // Looks like pseudo-exteriors considered as interiors in this case MWWorld::CellStore* cell = mPlayer->getPlayer().getCell(); if (cell->isExterior()) { float hour = getTimeStamp().getHour(); return mWeatherManager->useTorches(hour); } else { uint32_t ambient = cell->getCell()->mAmbi.mAmbient; int ambientTotal = (ambient & 0xff) + ((ambient>>8) & 0xff) + ((ambient>>16) & 0xff); return !(cell->getCell()->mData.mFlags & ESM::Cell::NoSleep) && ambientTotal <= 201; } } bool World::findInteriorPositionInWorldSpace(const MWWorld::CellStore* cell, osg::Vec3f& result) { if (cell->isExterior()) return false; // Search for a 'nearest' exterior, counting each cell between the starting // cell and the exterior as a distance of 1. Will fail for isolated interiors. std::set< std::string >checkedCells; std::set< std::string >currentCells; std::set< std::string >nextCells; nextCells.insert( cell->getCell()->mName ); while ( !nextCells.empty() ) { currentCells = nextCells; nextCells.clear(); for (const std::string ¤tCell : currentCells) { MWWorld::CellStore *next = getInterior(currentCell); if ( !next ) continue; // Check if any door in the cell leads to an exterior directly for (const MWWorld::LiveCellRef& ref : next->getReadOnlyDoors().mList) { if (!ref.mRef.getTeleport()) continue; if (ref.mRef.getDestCell().empty()) { ESM::Position pos = ref.mRef.getDoorDest(); result = pos.asVec3(); return true; } else { std::string dest = ref.mRef.getDestCell(); if ( !checkedCells.count(dest) && !currentCells.count(dest) ) nextCells.insert(dest); } } checkedCells.insert(currentCell); } } // No luck :( return false; } MWWorld::ConstPtr World::getClosestMarker( const MWWorld::Ptr &ptr, const std::string &id ) { if ( ptr.getCell()->isExterior() ) { return getClosestMarkerFromExteriorPosition(mPlayer->getLastKnownExteriorPosition(), id); } // Search for a 'nearest' marker, counting each cell between the starting // cell and the exterior as a distance of 1. If an exterior is found, jump // to the nearest exterior marker, without further interior searching. std::set< std::string >checkedCells; std::set< std::string >currentCells; std::set< std::string >nextCells; MWWorld::ConstPtr closestMarker; nextCells.insert( ptr.getCell()->getCell()->mName ); while ( !nextCells.empty() ) { currentCells = nextCells; nextCells.clear(); for (const std::string &cell : currentCells) { MWWorld::CellStore *next = getInterior(cell); checkedCells.insert(cell); if ( !next ) continue; closestMarker = next->searchConst( id ); if ( !closestMarker.isEmpty() ) { return closestMarker; } // Check if any door in the cell leads to an exterior directly for (const MWWorld::LiveCellRef& ref : next->getReadOnlyDoors().mList) { if (!ref.mRef.getTeleport()) continue; if (ref.mRef.getDestCell().empty()) { osg::Vec3f worldPos = ref.mRef.getDoorDest().asVec3(); return getClosestMarkerFromExteriorPosition(worldPos, id); } else { std::string dest = ref.mRef.getDestCell(); if ( !checkedCells.count(dest) && !currentCells.count(dest) ) nextCells.insert(dest); } } } } return MWWorld::Ptr(); } MWWorld::ConstPtr World::getClosestMarkerFromExteriorPosition( const osg::Vec3f& worldPos, const std::string &id ) { MWWorld::ConstPtr closestMarker; float closestDistance = std::numeric_limits::max(); std::vector markers; mCells.getExteriorPtrs(id, markers); for (const Ptr& marker : markers) { osg::Vec3f markerPos = marker.getRefData().getPosition().asVec3(); float distance = (worldPos - markerPos).length2(); if (distance < closestDistance) { closestDistance = distance; closestMarker = marker; } } return closestMarker; } void World::rest(double hours) { mCells.rest(hours); } void World::rechargeItems(double duration, bool activeOnly) { MWWorld::Ptr player = getPlayerPtr(); player.getClass().getInventoryStore(player).rechargeItems(duration); /* Start of tes3mp change (major) Don't unilaterally recharge world items on clients */ /* if (activeOnly) { for (auto &cell : mWorldScene->getActiveCells()) { cell->recharge(duration); } } else mCells.recharge(duration); */ /* End of tes3mp change (major) */ } void World::teleportToClosestMarker (const MWWorld::Ptr& ptr, const std::string& id) { MWWorld::ConstPtr closestMarker = getClosestMarker( ptr, id ); if ( closestMarker.isEmpty() ) { Log(Debug::Warning) << "Failed to teleport: no closest marker found"; return; } std::string cellName; if ( !closestMarker.mCell->isExterior() ) cellName = closestMarker.mCell->getCell()->mName; MWWorld::ActionTeleport action(cellName, closestMarker.getRefData().getPosition(), false); action.execute(ptr); } void World::updateWeather(float duration, bool paused) { bool isExterior = isCellExterior() || isCellQuasiExterior(); if (mPlayer->wasTeleported()) { mPlayer->setTeleported(false); const std::string playerRegion = Misc::StringUtils::lowerCase(getPlayerPtr().getCell()->getCell()->mRegion); mWeatherManager->playerTeleported(playerRegion, isExterior); } const TimeStamp time = getTimeStamp(); mWeatherManager->update(duration, paused, time, isExterior); } struct AddDetectedReferenceVisitor { AddDetectedReferenceVisitor(std::vector& out, const Ptr& detector, World::DetectionType type, float squaredDist) : mOut(out), mDetector(detector), mSquaredDist(squaredDist), mType(type) { } std::vector& mOut; Ptr mDetector; float mSquaredDist; World::DetectionType mType; bool operator() (const MWWorld::Ptr& ptr) { if ((ptr.getRefData().getPosition().asVec3() - mDetector.getRefData().getPosition().asVec3()).length2() >= mSquaredDist) return true; if (!ptr.getRefData().isEnabled() || ptr.getRefData().isDeleted()) return true; // Consider references inside containers as well (except if we are looking for a Creature, they cannot be in containers) bool isContainer = ptr.getClass().getTypeName() == typeid(ESM::Container).name(); if (mType != World::Detect_Creature && (ptr.getClass().isActor() || isContainer)) { // but ignore containers without resolved content if (isContainer && ptr.getRefData().getCustomData() == nullptr) return true; MWWorld::ContainerStore& store = ptr.getClass().getContainerStore(ptr); { for (MWWorld::ContainerStoreIterator it = store.begin(); it != store.end(); ++it) { if (needToAdd(*it, mDetector)) { mOut.push_back(ptr); return true; } } } } if (needToAdd(ptr, mDetector)) mOut.push_back(ptr); return true; } bool needToAdd (const MWWorld::Ptr& ptr, const MWWorld::Ptr& detector) { if (mType == World::Detect_Creature) { // If in werewolf form, this detects only NPCs, otherwise only creatures if (detector.getClass().isNpc() && detector.getClass().getNpcStats(detector).isWerewolf()) { if (ptr.getClass().getTypeName() != typeid(ESM::NPC).name()) return false; } else if (ptr.getClass().getTypeName() != typeid(ESM::Creature).name()) return false; if (ptr.getClass().getCreatureStats(ptr).isDead()) return false; } if (mType == World::Detect_Key && !ptr.getClass().isKey(ptr)) return false; if (mType == World::Detect_Enchantment && ptr.getClass().getEnchantment(ptr).empty()) return false; return true; } }; void World::listDetectedReferences(const Ptr &ptr, std::vector &out, DetectionType type) { const MWMechanics::MagicEffects& effects = ptr.getClass().getCreatureStats(ptr).getMagicEffects(); float dist=0; if (type == World::Detect_Creature) dist = effects.get(ESM::MagicEffect::DetectAnimal).getMagnitude(); else if (type == World::Detect_Key) dist = effects.get(ESM::MagicEffect::DetectKey).getMagnitude(); else if (type == World::Detect_Enchantment) dist = effects.get(ESM::MagicEffect::DetectEnchantment).getMagnitude(); if (!dist) return; dist = feetToGameUnits(dist); AddDetectedReferenceVisitor visitor (out, ptr, type, dist*dist); for (CellStore* cellStore : mWorldScene->getActiveCells()) { cellStore->forEach(visitor); } } float World::feetToGameUnits(float feet) { // Original engine rounds size upward static const int unitsPerFoot = ceil(Constants::UnitsPerFoot); return feet * unitsPerFoot; } float World::getActivationDistancePlusTelekinesis() { float telekinesisRangeBonus = mPlayer->getPlayer().getClass().getCreatureStats(mPlayer->getPlayer()).getMagicEffects() .get(ESM::MagicEffect::Telekinesis).getMagnitude(); telekinesisRangeBonus = feetToGameUnits(telekinesisRangeBonus); float activationDistance = getMaxActivationDistance() + telekinesisRangeBonus; return activationDistance; } MWWorld::Ptr World::getPlayerPtr() { return mPlayer->getPlayer(); } MWWorld::ConstPtr World::getPlayerConstPtr() const { return mPlayer->getConstPlayer(); } void World::updateDialogueGlobals() { MWWorld::Ptr player = getPlayerPtr(); int bounty = player.getClass().getNpcStats(player).getBounty(); int playerGold = player.getClass().getContainerStore(player).count(ContainerStore::sGoldId); static float fCrimeGoldDiscountMult = mStore.get().find("fCrimeGoldDiscountMult")->mValue.getFloat(); static float fCrimeGoldTurnInMult = mStore.get().find("fCrimeGoldTurnInMult")->mValue.getFloat(); int discount = static_cast(bounty * fCrimeGoldDiscountMult); int turnIn = static_cast(bounty * fCrimeGoldTurnInMult); if (bounty > 0) { discount = std::max(1, discount); turnIn = std::max(1, turnIn); } mGlobalVariables["pchascrimegold"].setInteger((bounty <= playerGold) ? 1 : 0); mGlobalVariables["pchasgolddiscount"].setInteger((discount <= playerGold) ? 1 : 0); mGlobalVariables["crimegolddiscount"].setInteger(discount); mGlobalVariables["crimegoldturnin"].setInteger(turnIn); mGlobalVariables["pchasturnin"].setInteger((turnIn <= playerGold) ? 1 : 0); } void World::confiscateStolenItems(const Ptr &ptr) { MWWorld::ConstPtr prisonMarker = getClosestMarker( ptr, "prisonmarker" ); if ( prisonMarker.isEmpty() ) { Log(Debug::Warning) << "Failed to confiscate items: no closest prison marker found."; return; } std::string prisonName = prisonMarker.getCellRef().getDestCell(); if ( prisonName.empty() ) { Log(Debug::Warning) << "Failed to confiscate items: prison marker not linked to prison interior"; return; } MWWorld::CellStore *prison = getInterior( prisonName ); if ( !prison ) { Log(Debug::Warning) << "Failed to confiscate items: failed to load cell " << prisonName; return; } MWWorld::Ptr closestChest = prison->search( "stolen_goods" ); if (!closestChest.isEmpty()) //Found a close chest { MWBase::Environment::get().getMechanicsManager()->confiscateStolenItems(ptr, closestChest); } else Log(Debug::Warning) << "Failed to confiscate items: no stolen_goods container found"; } void World::goToJail() { 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(); confiscateStolenItems(player); static int iDaysinPrisonMod = mStore.get().find("iDaysinPrisonMod")->mValue.getInteger(); mDaysInPrison = std::max(1, bounty / iDaysinPrisonMod); return; } else { mGoToJail = false; MWBase::Environment::get().getWindowManager()->removeGuiMode(MWGui::GM_Dialogue); MWBase::Environment::get().getWindowManager()->goToJail(mDaysInPrison); } } bool World::isPlayerInJail() const { return mPlayerInJail; } void World::setPlayerTraveling(bool traveling) { mPlayerTraveling = traveling; } bool World::isPlayerTraveling() const { return mPlayerTraveling; } float World::getTerrainHeightAt(const osg::Vec3f& worldPos) const { return mRendering->getTerrainHeightAt(worldPos); } osg::Vec3f World::getHalfExtents(const ConstPtr& object, bool rendering) const { if (!object.getClass().isActor()) return mRendering->getHalfExtents(object); // Handle actors separately because of bodyparts if (rendering) return mPhysics->getRenderingHalfExtents(object); else return mPhysics->getHalfExtents(object); } std::string World::exportSceneGraph(const Ptr &ptr) { std::string file = mUserDataPath + "/openmw.osgt"; if (!ptr.isEmpty()) { mRendering->pagingBlacklistObject(mStore.find(ptr.getCellRef().getRefId()), ptr); mWorldScene->removeFromPagedRefs(ptr); } mRendering->exportSceneGraph(ptr, file, "Ascii"); return file; } void World::spawnRandomCreature(const std::string &creatureList) { const ESM::CreatureLevList* list = mStore.get().find(creatureList); static int iNumberCreatures = mStore.get().find("iNumberCreatures")->mValue.getInteger(); int numCreatures = 1 + Misc::Rng::rollDice(iNumberCreatures); // [1, iNumberCreatures] for (int i=0; igetObjectList(); objectList->reset(); objectList->packetOrigin = mwmp::CLIENT_GAMEPLAY; objectList->addObjectSpawn(ptr); objectList->sendObjectSpawn(); deleteObject(ptr); /* End of tes3mp change (major) */ } } void World::spawnBloodEffect(const Ptr &ptr, const osg::Vec3f &worldPosition) { if (ptr == getPlayerPtr() && Settings::Manager::getBool("hit fader", "GUI")) return; std::string texture = Fallback::Map::getString("Blood_Texture_" + std::to_string(ptr.getClass().getBloodTexture(ptr))); if (texture.empty()) texture = Fallback::Map::getString("Blood_Texture_0"); std::string model = "meshes\\" + Fallback::Map::getString("Blood_Model_" + std::to_string(Misc::Rng::rollDice(3))); // [0, 2] mRendering->spawnEffect(model, texture, worldPosition, 1.0f, false); } void World::spawnEffect(const std::string &model, const std::string &textureOverride, const osg::Vec3f &worldPos, float scale, bool isMagicVFX) { mRendering->spawnEffect(model, textureOverride, worldPos, scale, isMagicVFX); } void World::explodeSpell(const osg::Vec3f& origin, const ESM::EffectList& effects, const Ptr& caster, const Ptr& ignore, ESM::RangeType rangeType, const std::string& id, const std::string& sourceName, const bool fromProjectile) { std::map > toApply; for (const ESM::ENAMstruct& effectInfo : effects.mList) { const ESM::MagicEffect* effect = mStore.get().find(effectInfo.mEffectID); if (effectInfo.mRange != rangeType || (effectInfo.mArea <= 0 && !ignore.isEmpty() && ignore.getClass().isActor())) continue; // Not right range type, or not area effect and hit an actor if (fromProjectile && effectInfo.mArea <= 0) continue; // Don't play explosion for projectiles with 0-area effects if (!fromProjectile && effectInfo.mRange == ESM::RT_Touch && !ignore.isEmpty() && !ignore.getClass().isActor() && !ignore.getClass().hasToolTip(ignore)) continue; // Don't play explosion for touch spells on non-activatable objects except when spell is from the projectile enchantment // Spawn the explosion orb effect const ESM::Static* areaStatic; if (!effect->mArea.empty()) areaStatic = mStore.get().find (effect->mArea); else areaStatic = mStore.get().find ("VFX_DefaultArea"); std::string texture = effect->mParticle; if (effectInfo.mArea <= 0) { if (effectInfo.mRange == ESM::RT_Target) mRendering->spawnEffect("meshes\\" + areaStatic->mModel, texture, origin, 1.0f); continue; } else mRendering->spawnEffect("meshes\\" + areaStatic->mModel, texture, origin, static_cast(effectInfo.mArea * 2)); // Play explosion sound (make sure to use NoTrack, since we will delete the projectile now) static const std::string schools[] = { "alteration", "conjuration", "destruction", "illusion", "mysticism", "restoration" }; { MWBase::SoundManager *sndMgr = MWBase::Environment::get().getSoundManager(); if(!effect->mAreaSound.empty()) sndMgr->playSound3D(origin, effect->mAreaSound, 1.0f, 1.0f); else sndMgr->playSound3D(origin, schools[effect->mData.mSchool]+" area", 1.0f, 1.0f); } // Get the actors in range of the effect std::vector objects; MWBase::Environment::get().getMechanicsManager()->getObjectsInRange( origin, feetToGameUnits(static_cast(effectInfo.mArea)), objects); for (const Ptr& affected : objects) { // Ignore actors without collisions here, otherwise it will be possible to hit actors outside processing range. if (affected.getClass().isActor() && !isActorCollisionEnabled(affected)) continue; toApply[affected].push_back(effectInfo); } } // Now apply the appropriate effects to each actor in range for (auto& applyPair : toApply) { MWWorld::Ptr source = caster; // Vanilla-compatible behaviour of never applying the spell to the caster // (could be changed by mods later) if (applyPair.first == caster) continue; if (applyPair.first == ignore) continue; if (source.isEmpty()) source = applyPair.first; MWMechanics::CastSpell cast(source, applyPair.first); cast.mHitPosition = origin; cast.mId = id; cast.mSourceName = sourceName; cast.mStack = false; ESM::EffectList effectsToApply; effectsToApply.mList = applyPair.second; cast.inflict(applyPair.first, caster, effectsToApply, rangeType, false, true); } } void World::activate(const Ptr &object, const Ptr &actor) { breakInvisibility(actor); if (object.getRefData().activate()) { std::shared_ptr action = (object.getClass().activate(object, actor)); action->execute (actor); } } struct ResetActorsVisitor { bool operator() (Ptr ptr) { if (ptr.getClass().isActor() && ptr.getCellRef().hasContentFile()) { if (ptr.getCell()->movedHere(ptr)) return true; const ESM::Position& origPos = ptr.getCellRef().getPosition(); MWBase::Environment::get().getWorld()->moveObject(ptr, origPos.pos[0], origPos.pos[1], origPos.pos[2]); MWBase::Environment::get().getWorld()->rotateObject(ptr, origPos.rot[0], origPos.rot[1], origPos.rot[2]); ptr.getClass().adjustPosition(ptr, true); } return true; } }; void World::resetActors() { for (CellStore* cellstore : mWorldScene->getActiveCells()) { ResetActorsVisitor visitor; cellstore->forEach(visitor); } } bool World::isWalkingOnWater(const ConstPtr &actor) const { const MWPhysics::Actor* physicActor = mPhysics->getActor(actor); if (physicActor && physicActor->isWalkingOnWater()) return true; return false; } osg::Vec3f World::aimToTarget(const ConstPtr &actor, const ConstPtr &target, bool isRangedCombat) { osg::Vec3f weaponPos = actor.getRefData().getPosition().asVec3(); float heightRatio = isRangedCombat ? 2.f * Constants::TorsoHeight : 1.f; weaponPos.z() += mPhysics->getHalfExtents(actor).z() * heightRatio; osg::Vec3f targetPos = mPhysics->getCollisionObjectPosition(target); return (targetPos - weaponPos); } float World::getHitDistance(const ConstPtr &actor, const ConstPtr &target) { osg::Vec3f weaponPos = actor.getRefData().getPosition().asVec3(); osg::Vec3f halfExtents = mPhysics->getHalfExtents(actor); weaponPos.z() += halfExtents.z(); return mPhysics->getHitDistance(weaponPos, target) - halfExtents.y(); } void preload(MWWorld::Scene* scene, const ESMStore& store, const std::string& obj) { if (obj.empty()) return; try { MWWorld::ManualRef ref(store, obj); std::string model = ref.getPtr().getClass().getModel(ref.getPtr()); if (!model.empty()) scene->preload(model, ref.getPtr().getClass().useAnim()); } catch(std::exception&) { } } void World::preloadEffects(const ESM::EffectList *effectList) { for (const ESM::ENAMstruct& effectInfo : effectList->mList) { const ESM::MagicEffect *effect = mStore.get().find(effectInfo.mEffectID); if (MWMechanics::isSummoningEffect(effectInfo.mEffectID)) { preload(mWorldScene.get(), mStore, "VFX_Summon_Start"); preload(mWorldScene.get(), mStore, MWMechanics::getSummonedCreature(effectInfo.mEffectID)); } preload(mWorldScene.get(), mStore, effect->mCasting); preload(mWorldScene.get(), mStore, effect->mHit); if (effectInfo.mArea > 0) preload(mWorldScene.get(), mStore, effect->mArea); if (effectInfo.mRange == ESM::RT_Target) preload(mWorldScene.get(), mStore, effect->mBolt); } } DetourNavigator::Navigator* World::getNavigator() const { return mNavigator.get(); } void World::updateActorPath(const MWWorld::ConstPtr& actor, const std::deque& path, const osg::Vec3f& halfExtents, const osg::Vec3f& start, const osg::Vec3f& end) const { mRendering->updateActorPath(actor, path, halfExtents, start, end); } void World::removeActorPath(const MWWorld::ConstPtr& actor) const { mRendering->removeActorPath(actor); } void World::setNavMeshNumberToRender(const std::size_t value) { mRendering->setNavMeshNumber(value); } osg::Vec3f World::getPathfindingHalfExtents(const MWWorld::ConstPtr& actor) const { if (actor.isInCell() && actor.getCell()->isExterior()) return mDefaultHalfExtents; // Using default half extents for better performance else return getHalfExtents(actor); } bool World::hasCollisionWithDoor(const MWWorld::ConstPtr& door, const osg::Vec3f& position, const osg::Vec3f& destination) const { const auto object = mPhysics->getObject(door); if (!object) return false; btVector3 aabbMin; btVector3 aabbMax; object->getShapeInstance()->getCollisionShape()->getAabb(btTransform::getIdentity(), aabbMin, aabbMax); const auto toLocal = object->getTransform().inverse(); const auto localFrom = toLocal(Misc::Convert::toBullet(position)); const auto localTo = toLocal(Misc::Convert::toBullet(destination)); btScalar hitDistance = 1; btVector3 hitNormal; return btRayAabb(localFrom, localTo, aabbMin, aabbMax, hitDistance, hitNormal); } bool World::isAreaOccupiedByOtherActor(const osg::Vec3f& position, const float radius, const MWWorld::ConstPtr& ignore) const { return mPhysics->isAreaOccupiedByOtherActor(position, radius, ignore); } void World::reportStats(unsigned int frameNumber, osg::Stats& stats) const { mNavigator->reportStats(frameNumber, stats); mPhysics->reportStats(frameNumber, stats); } void World::updateSkyDate() { ESM::EpochTimeStamp currentDate = mCurrentDate->getEpochTimeStamp(); mRendering->skySetDate(currentDate.mDay, currentDate.mMonth); } std::vector World::getAll(const std::string& id) { return mCells.getAll(id); } } ================================================ FILE: apps/openmw/mwworld/worldimp.hpp ================================================ #ifndef GAME_MWWORLD_WORLDIMP_H #define GAME_MWWORLD_WORLDIMP_H #include #include #include "../mwbase/world.hpp" #include "ptr.hpp" #include "scene.hpp" #include "esmstore.hpp" #include "cells.hpp" #include "localscripts.hpp" #include "timestamp.hpp" #include "globals.hpp" #include "contentloader.hpp" namespace osg { class Group; class Stats; } namespace osgViewer { class Viewer; } namespace Resource { class ResourceSystem; } namespace SceneUtil { class WorkQueue; } namespace ESM { struct Position; } namespace Files { class Collections; } namespace MWRender { class SkyManager; class Animation; class Camera; } namespace ToUTF8 { class Utf8Encoder; } namespace MWPhysics { class Object; } namespace MWWorld { class DateTimeManager; class WeatherManager; class Player; class ProjectileManager; /// \brief The game world and its visual representation class World final: public MWBase::World { private: Resource::ResourceSystem* mResourceSystem; std::vector mEsm; MWWorld::ESMStore mStore; LocalScripts mLocalScripts; MWWorld::Globals mGlobalVariables; Cells mCells; std::string mCurrentWorldSpace; std::unique_ptr mPlayer; std::unique_ptr mPhysics; std::unique_ptr mNavigator; std::unique_ptr mRendering; std::unique_ptr mWorldScene; std::unique_ptr mWeatherManager; std::unique_ptr mCurrentDate; std::shared_ptr mProjectileManager; bool mSky; bool mGodMode; bool mScriptsEnabled; bool mDiscardMovements; std::vector mContentFiles; std::string mUserDataPath; osg::Vec3f mDefaultHalfExtents; bool mShouldUpdateNavigator; int mActivationDistanceOverride; std::string mStartCell; float mSwimHeightScale; float mDistanceToFacedObject; bool mTeleportEnabled; bool mLevitationEnabled; bool mGoToJail; int mDaysInPrison; bool mPlayerTraveling; bool mPlayerInJail; float mSpellPreloadTimer; std::map mDoorStates; ///< only holds doors that are currently moving. 1 = opening, 2 = closing // not implemented World (const World&); World& operator= (const World&); void updateWeather(float duration, bool paused = false); void rotateObjectImp (const Ptr& ptr, const osg::Vec3f& rot, MWBase::RotationFlags flags); Ptr copyObjectToCell(const ConstPtr &ptr, CellStore* cell, ESM::Position pos, int count, bool adjustPos); void updateSoundListener(); void updatePlayer(); void preloadSpells(); MWWorld::Ptr getFacedObject(float maxDistance, bool ignorePlayer=true); /* Start of tes3mp change (major) This has been turned into a public method so it can be used in multiplayer's different approach to placing items */ public: void PCDropped(const Ptr& item); /* End of tes3mp change (major) */ private: bool rotateDoor(const Ptr door, DoorState state, float duration); void processDoors(float duration); ///< Run physics simulation and modify \a world accordingly. void doPhysics(float duration, osg::Timer_t frameStart, unsigned int frameNumber, osg::Stats& stats); ///< Run physics simulation and modify \a world accordingly. void updateNavigator(); void updateNavigatorObject(const MWPhysics::Object& object); void ensureNeededRecords(); void fillGlobalVariables(); 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& content, const std::vector& groundcover, ContentLoader& contentLoader); float feetToGameUnits(float feet); float getActivationDistancePlusTelekinesis(); MWWorld::ConstPtr getClosestMarker( const MWWorld::Ptr &ptr, const std::string &id ); MWWorld::ConstPtr getClosestMarkerFromExteriorPosition( const osg::Vec3f& worldPos, const std::string &id ); public: // FIXME void addContainerScripts(const Ptr& reference, CellStore* cell) override; void removeContainerScripts(const Ptr& reference) override; World ( osgViewer::Viewer* viewer, osg::ref_ptr rootNode, Resource::ResourceSystem* resourceSystem, SceneUtil::WorkQueue* workQueue, const Files::Collections& fileCollections, const std::vector& contentFiles, const std::vector& groundcoverFiles, ToUTF8::Utf8Encoder* encoder, int activationDistanceOverride, const std::string& startCell, const std::string& startupScript, const std::string& resourcePath, const std::string& userDataPath); virtual ~World(); void startNewGame (bool bypass) override; ///< \param bypass Bypass regular game start. void clear() override; int countSavedGameRecords() const override; int countSavedGameCells() const override; void write (ESM::ESMWriter& writer, Loading::Listener& progress) const override; void readRecord (ESM::ESMReader& reader, uint32_t type, const std::map& contentFileMap) override; CellStore *getExterior (int x, int y) override; CellStore *getInterior (const std::string& name) override; CellStore *getCell (const ESM::CellId& id) override; void testExteriorCells() override; void testInteriorCells() override; //switch to POV before showing player's death animation void useDeathCamera() override; void setWaterHeight(const float height) override; void rotateWorldObject (const MWWorld::Ptr& ptr, osg::Quat rotate) override; bool toggleWater() override; bool toggleWorld() override; bool toggleBorders() override; void adjustSky() override; Player& getPlayer() override; MWWorld::Ptr getPlayerPtr() override; MWWorld::ConstPtr getPlayerConstPtr() const override; const MWWorld::ESMStore& getStore() const override; /* Start of tes3mp addition Make it possible to get the World's ESMStore as a non-const */ MWWorld::ESMStore& getModifiableStore() override; /* End of tes3mp addition */ std::vector& getEsmReader() override; LocalScripts& getLocalScripts() override; bool hasCellChanged() const override; ///< Has the set of active cells changed, since the last frame? bool isCellExterior() const override; bool isCellQuasiExterior() const override; osg::Vec2f getNorthVector (const CellStore* cell) override; ///< get north vector for given interior cell void getDoorMarkers (MWWorld::CellStore* cell, std::vector& out) override; ///< get a list of teleport door markers for a given cell, to be displayed on the local map /* Start of tes3mp addition Make it possible to check whether global variables exist and to create new ones */ bool hasGlobal(const std::string& name); void createGlobal(const std::string& name, ESM::VarType varType); /* End of tes3mp addition */ void setGlobalInt (const std::string& name, int value) override; ///< Set value independently from real type. void setGlobalFloat (const std::string& name, float value) override; ///< Set value independently from real type. int getGlobalInt (const std::string& name) const override; ///< Get value independently from real type. float getGlobalFloat (const std::string& name) const override; ///< Get value independently from real type. char getGlobalVariableType (const std::string& name) const override; ///< Return ' ', if there is no global variable with this name. std::string getCellName (const MWWorld::CellStore *cell = nullptr) const override; ///< Return name of the cell. /// /// \note If cell==0, the cell the player is currently in will be used instead to /// generate a name. std::string getCellName(const ESM::Cell* cell) const override; void removeRefScript (MWWorld::RefData *ref) override; //< Remove the script attached to ref from mLocalScripts Ptr getPtr (const std::string& name, bool activeOnly) override; ///< Return a pointer to a liveCellRef with the given name. /// \param activeOnly do non search inactive cells. Ptr searchPtr (const std::string& name, bool activeOnly, bool searchInContainers = false) override; ///< Return a pointer to a liveCellRef with the given name. /// \param activeOnly do not search inactive cells. Ptr searchPtrViaActorId (int actorId) override; ///< Search is limited to the active cells. Ptr searchPtrViaRefNum (const std::string& id, const ESM::RefNum& refNum) override; /* Start of tes3mp addition Make it possible to find a Ptr in any active cell based on its refNum and mpNum */ Ptr searchPtrViaUniqueIndex(int refNum, int mpNum) override; /* End of tes3mp addition */ /* Start of tes3mp addition Make it possible to update all Ptrs in active cells that have a certain refId */ void updatePtrsWithRefId(std::string refId) override; /* End of tes3mp addition */ MWWorld::Ptr findContainer (const MWWorld::ConstPtr& ptr) override; ///< Return a pointer to a liveCellRef which contains \a ptr. /// \note Search is limited to the active cells. void adjustPosition (const Ptr& ptr, bool force) override; ///< Adjust position after load to be on ground. Must be called after model load. /// @param force do this even if the ptr is flying void fixPosition () override; ///< Attempt to fix position so that the player is not stuck inside the geometry. void enable (const Ptr& ptr) override; void disable (const Ptr& ptr) override; void advanceTime (double hours, bool incremental = false) override; ///< Advance in-game time. std::string getMonthName (int month = -1) const override; ///< Return name of month (-1: current month) TimeStamp getTimeStamp() const override; ///< Return current in-game time and number of day since new game start. ESM::EpochTimeStamp getEpochTimeStamp() const override; ///< Return current in-game date and time. bool toggleSky() override; ///< \return Resulting mode void changeWeather (const std::string& region, const unsigned int id) override; /* Start of tes3mp addition Make it possible to set a specific weather state for a region from elsewhere in the code */ void setRegionWeather(const std::string& region, const unsigned int currentWeather, const unsigned int nextWeather, const unsigned int queuedWeather, const float transitionFactor, bool force) override; /* End of tes3mp addition */ /* Start of tes3mp addition Make it possible to check whether the local WeatherManager has the ability to create weather changes */ bool getWeatherCreationState() override; /* End of tes3mp addition */ /* Start of tes3mp addition Make it possible to enable and disable the local WeatherManager's ability to create weather changes */ void setWeatherCreationState(bool state) override; /* End of tes3mp addition */ /* Start of tes3mp addition Make it possible to send the current weather in a WorldWeather packet when requested from elsewhere in the code */ void sendWeather() override; /* End of tes3mp addition */ int getCurrentWeather() const override; unsigned int getNightDayMode() const override; int getMasserPhase() const override; int getSecundaPhase() const override; void setMoonColour (bool red) override; void modRegion(const std::string ®ionid, const std::vector &chances) override; float getTimeScaleFactor() const override; void changeToInteriorCell (const std::string& cellName, const ESM::Position& position, bool adjustPlayerPos, bool changeEvent = true) override; ///< Move to interior cell. ///< @param changeEvent If false, do not trigger cell change flag or detect worldspace changes void changeToExteriorCell (const ESM::Position& position, bool adjustPlayerPos, bool changeEvent = true) override; ///< Move to exterior cell. ///< @param changeEvent If false, do not trigger cell change flag or detect worldspace changes void changeToCell (const ESM::CellId& cellId, const ESM::Position& position, bool adjustPlayerPos, bool changeEvent=true) override; ///< @param changeEvent If false, do not trigger cell change flag or detect worldspace changes const ESM::Cell *getExterior (const std::string& cellName) const override; ///< Return a cell matching the given name or a 0-pointer, if there is no such cell. void markCellAsUnchanged() override; MWWorld::Ptr getFacedObject() override; ///< Return pointer to the object the player is looking at, if it is within activation range float getDistanceToFacedObject() override; /// Returns a pointer to the object the provided object would hit (if within the /// specified distance), and the point where the hit occurs. This will attempt to /// use the "Head" node as a basis. std::pair getHitContact(const MWWorld::ConstPtr &ptr, float distance, std::vector &targets) override; /// @note No-op for items in containers. Use ContainerStore::removeItem instead. void deleteObject (const Ptr& ptr) override; void undeleteObject (const Ptr& ptr) override; MWWorld::Ptr moveObject (const Ptr& ptr, float x, float y, float z, bool movePhysics=true, bool moveToActive=false) override; ///< @return an updated Ptr in case the Ptr's cell changes MWWorld::Ptr moveObject (const Ptr& ptr, CellStore* newCell, float x, float y, float z, bool movePhysics=true) override; ///< @return an updated Ptr MWWorld::Ptr moveObjectBy(const Ptr& ptr, osg::Vec3f vec, bool moveToActive, bool ignoreCollisions) override; ///< @return an updated Ptr void scaleObject (const Ptr& ptr, float scale) override; /// World rotates object, uses radians /// @note Rotations via this method use a different rotation order than the initial rotations in the CS. This /// could be considered a bug, but is needed for MW compatibility. /// \param adjust indicates rotation should be set or adjusted void rotateObject (const Ptr& ptr, float x, float y, float z, MWBase::RotationFlags flags = MWBase::RotationFlag_inverseOrder) override; MWWorld::Ptr placeObject(const MWWorld::ConstPtr& ptr, MWWorld::CellStore* cell, ESM::Position pos) override; ///< Place an object. Makes a copy of the Ptr. MWWorld::Ptr safePlaceObject (const MWWorld::ConstPtr& ptr, const MWWorld::ConstPtr& referenceObject, MWWorld::CellStore* referenceCell, int direction, float distance) override; ///< Place an object in a safe place next to \a referenceObject. \a direction and \a distance specify the wanted placement /// relative to \a referenceObject (but the object may be placed somewhere else if the wanted location is obstructed). float getMaxActivationDistance() override; void indexToPosition (int cellX, int cellY, float &x, float &y, bool centre = false) const override; ///< Convert cell numbers to position. void positionToIndex (float x, float y, int &cellX, int &cellY) const override; ///< Convert position to cell numbers void queueMovement(const Ptr &ptr, const osg::Vec3f &velocity) override; ///< Queues movement for \a ptr (in local space), to be applied in the next call to /// doPhysics. /* Start of tes3mp addition Make it possible to set the inertial force of a Ptr directly */ void setInertialForce(const Ptr& ptr, const osg::Vec3f &force); /* End of tes3mp addition */ /* Start of tes3mp addition Make it possible to set whether a Ptr is on the ground or not, needed for proper synchronization in multiplayer */ void setOnGround(const Ptr& ptr, bool onGround); /* End of tes3mp addition */ /* Start of tes3mp addition Make it possible to set the physics framerate from elsewhere */ void setPhysicsFramerate(float physFramerate); /* End of tes3mp addition */ void updateAnimatedCollisionShape(const Ptr &ptr) override; const MWPhysics::RayCastingInterface* getRayCasting() const override; bool castRay (float x1, float y1, float z1, float x2, float y2, float z2, int mask) override; ///< cast a Ray and return true if there is an object in the ray path. bool castRay (float x1, float y1, float z1, float x2, float y2, float z2) override; bool castRay(const osg::Vec3f& from, const osg::Vec3f& to, int mask, const MWWorld::ConstPtr& ignore) override; void setActorCollisionMode(const Ptr& ptr, bool internal, bool external) override; bool isActorCollisionEnabled(const Ptr& ptr) override; bool toggleCollisionMode() override; ///< Toggle collision mode for player. If disabled player object should ignore /// collisions and gravity. ///< \return Resulting mode bool toggleRenderMode (MWRender::RenderMode mode) override; ///< Toggle a render mode. ///< \return Resulting mode const ESM::Potion *createRecord (const ESM::Potion& record) override; ///< Create a new record (of type potion) in the ESM store. /// \return pointer to created record const ESM::Spell *createRecord (const ESM::Spell& record) override; ///< Create a new record (of type spell) in the ESM store. /// \return pointer to created record const ESM::Class *createRecord (const ESM::Class& record) override; ///< Create a new record (of type class) in the ESM store. /// \return pointer to created record const ESM::Cell *createRecord (const ESM::Cell& record) override; ///< Create a new record (of type cell) in the ESM store. /// \return pointer to created record const ESM::NPC *createRecord(const ESM::NPC &record) override; ///< Create a new record (of type npc) in the ESM store. /// \return pointer to created record const ESM::Creature *createRecord(const ESM::Creature &record) override; ///< Create a new record (of type creature) in the ESM store. /// \return pointer to created record const ESM::Armor *createRecord (const ESM::Armor& record) override; ///< Create a new record (of type armor) in the ESM store. /// \return pointer to created record const ESM::Weapon *createRecord (const ESM::Weapon& record) override; ///< Create a new record (of type weapon) in the ESM store. /// \return pointer to created record const ESM::Clothing *createRecord (const ESM::Clothing& record) override; ///< Create a new record (of type clothing) in the ESM store. /// \return pointer to created record const ESM::Enchantment *createRecord (const ESM::Enchantment& record) override; ///< Create a new record (of type enchantment) in the ESM store. /// \return pointer to created record const ESM::Book *createRecord (const ESM::Book& record) override; ///< Create a new record (of type book) in the ESM store. /// \return pointer to created record const ESM::CreatureLevList *createOverrideRecord (const ESM::CreatureLevList& record) override; ///< Write this record to the ESM store, allowing it to override a pre-existing record with the same ID. /// \return pointer to created record const ESM::ItemLevList *createOverrideRecord (const ESM::ItemLevList& record) override; ///< Write this record to the ESM store, allowing it to override a pre-existing record with the same ID. /// \return pointer to created record const ESM::Creature *createOverrideRecord (const ESM::Creature& record) override; ///< Write this record to the ESM store, allowing it to override a pre-existing record with the same ID. /// \return pointer to created record const ESM::NPC *createOverrideRecord (const ESM::NPC& record) override; ///< Write this record to the ESM store, allowing it to override a pre-existing record with the same ID. /// \return pointer to created record const ESM::Container *createOverrideRecord (const ESM::Container& record) override; ///< Write this record to the ESM store, allowing it to override a pre-existing record with the same ID. /// \return pointer to created record void update (float duration, bool paused) override; void updatePhysics (float duration, bool paused, osg::Timer_t frameStart, unsigned int frameNumber, osg::Stats& stats) override; void updateWindowManager () override; MWWorld::Ptr placeObject (const MWWorld::ConstPtr& object, float cursorX, float cursorY, int amount) override; ///< copy and place an object into the gameworld at the specified cursor position /// @param object /// @param cursor X (relative 0-1) /// @param cursor Y (relative 0-1) /// @param number of objects to place MWWorld::Ptr dropObjectOnGround (const MWWorld::Ptr& actor, const MWWorld::ConstPtr& object, int amount) override; ///< copy and place an object into the gameworld at the given actor's position /// @param actor giving the dropped object position /// @param object /// @param number of objects to place bool canPlaceObject(float cursorX, float cursorY) override; ///< @return true if it is possible to place on object at specified cursor location void processChangedSettings(const Settings::CategorySettingVector& settings) override; bool isFlying(const MWWorld::Ptr &ptr) const override; bool isSlowFalling(const MWWorld::Ptr &ptr) const override; ///Is the head of the creature underwater? bool isSubmerged(const MWWorld::ConstPtr &object) const override; bool isSwimming(const MWWorld::ConstPtr &object) const override; bool isUnderwater(const MWWorld::CellStore* cell, const osg::Vec3f &pos) const override; bool isUnderwater(const MWWorld::ConstPtr &object, const float heightRatio) const override; bool isWading(const MWWorld::ConstPtr &object) const override; bool isWaterWalkingCastableOnTarget(const MWWorld::ConstPtr &target) const override; bool isOnGround(const MWWorld::Ptr &ptr) const override; osg::Matrixf getActorHeadTransform(const MWWorld::ConstPtr& actor) const override; void togglePOV(bool force = false) override; bool isFirstPerson() const override; bool isPreviewModeEnabled() const override; void togglePreviewMode(bool enable) override; bool toggleVanityMode(bool enable) override; void allowVanityMode(bool allow) override; bool vanityRotateCamera(float * rot) override; void adjustCameraDistance(float dist) override; void applyDeferredPreviewRotationToPlayer(float dt) override; void disableDeferredPreviewRotation() override; void saveLoaded() override; void setupPlayer() override; void renderPlayer() override; /// open or close a non-teleport door (depending on current state) void activateDoor(const MWWorld::Ptr& door) override; /// update movement state of a non-teleport door as specified /// @param state see MWClass::setDoorState /// @note throws an exception when invoked on a teleport door void activateDoor(const MWWorld::Ptr& door, MWWorld::DoorState state) override; /* Start of tes3mp addition Useful self-contained method for saving door states */ void saveDoorState(const MWWorld::Ptr& door, MWWorld::DoorState state) override; /* End of tes3mp addition */ /* Start of tes3mp addition Make it possible to check whether a cell is active */ bool isCellActive(const ESM::Cell& cell) override; /* End of tes3mp addition */ /* Start of tes3mp addition Make it possible to unload a cell from elsewhere */ void unloadCell(const ESM::Cell& cell) override; /* End of tes3mp addition */ /* Start of tes3mp addition Make it possible to unload all active cells from elsewhere */ void unloadActiveCells() override; /* End of tes3mp addition */ /* Start of tes3mp addition Clear the CellStore for a specific Cell from elsewhere */ virtual void clearCellStore(const ESM::Cell& cell) override; /* End of tes3mp addition */ void getActorsStandingOn (const MWWorld::ConstPtr& object, std::vector &actors) override; ///< get a list of actors standing on \a object bool getPlayerStandingOn (const MWWorld::ConstPtr& object) override; ///< @return true if the player is standing on \a object bool getActorStandingOn (const MWWorld::ConstPtr& object) override; ///< @return true if any actor is standing on \a object bool getPlayerCollidingWith(const MWWorld::ConstPtr& object) override; ///< @return true if the player is colliding with \a object bool getActorCollidingWith (const MWWorld::ConstPtr& object) override; ///< @return true if any actor is colliding with \a object void hurtStandingActors (const MWWorld::ConstPtr& object, float dmgPerSecond) override; ///< Apply a health difference to any actors standing on \a object. /// To hurt actors, healthPerSecond should be a positive value. For a negative value, actors will be healed. void hurtCollidingActors (const MWWorld::ConstPtr& object, float dmgPerSecond) override; ///< Apply a health difference to any actors colliding with \a object. /// To hurt actors, healthPerSecond should be a positive value. For a negative value, actors will be healed. float getWindSpeed() override; void getContainersOwnedBy (const MWWorld::ConstPtr& npc, std::vector& out) override; ///< get all containers in active cells owned by this Npc void getItemsOwnedBy (const MWWorld::ConstPtr& npc, std::vector& out) override; ///< get all items in active cells owned by this Npc bool getLOS(const MWWorld::ConstPtr& actor,const MWWorld::ConstPtr& targetActor) override; ///< get Line of Sight (morrowind stupid implementation) float getDistToNearestRayHit(const osg::Vec3f& from, const osg::Vec3f& dir, float maxDist, bool includeWater = false) override; void enableActorCollision(const MWWorld::Ptr& actor, bool enable) override; RestPermitted canRest() const override; ///< check if the player is allowed to rest void rest(double hours) override; void rechargeItems(double duration, bool activeOnly) override; /// \todo Probably shouldn't be here MWRender::Animation* getAnimation(const MWWorld::Ptr &ptr) override; const MWRender::Animation* getAnimation(const MWWorld::ConstPtr &ptr) const override; void reattachPlayerCamera() override; /// \todo this does not belong here void screenshot (osg::Image* image, int w, int h) override; bool screenshot360 (osg::Image* image) override; /// Find center of exterior cell above land surface /// \return false if exterior with given name not exists, true otherwise bool findExteriorPosition(const std::string &name, ESM::Position &pos) override; /// Find position in interior cell near door entrance /// \return false if interior with given name not exists, true otherwise bool findInteriorPosition(const std::string &name, ESM::Position &pos) override; /// Enables or disables use of teleport spell effects (recall, intervention, etc). void enableTeleporting(bool enable) override; /// Returns true if teleport spell effects are allowed. bool isTeleportingEnabled() const override; /// Enables or disables use of levitation spell effect. void enableLevitation(bool enable) override; /// Returns true if levitation spell effect is allowed. bool isLevitationEnabled() const override; bool getGodModeState() const override; bool toggleGodMode() override; bool toggleScripts() override; bool getScriptsEnabled() const override; /** * @brief startSpellCast attempt to start casting a spell. Might fail immediately if conditions are not met. * @param actor * @return true if the spell can be casted (i.e. the animation should start) */ bool startSpellCast (const MWWorld::Ptr& actor) override; /** * @brief Cast the actual spell, should be called mid-animation * @param actor */ void castSpell (const MWWorld::Ptr& actor, bool manualSpell=false) override; void launchMagicBolt (const std::string& spellId, const MWWorld::Ptr& caster, const osg::Vec3f& fallbackDirection) override; void launchProjectile (MWWorld::Ptr& actor, MWWorld::Ptr& projectile, const osg::Vec3f& worldPos, const osg::Quat& orient, MWWorld::Ptr& bow, float speed, float attackStrength) override; void updateProjectilesCasters() override; void applyLoopingParticles(const MWWorld::Ptr& ptr) override; const std::vector& getContentFiles() const override; void breakInvisibility (const MWWorld::Ptr& actor) override; // Allow NPCs to use torches? bool useTorches() const override; bool findInteriorPositionInWorldSpace(const MWWorld::CellStore* cell, osg::Vec3f& result) override; /// Teleports \a ptr to the closest reference of \a id (e.g. DivineMarker, PrisonMarker, TempleMarker) /// @note id must be lower case void teleportToClosestMarker (const MWWorld::Ptr& ptr, const std::string& id) override; /// List all references (filtered by \a type) detected by \a ptr. The range /// is determined by the current magnitude of the "Detect X" magic effect belonging to \a type. /// @note This also works for references in containers. void listDetectedReferences (const MWWorld::Ptr& ptr, std::vector& out, DetectionType type) override; /// Update the value of some globals according to the world state, which may be used by dialogue entries. /// This should be called when initiating a dialogue. void updateDialogueGlobals() override; /// Moves all stolen items from \a ptr to the closest evidence chest. void confiscateStolenItems(const MWWorld::Ptr& ptr) override; void goToJail () override; /// Spawn a random creature from a levelled list next to the player void spawnRandomCreature(const std::string& creatureList) override; /// Spawn a blood effect for \a ptr at \a worldPosition void spawnBloodEffect (const MWWorld::Ptr& ptr, const osg::Vec3f& worldPosition) override; void spawnEffect (const std::string& model, const std::string& textureOverride, const osg::Vec3f& worldPos, float scale = 1.f, bool isMagicVFX = true) override; void explodeSpell(const osg::Vec3f& origin, const ESM::EffectList& effects, const MWWorld::Ptr& caster, const MWWorld::Ptr& ignore, ESM::RangeType rangeType, const std::string& id, const std::string& sourceName, const bool fromProjectile=false) override; void activate (const MWWorld::Ptr& object, const MWWorld::Ptr& actor) override; /// @see MWWorld::WeatherManager::isInStorm bool isInStorm() const override; /// @see MWWorld::WeatherManager::getStormDirection osg::Vec3f getStormDirection() const override; /// Resets all actors in the current active cells to their original location within that cell. void resetActors() override; bool isWalkingOnWater (const MWWorld::ConstPtr& actor) const override; /// Return a vector aiming the actor's weapon towards a target. /// @note The length of the vector is the distance between actor and target. osg::Vec3f aimToTarget(const MWWorld::ConstPtr& actor, const MWWorld::ConstPtr& target, bool isRangedCombat) override; /// Return the distance between actor's weapon and target's collision box. float getHitDistance(const MWWorld::ConstPtr& actor, const MWWorld::ConstPtr& target) override; bool isPlayerInJail() const override; void setPlayerTraveling(bool traveling) override; bool isPlayerTraveling() const override; /// Return terrain height at \a worldPos position. float getTerrainHeightAt(const osg::Vec3f& worldPos) const override; /// Return physical or rendering half extents of the given actor. osg::Vec3f getHalfExtents(const MWWorld::ConstPtr& actor, bool rendering=false) const override; /// Export scene graph to a file and return the filename. /// \param ptr object to export scene graph for (if empty, export entire scene graph) std::string exportSceneGraph(const MWWorld::Ptr& ptr) override; /// Preload VFX associated with this effect list void preloadEffects(const ESM::EffectList* effectList) override; DetourNavigator::Navigator* getNavigator() const override; void updateActorPath(const MWWorld::ConstPtr& actor, const std::deque& path, const osg::Vec3f& halfExtents, const osg::Vec3f& start, const osg::Vec3f& end) const override; void removeActorPath(const MWWorld::ConstPtr& actor) const override; void setNavMeshNumberToRender(const std::size_t value) override; /// Return physical half extents of the given actor to be used in pathfinding osg::Vec3f getPathfindingHalfExtents(const MWWorld::ConstPtr& actor) const override; bool hasCollisionWithDoor(const MWWorld::ConstPtr& door, const osg::Vec3f& position, const osg::Vec3f& destination) const override; bool isAreaOccupiedByOtherActor(const osg::Vec3f& position, const float radius, const MWWorld::ConstPtr& ignore) const override; void reportStats(unsigned int frameNumber, osg::Stats& stats) const override; std::vector getAll(const std::string& id) override; }; } #endif ================================================ FILE: apps/openmw-mp/CMakeLists.txt ================================================ project(tes3mp-server) option(ENABLE_BREAKPAD "Enable Google Breakpad for Crash reporting" OFF) if(ENABLE_BREAKPAD) set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DENABLE_BREAKPAD") if (UNIX) set(Breakpad_Headers "${CMAKE_SOURCE_DIR}/extern/breakpad/src/client/linux") set(Breakpad_Library "${CMAKE_SOURCE_DIR}/extern/breakpad/src/client/linux/libbreakpad_client.a") elseif(WIN32) set(Breakpad_Headers "${CMAKE_SOURCE_DIR}/extern/breakpad/src/client/windows") set(Breakpad_Library "-lbreakpad_client") endif (UNIX) include_directories(${CMAKE_SOURCE_DIR}/extern/breakpad/src ${Breakpad_Headers}) endif(ENABLE_BREAKPAD) option(BUILD_WITH_LUA "Enable Lua language" ON) if(BUILD_WITH_LUA) find_package(LuaJit REQUIRED) MESSAGE(STATUS "Found LuaJit_LIBRARIES: ${LuaJit_LIBRARIES}") MESSAGE(STATUS "Found LuaJit_INCLUDE_DIRS: ${LuaJit_INCLUDE_DIRS}") set(LuaScript_Sources Script/LangLua/LangLua.cpp Script/LangLua/LuaFunc.cpp) set(LuaScript_Headers ${LUA_INCLUDE_DIR} ${CMAKE_SOURCE_DIR}/extern/LuaBridge ${CMAKE_SOURCE_DIR}/extern/LuaBridge/detail Script/LangLua/LangLua.hpp) set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DENABLE_LUA") include_directories(SYSTEM ${LuaJit_INCLUDE_DIRS} ${CMAKE_SOURCE_DIR}/extern/LuaBridge) endif(BUILD_WITH_LUA) include_directories(${CMAKE_SOURCE_DIR}/extern/PicoSHA2) set(NativeScript_Sources Script/LangNative/LangNative.cpp ) set(NativeScript_Headers Script/LangNative/LangNative.hpp ) # local files set(SERVER main.cpp Player.cpp Networking.cpp MasterClient.cpp Cell.cpp CellController.cpp Utils.cpp Script/Script.cpp Script/ScriptFunction.cpp Script/ScriptFunctions.cpp Script/Functions/Actors.cpp Script/Functions/Objects.cpp Script/Functions/Miscellaneous.cpp Script/Functions/Worldstate.cpp Script/Functions/Books.cpp Script/Functions/Cells.cpp Script/Functions/CharClass.cpp Script/Functions/Chat.cpp Script/Functions/Dialogue.cpp Script/Functions/Factions.cpp Script/Functions/GUI.cpp Script/Functions/Items.cpp Script/Functions/Mechanics.cpp Script/Functions/Positions.cpp Script/Functions/Quests.cpp Script/Functions/RecordsDynamic.cpp Script/Functions/Server.cpp Script/Functions/Settings.cpp Script/Functions/Shapeshift.cpp Script/Functions/Spells.cpp Script/Functions/Stats.cpp Script/Functions/Timer.cpp Script/API/TimerAPI.cpp Script/API/PublicFnAPI.cpp ${LuaScript_Sources} ${NativeScript_Sources} ) set(SERVER_HEADER Script/Types.hpp Script/Script.hpp Script/SystemInterface.hpp Script/ScriptFunction.hpp Script/Platform.hpp Script/Language.hpp Script/ScriptFunctions.hpp Script/API/TimerAPI.hpp Script/API/PublicFnAPI.hpp ${LuaScript_Headers} ${NativeScript_Headers} ) source_group(tes3mp-server FILES ${SERVER} ${SERVER_HEADER}) set(PROCESSORS_ACTOR processors/actor/ProcessorActorAI.hpp processors/actor/ProcessorActorAnimFlags.hpp processors/actor/ProcessorActorAnimPlay.hpp processors/actor/ProcessorActorAttack.hpp processors/actor/ProcessorActorCast.hpp processors/actor/ProcessorActorCellChange.hpp processors/actor/ProcessorActorDeath.hpp processors/actor/ProcessorActorEquipment.hpp processors/actor/ProcessorActorList.hpp processors/actor/ProcessorActorPosition.hpp processors/actor/ProcessorActorSpeech.hpp processors/actor/ProcessorActorSpellsActive.hpp processors/actor/ProcessorActorStatsDynamic.hpp processors/actor/ProcessorActorTest.hpp ) source_group(tes3mp-server\\processors\\actor FILES ${PROCESSORS_ACTOR}) set(PROCESSORS_PLAYER processors/player/ProcessorChatMsg.hpp processors/player/ProcessorGUIMessageBox.hpp processors/player/ProcessorPlayerAnimFlags.hpp processors/player/ProcessorPlayerAnimPlay.hpp processors/player/ProcessorPlayerAttack.hpp processors/player/ProcessorPlayerAttribute.hpp processors/player/ProcessorPlayerBook.hpp processors/player/ProcessorPlayerBounty.hpp processors/player/ProcessorPlayerCast.hpp processors/player/ProcessorPlayerCellChange.hpp processors/player/ProcessorPlayerCellState.hpp processors/player/ProcessorPlayerCharClass.hpp processors/player/ProcessorPlayerCharGen.hpp processors/player/ProcessorPlayerCooldowns.hpp processors/player/ProcessorPlayerDeath.hpp processors/player/ProcessorPlayerDisposition.hpp processors/player/ProcessorPlayerEquipment.hpp processors/player/ProcessorPlayerFaction.hpp processors/player/ProcessorPlayerInput.hpp processors/player/ProcessorPlayerInventory.hpp processors/player/ProcessorPlayerItemUse.hpp processors/player/ProcessorPlayerJournal.hpp processors/player/ProcessorPlayerPlaceholder.hpp processors/player/ProcessorPlayerLevel.hpp processors/player/ProcessorPlayerMiscellaneous.hpp processors/player/ProcessorPlayerPosition.hpp processors/player/ProcessorPlayerQuickKeys.hpp processors/player/ProcessorPlayerRest.hpp processors/player/ProcessorPlayerResurrect.hpp processors/player/ProcessorPlayerShapeshift.hpp processors/player/ProcessorPlayerSkill.hpp processors/player/ProcessorPlayerSpeech.hpp processors/player/ProcessorPlayerSpellbook.hpp processors/player/ProcessorPlayerSpellsActive.hpp processors/player/ProcessorPlayerStatsDynamic.hpp processors/player/ProcessorPlayerTopic.hpp ) source_group(tes3mp-server\\processors\\player FILES ${PROCESSORS_PLAYER}) set(PROCESSORS_OBJECT processors/object/ProcessorConsoleCommand.hpp processors/object/ProcessorContainer.hpp processors/object/ProcessorDoorState.hpp processors/object/ProcessorMusicPlay.hpp processors/object/ProcessorObjectActivate.hpp processors/object/ProcessorObjectAnimPlay.hpp processors/object/ProcessorObjectDelete.hpp processors/object/ProcessorObjectDialogueChoice.hpp processors/object/ProcessorObjectHit.hpp processors/object/ProcessorObjectLock.hpp processors/object/ProcessorObjectMiscellaneous.hpp processors/object/ProcessorObjectMove.hpp processors/object/ProcessorObjectPlace.hpp processors/object/ProcessorObjectRestock.hpp processors/object/ProcessorObjectRotate.hpp processors/object/ProcessorObjectScale.hpp processors/object/ProcessorObjectSound.hpp processors/object/ProcessorObjectSpawn.hpp processors/object/ProcessorObjectState.hpp processors/object/ProcessorObjectTrap.hpp processors/object/ProcessorClientScriptLocal.hpp processors/object/ProcessorScriptMemberShort.hpp processors/object/ProcessorVideoPlay.hpp ) source_group(tes3mp-server\\processors\\object FILES ${PROCESSORS_OBJECT}) set(PROCESSORS_WORLDSTATE processors/worldstate/ProcessorClientScriptGlobal.hpp processors/worldstate/ProcessorRecordDynamic.hpp processors/worldstate/ProcessorWorldKillCount.hpp processors/worldstate/ProcessorWorldMap.hpp processors/worldstate/ProcessorWorldWeather.hpp ) source_group(tes3mp-server\\processors\\worldstate FILES ${PROCESSORS_WORLDSTATE}) set(PROCESSORS processors/ProcessorInitializer.cpp processors/PlayerProcessor.cpp processors/ActorProcessor.cpp processors/ObjectProcessor.cpp processors/WorldstateProcessor.cpp ) source_group(tes3mp-server\\processors FILES ${PROCESSORS}) include_directories("./") include_directories(${CMAKE_SOURCE_DIR}/extern) # Main executable add_executable(tes3mp-server ${SERVER} ${SERVER_HEADER} ${PROCESSORS_ACTOR} ${PROCESSORS_PLAYER} ${PROCESSORS_OBJECT} ${PROCESSORS_WORLDSTATE} ${PROCESSORS} ${APPLE_BUNDLE_RESOURCES} ) target_compile_options(tes3mp-server PRIVATE $<$:/permissive->) if (OPENMW_MP_BUILD) target_compile_options(tes3mp-server PRIVATE $<$:/MP>) endif() set_target_properties(tes3mp-server PROPERTIES CXX_STANDARD 17 CXX_STANDARD_REQUIRED YES CXX_EXTENSIONS YES ) if (UNIX) target_compile_options(tes3mp-server PRIVATE -Wno-ignored-qualifiers) endif() target_link_libraries(tes3mp-server #${Boost_SYSTEM_LIBRARY} #${Boost_THREAD_LIBRARY} #${Boost_FILESYSTEM_LIBRARY} #${Boost_PROGRAM_OPTIONS_LIBRARY} ${RakNet_LIBRARY} components ${LuaJit_LIBRARIES} ${Breakpad_Library} ) if (UNIX) target_link_libraries(tes3mp-server dl) # Fix for not visible pthreads functions for linker with glibc 2.15 if(NOT APPLE) target_link_libraries(tes3mp-server ${CMAKE_THREAD_LIBS_INIT}) endif(NOT APPLE) endif(UNIX) if (BUILD_WITH_CODE_COVERAGE) add_definitions (--coverage) target_link_libraries(tes3mp-server gcov) endif() if (MSVC) # Debug version needs increased number of sections beyond 2^16 if (CMAKE_CL_64) set (CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} /bigobj") endif (CMAKE_CL_64) add_definitions("-D_USE_MATH_DEFINES") endif (MSVC) ================================================ FILE: apps/openmw-mp/Cell.cpp ================================================ #include "Cell.hpp" #include #include #include "Player.hpp" #include "Script/Script.hpp" Cell::Cell(ESM::Cell cell) : cell(cell) { cellActorList.count = 0; } Cell::Iterator Cell::begin() const { return players.begin(); } Cell::Iterator Cell::end() const { return players.end(); } void Cell::addPlayer(Player *player) { // Ensure the player hasn't already been added auto it = find(begin(), end(), player); if (it != end()) { LOG_APPEND(TimedLog::LOG_INFO, "- Attempt to add %s to Cell %s again was ignored", player->npc.mName.c_str(), getShortDescription().c_str()); return; } auto it2 = find(player->cells.begin(), player->cells.end(), this); if (it2 == player->cells.end()) { LOG_APPEND(TimedLog::LOG_INFO, "- Adding %s to Player %s", getShortDescription().c_str(), player->npc.mName.c_str()); player->cells.push_back(this); } LOG_APPEND(TimedLog::LOG_INFO, "- Adding %s to Cell %s", player->npc.mName.c_str(), getShortDescription().c_str()); Script::Call(player->getId(), getShortDescription().c_str()); players.push_back(player); } void Cell::removePlayer(Player *player, bool cleanPlayer) { for (Iterator it = begin(); it != end(); it++) { if (*it == player) { if (cleanPlayer) { auto it2 = find(player->cells.begin(), player->cells.end(), this); if (it2 != player->cells.end()) { LOG_APPEND(TimedLog::LOG_INFO, "- Removing %s from Player %s", getShortDescription().c_str(), player->npc.mName.c_str()); player->cells.erase(it2); } } LOG_APPEND(TimedLog::LOG_INFO, "- Removing %s from Cell %s", player->npc.mName.c_str(), getShortDescription().c_str()); Script::Call(player->getId(), getShortDescription().c_str()); players.erase(it); return; } } } void Cell::readActorList(unsigned char packetID, const mwmp::BaseActorList *newActorList) { for (unsigned int i = 0; i < newActorList->count; i++) { mwmp::BaseActor newActor = newActorList->baseActors.at(i); mwmp::BaseActor *cellActor; if (containsActor(newActor.refNum, newActor.mpNum)) { cellActor = getActor(newActor.refNum, newActor.mpNum); switch (packetID) { case ID_ACTOR_POSITION: cellActor->hasPositionData = true; cellActor->position = newActor.position; break; case ID_ACTOR_STATS_DYNAMIC: cellActor->hasStatsDynamicData = true; cellActor->creatureStats.mDynamic[0] = newActor.creatureStats.mDynamic[0]; cellActor->creatureStats.mDynamic[1] = newActor.creatureStats.mDynamic[1]; cellActor->creatureStats.mDynamic[2] = newActor.creatureStats.mDynamic[2]; break; } } else cellActorList.baseActors.push_back(newActor); } cellActorList.count = cellActorList.baseActors.size(); } bool Cell::containsActor(int refNum, int mpNum) { for (unsigned int i = 0; i < cellActorList.baseActors.size(); i++) { mwmp::BaseActor actor = cellActorList.baseActors.at(i); if (actor.refNum == refNum && actor.mpNum == mpNum) return true; } return false; } mwmp::BaseActor *Cell::getActor(int refNum, int mpNum) { for (unsigned int i = 0; i < cellActorList.baseActors.size(); i++) { mwmp::BaseActor *actor = &cellActorList.baseActors.at(i); if (actor->refNum == refNum && actor->mpNum == mpNum) return actor; } return 0; } void Cell::removeActors(const mwmp::BaseActorList *newActorList) { for (std::vector::iterator it = cellActorList.baseActors.begin(); it != cellActorList.baseActors.end();) { int refNum = (*it).refNum; int mpNum = (*it).mpNum; bool foundActor = false; for (unsigned int i = 0; i < newActorList->count; i++) { mwmp::BaseActor newActor = newActorList->baseActors.at(i); if (newActor.refNum == refNum && newActor.mpNum == mpNum) { it = cellActorList.baseActors.erase(it); foundActor = true; break; } } if (!foundActor) it++; } cellActorList.count = cellActorList.baseActors.size(); } RakNet::RakNetGUID *Cell::getAuthority() { return &authorityGuid; } void Cell::setAuthority(const RakNet::RakNetGUID& guid) { authorityGuid = guid; } mwmp::BaseActorList *Cell::getActorList() { return &cellActorList; } Cell::TPlayers Cell::getPlayers() const { return players; } void Cell::sendToLoaded(mwmp::ActorPacket *actorPacket, mwmp::BaseActorList *baseActorList) const { if (players.empty()) return; std::list plList; for (auto pl : players) { if (pl != nullptr && !pl->npc.mName.empty()) plList.push_back(pl); } plList.sort(); plList.unique(); for (auto pl : plList) { if (pl->guid == baseActorList->guid) continue; actorPacket->setActorList(baseActorList); // Send the packet to this eligible guid actorPacket->Send(pl->guid); } } void Cell::sendToLoaded(mwmp::ObjectPacket *objectPacket, mwmp::BaseObjectList *baseObjectList) const { if (players.empty()) return; std::list plList; for (auto pl : players) { if (pl != nullptr && !pl->npc.mName.empty()) plList.push_back(pl); } plList.sort(); plList.unique(); for (auto pl : plList) { if (pl->guid == baseObjectList->guid) continue; objectPacket->setObjectList(baseObjectList); // Send the packet to this eligible guid objectPacket->Send(pl->guid); } } std::string Cell::getShortDescription() const { return cell.getShortDescription(); } ================================================ FILE: apps/openmw-mp/Cell.hpp ================================================ #ifndef OPENMW_SERVERCELL_HPP #define OPENMW_SERVERCELL_HPP #include #include #include #include #include #include #include class Player; class Cell; class Cell { friend class CellController; public: Cell(ESM::Cell cell); typedef std::deque TPlayers; typedef TPlayers::const_iterator Iterator; Iterator begin() const; Iterator end() const; void addPlayer(Player *player); void removePlayer(Player *player, bool cleanPlayer = true); void readActorList(unsigned char packetID, const mwmp::BaseActorList *newActorList); bool containsActor(int refNum, int mpNum); mwmp::BaseActor *getActor(int refNum, int mpNum); void removeActors(const mwmp::BaseActorList *newActorList); RakNet::RakNetGUID *getAuthority(); void setAuthority(const RakNet::RakNetGUID& guid); mwmp::BaseActorList *getActorList(); TPlayers getPlayers() const; void sendToLoaded(mwmp::ActorPacket *actorPacket, mwmp::BaseActorList *baseActorList) const; void sendToLoaded(mwmp::ObjectPacket *objectPacket, mwmp::BaseObjectList *baseObjectList) const; std::string getShortDescription() const; private: TPlayers players; ESM::Cell cell; RakNet::RakNetGUID authorityGuid; mwmp::BaseActorList cellActorList; }; #endif //OPENMW_SERVERCELL_HPP ================================================ FILE: apps/openmw-mp/CellController.cpp ================================================ #include "CellController.hpp" #include #include "Cell.hpp" #include "Player.hpp" #include "Script/Script.hpp" CellController::CellController() { } CellController::~CellController() { for (auto cell : cells) delete cell; } CellController *CellController::sThis = nullptr; void CellController::create() { assert(!sThis); sThis = new CellController; } void CellController::destroy() { assert(sThis); delete sThis; sThis = nullptr; } CellController *CellController::get() { assert(sThis); return sThis; } Cell *CellController::getCell(ESM::Cell *esmCell) { if (esmCell->isExterior()) return getCellByXY(esmCell->mData.mX, esmCell->mData.mY); else return getCellByName(esmCell->mName); } Cell *CellController::getCellByXY(int x, int y) { auto it = find_if(cells.begin(), cells.end(), [x, y](const Cell *c) { return c->cell.mData.mX == x && c->cell.mData.mY == y; }); if (it == cells.end()) { LOG_APPEND(TimedLog::LOG_INFO, "- Attempt to get Cell at %i, %i failed!", x, y); return nullptr; } return *it; } Cell *CellController::getCellByName(std::string cellName) { auto it = find_if(cells.begin(), cells.end(), [cellName](const Cell *c) { return c->cell.mName == cellName; }); if (it == cells.end()) { LOG_APPEND(TimedLog::LOG_INFO, "- Attempt to get Cell at %s failed!", cellName.c_str()); return nullptr; } return *it; } Cell *CellController::addCell(ESM::Cell cellData) { LOG_APPEND(TimedLog::LOG_INFO, "- Loaded cells: %d", cells.size()); auto it = find_if(cells.begin(), cells.end(), [cellData](const Cell *c) { // Currently we cannot compare because plugin lists can be loaded in different order //return c->cell.sRecordId == cellData.sRecordId; if (c->cell.isExterior() && cellData.isExterior()) { if (c->cell.mData.mX == cellData.mData.mX && c->cell.mData.mY == cellData.mData.mY) return true; } else if (c->cell.mName == cellData.mName) return true; return false; }); Cell *cell; if (it == cells.end()) { LOG_APPEND(TimedLog::LOG_INFO, "- Adding %s to CellController", cellData.getShortDescription().c_str()); cell = new Cell(cellData); cells.push_back(cell); } else { LOG_APPEND(TimedLog::LOG_INFO, "- Found %s in CellController", cellData.getShortDescription().c_str()); cell = *it; } return cell; } void CellController::removeCell(Cell *cell) { if (cell == nullptr) return; for (auto it = cells.begin(); it != cells.end();) { if (*it != nullptr && *it == cell) { Script::Call(cell->getShortDescription().c_str()); LOG_APPEND(TimedLog::LOG_INFO, "- Removing %s from CellController", cell->getShortDescription().c_str()); delete *it; it = cells.erase(it); } else ++it; } } void CellController::deletePlayer(Player *player) { LOG_APPEND(TimedLog::LOG_INFO, "- Iterating through Cells from Player %s", player->npc.mName.c_str()); std::vector toDelete; auto it = player->getCells()->begin(); const auto endIter = player->getCells()->end(); for (; it != endIter; ++it) { Cell *c = *it; c->removePlayer(player, false); if (c->players.empty()) toDelete.push_back(c); } for (auto &&cell : toDelete) { LOG_APPEND(TimedLog::LOG_INFO, "- Cell %s has no players left", cell->getShortDescription().c_str()); removeCell(cell); } } void CellController::update(Player *player) { std::vector toDelete; for (auto &&cell : player->cellStateChanges) { if (cell.type == mwmp::CellState::LOAD) { Cell *c = addCell(cell.cell); c->addPlayer(player); } else { Cell *c; if (!cell.cell.isExterior()) c = getCellByName(cell.cell.mName); else c = getCellByXY(cell.cell.getGridX(), cell.cell.getGridY()); if (c != nullptr) { c->removePlayer(player); if (c->players.empty()) toDelete.push_back(c); } } } for (auto &&cell : toDelete) { LOG_APPEND(TimedLog::LOG_INFO, "- Cell %s has no players left", cell->getShortDescription().c_str()); removeCell(cell); } } ================================================ FILE: apps/openmw-mp/CellController.hpp ================================================ #ifndef OPENMW_SERVERCELLCONTROLLER_HPP #define OPENMW_SERVERCELLCONTROLLER_HPP #include #include #include #include #include #include class Player; class Cell; class CellController { private: CellController(); ~CellController(); CellController(CellController&); // not used public: static void create(); static void destroy(); static CellController *get(); public: typedef std::deque TContainer; typedef TContainer::iterator TIter; Cell * addCell(ESM::Cell cell); void removeCell(Cell *); void deletePlayer(Player *player); Cell *getCell(ESM::Cell *esmCell); Cell *getCellByXY(int x, int y); Cell *getCellByName(std::string cellName); void update(Player *player); private: static CellController *sThis; TContainer cells; }; #endif //OPENMW_SERVERCELLCONTROLLER_HPP ================================================ FILE: apps/openmw-mp/MasterClient.cpp ================================================ #include #include #include #include #include #include #include "MasterClient.hpp" #include #include #include #include "Networking.hpp" using namespace mwmp; using namespace RakNet; bool MasterClient::sRun = false; MasterClient::MasterClient(RakNet::RakPeerInterface *peer, std::string queryAddr, unsigned short queryPort) : masterServer(queryAddr.c_str(), queryPort), peer(peer), pma(peer) { timeout = 15000; // every 15 seconds pma.SetSendStream(&writeStream); pma.SetServer(&queryData); updated = true; } void MasterClient::SetPlayers(unsigned pl) { mutexData.lock(); if (queryData.GetPlayers() != pl) { queryData.SetPlayers(pl); updated = true; } mutexData.unlock(); } void MasterClient::SetMaxPlayers(unsigned pl) { mutexData.lock(); if (queryData.GetMaxPlayers() != pl) { queryData.SetMaxPlayers(pl); updated = true; } mutexData.unlock(); } void MasterClient::SetHostname(std::string hostname) { mutexData.lock(); std::string substr = hostname.substr(0, 200); if (queryData.GetName() != substr) { queryData.SetName(substr.c_str()); updated = true; } mutexData.unlock(); } void MasterClient::SetModname(std::string modname) { mutexData.lock(); std::string substr = modname.substr(0, 200); if (queryData.GetGameMode() != substr) { queryData.SetGameMode(substr.c_str()); updated = true; } mutexData.unlock(); } void MasterClient::SetRuleString(std::string key, std::string value) { mutexData.lock(); if (queryData.rules.find(key) == queryData.rules.end() || queryData.rules[key].type != 's' || queryData.rules[key].str != value) { ServerRule rule; rule.str = value; rule.type = ServerRule::Type::string; queryData.rules.insert({key, rule}); updated = true; } mutexData.unlock(); } void MasterClient::SetRuleValue(std::string key, double value) { mutexData.lock(); if (queryData.rules.find(key) == queryData.rules.end() || queryData.rules[key].type != 'v' || queryData.rules[key].val != value) { ServerRule rule; rule.val = value; rule.type = ServerRule::Type::number; queryData.rules.insert({key, rule}); updated = true; } mutexData.unlock(); } void MasterClient::PushPlugin(Plugin plugin) { mutexData.lock(); queryData.plugins.push_back(plugin); updated = true; mutexData.unlock(); } bool MasterClient::Process(RakNet::Packet *packet) { if (!sRun || packet->systemAddress != masterServer) return false; BitStream rs(packet->data, packet->length, false); unsigned char pid; rs.Read(pid); switch (pid) { case ID_SND_RECEIPT_ACKED: case ID_CONNECTION_ATTEMPT_FAILED: case ID_CONNECTION_REQUEST_ACCEPTED: case ID_DISCONNECTION_NOTIFICATION: break; case ID_MASTER_QUERY: break; case ID_MASTER_ANNOUNCE: pma.SetReadStream(&rs); pma.Read(); if (pma.GetFunc() == PacketMasterAnnounce::FUNCTION_KEEP) LOG_MESSAGE_SIMPLE(TimedLog::LOG_VERBOSE, "Server data successfully updated on master server"); else if (pma.GetFunc() == PacketMasterAnnounce::FUNCTION_DELETE) { if (timeout != 0) { LOG_MESSAGE_SIMPLE(TimedLog::LOG_WARN, "Update rate is too low," " and the master server has deleted information about the server. Trying low rate..."); if ((timeout - step_rate) >= step_rate) SetUpdateRate(timeout - step_rate); updated = true; } } break; default: LOG_MESSAGE_SIMPLE(TimedLog::LOG_ERROR, "Received wrong packet from master server with id: %d", packet->data[0]); return false; } return true; } void MasterClient::Send(mwmp::PacketMasterAnnounce::Func func) { peer->Connect(masterServer.ToString(false), masterServer.GetPort(), TES3MP_MASTERSERVER_PASSW, strlen(TES3MP_MASTERSERVER_PASSW), 0, 0, 5, 500); bool waitForConnect = true; while (waitForConnect) { ConnectionState state = peer->GetConnectionState(masterServer); switch (state) { case IS_CONNECTED: waitForConnect = false; break; case IS_NOT_CONNECTED: case IS_DISCONNECTED: case IS_SILENTLY_DISCONNECTING: case IS_DISCONNECTING: { LOG_MESSAGE_SIMPLE(TimedLog::LOG_WARN, "Cannot connect to master server: %s", masterServer.ToString()); return; } case IS_PENDING: case IS_CONNECTING: break; } RakSleep(500); } pma.SetFunc(func); pma.Send(masterServer); updated = false; } void MasterClient::Thread() { assert(!sRun); sRun = true; queryData.SetPassword((int) Networking::get().isPassworded()); queryData.SetVersion(TES3MP_VERSION); auto *players = Players::getPlayers(); while (sRun) { SetPlayers((int) players->size()); auto pIt = players->begin(); if (queryData.players.size() != players->size()) { queryData.players.clear(); updated = true; } else { for (int i = 0; pIt != players->end(); i++, pIt++) { if (queryData.players[i] != pIt->second->npc.mName) { queryData.players.clear(); updated = true; break; } } } if (updated) { updated = false; if (pIt != players->end()) { for (auto player : *players) { if (!player.second->npc.mName.empty()) queryData.players.push_back(player.second->npc.mName); } } Send(PacketMasterAnnounce::FUNCTION_ANNOUNCE); } else Send(PacketMasterAnnounce::FUNCTION_KEEP); RakSleep(timeout); } } void MasterClient::Start() { thrQuery = std::thread(&MasterClient::Thread, this); } void MasterClient::Stop() { if (!sRun) return; sRun = false; if (thrQuery.joinable()) thrQuery.join(); } void MasterClient::SetUpdateRate(unsigned int rate) { if (timeout < min_rate) timeout = min_rate; else if (timeout > max_rate) timeout = max_rate; timeout = rate; } ================================================ FILE: apps/openmw-mp/MasterClient.hpp ================================================ #ifndef OPENMW_MASTERCLIENT_HPP #define OPENMW_MASTERCLIENT_HPP #include #include #include #include #include #include class MasterClient { public: static const unsigned int step_rate = 1000; static const unsigned int min_rate = 1000; static const unsigned int max_rate = 60000; public: MasterClient(RakNet::RakPeerInterface *peer, std::string queryAddr, unsigned short queryPort); void SetPlayers(unsigned pl); void SetMaxPlayers(unsigned pl); void SetHostname(std::string hostname); void SetModname(std::string hostname); void SetRuleString(std::string key, std::string value); void SetRuleValue(std::string key, double value); void PushPlugin(Plugin plugin); bool Process(RakNet::Packet *packet); void Start(); void Stop(); void SetUpdateRate(unsigned int rate); private: void Send(mwmp::PacketMasterAnnounce::Func func); void Thread(); private: RakNet::SystemAddress masterServer; RakNet::RakPeerInterface *peer; QueryData queryData; unsigned int timeout; static bool sRun; std::mutex mutexData; std::thread thrQuery; mwmp::PacketMasterAnnounce pma; RakNet::BitStream writeStream; bool updated; }; #endif //OPENMW_MASTERCLIENT_HPP ================================================ FILE: apps/openmw-mp/Networking.cpp ================================================ #include "Player.hpp" #include "processors/ProcessorInitializer.hpp" #include #include #include #include #include #include #include #include #include